29 Commits 87fd3805aa ... f7aa12aa51

Autore SHA1 Messaggio Data
  yb f7aa12aa51 update code 6 giorni fa
  yb 2e3b0a0460 feat(locales): update English and Chinese translations with new terms 3 giorni fa
  yb 1339f2daf0 feat(system): add user and role management features 3 giorni fa
  yb b2e24ed96b refactor(views): streamline input and drawer components in lss/index.vue 3 giorni fa
  yb 9899a67b59 refactor(views): enhance input and table component consistency in live-stream and lss views 3 giorni fa
  yb 200362ef90 refactor(views): streamline input components and enhance layout consistency in live-stream and lss views 3 giorni fa
  yb 0c1cf6e2cc refactor(views): optimize input and drawer components in lss/index.vue 3 giorni fa
  yb 7a5c8d0baf refactor(views): optimize input and drawer components in lss/index.vue 3 giorni fa
  yb 6955820c69 refactor(views): streamline input and drawer components in lss/index.vue 3 giorni fa
  yb 19950cc0c9 refactor(views): streamline input components and enhance layout consistency 3 giorni fa
  yb 7894e642e0 refactor(styles, views): enhance drawer component styling and streamline input layouts 3 giorni fa
  yb 717f680af6 update code 3 giorni fa
  yb a971285085 refactor(styles, views): improve component structure and styling consistency 3 giorni fa
  yb 461f2bae83 refactor(layout, views): enhance component structure and readability 3 giorni fa
  yb 98ce548194 refactor(layout): improve sidebar structure and icon consistency 3 giorni fa
  yb 964d2b4dce refactor(layout, views): streamline sidebar and button components 3 giorni fa
  yb b3c611c306 feat(locales, views): add new translations and update demo view 3 giorni fa
  yb 2a0cfde5b8 feat(locales, views): update localization and enhance demo view 3 giorni fa
  yb 25cbb52178 chore: bump version to 1.0.28 for release v1.0.28 3 giorni fa
  yb 7981af21c3 feat(utils): update time formatting to use user's local timezone 3 giorni fa
  yb 76c21a7840 feat(ui, routing, tests): enhance UI components and update routing structure 3 giorni fa
  yb 7dace4ef1c feat(locales, views): enhance localization and UI consistency 3 giorni fa
  yb a6556b789c feat(bug-issues): add new bug tracking CSV for tg-live-game 3 giorni fa
  yb 420c85fc1c chore: bump version to 1.0.27 for release v1.0.27 3 giorni fa
  yb 3082f7d0fe feat(locales, utils, views): add time formatting utility and enhance localization 3 giorni fa
  yb 625cf1bc93 update code 3 giorni fa
  yb 05aef8ecbb change color 3 giorni fa
  yb b5a97c0c43 add crypto 3 giorni fa
  yb 87fd3805aa update code 6 giorni fa

+ 91 - 0
.cursorignore

@@ -0,0 +1,91 @@
+# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)
+
+# ============================================
+# 构建产物和依赖
+# ============================================
+node_modules/
+dist/
+dist-dev/
+dist-ssr/
+out-tsc/
+coverage/
+tmp/
+
+# ============================================
+# 锁文件和包管理
+# ============================================
+pnpm-lock.yaml
+package-lock.json
+yarn.lock
+
+# ============================================
+# 日志文件
+# ============================================
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+# ============================================
+# 自动生成的文件
+# ============================================
+types/auto-imports.d.ts
+types/components.d.ts
+*.local
+
+# ============================================
+# 大型二进制文件
+# ============================================
+*.wasm
+*.zip
+*.tar.gz
+
+# ============================================
+# 测试和覆盖率
+# ============================================
+/cypress/videos/
+/cypress/screenshots/
+coverage/
+
+# ============================================
+# CI/CD 和部署脚本(可选,如果不需要 AI 理解这些)
+# ============================================
+jenkins/
+Jenkinsfile
+*.sh
+
+# ============================================
+# 编辑器配置
+# ============================================
+.vscode/*
+!.vscode/extensions.json
+.idea/
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+# ============================================
+# 环境变量和敏感文件
+# ============================================
+.env
+.env.local
+.env.*.local
+
+# ============================================
+# 缓存目录
+# ============================================
+.cache/
+.wrangler/
+.DS_Store
+
+# ============================================
+# AI 工具自身文件
+# ============================================
+.cursor/
+.cursorrules*
+.claude/
+.crush/

+ 112 - 0
.cz-config.js

@@ -0,0 +1,112 @@
+module.exports = {
+  // type 类型(定义之后,可通过上下键选择)
+  types: [
+    {
+      value: 'feat',
+      name: 'feat:     新增功能'
+    },
+    {
+      value: 'fix',
+      name: 'fix:      修复 bug'
+    },
+    {
+      value: 'docs',
+      name: 'docs:     文档变更'
+    },
+    {
+      value: 'style',
+      name: 'style:    代码格式(不影响功能,例如空格、分号等格式修正)'
+    },
+    {
+      value: 'refactor',
+      name: 'refactor:    代码重构(不包括 bug 修复、功能新增)'
+    },
+    {
+      value: 'perf',
+      name: 'perf:     性能优化'
+    },
+    {
+      value: 'test',
+      name: 'test:     添加、修改测试用例'
+    },
+    {
+      value: 'build',
+      name: 'build:    构建流程、外部依赖变更(如升级 npm 包、修改 webpack 配置等)'
+    },
+    {
+      value: 'ci',
+      name: 'ci:       修改 CI 配置、脚本'
+    },
+    {
+      value: 'chore',
+      name: 'chore:    对构建过程或辅助工具和库的更改(不影响源文件、测试用例)'
+    },
+    {
+      value: 'revert',
+      name: 'revert:   回滚 commit'
+    }
+  ],
+
+  // scope 类型(定义之后,可通过上下键选择)
+  scopes: [
+    ['components', '组件相关'],
+    ['hooks', 'hook 相关'],
+    ['utils', 'utils 相关'],
+    ['element-plus', '对 element-plus 的调整'],
+    ['styles', '样式相关'],
+    ['deps', '项目依赖'],
+    ['auth', '对 auth 修改'],
+    ['other', '其他修改'],
+    // 如果选择 custom,后面会让你再输入一个自定义的 scope。也可以不设置此项,把后面的 allowCustomScopes 设置为 true
+    ['custom', '以上都不是?我要自定义']
+  ].map(([value, description]) => {
+    return {
+      value,
+      name: `${value.padEnd(30)} (${description})`
+    }
+  }),
+
+  // 是否允许自定义填写 scope,在 scope 选择的时候,会有 empty 和 custom 可以选择。
+  // allowCustomScopes: true,
+
+  // allowTicketNumber: false,
+  // isTicketNumberRequired: false,
+  // ticketNumberPrefix: 'TICKET-',
+  // ticketNumberRegExp: '\\d{1,5}',
+
+  // 针对每一个 type 去定义对应的 scopes,例如 fix
+  /*
+  scopeOverrides: {
+    fix: [
+      { name: 'merge' },
+      { name: 'style' },
+      { name: 'e2eTest' },
+      { name: 'unitTest' }
+    ]
+  },
+  */
+
+  // 交互提示信息
+  messages: {
+    type: '确保本次提交遵循 Angular 规范!\n选择你要提交的类型:',
+    scope: '\n选择一个 scope(可选):',
+    // 选择 scope: custom 时会出下面的提示
+    customScope: '请输入自定义的 scope:',
+    subject: '填写简短精炼的变更描述:\n',
+    body: '填写更加详细的变更描述(可选)。使用 "|" 换行:\n',
+    breaking: '列举非兼容性重大的变更(可选):\n',
+    footer: '列举出所有变更的 ISSUES CLOSED(可选)。 例如: #31, #34:\n',
+    confirmCommit: '确认提交?'
+  },
+
+  // 设置只有 type 选择了 feat 或 fix,才询问 breaking message
+  allowBreakingChanges: ['feat', 'fix'],
+
+  // 跳过要询问的步骤
+  // skipQuestions: ['body', 'footer'],
+
+  subjectLimit: 100, // subject 限制长度
+  breaklineChar: '|' // 换行符,支持 body 和 footer
+  // footerPrefix : 'ISSUES CLOSED:'
+  // askForBreakingChangeFirst : true,
+}

+ 38 - 0
bug-issues/tg-live-game-Bug.csv

@@ -0,0 +1,38 @@
+"Bug编号","所属产品","所属模块","所属项目","所属执行","相关需求","相关任务","Bug标题","关键词","严重程度","优先级","Bug类型","操作系统","浏览器","重现步骤","Bug状态","截止日期","激活次数","是否确认","抄送给","由谁创建","创建日期","影响版本","指派给","指派日期","解决者","解决方案","解决版本","解决日期","由谁关闭","关闭日期","重复Bug","相关Bug","相关用例","最后修改者","修改日期","附件","反馈者","通知邮箱"
+"4535","tg-live-game(#17)","/LSS管理/LSS列表(#566)","0","0","0","(#0)","【BUG】「重置」按钮点击后「LSS ID」查询匡和「名称」查询匡没有清空","","1(#1)","1(#1)","代码错误(#codeerror)","","","[步骤][结果]按钮点击后「LSS ID」查询匡和「名称」查询匡没有清空[期望]按钮点击后「LSS ID」查询匡和「名称」查询匡清空","激活(#active)","","0","未确认(#0)","","黄圣翔","2026-01-23 09:20:47","主干(#trunk)","yibu","2026-01-23 09:20:48","","","","","","","0","","(#0)","黄圣翔","2026-01-23 10:51:16","","","",""
+"4536","tg-live-game(#17)","/LSS管理/LSS列表(#566)","0","0","0","(#0)","【BUG】List表头状态 「活跃」⇨「active」","","3(#3)","3(#3)","代码错误(#codeerror)","","","[步骤][结果]<img src=&quot;{7148.png}&quot; alt=&quot;file-read-7148.png&quot; />[期望]List表头心跳 「活跃」⇨「active」","激活(#active)","","0","未确认(#0)","","黄圣翔","2026-01-23 09:23:00","主干(#trunk)","yibu","2026-01-23 09:23:00","","","","","","","0","","(#0)","黄圣翔","2026-01-23 13:15:04","","","",""
+"4537","tg-live-game(#17)","/LSS管理/LSS列表(#566)","0","0","0","(#0)","【BUG】上方标题「LSS管理」⇨「LSS列表」靠左","","3(#3)","3(#3)","代码错误(#codeerror)","","","[步骤][结果]<img src=&quot;{7149.png}&quot; alt=&quot;file-read-7149.png&quot; />[期望]上方标题「LSS管理」⇨「LSS列表」靠左参考 PP","激活(#active)","","0","未确认(#0)","","黄圣翔","2026-01-23 09:31:15","主干(#trunk)","yibu","2026-01-23 09:31:15","","","","","","","0","","(#0)","","","","","",""
+"4538","tg-live-game(#17)","/LSS管理/LSS列表(#566)","0","0","0","(#0)","【BUG】下方换页预设「20条/页」⇨「15条/页」","","3(#3)","3(#3)","代码错误(#codeerror)","","","[步骤][结果][期望]下方换页预设「20条/页」⇨「15条/页」","激活(#active)","","0","未确认(#0)","","黄圣翔","2026-01-23 09:33:41","主干(#trunk)","yibu","2026-01-23 09:33:41","","","","","","","0","","(#0)","","","","","",""
+"4540","tg-live-game(#17)","/LSS管理/LSS列表(#566)","0","0","0","(#0)","【界面优化】查询button一率使用蓝底(409EFF)白字","","3(#3)","3(#3)","代码错误(#codeerror)","","","[步骤][结果][期望]查询button一率使用蓝底(409EFF)白字","激活(#active)","","0","未确认(#0)","","黄圣翔","2026-01-23 10:29:45","主干(#trunk)","yibu","2026-01-23 10:29:45","","","","","","","0","","(#0)","","","","","",""
+"4541","tg-live-game(#17)","/LSS管理/LSS列表(#566)","0","0","0","(#0)","【界面优化】重置button一率使用灰底(909399)白字","","3(#3)","3(#3)","代码错误(#codeerror)","","","[步骤][结果][期望]重置button一率使用灰底(909399)白字","激活(#active)","","0","未确认(#0)","","黄圣翔","2026-01-23 10:30:04","主干(#trunk)","yibu","2026-01-23 10:30:04","","","","","","","0","","(#0)","","","","","",""
+"4542","tg-live-game(#17)","/LSS管理/LSS列表(#566)","0","0","0","(#0)","【界面优化】下方换页的当前页背景色使用蓝底(409EFF)白字","","3(#3)","3(#3)","代码错误(#codeerror)","","","[步骤][结果]<img src=&quot;{7155.png}&quot; alt=&quot;file-read-7155.png&quot; />[期望]下方换页的当前页背景色使用蓝底(409EFF)白字","激活(#active)","","0","未确认(#0)","","黄圣翔","2026-01-23 10:31:44","主干(#trunk)","yibu","2026-01-23 10:31:44","","","","","","","0","","(#0)","","","","","",""
+"4545","tg-live-game(#17)","/LSS管理/LSS列表(#566)","0","0","0","(#0)","【界面优化】按钮大小统一,圆角匡","","3(#3)","3(#3)","代码错误(#codeerror)","","","[步骤][结果]<img src=&quot;{7157.png}&quot; alt=&quot;file-read-7157.png&quot; />[期望]按钮大小统一,圆角匡","激活(#active)","","0","未确认(#0)","","黄圣翔","2026-01-23 10:40:53","主干(#trunk)","yibu","2026-01-23 10:40:53","","","","","","","0","","(#0)","","","","","",""
+"4549","tg-live-game(#17)","/LSS管理/LSS列表(#566)","0","0","0","(#0)","【需求新增】该页面为左方Menu 「LSS 管理」的子级「LSS列表」","","2(#2)","2(#2)","代码错误(#codeerror)","","","[步骤][结果]<img src=&quot;{7161.png}&quot; alt=&quot;file-read-7161.png&quot; />[期望]该页面为左方Menu 「LSS 管理」的子级「LSS列表」","激活(#active)","","0","未确认(#0)","","黄圣翔","2026-01-23 10:50:13","主干(#trunk)","yibu","2026-01-23 10:50:13","","","","","","","0","","(#0)","黄圣翔","2026-01-23 10:50:28","","","",""
+"4551","tg-live-game(#17)","/LSS管理/LSS列表(#566)","0","0","0","(#0)","【BUG】LSS详情,项目名以&quot;:&quot;对齐","","3(#3)","3(#3)","代码错误(#codeerror)","","","[步骤][结果]<img src=&quot;{7164.png}&quot; alt=&quot;file-read-7164.png&quot; />[期望]项目名以&quot;:&quot;对齐","激活(#active)","","0","未确认(#0)","","黄圣翔","2026-01-23 10:54:42","主干(#trunk)","yibu","2026-01-23 10:54:42","","","","","","","0","","(#0)","","","","","",""
+"4552","tg-live-game(#17)","/LSS管理/LSS列表(#566)","0","0","0","(#0)","【界面优化】LSS详情,更新button一率使用蓝底(409EFF)白字","","3(#3)","3(#3)","代码错误(#codeerror)","","","[步骤][结果][期望]更新button一率使用蓝底(409EFF)白字","激活(#active)","","0","未确认(#0)","","黄圣翔","2026-01-23 10:56:52","主干(#trunk)","yibu","2026-01-23 10:56:52","","","","","","","0","","(#0)","","","","","",""
+"4554","tg-live-game(#17)","/LSS管理/LSS列表(#566)","0","0","0","(#0)","【BUG】摄像头列表,「IP / 设备ID / 名称」查询匡⇨「设备ID」查询匡","","1(#1)","1(#1)","代码错误(#codeerror)","","","[步骤][结果][期望]「IP / 设备ID / 名称」查询匡⇨「设备ID」查询匡","激活(#active)","","0","未确认(#0)","","黄圣翔","2026-01-23 11:10:13","主干(#trunk)","yibu","2026-01-23 11:10:13","","","","","","","0","","(#0)","","","","","",""
+"4555","tg-live-game(#17)","/LSS管理/LSS列表(#566)","0","0","0","(#0)","【BUG】摄像头列表,缺少「设备名称」查询匡","","1(#1)","1(#1)","代码错误(#codeerror)","","","[步骤][结果][期望]缺少「设备名称」查询匡,位于「设备ID」查询匡右侧,「心跳」下拉匡左侧","激活(#active)","","0","未确认(#0)","","黄圣翔","2026-01-23 11:13:15","主干(#trunk)","yibu","2026-01-23 11:13:51","","","","","","","0","","(#0)","黄圣翔","2026-01-23 11:13:51","","","",""
+"4557","tg-live-game(#17)","/LSS管理/LSS列表(#566)","0","0","0","(#0)","【BUG】摄像头列表,「状态」下拉匡内容上到下排序「 &quot;全部&quot; &quot;在线&quot; &quot;离线&quot; 」 ⇨ 「 &quot;全部&quot; &quot;active&quot; &quot;hold&quot; &quot;dead&quot; 」","","3(#3)","3(#3)","代码错误(#codeerror)","","","[步骤][结果][期望]「状态」下拉匡内容上到下排序「 &quot;全部&quot; &quot;在线&quot; &quot;离线&quot; 」 ⇨ 「 &quot;全部&quot; &quot;active&quot; &quot;hold&quot; &quot;dead&quot; 」","激活(#active)","","0","未确认(#0)","","黄圣翔","2026-01-23 11:16:29","主干(#trunk)","yibu","2026-01-23 11:16:29","","","","","","","0","","(#0)","黄圣翔","2026-01-23 13:01:28","","","",""
+"4558","tg-live-game(#17)","/LSS管理/LSS列表(#566)","0","0","0","(#0)","【BUG】摄像头列表,没有数据时List表头要显示","","3(#3)","3(#3)","代码错误(#codeerror)","","","[步骤][结果]<img src=&quot;{7171.png}&quot; alt=&quot;file-read-7171.png&quot; />[期望]没有数据时List表头要显示","激活(#active)","","0","未确认(#0)","","黄圣翔","2026-01-23 11:17:55","主干(#trunk)","yibu","2026-01-23 11:17:55","","","","","","","0","","(#0)","","","","","",""
+"4559","tg-live-game(#17)","/LSS管理/LSS列表(#566)","0","0","0","(#0)","【界面优化】摄像头列表,更新button一率使用蓝底(409EFF)白字","","3(#3)","3(#3)","代码错误(#codeerror)","","","[步骤][结果][期望]更新button一率使用蓝底(409EFF)白字","激活(#active)","","0","未确认(#0)","","黄圣翔","2026-01-23 11:18:58","主干(#trunk)","yibu","2026-01-23 11:18:58","","","","","","","0","","(#0)","","","","","",""
+"4560","tg-live-game(#17)","/LSS管理/LSS列表(#566)","0","0","0","(#0)","【界面优化】摄像头列表,「新增」按钮位子在「重置」按钮右侧旁边","","3(#3)","3(#3)","代码错误(#codeerror)","","","[步骤][结果]<img src=&quot;{7172.png}&quot; alt=&quot;file-read-7172.png&quot; />[期望]「新增」按钮位子在「重置」按钮右侧旁边","激活(#active)","","0","未确认(#0)","","黄圣翔","2026-01-23 11:21:14","主干(#trunk)","yibu","2026-01-23 11:21:14","","","","","","","0","","(#0)","","","","","",""
+"4561","tg-live-game(#17)","/LSS管理/LSS列表(#566)","0","0","0","(#0)","【BUG】摄像头列表,删除右下「取消」「更新」按钮","","3(#3)","3(#3)","代码错误(#codeerror)","","","[步骤][结果]<img src=&quot;{7177.png}&quot; alt=&quot;file-read-7177.png&quot; />[期望]删除右下「取消」「更新」按钮","激活(#active)","","0","未确认(#0)","","黄圣翔","2026-01-23 12:35:13","主干(#trunk)","yibu","2026-01-23 12:35:14","","","","","","","0","","(#0)","","","","","",""
+"4562","tg-live-game(#17)","/LSS管理/LSS列表(#566)","0","0","0","(#0)","【BUG】摄像头列表,缺少下方换页功能","","3(#3)","3(#3)","代码错误(#codeerror)","","","[步骤][结果][期望]缺少下方换页功能","激活(#active)","","0","未确认(#0)","","黄圣翔","2026-01-23 12:37:44","主干(#trunk)","yibu","2026-01-23 12:37:44","","","","","","","0","","(#0)","","","","","",""
+"4567","tg-live-game(#17)","/LSS管理/LSS列表(#566)","0","0","0","(#0)","【BUG】List「厂商」没有显示和「摄像头详情」页一样的值","","3(#3)","3(#3)","代码错误(#codeerror)","","","[步骤][结果]<img src=&quot;{7182.png}&quot; alt=&quot;file-read-7182.png&quot; /><img src=&quot;{7183.png}&quot; alt=&quot;file-read-7183.png&quot; />[期望]List「厂商」没有显示和「摄像头详情」页一样的值","激活(#active)","","0","未确认(#0)","","黄圣翔","2026-01-23 13:24:36","主干(#trunk)","yibu","2026-01-23 16:47:45","","","","","","","0","","(#0)","黄圣翔","2026-01-23 16:47:45","","","",""
+"4569","tg-live-game(#17)","/LSS管理/LSS列表(#566)","0","0","0","(#0)","【BUG】编辑摄像头,「编辑摄像头」⇨「摄像头详情」","","3(#3)","3(#3)","代码错误(#codeerror)","","","[步骤][结果][期望]「编辑摄像头」⇨「摄像头详情」","激活(#active)","","0","未确认(#0)","","黄圣翔","2026-01-23 13:28:51","主干(#trunk)","yibu","2026-01-23 13:28:51","","","","","","","0","","(#0)","","","","","",""
+"4570","tg-live-game(#17)","/LSS管理/LSS列表(#566)","0","0","0","(#0)","【BUG】摄像头详情,缺少「添加时间:yy-mm-dd hh:mm:ss」","","1(#1)","1(#1)","代码错误(#codeerror)","","","[步骤][结果]缺少「添加时间:yy-mm-dd hh:mm:ss」<img src=&quot;{7184.png}&quot; alt=&quot;file-read-7184.png&quot; />[期望]追加「添加时间:yy-mm-dd hh:mm:ss」","激活(#active)","","0","未确认(#0)","","黄圣翔","2026-01-23 13:33:10","主干(#trunk)","yibu","2026-01-23 13:33:10","","","","","","","0","","(#0)","","","","","",""
+"4578","tg-live-game(#17)","/LSS管理/LSS列表(#566)","0","0","0","(#0)","【界面优化】摄像头详情,右下角的确定按钮,一率使用蓝底(409EFF)白字并且名称为「更新」,无需图样","","3(#3)","3(#3)","代码错误(#codeerror)","","","[步骤][结果][期望]右下角的确定按钮,一率使用蓝底(409EFF)白字并且名称为「更新」,无需图样","激活(#active)","","0","未确认(#0)","","黄圣翔","2026-01-23 14:55:46","主干(#trunk)","yibu","2026-01-23 14:55:46","","","","","","","0","","(#0)","","","","","",""
+"4579","tg-live-game(#17)","/LSS管理/LSS列表(#566)","0","0","0","(#0)","【界面优化】摄像头详情,右上角的「X」,一率移除,直接使用「取消」或是ESC","","3(#3)","3(#3)","代码错误(#codeerror)","","","[步骤][结果][期望]摄像头详情,右上角的「X」,一率移除,直接使用「取消」或是ESC","激活(#active)","","0","未确认(#0)","","黄圣翔","2026-01-23 14:58:59","主干(#trunk)","yibu","2026-01-23 14:58:59","","","","","","","0","","(#0)","","","","","",""
+"4582","tg-live-game(#17)","/LSS管理/LSS列表(#566)","0","0","0","(#0)","【BUG】摄像头详情,各个项目缺少 &quot;:&quot;","","3(#3)","3(#3)","代码错误(#codeerror)","","","[步骤][结果]<img src=&quot;{7203.png}&quot; alt=&quot;file-read-7203.png&quot; />[期望]各个项目缺少 &quot;:&quot;","激活(#active)","","0","未确认(#0)","","黄圣翔","2026-01-23 15:04:14","主干(#trunk)","yibu","2026-01-23 15:04:14","","","","","","","0","","(#0)","","","","","",""
+"4583","tg-live-game(#17)","/LSS管理/LSS列表(#566)","0","0","0","(#0)","【产品确认】摄像头列表,「名称」查询匡 ⇨「设备名称」查询匡","","3(#3)","3(#3)","代码错误(#codeerror)","","","[步骤][结果][期望]「名称」查询匡 ⇨「设备名称」查询匡","已解决(#resolved)","","0","已确认(#1)","","黄圣翔","2026-01-23 15:07:49","主干(#trunk)","黄圣翔","2026-01-26 13:33:00","allen","已解决(#fixed)","主干(#trunk)","2026-01-26 00:00:00","","","0","","(#0)","allen","2026-01-26 13:33:00","","","",""
+"4585","tg-live-game(#17)","/LSS管理/LSS列表(#566)","0","0","0","(#0)","【产品确认】List表头「名称」⇨「设备名称」","","3(#3)","3(#3)","代码错误(#codeerror)","","","[步骤][结果][期望]List表头「名称」⇨「设备名称」","已解决(#resolved)","","0","已确认(#1)","","黄圣翔","2026-01-23 15:09:13","主干(#trunk)","黄圣翔","2026-01-26 13:33:22","allen","已解决(#fixed)","主干(#trunk)","2026-01-26 00:00:00","","","0","","(#0)","allen","2026-01-26 13:33:22","","","",""
+"4587","tg-live-game(#17)","/LSS管理/LSS列表(#566)","0","0","0","(#0)","【界面优化】参数配置,右上角的「X」,一率移除,直接使用「取消」或是ESC","","3(#3)","3(#3)","代码错误(#codeerror)","","","[步骤][结果][期望]右上角的「X」,一率移除,直接使用「取消」或是ESC","激活(#active)","","0","未确认(#0)","","黄圣翔","2026-01-23 15:13:53","主干(#trunk)","yibu","2026-01-23 15:13:53","","","","","","","0","","(#0)","","","","","",""
+"4588","tg-live-game(#17)","/LSS管理/LSS列表(#566)","0","0","0","(#0)","【界面优化】参数配置,更新button一率使用蓝底(409EFF)白字","","3(#3)","3(#3)","代码错误(#codeerror)","","","[步骤][结果][期望]更新button一率使用蓝底(409EFF)白字","激活(#active)","","0","未确认(#0)","","黄圣翔","2026-01-23 15:20:32","主干(#trunk)","yibu","2026-01-23 15:20:32","","","","","","","0","","(#0)","","","","","",""
+"4593","tg-live-game(#17)","/LSS管理/LSS列表(#566)","0","0","0","(#0)","【BUG】删除提示,「 &quot;undefined&quot; 」⇨「 &quot;(设备名称)&quot; 」","","3(#3)","3(#3)","代码错误(#codeerror)","","","[步骤][结果]<img src=&quot;{7210.png}&quot; alt=&quot;file-read-7210.png&quot; />[期望]「 &quot;undefined&quot; 」⇨「 &quot;(设备名称)&quot; 」","激活(#active)","","0","未确认(#0)","","黄圣翔","2026-01-23 15:36:40","主干(#trunk)","yibu","2026-01-23 15:36:40","","","","","","","0","","(#0)","","","","","",""
+"4594","tg-live-game(#17)","/LSS管理/LSS列表(#566)","0","0","0","(#0)","【界面优化】删除提示,「确定」使用蓝底(409EFF)白字","","3(#3)","3(#3)","代码错误(#codeerror)","","","[步骤][结果]<img src=&quot;{7211.png}&quot; alt=&quot;file-read-7211.png&quot; />[期望]「确定」使用蓝底(409EFF)白字","激活(#active)","","0","未确认(#0)","","黄圣翔","2026-01-23 15:38:11","主干(#trunk)","yibu","2026-01-23 15:38:11","","","","","","","0","","(#0)","","","","","",""
+"4624","tg-live-game(#17)","/系统管理/帐号管理(#569)","0","0","0","(#0)","【需求新增】新增「账号管理」页面","","1(#1)","1(#1)","代码错误(#codeerror)","","","[期望]新增「账号管理」页面参考 PP<img src=&quot;{7271.png}&quot; alt=&quot;file-read-7271.png&quot; />","激活(#active)","","0","未确认(#0)","","黄圣翔","2026-01-26 13:01:40","主干(#trunk)","yo","2026-01-26 13:32:48","","","","","","","0","","(#0)","yo","2026-01-26 13:32:48","","","",""
+"4626","tg-live-game(#17)","/系统管理/角色管理(#570)","0","0","0","(#0)","【需求新增】新增「角色管理」页面","","1(#1)","1(#1)","代码错误(#codeerror)","","","[期望]新增「角色管理」页面参考 PP<img src=&quot;{7273.png}&quot; alt=&quot;file-read-7273.png&quot; />","激活(#active)","","0","未确认(#0)","","黄圣翔","2026-01-26 13:03:32","主干(#trunk)","yo","2026-01-26 13:32:29","","","","","","","0","","(#0)","yo","2026-01-26 13:32:29","","","",""
+"4627","tg-live-game(#17)","/live-stream管理/live-stream列表(#567)","0","0","0","(#0)","【BUG】List表「启动时间」和「关闭时间」值显示不对","","3(#3)","3(#3)","代码错误(#codeerror)","","","[期望]「启动时间」和「关闭时间」显示用户当前时区<img src=&quot;{7276.png}&quot; alt=&quot;file-read-7276.png&quot; />","激活(#active)","","0","未确认(#0)","","黄圣翔","2026-01-26 13:11:42","主干(#trunk)","yo","2026-01-26 13:11:42","","","","","","","0","","(#0)","","","","","",""
+"4628","tg-live-game(#17)","/live-stream管理/live-stream列表(#567)","0","0","0","(#0)","【BUG】命令模板,「关闭」⇨「取消」","","3(#3)","3(#3)","代码错误(#codeerror)","","","[期望]「关闭」⇨「取消」<img src=&quot;{7277.png}&quot; alt=&quot;file-read-7277.png&quot; />","激活(#active)","","0","未确认(#0)","","黄圣翔","2026-01-26 13:12:55","主干(#trunk)","yibu","2026-01-26 13:12:55","","","","","","","0","","(#0)","","","","","",""
+"4629","tg-live-game(#17)","/live-stream管理/live-stream列表(#567)","0","0","0","(#0)","【BUG】命令模板,右到左抽屉显示","","3(#3)","3(#3)","代码错误(#codeerror)","","","[期望]右到左抽屉显示<img src=&quot;{7278.png}&quot; alt=&quot;file-read-7278.png&quot; />","激活(#active)","","0","未确认(#0)","","黄圣翔","2026-01-26 13:14:52","主干(#trunk)","yibu","2026-01-26 13:14:52","","","","","","","0","","(#0)","","","","","",""
+"4630","tg-live-game(#17)","/live-stream管理/live-stream列表(#567)","0","0","0","(#0)","【BUG】「name」查询匡⇨「名称」查询匡","","3(#3)","3(#3)","代码错误(#codeerror)","","","[期望]「name」查询匡⇨「名称」查询匡<img src=&quot;{7279.png}&quot; alt=&quot;file-read-7279.png&quot; />","激活(#active)","","0","未确认(#0)","","黄圣翔","2026-01-26 13:16:18","主干(#trunk)","yibu","2026-01-26 13:16:18","","","","","","","0","","(#0)","","","","","",""

