Просмотр исходного кода

feat(locales, views): enhance localization and UI consistency

- Added new translations for various terms in English and Chinese locale files, including "FFmpeg Version", "Task Number", "Camera Details", and more.
- Updated placeholders and labels in the live-stream and LSS views to utilize localized strings, improving user experience and consistency across the application.
- Changed the title of the LSS management route from "LSS 管理" to "LSS 列表" for better clarity.
- Adjusted button styles for better visual feedback in the UI.
yb 3 дней назад
Родитель
Сommit
7dace4ef1c

+ 23 - 1
src/locales/en.json

@@ -1,12 +1,14 @@
 {
   "Cloudflare Stream": "Cloudflare Stream",
   "Cloudflare Stream 配置": "Cloudflare Stream Configuration",
+  "FFmpeg 版本": "FFmpeg Version",
   "IP": "IP",
   "IP / 设备ID / 名称": "IP / Device ID / Name",
   "IP地址": "IP Address",
   "LSS": "LSS",
   "LSS ID": "LSS ID",
   "LSS 管理": "LSS Management",
+  "LSS 节点": "LSS Node",
   "LSS 节点详情": "LSS Node Details",
   "LSS详情": "LSS Details",
   "LiveStream 管理": "LiveStream Management",
@@ -54,7 +56,9 @@
   "仅切换": "Switch Only",
   "仅在前端直接调用 API 时需要(不推荐)": "Only needed when directly calling the API in the frontend (not recommended)",
   "仪表盘": "Dashboard",
+  "任务数": "Task Number",
   "位置": "Location",
+  "例如: 测试推流-001": "For example: Test Stream-001",
   "保存配置": "Save Configuration",
   "修改失败": "Update failed",
   "修改密码": "Change Password",
@@ -69,6 +73,7 @@
   "共": "Total",
   "关闭": "Close",
   "关闭时间": "Closed At",
+  "其他设备": "Other Devices",
   "创建时间": "Created At",
   "初始化失败": "Initialization failed",
   "初始化成功": "Initialization successful",
@@ -116,6 +121,7 @@
   "已选择": "Selected",
   "序号": "No.",
   "开启": "Start",
+  "开始推流": "Start Stream",
   "开始日期": "Start Date",
   "当前状态": "Current Status",
   "待机": "Standby",
@@ -139,10 +145,10 @@
   "推流任务已启动": "Task started",
   "推流控制": "Stream Control",
   "推流方式": "Method",
-  "开始推流": "Start Stream",
   "推荐通过后端代理调用,避免暴露 Token": "Recommended to call through the backend proxy to avoid exposing the Token",
   "描述": "Description",
   "提示": "Notice",
+  "摄像头": "Camera",
   "摄像头ID": "Camera ID",
   "摄像头列表": "Camera List",
   "摄像头在线率": "Camera Online Rate",
@@ -150,6 +156,7 @@
   "摄像头数": "Cameras",
   "摄像头管理": "Camera Management",
   "摄像头管理系统": "Camera Management",
+  "摄像头详情": "Camera Details",
   "摄像头连接失败": "Camera connection failed",
   "摄像头连接正常": "Camera connection successful",
   "摄像头配置": "Camera Configuration",
@@ -173,6 +180,7 @@
   "是": "Yes",
   "暂停": "Pause",
   "暂无关联设备": "No associated devices",
+  "暂无其他设备数据": "No other device data",
   "暂无推币机数据": "No coin machine data",
   "暂无日志": "No logs",
   "暂无视频流": "No video stream",
@@ -180,8 +188,10 @@
   "更新": "Update",
   "更新失败": "Update failed",
   "更新成功": "Updated successfully",
+  "更新时间": "Updated At",
   "有声": "Sound",
   "未配置摄像头": "No camera configured",
+  "机器 ID": "Machine ID",
   "机器ID": "Machine ID",
   "机器总数": "Total Machines",
   "机器管理": "Machine Management",
@@ -200,6 +210,7 @@
   "测试视频": "Test Video",
   "测试连接": "Test Connection",
   "添加": "Add",
+  "添加摄像头": "Add Camera",
   "添加时间": "Add Time",
   "清空": "Clear",
   "版本": "Version",
@@ -251,8 +262,11 @@
   "视频播放测试": "Video Playback Test",
   "记住我": "Remember me",
   "设备ID": "Device ID",
+  "设备ID / 名称": "Device ID / Name",
   "设备列表": "Devices",
+  "设备名称": "Device Name",
   "设备控制": "Device Control",
+  "设备运行参数 (JSON)": "Device Runtime Parameters (JSON)",
   "请先新增 Live Stream,才能进行后续操作。": "Please create a Live Stream first to continue.",
   "请先配置摄像头": "Please configure the camera first",
   "请再次输入新密码": "Please enter the new password again",
@@ -262,7 +276,11 @@
   "请输入厂家代码": "Please enter factory code",
   "请输入厂家名称": "Please enter factory name",
   "请输入原密码": "Please enter the old password",
+  "请输入参数配置 (JSON)": "Please enter the parameter configuration (JSON)",
+  "请输入参数配置(JSON 格式)": "Please enter the parameter configuration (JSON format)",
   "请输入名称": "Please enter name",
+  "请输入地址": "Please enter address",
+  "请输入型号": "Please enter model",
   "请输入密码": "Please enter password",
   "请输入摄像头ID": "Please enter Camera ID",
   "请输入新密码": "Please enter the new password",
@@ -271,7 +289,11 @@
   "请输入用户名": "Please enter username",
   "请输入视频地址并点击播放": "Please enter video URL and click play",
   "请输入设备ID": "Please enter Device ID",
+  "请输入设备名称": "Please enter device name",
+  "请输入运行参数(JSON 格式)": "Please enter the runtime parameters (JSON format)",
+  "请选择": "Please select",
   "请选择 LSS 节点": "Please select LSS node",
+  "请选择摄像头": "Please select camera",
   "请选择视频源并点击播放": "Please select video source and click play",
   "跳转失败": "Jump failed",
   "转换服务地址": "Proxy Service URL",

+ 23 - 1
src/locales/zh-cn.json

@@ -1,12 +1,14 @@
 {
   "Cloudflare Stream": "Cloudflare Stream",
   "Cloudflare Stream 配置": "Cloudflare Stream 配置",
+  "FFmpeg 版本": "FFmpeg 版本",
   "IP": "IP",
   "IP / 设备ID / 名称": "IP / 设备ID / 名称",
   "IP地址": "IP地址",
   "LSS": "LSS",
   "LSS ID": "LSS ID",
   "LSS 管理": "LSS 管理",
+  "LSS 节点": "LSS 节点",
   "LSS 节点详情": "LSS 节点详情",
   "LSS详情": "LSS详情",
   "LiveStream 管理": "LiveStream 管理",
@@ -54,7 +56,9 @@
   "仅切换": "仅切换",
   "仅在前端直接调用 API 时需要(不推荐)": "仅在前端直接调用 API 时需要(不推荐)",
   "仪表盘": "仪表盘",
+  "任务数": "任务数",
   "位置": "位置",
+  "例如: 测试推流-001": "例如: 测试推流-001",
   "保存配置": "保存配置",
   "修改失败": "修改失败",
   "修改密码": "修改密码",
@@ -69,6 +73,7 @@
   "共": "共",
   "关闭": "关闭",
   "关闭时间": "关闭时间",
+  "其他设备": "其他设备",
   "创建时间": "创建时间",
   "初始化失败": "初始化失败",
   "初始化成功": "初始化成功",
@@ -116,6 +121,7 @@
   "已选择": "已选择",
   "序号": "序号",
   "开启": "开启",
+  "开始推流": "开始推流",
   "开始日期": "开始日期",
   "当前状态": "当前状态",
   "待机": "待机",
@@ -139,10 +145,10 @@
   "推流任务已启动": "推流任务已启动",
   "推流控制": "推流控制",
   "推流方式": "推流方式",
-  "开始推流": "开始推流",
   "推荐通过后端代理调用,避免暴露 Token": "推荐通过后端代理调用,避免暴露 Token",
   "描述": "描述",
   "提示": "提示",
+  "摄像头": "摄像头",
   "摄像头ID": "摄像头ID",
   "摄像头列表": "摄像头列表",
   "摄像头在线率": "摄像头在线率",
@@ -150,6 +156,7 @@
   "摄像头数": "摄像头数",
   "摄像头管理": "摄像头管理",
   "摄像头管理系统": "摄像头管理系统",
+  "摄像头详情": "摄像头详情",
   "摄像头连接失败": "摄像头连接失败",
   "摄像头连接正常": "摄像头连接正常",
   "摄像头配置": "摄像头配置",
@@ -173,6 +180,7 @@
   "是": "是",
   "暂停": "暂停",
   "暂无关联设备": "暂无关联设备",
+  "暂无其他设备数据": "暂无其他设备数据",
   "暂无推币机数据": "暂无推币机数据",
   "暂无日志": "暂无日志",
   "暂无视频流": "暂无视频流",
@@ -180,8 +188,10 @@
   "更新": "更新",
   "更新失败": "更新失败",
   "更新成功": "更新成功",
+  "更新时间": "更新时间",
   "有声": "有声",
   "未配置摄像头": "未配置摄像头",
+  "机器 ID": "机器 ID",
   "机器ID": "机器ID",
   "机器总数": "机器总数",
   "机器管理": "机器管理",
@@ -200,6 +210,7 @@
   "测试视频": "测试视频",
   "测试连接": "测试连接",
   "添加": "添加",
+  "添加摄像头": "添加摄像头",
   "添加时间": "添加时间",
   "清空": "清空",
   "版本": "版本",
@@ -251,8 +262,11 @@
   "视频播放测试": "视频播放测试",
   "记住我": "记住我",
   "设备ID": "设备ID",
+  "设备ID / 名称": "设备ID / 名称",
   "设备列表": "设备列表",
+  "设备名称": "设备名称",
   "设备控制": "设备控制",
+  "设备运行参数 (JSON)": "设备运行参数 (JSON)",
   "请先新增 Live Stream,才能进行后续操作。": "请先新增 Live Stream,才能进行后续操作。",
   "请先配置摄像头": "请先配置摄像头",
   "请再次输入新密码": "请再次输入新密码",
@@ -262,7 +276,11 @@
   "请输入厂家代码": "请输入厂家代码",
   "请输入厂家名称": "请输入厂家名称",
   "请输入原密码": "请输入原密码",
+  "请输入参数配置 (JSON)": "请输入参数配置 (JSON)",
+  "请输入参数配置(JSON 格式)": "请输入参数配置(JSON 格式)",
   "请输入名称": "请输入名称",
+  "请输入地址": "请输入地址",
+  "请输入型号": "请输入型号",
   "请输入密码": "请输入密码",
   "请输入摄像头ID": "请输入摄像头ID",
   "请输入新密码": "请输入新密码",
@@ -271,7 +289,11 @@
   "请输入用户名": "请输入用户名",
   "请输入视频地址并点击播放": "请输入视频地址并点击播放",
   "请输入设备ID": "请输入设备ID",
+  "请输入设备名称": "请输入设备名称",
+  "请输入运行参数(JSON 格式)": "请输入运行参数(JSON 格式)",
+  "请选择": "请选择",
   "请选择 LSS 节点": "请选择 LSS 节点",
+  "请选择摄像头": "请选择摄像头",
   "请选择视频源并点击播放": "请选择视频源并点击播放",
   "跳转失败": "跳转失败",
   "转换服务地址": "转换服务地址",

+ 1 - 1
src/router/index.ts

@@ -48,7 +48,7 @@ const routes: RouteRecordRaw[] = [
         path: 'lss',
         name: 'LSS',
         component: () => import('@/views/lss/index.vue'),
-        meta: { title: 'LSS 管理', icon: 'Connection' }
+        meta: { title: 'LSS 列表', icon: 'Connection' }
       },
       {
         path: 'live-stream',

+ 44 - 20
src/views/live-stream/index.vue

@@ -4,10 +4,15 @@
     <div class="search-form">
       <el-form :model="searchForm" inline>
         <el-form-item>
-          <el-input v-model.trim="searchForm.streamSn" placeholder="stream sn" clearable @keyup.enter="handleSearch" />
+          <el-input
+            v-model.trim="searchForm.streamSn"
+            :placeholder="t('stream sn')"
+            clearable
+            @keyup.enter="handleSearch"
+          />
         </el-form-item>
         <el-form-item>
-          <el-input v-model.trim="searchForm.name" placeholder="name" clearable @keyup.enter="handleSearch" />
+          <el-input v-model.trim="searchForm.name" :placeholder="t('名称')" clearable @keyup.enter="handleSearch" />
         </el-form-item>
         <el-form-item>
           <el-select v-model="searchForm.lssId" placeholder="LSS" clearable filterable style="width: 180px">
@@ -15,11 +20,16 @@
           </el-select>
         </el-form-item>
         <el-form-item>
-          <el-input v-model.trim="searchForm.cameraId" placeholder="设备ID" clearable @keyup.enter="handleSearch" />
+          <el-input
+            v-model.trim="searchForm.cameraId"
+            :placeholder="t('设备ID')"
+            clearable
+            @keyup.enter="handleSearch"
+          />
         </el-form-item>
         <el-form-item>
           <el-button type="primary" :icon="Search" @click="handleSearch">{{ t('查询') }}</el-button>
-          <el-button :icon="RefreshRight" @click="handleReset">{{ t('重置') }}</el-button>
+          <el-button type="info" :icon="RefreshRight" @click="handleReset">{{ t('重置') }}</el-button>
           <el-button type="primary" :icon="Plus" @click="handleAdd">{{ t('新增') }}</el-button>
         </el-form-item>
       </el-form>
@@ -138,11 +148,17 @@
         <div v-show="activeDrawerTab === 'edit'" class="tab-content edit-content">
           <div class="drawer-body">
             <el-form ref="formRef" :model="form" :rules="rules" label-width="auto" class="stream-form">
-              <el-form-item label="名称:" prop="name">
-                <el-input v-model="form.name" placeholder="例如: 测试推流-001" style="width: 300px" />
+              <el-form-item :label="t('名称') + ':'" prop="name">
+                <el-input v-model="form.name" :placeholder="t('例如: 测试推流-001')" style="width: 300px" />
               </el-form-item>
-              <el-form-item label="LSS 节点:" prop="lssId">
-                <el-select v-model="form.lssId" placeholder="请选择 LSS 节点" clearable filterable style="width: 300px">
+              <el-form-item :label="t('LSS 节点') + ':'" prop="lssId">
+                <el-select
+                  v-model="form.lssId"
+                  :placeholder="t('请选择 LSS 节点')"
+                  clearable
+                  filterable
+                  style="width: 300px"
+                >
                   <el-option
                     v-for="lss in lssOptions"
                     :key="lss.lssId"
@@ -151,8 +167,14 @@
                   />
                 </el-select>
               </el-form-item>
-              <el-form-item label="摄像头:" prop="cameraId">
-                <el-select v-model="form.cameraId" placeholder="请选择摄像头" clearable filterable style="width: 300px">
+              <el-form-item :label="t('摄像头') + ':'" prop="cameraId">
+                <el-select
+                  v-model="form.cameraId"
+                  :placeholder="t('请选择摄像头')"
+                  clearable
+                  filterable
+                  style="width: 300px"
+                >
                   <el-option
                     v-for="camera in cameraOptions"
                     :key="camera.cameraId"
@@ -161,12 +183,12 @@
                   />
                 </el-select>
               </el-form-item>
-              <el-form-item label="推流方式:" prop="pushMethod">
-                <el-select disabled v-model="form.pushMethod" placeholder="请选择" style="width: 300px">
+              <el-form-item :label="t('推流方式') + ':'" prop="pushMethod">
+                <el-select disabled v-model="form.pushMethod" :placeholder="t('请选择')" style="width: 300px">
                   <el-option label="ffmpeg" value="ffmpeg" />
                 </el-select>
               </el-form-item>
-              <el-form-item label="命令模板:" prop="commandTemplate">
+              <el-form-item :label="t('命令模板') + ':'" prop="commandTemplate">
                 <div class="code-editor-wrapper">
                   <CodeEditor
                     v-model="form.commandTemplate"
@@ -412,8 +434,8 @@
       </div>
     </el-drawer>
 
-    <!-- 命令模板查看/编辑弹窗 -->
-    <el-dialog v-model="commandDialogVisible" :title="t('命令模板')" width="800px" destroy-on-close>
+    <!-- 命令模板查看/编辑抽屉 -->
+    <el-drawer v-model="commandDialogVisible" :title="t('命令模板')" direction="rtl" size="800px" destroy-on-close>
       <CodeEditor
         v-model="currentCommandTemplate"
         language="bash"
@@ -421,12 +443,14 @@
         placeholder="#!/bin/bash&#10;# FFmpeg 推流命令模板"
       />
       <template #footer>
-        <el-button @click="commandDialogVisible = false">{{ t('关闭') }}</el-button>
-        <el-button type="primary" :loading="commandUpdateLoading" @click="handleUpdateCommandTemplate">
-          {{ t('更新') }}
-        </el-button>
+        <div class="drawer-footer">
+          <el-button @click="commandDialogVisible = false">{{ t('取消') }}</el-button>
+          <el-button type="primary" :loading="commandUpdateLoading" @click="handleUpdateCommandTemplate">
+            {{ t('更新') }}
+          </el-button>
+        </div>
       </template>
-    </el-dialog>
+    </el-drawer>
   </div>
 </template>
 

+ 81 - 73
src/views/lss/index.vue

@@ -33,7 +33,9 @@
           <el-button type="primary" :icon="Search" data-id="btn-search" @click="handleSearch">
             {{ t('查询') }}
           </el-button>
-          <el-button :icon="RefreshRight" data-id="btn-reset" @click="handleReset">{{ t('重置') }}</el-button>
+          <el-button type="info" :icon="RefreshRight" data-id="btn-reset" @click="handleReset">
+            {{ t('重置') }}
+          </el-button>
         </el-form-item>
       </el-form>
     </div>
@@ -57,15 +59,7 @@
         <el-table-column :label="t('心跳')" width="220" align="center">
           <template #default="{ row }">
             <span :class="getHeartbeatClass(row.status)">
-              {{
-                row.status === 'active'
-                  ? t('活跃')
-                  : row.status === 'hold'
-                  ? t('待机')
-                  : row.status === 'dead'
-                  ? t('离线')
-                  : '-'
-              }}
+              {{ row.status || '-' }}
             </span>
             | {{ formatTime(row.lastHeartbeatAt) }}
           </template>
@@ -101,27 +95,27 @@
     <!-- LSS 详情抽屉 -->
     <el-drawer v-model="detailDrawerVisible" :title="t('LSS 节点详情')" direction="rtl" size="500px" destroy-on-close>
       <el-descriptions :column="1" border>
-        <el-descriptions-item label="LSS ID">{{ currentLss?.lssId }}</el-descriptions-item>
-        <el-descriptions-item label="名称">{{ currentLss?.lssName }}</el-descriptions-item>
-        <el-descriptions-item label="地址">{{ currentLss?.address }}</el-descriptions-item>
-        <el-descriptions-item label="机器 ID">{{ currentLss?.machineId || '-' }}</el-descriptions-item>
-        <el-descriptions-item label="状态">
+        <el-descriptions-item :label="t('LSS ID')">{{ currentLss?.lssId }}</el-descriptions-item>
+        <el-descriptions-item :label="t('名称')">{{ currentLss?.lssName }}</el-descriptions-item>
+        <el-descriptions-item :label="t('地址')">{{ currentLss?.address }}</el-descriptions-item>
+        <el-descriptions-item :label="t('机器 ID')">{{ currentLss?.machineId || '-' }}</el-descriptions-item>
+        <el-descriptions-item :label="t('状态')">
           <el-tag :type="getStatusTagType(currentLss?.status)" size="small">
             {{ formatStatus(currentLss?.status) }}
           </el-tag>
         </el-descriptions-item>
-        <el-descriptions-item label="任务数">
+        <el-descriptions-item :label="t('任务数')">
           {{ currentLss?.currentTasks }} / {{ currentLss?.maxTasks }}
         </el-descriptions-item>
-        <el-descriptions-item label="FFmpeg 版本">{{ currentLss?.ffmpegVersion || '-' }}</el-descriptions-item>
-        <el-descriptions-item label="系统信息">{{ currentLss?.systemInfo || '-' }}</el-descriptions-item>
-        <el-descriptions-item label="启用状态">
+        <el-descriptions-item :label="t('FFmpeg 版本')">{{ currentLss?.ffmpegVersion || '-' }}</el-descriptions-item>
+        <el-descriptions-item :label="t('系统信息')">{{ currentLss?.systemInfo || '-' }}</el-descriptions-item>
+        <el-descriptions-item :label="t('启用状态')">
           <el-tag :type="currentLss?.enabled ? 'success' : 'info'" size="small">
             {{ currentLss?.enabled ? '已启用' : '已禁用' }}
           </el-tag>
         </el-descriptions-item>
-        <el-descriptions-item label="创建时间">{{ formatTime(currentLss?.createdAt) }}</el-descriptions-item>
-        <el-descriptions-item label="更新时间">{{ formatTime(currentLss?.updatedAt) }}</el-descriptions-item>
+        <el-descriptions-item :label="t('创建时间')">{{ formatTime(currentLss?.createdAt) }}</el-descriptions-item>
+        <el-descriptions-item :label="t('更新时间')">{{ formatTime(currentLss?.updatedAt) }}</el-descriptions-item>
       </el-descriptions>
     </el-drawer>
 
@@ -146,14 +140,14 @@
           <!-- LSS 详情 Tab -->
           <div v-show="editActiveTab === 'detail'" class="lss-detail-form">
             <el-form ref="lssEditFormRef" :model="lssEditForm" label-width="80px" label-position="left">
-              <el-form-item label="LSS ID:">
+              <el-form-item :label="t('LSS ID') + ':'">
                 <span class="form-value">{{ currentLss?.lssId }}</span>
               </el-form-item>
               <el-form-item :label="t('名称') + ':'" prop="lssName">
-                <el-input v-model="lssEditForm.lssName" placeholder="请输入名称" style="width: 180px" />
+                <el-input v-model="lssEditForm.lssName" :placeholder="t('请输入名称')" style="width: 180px" />
               </el-form-item>
               <el-form-item :label="t('地址') + ':'" prop="address">
-                <el-input v-model="lssEditForm.address" placeholder="请输入地址" />
+                <el-input v-model="lssEditForm.address" :placeholder="t('请输入地址')" />
               </el-form-item>
               <el-form-item :label="t('IP') + ':'">
                 <span class="form-value">{{ currentLss?.ip }}</span>
@@ -220,20 +214,22 @@
                 <el-form-item>
                   <el-select v-model="cameraSearchForm.status" :placeholder="t('状态')" clearable style="width: 120px">
                     <el-option :label="t('全部')" value="" />
-                    <el-option :label="t('在线')" value="ative" />
-                    <el-option :label="t('待机')" value="hold" />
-                    <el-option :label="t('离线')" value="dead" />
+                    <el-option label="active" value="active" />
+                    <el-option label="hold" value="hold" />
+                    <el-option label="dead" value="dead" />
                   </el-select>
                 </el-form-item>
                 <el-form-item>
                   <el-button type="primary" :icon="Search" @click="handleCameraSearch">{{ t('查询') }}</el-button>
-                  <el-button :icon="RefreshRight" @click="handleCameraReset">{{ t('重置') }}</el-button>
+                  <el-button type="info" :icon="RefreshRight" @click="handleCameraReset">{{ t('重置') }}</el-button>
                   <el-button type="primary" :icon="Plus" @click="handleAddCamera">{{ t('新增') }}</el-button>
                 </el-form-item>
               </el-form>
             </div>
-            <el-empty v-if="!cameraLoading && cameraList.length === 0" :description="t('暂无关联设备')" />
-            <el-table v-else :data="cameraList" stripe size="small" border>
+            <el-table :data="cameraList" stripe size="small" border>
+              <template #empty>
+                <el-empty :description="t('暂无关联设备')" />
+              </template>
               <el-table-column prop="cameraId" :label="t('设备ID')" min-width="100" show-overflow-tooltip />
               <el-table-column prop="cameraName" :label="t('名称')" min-width="100" show-overflow-tooltip />
               <el-table-column :label="t('状态(心跳)')" min-width="140">
@@ -295,7 +291,7 @@
           </div>
         </div>
 
-        <div class="drawer-footer">
+        <div v-show="editActiveTab === 'detail'" class="drawer-footer">
           <el-button @click="lssEditDrawerVisible = false">{{ t('取消') }}</el-button>
           <el-button type="primary" :loading="lssUpdating" @click="handleUpdateLss">{{ t('更新') }}</el-button>
         </div>
@@ -328,19 +324,22 @@
                 <el-form-item>
                   <el-select v-model="cameraSearchForm.status" :placeholder="t('状态')" clearable style="width: 120px">
                     <el-option :label="t('全部')" value="" />
-                    <el-option :label="t('在线')" value="ONLINE" />
-                    <el-option :label="t('离线')" value="OFFLINE" />
+                    <el-option label="active" value="active" />
+                    <el-option label="hold" value="hold" />
+                    <el-option label="dead" value="dead" />
                   </el-select>
                 </el-form-item>
                 <el-form-item>
                   <el-button type="primary" :icon="Search" @click="handleCameraSearch">{{ t('查询') }}</el-button>
-                  <el-button :icon="RefreshRight" @click="handleCameraReset">{{ t('重置') }}</el-button>
+                  <el-button type="info" :icon="RefreshRight" @click="handleCameraReset">{{ t('重置') }}</el-button>
+                  <el-button type="primary" :icon="Plus" @click="handleAddCamera">{{ t('新增') }}</el-button>
                 </el-form-item>
               </el-form>
-              <el-button type="primary" :icon="Plus" @click="handleAddCamera">{{ t('新增') }}</el-button>
             </div>
-            <el-empty v-if="!cameraLoading && cameraList.length === 0" :description="t('暂无关联设备')" />
-            <el-table v-else :data="cameraList" stripe size="small" border>
+            <el-table :data="cameraList" stripe size="small" border>
+              <template #empty>
+                <el-empty :description="t('暂无关联设备')" />
+              </template>
               <!-- <el-table-column prop="ip" label="本地IP" min-width="110" /> -->
               <el-table-column prop="cameraId" :label="t('设备ID')" min-width="100" show-overflow-tooltip />
               <el-table-column prop="cameraName" :label="t('名称')" min-width="100" show-overflow-tooltip />
@@ -385,52 +384,52 @@
             <div v-if="cameraList.length > 0" class="camera-count">共 {{ cameraList.length }} 个设备</div>
           </div>
         </el-tab-pane>
-        <el-tab-pane label="推币机列表" name="pusher">
+        <el-tab-pane :label="t('推币机列表')" name="pusher">
           <div class="tab-content-wrapper">
             <div class="camera-toolbar">
               <el-form inline>
                 <el-form-item>
-                  <el-input placeholder="设备ID / 名称" clearable style="width: 200px" />
+                  <el-input :placeholder="t('设备ID / 名称')" clearable style="width: 200px" />
                 </el-form-item>
                 <el-form-item>
-                  <el-select placeholder="状态" clearable style="width: 120px">
-                    <el-option label="全部" value="" />
-                    <el-option label="在线" value="ONLINE" />
-                    <el-option label="离线" value="OFFLINE" />
+                  <el-select :placeholder="t('状态')" clearable style="width: 120px">
+                    <el-option :label="t('全部')" value="" />
+                    <el-option :label="t('在线')" value="ONLINE" />
+                    <el-option :label="t('离线')" value="OFFLINE" />
                   </el-select>
                 </el-form-item>
                 <el-form-item>
-                  <el-button type="primary" :icon="Search">查询</el-button>
-                  <el-button :icon="RefreshRight">重置</el-button>
+                  <el-button type="primary" :icon="Search">{{ t('查询') }}</el-button>
+                  <el-button type="info" :icon="RefreshRight">{{ t('重置') }}</el-button>
                 </el-form-item>
               </el-form>
               <el-button type="primary" :icon="Plus">{{ t('新增') }}</el-button>
             </div>
-            <el-empty description="暂无推币机数据" />
+            <el-empty :description="t('暂无推币机数据')" />
           </div>
         </el-tab-pane>
-        <el-tab-pane label="其他设备" name="other">
+        <el-tab-pane :label="t('其他设备')" name="other">
           <div class="tab-content-wrapper">
             <div class="camera-toolbar">
               <el-form inline>
                 <el-form-item>
-                  <el-input placeholder="设备ID / 名称" clearable style="width: 200px" />
+                  <el-input :placeholder="t('设备ID / 名称')" clearable style="width: 200px" />
                 </el-form-item>
                 <el-form-item>
-                  <el-select placeholder="状态" clearable style="width: 120px">
-                    <el-option label="全部" value="" />
-                    <el-option label="在线" value="ONLINE" />
-                    <el-option label="离线" value="OFFLINE" />
+                  <el-select :placeholder="t('状态')" clearable style="width: 120px">
+                    <el-option :label="t('全部')" value="" />
+                    <el-option :label="t('在线')" value="ONLINE" />
+                    <el-option :label="t('离线')" value="OFFLINE" />
                   </el-select>
                 </el-form-item>
                 <el-form-item>
-                  <el-button type="primary" :icon="Search">查询</el-button>
-                  <el-button :icon="RefreshRight">重置</el-button>
+                  <el-button type="primary" :icon="Search">{{ t('查询') }}</el-button>
+                  <el-button type="info" :icon="RefreshRight">{{ t('重置') }}</el-button>
                 </el-form-item>
               </el-form>
               <el-button type="primary" :icon="Plus">{{ t('新增') }}</el-button>
             </div>
-            <el-empty description="暂无其他设备数据" />
+            <el-empty :description="t('暂无其他设备数据')" />
           </div>
         </el-tab-pane>
       </el-tabs>
@@ -439,10 +438,11 @@
     <!-- 摄像头编辑抽屉 -->
     <el-drawer
       v-model="cameraDialogVisible"
-      :title="isEditCamera ? '编辑摄像头' : '添加摄像头'"
+      :title="isEditCamera ? t('摄像头详情') : t('添加摄像头')"
       direction="rtl"
       size="500px"
       :close-on-click-modal="false"
+      :show-close="false"
       destroy-on-close
       class="camera-edit-drawer"
     >
@@ -450,16 +450,16 @@
         <!-- <el-form-item label="IP 地址" prop="ip">
           <el-input v-model="cameraForm.ip" :disabled="isEditCamera" placeholder="请输入 IP 地址" />
         </el-form-item> -->
-        <el-form-item label="设备ID" prop="cameraId">
-          <el-input v-model="cameraForm.cameraId" :disabled="isEditCamera" placeholder="请输入设备ID" />
+        <el-form-item :label="t('设备ID') + ':'" prop="cameraId">
+          <el-input v-model="cameraForm.cameraId" :disabled="isEditCamera" :placeholder="t('请输入设备ID')" />
         </el-form-item>
-        <el-form-item label="设备名称" prop="cameraName">
-          <el-input v-model="cameraForm.cameraName" placeholder="请输入设备名称" />
+        <el-form-item :label="t('设备名称') + ':'" prop="cameraName">
+          <el-input v-model="cameraForm.cameraName" :placeholder="t('请输入设备名称')" />
         </el-form-item>
-        <el-form-item label="厂商" prop="vendorName">
+        <el-form-item :label="t('厂商') + ':'" prop="vendorName">
           <el-select
             v-model="cameraForm.vendorName"
-            placeholder="请选择摄像头"
+            :placeholder="t('请选择摄像头')"
             style="width: 100%"
             filterable
             @change="handleVendorSelect"
@@ -477,8 +477,11 @@
             />
           </el-select>
         </el-form-item>
-        <el-form-item label="型号" prop="model">
-          <el-input v-model="cameraForm.model" placeholder="请输入型号" />
+        <el-form-item :label="t('型号') + ':'" prop="model">
+          <el-input v-model="cameraForm.model" :placeholder="t('请输入型号')" />
+        </el-form-item>
+        <el-form-item v-if="isEditCamera" :label="t('添加时间') + ':'">
+          <span class="form-value">{{ formatTime(currentCamera?.createdAt) }}</span>
         </el-form-item>
         <!-- <el-form-item label="摄像头型号" prop="cameraId">
           <el-select v-model="cameraForm.selectedVendorId" placeholder="请选择摄像头" style="width: 100%" filterable
@@ -498,28 +501,28 @@
         <el-form-item label="密码" prop="password">
           <el-input v-model="cameraForm.password" type="password" show-password placeholder="请输入密码" />
         </el-form-item> -->
-        <el-form-item label="参数配置">
+        <el-form-item :label="t('参数配置') + ':'">
           <CodeEditor
             v-model="cameraForm.paramConfig"
             language="json"
             height="200px"
-            placeholder="请输入参数配置 (JSON)"
+            :placeholder="t('请输入参数配置 (JSON)')"
           />
         </el-form-item>
         <br />
-        <el-form-item label="设备运行参数">
+        <el-form-item :label="t('运行参数') + ':'">
           <CodeEditor
             v-model="cameraForm.runtimeParams"
             language="json"
             height="200px"
-            placeholder="设备运行参数 (JSON)"
+            :placeholder="t('设备运行参数 (JSON)')"
           />
         </el-form-item>
       </el-form>
       <template #footer>
         <div class="drawer-footer">
           <el-button @click="cameraDialogVisible = false">{{ t('取消') }}</el-button>
-          <el-button type="primary" :loading="cameraSubmitting" @click="handleSubmitCamera">{{ t('确定') }}</el-button>
+          <el-button type="primary" :loading="cameraSubmitting" @click="handleSubmitCamera">{{ t('更新') }}</el-button>
         </div>
       </template>
     </el-drawer>
@@ -531,6 +534,7 @@
       direction="rtl"
       size="500px"
       :close-on-click-modal="false"
+      :show-close="false"
       destroy-on-close
       class="params-drawer"
     >
@@ -538,7 +542,9 @@
         v-model="paramsContent"
         language="json"
         height="500px"
-        :placeholder="paramsDialogType === 'config' ? '请输入参数配置(JSON 格式)' : '请输入运行参数(JSON 格式)'"
+        :placeholder="
+          paramsDialogType === 'config' ? t('请输入参数配置(JSON 格式)') : t('请输入运行参数(JSON 格式)')
+        "
       />
       <template #footer>
         <div class="drawer-footer">
@@ -553,7 +559,7 @@
       <el-pagination
         v-model:current-page="currentPage"
         v-model:page-size="pageSize"
-        :page-sizes="[10, 20, 50, 100]"
+        :page-sizes="[10, 15, 20, 50, 100]"
         :total="total"
         layout="total, sizes, prev, pager, next, jumper"
         background
@@ -866,7 +872,7 @@ const searchForm = reactive<{
 
 // 分页相关
 const currentPage = ref(1)
-const pageSize = ref(20)
+const pageSize = ref(15)
 const total = ref(0)
 
 async function getList() {
@@ -918,6 +924,8 @@ function handleReset() {
   searchForm.lssId = ''
   searchForm.lssName = ''
   searchForm.status = ''
+  sortState.sortBy = ''
+  sortState.sortDir = undefined
   currentPage.value = 1
   getList()
 }
@@ -1220,7 +1228,7 @@ async function handleSubmitCamera() {
 
 async function handleDeleteCamera(row: CameraInfoDTO) {
   try {
-    await ElMessageBox.confirm(`确定要删除摄像头 "${row.name}" 吗?`, '提示', {
+    await ElMessageBox.confirm(`确定要删除摄像头 "${row.cameraName}" 吗?`, '提示', {
       type: 'warning'
     })
     const res = await adminDeleteCamera(row.id)

+ 138 - 16
tests/e2e/live-stream.spec.ts

@@ -75,7 +75,7 @@ test.describe('LiveStream 管理 - 搜索功能测试', () => {
     })
 
     // 在 name 搜索框输入
-    await page.getByPlaceholder('name').fill('测试流')
+    await page.getByPlaceholder('名称').fill('测试流')
 
     // 点击查询
     await page.getByRole('button', { name: '查询' }).click()
@@ -180,7 +180,7 @@ test.describe('LiveStream 管理 - 搜索功能测试', () => {
 
     // 填入所有搜索条件
     await page.getByPlaceholder('stream sn').fill('stream_001')
-    await page.getByPlaceholder('name').fill('测试')
+    await page.getByPlaceholder('名称').fill('测试')
     await page.getByPlaceholder('设备ID').fill('EEE1')
 
     // 点击查询
@@ -203,7 +203,7 @@ test.describe('LiveStream 管理 - 搜索功能测试', () => {
 
     // 填入搜索条件
     await page.getByPlaceholder('stream sn').fill('test-sn')
-    await page.getByPlaceholder('name').fill('test-name')
+    await page.getByPlaceholder('名称').fill('test-name')
     await page.getByPlaceholder('设备ID').fill('test-device')
 
     // 点击重置
@@ -350,7 +350,7 @@ test.describe('LiveStream 管理 - CodeEditor 组件测试 (Bash模式)', () =>
     await viewLink.click()
 
     // 等待命令模板弹窗打开
-    const dialog = page.locator('.el-dialog').filter({ hasText: '命令模板' })
+    const dialog = page.locator('.el-drawer').filter({ hasText: '命令模板' })
     await expect(dialog).toBeVisible({ timeout: 5000 })
 
     // 验证 CodeEditor 头部显示 Bash Script 标签
@@ -381,7 +381,7 @@ test.describe('LiveStream 管理 - CodeEditor 组件测试 (Bash模式)', () =>
     await viewLink.click()
 
     // 等待命令模板弹窗打开
-    const dialog = page.locator('.el-dialog').filter({ hasText: '命令模板' })
+    const dialog = page.locator('.el-drawer').filter({ hasText: '命令模板' })
     await expect(dialog).toBeVisible({ timeout: 5000 })
 
     // 点击复制按钮
@@ -407,7 +407,7 @@ test.describe('LiveStream 管理 - CodeEditor 组件测试 (Bash模式)', () =>
     await viewLink.click()
 
     // 等待命令模板弹窗打开
-    const dialog = page.locator('.el-dialog').filter({ hasText: '命令模板' })
+    const dialog = page.locator('.el-drawer').filter({ hasText: '命令模板' })
     await expect(dialog).toBeVisible({ timeout: 5000 })
 
     // 验证编辑器存在并可以编辑
@@ -425,9 +425,9 @@ test.describe('LiveStream 管理 - CodeEditor 组件测试 (Bash模式)', () =>
   })
 
   /**
-   * 测试 CodeEditor Bash 模式 - 更新按钮存在
+   * 测试 CodeEditor Bash 模式 - 更新按钮存在 (Bug #4628: "关闭" changed to "取消", Bug #4629: dialog changed to drawer)
    */
-  test('CodeEditor Bash模式 - 弹窗包含关闭和更新按钮', async ({ page }) => {
+  test('CodeEditor Bash模式 - 抽屉包含取消和更新按钮', async ({ page }) => {
     await login(page)
     await page.goto('/live-stream')
 
@@ -439,15 +439,15 @@ test.describe('LiveStream 管理 - CodeEditor 组件测试 (Bash模式)', () =>
     await viewLink.click()
 
     // 等待命令模板弹窗打开
-    const dialog = page.locator('.el-dialog').filter({ hasText: '命令模板' })
+    const dialog = page.locator('.el-drawer').filter({ hasText: '命令模板' })
     await expect(dialog).toBeVisible({ timeout: 5000 })
 
     // 验证关闭和更新按钮存在
-    await expect(dialog.locator('button:has-text("关闭"), button:has-text("Close")')).toBeVisible()
+    await expect(dialog.locator('button:has-text("取消"), button:has-text("Cancel")')).toBeVisible()
     await expect(dialog.locator('button:has-text("更新"), button:has-text("Update")')).toBeVisible()
 
     // 点击关闭按钮
-    await dialog.locator('button:has-text("关闭"), button:has-text("Close")').click()
+    await dialog.locator('button:has-text("取消"), button:has-text("Cancel")').click()
     await expect(dialog).not.toBeVisible({ timeout: 5000 })
   })
 
@@ -466,7 +466,7 @@ test.describe('LiveStream 管理 - CodeEditor 组件测试 (Bash模式)', () =>
     await viewLink.click()
 
     // 等待命令模板弹窗打开
-    const dialog = page.locator('.el-dialog').filter({ hasText: '命令模板' })
+    const dialog = page.locator('.el-drawer').filter({ hasText: '命令模板' })
     await expect(dialog).toBeVisible({ timeout: 5000 })
 
     // 验证图标有 icon-bash 类(对应绿色)
@@ -489,7 +489,7 @@ test.describe('LiveStream 管理 - CodeEditor 组件测试 (Bash模式)', () =>
     await viewLink.click()
 
     // 等待命令模板弹窗打开
-    const dialog = page.locator('.el-dialog').filter({ hasText: '命令模板' })
+    const dialog = page.locator('.el-drawer').filter({ hasText: '命令模板' })
     await expect(dialog).toBeVisible({ timeout: 5000 })
 
     // 获取编辑器
@@ -520,7 +520,7 @@ test.describe('LiveStream 管理 - CodeEditor 组件测试 (Bash模式)', () =>
     await viewLink.click()
 
     // 等待弹窗重新打开
-    const dialogReopened = page.locator('.el-dialog').filter({ hasText: '命令模板' })
+    const dialogReopened = page.locator('.el-drawer').filter({ hasText: '命令模板' })
     await expect(dialogReopened).toBeVisible({ timeout: 5000 })
 
     // 验证内容包含我们添加的测试注释
@@ -543,7 +543,7 @@ test.describe('LiveStream 管理 - CodeEditor 组件测试 (Bash模式)', () =>
     await viewLink.click()
 
     // 等待命令模板弹窗打开
-    const dialog = page.locator('.el-dialog').filter({ hasText: '命令模板' })
+    const dialog = page.locator('.el-drawer').filter({ hasText: '命令模板' })
     await expect(dialog).toBeVisible({ timeout: 5000 })
 
     // 获取编辑器
@@ -579,7 +579,7 @@ ffmpeg -i {RTSP_URL} -c copy output.mp4`
     await page.waitForTimeout(500)
     await viewLink.click()
 
-    const dialogReopened = page.locator('.el-dialog').filter({ hasText: '命令模板' })
+    const dialogReopened = page.locator('.el-drawer').filter({ hasText: '命令模板' })
     await expect(dialogReopened).toBeVisible({ timeout: 5000 })
 
     const editorContentReopened = dialogReopened.locator('.cm-content')
@@ -734,3 +734,125 @@ test.describe('LiveStream 管理 - 从 LSS 页面创建流程', () => {
     expect(listRequestBody.cameraId).toBe('CAM_SEARCH_TEST')
   })
 })
+
+test.describe('LiveStream 管理 - Bug修复验证测试', () => {
+  // 登录辅助函数
+  async function loginHelper(page: Page) {
+    await page.goto('/login')
+    await page.evaluate(() => {
+      localStorage.clear()
+      document.cookie.split(';').forEach((c) => {
+        document.cookie = c.replace(/^ +/, '').replace(/=.*/, `=;expires=${new Date().toUTCString()};path=/`)
+      })
+    })
+    await page.reload()
+
+    await page.getByPlaceholder('用户名').fill(TEST_USERNAME)
+    await page.getByPlaceholder('密码').fill(TEST_PASSWORD)
+    await page.getByRole('button', { name: '登录' }).click()
+    await expect(page).not.toHaveURL(/\/login/, { timeout: 15000 })
+  }
+
+  /**
+   * Bug #4627: 启动时间和关闭时间显示用户当前时区
+   */
+  test('Bug #4627 - 时间列正确格式化显示', async ({ page }) => {
+    await loginHelper(page)
+    await page.goto('/live-stream')
+
+    // 等待表格加载
+    await page.waitForSelector('tbody tr', { timeout: 10000 })
+
+    // 验证启动时间列存在
+    await expect(page.locator('th:has-text("启动时间")')).toBeVisible()
+    await expect(page.locator('th:has-text("关闭时间")')).toBeVisible()
+
+    // 验证时间格式正确 (YYYY-MM-DD HH:mm:ss)
+    const startedAtCell = page.locator('tbody tr').first().locator('td').nth(7)
+    const startedAtText = await startedAtCell.textContent()
+
+    // 时间格式应该是 YYYY-MM-DD HH:mm:ss 或 "-"
+    if (startedAtText && startedAtText.trim() !== '-') {
+      expect(startedAtText).toMatch(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/)
+    }
+  })
+
+  /**
+   * Bug #4628: 命令模板按钮文字从"关闭"改为"取消"
+   */
+  test('Bug #4628 - 命令模板抽屉按钮显示"取消"而非"关闭"', async ({ page }) => {
+    await loginHelper(page)
+    await page.goto('/live-stream')
+
+    // 等待表格加载
+    await page.waitForSelector('tbody tr', { timeout: 10000 })
+
+    // 点击命令模板列的"查看"链接
+    const viewLink = page.locator('tbody tr').first().locator('a:has-text("查看"), a:has-text("View")')
+    await expect(viewLink).toBeVisible({ timeout: 5000 })
+    await viewLink.click()
+
+    // 等待命令模板抽屉打开
+    const drawer = page.locator('.el-drawer').filter({ hasText: '命令模板' })
+    await expect(drawer).toBeVisible({ timeout: 5000 })
+
+    // 验证有"取消"按钮而非"关闭"按钮
+    await expect(drawer.locator('button:has-text("取消")')).toBeVisible()
+    await expect(drawer.locator('button:has-text("关闭")')).not.toBeVisible()
+  })
+
+  /**
+   * Bug #4629: 命令模板使用右到左抽屉显示
+   */
+  test('Bug #4629 - 命令模板使用抽屉而非对话框', async ({ page }) => {
+    await loginHelper(page)
+    await page.goto('/live-stream')
+
+    // 等待表格加载
+    await page.waitForSelector('tbody tr', { timeout: 10000 })
+
+    // 点击命令模板列的"查看"链接
+    const viewLink = page.locator('tbody tr').first().locator('a:has-text("查看"), a:has-text("View")')
+    await expect(viewLink).toBeVisible({ timeout: 5000 })
+    await viewLink.click()
+
+    // 等待命令模板抽屉打开
+    const drawer = page.locator('.el-drawer').filter({ hasText: '命令模板' })
+    await expect(drawer).toBeVisible({ timeout: 5000 })
+
+    // 验证是抽屉(el-drawer)而非对话框(el-dialog)
+    const dialog = page.locator('.el-dialog').filter({ hasText: '命令模板' })
+    await expect(dialog).not.toBeVisible()
+  })
+
+  /**
+   * Bug #4630: 搜索框placeholder从"name"改为"名称"
+   */
+  test('Bug #4630 - 搜索框placeholder显示"名称"而非"name"', async ({ page }) => {
+    await loginHelper(page)
+    await page.goto('/live-stream')
+
+    // 验证名称搜索框的placeholder是"名称"
+    const nameInput = page.getByPlaceholder('名称')
+    await expect(nameInput).toBeVisible()
+
+    // 验证没有placeholder为"name"的输入框
+    const nameInputEnglish = page.getByPlaceholder('name')
+    await expect(nameInputEnglish).not.toBeVisible()
+  })
+
+  /**
+   * Bug #4541 (通用): 重置按钮使用灰底白字
+   */
+  test('Bug #4541 - 重置按钮使用灰底白字', async ({ page }) => {
+    await loginHelper(page)
+    await page.goto('/live-stream')
+
+    // 获取重置按钮
+    const resetButton = page.getByRole('button', { name: '重置' })
+    await expect(resetButton).toBeVisible()
+
+    // 验证按钮有 el-button--info 类(灰色按钮)
+    await expect(resetButton).toHaveClass(/el-button--info/)
+  })
+})

+ 493 - 5
tests/e2e/lss.spec.ts

@@ -35,8 +35,8 @@ test.describe('LSS管理 CRUD 测试', () => {
     await login(page)
     await page.goto('/lss')
 
-    // 验证页面标题
-    await expect(page.locator('text=LSS 管理')).toBeVisible()
+    // 验证页面标题 (Bug #4537: 页面标题从 "LSS 管理" 改为 "LSS 列表")
+    await expect(page.locator('text=LSS 列表')).toBeVisible()
 
     // 验证搜索表单元素
     await expect(page.getByPlaceholder('LSS ID')).toBeVisible()
@@ -171,12 +171,12 @@ test.describe('LSS管理 CRUD 测试', () => {
   test('从侧边栏导航到LSS管理', async ({ page }) => {
     await login(page)
 
-    // 点击侧边栏 LSS 管理菜单项
-    await page.getByText('LSS 管理').first().click()
+    // 点击侧边栏 LSS 列表菜单项 (Bug #4537: 菜单标题从 "LSS 管理" 改为 "LSS 列表")
+    await page.getByText('LSS 列表').first().click()
 
     // 验证跳转到 LSS 管理页面
     await expect(page).toHaveURL(/\/lss/)
-    await expect(page.locator('text=LSS 管理')).toBeVisible()
+    await expect(page.locator('text=LSS 列表')).toBeVisible()
   })
 })
 
@@ -1023,3 +1023,491 @@ test.describe('LSS管理 - 摄像头未创建 Live Stream 对话框测试', () =
     }
   })
 })
+
+test.describe('LSS管理 - Bug修复验证测试', () => {
+  // 登录辅助函数
+  async function login(page: Page) {
+    await page.goto('/login')
+    await page.evaluate(() => {
+      localStorage.clear()
+      document.cookie.split(';').forEach((c) => {
+        document.cookie = c.replace(/^ +/, '').replace(/=.*/, `=;expires=${new Date().toUTCString()};path=/`)
+      })
+    })
+    await page.reload()
+
+    await page.getByPlaceholder('用户名').fill(TEST_USERNAME)
+    await page.getByPlaceholder('密码').fill(TEST_PASSWORD)
+    await page.getByRole('button', { name: '登录' }).click()
+    await expect(page).not.toHaveURL(/\/login/, { timeout: 15000 })
+  }
+
+  /**
+   * Bug #4535: 重置按钮需要清除所有搜索条件(包括排序状态)
+   */
+  test('Bug #4535 - 重置按钮清除所有搜索条件包括排序状态', async ({ page }) => {
+    await login(page)
+    await page.goto('/lss')
+
+    // 等待表格加载
+    await page.waitForTimeout(1000)
+
+    // 填入搜索条件
+    await page.getByPlaceholder('LSS ID').fill('test-id')
+    await page.getByPlaceholder('名称').fill('test-name')
+
+    // 点击表头进行排序
+    const lssIdHeader = page.locator('th:has-text("LSS ID")')
+    await lssIdHeader.click()
+    await page.waitForTimeout(300)
+
+    // 验证排序图标出现
+    await expect(lssIdHeader.locator('.ascending, .descending, .caret-wrapper')).toBeVisible()
+
+    // 点击重置
+    await page.getByRole('button', { name: 'Reset' }).click()
+    await page.waitForTimeout(300)
+
+    // 验证搜索条件已清空
+    await expect(page.getByPlaceholder('LSS ID')).toHaveValue('')
+    await expect(page.getByPlaceholder('名称')).toHaveValue('')
+
+    // 验证排序状态已重置(表头不再显示排序方向)
+    // 排序图标应该恢复到默认状态(无 ascending 或 descending 类)
+    const sortIcon = lssIdHeader.locator('.ascending')
+    const hasAscending = await sortIcon.count()
+    const sortIconDesc = lssIdHeader.locator('.descending')
+    const hasDescending = await sortIconDesc.count()
+    // 排序应该被清除
+    expect(hasAscending + hasDescending).toBe(0)
+  })
+
+  /**
+   * Bug #4536: 心跳状态显示为英文(active/hold/dead)而非中文
+   */
+  test('Bug #4536 - 心跳状态显示为英文格式', async ({ page }) => {
+    await login(page)
+    await page.goto('/lss')
+
+    // 等待表格加载
+    await page.waitForTimeout(1000)
+
+    // 获取心跳列的内容
+    const heartbeatColumn = page.locator('tbody tr').first().locator('td').nth(4)
+    const heartbeatText = await heartbeatColumn.textContent()
+
+    // 验证心跳状态是英文格式(active/hold/dead),而非中文(活跃/待机/离线)
+    // 心跳列格式: "status | time" 例如 "active | 2024-01-26 10:00:00"
+    expect(heartbeatText).toMatch(/active|hold|dead/i)
+    // 不应该包含中文状态
+    expect(heartbeatText).not.toMatch(/活跃|待机|离线/)
+  })
+
+  /**
+   * Bug #4537: 页面标题从"LSS 管理"改为"LSS 列表"
+   */
+  test('Bug #4537 - 页面标题显示为"LSS 列表"', async ({ page }) => {
+    await login(page)
+    await page.goto('/lss')
+
+    // 等待页面加载
+    await page.waitForTimeout(500)
+
+    // 验证侧边栏菜单显示"LSS 列表"
+    await expect(page.locator('.el-menu-item.is-active, .el-sub-menu.is-active').first()).toContainText('LSS 列表')
+
+    // 验证页面内容区域标题(如果有面包屑或页面标题)
+    // 注意:根据实际UI调整选择器
+    const pageTitle = page.locator('text=LSS 列表').first()
+    await expect(pageTitle).toBeVisible()
+  })
+
+  /**
+   * Bug #4538: 分页默认显示15条/页
+   */
+  test('Bug #4538 - 分页默认显示15条/页', async ({ page }) => {
+    await login(page)
+    await page.goto('/lss')
+
+    // 等待表格加载
+    await page.waitForTimeout(1000)
+
+    // 验证分页选择器的默认值是15
+    const pageSizeSelector = page.locator(
+      '.el-pagination .el-select .el-input__inner, .el-pagination .el-select-v2__placeholder'
+    )
+    const pageSizeText = (await pageSizeSelector.first().textContent()) || (await pageSizeSelector.first().inputValue())
+
+    // 验证默认每页显示15条
+    expect(pageSizeText).toContain('15')
+  })
+
+  /**
+   * Bug #4538: 分页选项包含15
+   */
+  test('Bug #4538 - 分页选项包含15条/页选项', async ({ page }) => {
+    await login(page)
+    await page.goto('/lss')
+
+    // 等待表格加载
+    await page.waitForTimeout(1000)
+
+    // 点击分页选择器打开下拉菜单
+    const pageSizeSelector = page.locator('.el-pagination .el-select')
+    await pageSizeSelector.click()
+    await page.waitForTimeout(300)
+
+    // 验证下拉菜单中包含15选项
+    const dropdown = page.locator('.el-select-dropdown, .el-popper')
+    await expect(dropdown.locator('text=15')).toBeVisible()
+
+    // 验证选项顺序正确: 10, 15, 20, 50, 100
+    const options = dropdown.locator('.el-select-dropdown__item')
+    const optionsText = await options.allTextContents()
+
+    // 验证包含所有期望的选项
+    expect(optionsText.some((t) => t.includes('10'))).toBeTruthy()
+    expect(optionsText.some((t) => t.includes('15'))).toBeTruthy()
+    expect(optionsText.some((t) => t.includes('20'))).toBeTruthy()
+    expect(optionsText.some((t) => t.includes('50'))).toBeTruthy()
+    expect(optionsText.some((t) => t.includes('100'))).toBeTruthy()
+  })
+
+  /**
+   * Bug #4540: 查询按钮使用蓝底(409EFF)白字
+   */
+  test('Bug #4540 - 查询按钮使用蓝底白字', async ({ page }) => {
+    await login(page)
+    await page.goto('/lss')
+
+    // 等待页面加载
+    await page.waitForTimeout(500)
+
+    // 获取查询按钮
+    const searchButton = page.locator('[data-id="btn-search"]')
+    await expect(searchButton).toBeVisible()
+
+    // 验证按钮有 el-button--primary 类(蓝色按钮)
+    await expect(searchButton).toHaveClass(/el-button--primary/)
+
+    // 验证按钮背景色接近 #409EFF
+    const bgColor = await searchButton.evaluate((el) => {
+      return window.getComputedStyle(el).backgroundColor
+    })
+    // #409EFF 的 RGB 值为 rgb(64, 158, 255)
+    expect(bgColor).toMatch(/rgb\(64,\s*158,\s*255\)|rgba\(64,\s*158,\s*255/)
+  })
+
+  /**
+   * Bug #4541: 重置按钮使用灰底(909399)白字
+   */
+  test('Bug #4541 - 重置按钮使用灰底白字', async ({ page }) => {
+    await login(page)
+    await page.goto('/lss')
+
+    // 等待页面加载
+    await page.waitForTimeout(500)
+
+    // 获取重置按钮
+    const resetButton = page.locator('[data-id="btn-reset"]')
+    await expect(resetButton).toBeVisible()
+
+    // 验证按钮有 el-button--info 类(灰色按钮)
+    await expect(resetButton).toHaveClass(/el-button--info/)
+
+    // 验证按钮背景色接近 #909399
+    const bgColor = await resetButton.evaluate((el) => {
+      return window.getComputedStyle(el).backgroundColor
+    })
+    // #909399 的 RGB 值为 rgb(144, 147, 153)
+    expect(bgColor).toMatch(/rgb\(144,\s*147,\s*153\)|rgba\(144,\s*147,\s*153/)
+  })
+
+  /**
+   * Bug #4542: 分页当前页背景色使用蓝底(409EFF)白字
+   */
+  test('Bug #4542 - 分页当前页使用蓝底白字', async ({ page }) => {
+    await login(page)
+    await page.goto('/lss')
+
+    // 等待表格加载
+    await page.waitForTimeout(1000)
+
+    // 获取分页组件中当前激活的页码按钮
+    const activePager = page.locator('.el-pagination .el-pager .is-active, .el-pagination .el-pager .active')
+    await expect(activePager).toBeVisible()
+
+    // 验证当前页背景色接近 #409EFF
+    const bgColor = await activePager.evaluate((el) => {
+      return window.getComputedStyle(el).backgroundColor
+    })
+    // #409EFF 的 RGB 值为 rgb(64, 158, 255)
+    expect(bgColor).toMatch(/rgb\(64,\s*158,\s*255\)|rgba\(64,\s*158,\s*255/)
+  })
+
+  /**
+   * Bug #4557: 摄像头列表状态下拉框显示 active/hold/dead
+   */
+  test('Bug #4557 - 摄像头列表状态下拉框显示英文选项', async ({ page }) => {
+    await login(page)
+    await page.goto('/lss')
+
+    // 等待表格加载
+    await page.waitForTimeout(1000)
+
+    // 点击编辑按钮进入 LSS 编辑抽屉
+    const editButton = page.locator('tbody tr').first().locator('button').nth(1)
+    await expect(editButton).toBeVisible({ timeout: 10000 })
+    await editButton.click()
+
+    // 等待 LSS 编辑抽屉打开
+    const drawer = page.locator('.el-drawer').filter({ hasText: 'LSS详情' })
+    await expect(drawer).toBeVisible({ timeout: 5000 })
+
+    // 点击"摄像头列表" Tab
+    await drawer.locator('.el-tabs__item').filter({ hasText: '摄像头列表' }).click()
+    await page.waitForTimeout(500)
+
+    // 点击状态下拉框
+    const statusSelect = drawer.locator('.camera-toolbar .el-select').first()
+    await statusSelect.click()
+    await page.waitForTimeout(300)
+
+    // 验证下拉选项包含 active/hold/dead 而非中文
+    const dropdown = page.locator('.el-select-dropdown, .el-popper').last()
+    const options = dropdown.locator('.el-select-dropdown__item')
+    const optionsText = await options.allTextContents()
+
+    // 验证包含英文选项
+    expect(optionsText.some((t) => t.includes('active'))).toBeTruthy()
+    expect(optionsText.some((t) => t.includes('hold'))).toBeTruthy()
+    expect(optionsText.some((t) => t.includes('dead'))).toBeTruthy()
+
+    // 验证不包含中文状态
+    expect(optionsText.some((t) => t.includes('在线'))).toBeFalsy()
+    expect(optionsText.some((t) => t.includes('离线'))).toBeFalsy()
+  })
+
+  /**
+   * Bug #4558: 摄像头列表没有数据时表头要显示
+   */
+  test('Bug #4558 - 摄像头列表无数据时显示表头', async ({ page }) => {
+    await login(page)
+
+    // Mock 摄像头列表 API 返回空数据
+    await page.route('**/admin/camera/list*', async (route) => {
+      await route.fulfill({
+        status: 200,
+        contentType: 'application/json',
+        body: JSON.stringify({
+          success: true,
+          errCode: 0,
+          data: {
+            list: [],
+            total: 0
+          }
+        })
+      })
+    })
+
+    await page.goto('/lss')
+    await page.waitForTimeout(1000)
+
+    // 点击编辑按钮进入 LSS 编辑抽屉
+    const editButton = page.locator('tbody tr').first().locator('button').nth(1)
+    await expect(editButton).toBeVisible({ timeout: 10000 })
+    await editButton.click()
+
+    // 等待 LSS 编辑抽屉打开
+    const drawer = page.locator('.el-drawer').filter({ hasText: 'LSS详情' })
+    await expect(drawer).toBeVisible({ timeout: 5000 })
+
+    // 点击"摄像头列表" Tab
+    await drawer.locator('.el-tabs__item').filter({ hasText: '摄像头列表' }).click()
+    await page.waitForTimeout(500)
+
+    // 验证表头可见(即使没有数据)
+    await expect(drawer.locator('th:has-text("设备ID")')).toBeVisible()
+    await expect(drawer.locator('th:has-text("名称")')).toBeVisible()
+    await expect(drawer.locator('th:has-text("状态")')).toBeVisible()
+
+    // 验证空状态提示显示
+    await expect(drawer.locator('.el-empty')).toBeVisible()
+  })
+
+  /**
+   * Bug #4593: 删除提示显示正确的设备名称而非undefined
+   */
+  test('Bug #4593 - 删除提示显示正确的设备名称', async ({ page }) => {
+    await login(page)
+
+    // Mock 摄像头列表 API 返回有数据
+    const testCameraName = '测试摄像头001'
+    await page.route('**/admin/camera/list*', async (route) => {
+      await route.fulfill({
+        status: 200,
+        contentType: 'application/json',
+        body: JSON.stringify({
+          success: true,
+          errCode: 0,
+          data: {
+            list: [
+              {
+                id: 1,
+                cameraId: 'CAM_TEST_001',
+                cameraName: testCameraName,
+                lssId: 'LSS_001',
+                status: 'active',
+                streamSn: 'STREAM_001'
+              }
+            ],
+            total: 1
+          }
+        })
+      })
+    })
+
+    await page.goto('/lss')
+    await page.waitForTimeout(1000)
+
+    // 点击编辑按钮进入 LSS 编辑抽屉
+    const editButton = page.locator('tbody tr').first().locator('button').nth(1)
+    await expect(editButton).toBeVisible({ timeout: 10000 })
+    await editButton.click()
+
+    // 等待 LSS 编辑抽屉打开
+    const drawer = page.locator('.el-drawer').filter({ hasText: 'LSS详情' })
+    await expect(drawer).toBeVisible({ timeout: 5000 })
+
+    // 点击"摄像头列表" Tab
+    await drawer.locator('.el-tabs__item').filter({ hasText: '摄像头列表' }).click()
+    await page.waitForTimeout(500)
+
+    // 点击删除按钮
+    const deleteButton = drawer
+      .locator('tbody tr')
+      .first()
+      .locator('button[type="button"]')
+      .filter({ hasText: '' })
+      .last()
+    // 使用 Icon 选择器更精确
+    const deleteIcon = drawer.locator('tbody tr').first().locator('.iconify--mdi[data-icon*="delete"]').first()
+    if ((await deleteIcon.count()) > 0) {
+      await deleteIcon.click()
+    } else {
+      // 备选方案:点击最后一个按钮(删除按钮)
+      await drawer.locator('tbody tr').first().locator('button').last().click()
+    }
+
+    // 等待确认对话框
+    const messageBox = page.locator('.el-message-box')
+    await expect(messageBox).toBeVisible({ timeout: 5000 })
+
+    // 验证对话框中显示正确的设备名称,而非 "undefined"
+    const messageText = await messageBox.locator('.el-message-box__message').textContent()
+    expect(messageText).toContain(testCameraName)
+    expect(messageText).not.toContain('undefined')
+
+    // 关闭对话框
+    await messageBox.locator('button:has-text("取消")').click()
+  })
+
+  /**
+   * Bug #4569: 编辑摄像头标题改为"摄像头详情"
+   */
+  test('Bug #4569 - 编辑摄像头抽屉标题显示"摄像头详情"', async ({ page }) => {
+    await login(page)
+    await page.goto('/lss')
+    await page.waitForTimeout(1000)
+
+    // 点击编辑按钮进入 LSS 编辑抽屉
+    const editButton = page.locator('tbody tr').first().locator('button').nth(1)
+    await expect(editButton).toBeVisible({ timeout: 10000 })
+    await editButton.click()
+
+    // 等待 LSS 编辑抽屉打开
+    const drawer = page.locator('.el-drawer').filter({ hasText: 'LSS详情' })
+    await expect(drawer).toBeVisible({ timeout: 5000 })
+
+    // 点击"摄像头列表" Tab
+    await drawer.locator('.el-tabs__item').filter({ hasText: '摄像头列表' }).click()
+    await page.waitForTimeout(500)
+
+    // 点击摄像头编辑按钮
+    const cameraEditButton = drawer.locator('tbody tr').first().locator('button').first()
+    if ((await cameraEditButton.count()) > 0) {
+      await cameraEditButton.click()
+
+      // 等待摄像头详情抽屉打开
+      const cameraDrawer = page.locator('.el-drawer').filter({ hasText: '摄像头详情' })
+      await expect(cameraDrawer).toBeVisible({ timeout: 5000 })
+
+      // 验证标题是"摄像头详情"而非"编辑摄像头"
+      await expect(cameraDrawer.locator('.el-drawer__header, .el-drawer__title')).toContainText('摄像头详情')
+    }
+  })
+
+  /**
+   * Bug #4570: 摄像头详情增加"添加时间"字段
+   */
+  test('Bug #4570 - 摄像头详情显示添加时间', async ({ page }) => {
+    await login(page)
+
+    // Mock 摄像头详情 API 返回带有 createdAt 的数据
+    await page.route('**/admin/camera/get*', async (route) => {
+      await route.fulfill({
+        status: 200,
+        contentType: 'application/json',
+        body: JSON.stringify({
+          success: true,
+          errCode: 0,
+          data: {
+            id: 1,
+            cameraId: 'CAM_TEST_001',
+            cameraName: '测试摄像头',
+            lssId: 'LSS_001',
+            status: 'active',
+            vendorName: 'hikvision',
+            model: 'DS-2CD2043G0-I',
+            createdAt: '2024-01-15T10:30:00Z'
+          }
+        })
+      })
+    })
+
+    await page.goto('/lss')
+    await page.waitForTimeout(1000)
+
+    // 点击编辑按钮进入 LSS 编辑抽屉
+    const editButton = page.locator('tbody tr').first().locator('button').nth(1)
+    await expect(editButton).toBeVisible({ timeout: 10000 })
+    await editButton.click()
+
+    // 等待 LSS 编辑抽屉打开
+    const drawer = page.locator('.el-drawer').filter({ hasText: 'LSS详情' })
+    await expect(drawer).toBeVisible({ timeout: 5000 })
+
+    // 点击"摄像头列表" Tab
+    await drawer.locator('.el-tabs__item').filter({ hasText: '摄像头列表' }).click()
+    await page.waitForTimeout(500)
+
+    // 点击摄像头编辑按钮
+    const cameraEditButton = drawer.locator('tbody tr').first().locator('button').first()
+    if ((await cameraEditButton.count()) > 0) {
+      await cameraEditButton.click()
+
+      // 等待摄像头详情抽屉打开
+      const cameraDrawer = page.locator('.el-drawer').filter({ hasText: '摄像头详情' })
+      await expect(cameraDrawer).toBeVisible({ timeout: 5000 })
+
+      // 验证有"添加时间"字段
+      await expect(cameraDrawer.locator('label:has-text("添加时间")')).toBeVisible()
+
+      // 验证添加时间值显示正确格式
+      const timeValue = cameraDrawer.locator('label:has-text("添加时间")').locator('..').locator('.form-value')
+      const timeText = await timeValue.textContent()
+      // 验证时间格式或包含日期
+      expect(timeText).toMatch(/\d{4}-\d{2}-\d{2}|\-/)
+    }
+  })
+})