+ 4 - 4
i18next-scanner.config.cjs

@@ -9,10 +9,10 @@ const typescriptTransform = require('i18next-scanner-typescript')
 
 
 module.exports = {
 module.exports = {
   input: [
   input: [
-    'src/**/*.{js,jsx,ts,tsx,vue}',
-    // 排除不需要扫描的目录
-    '!src/locales/**',
-    '!**/node_modules/**'
+    'src/**/*.{js,jsx,ts,tsx,vue}', //  扫描 js、jsx、ts,tsx, vue 文件
+    '!test/**/*.spec.{js,ts}', // 排除测试文件
+    '!src/locales/**', // 排除 locales 文件夹
+    '!**/node_modules/**' // 排除 node_modules
   ],
   ],
   output: './',
   output: './',
   options: {
   options: {

+ 8 - 0
index.html

@@ -9,5 +9,13 @@
   <body>
   <body>
     <div id="app"></div>
     <div id="app"></div>
     <script type="module" src="/src/main.ts"></script>
     <script type="module" src="/src/main.ts"></script>
+    <!-- 加解密-->
+    <script src="wasm_exec.js"></script>
+    <script>
+      const go = new Go()
+      WebAssembly.instantiateStreaming(fetch('./mimlib.wasm'), go.importObject).then((result) => {
+        go.run(result.instance)
+      })
+    </script>
   </body>
   </body>
 </html>
 </html>

+ 11 - 1
package.json

@@ -1,7 +1,7 @@
 {
 {
   "name": "tg-live-game-web",
   "name": "tg-live-game-web",
   "private": true,
   "private": true,
-  "version": "1.0.26",
+  "version": "1.0.28",
   "type": "module",
   "type": "module",
   "scripts": {
   "scripts": {
     "dev": "vite",
     "dev": "vite",
@@ -40,6 +40,7 @@
     "@vueuse/core": "^14.1.0",
     "@vueuse/core": "^14.1.0",
     "axios": "^1.4.0",
     "axios": "^1.4.0",
     "codemirror": "^6.0.2",
     "codemirror": "^6.0.2",
+    "cz-customizable": "^7.5.1",
     "date-fns": "^4.1.0",
     "date-fns": "^4.1.0",
     "date-fns-tz": "^3.2.0",
     "date-fns-tz": "^3.2.0",
     "dayjs": "^1.11.19",
     "dayjs": "^1.11.19",
@@ -73,11 +74,15 @@
     "@vitest/ui": "^4.0.16",
     "@vitest/ui": "^4.0.16",
     "@vue/test-utils": "^2.0.0-rc.4",
     "@vue/test-utils": "^2.0.0-rc.4",
     "@vue/tsconfig": "^0.4.0",
     "@vue/tsconfig": "^0.4.0",
+    "@vue/eslint-config-prettier": "^7.1.0",
+    "@vue/eslint-config-typescript": "^11.0.3",
     "eslint": "^8.57.1",
     "eslint": "^8.57.1",
     "eslint-config-airbnb-base": "^14.2.1",
     "eslint-config-airbnb-base": "^14.2.1",
     "eslint-config-prettier": "^8.1.0",
     "eslint-config-prettier": "^8.1.0",
     "eslint-plugin-import": "^2.22.1",
     "eslint-plugin-import": "^2.22.1",
+    "eslint-plugin-jest": "^28.11.0",
     "eslint-plugin-prettier": "^3.3.1",
     "eslint-plugin-prettier": "^3.3.1",
+    "eslint-plugin-unused-imports": "^4.1.4",
     "eslint-plugin-vue": "^9.11.0",
     "eslint-plugin-vue": "^9.11.0",
     "happy-dom": "^20.0.11",
     "happy-dom": "^20.0.11",
     "husky": "^6.0.0",
     "husky": "^6.0.0",
@@ -93,5 +98,10 @@
     "vite-plugin-vue-devtools": "^7.7.0",
     "vite-plugin-vue-devtools": "^7.7.0",
     "vitest": "^4.0.16",
     "vitest": "^4.0.16",
     "vue-tsc": "2.1.10"
     "vue-tsc": "2.1.10"
+  },
+  "config": {
+    "commitizen": {
+      "path": "./node_modules/cz-customizable"
+    }
   }
   }
 }
 }

File diff suppressed because it is too large
+ 462 - 0
pnpm-lock.yaml


BIN
public/mimlib.wasm


+ 44 - 0
public/robots.txt

@@ -0,0 +1,44 @@
+# Robots.txt for tk-super-admin
+# Prevent all web crawlers from accessing the site
+
+User-agent: *
+Disallow: /
+Crawl-delay: 86400
+
+# Block specific bots
+User-agent: Googlebot
+Disallow: /
+
+User-agent: Bingbot
+Disallow: /
+
+User-agent: Slurp
+Disallow: /
+
+User-agent: DuckDuckBot
+Disallow: /
+
+User-agent: Baiduspider
+Disallow: /
+
+User-agent: YandexBot
+Disallow: /
+
+# Block social media bots
+User-agent: facebookexternalhit
+Disallow: /
+
+User-agent: Twitterbot
+Disallow: /
+
+User-agent: LinkedInBot
+Disallow: /
+
+User-agent: WhatsApp
+Disallow: /
+
+User-agent: TelegramBot
+Disallow: /
+
+# No sitemap
+Sitemap:

+ 609 - 0
public/wasm_exec.js

@@ -0,0 +1,609 @@
+// Copyright 2018 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+//
+// This file has been modified for use by the TinyGo compiler.
+
+;(() => {
+  // Map multiple JavaScript environments to a single common API,
+  // preferring web standards over Node.js API.
+  //
+  // Environments considered:
+  // - Browsers
+  // - Node.js
+  // - Electron
+  // - Parcel
+
+  if (typeof global !== 'undefined') {
+    // global already exists
+  } else if (typeof window !== 'undefined') {
+    window.global = window
+  } else if (typeof self !== 'undefined') {
+    self.global = self
+  } else {
+    throw new Error('cannot export Go (neither global, window nor self is defined)')
+  }
+
+  if (!global.require && typeof require !== 'undefined') {
+    global.require = require
+  }
+
+  if (!global.fs && global.require) {
+    global.fs = require('fs')
+  }
+
+  const enosys = () => {
+    const err = new Error('not implemented')
+    err.code = 'ENOSYS'
+    return err
+  }
+
+  if (!global.fs) {
+    let outputBuf = ''
+    global.fs = {
+      constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
+      writeSync(fd, buf) {
+        outputBuf += decoder.decode(buf)
+        const nl = outputBuf.lastIndexOf('\n')
+        if (nl != -1) {
+          console.log(outputBuf.substr(0, nl))
+          outputBuf = outputBuf.substr(nl + 1)
+        }
+        return buf.length
+      },
+      write(fd, buf, offset, length, position, callback) {
+        if (offset !== 0 || length !== buf.length || position !== null) {
+          callback(enosys())
+          return
+        }
+        const n = this.writeSync(fd, buf)
+        callback(null, n)
+      },
+      chmod(path, mode, callback) {
+        callback(enosys())
+      },
+      chown(path, uid, gid, callback) {
+        callback(enosys())
+      },
+      close(fd, callback) {
+        callback(enosys())
+      },
+      fchmod(fd, mode, callback) {
+        callback(enosys())
+      },
+      fchown(fd, uid, gid, callback) {
+        callback(enosys())
+      },
+      fstat(fd, callback) {
+        callback(enosys())
+      },
+      fsync(fd, callback) {
+        callback(null)
+      },
+      ftruncate(fd, length, callback) {
+        callback(enosys())
+      },
+      lchown(path, uid, gid, callback) {
+        callback(enosys())
+      },
+      link(path, link, callback) {
+        callback(enosys())
+      },
+      lstat(path, callback) {
+        callback(enosys())
+      },
+      mkdir(path, perm, callback) {
+        callback(enosys())
+      },
+      open(path, flags, mode, callback) {
+        callback(enosys())
+      },
+      read(fd, buffer, offset, length, position, callback) {
+        callback(enosys())
+      },
+      readdir(path, callback) {
+        callback(enosys())
+      },
+      readlink(path, callback) {
+        callback(enosys())
+      },
+      rename(from, to, callback) {
+        callback(enosys())
+      },
+      rmdir(path, callback) {
+        callback(enosys())
+      },
+      stat(path, callback) {
+        callback(enosys())
+      },
+      symlink(path, link, callback) {
+        callback(enosys())
+      },
+      truncate(path, length, callback) {
+        callback(enosys())
+      },
+      unlink(path, callback) {
+        callback(enosys())
+      },
+      utimes(path, atime, mtime, callback) {
+        callback(enosys())
+      }
+    }
+  }
+
+  if (!global.process) {
+    global.process = {
+      getuid() {
+        return -1
+      },
+      getgid() {
+        return -1
+      },
+      geteuid() {
+        return -1
+      },
+      getegid() {
+        return -1
+      },
+      getgroups() {
+        throw enosys()
+      },
+      pid: -1,
+      ppid: -1,
+      umask() {
+        throw enosys()
+      },
+      cwd() {
+        throw enosys()
+      },
+      chdir() {
+        throw enosys()
+      }
+    }
+  }
+
+  if (!global.crypto) {
+    const nodeCrypto = require('crypto')
+    global.crypto = {
+      getRandomValues(b) {
+        nodeCrypto.randomFillSync(b)
+      }
+    }
+  }
+
+  if (!global.performance) {
+    global.performance = {
+      now() {
+        const [sec, nsec] = process.hrtime()
+        return sec * 1000 + nsec / 1000000
+      }
+    }
+  }
+
+  if (!global.TextEncoder) {
+    global.TextEncoder = require('util').TextEncoder
+  }
+
+  if (!global.TextDecoder) {
+    global.TextDecoder = require('util').TextDecoder
+  }
+
+  // End of polyfills for common API.
+
+  const encoder = new TextEncoder('utf-8')
+  const decoder = new TextDecoder('utf-8')
+  var logLine = []
+
+  global.Go = class {
+    constructor() {
+      this._callbackTimeouts = new Map()
+      this._nextCallbackTimeoutID = 1
+
+      const mem = () => {
+        // The buffer may change when requesting more memory.
+        return new DataView(this._inst.exports.memory.buffer)
+      }
+
+      const setInt64 = (addr, v) => {
+        mem().setUint32(addr + 0, v, true)
+        mem().setUint32(addr + 4, Math.floor(v / 4294967296), true)
+      }
+
+      const getInt64 = (addr) => {
+        const low = mem().getUint32(addr + 0, true)
+        const high = mem().getInt32(addr + 4, true)
+        return low + high * 4294967296
+      }
+
+      const loadValue = (addr) => {
+        const f = mem().getFloat64(addr, true)
+        if (f === 0) {
+          return undefined
+        }
+        if (!isNaN(f)) {
+          return f
+        }
+
+        const id = mem().getUint32(addr, true)
+        return this._values[id]
+      }
+
+      const storeValue = (addr, v) => {
+        const nanHead = 0x7ff80000
+
+        if (typeof v === 'number') {
+          if (isNaN(v)) {
+            mem().setUint32(addr + 4, nanHead, true)
+            mem().setUint32(addr, 0, true)
+            return
+          }
+          if (v === 0) {
+            mem().setUint32(addr + 4, nanHead, true)
+            mem().setUint32(addr, 1, true)
+            return
+          }
+          mem().setFloat64(addr, v, true)
+          return
+        }
+
+        switch (v) {
+          case undefined:
+            mem().setFloat64(addr, 0, true)
+            return
+          case null:
+            mem().setUint32(addr + 4, nanHead, true)
+            mem().setUint32(addr, 2, true)
+            return
+          case true:
+            mem().setUint32(addr + 4, nanHead, true)
+            mem().setUint32(addr, 3, true)
+            return
+          case false:
+            mem().setUint32(addr + 4, nanHead, true)
+            mem().setUint32(addr, 4, true)
+            return
+        }
+
+        let id = this._ids.get(v)
+        if (id === undefined) {
+          id = this._idPool.pop()
+          if (id === undefined) {
+            id = this._values.length
+          }
+          this._values[id] = v
+          this._goRefCounts[id] = 0
+          this._ids.set(v, id)
+        }
+        this._goRefCounts[id]++
+        let typeFlag = 1
+        switch (typeof v) {
+          case 'string':
+            typeFlag = 2
+            break
+          case 'symbol':
+            typeFlag = 3
+            break
+          case 'function':
+            typeFlag = 4
+            break
+        }
+        mem().setUint32(addr + 4, nanHead | typeFlag, true)
+        mem().setUint32(addr, id, true)
+      }
+
+      const loadSlice = (array, len, cap) => {
+        return new Uint8Array(this._inst.exports.memory.buffer, array, len)
+      }
+
+      const loadSliceOfValues = (array, len, cap) => {
+        const a = new Array(len)
+        for (let i = 0; i < len; i++) {
+          a[i] = loadValue(array + i * 8)
+        }
+        return a
+      }
+
+      const loadString = (ptr, len) => {
+        return decoder.decode(new DataView(this._inst.exports.memory.buffer, ptr, len))
+      }
+
+      const timeOrigin = Date.now() - performance.now()
+      this.importObject = {
+        wasi_snapshot_preview1: {
+          // https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_write
+          fd_write: function (fd, iovs_ptr, iovs_len, nwritten_ptr) {
+            let nwritten = 0
+            if (fd == 1) {
+              for (let iovs_i = 0; iovs_i < iovs_len; iovs_i++) {
+                let iov_ptr = iovs_ptr + iovs_i * 8 // assuming wasm32
+                let ptr = mem().getUint32(iov_ptr + 0, true)
+                let len = mem().getUint32(iov_ptr + 4, true)
+                nwritten += len
+                for (let i = 0; i < len; i++) {
+                  let c = mem().getUint8(ptr + i)
+                  if (c == 13) {
+                    // CR
+                    // ignore
+                  } else if (c == 10) {
+                    // LF
+                    // write line
+                    let line = decoder.decode(new Uint8Array(logLine))
+                    logLine = []
+                    console.log(line)
+                  } else {
+                    logLine.push(c)
+                  }
+                }
+              }
+            } else {
+              console.error('invalid file descriptor:', fd)
+            }
+            mem().setUint32(nwritten_ptr, nwritten, true)
+            return 0
+          },
+          fd_close: () => 0, // dummy
+          fd_fdstat_get: () => 0, // dummy
+          fd_seek: () => 0, // dummy
+          proc_exit: (code) => {
+            if (global.process) {
+              // Node.js
+              process.exit(code)
+            } else {
+              // Can't exit in a browser.
+              throw 'trying to exit with code ' + code
+            }
+          },
+          random_get: (bufPtr, bufLen) => {
+            crypto.getRandomValues(loadSlice(bufPtr, bufLen))
+            return 0
+          }
+        },
+        env: {
+          // func ticks() float64
+          'runtime.ticks': () => {
+            return timeOrigin + performance.now()
+          },
+
+          // func sleepTicks(timeout float64)
+          'runtime.sleepTicks': (timeout) => {
+            // Do not sleep, only reactivate scheduler after the given timeout.
+            setTimeout(this._inst.exports.go_scheduler, timeout)
+          },
+
+          // func finalizeRef(v ref)
+          'syscall/js.finalizeRef': (v_addr) => {
+            // Note: TinyGo does not support finalizers so this is only called
+            // for one specific case, by js.go:jsString.
+            const id = mem().getUint32(v_addr, true)
+            this._goRefCounts[id]--
+            if (this._goRefCounts[id] === 0) {
+              const v = this._values[id]
+              this._values[id] = null
+              this._ids.delete(v)
+              this._idPool.push(id)
+            }
+          },
+
+          // func stringVal(value string) ref
+          'syscall/js.stringVal': (ret_ptr, value_ptr, value_len) => {
+            const s = loadString(value_ptr, value_len)
+            storeValue(ret_ptr, s)
+          },
+
+          // func valueGet(v ref, p string) ref
+          'syscall/js.valueGet': (retval, v_addr, p_ptr, p_len) => {
+            let prop = loadString(p_ptr, p_len)
+            let value = loadValue(v_addr)
+            let result = Reflect.get(value, prop)
+            storeValue(retval, result)
+          },
+
+          // func valueSet(v ref, p string, x ref)
+          'syscall/js.valueSet': (v_addr, p_ptr, p_len, x_addr) => {
+            const v = loadValue(v_addr)
+            const p = loadString(p_ptr, p_len)
+            const x = loadValue(x_addr)
+            Reflect.set(v, p, x)
+          },
+
+          // func valueDelete(v ref, p string)
+          'syscall/js.valueDelete': (v_addr, p_ptr, p_len) => {
+            const v = loadValue(v_addr)
+            const p = loadString(p_ptr, p_len)
+            Reflect.deleteProperty(v, p)
+          },
+
+          // func valueIndex(v ref, i int) ref
+          'syscall/js.valueIndex': (ret_addr, v_addr, i) => {
+            storeValue(ret_addr, Reflect.get(loadValue(v_addr), i))
+          },
+
+          // valueSetIndex(v ref, i int, x ref)
+          'syscall/js.valueSetIndex': (v_addr, i, x_addr) => {
+            Reflect.set(loadValue(v_addr), i, loadValue(x_addr))
+          },
+
+          // func valueCall(v ref, m string, args []ref) (ref, bool)
+          'syscall/js.valueCall': (ret_addr, v_addr, m_ptr, m_len, args_ptr, args_len, args_cap) => {
+            const v = loadValue(v_addr)
+            const name = loadString(m_ptr, m_len)
+            const args = loadSliceOfValues(args_ptr, args_len, args_cap)
+            try {
+              const m = Reflect.get(v, name)
+              storeValue(ret_addr, Reflect.apply(m, v, args))
+              mem().setUint8(ret_addr + 8, 1)
+            } catch (err) {
+              storeValue(ret_addr, err)
+              mem().setUint8(ret_addr + 8, 0)
+            }
+          },
+
+          // func valueInvoke(v ref, args []ref) (ref, bool)
+          'syscall/js.valueInvoke': (ret_addr, v_addr, args_ptr, args_len, args_cap) => {
+            try {
+              const v = loadValue(v_addr)
+              const args = loadSliceOfValues(args_ptr, args_len, args_cap)
+              storeValue(ret_addr, Reflect.apply(v, undefined, args))
+              mem().setUint8(ret_addr + 8, 1)
+            } catch (err) {
+              storeValue(ret_addr, err)
+              mem().setUint8(ret_addr + 8, 0)
+            }
+          },
+
+          // func valueNew(v ref, args []ref) (ref, bool)
+          'syscall/js.valueNew': (ret_addr, v_addr, args_ptr, args_len, args_cap) => {
+            const v = loadValue(v_addr)
+            const args = loadSliceOfValues(args_ptr, args_len, args_cap)
+            try {
+              storeValue(ret_addr, Reflect.construct(v, args))
+              mem().setUint8(ret_addr + 8, 1)
+            } catch (err) {
+              storeValue(ret_addr, err)
+              mem().setUint8(ret_addr + 8, 0)
+            }
+          },
+
+          // func valueLength(v ref) int
+          'syscall/js.valueLength': (v_addr) => {
+            return loadValue(v_addr).length
+          },
+
+          // valuePrepareString(v ref) (ref, int)
+          'syscall/js.valuePrepareString': (ret_addr, v_addr) => {
+            const s = String(loadValue(v_addr))
+            const str = encoder.encode(s)
+            storeValue(ret_addr, str)
+            setInt64(ret_addr + 8, str.length)
+          },
+
+          // valueLoadString(v ref, b []byte)
+          'syscall/js.valueLoadString': (v_addr, slice_ptr, slice_len, slice_cap) => {
+            const str = loadValue(v_addr)
+            loadSlice(slice_ptr, slice_len, slice_cap).set(str)
+          },
+
+          // func valueInstanceOf(v ref, t ref) bool
+          'syscall/js.valueInstanceOf': (v_addr, t_addr) => {
+            return loadValue(v_addr) instanceof loadValue(t_addr)
+          },
+
+          // func copyBytesToGo(dst []byte, src ref) (int, bool)
+          'syscall/js.copyBytesToGo': (ret_addr, dest_addr, dest_len, dest_cap, source_addr) => {
+            let num_bytes_copied_addr = ret_addr
+            let returned_status_addr = ret_addr + 4 // Address of returned boolean status variable
+
+            const dst = loadSlice(dest_addr, dest_len)
+            const src = loadValue(source_addr)
+            if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
+              mem().setUint8(returned_status_addr, 0) // Return "not ok" status
+              return
+            }
+            const toCopy = src.subarray(0, dst.length)
+            dst.set(toCopy)
+            setInt64(num_bytes_copied_addr, toCopy.length)
+            mem().setUint8(returned_status_addr, 1) // Return "ok" status
+          },
+
+          // copyBytesToJS(dst ref, src []byte) (int, bool)
+          // Originally copied from upstream Go project, then modified:
+          //   https://github.com/golang/go/blob/3f995c3f3b43033013013e6c7ccc93a9b1411ca9/misc/wasm/wasm_exec.js#L404-L416
+          'syscall/js.copyBytesToJS': (ret_addr, dest_addr, source_addr, source_len, source_cap) => {
+            let num_bytes_copied_addr = ret_addr
+            let returned_status_addr = ret_addr + 4 // Address of returned boolean status variable
+
+            const dst = loadValue(dest_addr)
+            const src = loadSlice(source_addr, source_len)
+            if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
+              mem().setUint8(returned_status_addr, 0) // Return "not ok" status
+              return
+            }
+            const toCopy = src.subarray(0, dst.length)
+            dst.set(toCopy)
+            setInt64(num_bytes_copied_addr, toCopy.length)
+            mem().setUint8(returned_status_addr, 1) // Return "ok" status
+          }
+        }
+      }
+    }
+
+    async run(instance) {
+      this._inst = instance
+      this._values = [
+        // JS values that Go currently has references to, indexed by reference id
+        NaN,
+        0,
+        null,
+        true,
+        false,
+        global,
+        this
+      ]
+      this._goRefCounts = [] // number of references that Go has to a JS value, indexed by reference id
+      this._ids = new Map() // mapping from JS values to reference ids
+      this._idPool = [] // unused ids that have been garbage collected
+      this.exited = false // whether the Go program has exited
+
+      const mem = new DataView(this._inst.exports.memory.buffer)
+
+      while (true) {
+        const callbackPromise = new Promise((resolve) => {
+          this._resolveCallbackPromise = () => {
+            if (this.exited) {
+              throw new Error('bad callback: Go program has already exited')
+            }
+            setTimeout(resolve, 0) // make sure it is asynchronous
+          }
+        })
+        this._inst.exports._start()
+        if (this.exited) {
+          break
+        }
+        await callbackPromise
+      }
+    }
+
+    _resume() {
+      if (this.exited) {
+        throw new Error('Go program has already exited')
+      }
+      this._inst.exports.resume()
+      if (this.exited) {
+        this._resolveExitPromise()
+      }
+    }
+
+    _makeFuncWrapper(id) {
+      const go = this
+      return function () {
+        const event = { id: id, this: this, args: arguments }
+        go._pendingEvent = event
+        go._resume()
+        return event.result
+      }
+    }
+  }
+
+  if (
+    global.require &&
+    global.require.main === module &&
+    global.process &&
+    global.process.versions &&
+    !global.process.versions.electron
+  ) {
+    if (process.argv.length != 3) {
+      console.error('usage: go_js_wasm_exec [wasm binary] [arguments]')
+      process.exit(1)
+    }
+
+    const go = new Go()
+    WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject)
+      .then((result) => {
+        return go.run(result.instance)
+      })
+      .catch((err) => {
+        console.error(err)
+        process.exit(1)
+      })
+  }
+})()

+ 66 - 0
src/assets/styles/theme/element-override.scss

@@ -172,6 +172,50 @@
   .el-pager li {
   .el-pager li {
     border-radius: var(--radius-sm);
     border-radius: var(--radius-sm);
   }
   }
+
+  // #4542: 分页当前页使用蓝底白字
+  &.is-background {
+    .el-pager li:not(.is-disabled).is-active {
+      background-color: #409eff;
+      color: #ffffff;
+    }
+  }
+}
+
+// #4545: 按钮统一大小和圆角
+.el-button {
+  border-radius: 4px;
+  min-height: 32px;
+  padding: 8px 15px;
+  font-size: 14px;
+
+  &.el-button--small {
+    min-height: 24px;
+    padding: 5px 11px;
+    font-size: 12px;
+  }
+
+  &.el-button--large {
+    min-height: 40px;
+    padding: 12px 19px;
+    font-size: 14px;
+  }
+}
+
+// #4594: 删除确认对话框确定按钮蓝底白字
+.el-message-box {
+  .el-message-box__btns {
+    .el-button--primary {
+      background-color: #409eff !important;
+      border-color: #409eff !important;
+      color: #ffffff !important;
+
+      &:hover {
+        background-color: #66b1ff !important;
+        border-color: #66b1ff !important;
+      }
+    }
+  }
 }
 }
 
 
 // 对话框
 // 对话框
@@ -183,6 +227,28 @@
 // 抽屉
 // 抽屉
 .el-drawer {
 .el-drawer {
   --el-drawer-bg-color: var(--bg-container);
   --el-drawer-bg-color: var(--bg-container);
+
+  .el-drawer__header {
+    margin-bottom: 0;
+    padding: 16px 20px;
+    border-bottom: 1px solid #e5e7eb;
+  }
+
+  .el-drawer__body {
+    padding: 16px 0;
+  }
+
+  .el-descriptions {
+    .el-descriptions__label {
+      width: 100px;
+      font-weight: 600;
+    }
+  }
+}
+
+.el-drawer__footer {
+  padding-left: 0 !important;
+  padding-right: 0 !important;
 }
 }
 
 
 // 消息
 // 消息

+ 99 - 21
src/layout/index.vue

@@ -42,7 +42,7 @@
               :class="{ 'layout__nav-item--active': isGroupActive(item) }"
               :class="{ 'layout__nav-item--active': isGroupActive(item) }"
               @click="toggleSubMenu(item.path)"
               @click="toggleSubMenu(item.path)"
             >
             >
-              <Icon :icon="item.icon" class="layout__nav-icon" />
+              <Icon :icon="item.icon" width="20" height="20" class="layout__nav-icon" />
               <span v-show="sidebarOpened || isMobile">{{ t(item.title) }}</span>
               <span v-show="sidebarOpened || isMobile">{{ t(item.title) }}</span>
               <svg
               <svg
                 v-show="sidebarOpened || isMobile"
                 v-show="sidebarOpened || isMobile"
@@ -64,7 +64,7 @@
                 :class="{ 'layout__nav-item--active': isActive(child.path) }"
                 :class="{ 'layout__nav-item--active': isActive(child.path) }"
                 @click="isMobile && closeSidebar()"
                 @click="isMobile && closeSidebar()"
               >
               >
-                <Icon :icon="child.icon" class="layout__nav-icon" />
+                <Icon :icon="child.icon" width="20" height="20" class="layout__nav-icon" />
                 <span v-show="sidebarOpened || isMobile">{{ t(child.title) }}</span>
                 <span v-show="sidebarOpened || isMobile">{{ t(child.title) }}</span>
               </router-link>
               </router-link>
             </div>
             </div>
@@ -77,7 +77,7 @@
             :class="{ 'layout__nav-item--active': isActive(item.path) }"
             :class="{ 'layout__nav-item--active': isActive(item.path) }"
             @click="isMobile && closeSidebar()"
             @click="isMobile && closeSidebar()"
           >
           >
-            <Icon :icon="item.icon" class="layout__nav-icon" />
+            <Icon :icon="item.icon" width="20" height="20" class="layout__nav-icon" />
             <span v-show="sidebarOpened || isMobile">{{ t(item.title) }}</span>
             <span v-show="sidebarOpened || isMobile">{{ t(item.title) }}</span>
           </router-link>
           </router-link>
         </template>
         </template>
@@ -104,18 +104,32 @@
       <!-- Header -->
       <!-- Header -->
       <header class="layout__header">
       <header class="layout__header">
         <!-- Mobile menu button -->
         <!-- Mobile menu button -->
-        <button class="layout__menu-btn" @click="toggleSidebar">
-          <svg class="layout__icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
-          </svg>
-        </button>
-
-        <!-- Page Title / Breadcrumb -->
-        <div class="layout__breadcrumb">
-          <span v-for="(item, index) in breadcrumbs" :key="item.path" class="layout__breadcrumb-item">
-            <span v-if="index > 0" class="layout__breadcrumb-separator">/</span>
-            {{ t(item.meta?.title as string) }}
-          </span>
+        <div class="layout__header-left">
+          <!-- Mobile: Hamburger menu -->
+          <button class="layout__menu-btn layout__menu-btn--mobile" @click="toggleSidebar">
+            <svg class="layout__icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
+            </svg>
+          </button>
+
+          <!-- Desktop: Compact chevron toggle -->
+          <button link class="layout__menu-btn layout__menu-btn--desktop" @click="toggleSidebar">
+            <Icon
+              icon="rivet-icons:chevron-left"
+              width="20"
+              height="20"
+              class="layout__menu-icon"
+              :class="{ 'layout__menu-icon--collapsed': !sidebarOpened }"
+            />
+          </button>
+
+          <!-- Page Title / Breadcrumb -->
+          <div class="layout__breadcrumb">
+            <span v-for="(item, index) in breadcrumbs" :key="item.path" class="layout__breadcrumb-item">
+              <span v-if="index > 0" class="layout__breadcrumb-separator">/</span>
+              {{ t(item.meta?.title as string) }}
+            </span>
+          </div>
         </div>
         </div>
 
 
         <!-- Header Right -->
         <!-- Header Right -->
@@ -215,14 +229,33 @@ interface MenuItem {
 // Menu configuration with Iconify icon names
 // Menu configuration with Iconify icon names
 const menuItems: MenuItem[] = [
 const menuItems: MenuItem[] = [
   { path: '/', title: '仪表盘', icon: 'mdi:view-dashboard' },
   { path: '/', title: '仪表盘', icon: 'mdi:view-dashboard' },
-  { path: '/lss', title: 'LSS 管理', icon: 'mdi:power-plug' },
-  { path: '/live-stream', title: 'LiveStream 管理', icon: 'mdi:broadcast' },
+  {
+    path: '/lss-manage',
+    title: 'LSS 管理',
+    icon: 'mdi:connection',
+    children: [{ path: '/lss-manage/list', title: 'LSS 列表', icon: 'pixelarticons:list' }]
+  },
+  {
+    path: '/live-stream-manage',
+    title: 'LiveStream 管理',
+    icon: 'mdi:video-wireless',
+    children: [{ path: '/live-stream-manage/list', title: 'LiveStream 列表', icon: 'pixelarticons:list' }]
+  },
   { path: '/cc', title: 'Cloudflare Stream', icon: 'mdi:cloud' },
   { path: '/cc', title: 'Cloudflare Stream', icon: 'mdi:cloud' },
   { path: '/camera-vendor', title: '摄像头配置', icon: 'mdi:cctv' },
   { path: '/camera-vendor', title: '摄像头配置', icon: 'mdi:cctv' },
   { path: '/webrtc', title: 'WebRTC 流', icon: 'mdi:wifi' },
   { path: '/webrtc', title: 'WebRTC 流', icon: 'mdi:wifi' },
   { path: '/monitor', title: '多视频监控', icon: 'mdi:video' },
   { path: '/monitor', title: '多视频监控', icon: 'mdi:video' },
   { path: '/machine', title: '机器管理', icon: 'mdi:monitor' },
   { path: '/machine', title: '机器管理', icon: 'mdi:monitor' },
-  { path: '/camera', title: '摄像头管理', icon: 'mdi:video' }
+  { path: '/camera', title: '摄像头管理', icon: 'mdi:video' },
+  {
+    path: '/system',
+    title: '系统管理',
+    icon: 'mdi:cog',
+    children: [
+      { path: '/system/user', title: '用户管理', icon: 'mdi:account' },
+      { path: '/system/role', title: '角色管理', icon: 'mdi:shield-account' }
+    ]
+  }
   // {
   // {
   //   path: '/demo',
   //   path: '/demo',
   //   title: '视频测试',
   //   title: '视频测试',
@@ -648,7 +681,7 @@ onUnmounted(() => {
     border-bottom: 1px solid #e5e7eb;
     border-bottom: 1px solid #e5e7eb;
 
 
     @media (min-width: 1024px) {
     @media (min-width: 1024px) {
-      padding: 0 2rem;
+      padding: 0 2rem 0 0;
     }
     }
   }
   }
 
 
@@ -659,11 +692,56 @@ onUnmounted(() => {
     border: none;
     border: none;
     color: #6b7280;
     color: #6b7280;
     cursor: pointer;
     cursor: pointer;
-    transition: color 150ms ease-in-out;
+    transition: all 150ms ease-in-out;
 
 
     &:hover {
     &:hover {
-      color: #000000;
+      // color: #000000;
     }
     }
+
+    // Mobile: show hamburger, hide chevron
+    &--mobile {
+      display: block;
+
+      @media (min-width: 1024px) {
+        display: none;
+      }
+    }
+
+    // Desktop: show chevron, hide hamburger
+    &--desktop {
+      display: none;
+      height: 56px;
+      padding: 0;
+      margin-left: 0;
+      margin-right: 0;
+      background: #409eff;
+      color: #ffffff;
+
+      @media (min-width: 1024px) {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+      }
+
+      &:hover {
+        background: #66b1ff;
+      }
+    }
+  }
+
+  // Menu icon rotation
+  &__menu-icon {
+    transition: transform 150ms ease-in-out;
+
+    &--collapsed {
+      transform: rotate(180deg);
+    }
+  }
+
+  &__header-left {
+    display: flex;
+    align-items: center;
+    gap: 1rem;
   }
   }
 
 
   &__breadcrumb {
   &__breadcrumb {

+ 58 - 3
src/locales/en.json

@@ -1,14 +1,18 @@
 {
 {
   "Cloudflare Stream": "Cloudflare Stream",
   "Cloudflare Stream": "Cloudflare Stream",
   "Cloudflare Stream 配置": "Cloudflare Stream Configuration",
   "Cloudflare Stream 配置": "Cloudflare Stream Configuration",
+  "FFmpeg 版本": "FFmpeg Version",
+  "ID": "ID",
   "IP": "IP",
   "IP": "IP",
-  "IP / 设备ID / 名称": "IP / Device ID / Name",
   "IP地址": "IP Address",
   "IP地址": "IP Address",
   "LSS": "LSS",
   "LSS": "LSS",
   "LSS ID": "LSS ID",
   "LSS ID": "LSS ID",
+  "LSS 列表": "LSS List",
   "LSS 管理": "LSS Management",
   "LSS 管理": "LSS Management",
+  "LSS 节点": "LSS Node",
   "LSS 节点详情": "LSS Node Details",
   "LSS 节点详情": "LSS Node Details",
   "LSS详情": "LSS Details",
   "LSS详情": "LSS Details",
+  "LiveStream 列表": "LiveStream List",
   "LiveStream 管理": "LiveStream Management",
   "LiveStream 管理": "LiveStream Management",
   "Logo URL": "Logo URL",
   "Logo URL": "Logo URL",
   "PTZ": "PTZ",
   "PTZ": "PTZ",
@@ -23,6 +27,7 @@
   "Video ID": "Video ID",
   "Video ID": "Video ID",
   "WebRTC 流": "WebRTC Stream",
   "WebRTC 流": "WebRTC Stream",
   "ably": "ably",
   "ably": "ably",
+  "active": "active",
   "button.cancel": "Cancel",
   "button.cancel": "Cancel",
   "button.confirm": "Confirm",
   "button.confirm": "Confirm",
   "button.disable": "Disable",
   "button.disable": "Disable",
@@ -30,7 +35,9 @@
   "button.enable": "Enable",
   "button.enable": "Enable",
   "button.selectedSsers": "Selected Users",
   "button.selectedSsers": "Selected Users",
   "button.whether": "Whether",
   "button.whether": "Whether",
+  "dead": "dead",
   "errorCode.0": "Success",
   "errorCode.0": "Success",
+  "hold": "hold",
   "iframe 模式": "iframe Mode",
   "iframe 模式": "iframe Mode",
   "input.SelectAll": "Select All",
   "input.SelectAll": "Select All",
   "login.confirmPassword": "Confirm Password",
   "login.confirmPassword": "Confirm Password",
@@ -47,27 +54,30 @@
   "两次输入的密码不一致": "The passwords entered twice do not match",
   "两次输入的密码不一致": "The passwords entered twice do not match",
   "个厂家": "Factory",
   "个厂家": "Factory",
   "个厂家吗?": "Factory?",
   "个厂家吗?": "Factory?",
-  "个设备": "Device",
   "事件日志": "Event Log",
   "事件日志": "Event Log",
   "五分钟内有返回": "Returned within 5 minutes",
   "五分钟内有返回": "Returned within 5 minutes",
   "五分钟内没有返回": "Not returned within 5 minutes",
   "五分钟内没有返回": "Not returned within 5 minutes",
   "仅切换": "Switch Only",
   "仅切换": "Switch Only",
   "仅在前端直接调用 API 时需要(不推荐)": "Only needed when directly calling the API in the frontend (not recommended)",
   "仅在前端直接调用 API 时需要(不推荐)": "Only needed when directly calling the API in the frontend (not recommended)",
   "仪表盘": "Dashboard",
   "仪表盘": "Dashboard",
+  "任务数": "Task Number",
   "位置": "Location",
   "位置": "Location",
+  "例如: 测试推流-001": "For example: Test Stream-001",
+  "保存": "Save",
   "保存配置": "Save Configuration",
   "保存配置": "Save Configuration",
   "修改失败": "Update failed",
   "修改失败": "Update failed",
   "修改密码": "Change Password",
   "修改密码": "Change Password",
   "修改成功": "Updated successfully",
   "修改成功": "Updated successfully",
   "停止": "Stop",
   "停止": "Stop",
   "停止失败": "Stop failed",
   "停止失败": "Stop failed",
+  "停止推流": "Stop Stream",
   "停止推流失败": "Stop stream failed",
   "停止推流失败": "Stop stream failed",
   "停止时间未超过24小时,暂时无法删除": "Cannot delete: stopped less than 24 hours ago",
   "停止时间未超过24小时,暂时无法删除": "Cannot delete: stopped less than 24 hours ago",
   "全屏": "Fullscreen",
   "全屏": "Fullscreen",
   "全部": "All",
   "全部": "All",
-  "共": "Total",
   "关闭": "Close",
   "关闭": "Close",
   "关闭时间": "Closed At",
   "关闭时间": "Closed At",
+  "其他设备": "Other Devices",
   "创建时间": "Created At",
   "创建时间": "Created At",
   "初始化失败": "Initialization failed",
   "初始化失败": "Initialization failed",
   "初始化成功": "Initialization successful",
   "初始化成功": "Initialization successful",
@@ -101,11 +111,14 @@
   "在线": "Online",
   "在线": "Online",
   "地址": "Address",
   "地址": "Address",
   "型号": "Model",
   "型号": "Model",
+  "备注": "Remark",
   "复制": "Copy",
   "复制": "Copy",
   "复制失败": "Copy failed",
   "复制失败": "Copy failed",
   "多视频监控": "Multi-video Monitoring",
   "多视频监控": "Multi-video Monitoring",
   "如何获取 Customer Subdomain": "How to get Customer Subdomain",
   "如何获取 Customer Subdomain": "How to get Customer Subdomain",
+  "姓名": "Name",
   "密码": "Password",
   "密码": "Password",
+  "密码已重置为默认密码": "Password has been reset to the default password",
   "密码长度不能少于6位": "Password length must be at least 6 characters",
   "密码长度不能少于6位": "Password length must be at least 6 characters",
   "尚未建立 Live Stream": "Live Stream Not Created",
   "尚未建立 Live Stream": "Live Stream Not Created",
   "已启用": "Enabled",
   "已启用": "Enabled",
@@ -115,6 +128,7 @@
   "已选择": "Selected",
   "已选择": "Selected",
   "序号": "No.",
   "序号": "No.",
   "开启": "Start",
   "开启": "Start",
+  "开始推流": "Start Stream",
   "开始日期": "Start Date",
   "开始日期": "Start Date",
   "当前状态": "Current Status",
   "当前状态": "Current Status",
   "待机": "Standby",
   "待机": "Standby",
@@ -127,6 +141,7 @@
   "截图": "Screenshot",
   "截图": "Screenshot",
   "所属机器": "Machine",
   "所属机器": "Machine",
   "手动": "Manual",
   "手动": "Manual",
+  "手机号": "Mobile Phone",
   "批量删除": "Batch Delete",
   "批量删除": "Batch Delete",
   "批量删除失败": "Batch delete failed",
   "批量删除失败": "Batch delete failed",
   "技术支持": "Support",
   "技术支持": "Support",
@@ -141,6 +156,7 @@
   "推荐通过后端代理调用,避免暴露 Token": "Recommended to call through the backend proxy to avoid exposing the Token",
   "推荐通过后端代理调用,避免暴露 Token": "Recommended to call through the backend proxy to avoid exposing the Token",
   "描述": "Description",
   "描述": "Description",
   "提示": "Notice",
   "提示": "Notice",
+  "摄像头": "Camera",
   "摄像头ID": "Camera ID",
   "摄像头ID": "Camera ID",
   "摄像头列表": "Camera List",
   "摄像头列表": "Camera List",
   "摄像头在线率": "Camera Online Rate",
   "摄像头在线率": "Camera Online Rate",
@@ -148,6 +164,7 @@
   "摄像头数": "Cameras",
   "摄像头数": "Cameras",
   "摄像头管理": "Camera Management",
   "摄像头管理": "Camera Management",
   "摄像头管理系统": "Camera Management",
   "摄像头管理系统": "Camera Management",
+  "摄像头详情": "Camera Details",
   "摄像头连接失败": "Camera connection failed",
   "摄像头连接失败": "Camera connection failed",
   "摄像头连接正常": "Camera connection successful",
   "摄像头连接正常": "Camera connection successful",
   "摄像头配置": "Camera Configuration",
   "摄像头配置": "Camera Configuration",
@@ -166,11 +183,14 @@
   "新增成功": "Added successfully",
   "新增成功": "Added successfully",
   "新增摄像头": "Add Camera",
   "新增摄像头": "Add Camera",
   "新增机器": "Add Machine",
   "新增机器": "Add Machine",
+  "新增用户": "Add User",
+  "新增角色": "Add Role",
   "新密码": "New Password",
   "新密码": "New Password",
   "新建标签": "New Tab",
   "新建标签": "New Tab",
   "是": "Yes",
   "是": "Yes",
   "暂停": "Pause",
   "暂停": "Pause",
   "暂无关联设备": "No associated devices",
   "暂无关联设备": "No associated devices",
+  "暂无其他设备数据": "No other device data",
   "暂无推币机数据": "No coin machine data",
   "暂无推币机数据": "No coin machine data",
   "暂无日志": "No logs",
   "暂无日志": "No logs",
   "暂无视频流": "No video stream",
   "暂无视频流": "No video stream",
@@ -178,11 +198,15 @@
   "更新": "Update",
   "更新": "Update",
   "更新失败": "Update failed",
   "更新失败": "Update failed",
   "更新成功": "Updated successfully",
   "更新成功": "Updated successfully",
+  "更新时间": "Updated At",
   "有声": "Sound",
   "有声": "Sound",
   "未配置摄像头": "No camera configured",
   "未配置摄像头": "No camera configured",
+  "机器 ID": "Machine ID",
   "机器ID": "Machine ID",
   "机器ID": "Machine ID",
   "机器总数": "Total Machines",
   "机器总数": "Total Machines",
   "机器管理": "Machine Management",
   "机器管理": "Machine Management",
+  "权限配置": "Permission Configuration",
+  "权限配置保存成功": "Permission configuration saved successfully",
   "查看": "View",
   "查看": "View",
   "查看Cloudflare Stream": "View Cloudflare Stream",
   "查看Cloudflare Stream": "View Cloudflare Stream",
   "查询": "Search",
   "查询": "Search",
@@ -198,6 +222,8 @@
   "测试视频": "Test Video",
   "测试视频": "Test Video",
   "测试连接": "Test Connection",
   "测试连接": "Test Connection",
   "添加": "Add",
   "添加": "Add",
+  "添加成功": "Added successfully",
+  "添加摄像头": "Add Camera",
   "添加时间": "Add Time",
   "添加时间": "Add Time",
   "清空": "Clear",
   "清空": "Clear",
   "版本": "Version",
   "版本": "Version",
@@ -207,6 +233,7 @@
   "生成的地址": "Generated URL",
   "生成的地址": "Generated URL",
   "用户": "Users",
   "用户": "Users",
   "用户名": "Username",
   "用户名": "Username",
+  "用户数": "User Count",
   "登录": "Sign In",
   "登录": "Sign In",
   "登录失败": "Login failed",
   "登录失败": "Login failed",
   "登录失败,请检查网络": "Login failed, please check your network",
   "登录失败,请检查网络": "Login failed, please check your network",
@@ -224,9 +251,11 @@
   "确定要删除该 Live Stream 吗?": "Are you sure you want to delete the Live Stream?",
   "确定要删除该 Live Stream 吗?": "Are you sure you want to delete the Live Stream?",
   "确定要删除选中的": "Are you sure you want to delete the selected",
   "确定要删除选中的": "Are you sure you want to delete the selected",
   "确认密码": "Confirm Password",
   "确认密码": "Confirm Password",
+  "禁用": "Disable",
   "离线": "Offline",
   "离线": "Offline",
   "稳定性": "Uptime",
   "稳定性": "Uptime",
   "端口": "Port",
   "端口": "Port",
+  "管理员角色不能删除": "Admin role cannot be deleted",
   "系统信息": "System Info",
   "系统信息": "System Info",
   "系统状态": "System Status",
   "系统状态": "System Status",
   "系统运行正常": "System running normally",
   "系统运行正常": "System running normally",
@@ -236,6 +265,8 @@
   "编辑厂家": "Edit Factory",
   "编辑厂家": "Edit Factory",
   "编辑摄像头": "Edit Camera",
   "编辑摄像头": "Edit Camera",
   "编辑机器": "Edit Machine",
   "编辑机器": "Edit Machine",
+  "编辑用户": "Edit User",
+  "编辑角色": "Edit Role",
   "缩小": "Zoom Out",
   "缩小": "Zoom Out",
   "能力": "Capabilities",
   "能力": "Capabilities",
   "自动播放": "Autoplay",
   "自动播放": "Autoplay",
@@ -247,10 +278,16 @@
   "观看统计": "Watching Statistics",
   "观看统计": "Watching Statistics",
   "视频地址": "Video URL",
   "视频地址": "Video URL",
   "视频播放测试": "Video Playback Test",
   "视频播放测试": "Video Playback Test",
+  "角色": "Role",
+  "角色名称": "Role Name",
+  "角色编码": "Role Code",
   "记住我": "Remember me",
   "记住我": "Remember me",
   "设备ID": "Device ID",
   "设备ID": "Device ID",
+  "设备ID / 名称": "Device ID / Name",
   "设备列表": "Devices",
   "设备列表": "Devices",
+  "设备名称": "Device Name",
   "设备控制": "Device Control",
   "设备控制": "Device Control",
+  "设备运行参数 (JSON)": "Device Runtime Parameters (JSON)",
   "请先新增 Live Stream,才能进行后续操作。": "Please create a Live Stream first to continue.",
   "请先新增 Live Stream,才能进行后续操作。": "Please create a Live Stream first to continue.",
   "请先配置摄像头": "Please configure the camera first",
   "请先配置摄像头": "Please configure the camera first",
   "请再次输入新密码": "Please enter the new password again",
   "请再次输入新密码": "Please enter the new password again",
@@ -260,17 +297,34 @@
   "请输入厂家代码": "Please enter factory code",
   "请输入厂家代码": "Please enter factory code",
   "请输入厂家名称": "Please enter factory name",
   "请输入厂家名称": "Please enter factory name",
   "请输入原密码": "Please enter the old password",
   "请输入原密码": "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 name",
+  "请输入地址": "Please enter address",
+  "请输入型号": "Please enter model",
+  "请输入备注": "Please enter remark",
+  "请输入姓名": "Please enter name",
   "请输入密码": "Please enter password",
   "请输入密码": "Please enter password",
+  "请输入手机号": "Please enter mobile phone",
+  "请输入描述": "Please enter description",
   "请输入摄像头ID": "Please enter Camera ID",
   "请输入摄像头ID": "Please enter Camera ID",
   "请输入新密码": "Please enter the new password",
   "请输入新密码": "Please enter the new password",
   "请输入机器ID": "Please enter Machine ID",
   "请输入机器ID": "Please enter Machine ID",
   "请输入正确的IP地址": "Please enter a valid IP address",
   "请输入正确的IP地址": "Please enter a valid IP address",
+  "请输入正确的邮箱": "Please enter a valid email",
   "请输入用户名": "Please enter username",
   "请输入用户名": "Please enter username",
   "请输入视频地址并点击播放": "Please enter video URL and click play",
   "请输入视频地址并点击播放": "Please enter video URL and click play",
+  "请输入角色名称": "Please enter role name",
+  "请输入角色编码": "Please enter role code",
   "请输入设备ID": "Please enter Device ID",
   "请输入设备ID": "Please enter Device ID",
+  "请输入设备名称": "Please enter device name",
+  "请输入运行参数(JSON 格式)": "Please enter the runtime parameters (JSON format)",
+  "请输入邮箱": "Please enter email",
+  "请选择": "Please select",
   "请选择 LSS 节点": "Please select LSS node",
   "请选择 LSS 节点": "Please select LSS node",
+  "请选择摄像头": "Please select camera",
   "请选择视频源并点击播放": "Please select video source and click play",
   "请选择视频源并点击播放": "Please select video source and click play",
+  "请选择角色": "Please select role",
   "跳转失败": "Jump failed",
   "跳转失败": "Jump failed",
   "转换服务地址": "Proxy Service URL",
   "转换服务地址": "Proxy Service URL",
   "运行参数": "Runtime",
   "运行参数": "Runtime",
@@ -281,6 +335,7 @@
   "通道列表": "Channel List",
   "通道列表": "Channel List",
   "通道总数": "Total Channels",
   "通道总数": "Total Channels",
   "速度": "Speed",
   "速度": "Speed",
+  "邮箱": "Email",
   "配置说明": "Configuration Description",
   "配置说明": "Configuration Description",
   "重置": "Reset",
   "重置": "Reset",
   "静音": "Muted",
   "静音": "Muted",

+ 58 - 3
src/locales/zh-cn.json

@@ -1,14 +1,18 @@
 {
 {
   "Cloudflare Stream": "Cloudflare Stream",
   "Cloudflare Stream": "Cloudflare Stream",
   "Cloudflare Stream 配置": "Cloudflare Stream 配置",
   "Cloudflare Stream 配置": "Cloudflare Stream 配置",
+  "FFmpeg 版本": "FFmpeg 版本",
+  "ID": "ID",
   "IP": "IP",
   "IP": "IP",
-  "IP / 设备ID / 名称": "IP / 设备ID / 名称",
   "IP地址": "IP地址",
   "IP地址": "IP地址",
   "LSS": "LSS",
   "LSS": "LSS",
   "LSS ID": "LSS ID",
   "LSS ID": "LSS ID",
+  "LSS 列表": "LSS 列表",
   "LSS 管理": "LSS 管理",
   "LSS 管理": "LSS 管理",
+  "LSS 节点": "LSS 节点",
   "LSS 节点详情": "LSS 节点详情",
   "LSS 节点详情": "LSS 节点详情",
   "LSS详情": "LSS详情",
   "LSS详情": "LSS详情",
+  "LiveStream 列表": "LiveStream 列表",
   "LiveStream 管理": "LiveStream 管理",
   "LiveStream 管理": "LiveStream 管理",
   "Logo URL": "Logo URL",
   "Logo URL": "Logo URL",
   "PTZ": "PTZ",
   "PTZ": "PTZ",
@@ -23,6 +27,7 @@
   "Video ID": "Video ID",
   "Video ID": "Video ID",
   "WebRTC 流": "WebRTC 流",
   "WebRTC 流": "WebRTC 流",
   "ably": "ably",
   "ably": "ably",
+  "active": "active",
   "button.cancel": "button.cancel",
   "button.cancel": "button.cancel",
   "button.confirm": "button.confirm",
   "button.confirm": "button.confirm",
   "button.disable": "button.disable",
   "button.disable": "button.disable",
@@ -30,7 +35,9 @@
   "button.enable": "button.enable",
   "button.enable": "button.enable",
   "button.selectedSsers": "button.selectedSsers",
   "button.selectedSsers": "button.selectedSsers",
   "button.whether": "button.whether",
   "button.whether": "button.whether",
+  "dead": "dead",
   "errorCode.0": "errorCode.0",
   "errorCode.0": "errorCode.0",
+  "hold": "hold",
   "iframe 模式": "iframe 模式",
   "iframe 模式": "iframe 模式",
   "input.SelectAll": "input.SelectAll",
   "input.SelectAll": "input.SelectAll",
   "login.confirmPassword": "login.confirmPassword",
   "login.confirmPassword": "login.confirmPassword",
@@ -47,27 +54,30 @@
   "两次输入的密码不一致": "两次输入的密码不一致",
   "两次输入的密码不一致": "两次输入的密码不一致",
   "个厂家": "个厂家",
   "个厂家": "个厂家",
   "个厂家吗?": "个厂家吗?",
   "个厂家吗?": "个厂家吗?",
-  "个设备": "个设备",
   "事件日志": "事件日志",
   "事件日志": "事件日志",
   "五分钟内有返回": "五分钟内有返回",
   "五分钟内有返回": "五分钟内有返回",
   "五分钟内没有返回": "五分钟内没有返回",
   "五分钟内没有返回": "五分钟内没有返回",
   "仅切换": "仅切换",
   "仅切换": "仅切换",
   "仅在前端直接调用 API 时需要(不推荐)": "仅在前端直接调用 API 时需要(不推荐)",
   "仅在前端直接调用 API 时需要(不推荐)": "仅在前端直接调用 API 时需要(不推荐)",
   "仪表盘": "仪表盘",
   "仪表盘": "仪表盘",
+  "任务数": "任务数",
   "位置": "位置",
   "位置": "位置",
+  "例如: 测试推流-001": "例如: 测试推流-001",
+  "保存": "保存",
   "保存配置": "保存配置",
   "保存配置": "保存配置",
   "修改失败": "修改失败",
   "修改失败": "修改失败",
   "修改密码": "修改密码",
   "修改密码": "修改密码",
   "修改成功": "修改成功",
   "修改成功": "修改成功",
   "停止": "停止",
   "停止": "停止",
   "停止失败": "停止失败",
   "停止失败": "停止失败",
+  "停止推流": "停止推流",
   "停止推流失败": "停止推流失败",
   "停止推流失败": "停止推流失败",
   "停止时间未超过24小时,暂时无法删除": "停止时间未超过24小时,暂时无法删除",
   "停止时间未超过24小时,暂时无法删除": "停止时间未超过24小时,暂时无法删除",
   "全屏": "全屏",
   "全屏": "全屏",
   "全部": "全部",
   "全部": "全部",
-  "共": "共",
   "关闭": "关闭",
   "关闭": "关闭",
   "关闭时间": "关闭时间",
   "关闭时间": "关闭时间",
+  "其他设备": "其他设备",
   "创建时间": "创建时间",
   "创建时间": "创建时间",
   "初始化失败": "初始化失败",
   "初始化失败": "初始化失败",
   "初始化成功": "初始化成功",
   "初始化成功": "初始化成功",
@@ -101,11 +111,14 @@
   "在线": "在线",
   "在线": "在线",
   "地址": "地址",
   "地址": "地址",
   "型号": "型号",
   "型号": "型号",
+  "备注": "备注",
   "复制": "复制",
   "复制": "复制",
   "复制失败": "复制失败",
   "复制失败": "复制失败",
   "多视频监控": "多视频监控",
   "多视频监控": "多视频监控",
   "如何获取 Customer Subdomain": "如何获取 Customer Subdomain",
   "如何获取 Customer Subdomain": "如何获取 Customer Subdomain",
+  "姓名": "姓名",
   "密码": "密码",
   "密码": "密码",
+  "密码已重置为默认密码": "密码已重置为默认密码",
   "密码长度不能少于6位": "密码长度不能少于6位",
   "密码长度不能少于6位": "密码长度不能少于6位",
   "尚未建立 Live Stream": "尚未建立 Live Stream",
   "尚未建立 Live Stream": "尚未建立 Live Stream",
   "已启用": "已启用",
   "已启用": "已启用",
@@ -115,6 +128,7 @@
   "已选择": "已选择",
   "已选择": "已选择",
   "序号": "序号",
   "序号": "序号",
   "开启": "开启",
   "开启": "开启",
+  "开始推流": "开始推流",
   "开始日期": "开始日期",
   "开始日期": "开始日期",
   "当前状态": "当前状态",
   "当前状态": "当前状态",
   "待机": "待机",
   "待机": "待机",
@@ -127,6 +141,7 @@
   "截图": "截图",
   "截图": "截图",
   "所属机器": "所属机器",
   "所属机器": "所属机器",
   "手动": "手动",
   "手动": "手动",
+  "手机号": "手机号",
   "批量删除": "批量删除",
   "批量删除": "批量删除",
   "批量删除失败": "批量删除失败",
   "批量删除失败": "批量删除失败",
   "技术支持": "技术支持",
   "技术支持": "技术支持",
@@ -141,6 +156,7 @@
   "推荐通过后端代理调用,避免暴露 Token": "推荐通过后端代理调用,避免暴露 Token",
   "推荐通过后端代理调用,避免暴露 Token": "推荐通过后端代理调用,避免暴露 Token",
   "描述": "描述",
   "描述": "描述",
   "提示": "提示",
   "提示": "提示",
+  "摄像头": "摄像头",
   "摄像头ID": "摄像头ID",
   "摄像头ID": "摄像头ID",
   "摄像头列表": "摄像头列表",
   "摄像头列表": "摄像头列表",
   "摄像头在线率": "摄像头在线率",
   "摄像头在线率": "摄像头在线率",
@@ -148,6 +164,7 @@
   "摄像头数": "摄像头数",
   "摄像头数": "摄像头数",
   "摄像头管理": "摄像头管理",
   "摄像头管理": "摄像头管理",
   "摄像头管理系统": "摄像头管理系统",
   "摄像头管理系统": "摄像头管理系统",
+  "摄像头详情": "摄像头详情",
   "摄像头连接失败": "摄像头连接失败",
   "摄像头连接失败": "摄像头连接失败",
   "摄像头连接正常": "摄像头连接正常",
   "摄像头连接正常": "摄像头连接正常",
   "摄像头配置": "摄像头配置",
   "摄像头配置": "摄像头配置",
@@ -166,11 +183,14 @@
   "新增成功": "新增成功",
   "新增成功": "新增成功",
   "新增摄像头": "新增摄像头",
   "新增摄像头": "新增摄像头",
   "新增机器": "新增机器",
   "新增机器": "新增机器",
+  "新增用户": "新增用户",
+  "新增角色": "新增角色",
   "新密码": "新密码",
   "新密码": "新密码",
   "新建标签": "新建标签",
   "新建标签": "新建标签",
   "是": "是",
   "是": "是",
   "暂停": "暂停",
   "暂停": "暂停",
   "暂无关联设备": "暂无关联设备",
   "暂无关联设备": "暂无关联设备",
+  "暂无其他设备数据": "暂无其他设备数据",
   "暂无推币机数据": "暂无推币机数据",
   "暂无推币机数据": "暂无推币机数据",
   "暂无日志": "暂无日志",
   "暂无日志": "暂无日志",
   "暂无视频流": "暂无视频流",
   "暂无视频流": "暂无视频流",
@@ -178,11 +198,15 @@
   "更新": "更新",
   "更新": "更新",
   "更新失败": "更新失败",
   "更新失败": "更新失败",
   "更新成功": "更新成功",
   "更新成功": "更新成功",
+  "更新时间": "更新时间",
   "有声": "有声",
   "有声": "有声",
   "未配置摄像头": "未配置摄像头",
   "未配置摄像头": "未配置摄像头",
+  "机器 ID": "机器 ID",
   "机器ID": "机器ID",
   "机器ID": "机器ID",
   "机器总数": "机器总数",
   "机器总数": "机器总数",
   "机器管理": "机器管理",
   "机器管理": "机器管理",
+  "权限配置": "权限配置",
+  "权限配置保存成功": "权限配置保存成功",
   "查看": "查看",
   "查看": "查看",
   "查看Cloudflare Stream": "查看Cloudflare Stream",
   "查看Cloudflare Stream": "查看Cloudflare Stream",
   "查询": "查询",
   "查询": "查询",
@@ -198,6 +222,8 @@
   "测试视频": "测试视频",
   "测试视频": "测试视频",
   "测试连接": "测试连接",
   "测试连接": "测试连接",
   "添加": "添加",
   "添加": "添加",
+  "添加成功": "添加成功",
+  "添加摄像头": "添加摄像头",
   "添加时间": "添加时间",
   "添加时间": "添加时间",
   "清空": "清空",
   "清空": "清空",
   "版本": "版本",
   "版本": "版本",
@@ -207,6 +233,7 @@
   "生成的地址": "生成的地址",
   "生成的地址": "生成的地址",
   "用户": "用户",
   "用户": "用户",
   "用户名": "用户名",
   "用户名": "用户名",
+  "用户数": "用户数",
   "登录": "登录",
   "登录": "登录",
   "登录失败": "登录失败",
   "登录失败": "登录失败",
   "登录失败,请检查网络": "登录失败,请检查网络",
   "登录失败,请检查网络": "登录失败,请检查网络",
@@ -224,9 +251,11 @@
   "确定要删除该 Live Stream 吗?": "确定要删除该 Live Stream 吗?",
   "确定要删除该 Live Stream 吗?": "确定要删除该 Live Stream 吗?",
   "确定要删除选中的": "确定要删除选中的",
   "确定要删除选中的": "确定要删除选中的",
   "确认密码": "确认密码",
   "确认密码": "确认密码",
+  "禁用": "禁用",
   "离线": "离线",
   "离线": "离线",
   "稳定性": "稳定性",
   "稳定性": "稳定性",
   "端口": "端口",
   "端口": "端口",
+  "管理员角色不能删除": "管理员角色不能删除",
   "系统信息": "系统信息",
   "系统信息": "系统信息",
   "系统状态": "系统状态",
   "系统状态": "系统状态",
   "系统运行正常": "系统运行正常",
   "系统运行正常": "系统运行正常",
@@ -236,6 +265,8 @@
   "编辑厂家": "编辑厂家",
   "编辑厂家": "编辑厂家",
   "编辑摄像头": "编辑摄像头",
   "编辑摄像头": "编辑摄像头",
   "编辑机器": "编辑机器",
   "编辑机器": "编辑机器",
+  "编辑用户": "编辑用户",
+  "编辑角色": "编辑角色",
   "缩小": "缩小",
   "缩小": "缩小",
   "能力": "能力",
   "能力": "能力",
   "自动播放": "自动播放",
   "自动播放": "自动播放",
@@ -247,10 +278,16 @@
   "观看统计": "观看统计",
   "观看统计": "观看统计",
   "视频地址": "视频地址",
   "视频地址": "视频地址",
   "视频播放测试": "视频播放测试",
   "视频播放测试": "视频播放测试",
+  "角色": "角色",
+  "角色名称": "角色名称",
+  "角色编码": "角色编码",
   "记住我": "记住我",
   "记住我": "记住我",
   "设备ID": "设备ID",
   "设备ID": "设备ID",
+  "设备ID / 名称": "设备ID / 名称",
   "设备列表": "设备列表",
   "设备列表": "设备列表",
+  "设备名称": "设备名称",
   "设备控制": "设备控制",
   "设备控制": "设备控制",
+  "设备运行参数 (JSON)": "设备运行参数 (JSON)",
   "请先新增 Live Stream,才能进行后续操作。": "请先新增 Live Stream,才能进行后续操作。",
   "请先新增 Live Stream,才能进行后续操作。": "请先新增 Live Stream,才能进行后续操作。",
   "请先配置摄像头": "请先配置摄像头",
   "请先配置摄像头": "请先配置摄像头",
   "请再次输入新密码": "请再次输入新密码",
   "请再次输入新密码": "请再次输入新密码",
@@ -260,17 +297,34 @@
   "请输入厂家代码": "请输入厂家代码",
   "请输入厂家代码": "请输入厂家代码",
   "请输入厂家名称": "请输入厂家名称",
   "请输入厂家名称": "请输入厂家名称",
   "请输入原密码": "请输入原密码",
   "请输入原密码": "请输入原密码",
+  "请输入参数配置 (JSON)": "请输入参数配置 (JSON)",
+  "请输入参数配置(JSON 格式)": "请输入参数配置(JSON 格式)",
   "请输入名称": "请输入名称",
   "请输入名称": "请输入名称",
+  "请输入地址": "请输入地址",
+  "请输入型号": "请输入型号",
+  "请输入备注": "请输入备注",
+  "请输入姓名": "请输入姓名",
   "请输入密码": "请输入密码",
   "请输入密码": "请输入密码",
+  "请输入手机号": "请输入手机号",
+  "请输入描述": "请输入描述",
   "请输入摄像头ID": "请输入摄像头ID",
   "请输入摄像头ID": "请输入摄像头ID",
   "请输入新密码": "请输入新密码",
   "请输入新密码": "请输入新密码",
   "请输入机器ID": "请输入机器ID",
   "请输入机器ID": "请输入机器ID",
   "请输入正确的IP地址": "请输入正确的IP地址",
   "请输入正确的IP地址": "请输入正确的IP地址",
+  "请输入正确的邮箱": "请输入正确的邮箱",
   "请输入用户名": "请输入用户名",
   "请输入用户名": "请输入用户名",
   "请输入视频地址并点击播放": "请输入视频地址并点击播放",
   "请输入视频地址并点击播放": "请输入视频地址并点击播放",
+  "请输入角色名称": "请输入角色名称",
+  "请输入角色编码": "请输入角色编码",
   "请输入设备ID": "请输入设备ID",
   "请输入设备ID": "请输入设备ID",
+  "请输入设备名称": "请输入设备名称",
+  "请输入运行参数(JSON 格式)": "请输入运行参数(JSON 格式)",
+  "请输入邮箱": "请输入邮箱",
+  "请选择": "请选择",
   "请选择 LSS 节点": "请选择 LSS 节点",
   "请选择 LSS 节点": "请选择 LSS 节点",
+  "请选择摄像头": "请选择摄像头",
   "请选择视频源并点击播放": "请选择视频源并点击播放",
   "请选择视频源并点击播放": "请选择视频源并点击播放",
+  "请选择角色": "请选择角色",
   "跳转失败": "跳转失败",
   "跳转失败": "跳转失败",
   "转换服务地址": "转换服务地址",
   "转换服务地址": "转换服务地址",
   "运行参数": "运行参数",
   "运行参数": "运行参数",
@@ -281,6 +335,7 @@
   "通道列表": "通道列表",
   "通道列表": "通道列表",
   "通道总数": "通道总数",
   "通道总数": "通道总数",
   "速度": "速度",
   "速度": "速度",
+  "邮箱": "邮箱",
   "配置说明": "配置说明",
   "配置说明": "配置说明",
   "重置": "重置",
   "重置": "重置",
   "静音": "静音",
   "静音": "静音",

+ 46 - 10
src/router/index.ts

@@ -45,16 +45,32 @@ const routes: RouteRecordRaw[] = [
         meta: { title: '摄像头厂家', icon: 'OfficeBuilding' }
         meta: { title: '摄像头厂家', icon: 'OfficeBuilding' }
       },
       },
       {
       {
-        path: 'lss',
-        name: 'LSS',
-        component: () => import('@/views/lss/index.vue'),
-        meta: { title: 'LSS 管理', icon: 'Connection' }
-      },
-      {
-        path: 'live-stream',
-        name: 'LiveStream',
-        component: () => import('@/views/live-stream/index.vue'),
-        meta: { title: 'LiveStream 管理', icon: 'VideoCamera' }
+        path: 'lss-manage',
+        name: 'LssManage',
+        meta: { title: 'LSS 管理', icon: 'Connection' },
+        redirect: '/lss-manage/list',
+        children: [
+          {
+            path: 'list',
+            name: 'LssList',
+            component: () => import('@/views/lss/index.vue'),
+            meta: { title: 'LSS 列表', icon: 'List' }
+          }
+        ]
+      },
+      {
+        path: 'live-stream-manage',
+        name: 'LiveStreamManage',
+        meta: { title: 'LiveStream 管理', icon: 'VideoCamera' },
+        redirect: '/live-stream-manage/list',
+        children: [
+          {
+            path: 'list',
+            name: 'LiveStreamList',
+            component: () => import('@/views/live-stream/index.vue'),
+            meta: { title: 'LiveStream 列表', icon: 'List' }
+          }
+        ]
       },
       },
       {
       {
         path: 'cloud',
         path: 'cloud',
@@ -169,6 +185,26 @@ const routes: RouteRecordRaw[] = [
         name: 'MTableDemo',
         name: 'MTableDemo',
         component: () => import('@/views/test/m-table-demo.vue'),
         component: () => import('@/views/test/m-table-demo.vue'),
         meta: { title: 'MTable 测试', icon: 'Grid' }
         meta: { title: 'MTable 测试', icon: 'Grid' }
+      },
+      {
+        path: 'system',
+        name: 'System',
+        meta: { title: '系统管理', icon: 'Setting' },
+        redirect: '/system/user',
+        children: [
+          {
+            path: 'user',
+            name: 'SystemUser',
+            component: () => import('@/views/system/user/index.vue'),
+            meta: { title: '用户管理', icon: 'User' }
+          },
+          {
+            path: 'role',
+            name: 'SystemRole',
+            component: () => import('@/views/system/role/index.vue'),
+            meta: { title: '角色管理', icon: 'UserFilled' }
+          }
+        ]
       }
       }
     ]
     ]
   },
   },

+ 1 - 0
src/types/index.ts

@@ -711,6 +711,7 @@ export interface StartStreamTaskRequest {
   whipUrl?: string
   whipUrl?: string
   playbackUrl?: string
   playbackUrl?: string
   remark?: string
   remark?: string
+  commandTemplate?: string
 }
 }
 
 
 // 停止推流任务请求
 // 停止推流任务请求

+ 42 - 0
src/utils/dayjs.ts

@@ -0,0 +1,42 @@
+import dayjs from 'dayjs'
+import utc from 'dayjs/plugin/utc'
+import timezone from 'dayjs/plugin/timezone'
+
+// 扩展 dayjs 插件
+dayjs.extend(utc)
+dayjs.extend(timezone)
+
+// 获取用户当前时区(浏览器本地时区)
+const getUserTimezone = () => Intl.DateTimeFormat().resolvedOptions().timeZone
+
+/**
+ * 格式化时间(使用用户当前时区)
+ * @param time 时间字符串
+ * @param format 格式化模板,默认 'YYYY-MM-DD HH:mm:ss'
+ * @returns 格式化后的时间字符串
+ */
+export function formatTime(time: string | undefined | null, format = 'YYYY-MM-DD HH:mm:ss'): string {
+  if (!time) return '-'
+  return dayjs(time).tz(getUserTimezone()).format(format)
+}
+
+/**
+ * 格式化日期(使用用户当前时区)
+ * @param time 时间字符串
+ * @returns 格式化后的日期字符串 (YYYY-MM-DD)
+ */
+export function formatDate(time: string | undefined | null): string {
+  return formatTime(time, 'YYYY-MM-DD')
+}
+
+/**
+ * 获取配置了时区的 dayjs 实例(使用用户当前时区)
+ * @param time 时间字符串
+ * @returns dayjs 实例
+ */
+export function dayjsTZ(time?: string | Date) {
+  return dayjs(time).tz(getUserTimezone())
+}
+
+export { dayjs }
+export default dayjs

+ 11 - 12
src/views/camera-vendor/index.vue

@@ -29,16 +29,16 @@
           </el-select>
           </el-select>
         </el-form-item>
         </el-form-item>
         <el-form-item>
         <el-form-item>
-          <el-button type="primary" :icon="Search" data-id="btn-search" @click="handleSearch">
+          <el-button type="primary" data-id="btn-search" @click="handleSearch">
             {{ t('查询') }}
             {{ t('查询') }}
           </el-button>
           </el-button>
           <el-button :icon="RefreshRight" data-id="btn-reset" @click="handleReset">{{ t('重置') }}</el-button>
           <el-button :icon="RefreshRight" data-id="btn-reset" @click="handleReset">{{ t('重置') }}</el-button>
-          <el-button type="primary" :icon="Plus" data-id="btn-add-vendor" @click="handleAdd">
+          <el-button type="primary" data-id="btn-add-vendor" @click="handleAdd">
             {{ t('新增') }}
             {{ t('新增') }}
           </el-button>
           </el-button>
-          <el-button type="success" :icon="Setting" data-id="btn-init" @click="handleInit">
+          <!-- <el-button type="success" :icon="Setting" data-id="btn-init" @click="handleInit">
             {{ t('初始化默认数据') }}
             {{ t('初始化默认数据') }}
-          </el-button>
+          </el-button> -->
         </el-form-item>
         </el-form-item>
       </el-form>
       </el-form>
     </div>
     </div>
@@ -114,18 +114,17 @@
         </el-table-column>
         </el-table-column>
         <el-table-column :label="t('操作')" min-width="90" align="center" fixed="right">
         <el-table-column :label="t('操作')" min-width="90" align="center" fixed="right">
           <template #default="{ row }">
           <template #default="{ row }">
-            <el-button type="primary" link :icon="Edit" :data-id="`btn-edit-${row.code}`" @click="handleEdit(row)">
-              {{ t('编辑') }}
+            <el-button type="primary" link :data-id="`btn-edit-${row.code}`" @click="handleEdit(row)">
+              <Icon icon="mdi:note-edit-outline" width="20" height="20" />
             </el-button>
             </el-button>
             <el-button
             <el-button
               type="danger"
               type="danger"
               link
               link
-              :icon="Delete"
               :disabled="deleteLoading"
               :disabled="deleteLoading"
               :data-id="`btn-delete-${row.code}`"
               :data-id="`btn-delete-${row.code}`"
               @click="handleDelete(row)"
               @click="handleDelete(row)"
             >
             >
-              {{ t('删除') }}
+              <Icon icon="mdi:delete" width="20" height="20" />
             </el-button>
             </el-button>
           </template>
           </template>
         </el-table-column>
         </el-table-column>
@@ -241,7 +240,7 @@
 import { ref, reactive, onMounted, computed } from 'vue'
 import { ref, reactive, onMounted, computed } from 'vue'
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules, type TableInstance } from 'element-plus'
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules, type TableInstance } from 'element-plus'
 import { Plus, Edit, Delete, Search, RefreshRight, Setting } from '@element-plus/icons-vue'
 import { Plus, Edit, Delete, Search, RefreshRight, Setting } from '@element-plus/icons-vue'
-import dayjs from 'dayjs'
+import { Icon } from '@iconify/vue'
 import { useI18n } from 'vue-i18n'
 import { useI18n } from 'vue-i18n'
 import {
 import {
   listCameraVendors,
   listCameraVendors,
@@ -250,14 +249,14 @@ import {
   deleteCameraVendor,
   deleteCameraVendor,
   initCameraVendors
   initCameraVendors
 } from '@/api/camera-vendor'
 } from '@/api/camera-vendor'
+import { formatTime } from '@/utils/dayjs'
 import type { CameraVendorDTO, CameraVendorAddRequest, CameraVendorUpdateRequest } from '@/types'
 import type { CameraVendorDTO, CameraVendorAddRequest, CameraVendorUpdateRequest } from '@/types'
 
 
 const { t } = useI18n({ useScope: 'global' })
 const { t } = useI18n({ useScope: 'global' })
 
 
-// 格式化时间
+// 格式化时间(不含秒)
 function formatDateTime(dateStr: string | undefined): string {
 function formatDateTime(dateStr: string | undefined): string {
-  if (!dateStr) return '-'
-  return dayjs(dateStr).format('YYYY-MM-DD HH:mm')
+  return formatTime(dateStr, 'YYYY-MM-DD HH:mm')
 }
 }
 
 
 const loading = ref(false)
 const loading = ref(false)

+ 3 - 4
src/views/camera/index.vue

@@ -311,18 +311,17 @@
 import { ref, reactive, onMounted, computed } from 'vue'
 import { ref, reactive, onMounted, computed } from 'vue'
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
 import { Plus, Edit, Delete, Search, RefreshRight, View, Connection } from '@element-plus/icons-vue'
 import { Plus, Edit, Delete, Search, RefreshRight, View, Connection } from '@element-plus/icons-vue'
-import dayjs from 'dayjs'
 import { useI18n } from 'vue-i18n'
 import { useI18n } from 'vue-i18n'
 import { adminListCameras, adminAddCamera, adminUpdateCamera, adminDeleteCamera, adminCheckCamera } from '@/api/camera'
 import { adminListCameras, adminAddCamera, adminUpdateCamera, adminDeleteCamera, adminCheckCamera } from '@/api/camera'
 import { listAllMachines } from '@/api/machine'
 import { listAllMachines } from '@/api/machine'
+import { formatTime } from '@/utils/dayjs'
 import type { CameraInfoDTO, ChannelInfoDTO, CameraAddRequest, CameraUpdateRequest, MachineDTO } from '@/types'
 import type { CameraInfoDTO, ChannelInfoDTO, CameraAddRequest, CameraUpdateRequest, MachineDTO } from '@/types'
 
 
 const { t } = useI18n({ useScope: 'global' })
 const { t } = useI18n({ useScope: 'global' })
 
 
-// 格式化时间
+// 格式化时间(不含秒)
 function formatDateTime(dateStr: string | undefined): string {
 function formatDateTime(dateStr: string | undefined): string {
-  if (!dateStr) return '-'
-  return dayjs(dateStr).format('YYYY-MM-DD HH:mm')
+  return formatTime(dateStr, 'YYYY-MM-DD HH:mm')
 }
 }
 
 
 // 获取品牌标签
 // 获取品牌标签

+ 22 - 0
src/views/demo.vue

@@ -11,6 +11,28 @@ const { t } = useI18n()
     {{ t('摄像头配置') }}
     {{ t('摄像头配置') }}
     {{ t('WebRTC 流') }}
     {{ t('WebRTC 流') }}
     {{ t('多视频监控') }}
     {{ t('多视频监控') }}
+    {{ t('新增机器') }}
+    {{ t('机器ID') }}
+    {{ t('名称') }}
+    {{ t('启用状态') }}
+    {{ t('创建时间') }}
+    {{ t('查询') }}
+    {{ t('重置') }}
+    {{ t('新增') }}
+    {{ t('序号') }}
+    {{ t('机器ID') }}
+    {{ t('名称') }}
+    {{ t('位置') }}
+    {{ t('描述') }}
+    {{ t('摄像头数') }}
+    {{ t('启用') }}
+    {{ t('创建时间') }}
+    {{ t('操作') }}
+    {{ t('active') }}
+    {{ t('hold') }}
+    {{ t('dead') }}
+    {{ t('LSS 列表') }}
+    {{ t('LiveStream 列表') }}
   </div>
   </div>
 </template>
 </template>
 
 

+ 493 - 276
src/views/live-stream/index.vue

@@ -4,10 +4,15 @@
     <div class="search-form">
     <div class="search-form">
       <el-form :model="searchForm" inline>
       <el-form :model="searchForm" inline>
         <el-form-item>
         <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-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-form-item>
         <el-form-item>
           <el-select v-model="searchForm.lssId" placeholder="LSS" clearable filterable style="width: 180px">
           <el-select v-model="searchForm.lssId" placeholder="LSS" clearable filterable style="width: 180px">
@@ -15,11 +20,16 @@
           </el-select>
           </el-select>
         </el-form-item>
         </el-form-item>
         <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-form-item>
         <el-form-item>
           <el-button type="primary" :icon="Search" @click="handleSearch">{{ t('查询') }}</el-button>
           <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-button type="primary" :icon="Plus" @click="handleAdd">{{ t('新增') }}</el-button>
         </el-form-item>
         </el-form-item>
       </el-form>
       </el-form>
@@ -61,12 +71,13 @@
             <el-tag size="small">{{ row.pushMethod || 'ffmpeg' }}</el-tag>
             <el-tag size="small">{{ row.pushMethod || 'ffmpeg' }}</el-tag>
           </template>
           </template>
         </el-table-column>
         </el-table-column>
+
         <el-table-column prop="commandTemplate" :label="t('命令模板')" align="center">
         <el-table-column prop="commandTemplate" :label="t('命令模板')" align="center">
           <template #default="{ row }">
           <template #default="{ row }">
             <el-link type="primary" @click="openCommandDialog(row)">{{ t('查看') }}</el-link>
             <el-link type="primary" @click="openCommandDialog(row)">{{ t('查看') }}</el-link>
           </template>
           </template>
         </el-table-column>
         </el-table-column>
-        <el-table-column :label="t('推流控制')" align="center">
+        <el-table-column :label="t('推流控制')" width="110" align="center">
           <template #default="{ row }">
           <template #default="{ row }">
             {{ row.status === '1' ? t('开启') : t('关闭') }}
             {{ row.status === '1' ? t('开启') : t('关闭') }}
             <el-switch
             <el-switch
@@ -77,17 +88,17 @@
             />
             />
           </template>
           </template>
         </el-table-column>
         </el-table-column>
-        <el-table-column prop="startedAt" :label="t('启动时间')" width="160" align="center">
+        <el-table-column prop="startedAt" :label="t('启动时间')" width="165" align="center">
           <template #default="{ row }">
           <template #default="{ row }">
-            {{ formatDateTime(row.startedAt) }}
+            {{ formatTime(row.startedAt) }}
           </template>
           </template>
         </el-table-column>
         </el-table-column>
-        <el-table-column prop="stoppedAt" :label="t('关闭时间')" width="160" align="center">
+        <el-table-column prop="stoppedAt" :label="t('关闭时间')" width="165" align="center">
           <template #default="{ row }">
           <template #default="{ row }">
-            {{ formatDateTime(row.stoppedAt) }}
+            {{ formatTime(row.stoppedAt) }}
           </template>
           </template>
         </el-table-column>
         </el-table-column>
-        <el-table-column :label="t('操作')" align="center" fixed="right">
+        <el-table-column :label="t('操作')" fixed="right" min-width="130">
           <template #default="{ row }">
           <template #default="{ row }">
             <el-button type="primary" link @click="handleEdit(row)">
             <el-button type="primary" link @click="handleEdit(row)">
               <Icon icon="mdi:note-edit-outline" width="20" height="20" />
               <Icon icon="mdi:note-edit-outline" width="20" height="20" />
@@ -117,277 +128,329 @@
       />
       />
     </div>
     </div>
 
 
-    <!-- 新增/编辑抽屉 -->
+    <!-- 合并的编辑/播放抽屉 -->
     <el-drawer
     <el-drawer
       v-model="drawerVisible"
       v-model="drawerVisible"
       direction="rtl"
       direction="rtl"
-      size="550px"
+      :size="activeDrawerTab === 'edit' ? '800px' : '90%'"
       :with-header="false"
       :with-header="false"
       destroy-on-close
       destroy-on-close
-      class="stream-drawer"
+      class="combined-drawer"
     >
     >
       <div class="drawer-content">
       <div class="drawer-content">
-        <div class="drawer-header">{{ drawerTitle }}</div>
-        <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>
-            <el-form-item label="LSS 节点:" prop="lssId">
-              <el-select v-model="form.lssId" placeholder="请选择 LSS 节点" clearable filterable style="width: 300px">
-                <el-option
-                  v-for="lss in lssOptions"
-                  :key="lss.lssId"
-                  :label="`${lss.lssId} - ${lss.lssName}`"
-                  :value="lss.lssId"
-                />
-              </el-select>
-            </el-form-item>
-            <el-form-item label="摄像头:" prop="cameraId">
-              <el-select v-model="form.cameraId" placeholder="请选择摄像头" clearable filterable style="width: 300px">
-                <el-option
-                  v-for="camera in cameraOptions"
-                  :key="camera.cameraId"
-                  :label="`${camera.cameraId} - ${camera.cameraName}`"
-                  :value="camera.cameraId"
-                />
-              </el-select>
-            </el-form-item>
-            <el-form-item label="推流方式:" prop="pushMethod">
-              <el-select disabled v-model="form.pushMethod" placeholder="请选择" style="width: 300px">
-                <el-option label="ffmpeg" value="ffmpeg" />
-              </el-select>
-            </el-form-item>
-            <!-- <el-form-item label="超时时间:" prop="timeoutSeconds">
-              <el-input-number v-model="form.timeoutSeconds" :min="1" :max="300" placeholder="秒" style="width: 150px" />
-              <span style="margin-left: 8px; color: #909399">秒</span>
-            </el-form-item> -->
-            <el-form-item label="命令模板:" prop="commandTemplate">
-              <div class="code-editor-wrapper">
+        <!-- 顶部 Tabs -->
+        <el-tabs v-model="activeDrawerTab" class="drawer-tabs">
+          <el-tab-pane :label="t('编辑')" name="edit" />
+          <el-tab-pane :label="t('播放')" name="play" :disabled="!isEdit" />
+        </el-tabs>
+
+        <!-- 编辑 Tab 内容 -->
+        <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="t('名称') + ':'" prop="name">
+                <el-input v-model="form.name" :placeholder="t('例如: 测试推流-001')" style="width: 300px" />
+              </el-form-item>
+              <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"
+                    :label="`${lss.lssId} - ${lss.lssName}`"
+                    :value="lss.lssId"
+                  />
+                </el-select>
+              </el-form-item>
+              <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"
+                    :label="`${camera.cameraId} - ${camera.cameraName}`"
+                    :value="camera.cameraId"
+                  />
+                </el-select>
+              </el-form-item>
+              <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="t('命令模板') + ':'" prop="commandTemplate">
                 <CodeEditor
                 <CodeEditor
                   v-model="form.commandTemplate"
                   v-model="form.commandTemplate"
                   language="bash"
                   language="bash"
-                  height="200px"
+                  height="400px"
                   placeholder="#!/bin/bash&#10;# FFmpeg 命令模板,可使用 {RTSP_URL} 等变量"
                   placeholder="#!/bin/bash&#10;# FFmpeg 命令模板,可使用 {RTSP_URL} 等变量"
                 />
                 />
-              </div>
-            </el-form-item>
-          </el-form>
-        </div>
-        <div class="drawer-footer">
-          <el-button @click="drawerVisible = false">{{ t('取消') }}</el-button>
-          <el-button type="primary" :loading="submitLoading" @click="handleSubmit">
-            {{ isEdit ? t('更新') : t('添加') }}
-          </el-button>
-        </div>
-      </div>
-    </el-drawer>
-
-    <!-- 命令模板查看/编辑弹窗 -->
-    <el-dialog v-model="commandDialogVisible" :title="t('命令模板')" width="800px" destroy-on-close>
-      <CodeEditor
-        v-model="currentCommandTemplate"
-        language="bash"
-        height="450px"
-        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>
-      </template>
-    </el-dialog>
-
-    <!-- 流媒体播放抽屉 -->
-    <el-drawer
-      v-model="mediaDrawerVisible"
-      direction="rtl"
-      size="90%"
-      :with-header="false"
-      destroy-on-close
-      class="media-drawer"
-    >
-      <!-- 左上角关闭按钮 -->
-      <div class="drawer-close-btn" @click="mediaDrawerVisible = false">
-        <el-icon :size="20">
-          <Close />
-        </el-icon>
-      </div>
-      <div class="media-drawer-content">
-        <!-- 左侧:视频播放区域 -->
-        <div class="video-area">
-          <div class="video-header">
-            <div class="header-left">
-              <span class="title">{{ currentMediaStream?.name || t('流媒体播放') }}</span>
-              <el-tag v-if="playbackInfo.isLive" type="success" size="small">{{ t('直播中') }}</el-tag>
-              <el-tag v-else type="info" size="small">{{ t('离线') }}</el-tag>
-            </div>
-            <el-button type="danger" size="small" @click="mediaDrawerVisible = false">
-              <Icon icon="mdi:close" width="16" height="16" />
-              {{ t('关闭') }}
-            </el-button>
-          </div>
-          <div class="player-container">
-            <div v-if="!playbackInfo.videoId" class="player-placeholder">
-              <el-icon :size="80" color="#666">
-                <VideoPlay />
-              </el-icon>
-              <p>{{ t('暂无视频流') }}</p>
-            </div>
-            <VideoPlayer
-              v-else
-              ref="playerRef"
-              player-type="cloudflare"
-              :video-id="playbackInfo.videoId"
-              :customer-domain="playbackInfo.customerDomain"
-              :use-iframe="true"
-              :autoplay="playConfig.autoplay"
-              :muted="playConfig.muted"
-              :controls="true"
-            />
+              </el-form-item>
+            </el-form>
           </div>
           </div>
-          <!-- 底部播放控制 -->
-          <div class="player-controls">
-            <el-button type="primary" size="small" @click="handlePlay">{{ t('播放') }}</el-button>
-            <el-button size="small" @click="handlePause">{{ t('暂停') }}</el-button>
-            <el-button type="danger" size="small" @click="handlePlayerStop">{{ t('停止') }}</el-button>
-            <el-button size="small" @click="handleScreenshot">{{ t('截图') }}</el-button>
-            <el-button size="small" @click="handleFullscreen">{{ t('全屏') }}</el-button>
-            <el-switch
-              v-model="playConfig.muted"
-              :active-text="t('静音')"
-              :inactive-text="t('有声')"
-              style="margin-left: 16px"
-            />
+          <div class="drawer-footer">
+            <el-button @click="drawerVisible = false">{{ t('取消') }}</el-button>
+            <el-button type="primary" :loading="submitLoading" @click="handleSubmit">
+              {{ isEdit ? t('更新') : t('添加') }}
+            </el-button>
           </div>
           </div>
         </div>
         </div>
 
 
-        <!-- 右侧:PTZ 控制面板 -->
-        <div class="control-panel">
-          <!-- PTZ 方向控制 -->
-          <div class="panel-section">
-            <div class="section-title">{{ t('PTZ') }}</div>
-            <div class="ptz-grid">
-              <div
-                class="ptz-btn"
-                @mousedown="handlePTZ('UP_LEFT')"
-                @mouseup="handlePTZStop"
-                @mouseleave="handlePTZStop"
-              >
-                <el-icon>
-                  <TopLeft />
-                </el-icon>
+        <!-- 播放 Tab 内容 -->
+        <div v-show="activeDrawerTab === 'play'" class="tab-content play-content">
+          <div class="media-drawer-content">
+            <!-- 左侧:视频播放区域 -->
+            <div class="video-area">
+              <div class="video-header">
+                <div class="header-left">
+                  <span class="title">{{ currentMediaStream?.name || t('流媒体播放') }}</span>
+                  <el-tag v-if="playbackInfo.isLive" type="success" size="small">{{ t('直播中') }}</el-tag>
+                  <el-tag v-else type="info" size="small">{{ t('离线') }}</el-tag>
+                </div>
+                <!-- <el-button type="danger" size="small" @click="drawerVisible = false">
+                  <Icon icon="mdi:close" width="16" height="16" />
+                  {{ t('关闭') }}
+                </el-button> -->
+                <el-button
+                  v-if="currentMediaStream && currentMediaStream.status === '1'"
+                  type="danger"
+                  size="small"
+                  :loading="streamStopping"
+                  @click="handleStopStreamFromPlayer"
+                >
+                  {{ t('停止推流') }}
+                </el-button>
               </div>
               </div>
-              <div class="ptz-btn" @mousedown="handlePTZ('UP')" @mouseup="handlePTZStop" @mouseleave="handlePTZStop">
-                <el-icon>
-                  <Top />
-                </el-icon>
-              </div>
-              <div
-                class="ptz-btn"
-                @mousedown="handlePTZ('UP_RIGHT')"
-                @mouseup="handlePTZStop"
-                @mouseleave="handlePTZStop"
-              >
-                <el-icon>
-                  <TopRight />
-                </el-icon>
-              </div>
-              <div class="ptz-btn" @mousedown="handlePTZ('LEFT')" @mouseup="handlePTZStop" @mouseleave="handlePTZStop">
-                <el-icon>
-                  <Back />
-                </el-icon>
-              </div>
-              <div class="ptz-btn ptz-center" @click="handlePTZStop">
-                <el-icon>
-                  <Refresh />
-                </el-icon>
-              </div>
-              <div class="ptz-btn" @mousedown="handlePTZ('RIGHT')" @mouseup="handlePTZStop" @mouseleave="handlePTZStop">
-                <el-icon>
-                  <Right />
-                </el-icon>
-              </div>
-              <div
-                class="ptz-btn"
-                @mousedown="handlePTZ('DOWN_LEFT')"
-                @mouseup="handlePTZStop"
-                @mouseleave="handlePTZStop"
-              >
-                <el-icon>
-                  <BottomLeft />
-                </el-icon>
-              </div>
-              <div class="ptz-btn" @mousedown="handlePTZ('DOWN')" @mouseup="handlePTZStop" @mouseleave="handlePTZStop">
-                <el-icon>
-                  <Bottom />
-                </el-icon>
+              <div class="player-container">
+                <div v-if="!playbackInfo.videoId" class="player-placeholder">
+                  <el-icon :size="80" color="#666">
+                    <VideoPlay />
+                  </el-icon>
+                  <p>{{ t('暂无视频流') }}</p>
+                </div>
+                <VideoPlayer
+                  v-else
+                  ref="playerRef"
+                  player-type="cloudflare"
+                  :video-id="playbackInfo.videoId"
+                  :customer-domain="playbackInfo.customerDomain"
+                  :use-iframe="true"
+                  :autoplay="playConfig.autoplay"
+                  :muted="playConfig.muted"
+                  :controls="true"
+                />
+                <!-- 开始推流按钮 -->
+                <div v-if="currentMediaStream && currentMediaStream.status !== '1'" class="stream-control-overlay">
+                  <el-button type="success" size="large" :loading="streamStarting" @click="handleStartStreamFromPlayer">
+                    {{ t('开始推流') }}
+                  </el-button>
+                </div>
               </div>
               </div>
-              <div
-                class="ptz-btn"
-                @mousedown="handlePTZ('DOWN_RIGHT')"
-                @mouseup="handlePTZStop"
-                @mouseleave="handlePTZStop"
-              >
-                <el-icon>
-                  <BottomRight />
-                </el-icon>
+              <!-- 底部播放控制 -->
+              <div class="player-controls">
+                <el-button type="primary" size="small" @click="handlePlay">{{ t('播放') }}</el-button>
+                <el-button size="small" @click="handlePause">{{ t('暂停') }}</el-button>
+                <el-button type="danger" size="small" @click="handlePlayerStop">{{ t('停止') }}</el-button>
+                <el-button size="small" @click="handleScreenshot">{{ t('截图') }}</el-button>
+                <el-button size="small" @click="handleFullscreen">{{ t('全屏') }}</el-button>
+                <el-switch
+                  v-model="playConfig.muted"
+                  :active-text="t('静音')"
+                  :inactive-text="t('有声')"
+                  style="margin-left: 16px"
+                />
+                <el-divider direction="vertical" />
+                <!-- 停止推流按钮 -->
               </div>
               </div>
             </div>
             </div>
 
 
-            <!-- 缩放按钮 -->
-            <div class="zoom-buttons">
-              <el-button size="small" @mousedown="handleZoomIn" @mouseup="handlePTZStop" @mouseleave="handlePTZStop">
-                <el-icon>
-                  <ZoomIn />
-                </el-icon>
-              </el-button>
-              <el-button size="small" @mousedown="handleZoomOut" @mouseup="handlePTZStop" @mouseleave="handlePTZStop">
-                <el-icon>
-                  <ZoomOut />
-                </el-icon>
-              </el-button>
-            </div>
-
-            <!-- 速度滑块 -->
-            <div class="speed-slider">
-              <span class="label">{{ t('速度') }}</span>
-              <el-slider v-model="ptzSpeed" :min="10" :max="100" :step="10" size="small" />
-              <span class="value">{{ ptzSpeed }}</span>
-            </div>
-          </div>
+            <!-- 右侧:PTZ 控制面板 -->
+            <div class="control-panel">
+              <!-- PTZ 方向控制 -->
+              <div class="panel-section">
+                <div class="section-title">{{ t('PTZ') }}</div>
+                <div class="ptz-grid">
+                  <div
+                    class="ptz-btn"
+                    @mousedown="handlePTZ('UP_LEFT')"
+                    @mouseup="handlePTZStop"
+                    @mouseleave="handlePTZStop"
+                  >
+                    <el-icon>
+                      <TopLeft />
+                    </el-icon>
+                  </div>
+                  <div
+                    class="ptz-btn"
+                    @mousedown="handlePTZ('UP')"
+                    @mouseup="handlePTZStop"
+                    @mouseleave="handlePTZStop"
+                  >
+                    <el-icon>
+                      <Top />
+                    </el-icon>
+                  </div>
+                  <div
+                    class="ptz-btn"
+                    @mousedown="handlePTZ('UP_RIGHT')"
+                    @mouseup="handlePTZStop"
+                    @mouseleave="handlePTZStop"
+                  >
+                    <el-icon>
+                      <TopRight />
+                    </el-icon>
+                  </div>
+                  <div
+                    class="ptz-btn"
+                    @mousedown="handlePTZ('LEFT')"
+                    @mouseup="handlePTZStop"
+                    @mouseleave="handlePTZStop"
+                  >
+                    <el-icon>
+                      <Back />
+                    </el-icon>
+                  </div>
+                  <div class="ptz-btn ptz-center" @click="handlePTZStop">
+                    <el-icon>
+                      <Refresh />
+                    </el-icon>
+                  </div>
+                  <div
+                    class="ptz-btn"
+                    @mousedown="handlePTZ('RIGHT')"
+                    @mouseup="handlePTZStop"
+                    @mouseleave="handlePTZStop"
+                  >
+                    <el-icon>
+                      <Right />
+                    </el-icon>
+                  </div>
+                  <div
+                    class="ptz-btn"
+                    @mousedown="handlePTZ('DOWN_LEFT')"
+                    @mouseup="handlePTZStop"
+                    @mouseleave="handlePTZStop"
+                  >
+                    <el-icon>
+                      <BottomLeft />
+                    </el-icon>
+                  </div>
+                  <div
+                    class="ptz-btn"
+                    @mousedown="handlePTZ('DOWN')"
+                    @mouseup="handlePTZStop"
+                    @mouseleave="handlePTZStop"
+                  >
+                    <el-icon>
+                      <Bottom />
+                    </el-icon>
+                  </div>
+                  <div
+                    class="ptz-btn"
+                    @mousedown="handlePTZ('DOWN_RIGHT')"
+                    @mouseup="handlePTZStop"
+                    @mouseleave="handlePTZStop"
+                  >
+                    <el-icon>
+                      <BottomRight />
+                    </el-icon>
+                  </div>
+                </div>
+
+                <!-- 缩放按钮 -->
+                <div class="zoom-buttons">
+                  <el-button
+                    size="small"
+                    @mousedown="handleZoomIn"
+                    @mouseup="handlePTZStop"
+                    @mouseleave="handlePTZStop"
+                  >
+                    <el-icon>
+                      <ZoomIn />
+                    </el-icon>
+                  </el-button>
+                  <el-button
+                    size="small"
+                    @mousedown="handleZoomOut"
+                    @mouseup="handlePTZStop"
+                    @mouseleave="handlePTZStop"
+                  >
+                    <el-icon>
+                      <ZoomOut />
+                    </el-icon>
+                  </el-button>
+                </div>
+
+                <!-- 速度滑块 -->
+                <div class="speed-slider">
+                  <span class="label">{{ t('速度') }}</span>
+                  <el-slider v-model="ptzSpeed" :min="10" :max="100" :step="10" size="small" />
+                  <span class="value">{{ ptzSpeed }}</span>
+                </div>
+              </div>
 
 
-          <!-- 预置位列表 -->
-          <div class="panel-section preset-section">
-            <div class="section-title">
-              <span>{{ t('预置位') }}</span>
-              <el-button type="primary" link size="small" @click="loadPresets" :loading="presetsLoading">
-                <el-icon>
-                  <Refresh />
-                </el-icon>
-              </el-button>
-            </div>
-            <div class="preset-list" v-loading="presetsLoading">
-              <div
-                v-for="preset in presetList"
-                :key="preset.token"
-                :class="['preset-item', { active: activePresetToken === preset.token }]"
-                @click="handleGotoPreset(preset)"
-              >
-                <span class="preset-index">{{ preset.token }}</span>
-                <span class="preset-name">{{ preset.name || `Preset ${preset.token}` }}</span>
+              <!-- 预置位列表 -->
+              <div class="panel-section preset-section">
+                <div class="section-title">
+                  <span>{{ t('预置位') }}</span>
+                  <el-button type="primary" link size="small" @click="loadPresets" :loading="presetsLoading">
+                    <el-icon>
+                      <Refresh />
+                    </el-icon>
+                  </el-button>
+                </div>
+                <div class="preset-list" v-loading="presetsLoading">
+                  <div
+                    v-for="preset in presetList"
+                    :key="preset.token"
+                    :class="['preset-item', { active: activePresetToken === preset.token }]"
+                    @click="handleGotoPreset(preset)"
+                  >
+                    <span class="preset-index">{{ preset.token }}</span>
+                    <span class="preset-name">{{ preset.name || `Preset ${preset.token}` }}</span>
+                  </div>
+                  <el-empty
+                    v-if="!presetsLoading && presetList.length === 0"
+                    :description="t('暂无预置位')"
+                    :image-size="60"
+                  />
+                </div>
               </div>
               </div>
-              <el-empty
-                v-if="!presetsLoading && presetList.length === 0"
-                :description="t('暂无预置位')"
-                :image-size="60"
-              />
             </div>
             </div>
           </div>
           </div>
         </div>
         </div>
       </div>
       </div>
     </el-drawer>
     </el-drawer>
+
+    <!-- 命令模板查看/编辑抽屉 -->
+    <el-drawer v-model="commandDialogVisible" :title="t('命令模板')" direction="rtl" size="800px" destroy-on-close>
+      <div class="command-template-container">
+        <CodeEditor
+          v-model="currentCommandTemplate"
+          language="bash"
+          height="450px"
+          placeholder="#!/bin/bash&#10;# FFmpeg 推流命令模板"
+        />
+      </div>
+      <template #footer>
+        <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-drawer>
   </div>
   </div>
 </template>
 </template>
 
 
@@ -414,8 +477,8 @@ import {
   Close
   Close
 } from '@element-plus/icons-vue'
 } from '@element-plus/icons-vue'
 import { Icon } from '@iconify/vue'
 import { Icon } from '@iconify/vue'
-import dayjs from 'dayjs'
 import { useI18n } from 'vue-i18n'
 import { useI18n } from 'vue-i18n'
+import { formatTime } from '@/utils/dayjs'
 import { listLiveStreams, addLiveStream, updateLiveStream, deleteLiveStream } from '@/api/live-stream'
 import { listLiveStreams, addLiveStream, updateLiveStream, deleteLiveStream } from '@/api/live-stream'
 import { listAllLssNodes } from '@/api/lss'
 import { listAllLssNodes } from '@/api/lss'
 import { adminListCameras } from '@/api/camera'
 import { adminListCameras } from '@/api/camera'
@@ -429,12 +492,6 @@ const { t } = useI18n({ useScope: 'global' })
 const route = useRoute()
 const route = useRoute()
 const router = useRouter()
 const router = useRouter()
 
 
-// 格式化时间
-function formatDateTime(dateStr: string | undefined): string {
-  if (!dateStr) return '-'
-  return dayjs(dateStr).format('YYYY-MM-DD HH:mm:ss')
-}
-
 const loading = ref(false)
 const loading = ref(false)
 const submitLoading = ref(false)
 const submitLoading = ref(false)
 const streamList = ref<LiveStreamDTO[]>([])
 const streamList = ref<LiveStreamDTO[]>([])
@@ -447,9 +504,11 @@ const currentCommandTemplate = ref('')
 const currentStreamId = ref<number | null>(null)
 const currentStreamId = ref<number | null>(null)
 const commandUpdateLoading = ref(false)
 const commandUpdateLoading = ref(false)
 
 
-// 流媒体播放抽屉
-const mediaDrawerVisible = ref(false)
+// 合并抽屉的 tab 状态
+const activeDrawerTab = ref<'edit' | 'play'>('edit')
 const currentMediaStream = ref<LiveStreamDTO | null>(null)
 const currentMediaStream = ref<LiveStreamDTO | null>(null)
+const streamStarting = ref(false)
+const streamStopping = ref(false)
 const playerRef = ref<InstanceType<typeof VideoPlayer>>()
 const playerRef = ref<InstanceType<typeof VideoPlayer>>()
 const playbackInfo = ref<{
 const playbackInfo = ref<{
   videoId: string
   videoId: string
@@ -641,6 +700,8 @@ function handleAdd() {
     remark: '',
     remark: '',
     enabled: true
     enabled: true
   })
   })
+  currentMediaStream.value = null
+  activeDrawerTab.value = 'edit'
   drawerVisible.value = true
   drawerVisible.value = true
 }
 }
 
 
@@ -657,6 +718,8 @@ function handleEdit(row: LiveStreamDTO) {
     remark: row.remark || '',
     remark: row.remark || '',
     enabled: row.enabled
     enabled: row.enabled
   })
   })
+  currentMediaStream.value = row
+  activeDrawerTab.value = 'edit'
   drawerVisible.value = true
   drawerVisible.value = true
 }
 }
 
 
@@ -813,7 +876,8 @@ async function handleStartStream(row: LiveStreamDTO) {
     const res = await startStreamTask({
     const res = await startStreamTask({
       name: row.name,
       name: row.name,
       lssId: row.lssId,
       lssId: row.lssId,
-      cameraId: row.cameraId
+      cameraId: row.cameraId,
+      commandTemplate: row.commandTemplate
     })
     })
     if (res.success) {
     if (res.success) {
       ElMessage.success(t('推流任务已启动'))
       ElMessage.success(t('推流任务已启动'))
@@ -829,6 +893,90 @@ async function handleStartStream(row: LiveStreamDTO) {
   }
   }
 }
 }
 
 
+// 从播放器窗口启动推流
+async function handleStartStreamFromPlayer() {
+  if (!currentMediaStream.value) return
+
+  if (!currentMediaStream.value.cameraId) {
+    ElMessage.warning(t('请先配置摄像头'))
+    return
+  }
+
+  streamStarting.value = true
+  try {
+    const res = await startStreamTask({
+      name: currentMediaStream.value.name,
+      lssId: currentMediaStream.value.lssId,
+      cameraId: currentMediaStream.value.cameraId,
+      commandTemplate: currentMediaStream.value.commandTemplate
+    })
+    if (res.success) {
+      ElMessage.success(t('推流任务已启动'))
+      // 更新当前流的状态
+      currentMediaStream.value.status = '1'
+      // 刷新播放信息
+      if (currentMediaStream.value.streamSn) {
+        try {
+          const playbackRes = await getStreamPlayback(currentMediaStream.value.streamSn)
+          if (playbackRes.success && playbackRes.data) {
+            playbackInfo.value = {
+              ...playbackInfo.value,
+              hlsUrl: playbackRes.data.hlsUrl,
+              whepUrl: playbackRes.data.whepUrl,
+              isLive: playbackRes.data.isLive
+            }
+          }
+        } catch (e) {
+          console.error('刷新播放信息失败', e)
+        }
+      }
+      getList()
+    } else {
+      ElMessage.error(res.errMessage || t('启动失败'))
+    }
+  } catch (error) {
+    console.error('启动推流失败', error)
+    ElMessage.error(t('启动推流失败'))
+  } finally {
+    streamStarting.value = false
+  }
+}
+
+// 从播放器窗口停止推流
+async function handleStopStreamFromPlayer() {
+  if (!currentMediaStream.value) return
+
+  try {
+    await ElMessageBox.confirm(t('确定要停止该推流任务吗?'), t('提示'), {
+      type: 'warning',
+      confirmButtonText: t('确定'),
+      cancelButtonText: t('取消')
+    })
+
+    streamStopping.value = true
+    const res = await stopStreamTask({
+      taskId: currentMediaStream.value.taskStreamSn,
+      lssId: currentMediaStream.value.lssId
+    })
+    if (res.success) {
+      ElMessage.success(t('推流任务已停止'))
+      // 更新当前流的状态
+      currentMediaStream.value.status = '0'
+      playbackInfo.value.isLive = false
+      getList()
+    } else {
+      ElMessage.error(res.errMessage || t('停止失败'))
+    }
+  } catch (error) {
+    if (error !== 'cancel') {
+      console.error('停止推流失败', error)
+      ElMessage.error(t('停止推流失败'))
+    }
+  } finally {
+    streamStopping.value = false
+  }
+}
+
 // 停止推流
 // 停止推流
 async function handleStopStream(row: LiveStreamDTO) {
 async function handleStopStream(row: LiveStreamDTO) {
   try {
   try {
@@ -876,6 +1024,20 @@ function parsePlaybackUrl(playbackUrl: string): { videoId: string; customerDomai
 async function handleViewCloudflare(row: LiveStreamDTO) {
 async function handleViewCloudflare(row: LiveStreamDTO) {
   currentMediaStream.value = row
   currentMediaStream.value = row
 
 
+  // 同时填充表单数据,以便用户可以切换到编辑 tab
+  Object.assign(form, {
+    id: row.id,
+    name: row.name,
+    lssId: row.lssId || '',
+    cameraId: row.cameraId || '',
+    channelId: row.channelId,
+    pushMethod: row.pushMethod || 'ffmpeg',
+    commandTemplate: row.commandTemplate || '',
+    timeoutSeconds: row.timeoutSeconds || 30,
+    remark: row.remark || '',
+    enabled: row.enabled
+  })
+
   // 默认值
   // 默认值
   let videoId = ''
   let videoId = ''
   let customerDomain = 'customer-pj89kn2ke2tcuh19.cloudflarestream.com'
   let customerDomain = 'customer-pj89kn2ke2tcuh19.cloudflarestream.com'
@@ -924,7 +1086,8 @@ async function handleViewCloudflare(row: LiveStreamDTO) {
     }
     }
   }
   }
 
 
-  mediaDrawerVisible.value = true
+  activeDrawerTab.value = 'play'
+  drawerVisible.value = true
 }
 }
 
 
 // 播放控制
 // 播放控制
@@ -1134,7 +1297,8 @@ onMounted(async () => {
   box-sizing: border-box;
   box-sizing: border-box;
 }
 }
 
 
-.code-editor-wrapper {
+.command-template-container {
+  padding: 0 20px;
 }
 }
 
 
 .search-form {
 .search-form {
@@ -1181,8 +1345,8 @@ onMounted(async () => {
   }
   }
 }
 }
 
 
-// 抽屉样式
-.stream-drawer {
+// 合并抽屉样式
+.combined-drawer {
   :deep(.el-drawer__body) {
   :deep(.el-drawer__body) {
     padding: 0;
     padding: 0;
     display: flex;
     display: flex;
@@ -1197,6 +1361,46 @@ onMounted(async () => {
   height: 100%;
   height: 100%;
 }
 }
 
 
+.drawer-tabs {
+  flex-shrink: 0;
+  padding: 0 20px;
+  border-bottom: 1px solid #e5e7eb;
+
+  :deep(.el-tabs__header) {
+    margin: 0;
+  }
+
+  :deep(.el-tabs__nav-wrap::after) {
+    display: none;
+  }
+
+  :deep(.el-tabs__item) {
+    font-size: 15px;
+    padding: 0 20px;
+    height: 48px;
+    line-height: 48px;
+  }
+}
+
+.tab-content {
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.edit-content {
+  .drawer-body {
+    flex: 1;
+    overflow-y: auto;
+  }
+}
+
+.play-content {
+  //background-color: #f5f7fa;
+}
+
 .drawer-header {
 .drawer-header {
   flex-shrink: 0;
   flex-shrink: 0;
   padding: 16px 20px;
   padding: 16px 20px;
@@ -1268,8 +1472,8 @@ onMounted(async () => {
 .media-drawer-content {
 .media-drawer-content {
   display: flex;
   display: flex;
   height: 100%;
   height: 100%;
-  padding: 16px;
-  gap: 16px;
+  padding: 20px;
+  gap: 8px;
   overflow: hidden;
   overflow: hidden;
 }
 }
 
 
@@ -1279,18 +1483,17 @@ onMounted(async () => {
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
   min-width: 0;
   min-width: 0;
-  background-color: #fff;
-  border-radius: 8px;
+  background-color: #e1e1e1;
+  // border-radius: 0px;
   overflow: hidden;
   overflow: hidden;
-  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
+  // box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
 
 
   .video-header {
   .video-header {
     display: flex;
     display: flex;
     align-items: center;
     align-items: center;
     justify-content: space-between;
     justify-content: space-between;
     padding: 12px 16px;
     padding: 12px 16px;
-    background-color: #fff;
-    border-bottom: 1px solid #e5e7eb;
+    background-color: #f5f7fa;
 
 
     .header-left {
     .header-left {
       display: flex;
       display: flex;
@@ -1312,6 +1515,7 @@ onMounted(async () => {
     display: flex;
     display: flex;
     align-items: center;
     align-items: center;
     justify-content: center;
     justify-content: center;
+    position: relative;
 
 
     .player-placeholder {
     .player-placeholder {
       display: flex;
       display: flex;
@@ -1325,6 +1529,20 @@ onMounted(async () => {
         font-size: 14px;
         font-size: 14px;
       }
       }
     }
     }
+
+    .stream-control-overlay {
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      z-index: 10;
+
+      .el-button {
+        font-size: 18px;
+        padding: 16px 32px;
+        border-radius: 0;
+      }
+    }
   }
   }
 
 
   .player-controls {
   .player-controls {
@@ -1332,8 +1550,7 @@ onMounted(async () => {
     align-items: center;
     align-items: center;
     gap: 8px;
     gap: 8px;
     padding: 12px 16px;
     padding: 12px 16px;
-    background-color: #fff;
-    border-top: 1px solid #e5e7eb;
+    background: #e5e7eb;
   }
   }
 }
 }
 
 

+ 287 - 185
src/views/lss/index.vue

@@ -24,16 +24,20 @@
         <el-form-item>
         <el-form-item>
           <el-select v-model="searchForm.status" :placeholder="t('心跳')" clearable data-id="search-enabled">
           <el-select v-model="searchForm.status" :placeholder="t('心跳')" clearable data-id="search-enabled">
             <el-option :label="t('全部')" value="" />
             <el-option :label="t('全部')" value="" />
-            <el-option :label="t('活跃')" value="active" />
-            <el-option :label="t('待机')" value="hold" />
-            <el-option :label="t('离线')" value="dead" />
+            <el-option :label="t('active')" value="active" />
+            <el-option :label="t('hold')" value="hold" />
+            <el-option :label="t('dead')" value="dead" />
           </el-select>
           </el-select>
         </el-form-item>
         </el-form-item>
         <el-form-item>
         <el-form-item>
-          <el-button type="primary" :icon="Search" data-id="btn-search" @click="handleSearch">
+          <el-button type="primary" data-id="btn-search" @click="handleSearch">
+            <Icon icon="mdi:magnify" width="16" height="16" style="margin-right: 4px" />
             {{ t('查询') }}
             {{ t('查询') }}
           </el-button>
           </el-button>
-          <el-button :icon="RefreshRight" data-id="btn-reset" @click="handleReset">{{ t('重置') }}</el-button>
+          <el-button type="info" data-id="btn-reset" @click="handleReset">
+            <Icon icon="mdi:refresh" width="16" height="16" style="margin-right: 4px" />
+            {{ t('重置') }}
+          </el-button>
         </el-form-item>
         </el-form-item>
       </el-form>
       </el-form>
     </div>
     </div>
@@ -56,17 +60,10 @@
         <el-table-column prop="ip" :label="t('IP')" min-width="180" sortable="custom" show-overflow-tooltip />
         <el-table-column prop="ip" :label="t('IP')" min-width="180" sortable="custom" show-overflow-tooltip />
         <el-table-column :label="t('心跳')" width="220" align="center">
         <el-table-column :label="t('心跳')" width="220" align="center">
           <template #default="{ row }">
           <template #default="{ row }">
-            {{
-              row.status === 'active'
-                ? t('活跃')
-                : row.status === 'hold'
-                ? t('待机')
-                : row.status === 'dead'
-                ? t('离线')
-                : '-'
-            }}
-            |
-            {{ formatTime(row.lastHeartbeatAt) }}
+            <span :class="getHeartbeatClass(row.status)">
+              {{ t(row.status) || '-' }}
+            </span>
+            | {{ formatTime(row.lastHeartbeatAt) }}
           </template>
           </template>
         </el-table-column>
         </el-table-column>
         <el-table-column :label="t('设备列表')" align="center">
         <el-table-column :label="t('设备列表')" align="center">
@@ -100,27 +97,27 @@
     <!-- LSS 详情抽屉 -->
     <!-- LSS 详情抽屉 -->
     <el-drawer v-model="detailDrawerVisible" :title="t('LSS 节点详情')" direction="rtl" size="500px" destroy-on-close>
     <el-drawer v-model="detailDrawerVisible" :title="t('LSS 节点详情')" direction="rtl" size="500px" destroy-on-close>
       <el-descriptions :column="1" border>
       <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">
           <el-tag :type="getStatusTagType(currentLss?.status)" size="small">
             {{ formatStatus(currentLss?.status) }}
             {{ formatStatus(currentLss?.status) }}
           </el-tag>
           </el-tag>
         </el-descriptions-item>
         </el-descriptions-item>
-        <el-descriptions-item label="任务数">
+        <el-descriptions-item :label="t('任务数')">
           {{ currentLss?.currentTasks }} / {{ currentLss?.maxTasks }}
           {{ currentLss?.currentTasks }} / {{ currentLss?.maxTasks }}
         </el-descriptions-item>
         </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">
           <el-tag :type="currentLss?.enabled ? 'success' : 'info'" size="small">
             {{ currentLss?.enabled ? '已启用' : '已禁用' }}
             {{ currentLss?.enabled ? '已启用' : '已禁用' }}
           </el-tag>
           </el-tag>
         </el-descriptions-item>
         </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-descriptions>
     </el-drawer>
     </el-drawer>
 
 
@@ -144,15 +141,15 @@
         <div class="drawer-body">
         <div class="drawer-body">
           <!-- LSS 详情 Tab -->
           <!-- LSS 详情 Tab -->
           <div v-show="editActiveTab === 'detail'" class="lss-detail-form">
           <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 ref="lssEditFormRef" :model="lssEditForm" label-width="auto">
+              <el-form-item :label="t('LSS ID') + ':'">
                 <span class="form-value">{{ currentLss?.lssId }}</span>
                 <span class="form-value">{{ currentLss?.lssId }}</span>
               </el-form-item>
               </el-form-item>
               <el-form-item :label="t('名称') + ':'" prop="lssName">
               <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>
               <el-form-item :label="t('地址') + ':'" prop="address">
               <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>
               <el-form-item :label="t('IP') + ':'">
               <el-form-item :label="t('IP') + ':'">
                 <span class="form-value">{{ currentLss?.ip }}</span>
                 <span class="form-value">{{ currentLss?.ip }}</span>
@@ -173,9 +170,7 @@
                       <div class="tooltip-example">{{ t('Status') }} [yy-mm-dd 00:00:00]</div>
                       <div class="tooltip-example">{{ t('Status') }} [yy-mm-dd 00:00:00]</div>
                     </div>
                     </div>
                   </template>
                   </template>
-                  <el-icon class="heartbeat-info-icon">
-                    <QuestionFilled />
-                  </el-icon>
+                  <Icon icon="mdi:help-circle" class="heartbeat-info-icon" width="16" height="16" />
                 </el-tooltip>
                 </el-tooltip>
               </el-form-item>
               </el-form-item>
               <el-form-item :label="t('ably') + ':'" prop="ably">
               <el-form-item :label="t('ably') + ':'" prop="ably">
@@ -219,20 +214,31 @@
                 <el-form-item>
                 <el-form-item>
                   <el-select v-model="cameraSearchForm.status" :placeholder="t('状态')" clearable style="width: 120px">
                   <el-select v-model="cameraSearchForm.status" :placeholder="t('状态')" clearable style="width: 120px">
                     <el-option :label="t('全部')" value="" />
                     <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-select>
                 </el-form-item>
                 </el-form-item>
                 <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="primary" :icon="Plus" @click="handleAddCamera">{{ t('新增') }}</el-button>
+                  <el-button type="primary" @click="handleCameraSearch">
+                    <Icon icon="mdi:magnify" width="16" height="16" style="margin-right: 4px" />
+                    {{ t('查询') }}
+                  </el-button>
+                  <el-button type="info" @click="handleCameraReset">
+                    <Icon icon="mdi:refresh" width="16" height="16" style="margin-right: 4px" />
+                    {{ t('重置') }}
+                  </el-button>
+                  <el-button type="primary" @click="handleAddCamera">
+                    <Icon icon="mdi:plus" width="16" height="16" style="margin-right: 4px" />
+                    {{ t('新增') }}
+                  </el-button>
                 </el-form-item>
                 </el-form-item>
               </el-form>
               </el-form>
             </div>
             </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 :height="cameraTableHeight">
+              <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="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 prop="cameraName" :label="t('名称')" min-width="100" show-overflow-tooltip />
               <el-table-column :label="t('状态(心跳)')" min-width="140">
               <el-table-column :label="t('状态(心跳)')" min-width="140">
@@ -283,8 +289,17 @@
                 </template>
                 </template>
               </el-table-column>
               </el-table-column>
             </el-table>
             </el-table>
-            <div v-if="cameraList.length > 0" class="camera-count">
-              {{ t('共') }} {{ cameraList.length }} {{ t('个设备') }}
+            <div class="camera-pagination">
+              <el-pagination
+                v-model:current-page="cameraCurrentPage"
+                v-model:page-size="cameraPageSize"
+                :page-sizes="[10, 15, 20, 50, 100]"
+                :total="cameraTotal"
+                layout="total, sizes, prev, pager, next, jumper"
+                background
+                @size-change="handleCameraSizeChange"
+                @current-change="handleCameraPageChange"
+              />
             </div>
             </div>
           </div>
           </div>
 
 
@@ -294,7 +309,7 @@
           </div>
           </div>
         </div>
         </div>
 
 
-        <div class="drawer-footer">
+        <div v-show="editActiveTab === 'detail'" class="drawer-footer">
           <el-button @click="lssEditDrawerVisible = false">{{ t('取消') }}</el-button>
           <el-button @click="lssEditDrawerVisible = false">{{ t('取消') }}</el-button>
           <el-button type="primary" :loading="lssUpdating" @click="handleUpdateLss">{{ t('更新') }}</el-button>
           <el-button type="primary" :loading="lssUpdating" @click="handleUpdateLss">{{ t('更新') }}</el-button>
         </div>
         </div>
@@ -317,29 +332,50 @@
               <el-form :model="cameraSearchForm" inline>
               <el-form :model="cameraSearchForm" inline>
                 <el-form-item>
                 <el-form-item>
                   <el-input
                   <el-input
-                    v-model.trim="cameraSearchForm.keyword"
-                    :placeholder="t('IP / 设备ID / 名称')"
+                    v-model.trim="cameraSearchForm.cameraId"
+                    :placeholder="t('设备ID')"
                     clearable
                     clearable
-                    style="width: 200px"
+                    style="width: 150px"
+                    @keyup.enter="handleCameraSearch"
+                  />
+                </el-form-item>
+                <el-form-item>
+                  <el-input
+                    v-model.trim="cameraSearchForm.cameraName"
+                    :placeholder="t('设备名称')"
+                    clearable
+                    style="width: 150px"
                     @keyup.enter="handleCameraSearch"
                     @keyup.enter="handleCameraSearch"
                   />
                   />
                 </el-form-item>
                 </el-form-item>
                 <el-form-item>
                 <el-form-item>
                   <el-select v-model="cameraSearchForm.status" :placeholder="t('状态')" clearable style="width: 120px">
                   <el-select v-model="cameraSearchForm.status" :placeholder="t('状态')" clearable style="width: 120px">
                     <el-option :label="t('全部')" value="" />
                     <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-select>
                 </el-form-item>
                 </el-form-item>
                 <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="primary" @click="handleCameraSearch">
+                    <Icon icon="mdi:magnify" width="16" height="16" style="margin-right: 4px" />
+                    {{ t('查询') }}
+                  </el-button>
+                  <el-button type="info" @click="handleCameraReset">
+                    <Icon icon="mdi:refresh" width="16" height="16" style="margin-right: 4px" />
+                    {{ t('重置') }}
+                  </el-button>
+                  <el-button type="primary" @click="handleAddCamera">
+                    <Icon icon="mdi:plus" width="16" height="16" style="margin-right: 4px" />
+                    {{ t('新增') }}
+                  </el-button>
                 </el-form-item>
                 </el-form-item>
               </el-form>
               </el-form>
-              <el-button type="primary" :icon="Plus" @click="handleAddCamera">{{ t('新增') }}</el-button>
             </div>
             </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>
+              <template #empty>
+                <el-empty :description="t('暂无关联设备')" />
+              </template>
               <!-- <el-table-column prop="ip" label="本地IP" min-width="110" /> -->
               <!-- <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="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 prop="cameraName" :label="t('名称')" min-width="100" show-overflow-tooltip />
@@ -360,9 +396,9 @@
                   <el-button type="primary" link @click="handleViewRunParams(row)">查看</el-button>
                   <el-button type="primary" link @click="handleViewRunParams(row)">查看</el-button>
                 </template>
                 </template>
               </el-table-column>
               </el-table-column>
-              <el-table-column prop="brand" :label="t('厂商')" min-width="90">
+              <el-table-column prop="vendorName" :label="t('厂商')" min-width="90">
                 <template #default="{ row }">
                 <template #default="{ row }">
-                  {{ formatBrand(row.brand) }}
+                  {{ formatBrand(row.vendorName) }}
                 </template>
                 </template>
               </el-table-column>
               </el-table-column>
               <el-table-column prop="model" :label="t('型号')" min-width="130" show-overflow-tooltip />
               <el-table-column prop="model" :label="t('型号')" min-width="130" show-overflow-tooltip />
@@ -373,8 +409,12 @@
               </el-table-column>
               </el-table-column>
               <el-table-column :label="t('设备控制')" min-width="100" align="center" fixed="right">
               <el-table-column :label="t('设备控制')" min-width="100" align="center" fixed="right">
                 <template #default="{ row }">
                 <template #default="{ row }">
-                  <el-button type="primary" link :icon="Edit" @click="handleEditCamera(row)" />
-                  <el-button type="danger" link :icon="Delete" @click="handleDeleteCamera(row)" />
+                  <el-button type="primary" link @click="handleEditCamera(row)">
+                    <Icon icon="mdi:note-edit-outline" width="20" height="20" />
+                  </el-button>
+                  <el-button type="danger" link @click="handleDeleteCamera(row)">
+                    <Icon icon="mdi:delete" width="20" height="20" />
+                  </el-button>
                   <el-button link :class="['crosshairs-btn', { active: !row.streamSn }]" @click="handleViewCamera(row)">
                   <el-button link :class="['crosshairs-btn', { active: !row.streamSn }]" @click="handleViewCamera(row)">
                     <Icon icon="mdi:crosshairs" />
                     <Icon icon="mdi:crosshairs" />
                   </el-button>
                   </el-button>
@@ -384,52 +424,70 @@
             <div v-if="cameraList.length > 0" class="camera-count">共 {{ cameraList.length }} 个设备</div>
             <div v-if="cameraList.length > 0" class="camera-count">共 {{ cameraList.length }} 个设备</div>
           </div>
           </div>
         </el-tab-pane>
         </el-tab-pane>
-        <el-tab-pane label="推币机列表" name="pusher">
+        <el-tab-pane :label="t('推币机列表')" name="pusher">
           <div class="tab-content-wrapper">
           <div class="tab-content-wrapper">
             <div class="camera-toolbar">
             <div class="camera-toolbar">
               <el-form inline>
               <el-form inline>
                 <el-form-item>
                 <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-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-select>
                 </el-form-item>
                 </el-form-item>
                 <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 icon="mdi:magnify" width="16" height="16" style="margin-right: 4px" />
+                    {{ t('查询') }}
+                  </el-button>
+                  <el-button type="info">
+                    <Icon icon="mdi:refresh" width="16" height="16" style="margin-right: 4px" />
+                    {{ t('重置') }}
+                  </el-button>
                 </el-form-item>
                 </el-form-item>
               </el-form>
               </el-form>
-              <el-button type="primary" :icon="Plus">{{ t('新增') }}</el-button>
+              <el-button type="primary">
+                <Icon icon="mdi:plus" width="16" height="16" style="margin-right: 4px" />
+                {{ t('新增') }}
+              </el-button>
             </div>
             </div>
-            <el-empty description="暂无推币机数据" />
+            <el-empty :description="t('暂无推币机数据')" />
           </div>
           </div>
         </el-tab-pane>
         </el-tab-pane>
-        <el-tab-pane label="其他设备" name="other">
+        <el-tab-pane :label="t('其他设备')" name="other">
           <div class="tab-content-wrapper">
           <div class="tab-content-wrapper">
             <div class="camera-toolbar">
             <div class="camera-toolbar">
               <el-form inline>
               <el-form inline>
                 <el-form-item>
                 <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-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-select>
                 </el-form-item>
                 </el-form-item>
                 <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 icon="mdi:magnify" width="16" height="16" style="margin-right: 4px" />
+                    {{ t('查询') }}
+                  </el-button>
+                  <el-button type="info">
+                    <Icon icon="mdi:refresh" width="16" height="16" style="margin-right: 4px" />
+                    {{ t('重置') }}
+                  </el-button>
                 </el-form-item>
                 </el-form-item>
               </el-form>
               </el-form>
-              <el-button type="primary" :icon="Plus">{{ t('新增') }}</el-button>
+              <el-button type="primary">
+                <Icon icon="mdi:plus" width="16" height="16" style="margin-right: 4px" />
+                {{ t('新增') }}
+              </el-button>
             </div>
             </div>
-            <el-empty description="暂无其他设备数据" />
+            <el-empty :description="t('暂无其他设备数据')" />
           </div>
           </div>
         </el-tab-pane>
         </el-tab-pane>
       </el-tabs>
       </el-tabs>
@@ -438,87 +496,102 @@
     <!-- 摄像头编辑抽屉 -->
     <!-- 摄像头编辑抽屉 -->
     <el-drawer
     <el-drawer
       v-model="cameraDialogVisible"
       v-model="cameraDialogVisible"
-      :title="isEditCamera ? '编辑摄像头' : '添加摄像头'"
+      :title="isEditCamera ? t('摄像头详情') : t('添加摄像头')"
       direction="rtl"
       direction="rtl"
-      size="500px"
+      size="600px"
       :close-on-click-modal="false"
       :close-on-click-modal="false"
+      :show-close="false"
       destroy-on-close
       destroy-on-close
       class="camera-edit-drawer"
       class="camera-edit-drawer"
     >
     >
-      <el-form ref="cameraFormRef" :model="cameraForm" :rules="cameraRules" label-width="100px">
-        <!-- <el-form-item label="IP 地址" prop="ip">
+      <el-form ref="cameraFormRef" :model="cameraForm" :rules="cameraRules" label-width="auto">
+        <div class="camera-form-container">
+          <!-- <el-form-item label="IP 地址" prop="ip">
           <el-input v-model="cameraForm.ip" :disabled="isEditCamera" placeholder="请输入 IP 地址" />
           <el-input v-model="cameraForm.ip" :disabled="isEditCamera" placeholder="请输入 IP 地址" />
         </el-form-item> -->
         </el-form-item> -->
-        <el-form-item label="设备ID" prop="cameraId">
-          <el-input v-model="cameraForm.cameraId" :disabled="isEditCamera" placeholder="请输入设备ID" />
-        </el-form-item>
-        <el-form-item label="设备名称" prop="cameraName">
-          <el-input v-model="cameraForm.cameraName" placeholder="请输入设备名称" />
-        </el-form-item>
-        <el-form-item label="厂商" prop="vendorName">
-          <el-select
-            v-model="cameraForm.vendorName"
-            placeholder="请选择摄像头"
-            style="width: 100%"
-            filterable
-            @change="handleVendorSelect"
-          >
-            <el-option
-              v-for="vendor in [
-                { id: 'hikvision', name: '海康威视' },
-                { id: 'dahua', name: '大华' },
-                { id: 'uniview', name: '宇视' },
-                { id: 'other', name: '其他' }
-              ]"
-              :key="vendor.id"
-              :label="vendor.name"
-              :value="vendor.id"
+          <el-form-item :label="t('设备ID') + ':'" prop="cameraId">
+            <el-input
+              v-model="cameraForm.cameraId"
+              :disabled="isEditCamera"
+              :placeholder="t('请输入设备ID')"
+              style="max-width: 300px"
             />
             />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="型号" prop="model">
-          <el-input v-model="cameraForm.model" placeholder="请输入型号" />
-        </el-form-item>
-        <!-- <el-form-item label="摄像头型号" prop="cameraId">
+          </el-form-item>
+          <el-form-item :label="t('设备名称') + ':'" prop="cameraName">
+            <el-input v-model="cameraForm.cameraName" :placeholder="t('请输入设备名称')" style="max-width: 300px" />
+          </el-form-item>
+          <el-form-item :label="t('厂商') + ':'" prop="vendorName">
+            <el-select
+              v-model="cameraForm.vendorName"
+              :placeholder="t('请选择摄像头')"
+              style="width: 100%; max-width: 300px"
+              filterable
+              @change="handleVendorSelect"
+            >
+              <el-option
+                v-for="vendor in [
+                  { id: 'hikvision', name: '海康威视' },
+                  { id: 'dahua', name: '大华' },
+                  { id: 'uniview', name: '宇视' },
+                  { id: 'other', name: '其他' }
+                ]"
+                :key="vendor.id"
+                :label="vendor.name"
+                :value="vendor.id"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item :label="t('型号') + ':'" prop="model">
+            <el-input v-model="cameraForm.model" :placeholder="t('请输入型号')" style="max-width: 300px" />
+          </el-form-item>
+          <!-- <el-form-item label="摄像头型号" prop="cameraId">
           <el-select v-model="cameraForm.selectedVendorId" placeholder="请选择摄像头" style="width: 100%" filterable
           <el-select v-model="cameraForm.selectedVendorId" placeholder="请选择摄像头" style="width: 100%" filterable
             @change="handleVendorSelect">
             @change="handleVendorSelect">
             <el-option v-for="vendor in cameraVendorList" :key="vendor.id" :label="vendor.name" :value="vendor.id" />
             <el-option v-for="vendor in cameraVendorList" :key="vendor.id" :label="vendor.name" :value="vendor.id" />
           </el-select>
           </el-select>
         </el-form-item> -->
         </el-form-item> -->
-        <!-- <el-form-item label="名称" prop="name">
+          <!-- <el-form-item label="名称" prop="name">
           <el-input v-model="cameraForm.name" placeholder="请输入名称" />
           <el-input v-model="cameraForm.name" placeholder="请输入名称" />
         </el-form-item> -->
         </el-form-item> -->
-        <!-- <el-form-item label="端口" prop="port">
+          <!-- <el-form-item label="端口" prop="port">
           <el-input-number v-model="cameraForm.port" :min="1" :max="65535" style="width: 100%" />
           <el-input-number v-model="cameraForm.port" :min="1" :max="65535" style="width: 100%" />
         </el-form-item> -->
         </el-form-item> -->
-        <!-- <el-form-item label="用户名" prop="username">
+          <!-- <el-form-item label="用户名" prop="username">
           <el-input v-model="cameraForm.username" placeholder="请输入用户名" />
           <el-input v-model="cameraForm.username" placeholder="请输入用户名" />
         </el-form-item>
         </el-form-item>
         <el-form-item label="密码" prop="password">
         <el-form-item label="密码" prop="password">
           <el-input v-model="cameraForm.password" type="password" show-password placeholder="请输入密码" />
           <el-input v-model="cameraForm.password" type="password" show-password placeholder="请输入密码" />
         </el-form-item> -->
         </el-form-item> -->
-        <el-form-item label="参数配置">
-          <CodeEditor
-            v-model="cameraForm.paramConfig"
-            language="json"
-            height="200px"
-            placeholder="请输入参数配置 (JSON)"
-          />
-        </el-form-item>
-        <br />
-        <el-form-item label="设备运行参数">
-          <CodeEditor
-            v-model="cameraForm.runtimeParams"
-            language="json"
-            height="200px"
-            placeholder="设备运行参数 (JSON)"
-          />
-        </el-form-item>
+          <el-form-item :label="t('参数配置') + ':'">
+            <CodeEditor
+              v-model="cameraForm.paramConfig"
+              language="json"
+              height="200px"
+              :placeholder="t('请输入参数配置 (JSON)')"
+            />
+          </el-form-item>
+          <br />
+          <el-form-item :label="t('运行参数') + ':'">
+            <CodeEditor
+              v-model="cameraForm.runtimeParams"
+              language="json"
+              height="200px"
+              :placeholder="t('设备运行参数 (JSON)')"
+            />
+          </el-form-item>
+
+          <el-form-item v-if="isEditCamera" :label="t('添加时间') + ':'">
+            {{ formatTime(cameraForm.createdAt) }}
+          </el-form-item>
+        </div>
       </el-form>
       </el-form>
+
       <template #footer>
       <template #footer>
         <div class="drawer-footer">
         <div class="drawer-footer">
           <el-button @click="cameraDialogVisible = false">{{ t('取消') }}</el-button>
           <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">
+            {{ isEditCamera ? t('更新') : t('添加') }}
+          </el-button>
         </div>
         </div>
       </template>
       </template>
     </el-drawer>
     </el-drawer>
@@ -528,17 +601,22 @@
       v-model="paramsDialogVisible"
       v-model="paramsDialogVisible"
       :title="paramsDialogTitle"
       :title="paramsDialogTitle"
       direction="rtl"
       direction="rtl"
-      size="500px"
+      size="650px"
       :close-on-click-modal="false"
       :close-on-click-modal="false"
+      :show-close="false"
       destroy-on-close
       destroy-on-close
       class="params-drawer"
       class="params-drawer"
     >
     >
-      <CodeEditor
-        v-model="paramsContent"
-        language="json"
-        height="500px"
-        :placeholder="paramsDialogType === 'config' ? '请输入参数配置(JSON 格式)' : '请输入运行参数(JSON 格式)'"
-      />
+      <div class="params-content-container">
+        <CodeEditor
+          v-model="paramsContent"
+          language="json"
+          height="500px"
+          :placeholder="
+            paramsDialogType === 'config' ? t('请输入参数配置(JSON 格式)') : t('请输入运行参数(JSON 格式)')
+          "
+        />
+      </div>
       <template #footer>
       <template #footer>
         <div class="drawer-footer">
         <div class="drawer-footer">
           <el-button @click="paramsDialogVisible = false">{{ t('取消') }}</el-button>
           <el-button @click="paramsDialogVisible = false">{{ t('取消') }}</el-button>
@@ -552,7 +630,7 @@
       <el-pagination
       <el-pagination
         v-model:current-page="currentPage"
         v-model:current-page="currentPage"
         v-model:page-size="pageSize"
         v-model:page-size="pageSize"
-        :page-sizes="[10, 20, 50, 100]"
+        :page-sizes="[10, 15, 20, 50, 100]"
         :total="total"
         :total="total"
         layout="total, sizes, prev, pager, next, jumper"
         layout="total, sizes, prev, pager, next, jumper"
         background
         background
@@ -565,12 +643,12 @@
 
 
 <script setup lang="ts">
 <script setup lang="ts">
 import { ref, reactive, onMounted, computed, watch } from 'vue'
 import { ref, reactive, onMounted, computed, watch } from 'vue'
-import { Search, RefreshRight, Delete, View, Edit, VideoCamera, Plus, QuestionFilled } from '@element-plus/icons-vue'
+// Element Plus icons removed - using Iconify instead
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { Icon } from '@iconify/vue'
 import { Icon } from '@iconify/vue'
 import type { FormInstance, FormRules } from 'element-plus'
 import type { FormInstance, FormRules } from 'element-plus'
-import dayjs from 'dayjs'
 import { useI18n } from 'vue-i18n'
 import { useI18n } from 'vue-i18n'
+import { formatTime } from '@/utils/dayjs'
 import { useRouter } from 'vue-router'
 import { useRouter } from 'vue-router'
 import { listLssNodes, deleteLssNode, setLssNodeEnabled, updateLssNode } from '@/api/lss'
 import { listLssNodes, deleteLssNode, setLssNodeEnabled, updateLssNode } from '@/api/lss'
 import { adminListCameras, adminAddCamera, adminUpdateCamera, adminDeleteCamera, adminGetCamera } from '@/api/camera'
 import { adminListCameras, adminAddCamera, adminUpdateCamera, adminDeleteCamera, adminGetCamera } from '@/api/camera'
@@ -621,12 +699,6 @@ function getStatusTagType(status: LssNodeStatus | undefined): 'success' | 'dange
   }
   }
 }
 }
 
 
-// 格式化时间
-function formatTime(time: string | undefined): string {
-  if (!time) return '-'
-  return dayjs(time).format('YYYY-MM-DD HH:mm:ss')
-}
-
 // 格式化摄像头状态
 // 格式化摄像头状态
 function formatCameraStatus(row: CameraInfoDTO): string {
 function formatCameraStatus(row: CameraInfoDTO): string {
   if (row.status === 'active') {
   if (row.status === 'active') {
@@ -669,9 +741,9 @@ async function handleViewCamera(row: CameraInfoDTO) {
 // 格式化品牌
 // 格式化品牌
 function formatBrand(brand: string | undefined): string {
 function formatBrand(brand: string | undefined): string {
   const brandMap: Record<string, string> = {
   const brandMap: Record<string, string> = {
-    hikvision: 'HIKVISION',
-    dahua: 'DAHUA',
-    uniview: 'UNIVIEW',
+    hikvision: '海康威视',
+    dahua: '大华',
+    uniview: '宇视',
     other: '其他'
     other: '其他'
   }
   }
   return brand ? brandMap[brand] || brand.toUpperCase() : '-'
   return brand ? brandMap[brand] || brand.toUpperCase() : '-'
@@ -791,7 +863,7 @@ const lssEditForm = reactive({
 
 
 // 根据当前 tab 计算抽屉宽度
 // 根据当前 tab 计算抽屉宽度
 const editDrawerSize = computed(() => {
 const editDrawerSize = computed(() => {
-  return editActiveTab.value === 'detail' ? '500px' : '80%'
+  return editActiveTab.value === 'detail' ? '800px' : '80%'
 })
 })
 
 
 // 设备列表抽屉状态
 // 设备列表抽屉状态
@@ -801,6 +873,16 @@ const deviceActiveTab = ref('camera')
 const cameraList = ref<CameraInfoDTO[]>([])
 const cameraList = ref<CameraInfoDTO[]>([])
 const cameraVendorList = ref<CameraVendorDTO[]>([])
 const cameraVendorList = ref<CameraVendorDTO[]>([])
 
 
+// 摄像头分页
+const cameraCurrentPage = ref(1)
+const cameraPageSize = ref(15)
+const cameraTotal = ref(0)
+
+// 摄像头表格高度 (视口高度 - 顶部导航 - tabs - 搜索栏 - 分页 - padding)
+const cameraTableHeight = computed(() => {
+  return 'calc(100vh - 238px)'
+})
+
 // 摄像头搜索表单
 // 摄像头搜索表单
 const cameraSearchForm = reactive({
 const cameraSearchForm = reactive({
   cameraId: '',
   cameraId: '',
@@ -841,7 +923,9 @@ const cameraForm = reactive({
   remark: '',
   remark: '',
   enabled: true,
   enabled: true,
   paramConfig: '',
   paramConfig: '',
-  runtimeParams: ''
+  runtimeParams: '',
+  createdAt: '',
+  updatedAt: ''
 })
 })
 
 
 // 摄像头表单验证规则(动态)
 // 摄像头表单验证规则(动态)
@@ -871,7 +955,7 @@ const searchForm = reactive<{
 
 
 // 分页相关
 // 分页相关
 const currentPage = ref(1)
 const currentPage = ref(1)
-const pageSize = ref(20)
+const pageSize = ref(15)
 const total = ref(0)
 const total = ref(0)
 
 
 async function getList() {
 async function getList() {
@@ -923,6 +1007,8 @@ function handleReset() {
   searchForm.lssId = ''
   searchForm.lssId = ''
   searchForm.lssName = ''
   searchForm.lssName = ''
   searchForm.status = ''
   searchForm.status = ''
+  sortState.sortBy = ''
+  sortState.sortDir = undefined
   currentPage.value = 1
   currentPage.value = 1
   getList()
   getList()
 }
 }
@@ -1009,7 +1095,11 @@ async function loadCameraList() {
   cameraList.value = []
   cameraList.value = []
 
 
   try {
   try {
-    const params: any = { lssId: currentLss.value.lssId }
+    const params: any = {
+      lssId: currentLss.value.lssId,
+      page: cameraCurrentPage.value,
+      size: cameraPageSize.value
+    }
 
 
     if (cameraSearchForm.cameraId) {
     if (cameraSearchForm.cameraId) {
       params.cameraId = cameraSearchForm.cameraId
       params.cameraId = cameraSearchForm.cameraId
@@ -1026,6 +1116,7 @@ async function loadCameraList() {
     const res = await adminListCameras(params)
     const res = await adminListCameras(params)
     if (res.success && res.data) {
     if (res.success && res.data) {
       cameraList.value = res.data.list || []
       cameraList.value = res.data.list || []
+      cameraTotal.value = res.data.total || 0
     } else {
     } else {
       ElMessage.error(res.errMessage || '获取摄像头列表失败')
       ElMessage.error(res.errMessage || '获取摄像头列表失败')
     }
     }
@@ -1038,6 +1129,7 @@ async function loadCameraList() {
 }
 }
 
 
 function handleCameraSearch() {
 function handleCameraSearch() {
+  cameraCurrentPage.value = 1
   loadCameraList()
   loadCameraList()
 }
 }
 
 
@@ -1045,6 +1137,18 @@ function handleCameraReset() {
   cameraSearchForm.cameraId = ''
   cameraSearchForm.cameraId = ''
   cameraSearchForm.cameraName = ''
   cameraSearchForm.cameraName = ''
   cameraSearchForm.status = ''
   cameraSearchForm.status = ''
+  cameraCurrentPage.value = 1
+  loadCameraList()
+}
+
+function handleCameraSizeChange(val: number) {
+  cameraPageSize.value = val
+  cameraCurrentPage.value = 1
+  loadCameraList()
+}
+
+function handleCameraPageChange(val: number) {
+  cameraCurrentPage.value = val
   loadCameraList()
   loadCameraList()
 }
 }
 
 
@@ -1067,6 +1171,8 @@ function resetCameraForm() {
   cameraForm.enabled = true
   cameraForm.enabled = true
   cameraForm.paramConfig = ''
   cameraForm.paramConfig = ''
   cameraForm.runtimeParams = ''
   cameraForm.runtimeParams = ''
+  cameraForm.createdAt = ''
+  cameraForm.updatedAt = ''
   cameraFormRef.value?.clearValidate()
   cameraFormRef.value?.clearValidate()
 }
 }
 
 
@@ -1130,6 +1236,8 @@ async function handleEditCamera(row: CameraInfoDTO) {
     cameraForm.model = camera.model || ''
     cameraForm.model = camera.model || ''
     cameraForm.channelNo = camera.channelNo || ''
     cameraForm.channelNo = camera.channelNo || ''
     cameraForm.remark = camera.remark || ''
     cameraForm.remark = camera.remark || ''
+    cameraForm.createdAt = camera.createdAt || ''
+    cameraForm.updatedAt = camera.updatedAt || ''
     cameraForm.enabled = camera.enabled
     cameraForm.enabled = camera.enabled
     cameraForm.paramConfig = camera.paramConfig || ''
     cameraForm.paramConfig = camera.paramConfig || ''
     cameraForm.runtimeParams = camera.runtimeParams || ''
     cameraForm.runtimeParams = camera.runtimeParams || ''
@@ -1225,9 +1333,14 @@ async function handleSubmitCamera() {
 
 
 async function handleDeleteCamera(row: CameraInfoDTO) {
 async function handleDeleteCamera(row: CameraInfoDTO) {
   try {
   try {
-    await ElMessageBox.confirm(`确定要删除摄像头 "${row.name}" 吗?`, '提示', {
-      type: 'warning'
-    })
+    await ElMessageBox.confirm(
+      `你确定要删除这个设备吗?<br/>设备ID:${row.cameraId}<br/>设备名称:${row.cameraName}`,
+      '提示',
+      {
+        type: 'warning',
+        dangerouslyUseHTMLString: true
+      }
+    )
     const res = await adminDeleteCamera(row.id)
     const res = await adminDeleteCamera(row.id)
     if (res.success) {
     if (res.success) {
       ElMessage.success('删除成功')
       ElMessage.success('删除成功')
@@ -1286,8 +1399,10 @@ async function handleDelete(row: LssNodeDTO) {
 // 监听 tab 切换,加载对应数据
 // 监听 tab 切换,加载对应数据
 watch(editActiveTab, (newTab) => {
 watch(editActiveTab, (newTab) => {
   if (newTab === 'camera' && currentLss.value) {
   if (newTab === 'camera' && currentLss.value) {
-    cameraSearchForm.keyword = ''
+    cameraSearchForm.cameraId = ''
+    cameraSearchForm.cameraName = ''
     cameraSearchForm.status = ''
     cameraSearchForm.status = ''
+    cameraCurrentPage.value = 1
     loadCameraList()
     loadCameraList()
   }
   }
 })
 })
@@ -1304,6 +1419,14 @@ onMounted(() => {
   box-sizing: border-box;
   box-sizing: border-box;
 }
 }
 
 
+.camera-form-container {
+  padding: 0 20px;
+}
+
+.params-content-container {
+  padding: 0 20px;
+}
+
 .search-form {
 .search-form {
   flex-shrink: 0;
   flex-shrink: 0;
   margin-bottom: 16px;
   margin-bottom: 16px;
@@ -1348,11 +1471,10 @@ onMounted(() => {
   }
   }
 }
 }
 
 
-.camera-count {
+.camera-pagination {
   margin-top: 16px;
   margin-top: 16px;
-  text-align: right;
-  color: #666;
-  font-size: 14px;
+  display: flex;
+  justify-content: flex-end;
 }
 }
 
 
 .status-text {
 .status-text {
@@ -1380,26 +1502,6 @@ onMounted(() => {
   color: #f56c6c;
   color: #f56c6c;
 }
 }
 
 
-// 抽屉样式
-:deep(.el-drawer) {
-  .el-drawer__header {
-    margin-bottom: 0;
-    padding: 16px 20px;
-    border-bottom: 1px solid #e5e7eb;
-  }
-
-  .el-drawer__body {
-    padding: 16px;
-  }
-
-  .el-descriptions {
-    .el-descriptions__label {
-      width: 100px;
-      font-weight: 600;
-    }
-  }
-}
-
 // LSS 编辑抽屉样式
 // LSS 编辑抽屉样式
 .lss-edit-drawer {
 .lss-edit-drawer {
   :deep(.el-drawer__body) {
   :deep(.el-drawer__body) {

+ 3 - 4
src/views/machine/index.vue

@@ -188,17 +188,16 @@
 import { ref, reactive, onMounted, computed } from 'vue'
 import { ref, reactive, onMounted, computed } from 'vue'
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules, type TableInstance } from 'element-plus'
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules, type TableInstance } from 'element-plus'
 import { Plus, Edit, Delete, Search, RefreshRight } from '@element-plus/icons-vue'
 import { Plus, Edit, Delete, Search, RefreshRight } from '@element-plus/icons-vue'
-import dayjs from 'dayjs'
 import { useI18n } from 'vue-i18n'
 import { useI18n } from 'vue-i18n'
 import { listMachines, addMachine, updateMachine, deleteMachine } from '@/api/machine'
 import { listMachines, addMachine, updateMachine, deleteMachine } from '@/api/machine'
+import { formatTime } from '@/utils/dayjs'
 import type { MachineDTO, MachineAddRequest, MachineUpdateRequest } from '@/types'
 import type { MachineDTO, MachineAddRequest, MachineUpdateRequest } from '@/types'
 
 
 const { t } = useI18n({ useScope: 'global' })
 const { t } = useI18n({ useScope: 'global' })
 
 
-// 格式化时间
+// 格式化时间(不含秒)
 function formatDateTime(dateStr: string | undefined): string {
 function formatDateTime(dateStr: string | undefined): string {
-  if (!dateStr) return '-'
-  return dayjs(dateStr).format('YYYY-MM-DD HH:mm')
+  return formatTime(dateStr, 'YYYY-MM-DD HH:mm')
 }
 }
 
 
 const loading = ref(false)
 const loading = ref(false)

+ 552 - 0
src/views/system/role/index.vue

@@ -0,0 +1,552 @@
+<template>
+  <div class="page-container">
+    <!-- 搜索表单 -->
+    <div class="search-form">
+      <el-form :model="searchForm" inline data-id="search-form">
+        <el-form-item>
+          <el-input
+            v-model.trim="searchForm.roleName"
+            :placeholder="t('角色名称')"
+            clearable
+            @keyup.enter="handleSearch"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-input
+            v-model.trim="searchForm.roleCode"
+            :placeholder="t('角色编码')"
+            clearable
+            @keyup.enter="handleSearch"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-select v-model="searchForm.status" :placeholder="t('状态')" clearable>
+            <el-option :label="t('全部')" value="" />
+            <el-option :label="t('启用')" value="enabled" />
+            <el-option :label="t('禁用')" value="disabled" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="handleSearch">
+            <Icon icon="mdi:magnify" width="16" height="16" style="margin-right: 4px" />
+            {{ t('查询') }}
+          </el-button>
+          <el-button type="info" @click="handleReset">
+            <Icon icon="mdi:refresh" width="16" height="16" style="margin-right: 4px" />
+            {{ t('重置') }}
+          </el-button>
+          <el-button type="primary" @click="handleAdd">
+            <Icon icon="mdi:plus" width="16" height="16" style="margin-right: 4px" />
+            {{ t('新增') }}
+          </el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+
+    <!-- 数据表格 -->
+    <div class="table-wrapper">
+      <el-table
+        ref="tableRef"
+        v-loading="loading"
+        :data="roleList"
+        stripe
+        size="default"
+        height="100%"
+        @sort-change="handleSortChange"
+      >
+        <el-table-column prop="id" :label="t('ID')" width="80" />
+        <el-table-column
+          prop="roleName"
+          :label="t('角色名称')"
+          min-width="120"
+          sortable="custom"
+          show-overflow-tooltip
+        />
+        <el-table-column prop="roleCode" :label="t('角色编码')" min-width="120" show-overflow-tooltip />
+        <el-table-column :label="t('用户数')" width="100" align="center">
+          <template #default="{ row }">
+            <el-tag type="info" size="small">{{ row.userCount }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column :label="t('状态')" width="100" align="center">
+          <template #default="{ row }">
+            <el-tag :type="row.status === 'enabled' ? 'success' : 'danger'" size="small">
+              {{ row.status === 'enabled' ? t('启用') : t('禁用') }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="description" :label="t('描述')" min-width="200" show-overflow-tooltip />
+        <el-table-column :label="t('创建时间')" min-width="160">
+          <template #default="{ row }">
+            {{ formatTime(row.createdAt) }}
+          </template>
+        </el-table-column>
+        <el-table-column :label="t('操作')" width="150" align="center" fixed="right">
+          <template #default="{ row }">
+            <el-button type="primary" link @click="handleEdit(row)">
+              <Icon icon="mdi:note-edit-outline" width="20" height="20" />
+            </el-button>
+            <el-button type="primary" link @click="handlePermission(row)">
+              <Icon icon="mdi:shield-key" width="20" height="20" />
+            </el-button>
+            <el-button type="danger" link :disabled="row.roleCode === 'admin'" @click="handleDelete(row)">
+              <Icon icon="mdi:delete" width="20" height="20" />
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+
+    <!-- 分页 -->
+    <div class="pagination-container">
+      <el-pagination
+        v-model:current-page="currentPage"
+        v-model:page-size="pageSize"
+        :page-sizes="[10, 15, 20, 50, 100]"
+        :total="total"
+        layout="total, sizes, prev, pager, next, jumper"
+        background
+        @size-change="handleSizeChange"
+        @current-change="handleCurrentChange"
+      />
+    </div>
+
+    <!-- 角色编辑抽屉 -->
+    <el-drawer
+      v-model="drawerVisible"
+      :title="isEdit ? t('编辑角色') : t('新增角色')"
+      direction="rtl"
+      size="500px"
+      :close-on-click-modal="false"
+      destroy-on-close
+    >
+      <el-form ref="formRef" :model="form" :rules="rules" label-width="auto">
+        <div class="role-form-container">
+          <el-form-item :label="t('角色名称')" prop="roleName">
+            <el-input v-model="form.roleName" :placeholder="t('请输入角色名称')" />
+          </el-form-item>
+          <el-form-item :label="t('角色编码')" prop="roleCode">
+            <el-input v-model="form.roleCode" :disabled="isEdit" :placeholder="t('请输入角色编码')" />
+          </el-form-item>
+          <el-form-item :label="t('排序')" prop="sort">
+            <el-input-number v-model="form.sort" :min="0" :max="999" />
+          </el-form-item>
+          <el-form-item :label="t('状态')" prop="status">
+            <el-radio-group v-model="form.status">
+              <el-radio value="enabled">{{ t('启用') }}</el-radio>
+              <el-radio value="disabled">{{ t('禁用') }}</el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item :label="t('描述')" prop="description">
+            <el-input v-model="form.description" type="textarea" :rows="3" :placeholder="t('请输入描述')" />
+          </el-form-item>
+        </div>
+      </el-form>
+      <template #footer>
+        <div class="drawer-footer">
+          <el-button @click="drawerVisible = false">{{ t('取消') }}</el-button>
+          <el-button type="primary" :loading="submitting" @click="handleSubmit">
+            {{ isEdit ? t('更新') : t('添加') }}
+          </el-button>
+        </div>
+      </template>
+    </el-drawer>
+
+    <!-- 权限配置抽屉 -->
+    <el-drawer
+      v-model="permissionDrawerVisible"
+      :title="`${t('权限配置')} - ${currentRole?.roleName || ''}`"
+      direction="rtl"
+      size="500px"
+      :close-on-click-modal="false"
+      destroy-on-close
+    >
+      <el-tree
+        ref="treeRef"
+        :data="permissionTree"
+        show-checkbox
+        node-key="id"
+        :default-checked-keys="checkedPermissions"
+        :props="{ label: 'name', children: 'children' }"
+      />
+      <template #footer>
+        <div class="drawer-footer">
+          <el-button @click="permissionDrawerVisible = false">{{ t('取消') }}</el-button>
+          <el-button type="primary" :loading="permissionSubmitting" @click="handleSavePermission">
+            {{ t('保存') }}
+          </el-button>
+        </div>
+      </template>
+    </el-drawer>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted, computed } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Icon } from '@iconify/vue'
+import type { FormInstance, FormRules } from 'element-plus'
+import { useI18n } from 'vue-i18n'
+import { formatTime } from '@/utils/dayjs'
+
+const { t } = useI18n({ useScope: 'global' })
+
+// Mock 数据
+interface Role {
+  id: number
+  roleName: string
+  roleCode: string
+  userCount: number
+  status: 'enabled' | 'disabled'
+  description: string
+  sort: number
+  createdAt: string
+}
+
+interface Permission {
+  id: number
+  name: string
+  children?: Permission[]
+}
+
+const mockRoles: Role[] = [
+  {
+    id: 1,
+    roleName: '管理员',
+    roleCode: 'admin',
+    userCount: 1,
+    status: 'enabled',
+    description: '系统管理员,拥有所有权限',
+    sort: 0,
+    createdAt: '2024-01-01T10:00:00Z'
+  },
+  {
+    id: 2,
+    roleName: '操作员',
+    roleCode: 'operator',
+    userCount: 2,
+    status: 'enabled',
+    description: '可以操作设备和查看数据',
+    sort: 1,
+    createdAt: '2024-01-15T14:30:00Z'
+  },
+  {
+    id: 3,
+    roleName: '查看者',
+    roleCode: 'viewer',
+    userCount: 3,
+    status: 'enabled',
+    description: '只能查看数据,无法操作',
+    sort: 2,
+    createdAt: '2024-02-01T09:00:00Z'
+  },
+  {
+    id: 4,
+    roleName: '测试角色',
+    roleCode: 'test',
+    userCount: 0,
+    status: 'disabled',
+    description: '测试用角色',
+    sort: 99,
+    createdAt: '2024-02-15T16:00:00Z'
+  }
+]
+
+const mockPermissions: Permission[] = [
+  {
+    id: 1,
+    name: '仪表盘',
+    children: [{ id: 11, name: '查看仪表盘' }]
+  },
+  {
+    id: 2,
+    name: 'LSS 管理',
+    children: [
+      { id: 21, name: '查看 LSS 列表' },
+      { id: 22, name: '编辑 LSS' },
+      { id: 23, name: '删除 LSS' }
+    ]
+  },
+  {
+    id: 3,
+    name: '设备管理',
+    children: [
+      { id: 31, name: '查看设备列表' },
+      { id: 32, name: '添加设备' },
+      { id: 33, name: '编辑设备' },
+      { id: 34, name: '删除设备' }
+    ]
+  },
+  {
+    id: 4,
+    name: '系统管理',
+    children: [
+      { id: 41, name: '用户管理' },
+      { id: 42, name: '角色管理' }
+    ]
+  }
+]
+
+const loading = ref(false)
+const roleList = ref<Role[]>([])
+const tableRef = ref()
+const treeRef = ref()
+
+// 搜索表单
+const searchForm = reactive({
+  roleName: '',
+  roleCode: '',
+  status: '' as '' | 'enabled' | 'disabled'
+})
+
+// 分页
+const currentPage = ref(1)
+const pageSize = ref(15)
+const total = ref(0)
+
+// 排序
+const sortState = reactive({
+  sortBy: '',
+  sortDir: '' as 'ASC' | 'DESC' | ''
+})
+
+// 抽屉
+const drawerVisible = ref(false)
+const isEdit = ref(false)
+const submitting = ref(false)
+const formRef = ref<FormInstance>()
+const currentRole = ref<Role | null>(null)
+
+// 权限配置
+const permissionDrawerVisible = ref(false)
+const permissionSubmitting = ref(false)
+const permissionTree = ref<Permission[]>(mockPermissions)
+const checkedPermissions = ref<number[]>([])
+
+// 表单
+const form = reactive({
+  roleName: '',
+  roleCode: '',
+  sort: 0,
+  status: 'enabled' as 'enabled' | 'disabled',
+  description: ''
+})
+
+// 表单验证规则
+const rules = computed<FormRules>(() => ({
+  roleName: [{ required: true, message: t('请输入角色名称'), trigger: 'blur' }],
+  roleCode: [{ required: true, message: t('请输入角色编码'), trigger: 'blur' }]
+}))
+
+// 获取列表
+async function getList() {
+  loading.value = true
+  try {
+    await new Promise((resolve) => setTimeout(resolve, 300))
+
+    let filtered = [...mockRoles]
+
+    // 搜索过滤
+    if (searchForm.roleName) {
+      filtered = filtered.filter((r) => r.roleName.includes(searchForm.roleName))
+    }
+    if (searchForm.roleCode) {
+      filtered = filtered.filter((r) => r.roleCode.includes(searchForm.roleCode))
+    }
+    if (searchForm.status) {
+      filtered = filtered.filter((r) => r.status === searchForm.status)
+    }
+
+    // 排序
+    if (sortState.sortBy) {
+      filtered.sort((a, b) => {
+        const aVal = a[sortState.sortBy as keyof Role] as string
+        const bVal = b[sortState.sortBy as keyof Role] as string
+        return sortState.sortDir === 'ASC' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal)
+      })
+    }
+
+    total.value = filtered.length
+    const start = (currentPage.value - 1) * pageSize.value
+    roleList.value = filtered.slice(start, start + pageSize.value)
+  } finally {
+    loading.value = false
+  }
+}
+
+function handleSearch() {
+  currentPage.value = 1
+  getList()
+}
+
+function handleReset() {
+  searchForm.roleName = ''
+  searchForm.roleCode = ''
+  searchForm.status = ''
+  currentPage.value = 1
+  getList()
+}
+
+function handleSortChange({ prop, order }: { prop: string; order: 'ascending' | 'descending' | null }) {
+  sortState.sortBy = prop || ''
+  sortState.sortDir = order === 'ascending' ? 'ASC' : order === 'descending' ? 'DESC' : ''
+  getList()
+}
+
+function handleSizeChange(val: number) {
+  pageSize.value = val
+  currentPage.value = 1
+  getList()
+}
+
+function handleCurrentChange(val: number) {
+  currentPage.value = val
+  getList()
+}
+
+function resetForm() {
+  form.roleName = ''
+  form.roleCode = ''
+  form.sort = 0
+  form.status = 'enabled'
+  form.description = ''
+  formRef.value?.clearValidate()
+}
+
+function handleAdd() {
+  isEdit.value = false
+  currentRole.value = null
+  resetForm()
+  drawerVisible.value = true
+}
+
+function handleEdit(row: Role) {
+  isEdit.value = true
+  currentRole.value = row
+  form.roleName = row.roleName
+  form.roleCode = row.roleCode
+  form.sort = row.sort
+  form.status = row.status
+  form.description = row.description
+  drawerVisible.value = true
+}
+
+async function handleSubmit() {
+  if (!formRef.value) return
+
+  await formRef.value.validate(async (valid) => {
+    if (!valid) return
+
+    submitting.value = true
+    try {
+      await new Promise((resolve) => setTimeout(resolve, 500))
+      ElMessage.success(isEdit.value ? t('更新成功') : t('添加成功'))
+      drawerVisible.value = false
+      getList()
+    } finally {
+      submitting.value = false
+    }
+  })
+}
+
+async function handleDelete(row: Role) {
+  if (row.roleCode === 'admin') {
+    ElMessage.warning(t('管理员角色不能删除'))
+    return
+  }
+
+  try {
+    await ElMessageBox.confirm(
+      `你确定要删除这个角色吗?<br/><br/>角色名称:${row.roleName}<br/>角色编码:${row.roleCode}`,
+      t('提示'),
+      {
+        type: 'warning',
+        dangerouslyUseHTMLString: true
+      }
+    )
+    await new Promise((resolve) => setTimeout(resolve, 300))
+    ElMessage.success(t('删除成功'))
+    getList()
+  } catch {
+    // 取消
+  }
+}
+
+function handlePermission(row: Role) {
+  currentRole.value = row
+  // Mock 已选权限
+  if (row.roleCode === 'admin') {
+    checkedPermissions.value = [11, 21, 22, 23, 31, 32, 33, 34, 41, 42]
+  } else if (row.roleCode === 'operator') {
+    checkedPermissions.value = [11, 21, 22, 31, 32, 33]
+  } else if (row.roleCode === 'viewer') {
+    checkedPermissions.value = [11, 21, 31]
+  } else {
+    checkedPermissions.value = []
+  }
+  permissionDrawerVisible.value = true
+}
+
+async function handleSavePermission() {
+  permissionSubmitting.value = true
+  try {
+    await new Promise((resolve) => setTimeout(resolve, 500))
+    ElMessage.success(t('权限配置保存成功'))
+    permissionDrawerVisible.value = false
+  } finally {
+    permissionSubmitting.value = false
+  }
+}
+
+onMounted(() => {
+  getList()
+})
+</script>
+
+<style lang="scss" scoped>
+.page-container {
+  display: flex;
+  flex-direction: column;
+  box-sizing: border-box;
+}
+
+.role-form-container {
+  padding: 0 20px;
+}
+
+.search-form {
+  flex-shrink: 0;
+  margin-bottom: 16px;
+  padding: 16px 16px 4px 16px;
+  background: #f5f7fa;
+
+  :deep(.el-form-item) {
+    margin-bottom: 12px;
+    margin-right: 16px;
+  }
+
+  :deep(.el-input),
+  :deep(.el-select) {
+    width: 160px;
+  }
+}
+
+.table-wrapper {
+  flex: 1;
+  min-height: 0;
+  overflow: hidden;
+}
+
+.pagination-container {
+  flex-shrink: 0;
+  display: flex;
+  justify-content: flex-end;
+  padding-top: 16px;
+}
+
+.drawer-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
+}
+</style>

+ 491 - 0
src/views/system/user/index.vue

@@ -0,0 +1,491 @@
+<template>
+  <div class="page-container">
+    <!-- 搜索表单 -->
+    <div class="search-form">
+      <el-form :model="searchForm" inline data-id="search-form">
+        <el-form-item>
+          <el-input
+            v-model.trim="searchForm.username"
+            :placeholder="t('用户名')"
+            clearable
+            @keyup.enter="handleSearch"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-input v-model.trim="searchForm.realName" :placeholder="t('姓名')" clearable @keyup.enter="handleSearch" />
+        </el-form-item>
+        <el-form-item>
+          <el-select v-model="searchForm.status" :placeholder="t('状态')" clearable>
+            <el-option :label="t('全部')" value="" />
+            <el-option :label="t('启用')" value="enabled" />
+            <el-option :label="t('禁用')" value="disabled" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="handleSearch">
+            <Icon icon="mdi:magnify" width="16" height="16" style="margin-right: 4px" />
+            {{ t('查询') }}
+          </el-button>
+          <el-button type="info" @click="handleReset">
+            <Icon icon="mdi:refresh" width="16" height="16" style="margin-right: 4px" />
+            {{ t('重置') }}
+          </el-button>
+          <el-button type="primary" @click="handleAdd">
+            <Icon icon="mdi:plus" width="16" height="16" style="margin-right: 4px" />
+            {{ t('新增') }}
+          </el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+
+    <!-- 数据表格 -->
+    <div class="table-wrapper">
+      <el-table
+        ref="tableRef"
+        v-loading="loading"
+        :data="userList"
+        stripe
+        size="default"
+        height="100%"
+        @sort-change="handleSortChange"
+      >
+        <el-table-column prop="id" :label="t('ID')" width="80" />
+        <el-table-column prop="username" :label="t('用户名')" min-width="120" sortable="custom" show-overflow-tooltip />
+        <el-table-column prop="realName" :label="t('姓名')" min-width="120" show-overflow-tooltip />
+        <el-table-column prop="email" :label="t('邮箱')" min-width="180" show-overflow-tooltip />
+        <el-table-column prop="phone" :label="t('手机号')" min-width="130" show-overflow-tooltip />
+        <el-table-column :label="t('角色')" min-width="120">
+          <template #default="{ row }">
+            <el-tag v-for="role in row.roles" :key="role" size="small" style="margin-right: 4px">
+              {{ role }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column :label="t('状态')" width="100" align="center">
+          <template #default="{ row }">
+            <el-tag :type="row.status === 'enabled' ? 'success' : 'danger'" size="small">
+              {{ row.status === 'enabled' ? t('启用') : t('禁用') }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column :label="t('创建时间')" min-width="160">
+          <template #default="{ row }">
+            {{ formatTime(row.createdAt) }}
+          </template>
+        </el-table-column>
+        <el-table-column :label="t('操作')" width="150" align="center" fixed="right">
+          <template #default="{ row }">
+            <el-button type="primary" link @click="handleEdit(row)">
+              <Icon icon="mdi:note-edit-outline" width="20" height="20" />
+            </el-button>
+            <el-button type="primary" link @click="handleResetPassword(row)">
+              <Icon icon="mdi:lock-reset" width="20" height="20" />
+            </el-button>
+            <el-button type="danger" link @click="handleDelete(row)">
+              <Icon icon="mdi:delete" width="20" height="20" />
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+
+    <!-- 分页 -->
+    <div class="pagination-container">
+      <el-pagination
+        v-model:current-page="currentPage"
+        v-model:page-size="pageSize"
+        :page-sizes="[10, 15, 20, 50, 100]"
+        :total="total"
+        layout="total, sizes, prev, pager, next, jumper"
+        background
+        @size-change="handleSizeChange"
+        @current-change="handleCurrentChange"
+      />
+    </div>
+
+    <!-- 用户编辑抽屉 -->
+    <el-drawer
+      v-model="drawerVisible"
+      :title="isEdit ? t('编辑用户') : t('新增用户')"
+      direction="rtl"
+      size="500px"
+      :close-on-click-modal="false"
+      destroy-on-close
+    >
+      <el-form ref="formRef" :model="form" :rules="rules" label-width="auto">
+        <div class="role-form-container">
+          <el-form-item :label="t('用户名')" prop="username">
+            <el-input v-model="form.username" :disabled="isEdit" :placeholder="t('请输入用户名')" />
+          </el-form-item>
+          <el-form-item :label="t('姓名')" prop="realName">
+            <el-input v-model="form.realName" :placeholder="t('请输入姓名')" />
+          </el-form-item>
+          <el-form-item :label="t('邮箱')" prop="email">
+            <el-input v-model="form.email" :placeholder="t('请输入邮箱')" />
+          </el-form-item>
+          <el-form-item :label="t('手机号')" prop="phone">
+            <el-input v-model="form.phone" :placeholder="t('请输入手机号')" />
+          </el-form-item>
+          <el-form-item :label="t('角色')" prop="roles">
+            <el-select v-model="form.roles" multiple :placeholder="t('请选择角色')" style="width: 100%">
+              <el-option v-for="role in roleOptions" :key="role.value" :label="role.label" :value="role.value" />
+            </el-select>
+          </el-form-item>
+          <el-form-item v-if="!isEdit" :label="t('密码')" prop="password">
+            <el-input v-model="form.password" type="password" show-password :placeholder="t('请输入密码')" />
+          </el-form-item>
+          <el-form-item :label="t('状态')" prop="status">
+            <el-radio-group v-model="form.status">
+              <el-radio value="enabled">{{ t('启用') }}</el-radio>
+              <el-radio value="disabled">{{ t('禁用') }}</el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item :label="t('备注')" prop="remark">
+            <el-input v-model="form.remark" type="textarea" :rows="3" :placeholder="t('请输入备注')" />
+          </el-form-item>
+        </div>
+      </el-form>
+      <template #footer>
+        <div class="drawer-footer">
+          <el-button @click="drawerVisible = false">{{ t('取消') }}</el-button>
+          <el-button type="primary" :loading="submitting" @click="handleSubmit">
+            {{ isEdit ? t('更新') : t('添加') }}
+          </el-button>
+        </div>
+      </template>
+    </el-drawer>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted, computed } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Icon } from '@iconify/vue'
+import type { FormInstance, FormRules } from 'element-plus'
+import { useI18n } from 'vue-i18n'
+import { formatTime } from '@/utils/dayjs'
+
+const { t } = useI18n({ useScope: 'global' })
+
+// Mock 数据
+interface User {
+  id: number
+  username: string
+  realName: string
+  email: string
+  phone: string
+  roles: string[]
+  status: 'enabled' | 'disabled'
+  createdAt: string
+  remark?: string
+}
+
+const mockUsers: User[] = [
+  {
+    id: 1,
+    username: 'admin',
+    realName: '系统管理员',
+    email: 'admin@example.com',
+    phone: '13800138000',
+    roles: ['管理员'],
+    status: 'enabled',
+    createdAt: '2024-01-01T10:00:00Z'
+  },
+  {
+    id: 2,
+    username: 'operator',
+    realName: '张三',
+    email: 'zhangsan@example.com',
+    phone: '13800138001',
+    roles: ['操作员'],
+    status: 'enabled',
+    createdAt: '2024-01-15T14:30:00Z'
+  },
+  {
+    id: 3,
+    username: 'viewer',
+    realName: '李四',
+    email: 'lisi@example.com',
+    phone: '13800138002',
+    roles: ['查看者'],
+    status: 'enabled',
+    createdAt: '2024-02-01T09:00:00Z'
+  },
+  {
+    id: 4,
+    username: 'test',
+    realName: '王五',
+    email: 'wangwu@example.com',
+    phone: '13800138003',
+    roles: ['操作员', '查看者'],
+    status: 'disabled',
+    createdAt: '2024-02-15T16:00:00Z'
+  },
+  {
+    id: 5,
+    username: 'user001',
+    realName: '赵六',
+    email: 'zhaoliu@example.com',
+    phone: '13800138004',
+    roles: ['查看者'],
+    status: 'enabled',
+    createdAt: '2024-03-01T11:30:00Z'
+  }
+]
+
+const loading = ref(false)
+const userList = ref<User[]>([])
+const tableRef = ref()
+
+// 搜索表单
+const searchForm = reactive({
+  username: '',
+  realName: '',
+  status: '' as '' | 'enabled' | 'disabled'
+})
+
+// 分页
+const currentPage = ref(1)
+const pageSize = ref(15)
+const total = ref(0)
+
+// 排序
+const sortState = reactive({
+  sortBy: '',
+  sortDir: '' as 'ASC' | 'DESC' | ''
+})
+
+// 抽屉
+const drawerVisible = ref(false)
+const isEdit = ref(false)
+const submitting = ref(false)
+const formRef = ref<FormInstance>()
+const currentUser = ref<User | null>(null)
+
+// 角色选项
+const roleOptions = [
+  { label: '管理员', value: '管理员' },
+  { label: '操作员', value: '操作员' },
+  { label: '查看者', value: '查看者' }
+]
+
+// 表单
+const form = reactive({
+  username: '',
+  realName: '',
+  email: '',
+  phone: '',
+  roles: [] as string[],
+  password: '',
+  status: 'enabled' as 'enabled' | 'disabled',
+  remark: ''
+})
+
+// 表单验证规则
+const rules = computed<FormRules>(() => ({
+  username: [{ required: true, message: t('请输入用户名'), trigger: 'blur' }],
+  realName: [{ required: true, message: t('请输入姓名'), trigger: 'blur' }],
+  email: [{ type: 'email', message: t('请输入正确的邮箱'), trigger: 'blur' }],
+  roles: [{ required: true, message: t('请选择角色'), trigger: 'change' }],
+  password: [{ required: !isEdit.value, message: t('请输入密码'), trigger: 'blur' }]
+}))
+
+// 获取列表
+async function getList() {
+  loading.value = true
+  try {
+    // 模拟 API 延迟
+    await new Promise((resolve) => setTimeout(resolve, 300))
+
+    let filtered = [...mockUsers]
+
+    // 搜索过滤
+    if (searchForm.username) {
+      filtered = filtered.filter((u) => u.username.includes(searchForm.username))
+    }
+    if (searchForm.realName) {
+      filtered = filtered.filter((u) => u.realName.includes(searchForm.realName))
+    }
+    if (searchForm.status) {
+      filtered = filtered.filter((u) => u.status === searchForm.status)
+    }
+
+    // 排序
+    if (sortState.sortBy) {
+      filtered.sort((a, b) => {
+        const aVal = a[sortState.sortBy as keyof User] as string
+        const bVal = b[sortState.sortBy as keyof User] as string
+        return sortState.sortDir === 'ASC' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal)
+      })
+    }
+
+    total.value = filtered.length
+    const start = (currentPage.value - 1) * pageSize.value
+    userList.value = filtered.slice(start, start + pageSize.value)
+  } finally {
+    loading.value = false
+  }
+}
+
+function handleSearch() {
+  currentPage.value = 1
+  getList()
+}
+
+function handleReset() {
+  searchForm.username = ''
+  searchForm.realName = ''
+  searchForm.status = ''
+  currentPage.value = 1
+  getList()
+}
+
+function handleSortChange({ prop, order }: { prop: string; order: 'ascending' | 'descending' | null }) {
+  sortState.sortBy = prop || ''
+  sortState.sortDir = order === 'ascending' ? 'ASC' : order === 'descending' ? 'DESC' : ''
+  getList()
+}
+
+function handleSizeChange(val: number) {
+  pageSize.value = val
+  currentPage.value = 1
+  getList()
+}
+
+function handleCurrentChange(val: number) {
+  currentPage.value = val
+  getList()
+}
+
+function resetForm() {
+  form.username = ''
+  form.realName = ''
+  form.email = ''
+  form.phone = ''
+  form.roles = []
+  form.password = ''
+  form.status = 'enabled'
+  form.remark = ''
+  formRef.value?.clearValidate()
+}
+
+function handleAdd() {
+  isEdit.value = false
+  currentUser.value = null
+  resetForm()
+  drawerVisible.value = true
+}
+
+function handleEdit(row: User) {
+  isEdit.value = true
+  currentUser.value = row
+  form.username = row.username
+  form.realName = row.realName
+  form.email = row.email
+  form.phone = row.phone
+  form.roles = [...row.roles]
+  form.status = row.status
+  form.remark = row.remark || ''
+  drawerVisible.value = true
+}
+
+async function handleSubmit() {
+  if (!formRef.value) return
+
+  await formRef.value.validate(async (valid) => {
+    if (!valid) return
+
+    submitting.value = true
+    try {
+      await new Promise((resolve) => setTimeout(resolve, 500))
+      ElMessage.success(isEdit.value ? t('更新成功') : t('添加成功'))
+      drawerVisible.value = false
+      getList()
+    } finally {
+      submitting.value = false
+    }
+  })
+}
+
+async function handleDelete(row: User) {
+  try {
+    await ElMessageBox.confirm(
+      `你确定要删除这个用户吗?<br/><br/>用户名:${row.username}<br/>姓名:${row.realName}`,
+      t('提示'),
+      {
+        type: 'warning',
+        dangerouslyUseHTMLString: true
+      }
+    )
+    await new Promise((resolve) => setTimeout(resolve, 300))
+    ElMessage.success(t('删除成功'))
+    getList()
+  } catch {
+    // 取消
+  }
+}
+
+async function handleResetPassword(row: User) {
+  try {
+    await ElMessageBox.confirm(`确定要重置用户 "${row.username}" 的密码吗?`, t('提示'), {
+      type: 'warning'
+    })
+    await new Promise((resolve) => setTimeout(resolve, 300))
+    ElMessage.success(t('密码已重置为默认密码'))
+  } catch {
+    // 取消
+  }
+}
+
+onMounted(() => {
+  getList()
+})
+</script>
+
+<style lang="scss" scoped>
+.page-container {
+  display: flex;
+  flex-direction: column;
+  box-sizing: border-box;
+}
+
+.role-form-container {
+  padding: 0 20px;
+}
+
+.search-form {
+  flex-shrink: 0;
+  margin-bottom: 16px;
+  padding: 16px 16px 4px 16px;
+  background: #f5f7fa;
+
+  :deep(.el-form-item) {
+    margin-bottom: 12px;
+    margin-right: 16px;
+  }
+
+  :deep(.el-input),
+  :deep(.el-select) {
+    width: 160px;
+  }
+}
+
+.table-wrapper {
+  flex: 1;
+  min-height: 0;
+  overflow: hidden;
+}
+
+.pagination-container {
+  flex-shrink: 0;
+  display: flex;
+  justify-content: flex-end;
+  padding-top: 16px;
+}
+
+.drawer-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
+}
+</style>

+ 157 - 35
tests/e2e/live-stream.spec.ts

@@ -27,7 +27,7 @@ test.describe('LiveStream 管理 - 搜索功能测试', () => {
    */
    */
   test('按 Stream SN 搜索 - 验证参数传递', async ({ page }) => {
   test('按 Stream SN 搜索 - 验证参数传递', async ({ page }) => {
     await login(page)
     await login(page)
-    await page.goto('/live-stream')
+    await page.goto('/live-stream-manage/list')
 
 
     // 等待表格加载
     // 等待表格加载
     await page.waitForSelector('tbody tr', { timeout: 10000 })
     await page.waitForSelector('tbody tr', { timeout: 10000 })
@@ -59,7 +59,7 @@ test.describe('LiveStream 管理 - 搜索功能测试', () => {
    */
    */
   test('按 Name 搜索 - 验证参数传递', async ({ page }) => {
   test('按 Name 搜索 - 验证参数传递', async ({ page }) => {
     await login(page)
     await login(page)
-    await page.goto('/live-stream')
+    await page.goto('/live-stream-manage/list')
 
 
     // 等待表格加载
     // 等待表格加载
     await page.waitForSelector('tbody tr', { timeout: 10000 })
     await page.waitForSelector('tbody tr', { timeout: 10000 })
@@ -75,7 +75,7 @@ test.describe('LiveStream 管理 - 搜索功能测试', () => {
     })
     })
 
 
     // 在 name 搜索框输入
     // 在 name 搜索框输入
-    await page.getByPlaceholder('name').fill('测试流')
+    await page.getByPlaceholder('名称').fill('测试流')
 
 
     // 点击查询
     // 点击查询
     await page.getByRole('button', { name: '查询' }).click()
     await page.getByRole('button', { name: '查询' }).click()
@@ -91,7 +91,7 @@ test.describe('LiveStream 管理 - 搜索功能测试', () => {
    */
    */
   test('按 LSS 搜索 - 验证参数传递', async ({ page }) => {
   test('按 LSS 搜索 - 验证参数传递', async ({ page }) => {
     await login(page)
     await login(page)
-    await page.goto('/live-stream')
+    await page.goto('/live-stream-manage/list')
 
 
     // 等待表格加载
     // 等待表格加载
     await page.waitForSelector('tbody tr', { timeout: 10000 })
     await page.waitForSelector('tbody tr', { timeout: 10000 })
@@ -131,7 +131,7 @@ test.describe('LiveStream 管理 - 搜索功能测试', () => {
    */
    */
   test('按设备ID搜索 - 验证参数传递', async ({ page }) => {
   test('按设备ID搜索 - 验证参数传递', async ({ page }) => {
     await login(page)
     await login(page)
-    await page.goto('/live-stream')
+    await page.goto('/live-stream-manage/list')
 
 
     // 等待表格加载
     // 等待表格加载
     await page.waitForSelector('tbody tr', { timeout: 10000 })
     await page.waitForSelector('tbody tr', { timeout: 10000 })
@@ -163,7 +163,7 @@ test.describe('LiveStream 管理 - 搜索功能测试', () => {
    */
    */
   test('组合搜索 - 多字段同时搜索', async ({ page }) => {
   test('组合搜索 - 多字段同时搜索', async ({ page }) => {
     await login(page)
     await login(page)
-    await page.goto('/live-stream')
+    await page.goto('/live-stream-manage/list')
 
 
     // 等待表格加载
     // 等待表格加载
     await page.waitForSelector('tbody tr', { timeout: 10000 })
     await page.waitForSelector('tbody tr', { timeout: 10000 })
@@ -180,7 +180,7 @@ test.describe('LiveStream 管理 - 搜索功能测试', () => {
 
 
     // 填入所有搜索条件
     // 填入所有搜索条件
     await page.getByPlaceholder('stream sn').fill('stream_001')
     await page.getByPlaceholder('stream sn').fill('stream_001')
-    await page.getByPlaceholder('name').fill('测试')
+    await page.getByPlaceholder('名称').fill('测试')
     await page.getByPlaceholder('设备ID').fill('EEE1')
     await page.getByPlaceholder('设备ID').fill('EEE1')
 
 
     // 点击查询
     // 点击查询
@@ -199,11 +199,11 @@ test.describe('LiveStream 管理 - 搜索功能测试', () => {
    */
    */
   test('重置搜索条件 - 清空所有输入', async ({ page }) => {
   test('重置搜索条件 - 清空所有输入', async ({ page }) => {
     await login(page)
     await login(page)
-    await page.goto('/live-stream')
+    await page.goto('/live-stream-manage/list')
 
 
     // 填入搜索条件
     // 填入搜索条件
     await page.getByPlaceholder('stream sn').fill('test-sn')
     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')
     await page.getByPlaceholder('设备ID').fill('test-device')
 
 
     // 点击重置
     // 点击重置
@@ -233,7 +233,7 @@ test.describe('LiveStream 管理 - BUG 回归测试', () => {
    */
    */
   test('BUG: 按设备ID搜索 EEE1 应该只返回 1 条匹配记录', async ({ page }) => {
   test('BUG: 按设备ID搜索 EEE1 应该只返回 1 条匹配记录', async ({ page }) => {
     await login(page)
     await login(page)
-    await page.goto('/live-stream')
+    await page.goto('/live-stream-manage/list')
 
 
     // 等待表格加载
     // 等待表格加载
     await page.waitForSelector('tbody tr', { timeout: 10000 })
     await page.waitForSelector('tbody tr', { timeout: 10000 })
@@ -265,7 +265,7 @@ test.describe('LiveStream 管理 - BUG 回归测试', () => {
 test.describe('LiveStream 管理 - 页面功能测试', () => {
 test.describe('LiveStream 管理 - 页面功能测试', () => {
   test('LiveStream 管理页面正确显示', async ({ page }) => {
   test('LiveStream 管理页面正确显示', async ({ page }) => {
     await login(page)
     await login(page)
-    await page.goto('/live-stream')
+    await page.goto('/live-stream-manage/list')
 
 
     // 验证页面标题
     // 验证页面标题
     await expect(page.locator('text=LiveStream 管理')).toBeVisible()
     await expect(page.locator('text=LiveStream 管理')).toBeVisible()
@@ -287,7 +287,7 @@ test.describe('LiveStream 管理 - 页面功能测试', () => {
 
 
   test('打开新增 LiveStream 抽屉', async ({ page }) => {
   test('打开新增 LiveStream 抽屉', async ({ page }) => {
     await login(page)
     await login(page)
-    await page.goto('/live-stream')
+    await page.goto('/live-stream-manage/list')
 
 
     // 点击新增按钮
     // 点击新增按钮
     await page.getByRole('button', { name: '新增' }).click()
     await page.getByRole('button', { name: '新增' }).click()
@@ -308,7 +308,7 @@ test.describe('LiveStream 管理 - 页面功能测试', () => {
 
 
   test('分页功能正常', async ({ page }) => {
   test('分页功能正常', async ({ page }) => {
     await login(page)
     await login(page)
-    await page.goto('/live-stream')
+    await page.goto('/live-stream-manage/list')
 
 
     // 等待表格加载
     // 等待表格加载
     await page.waitForTimeout(1000)
     await page.waitForTimeout(1000)
@@ -328,7 +328,7 @@ test.describe('LiveStream 管理 - 页面功能测试', () => {
     await page.getByText('LiveStream 管理').first().click()
     await page.getByText('LiveStream 管理').first().click()
 
 
     // 验证跳转到 LiveStream 管理页面
     // 验证跳转到 LiveStream 管理页面
-    await expect(page).toHaveURL(/\/live-stream/)
+    await expect(page).toHaveURL(/\/live-stream-manage\/list/)
     await expect(page.locator('text=LiveStream 管理')).toBeVisible()
     await expect(page.locator('text=LiveStream 管理')).toBeVisible()
   })
   })
 })
 })
@@ -339,7 +339,7 @@ test.describe('LiveStream 管理 - CodeEditor 组件测试 (Bash模式)', () =>
    */
    */
   test('CodeEditor Bash模式 - 验证头部显示Bash Script标签和复制按钮', async ({ page }) => {
   test('CodeEditor Bash模式 - 验证头部显示Bash Script标签和复制按钮', async ({ page }) => {
     await login(page)
     await login(page)
-    await page.goto('/live-stream')
+    await page.goto('/live-stream-manage/list')
 
 
     // 等待表格加载
     // 等待表格加载
     await page.waitForSelector('tbody tr', { timeout: 10000 })
     await page.waitForSelector('tbody tr', { timeout: 10000 })
@@ -350,7 +350,7 @@ test.describe('LiveStream 管理 - CodeEditor 组件测试 (Bash模式)', () =>
     await viewLink.click()
     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).toBeVisible({ timeout: 5000 })
 
 
     // 验证 CodeEditor 头部显示 Bash Script 标签
     // 验证 CodeEditor 头部显示 Bash Script 标签
@@ -371,7 +371,7 @@ test.describe('LiveStream 管理 - CodeEditor 组件测试 (Bash模式)', () =>
    */
    */
   test('CodeEditor Bash模式 - 复制按钮功能', async ({ page }) => {
   test('CodeEditor Bash模式 - 复制按钮功能', async ({ page }) => {
     await login(page)
     await login(page)
-    await page.goto('/live-stream')
+    await page.goto('/live-stream-manage/list')
 
 
     // 等待表格加载
     // 等待表格加载
     await page.waitForSelector('tbody tr', { timeout: 10000 })
     await page.waitForSelector('tbody tr', { timeout: 10000 })
@@ -381,7 +381,7 @@ test.describe('LiveStream 管理 - CodeEditor 组件测试 (Bash模式)', () =>
     await viewLink.click()
     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).toBeVisible({ timeout: 5000 })
 
 
     // 点击复制按钮
     // 点击复制按钮
@@ -397,7 +397,7 @@ test.describe('LiveStream 管理 - CodeEditor 组件测试 (Bash模式)', () =>
    */
    */
   test('CodeEditor Bash模式 - 编辑器可编辑', async ({ page }) => {
   test('CodeEditor Bash模式 - 编辑器可编辑', async ({ page }) => {
     await login(page)
     await login(page)
-    await page.goto('/live-stream')
+    await page.goto('/live-stream-manage/list')
 
 
     // 等待表格加载
     // 等待表格加载
     await page.waitForSelector('tbody tr', { timeout: 10000 })
     await page.waitForSelector('tbody tr', { timeout: 10000 })
@@ -407,7 +407,7 @@ test.describe('LiveStream 管理 - CodeEditor 组件测试 (Bash模式)', () =>
     await viewLink.click()
     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).toBeVisible({ timeout: 5000 })
 
 
     // 验证编辑器存在并可以编辑
     // 验证编辑器存在并可以编辑
@@ -425,11 +425,11 @@ 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 login(page)
-    await page.goto('/live-stream')
+    await page.goto('/live-stream-manage/list')
 
 
     // 等待表格加载
     // 等待表格加载
     await page.waitForSelector('tbody tr', { timeout: 10000 })
     await page.waitForSelector('tbody tr', { timeout: 10000 })
@@ -439,15 +439,15 @@ test.describe('LiveStream 管理 - CodeEditor 组件测试 (Bash模式)', () =>
     await viewLink.click()
     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).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 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 })
     await expect(dialog).not.toBeVisible({ timeout: 5000 })
   })
   })
 
 
@@ -456,7 +456,7 @@ test.describe('LiveStream 管理 - CodeEditor 组件测试 (Bash模式)', () =>
    */
    */
   test('CodeEditor Bash模式 - 图标显示为绿色', async ({ page }) => {
   test('CodeEditor Bash模式 - 图标显示为绿色', async ({ page }) => {
     await login(page)
     await login(page)
-    await page.goto('/live-stream')
+    await page.goto('/live-stream-manage/list')
 
 
     // 等待表格加载
     // 等待表格加载
     await page.waitForSelector('tbody tr', { timeout: 10000 })
     await page.waitForSelector('tbody tr', { timeout: 10000 })
@@ -466,7 +466,7 @@ test.describe('LiveStream 管理 - CodeEditor 组件测试 (Bash模式)', () =>
     await viewLink.click()
     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).toBeVisible({ timeout: 5000 })
 
 
     // 验证图标有 icon-bash 类(对应绿色)
     // 验证图标有 icon-bash 类(对应绿色)
@@ -479,7 +479,7 @@ test.describe('LiveStream 管理 - CodeEditor 组件测试 (Bash模式)', () =>
    */
    */
   test('CodeEditor Bash模式 - 更新命令模板并验证保存成功', async ({ page }) => {
   test('CodeEditor Bash模式 - 更新命令模板并验证保存成功', async ({ page }) => {
     await login(page)
     await login(page)
-    await page.goto('/live-stream')
+    await page.goto('/live-stream-manage/list')
 
 
     // 等待表格加载
     // 等待表格加载
     await page.waitForSelector('tbody tr', { timeout: 10000 })
     await page.waitForSelector('tbody tr', { timeout: 10000 })
@@ -489,7 +489,7 @@ test.describe('LiveStream 管理 - CodeEditor 组件测试 (Bash模式)', () =>
     await viewLink.click()
     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).toBeVisible({ timeout: 5000 })
 
 
     // 获取编辑器
     // 获取编辑器
@@ -520,7 +520,7 @@ test.describe('LiveStream 管理 - CodeEditor 组件测试 (Bash模式)', () =>
     await viewLink.click()
     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 })
     await expect(dialogReopened).toBeVisible({ timeout: 5000 })
 
 
     // 验证内容包含我们添加的测试注释
     // 验证内容包含我们添加的测试注释
@@ -533,7 +533,7 @@ test.describe('LiveStream 管理 - CodeEditor 组件测试 (Bash模式)', () =>
    */
    */
   test('CodeEditor Bash模式 - 替换全部命令模板并验证保存', async ({ page }) => {
   test('CodeEditor Bash模式 - 替换全部命令模板并验证保存', async ({ page }) => {
     await login(page)
     await login(page)
-    await page.goto('/live-stream')
+    await page.goto('/live-stream-manage/list')
 
 
     // 等待表格加载
     // 等待表格加载
     await page.waitForSelector('tbody tr', { timeout: 10000 })
     await page.waitForSelector('tbody tr', { timeout: 10000 })
@@ -543,7 +543,7 @@ test.describe('LiveStream 管理 - CodeEditor 组件测试 (Bash模式)', () =>
     await viewLink.click()
     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).toBeVisible({ timeout: 5000 })
 
 
     // 获取编辑器
     // 获取编辑器
@@ -579,7 +579,7 @@ ffmpeg -i {RTSP_URL} -c copy output.mp4`
     await page.waitForTimeout(500)
     await page.waitForTimeout(500)
     await viewLink.click()
     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 })
     await expect(dialogReopened).toBeVisible({ timeout: 5000 })
 
 
     const editorContentReopened = dialogReopened.locator('.cm-content')
     const editorContentReopened = dialogReopened.locator('.cm-content')
@@ -700,7 +700,7 @@ test.describe('LiveStream 管理 - 从 LSS 页面创建流程', () => {
     await expect(drawer).not.toBeVisible({ timeout: 3000 })
     await expect(drawer).not.toBeVisible({ timeout: 3000 })
 
 
     // 验证仍在 live-stream 页面
     // 验证仍在 live-stream 页面
-    await expect(page).toHaveURL(/\/live-stream/)
+    await expect(page).toHaveURL(/\/live-stream-manage\/list/)
   })
   })
 
 
   /**
   /**
@@ -734,3 +734,125 @@ test.describe('LiveStream 管理 - 从 LSS 页面创建流程', () => {
     expect(listRequestBody.cameraId).toBe('CAM_SEARCH_TEST')
     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-manage/list')
+
+    // 等待表格加载
+    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-manage/list')
+
+    // 等待表格加载
+    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-manage/list')
+
+    // 等待表格加载
+    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-manage/list')
+
+    // 验证名称搜索框的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-manage/list')
+
+    // 获取重置按钮
+    const resetButton = page.getByRole('button', { name: '重置' })
+    await expect(resetButton).toBeVisible()
+
+    // 验证按钮有 el-button--info 类(灰色按钮)
+    await expect(resetButton).toHaveClass(/el-button--info/)
+  })
+})

+ 763 - 20
tests/e2e/lss.spec.ts

@@ -33,10 +33,10 @@ test.describe('LSS管理 CRUD 测试', () => {
 
 
   test('LSS管理页面正确显示', async ({ page }) => {
   test('LSS管理页面正确显示', async ({ page }) => {
     await login(page)
     await login(page)
-    await page.goto('/lss')
+    await page.goto('/lss-manage/list')
 
 
-    // 验证页面标题
-    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()
     await expect(page.getByPlaceholder('LSS ID')).toBeVisible()
@@ -54,7 +54,7 @@ test.describe('LSS管理 CRUD 测试', () => {
 
 
   test('编辑LSS节点 - 修改Name和Address', async ({ page }) => {
   test('编辑LSS节点 - 修改Name和Address', async ({ page }) => {
     await login(page)
     await login(page)
-    await page.goto('/lss')
+    await page.goto('/lss-manage/list')
 
 
     // 等待表格加载
     // 等待表格加载
     await page.waitForTimeout(1000)
     await page.waitForTimeout(1000)
@@ -100,7 +100,7 @@ test.describe('LSS管理 CRUD 测试', () => {
 
 
   test('查询LSS节点', async ({ page }) => {
   test('查询LSS节点', async ({ page }) => {
     await login(page)
     await login(page)
-    await page.goto('/lss')
+    await page.goto('/lss-manage/list')
 
 
     // 等待表格加载
     // 等待表格加载
     await page.waitForTimeout(1000)
     await page.waitForTimeout(1000)
@@ -128,7 +128,7 @@ test.describe('LSS管理 CRUD 测试', () => {
 
 
   test('重置搜索条件', async ({ page }) => {
   test('重置搜索条件', async ({ page }) => {
     await login(page)
     await login(page)
-    await page.goto('/lss')
+    await page.goto('/lss-manage/list')
 
 
     // 填入搜索条件
     // 填入搜索条件
     await page.getByPlaceholder('LSS ID').fill('test-id')
     await page.getByPlaceholder('LSS ID').fill('test-id')
@@ -145,7 +145,7 @@ test.describe('LSS管理 CRUD 测试', () => {
 
 
   test('查看LSS节点设备列表', async ({ page }) => {
   test('查看LSS节点设备列表', async ({ page }) => {
     await login(page)
     await login(page)
-    await page.goto('/lss')
+    await page.goto('/lss-manage/list')
 
 
     // 等待表格加载
     // 等待表格加载
     await page.waitForTimeout(1000)
     await page.waitForTimeout(1000)
@@ -171,12 +171,12 @@ test.describe('LSS管理 CRUD 测试', () => {
   test('从侧边栏导航到LSS管理', async ({ page }) => {
   test('从侧边栏导航到LSS管理', async ({ page }) => {
     await login(page)
     await login(page)
 
 
-    // 点击侧边栏 LSS 管理菜单项
-    await page.getByText('LSS 管理').first().click()
+    // 点击侧边栏 LSS 列表菜单项 (Bug #4537: 菜单标题从 "LSS 管理" 改为 "LSS 列表")
+    await page.getByText('LSS 列表').first().click()
 
 
     // 验证跳转到 LSS 管理页面
     // 验证跳转到 LSS 管理页面
-    await expect(page).toHaveURL(/\/lss/)
-    await expect(page.locator('text=LSS 管理')).toBeVisible()
+    await expect(page).toHaveURL(/\/lss-manage\/list/)
+    await expect(page.locator('text=LSS 列表')).toBeVisible()
   })
   })
 })
 })
 
 
@@ -203,7 +203,7 @@ test.describe('LSS管理 - CodeEditor 组件测试 (JSON模式)', () => {
    */
    */
   test('CodeEditor JSON模式 - 验证头部显示JSON标签和按钮', async ({ page }) => {
   test('CodeEditor JSON模式 - 验证头部显示JSON标签和按钮', async ({ page }) => {
     await login(page)
     await login(page)
-    await page.goto('/lss')
+    await page.goto('/lss-manage/list')
 
 
     // 等待表格加载
     // 等待表格加载
     await page.waitForTimeout(1000)
     await page.waitForTimeout(1000)
@@ -244,7 +244,7 @@ test.describe('LSS管理 - CodeEditor 组件测试 (JSON模式)', () => {
    */
    */
   test('CodeEditor JSON模式 - 复制按钮功能', async ({ page }) => {
   test('CodeEditor JSON模式 - 复制按钮功能', async ({ page }) => {
     await login(page)
     await login(page)
-    await page.goto('/lss')
+    await page.goto('/lss-manage/list')
 
 
     // 等待表格加载
     // 等待表格加载
     await page.waitForTimeout(1000)
     await page.waitForTimeout(1000)
@@ -280,7 +280,7 @@ test.describe('LSS管理 - CodeEditor 组件测试 (JSON模式)', () => {
    */
    */
   test('CodeEditor JSON模式 - 格式化按钮功能', async ({ page }) => {
   test('CodeEditor JSON模式 - 格式化按钮功能', async ({ page }) => {
     await login(page)
     await login(page)
-    await page.goto('/lss')
+    await page.goto('/lss-manage/list')
 
 
     // 等待表格加载
     // 等待表格加载
     await page.waitForTimeout(1000)
     await page.waitForTimeout(1000)
@@ -319,7 +319,7 @@ test.describe('LSS管理 - CodeEditor 组件测试 (JSON模式)', () => {
    */
    */
   test('CodeEditor JSON模式 - 无效JSON显示错误提示', async ({ page }) => {
   test('CodeEditor JSON模式 - 无效JSON显示错误提示', async ({ page }) => {
     await login(page)
     await login(page)
-    await page.goto('/lss')
+    await page.goto('/lss-manage/list')
 
 
     // 等待表格加载
     // 等待表格加载
     await page.waitForTimeout(1000)
     await page.waitForTimeout(1000)
@@ -363,7 +363,7 @@ test.describe('LSS管理 - CodeEditor 组件测试 (JSON模式)', () => {
    */
    */
   test('CodeEditor JSON模式 - 更新参数配置并验证保存成功', async ({ page }) => {
   test('CodeEditor JSON模式 - 更新参数配置并验证保存成功', async ({ page }) => {
     await login(page)
     await login(page)
-    await page.goto('/lss')
+    await page.goto('/lss-manage/list')
 
 
     // 等待表格加载
     // 等待表格加载
     await page.waitForTimeout(1000)
     await page.waitForTimeout(1000)
@@ -426,7 +426,7 @@ test.describe('LSS管理 - CodeEditor 组件测试 (JSON模式)', () => {
    */
    */
   test('CodeEditor JSON模式 - 更新运行参数并验证保存成功', async ({ page }) => {
   test('CodeEditor JSON模式 - 更新运行参数并验证保存成功', async ({ page }) => {
     await login(page)
     await login(page)
-    await page.goto('/lss')
+    await page.goto('/lss-manage/list')
 
 
     // 等待表格加载
     // 等待表格加载
     await page.waitForTimeout(1000)
     await page.waitForTimeout(1000)
@@ -506,7 +506,7 @@ test.describe('LSS管理 - 摄像头列表搜索测试', () => {
 
 
   // 打开摄像头列表辅助函数
   // 打开摄像头列表辅助函数
   async function openCameraList(page: Page) {
   async function openCameraList(page: Page) {
-    await page.goto('/lss')
+    await page.goto('/lss-manage/list')
     await page.waitForTimeout(1000)
     await page.waitForTimeout(1000)
 
 
     // 点击 Device List 按钮打开设备列表 - 点击编辑按钮进入 LSS 编辑抽屉
     // 点击 Device List 按钮打开设备列表 - 点击编辑按钮进入 LSS 编辑抽屉
@@ -832,7 +832,7 @@ test.describe('LSS管理 - 摄像头未创建 Live Stream 对话框测试', () =
 
 
   // 打开摄像头列表辅助函数
   // 打开摄像头列表辅助函数
   async function openCameraList(page: Page) {
   async function openCameraList(page: Page) {
-    await page.goto('/lss')
+    await page.goto('/lss-manage/list')
     await page.waitForTimeout(1000)
     await page.waitForTimeout(1000)
 
 
     // 点击编辑按钮进入 LSS 编辑抽屉
     // 点击编辑按钮进入 LSS 编辑抽屉
@@ -956,7 +956,7 @@ test.describe('LSS管理 - 摄像头未创建 Live Stream 对话框测试', () =
     await expect(messageBox).not.toBeVisible({ timeout: 3000 })
     await expect(messageBox).not.toBeVisible({ timeout: 3000 })
 
 
     // 验证仍在 LSS 页面
     // 验证仍在 LSS 页面
-    await expect(page).toHaveURL(/\/lss/)
+    await expect(page).toHaveURL(/\/lss-manage\/list/)
   })
   })
 
 
   /**
   /**
@@ -1023,3 +1023,746 @@ 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-manage/list')
+
+    // 等待表格加载
+    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-manage/list')
+
+    // 等待表格加载
+    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-manage/list')
+
+    // 等待页面加载
+    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-manage/list')
+
+    // 等待表格加载
+    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-manage/list')
+
+    // 等待表格加载
+    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-manage/list')
+
+    // 等待页面加载
+    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-manage/list')
+
+    // 等待页面加载
+    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-manage/list')
+
+    // 等待表格加载
+    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-manage/list')
+
+    // 等待表格加载
+    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-manage/list')
+    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-manage/list')
+    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-manage/list')
+    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-manage/list')
+    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}|\-/)
+    }
+  })
+
+  /**
+   * Bug #4545: 按钮大小统一,圆角框
+   */
+  test('Bug #4545 - 按钮有统一的圆角', async ({ page }) => {
+    await login(page)
+    await page.goto('/lss-manage/list')
+
+    // 等待页面加载
+    await page.waitForTimeout(500)
+
+    // 获取查询按钮
+    const searchButton = page.locator('[data-id="btn-search"]')
+    await expect(searchButton).toBeVisible()
+
+    // 验证按钮有圆角
+    const borderRadius = await searchButton.evaluate((el) => {
+      return window.getComputedStyle(el).borderRadius
+    })
+    // 验证圆角值不为0
+    expect(borderRadius).not.toBe('0px')
+    expect(parseInt(borderRadius)).toBeGreaterThanOrEqual(4)
+  })
+
+  /**
+   * Bug #4549: LSS列表作为「LSS 管理」的子菜单
+   */
+  test('Bug #4549 - LSS列表作为LSS管理的子菜单', async ({ page }) => {
+    await login(page)
+
+    // 等待侧边栏加载
+    await page.waitForTimeout(500)
+
+    // 验证"LSS 管理"父菜单存在
+    const lssManageMenu = page.locator('.layout__nav-item--parent').filter({ hasText: 'LSS 管理' })
+    await expect(lssManageMenu).toBeVisible()
+
+    // 点击展开子菜单
+    await lssManageMenu.click()
+    await page.waitForTimeout(300)
+
+    // 验证"LSS 列表"子菜单存在
+    const lssListMenu = page.locator('.layout__nav-item--child').filter({ hasText: 'LSS 列表' })
+    await expect(lssListMenu).toBeVisible()
+
+    // 点击子菜单导航到页面
+    await lssListMenu.click()
+    await expect(page).toHaveURL(/\/lss-manage\/list/)
+  })
+
+  /**
+   * Bug #4554 & #4555: 摄像头列表搜索字段
+   */
+  test('Bug #4554/#4555 - 摄像头列表有设备ID和设备名称搜索字段', async ({ page }) => {
+    await login(page)
+    await page.goto('/lss-manage/list')
+    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)
+
+    // 验证有"设备ID"搜索框
+    await expect(drawer.getByPlaceholder('设备ID')).toBeVisible()
+
+    // 验证有"名称"搜索框
+    await expect(drawer.getByPlaceholder('名称')).toBeVisible()
+
+    // 验证没有"IP / 设备ID / 名称"混合搜索框
+    const mixedSearch = drawer.getByPlaceholder('IP / 设备ID / 名称')
+    await expect(mixedSearch).not.toBeVisible()
+  })
+
+  /**
+   * Bug #4562: 摄像头列表有分页功能
+   */
+  test('Bug #4562 - 摄像头列表有分页功能', async ({ page }) => {
+    await login(page)
+    await page.goto('/lss-manage/list')
+    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('.el-pagination')).toBeVisible()
+  })
+
+  /**
+   * Bug #4567: List厂商显示和详情页一样
+   */
+  test('Bug #4567 - 厂商列使用vendorName字段', async ({ page }) => {
+    await login(page)
+
+    // Mock 摄像头列表 API 返回带有 vendorName 的数据
+    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_001',
+                cameraName: '测试摄像头',
+                lssId: 'LSS_001',
+                status: 'active',
+                vendorName: 'hikvision',
+                model: 'DS-2CD2T45'
+              }
+            ],
+            total: 1
+          }
+        })
+      })
+    })
+
+    await page.goto('/lss-manage/list')
+    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 vendorCell = drawer.locator('tbody tr').first().locator('td').nth(5)
+    const vendorText = await vendorCell.textContent()
+    expect(vendorText).toContain('HIKVISION')
+  })
+
+  /**
+   * Bug #4588: 参数配置更新按钮蓝底白字
+   */
+  test('Bug #4588 - 参数配置更新按钮使用蓝底白字', async ({ page }) => {
+    await login(page)
+    await page.goto('/lss-manage/list')
+    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 viewButton = drawer.locator('tbody tr').first().locator('button:has-text("查看")').first()
+    if ((await viewButton.count()) > 0) {
+      await viewButton.click()
+
+      // 等待参数配置抽屉打开
+      const paramsDrawer = page.locator('.el-drawer').filter({ hasText: '参数配置' })
+      await expect(paramsDrawer).toBeVisible({ timeout: 5000 })
+
+      // 验证更新按钮是蓝色
+      const updateButton = paramsDrawer.locator('button:has-text("更新")')
+      await expect(updateButton).toHaveClass(/el-button--primary/)
+    }
+  })
+
+  /**
+   * Bug #4594: 删除确认对话框确定按钮蓝底白字
+   */
+  test('Bug #4594 - 删除确认对话框确定按钮蓝底白字', 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: [{ id: 1, cameraId: 'CAM_001', cameraName: '测试', status: 'active' }],
+            total: 1
+          }
+        })
+      })
+    })
+
+    await page.goto('/lss-manage/list')
+    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 drawer.locator('tbody tr').first().locator('button').last().click()
+
+    // 等待确认对话框
+    const messageBox = page.locator('.el-message-box')
+    await expect(messageBox).toBeVisible({ timeout: 5000 })
+
+    // 验证确定按钮是蓝色
+    const confirmButton = messageBox.locator('button.el-button--primary')
+    await expect(confirmButton).toBeVisible()
+
+    const bgColor = await confirmButton.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/)
+
+    // 关闭对话框
+    await messageBox.locator('button:has-text("取消")').click()
+  })
+})

Some files were not shown because too many files changed in this diff