Quellcode durchsuchen

feat(table): add draggable table component and multi-select functionality

- Introduced a new draggable table component using element-plus-table-dragable for enhanced user interaction.
- Implemented multi-select logic to allow users to select multiple rows with checkboxes.
- Added necessary utility functions for data formatting and error handling.
- Updated package.json and pnpm-lock.yaml to include new dependencies for the draggable table feature.
- Created supporting types and utility functions for better type safety and code organization.

This commit enhances the table component's usability and flexibility, improving the overall user experience.
yb vor 2 Wochen
Ursprung
Commit
1a952d78c7
44 geänderte Dateien mit 3032 neuen und 198 gelöschten Zeilen
  1. 3 0
      package.json
  2. 35 0
      pnpm-lock.yaml
  3. 6 6
      src/api/audit.ts
  4. 16 14
      src/api/camera.ts
  5. 5 5
      src/api/login.ts
  6. 13 6
      src/api/machine.ts
  7. 2 2
      src/api/stats.ts
  8. 7 7
      src/api/user.ts
  9. 2 0
      src/components.d.ts
  10. 315 0
      src/components/mTable/index.vue
  11. 53 0
      src/components/mTable/types.ts
  12. 2 2
      src/components/monitor/CameraSelector.vue
  13. 228 0
      src/composables/useMultiSelectLogic.ts
  14. 192 0
      src/const/options.json
  15. 2 2
      src/layout/index.vue
  16. 6 0
      src/router/index.ts
  17. 2 2
      src/store/user.ts
  18. 28 8
      src/types/index.ts
  19. 421 0
      src/utils/dataFormat.ts
  20. 211 0
      src/utils/dateFormat/index.ts
  21. 18 23
      src/utils/request.ts
  22. 18 0
      src/utils/showErrorMessage.ts
  23. 568 0
      src/views/account/addUserDialog.vue
  24. 46 0
      src/views/account/data.ts
  25. 276 0
      src/views/account/index.vue
  26. 4 4
      src/views/audit/index.vue
  27. 1 1
      src/views/camera/channel.vue
  28. 8 8
      src/views/camera/index.vue
  29. 2 2
      src/views/dashboard/index.vue
  30. 2 2
      src/views/login/index.vue
  31. 5 5
      src/views/machine/index.vue
  32. 2 2
      src/views/stats/index.vue
  33. 425 0
      src/views/test/m-table-demo.vue
  34. 6 6
      src/views/user/index.vue
  35. 34 23
      tests/fixtures/index.ts
  36. 25 25
      tests/mocks/api.ts
  37. 9 9
      tests/unit/api/login.spec.ts
  38. 15 15
      tests/unit/api/machine.spec.ts
  39. 4 4
      tests/unit/store/user.spec.ts
  40. 4 4
      tests/unit/views/audit/index.spec.ts
  41. 4 4
      tests/unit/views/camera/index.spec.ts
  42. 2 2
      tests/unit/views/dashboard/index.spec.ts
  43. 4 4
      tests/unit/views/machine/index.spec.ts
  44. 1 1
      tests/unit/views/stats/index.spec.ts

+ 3 - 0
package.json

@@ -36,8 +36,11 @@
     "@element-plus/icons-vue": "^2.1.0",
     "@element-plus/icons-vue": "^2.1.0",
     "@vueuse/core": "^14.1.0",
     "@vueuse/core": "^14.1.0",
     "axios": "^1.4.0",
     "axios": "^1.4.0",
+    "date-fns": "^4.1.0",
+    "date-fns-tz": "^3.2.0",
     "dayjs": "^1.11.19",
     "dayjs": "^1.11.19",
     "element-plus": "2.7.5",
     "element-plus": "2.7.5",
+    "element-plus-table-dragable": "^1.0.0",
     "hls.js": "^1.4.10",
     "hls.js": "^1.4.10",
     "lodash-es": "^4.17.22",
     "lodash-es": "^4.17.22",
     "nprogress": "^0.2.0",
     "nprogress": "^0.2.0",

+ 35 - 0
pnpm-lock.yaml

@@ -17,12 +17,21 @@ importers:
       axios:
       axios:
         specifier: ^1.4.0
         specifier: ^1.4.0
         version: 1.13.2
         version: 1.13.2
+      date-fns:
+        specifier: ^4.1.0
+        version: 4.1.0
+      date-fns-tz:
+        specifier: ^3.2.0
+        version: 3.2.0(date-fns@4.1.0)
       dayjs:
       dayjs:
         specifier: ^1.11.19
         specifier: ^1.11.19
         version: 1.11.19
         version: 1.11.19
       element-plus:
       element-plus:
         specifier: 2.7.5
         specifier: 2.7.5
         version: 2.7.5(vue@3.5.26(typescript@5.6.3))
         version: 2.7.5(vue@3.5.26(typescript@5.6.3))
+      element-plus-table-dragable:
+        specifier: ^1.0.0
+        version: 1.0.0
       hls.js:
       hls.js:
         specifier: ^1.4.10
         specifier: ^1.4.10
         version: 1.6.15
         version: 1.6.15
@@ -1688,6 +1697,14 @@ packages:
     resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
     resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
     engines: {node: '>= 0.4'}
     engines: {node: '>= 0.4'}
 
 
+  date-fns-tz@3.2.0:
+    resolution: {integrity: sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==}
+    peerDependencies:
+      date-fns: ^3.0.0 || ^4.0.0
+
+  date-fns@4.1.0:
+    resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
+
   dayjs@1.11.19:
   dayjs@1.11.19:
     resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==}
     resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==}
 
 
@@ -1785,6 +1802,9 @@ packages:
   electron-to-chromium@1.5.267:
   electron-to-chromium@1.5.267:
     resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==}
     resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==}
 
 
+  element-plus-table-dragable@1.0.0:
+    resolution: {integrity: sha512-NKa95z3wkKv8l4iydQksJh+uGYtbI3ixiUio6tZzmY9XyJZDtgzWHx6kpqRzCWoba7EUoaIhA+Se/QKWKeOhTA==}
+
   element-plus@2.7.5:
   element-plus@2.7.5:
     resolution: {integrity: sha512-e4oqhfRGBpdblgsjEBK+tA2+fg1H1KZ2Qinty1SaJl0X49FrMLK0lpXQNheWyBqI4V/pyjVOF9sRjz2hfyoctw==}
     resolution: {integrity: sha512-e4oqhfRGBpdblgsjEBK+tA2+fg1H1KZ2Qinty1SaJl0X49FrMLK0lpXQNheWyBqI4V/pyjVOF9sRjz2hfyoctw==}
     peerDependencies:
     peerDependencies:
@@ -3283,6 +3303,9 @@ packages:
     resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==}
     resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==}
     engines: {node: '>=10'}
     engines: {node: '>=10'}
 
 
+  sortablejs@1.15.6:
+    resolution: {integrity: sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A==}
+
   sortobject@4.17.0:
   sortobject@4.17.0:
     resolution: {integrity: sha512-gzx7USv55AFRQ7UCWJHHauwD/ptUHF9MLXCGO3f5M9zauDPZ/4a9H6/VVbOXefdpEoI1unwB/bArHIVMbWBHmA==}
     resolution: {integrity: sha512-gzx7USv55AFRQ7UCWJHHauwD/ptUHF9MLXCGO3f5M9zauDPZ/4a9H6/VVbOXefdpEoI1unwB/bArHIVMbWBHmA==}
     engines: {node: '>=10'}
     engines: {node: '>=10'}
@@ -5431,6 +5454,12 @@ snapshots:
       es-errors: 1.3.0
       es-errors: 1.3.0
       is-data-view: 1.0.2
       is-data-view: 1.0.2
 
 
+  date-fns-tz@3.2.0(date-fns@4.1.0):
+    dependencies:
+      date-fns: 4.1.0
+
+  date-fns@4.1.0: {}
+
   dayjs@1.11.19: {}
   dayjs@1.11.19: {}
 
 
   de-indent@1.0.2: {}
   de-indent@1.0.2: {}
@@ -5511,6 +5540,10 @@ snapshots:
 
 
   electron-to-chromium@1.5.267: {}
   electron-to-chromium@1.5.267: {}
 
 
+  element-plus-table-dragable@1.0.0:
+    dependencies:
+      sortablejs: 1.15.6
+
   element-plus@2.7.5(vue@3.5.26(typescript@5.6.3)):
   element-plus@2.7.5(vue@3.5.26(typescript@5.6.3)):
     dependencies:
     dependencies:
       '@ctrl/tinycolor': 3.6.1
       '@ctrl/tinycolor': 3.6.1
@@ -7233,6 +7266,8 @@ snapshots:
       astral-regex: 2.0.0
       astral-regex: 2.0.0
       is-fullwidth-code-point: 3.0.0
       is-fullwidth-code-point: 3.0.0
 
 
+  sortablejs@1.15.6: {}
+
   sortobject@4.17.0: {}
   sortobject@4.17.0: {}
 
 
   source-map-js@1.2.1: {}
   source-map-js@1.2.1: {}

+ 6 - 6
src/api/audit.ts

@@ -1,5 +1,5 @@
 import { get } from '@/utils/request'
 import { get } from '@/utils/request'
-import type { ApiResponse, PageResult } from '@/types'
+import type { IResponse, IBaseResponse } from '@/types'
 
 
 // 审计日志类型
 // 审计日志类型
 export interface AuditLog {
 export interface AuditLog {
@@ -38,17 +38,17 @@ export interface AuditLogStats {
 }
 }
 
 
 // 获取审计日志列表
 // 获取审计日志列表
-export function getAuditLogs(params?: AuditLogQueryParams): Promise<ApiResponse<PageResult<AuditLog>>> {
+export function getAuditLogs(params?: AuditLogQueryParams): Promise<IResponse<AuditLog>> {
   return get('/audit-logs', params)
   return get('/audit-logs', params)
 }
 }
 
 
 // 获取审计日志详情
 // 获取审计日志详情
-export function getAuditLogDetail(id: string): Promise<ApiResponse<AuditLog>> {
+export function getAuditLogDetail(id: string): Promise<IBaseResponse<AuditLog>> {
   return get(`/audit-logs/${id}`)
   return get(`/audit-logs/${id}`)
 }
 }
 
 
 // 获取审计日志统计
 // 获取审计日志统计
-export function getAuditLogStats(days = 7): Promise<ApiResponse<AuditLogStats>> {
+export function getAuditLogStats(days = 7): Promise<IBaseResponse<AuditLogStats>> {
   return get('/audit-logs/stats/summary', { days })
   return get('/audit-logs/stats/summary', { days })
 }
 }
 
 
@@ -56,7 +56,7 @@ export function getAuditLogStats(days = 7): Promise<ApiResponse<AuditLogStats>>
 export function getUserAuditLogs(
 export function getUserAuditLogs(
   userId: string,
   userId: string,
   params?: { page?: number; pageSize?: number }
   params?: { page?: number; pageSize?: number }
-): Promise<ApiResponse<PageResult<AuditLog>>> {
+): Promise<IResponse<AuditLog>> {
   return get(`/audit-logs/user/${userId}`, params)
   return get(`/audit-logs/user/${userId}`, params)
 }
 }
 
 
@@ -65,6 +65,6 @@ export function getResourceAuditLogs(
   resource: string,
   resource: string,
   resourceId: string,
   resourceId: string,
   params?: { page?: number; pageSize?: number }
   params?: { page?: number; pageSize?: number }
-): Promise<ApiResponse<PageResult<AuditLog>>> {
+): Promise<IResponse<AuditLog>> {
   return get(`/audit-logs/resource/${resource}/${resourceId}`, params)
   return get(`/audit-logs/resource/${resource}/${resourceId}`, params)
 }
 }

+ 16 - 14
src/api/camera.ts

@@ -1,6 +1,8 @@
 import { get, post } from '@/utils/request'
 import { get, post } from '@/utils/request'
 import type {
 import type {
-  ApiResponse,
+  IResponse,
+  IBaseResponse,
+  BaseResponse,
   CameraDTO,
   CameraDTO,
   ChannelDTO,
   ChannelDTO,
   CameraInfoDTO,
   CameraInfoDTO,
@@ -13,68 +15,68 @@ import type {
 // ==================== Controller APIs (MVP) ====================
 // ==================== Controller APIs (MVP) ====================
 
 
 // 获取摄像头列表
 // 获取摄像头列表
-export function listCameras(machineId?: string): Promise<ApiResponse<CameraDTO[]>> {
+export function listCameras(machineId?: string): Promise<IResponse<CameraDTO>> {
   return get('/camera/list', machineId ? { machineId } : undefined)
   return get('/camera/list', machineId ? { machineId } : undefined)
 }
 }
 
 
 // 获取摄像头信息
 // 获取摄像头信息
-export function getCamera(cameraId: string): Promise<ApiResponse<CameraDTO>> {
+export function getCamera(cameraId: string): Promise<IBaseResponse<CameraDTO>> {
   return get(`/camera/${cameraId}`)
   return get(`/camera/${cameraId}`)
 }
 }
 
 
 // 切换摄像头通道 (MVP核心)
 // 切换摄像头通道 (MVP核心)
-export function switchChannel(data: SwitchChannelRequest): Promise<ApiResponse<ChannelDTO>> {
+export function switchChannel(data: SwitchChannelRequest): Promise<IBaseResponse<ChannelDTO>> {
   return post('/camera/switch', data)
   return post('/camera/switch', data)
 }
 }
 
 
 // 获取当前活动通道
 // 获取当前活动通道
-export function getCurrentChannel(machineId: string): Promise<ApiResponse<ChannelDTO>> {
+export function getCurrentChannel(machineId: string): Promise<IBaseResponse<ChannelDTO>> {
   return get('/camera/current', { machineId })
   return get('/camera/current', { machineId })
 }
 }
 
 
 // 开始PTZ控制 (后台专用)
 // 开始PTZ控制 (后台专用)
-export function ptzStart(cameraId: string, action: PTZAction, speed: number = 50): Promise<ApiResponse<null>> {
+export function ptzStart(cameraId: string, action: PTZAction, speed: number = 50): Promise<BaseResponse> {
   return post(`/camera/${cameraId}/ptz/start`, undefined, {
   return post(`/camera/${cameraId}/ptz/start`, undefined, {
     params: { action, speed }
     params: { action, speed }
   })
   })
 }
 }
 
 
 // 停止PTZ控制 (后台专用)
 // 停止PTZ控制 (后台专用)
-export function ptzStop(cameraId: string): Promise<ApiResponse<null>> {
+export function ptzStop(cameraId: string): Promise<BaseResponse> {
   return post(`/camera/${cameraId}/ptz/stop`)
   return post(`/camera/${cameraId}/ptz/stop`)
 }
 }
 
 
 // ==================== Admin APIs ====================
 // ==================== Admin APIs ====================
 
 
 // 获取摄像头列表 (管理后台)
 // 获取摄像头列表 (管理后台)
-export function adminListCameras(machineId?: string): Promise<ApiResponse<CameraInfoDTO[]>> {
+export function adminListCameras(machineId?: string): Promise<IResponse<CameraInfoDTO>> {
   return get('/admin/cameras/list', machineId ? { machineId } : undefined)
   return get('/admin/cameras/list', machineId ? { machineId } : undefined)
 }
 }
 
 
 // 获取摄像头详情
 // 获取摄像头详情
-export function adminGetCamera(id: number): Promise<ApiResponse<CameraInfoDTO>> {
+export function adminGetCamera(id: number): Promise<IBaseResponse<CameraInfoDTO>> {
   return get('/admin/cameras/detail', { id })
   return get('/admin/cameras/detail', { id })
 }
 }
 
 
 // 添加摄像头
 // 添加摄像头
-export function adminAddCamera(data: CameraAddRequest): Promise<ApiResponse<CameraInfoDTO>> {
+export function adminAddCamera(data: CameraAddRequest): Promise<IBaseResponse<CameraInfoDTO>> {
   return post('/admin/cameras/add', data)
   return post('/admin/cameras/add', data)
 }
 }
 
 
 // 更新摄像头
 // 更新摄像头
-export function adminUpdateCamera(data: CameraUpdateRequest): Promise<ApiResponse<CameraInfoDTO>> {
+export function adminUpdateCamera(data: CameraUpdateRequest): Promise<IBaseResponse<CameraInfoDTO>> {
   return post('/admin/cameras/update', data)
   return post('/admin/cameras/update', data)
 }
 }
 
 
 // 删除摄像头
 // 删除摄像头
-export function adminDeleteCamera(id: number): Promise<ApiResponse<null>> {
+export function adminDeleteCamera(id: number): Promise<BaseResponse> {
   return post('/admin/cameras/delete', undefined, {
   return post('/admin/cameras/delete', undefined, {
     params: { id }
     params: { id }
   })
   })
 }
 }
 
 
 // 检测摄像头连通性
 // 检测摄像头连通性
-export function adminCheckCamera(id: number): Promise<ApiResponse<boolean>> {
+export function adminCheckCamera(id: number): Promise<IBaseResponse<boolean>> {
   return post('/admin/cameras/check', undefined, {
   return post('/admin/cameras/check', undefined, {
     params: { id }
     params: { id }
   })
   })
@@ -105,7 +107,7 @@ export function ptzControl(
   horizonSpeed?: number,
   horizonSpeed?: number,
   _verticalSpeed?: number,
   _verticalSpeed?: number,
   _zoomSpeed?: number
   _zoomSpeed?: number
-): Promise<ApiResponse<null>> {
+): Promise<BaseResponse> {
   // 映射旧的命令到新的 action
   // 映射旧的命令到新的 action
   const actionMap: Record<string, PTZAction> = {
   const actionMap: Record<string, PTZAction> = {
     up: 'up',
     up: 'up',

+ 5 - 5
src/api/login.ts

@@ -1,22 +1,22 @@
 import { get, post } from '@/utils/request'
 import { get, post } from '@/utils/request'
-import type { ApiResponse, LoginParams, LoginResponse, AdminInfo, ChangePasswordRequest } from '@/types'
+import type { IBaseResponse, BaseResponse, LoginParams, LoginResponse, AdminInfo, ChangePasswordRequest } from '@/types'
 
 
 // 登录
 // 登录
-export function login(data: LoginParams): Promise<ApiResponse<LoginResponse>> {
+export function login(data: LoginParams): Promise<IBaseResponse<LoginResponse>> {
   return post('/admin/auth/login', data)
   return post('/admin/auth/login', data)
 }
 }
 
 
 // 获取当前用户信息
 // 获取当前用户信息
-export function getInfo(): Promise<ApiResponse<AdminInfo>> {
+export function getInfo(): Promise<IBaseResponse<AdminInfo>> {
   return get('/admin/auth/info')
   return get('/admin/auth/info')
 }
 }
 
 
 // 退出登录
 // 退出登录
-export function logout(): Promise<ApiResponse<null>> {
+export function logout(): Promise<BaseResponse> {
   return post('/admin/auth/logout')
   return post('/admin/auth/logout')
 }
 }
 
 
 // 修改密码
 // 修改密码
-export function changePassword(data: ChangePasswordRequest): Promise<ApiResponse<null>> {
+export function changePassword(data: ChangePasswordRequest): Promise<BaseResponse> {
   return post('/admin/auth/password', data)
   return post('/admin/auth/password', data)
 }
 }

+ 13 - 6
src/api/machine.ts

@@ -1,28 +1,35 @@
 import { get, post } from '@/utils/request'
 import { get, post } from '@/utils/request'
-import type { ApiResponse, MachineDTO, MachineAddRequest, MachineUpdateRequest } from '@/types'
+import type {
+  IResponse,
+  IBaseResponse,
+  BaseResponse,
+  MachineDTO,
+  MachineAddRequest,
+  MachineUpdateRequest
+} from '@/types'
 
 
 // 获取机器列表
 // 获取机器列表
-export function listMachines(): Promise<ApiResponse<MachineDTO[]>> {
+export function listMachines(): Promise<IResponse<MachineDTO>> {
   return get('/admin/machines/list')
   return get('/admin/machines/list')
 }
 }
 
 
 // 获取机器详情
 // 获取机器详情
-export function getMachine(id: number): Promise<ApiResponse<MachineDTO>> {
+export function getMachine(id: number): Promise<IBaseResponse<MachineDTO>> {
   return get('/admin/machines/detail', { id })
   return get('/admin/machines/detail', { id })
 }
 }
 
 
 // 添加机器
 // 添加机器
-export function addMachine(data: MachineAddRequest): Promise<ApiResponse<MachineDTO>> {
+export function addMachine(data: MachineAddRequest): Promise<IBaseResponse<MachineDTO>> {
   return post('/admin/machines/add', data)
   return post('/admin/machines/add', data)
 }
 }
 
 
 // 更新机器
 // 更新机器
-export function updateMachine(data: MachineUpdateRequest): Promise<ApiResponse<MachineDTO>> {
+export function updateMachine(data: MachineUpdateRequest): Promise<IBaseResponse<MachineDTO>> {
   return post('/admin/machines/update', data)
   return post('/admin/machines/update', data)
 }
 }
 
 
 // 删除机器
 // 删除机器
-export function deleteMachine(id: number): Promise<ApiResponse<null>> {
+export function deleteMachine(id: number): Promise<BaseResponse> {
   return post('/admin/machines/delete', undefined, {
   return post('/admin/machines/delete', undefined, {
     params: { id }
     params: { id }
   })
   })

+ 2 - 2
src/api/stats.ts

@@ -1,8 +1,8 @@
 import { get } from '@/utils/request'
 import { get } from '@/utils/request'
-import type { ApiResponse, DashboardStatsDTO } from '@/types'
+import type { IBaseResponse, DashboardStatsDTO } from '@/types'
 
 
 // 获取仪表盘统计数据
 // 获取仪表盘统计数据
-export function getDashboardStats(): Promise<ApiResponse<DashboardStatsDTO>> {
+export function getDashboardStats(): Promise<IBaseResponse<DashboardStatsDTO>> {
   return get('/admin/stats/dashboard')
   return get('/admin/stats/dashboard')
 }
 }
 
 

+ 7 - 7
src/api/user.ts

@@ -1,5 +1,5 @@
 import { get, post, put, del } from '@/utils/request'
 import { get, post, put, del } from '@/utils/request'
-import type { ApiResponse, PageResult } from '@/types'
+import type { IResponse, IBaseResponse, BaseResponse } from '@/types'
 
 
 // 用户类型
 // 用户类型
 export interface User {
 export interface User {
@@ -27,31 +27,31 @@ export interface UserForm {
 }
 }
 
 
 // 获取用户列表
 // 获取用户列表
-export function listUsers(params?: UserQueryParams): Promise<ApiResponse<PageResult<User>>> {
+export function listUsers(params?: UserQueryParams): Promise<IResponse<User>> {
   return get('/users', params)
   return get('/users', params)
 }
 }
 
 
 // 获取用户详情
 // 获取用户详情
-export function getUser(id: string): Promise<ApiResponse<User>> {
+export function getUser(id: string): Promise<IBaseResponse<User>> {
   return get(`/users/${id}`)
   return get(`/users/${id}`)
 }
 }
 
 
 // 创建用户
 // 创建用户
-export function createUser(data: UserForm): Promise<ApiResponse<User>> {
+export function createUser(data: UserForm): Promise<IBaseResponse<User>> {
   return post('/users', data)
   return post('/users', data)
 }
 }
 
 
 // 更新用户
 // 更新用户
-export function updateUser(id: string, data: Partial<UserForm>): Promise<ApiResponse<User>> {
+export function updateUser(id: string, data: Partial<UserForm>): Promise<IBaseResponse<User>> {
   return put(`/users/${id}`, data)
   return put(`/users/${id}`, data)
 }
 }
 
 
 // 删除用户
 // 删除用户
-export function deleteUser(id: string): Promise<ApiResponse<null>> {
+export function deleteUser(id: string): Promise<BaseResponse> {
   return del(`/users/${id}`)
   return del(`/users/${id}`)
 }
 }
 
 
 // 重置用户密码
 // 重置用户密码
-export function resetPassword(id: string, password: string): Promise<ApiResponse<null>> {
+export function resetPassword(id: string, password: string): Promise<BaseResponse> {
   return post(`/users/${id}/reset-password`, { password })
   return post(`/users/${id}/reset-password`, { password })
 }
 }

+ 2 - 0
src/components.d.ts

@@ -11,6 +11,7 @@ declare module 'vue' {
     ElAlert: typeof import('element-plus/es')['ElAlert']
     ElAlert: typeof import('element-plus/es')['ElAlert']
     ElButton: typeof import('element-plus/es')['ElButton']
     ElButton: typeof import('element-plus/es')['ElButton']
     ElCard: typeof import('element-plus/es')['ElCard']
     ElCard: typeof import('element-plus/es')['ElCard']
+    ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
     ElCol: typeof import('element-plus/es')['ElCol']
     ElCol: typeof import('element-plus/es')['ElCol']
     ElCollapse: typeof import('element-plus/es')['ElCollapse']
     ElCollapse: typeof import('element-plus/es')['ElCollapse']
     ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
     ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
@@ -62,6 +63,7 @@ declare module 'vue' {
     IEpTopLeft: typeof import('~icons/ep/top-left')['default']
     IEpTopLeft: typeof import('~icons/ep/top-left')['default']
     IEpTopRight: typeof import('~icons/ep/top-right')['default']
     IEpTopRight: typeof import('~icons/ep/top-right')['default']
     LangDropdown: typeof import('./components/LangDropdown.vue')['default']
     LangDropdown: typeof import('./components/LangDropdown.vue')['default']
+    MTable: typeof import('./components/mTable/index.vue')['default']
     PTZController: typeof import('./components/PTZController.vue')['default']
     PTZController: typeof import('./components/PTZController.vue')['default']
     PtzOverlay: typeof import('./components/monitor/PtzOverlay.vue')['default']
     PtzOverlay: typeof import('./components/monitor/PtzOverlay.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterLink: typeof import('vue-router')['RouterLink']

+ 315 - 0
src/components/mTable/index.vue

@@ -0,0 +1,315 @@
+<template>
+  <!-- 表格组件 -->
+  <el-table
+    :data="props.data"
+    v-loading="isLoading"
+    :element-loading-text="elementLoadingText"
+    :element-loading-spinner="elementLoadingSpinner"
+    :element-loading-background="elementLoadingBackground"
+    :element-loading-svg="elementLoadingSvg"
+    :element-loading-svg-view-box="elementLoadingSvgViewBox"
+    :height="tableHeight"
+    @row-click="handleRowClick"
+    v-dragable="tableDragableOptions"
+    :style="{ width: '100%', height: tableHeight ? 'initial' : '100%' }"
+    v-bind="$attrs"
+  >
+    <!-- Multi-select checkbox column -->
+    <el-table-column v-if="multiSelect" width="40" fixed="left">
+      <template #header>
+        <el-checkbox
+          :model-value="multiSelectLogic?.isAllSelected.value"
+          :indeterminate="multiSelectLogic?.isIndeterminate.value"
+          @click="multiSelectLogic?.handleSelectAll(props.data)(!multiSelectLogic?.isAllSelected.value)"
+        />
+      </template>
+      <template #default="{ row }">
+        <el-checkbox
+          v-model="row.checked"
+          :disabled="multiSelectLogic?.checkableStrategy?.(row) || false"
+          @change="(val) => multiSelectLogic?.handleCheckboxChange(props.data)(row, !!val)"
+        />
+      </template>
+    </el-table-column>
+
+    <!-- 渲染一般列和自定义插槽列 -->
+    <template v-for="(item, index) in tableOption" :key="index">
+      <!-- 表格列 -->
+      <el-table-column
+        v-if="item.prop && !item.action"
+        :label="item.label"
+        :prop="item.prop"
+        :width="item.width"
+        :min-width="item.minWidth"
+        :max-width="item.maxWidth"
+        :align="item.align"
+        :fixed="item.fixed"
+        :type="item.type"
+        v-bind="{
+          ...(item.showOmission ? { 'show-overflow-tooltip': { disabled: true } } : {}),
+          ...item.columAttr
+        }"
+      >
+        <template #default="scope">
+          <!-- 自定义插槽 -->
+          <slot v-if="item.slot" :name="item.slot" :scope="scope"></slot>
+
+          <!-- 日期格式化 -->
+          <template v-else-if="item.dateName">
+            {{
+              item.dateName
+                ? unitFormatDate(
+                    typeof scope.row[item.dateName] === 'string'
+                      ? Number(scope.row[item.dateName])
+                      : scope.row[item.dateName],
+                    item.formatStr || 'yyyy-MM-dd'
+                  )
+                : null
+            }}
+          </template>
+
+          <!-- 图片数据  -->
+          <template v-else-if="item.cellType === 'image'">
+            <el-image v-if="item.image" class="m_image" :src="item.image.src" />
+          </template>
+
+          <!-- JSON数据显示  只要在JSON中配置了选项就可以在这里遍历显示-->
+          <template v-else-if="item.json">
+            {{ jsonOptions[item.json.sourceName][scope.row[item.json.keyName]] }}
+          </template>
+
+          <!-- 可复制文本 -->
+          <template v-else-if="item.isCopyText">
+            <span>{{ scope.row[item.prop!] }}</span>
+            <copy :content="scope.row[item.prop!]" />
+          </template>
+          <template v-else-if="item.truncateText">
+            <span>{{ truncateText(scope.row[item.prop!], item.truncateTextLength) }}</span>
+          </template>
+          <!-- 普通文本 -->
+          <span v-else>{{ scope.row[item.prop!] }}</span>
+        </template>
+      </el-table-column>
+    </template>
+
+    <!-- 渲染操作列 -->
+    <el-table-column
+      v-if="actionOption"
+      :label="actionOption.label"
+      :width="actionOption.width"
+      :align="actionOption.align"
+      fixed="right"
+    >
+      <template #default="scope">
+        <slot name="action" :scope="scope"></slot>
+      </template>
+    </el-table-column>
+  </el-table>
+
+  <!-- 分页组件 -->
+  <div v-if="pagination && !isLoading" class="pagination" :style="{ justifyContent }">
+    <el-pagination
+      :current-page="currentPage"
+      :page-sizes="pageSizes"
+      :page-size="pageSize"
+      layout="prev, pager, next, jumper, ->, sizes, total"
+      :total="total"
+      @size-change="handleSizeChange"
+      @current-change="handleCurrentChange"
+    ></el-pagination>
+  </div>
+</template>
+<script lang="ts" setup>
+import { type PropType, computed, ref, watch } from 'vue'
+import { vDragable } from 'element-plus-table-dragable'
+import { unitFormatDate } from '@/utils/dateFormat/index'
+import { truncateText } from '@/utils/dataFormat'
+import jsonOptions from '@/const/options.json'
+import type { TableOptions } from './types'
+import type { MultiSelectLogic } from '@/composables/useMultiSelectLogic'
+
+// 移动数组元素的函数
+function moveArrayItem(arr, fromIndex, toIndex) {
+  // 创建数组副本
+  const newData = [...arr]
+
+  // 确保索引有效
+  if (fromIndex < 0 || fromIndex >= newData.length || toIndex < 0 || toIndex >= newData.length) {
+    console.log('无效的索引')
+    return newData
+  }
+
+  // 移除并插入元素
+  const [movedItem] = newData.splice(fromIndex, 1)
+  newData.splice(toIndex, 0, movedItem)
+
+  return newData
+}
+
+// 定义props
+const props = defineProps({
+  // 表格配置选项
+  options: {
+    type: Array as PropType<TableOptions[]>,
+    required: true
+  },
+  // 表格数据
+  data: {
+    type: Array as () => any[],
+    required: true
+  },
+  // 加载文案
+  elementLoadingText: {
+    type: String
+  },
+  // 加载图标名
+  elementLoadingSpinner: {
+    type: String
+  },
+  // 加载背景颜色
+  elementLoadingBackground: {
+    type: String
+  },
+  // 加载图标是svg
+  elementLoadingSvg: {
+    type: String
+  },
+  // 加载图标是svg的配置
+  elementLoadingSvgViewBox: {
+    type: String
+  },
+  // 是否显示分页
+  pagination: {
+    type: Boolean,
+    default: false
+  },
+  // 显示分页的对齐方式
+  paginationAlign: {
+    type: String as PropType<'left' | 'center' | 'right'>,
+    default: 'left'
+  },
+  // 当前是第几页
+  currentPage: {
+    type: Number,
+    default: 1
+  },
+  // 当前一页多少条数据
+  pageSize: {
+    type: Number,
+    default: 15
+  },
+  // 显示分页数据多少条的选项
+  pageSizes: {
+    type: Array<number>,
+    default: () => [15, 30, 50, 100]
+  },
+  // 数据总条数
+  total: {
+    type: Number,
+    default: 0
+  },
+  //表格高度
+  tableHeight: {
+    type: String
+  },
+  isLoading: {
+    type: Boolean
+  },
+  defaultOptions: {
+    type: Array,
+    default: () => []
+  },
+  isDragable: {
+    type: Boolean,
+    default: false
+  },
+  dragableTableOptions: {
+    type: Array<any>,
+    default: () => []
+  },
+  // 是否启用多选功能
+  multiSelect: {
+    type: Boolean,
+    default: false
+  },
+  // Multi-select logic from external composable
+  multiSelectLogic: {
+    type: Object as PropType<MultiSelectLogic>,
+    default: null
+  }
+})
+
+// 定义emits
+const emits = defineEmits(['size-change', 'current-change', 'row-click', 'on-drag'])
+
+// 分页的每一页数据变化
+const handleSizeChange = (val: number) => {
+  emits('size-change', val)
+}
+
+// 分页页数改变
+const handleCurrentChange = (val: number) => {
+  emits('current-change', val)
+}
+
+// 行点击事件处理函数
+const handleRowClick = (row: any, _column: any, _event: Event) => {
+  emits('row-click', row)
+}
+
+// 过滤操作项之后的配置
+const tableOption = computed(() => props.options.filter((item) => !item.action))
+
+// 操作项
+const actionOption = computed(() => props.options.find((item) => item.action))
+
+// 表格分页的排列方式
+const justifyContent = computed(() => {
+  if (props.paginationAlign === 'left') return 'flex-start'
+  if (props.paginationAlign === 'right') return 'flex-end'
+  return 'center'
+})
+
+const defaultDragableOptions = [
+  {
+    selector: 'tbody',
+    option: {
+      animation: 150,
+      onEnd: (evt) => {
+        const newData = moveArrayItem(props.data, evt.oldIndex, evt.newIndex)
+        emits('on-drag', newData)
+      }
+    }
+  }
+]
+
+const tableDragableOptions = ref<boolean | Array<any>>([])
+
+if (props.isDragable) {
+  if (props.dragableTableOptions.length > 0) {
+    tableDragableOptions.value = props.dragableTableOptions
+  } else {
+    tableDragableOptions.value = defaultDragableOptions
+  }
+} else {
+  tableDragableOptions.value = false
+}
+
+// Initialize multi-select data when data changes
+watch(
+  () => props.data,
+  (newData) => {
+    if (props.multiSelect && props.multiSelectLogic && newData) {
+      props.multiSelectLogic.initializeData(newData)
+    }
+  },
+  { deep: true, immediate: true }
+)
+</script>
+
+<style lang="css" scoped>
+.m_image {
+  width: 48px;
+  height: 48px;
+}
+</style>

+ 53 - 0
src/components/mTable/types.ts

@@ -0,0 +1,53 @@
+export interface TableOptions {
+  // 字段名称
+  prop?: string
+  // 表头
+  label: string
+  // 对应列的宽度
+  width?: string | number
+  minWidth?: string | number
+  maxWidth?: string | number
+  // 对齐方式
+  align?: 'left' | 'center' | 'right'
+  //表格行配置项
+  type?: 'selection' | 'index' | 'expand'
+  columAttr?: Record<string, any>
+  // 自定义列模板的插槽名
+  slot?: string
+  // 是否是操作项
+  action?: {
+    label?: string
+    width?: string | number
+    align?: 'left' | 'center' | 'right'
+  }
+  isCopyText?: boolean
+  //时间数据
+  dateName?: string
+  //时间格式化
+  formatStr?: string
+
+  //图片数据
+  image?: {
+    src: string
+  }
+
+  cellType?: 'isCopyText' | 'dateName' | 'jsonData' | 'image'
+
+  //JSON数据 sourceName 为JSON的名称,keyName为JSON的值
+  json?: {
+    sourceName: string
+    keyName: string
+  }
+  // 是否可以编辑
+  editable?: boolean
+
+  //是否显示省略号
+  showOmission?: boolean
+
+  //isFixed 是否固定显示
+  fixed?: 'left' | 'right'
+  hide?: boolean
+  truncateText?: boolean
+  truncateTextLength?: number
+  postTime?: boolean
+}

+ 2 - 2
src/components/monitor/CameraSelector.vue

@@ -96,8 +96,8 @@ async function loadCameras() {
   loading.value = true
   loading.value = true
   try {
   try {
     const res = await adminListCameras()
     const res = await adminListCameras()
-    if (res.code === 200 && res.data) {
-      cameras.value = res.data.map((item) => ({
+    if (res.success && res.data.list) {
+      cameras.value = res.data.list.map((item) => ({
         id: String(item.id),
         id: String(item.id),
         name: item.name || `摄像头 ${item.id}`,
         name: item.name || `摄像头 ${item.id}`,
         // 根据实际数据判断 streamType,这里暂时默认为 webrtc
         // 根据实际数据判断 streamType,这里暂时默认为 webrtc

+ 228 - 0
src/composables/useMultiSelectLogic.ts

@@ -0,0 +1,228 @@
+import { ref, computed, type Ref, type ComputedRef } from 'vue'
+import type { CheckboxValueType } from 'element-plus'
+
+export interface MultiSelectState {
+  isAllSelected: boolean
+  isIndeterminate: boolean
+  selectedKeys: string[]
+  selectedRows: any[]
+  checkedMap?: Map<string, boolean>
+}
+
+export interface MultiSelectOptions {
+  selectKey?: string
+  useCheckedMap?: boolean
+  initialCheckedMap?: Map<string, boolean>
+  onSelectionChange?: (_state: MultiSelectState) => void
+  checkableStrategy?: (_row: any) => boolean
+}
+
+export interface MultiSelectLogic {
+  // State
+  isAllSelected: Ref<boolean>
+  isIndeterminate: Ref<boolean>
+  selectedKeys: ComputedRef<string[]>
+  selectedRows: ComputedRef<any[]>
+  checkedMap?: Ref<Map<string, boolean>>
+  multiSelectState: ComputedRef<MultiSelectState>
+  checkableStrategy?: (_row: any) => boolean
+
+  // Methods
+  handleSelectAll: (_data: any[]) => (_val: CheckboxValueType) => void
+  handleCheckboxChange: (_data: any[]) => (_row: any, _val: boolean) => void
+  clearSelection: (_data: any[]) => void
+  getSelectedData: (_data: any[]) => MultiSelectState
+  selectItems: (_data: any[], _keys: string[]) => void
+  unselectItems: (_data: any[], _keys: string[]) => void
+  toggleItem: (_data: any[], _key: string) => void
+  resetAllCheckedStatus: (_data: any[]) => void
+  initializeData: (_data: any[]) => void
+}
+
+export function useMultiSelectLogic(options: MultiSelectOptions = {}) {
+  const {
+    selectKey = 'id',
+    useCheckedMap = false,
+    initialCheckedMap = new Map(),
+    onSelectionChange,
+    checkableStrategy
+  } = options
+
+  // State
+  const isAllSelected = ref(false)
+  const isIndeterminate = ref(false)
+  const checkedMap = useCheckedMap ? ref(new Map(initialCheckedMap)) : undefined
+
+  // Computed values
+  const selectedKeys = computed(() => {
+    console.log('checkedMap', checkedMap)
+    if (checkedMap) {
+      return Array.from(checkedMap.value.keys()).filter((key) => checkedMap.value.get(key))
+    }
+    // Fallback: find selected from data directly (will be provided by methods)
+    return []
+  })
+
+  const selectedRows = computed(() => {
+    // This will be updated by the methods when called with data
+    return []
+  })
+
+  const multiSelectState = computed<MultiSelectState>(() => ({
+    isAllSelected: isAllSelected.value,
+    isIndeterminate: isIndeterminate.value,
+    selectedKeys: selectedKeys.value,
+    selectedRows: selectedRows.value,
+    ...(checkedMap && { checkedMap: checkedMap.value })
+  }))
+  // Internal method to get selected data
+  const getSelectedDataInternal = (data: any[]): MultiSelectState => {
+    const selectedRowsArray = data.filter((row) => row.checked)
+    const selectedKeysArray = selectedRowsArray.map((row) => row[selectKey])
+
+    return {
+      isAllSelected: isAllSelected.value,
+      isIndeterminate: isIndeterminate.value,
+      selectedKeys: checkedMap ? selectedKeys.value : selectedKeysArray,
+      selectedRows: selectedRowsArray,
+      ...(checkedMap && { checkedMap: checkedMap.value })
+    }
+  }
+  // Internal method to emit selection change
+  const emitSelectionChange = (data: any[]) => {
+    if (onSelectionChange) {
+      const state = getSelectedDataInternal(data)
+      onSelectionChange(state)
+    }
+  }
+
+  const resetAllCheckedStatus = (data: any[]) => {
+    const allChecked = data.length > 0 && data.every((item) => item.checked)
+    const someChecked = data.some((item) => item.checked)
+    isAllSelected.value = allChecked
+    isIndeterminate.value = someChecked && !allChecked
+  }
+
+  const handleSelectAll = (data: any[]) => (val: CheckboxValueType) => {
+    console.log('handleSelectAll', data, val)
+    const boolVal = !!val
+    data
+      .filter((row) => {
+        console.log('row', row)
+        console.log('checkableStrategy', checkableStrategy?.(row))
+        return checkableStrategy?.(row) || true
+      })
+      .forEach((row) => {
+        let checked = boolVal
+        if (checkableStrategy?.(row)) {
+          checked = false
+        }
+        row.checked = checked
+        if (checkedMap) {
+          checkedMap.value.set(row[selectKey], checked)
+        }
+      })
+    isAllSelected.value = boolVal
+    isIndeterminate.value = false
+    emitSelectionChange(data)
+  }
+
+  const handleCheckboxChange = (data: any[]) => (row: any, val: boolean) => {
+    if (checkedMap) {
+      checkedMap.value.set(row[selectKey], val)
+    }
+    resetAllCheckedStatus(data)
+    emitSelectionChange(data)
+  }
+
+  const clearSelection = (data: any[]) => {
+    data.forEach((row) => {
+      row.checked = false
+    })
+    if (checkedMap) {
+      checkedMap.value.clear()
+    }
+    resetAllCheckedStatus(data)
+    emitSelectionChange(data)
+  }
+
+  const getSelectedData = (data: any[]): MultiSelectState => {
+    return getSelectedDataInternal(data)
+  }
+
+  const selectItems = (data: any[], keys: string[]) => {
+    keys.forEach((key) => {
+      const row = data.find((item) => item[selectKey] === key)
+      if (row) {
+        row.checked = true
+        if (checkedMap) {
+          checkedMap.value.set(key, true)
+        }
+      }
+    })
+    resetAllCheckedStatus(data)
+    emitSelectionChange(data)
+  }
+
+  const unselectItems = (data: any[], keys: string[]) => {
+    keys.forEach((key) => {
+      const row = data.find((item) => item[selectKey] === key)
+      if (row) {
+        row.checked = false
+        if (checkedMap) {
+          checkedMap.value.set(key, false)
+        }
+      }
+    })
+    resetAllCheckedStatus(data)
+    emitSelectionChange(data)
+  }
+
+  const toggleItem = (data: any[], key: string) => {
+    const row = data.find((item) => item[selectKey] === key)
+    if (row) {
+      const newVal = !row.checked
+      row.checked = newVal
+      if (checkedMap) {
+        checkedMap.value.set(key, newVal)
+      }
+      resetAllCheckedStatus(data)
+      emitSelectionChange(data)
+    }
+  }
+
+  const initializeData = (data: any[]) => {
+    // Initialize new data's checked status
+    data.forEach((row) => {
+      if (row.checked === undefined) {
+        row.checked = false
+        if (useCheckedMap) {
+          row.checked = checkedMap?.value?.get(row[selectKey]) || false
+        }
+      }
+    })
+    resetAllCheckedStatus(data)
+  }
+
+  return {
+    // State
+    isAllSelected,
+    isIndeterminate,
+    selectedKeys,
+    selectedRows,
+    ...(checkedMap && { checkedMap }),
+    multiSelectState,
+
+    // Methods
+    handleSelectAll,
+    handleCheckboxChange,
+    clearSelection,
+    getSelectedData,
+    selectItems,
+    unselectItems,
+    toggleItem,
+    resetAllCheckedStatus,
+    initializeData,
+    checkableStrategy
+  }
+}

+ 192 - 0
src/const/options.json

@@ -0,0 +1,192 @@
+{
+  "domainSource": {
+    "c": "客户",
+    "s": "内置"
+  },
+  "domainType": {
+    "f": "前端",
+    "m": "管端",
+    "z": "总管",
+    "p": "前端永久",
+    "b": "前端备用"
+  },
+  "landingPage": {
+    "p": "首页",
+    "r": "注册页",
+    "u": "会员中心",
+    "c": "充值页",
+    "l": "登录页",
+    "a": "活动页",
+    "m": "消息中心"
+  },
+  "attachmentType": {
+    "p": "图片",
+    "f": "文件",
+    "a": "APK",
+    "e": "EXE",
+    "v": "视频",
+    "s": "声频",
+    "m": "音乐"
+  },
+  "analyticsCode": {
+    "y": "统计",
+    "n": "不统计"
+  },
+  "prefixPostfixType": {
+    "h": "在前面(head)",
+    "t": "在尾部(tail)"
+  },
+  "isOpen": {
+    "y": "开启",
+    "n": "关闭"
+  },
+  "isSee": {
+    "y": "是",
+    "n": "否"
+  },
+  "yOrN": {
+    "y": "是",
+    "n": "否"
+  },
+  "prefixPostfix": {
+    "h": "在前面(head)",
+    "t": "在尾部(tail)"
+  },
+  "forumCategoryType": {
+    "综合讨论": "综合讨论",
+    "技术和科学": "技术和科学",
+    "娱乐和休闲": "娱乐和休闲",
+    "生活和健康": "生活和健康",
+    "学术和教育": "学术和教育",
+    "文化和艺术": "文化和艺术",
+    "地区和社区": "地区和社区",
+    "职业和工作": "职业和工作",
+    "电子商务和网络营销": "电子商务和网络营销"
+  },
+  "viewFlag": {
+    "a": "所有可见",
+    "u": "用户可见",
+    "r": "关注可见",
+    "f": "好友可见"
+  },
+  "businessType": {
+    "S_WEB": "超管",
+    "C_APP": "移动端",
+    "C_WEB": "PC端",
+    "C_H5": "H5端"
+  },
+  "clientFlag": {
+    "w": "web",
+    "h": "H5",
+    "a": "android",
+    "i": "iOS"
+  },
+  "statusType": {
+    "1": "正常",
+    "0": "删除"
+  },
+  "relationFlag": {
+    "1": "粉丝",
+    "2": "好友"
+  },
+  "favoriteFlag": {
+    "b": "bbs",
+    "f": "论坛",
+    "m": "主板",
+    "p": "帖子",
+    "t": "图库"
+  },
+  "likeFlag": {
+    "b": "bbs",
+    "f": "论坛",
+    "m": "主板",
+    "p": "帖子",
+    "t": "图库"
+  },
+  "optionsArea": {
+    "cn": "中国",
+    "tw": "台湾",
+    "hk": "香港",
+    "mo": "澳门",
+    "th": "泰国",
+    "kr": "韩国",
+    "jp": "日本",
+    "us": "美国"
+  },
+  "componentType": {
+    "sysComponentConfig": "系统组件配置表",
+    "templateComponentConfig": "模版组件配置表",
+    "websiteComponentConfig": "站点组件配置表"
+  },
+  "isAttachment": {
+    "y": "有",
+    "n": "无"
+  },
+  "isForced": {
+    "y": "是",
+    "n": "否"
+  },
+
+  "domainStatus": {
+    "y": "启用",
+    "c": "关闭",
+    "n": "维护"
+  },
+
+  "isAnonymous": {
+    "y": "可",
+    "n": "否"
+  },
+  "levelOfNews": {
+    "1": "1",
+    "2": "2",
+    "3": "3",
+    "4": "4",
+    "5": "5"
+  },
+  "gameType": {
+    "211": "澳彩",
+    "84": "台彩",
+    "1": "港彩",
+    "3995": "新彩"
+  },
+  "voteType": {
+    "g": "竞猜",
+    "v": "投票"
+  },
+  "vote": {
+    "1": "鼠",
+    "2": "牛",
+    "3": "虎",
+    "4": "兔",
+    "5": "龙",
+    "6": "蛇",
+    "7": "马",
+    "8": "羊",
+    "9": "猴",
+    "10": "鸡",
+    "11": "狗",
+    "12": "猪"
+  },
+  "viewType": {
+    "tk": "图库",
+    "bbs": "BBS",
+    "news": "新闻",
+    "soft": "软件站"
+  },
+  "taskStatus": {
+    "n": "初始化",
+    "s": "已停止",
+    "e": "错误",
+    "f": "已完成",
+    "r": "执行中"
+  },
+  "bizType": {
+    "1": "积分增加",
+    "2": "积分减少"
+  },
+  "forStarBizType": {
+    "1": "星星增加",
+    "2": "星星减少"
+  }
+}

+ 2 - 2
src/layout/index.vue

@@ -555,14 +555,14 @@ async function handleChangePassword() {
           oldPassword: passwordForm.oldPassword,
           oldPassword: passwordForm.oldPassword,
           newPassword: passwordForm.newPassword
           newPassword: passwordForm.newPassword
         })
         })
-        if (res.code === 200) {
+        if (res.success) {
           ElMessage.success('密码修改成功,请重新登录')
           ElMessage.success('密码修改成功,请重新登录')
           passwordDialogVisible.value = false
           passwordDialogVisible.value = false
           resetPasswordForm()
           resetPasswordForm()
           await userStore.logoutAction()
           await userStore.logoutAction()
           router.push('/login')
           router.push('/login')
         } else {
         } else {
-          ElMessage.error(res.message || '密码修改失败')
+          ElMessage.error(res.errMessage || '密码修改失败')
         }
         }
       } catch (error: any) {
       } catch (error: any) {
         ElMessage.error(error.message || '密码修改失败')
         ElMessage.error(error.message || '密码修改失败')

+ 6 - 0
src/router/index.ts

@@ -145,6 +145,12 @@ const routes: RouteRecordRaw[] = [
         name: 'Monitor',
         name: 'Monitor',
         component: () => import('@/views/monitor/index.vue'),
         component: () => import('@/views/monitor/index.vue'),
         meta: { title: '多视频监控', icon: 'Monitor' }
         meta: { title: '多视频监控', icon: 'Monitor' }
+      },
+      {
+        path: 'test/m-table',
+        name: 'MTableDemo',
+        component: () => import('@/views/test/m-table-demo.vue'),
+        meta: { title: 'MTable 测试', icon: 'Grid' }
       }
       }
     ]
     ]
   },
   },

+ 2 - 2
src/store/user.ts

@@ -10,7 +10,7 @@ export const useUserStore = defineStore('user', () => {
 
 
   async function loginAction(loginForm: LoginParams) {
   async function loginAction(loginForm: LoginParams) {
     const res = await login(loginForm)
     const res = await login(loginForm)
-    if (res.code === 200 && res.data) {
+    if (res.success && res.data) {
       const { token: accessToken, expiresIn, admin } = res.data
       const { token: accessToken, expiresIn, admin } = res.data
       token.value = accessToken
       token.value = accessToken
       // 使用 cookie 存储 token,设置过期时间
       // 使用 cookie 存储 token,设置过期时间
@@ -25,7 +25,7 @@ export const useUserStore = defineStore('user', () => {
 
 
   async function getUserInfo() {
   async function getUserInfo() {
     const res = await getInfo()
     const res = await getInfo()
-    if (res.code === 200 && res.data) {
+    if (res.success && res.data) {
       userInfo.value = res.data
       userInfo.value = res.data
     }
     }
     return res
     return res

+ 28 - 8
src/types/index.ts

@@ -1,11 +1,31 @@
-// API 响应类型
-export interface ApiResponse<T = any> {
-  code: number
-  message: string
-  data: T
-  timestamp?: number
-  traceId?: string
-}
+// API 响应类型 - 基础响应(无数据)
+export interface BaseResponse {
+  success: boolean
+  errCode?: string
+  errMessage?: string
+}
+
+// API 响应类型 - 单个数据响应
+export interface IBaseResponse<T> {
+  data?: T
+  success: boolean
+  errCode?: string
+  errMessage?: string
+}
+
+// API 响应类型 - 分页列表响应
+export interface IResponse<T> {
+  success: boolean
+  errCode?: string
+  errMessage?: string
+  data: {
+    total: string
+    list: T[]
+  }
+}
+
+// 兼容旧代码 - ApiResponse 别名
+export type ApiResponse<T = any> = IResponse<T>
 
 
 // 分页参数
 // 分页参数
 export interface PageParams {
 export interface PageParams {

+ 421 - 0
src/utils/dataFormat.ts

@@ -0,0 +1,421 @@
+import { ref } from 'vue'
+export interface FormOptions {
+  value: string
+  label: string
+  type: string
+}
+export interface UploadData {
+  fileType: string
+  uploadFrom: string
+  storageStyle: number
+}
+
+export interface UploadResult {
+  uploadData: UploadData
+  fileType: string
+}
+
+export interface MimeTypeMapping {
+  [mimeType: string]: {
+    uploadType: string
+    fileType: string
+  }
+}
+
+export const defaultMimeTypeMappings: MimeTypeMapping = {
+  'image/jpeg': { uploadType: 'img', fileType: 'p' },
+  'image/png': { uploadType: 'img', fileType: 'p' },
+  'image/gif': { uploadType: 'img', fileType: 'p' },
+  'image/svg+xml': { uploadType: 'img', fileType: 'p' },
+  'text/plain': { uploadType: 'ico', fileType: 'f' },
+  'application/pdf': { uploadType: 'ico', fileType: 'f' },
+  'application/msword': { uploadType: 'ico', fileType: 'f' },
+  'application/vnd.openxmlformats-officedocument.wordprocessingml.document': {
+    uploadType: 'ico',
+    fileType: 'f'
+  },
+  'application/vnd.ms-excel': { uploadType: 'ico', fileType: 'f' },
+  'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': {
+    uploadType: 'ico',
+    fileType: 'f'
+  },
+  'application/vnd.android.package-archive': { uploadType: 'file', fileType: 'a' },
+  'application/octet-stream': { uploadType: 'file', fileType: 'e' },
+  'video/mp4': { uploadType: 'video', fileType: 'v' },
+  'video/webm': { uploadType: 'video', fileType: 'v' },
+  'video/ogg': { uploadType: 'video', fileType: 'v' },
+  'audio/mpeg': { uploadType: 'video', fileType: 's' },
+  'audio/wav': { uploadType: 'video', fileType: 's' },
+  'audio/ogg': { uploadType: 'video', fileType: 's' }
+}
+export const changeUploadDataWithCustom = (
+  mimeType: string,
+  attachmentId: string,
+  customMimeTypeMappings: Partial<MimeTypeMapping> = {},
+  basePath: string = 'bbs/forum/attachment'
+): UploadResult => {
+  // Merge custom mappings with default mappings
+  const mergedMappings: MimeTypeMapping = {
+    ...defaultMimeTypeMappings,
+    ...(customMimeTypeMappings as MimeTypeMapping)
+  }
+
+  const mapping = mergedMappings[mimeType]
+
+  if (mapping) {
+    return {
+      uploadData: {
+        fileType: mapping.uploadType,
+        uploadFrom: `${basePath}${attachmentId ? '/' + attachmentId : ''}`,
+        storageStyle: 6
+      },
+      fileType: mapping.fileType
+    }
+  }
+
+  // Fallback to general type matching if specific MIME type is not found
+  const generalType = mimeType.split('/')[0]
+  const generalMapping = Object.entries(mergedMappings).find(([key]) => key.startsWith(`${generalType}/`))
+
+  if (generalMapping) {
+    const [, mapping] = generalMapping
+    return {
+      uploadData: {
+        fileType: mapping.uploadType,
+        uploadFrom: `${basePath}${attachmentId ? '/' + attachmentId : ''}`,
+        storageStyle: 6
+      },
+      fileType: mapping.fileType
+    }
+  }
+
+  throw new Error(`Unsupported MIME type: ${mimeType}`)
+}
+export const addTypeToObjects = (arr: Omit<FormOptions, 'type'>[], value: string): FormOptions[] => {
+  return arr.map(
+    (obj) =>
+      ({
+        ...obj,
+        type: value
+      } as FormOptions)
+  )
+}
+
+//用于将文件大小转换成MB的形式显示
+export const showFileSize = (_size: string | number) => {
+  let size = _size
+  if (typeof size === 'string') {
+    size = Number(size)
+  }
+  return (size / 1024 / 1024).toFixed(3) + 'MB'
+}
+
+//将6位以上的值 以*显示,总长度限制在10位
+export const formateStrNum = (input: string | number, limit = 6, firstLength = 3, lastLength = 4): string => {
+  const str: string = input?.toString() || ''
+
+  if (str.length <= limit) {
+    return str
+  }
+
+  const firstText: string = str.slice(0, firstLength)
+  const lastText: string = str.slice(-lastLength)
+  const middleLength: number = Math.min(str.length - limit, 3) // 限制中间部分最多4位
+  const middle: string = '*'.repeat(middleLength)
+
+  return firstText + middle + lastText
+}
+
+/**
+ * 将字符串处理为保留前十位和后十位,中间部分显示为三个星号 '***'。
+ * 如果字符串长度小于或等于20,则返回原始字符串。
+ *
+ * @param {string} str - 需要处理的字符串。
+ * @returns {string} - 处理后的字符串,格式为 "前十位***后十位"。
+ *
+ * @example
+ * const originalString = "abcdefghijklmnopqrstuvwxyz0123456789";
+ * const maskedString = maskString(originalString);
+ * console.log(maskedString); // 输出: abcdefghij***0123456789
+ */
+export function handleStr10And10(str: any) {
+  if (str.length <= 20) {
+    return str // 如果字符串长度小于或等于20,则不做任何处理
+  }
+
+  const start = str.slice(0, 10) // 取前十位
+  const end = str.slice(-10) // 取后十位
+  return `${start}***${end}` // 中间替换为三个星号
+}
+
+/**
+ * 将字符串处理为保留前面和后面固定长度的字符串,中间部分显示为可配置的穿参。
+ * 如果字符串长度小于或等于maxLength,则返回原始字符串。
+ *
+ * @param {string} str - 需要处理的字符串。
+ * @param {number} [firstLength=4] - 保留的前面字符串的长度。
+ * @param {number} [lastLength=4] - 保留的后面字符串的长度。
+ * @param {number} [maxLength=8] - 如果字符串长度小于或等于maxLength,则不做任何处理。
+ * @param {string} [mask='***'] - 用于替换的字符串。
+ * @returns {string} - 处理后的字符串,格式为 "前面***后面"。
+ *
+ * @example
+ * const originalString = "abcdefghijklmnopqrstuvwxyz0123456789";
+ * const maskedString = handleStrMask(originalString);
+ * console.log(maskedString); // 输出: abcd***0123
+ */
+export function handleStrMask(
+  str: string,
+  firstLength: number = 4,
+  lastLength: number = 4,
+  maxLength: number = 8,
+  mask: string = '***'
+) {
+  if (str.length <= maxLength) {
+    return str // 如果字符串长度小于或等于20,则不做任何处理
+  }
+
+  const start = str.slice(0, firstLength) // 取前几位
+  const end = str.slice(-lastLength) // 取后几位
+  return `${start}${mask}${end}` // 中间替换为可配置的穿参
+}
+
+// src/utils/urlUtils.ts
+
+export function createWholeUrl(url: string | null | undefined, frontPath?: string): string {
+  if (!url) {
+    return ''
+  }
+
+  // If it's already a full URL, return it
+  if (url.startsWith('http://') || url.startsWith('https://')) {
+    return url
+  }
+
+  // If frontPath is provided
+  if (frontPath) {
+    // Remove trailing slash from frontPath if it exists
+    const cleanFrontPath = frontPath.endsWith('/') ? frontPath.slice(0, -1) : frontPath
+
+    // If url starts with a slash, remove it
+    const cleanUrl = url.startsWith('/') ? url.slice(1) : url
+
+    return `${cleanFrontPath}/${cleanUrl}`
+  }
+
+  // If no frontPath, just add the protocol
+  const protocol = 'https://'
+  return `${protocol}${url.startsWith('/') ? url.slice(1) : url}`
+}
+
+/**
+ * 将 HTML 实体解码为原始字符串。
+ * 该函数对于将 HTML 编码的字符串转换回其原始形式非常有用。
+ * 它会创建一个临时的 textarea 元素,将 HTML 编码的字符串赋值给该元素的 innerHTML,
+ * 然后从 textarea 元素的 value 属性中获取解码后的值。
+ *
+ * @param {string} str - 需要解码的 HTML 编码字符串。
+ * @returns {string} - 解码后的字符串。
+ */
+export const decodeHtmlEntities = (str: string) => {
+  // 创建一个临时的 textarea 元素
+  const txt = document.createElement('textarea')
+
+  // 将 HTML 编码的字符串赋值给 textarea 元素的 innerHTML
+  txt.innerHTML = str
+
+  // 获取并返回解码后的值
+  return txt.value
+}
+
+export const changeUploadData = (mimeType: string, uploadPath: string) => {
+  console.log(mimeType)
+
+  let fileType, uploadType
+
+  switch (mimeType) {
+    case 'image/jpeg':
+    case 'image/png':
+    case 'image/gif':
+    case 'image/svg+xml':
+      fileType = 'p'
+      uploadType = 'img'
+      break
+
+    case 'text/plain':
+    case 'application/pdf':
+    case 'application/msword':
+    case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
+    case 'application/vnd.ms-excel':
+    case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
+      fileType = 'f'
+      uploadType = 'ico'
+      break
+
+    case 'application/vnd.android.package-archive':
+      fileType = 'a'
+      uploadType = 'file'
+      break
+
+    case 'application/octet-stream':
+      fileType = 'e'
+      uploadType = 'file'
+      break
+
+    case 'video/mp4':
+    case 'video/webm':
+    case 'video/ogg':
+      fileType = 'v'
+      uploadType = 'video'
+      break
+
+    case 'audio/mpeg':
+    case 'audio/wav':
+    case 'audio/ogg':
+      fileType = 's'
+      uploadType = 'video' // Note: This is set to 'video' as in the original code
+      break
+
+    default:
+      fileType = ''
+      uploadType = ''
+      break
+  }
+
+  return {
+    uploadData: {
+      fileType: uploadType,
+      uploadFrom: uploadPath,
+      storageStyle: 6 // This value is kept as it might be a constant for your system
+    },
+    fileType: fileType
+  }
+}
+/**
+ * 计算年份选项的起始年份
+ * 规则:12月时从下一年开始,其他月份从当前年开始
+ * @param date 可选的日期参数,用于测试
+ */
+export const getYearStartValue = (date: Date = new Date()): number => {
+  const currentYear = date.getFullYear()
+  const currentMonth = date.getMonth() // 0-11,12月为11
+  return currentMonth === 11 ? currentYear + 1 : currentYear
+}
+
+/**
+ * 生成年份选项列表
+ * @param startYear 起始年份
+ * @param count 生成数量,默认5个
+ */
+export const generateYearOptions = (
+  startYear: number,
+  count: number = 5
+): Array<{ label: string; value: number; type: string }> => {
+  return Array.from({ length: count }, (_, index) => ({
+    label: String(startYear - index),
+    value: startYear - index,
+    type: 'option'
+  }))
+}
+
+// 导出默认的年份选项(基于当前日期计算)
+export const YearTypes = generateYearOptions(getYearStartValue())
+
+// 手动实现将对象转换为查询字符串的函数
+export const qs = (obj: Record<string, any>): string => {
+  return Object.entries(obj)
+    .map(([key, value]) => {
+      // 对键和值进行编码
+      return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
+    })
+    .join('&')
+}
+
+export const truncateText = (text: string, limit = 10) => {
+  if (!text) return ''
+  return text.length > limit ? text.slice(0, limit) + '...' : text
+}
+
+export const extractTextFromHTML = (text: string, limit = 10) => {
+  if (!text) return ''
+  try {
+    // Create a new DOMParser and parse the HTML string
+    const parser = new DOMParser()
+    const doc = parser.parseFromString(text, 'text/html')
+    // Get the text content from the parsed HTML
+    const plainText = doc.body.textContent || text
+    return plainText.length > limit ? plainText.slice(0, limit) + '...' : plainText
+  } catch (error) {
+    // If parsing fails, return the original text
+    return text.length > limit ? text.slice(0, limit) + '...' : text
+  }
+}
+
+export const cleanObject = (obj: any) => {
+  const result: any = {}
+  for (const key in obj) {
+    if (Object.prototype.hasOwnProperty.call(obj, key)) {
+      const value = obj[key]
+      // 检查 key 是否为空或仅包含空格
+      if (key.trim() === '') {
+        continue
+      }
+      // 检查 value 是否为无效值
+      if (
+        value === null || // null
+        value === undefined || // undefined
+        (Array.isArray(value) && value.length === 0) || // 空数组
+        (typeof value === 'string' && value.trim() === '') || // 空字符串
+        (typeof value === 'object' && Object.keys(value).length === 0) // 空对象
+      ) {
+        continue
+      }
+      // 保留有效的键值对
+      result[key] = value
+    }
+  }
+  return result
+}
+
+export const flagOptions = ref<any[]>([
+  {
+    label: '用户标签',
+    value: '用户标签'
+  },
+  {
+    label: '平台标签',
+    value: '平台标签'
+  },
+  {
+    label: '内容标签',
+    value: '内容标签'
+  },
+  {
+    label: '内部标签',
+    value: '内部标签'
+  }
+])
+
+export function getImageDimensionsURL(file: File): Promise<{ width: number; height: number }> {
+  return new Promise((resolve, reject) => {
+    const img = new Image()
+
+    img.onload = () => {
+      // Clean up the object URL to free memory
+      URL.revokeObjectURL(img.src)
+
+      resolve({
+        width: img.width,
+        height: img.height
+      })
+    }
+
+    img.onerror = () => {
+      URL.revokeObjectURL(img.src)
+      reject(new Error('Failed to load image'))
+    }
+
+    // Create a URL for the file
+    img.src = URL.createObjectURL(file)
+  })
+}

+ 211 - 0
src/utils/dateFormat/index.ts

@@ -0,0 +1,211 @@
+// import timezone from '@/utils/timezone'
+import { format, fromUnixTime, isValid, parse } from 'date-fns'
+import { toZonedTime } from 'date-fns-tz'
+
+/**
+ * 格式化并转换时区的日期时间
+ * @param {number} timestamp - 时间戳(毫秒)
+ * @param {string} [formatStr='yyyy-MM-dd'] - 日期格式字符串
+ * @param {string} [timeZone] - 目标时区,不填则使用本地时区
+ * @returns {string} 格式化后的日期字符串
+ */
+export const unitFormatDate = (
+  timestamp: number | string | undefined,
+  formatStr: string = 'yyyy-MM-dd',
+  timeZone?: string
+): string | null => {
+  if (typeof timestamp === 'number') {
+    if (timestamp <= 3600000) {
+      return '--'
+    }
+  } else if (parseInt(timestamp) <= 3600000) {
+    return '--'
+  }
+
+  if (!timestamp) {
+    return '--'
+  }
+  if (typeof timestamp === 'string') timestamp = Number(timestamp)
+
+  try {
+    // 如果没有指定时区,使用本地时区
+    if (!timeZone) {
+      timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
+    }
+    // 将时间戳转换为目标时区的 Date 对象
+    const zonedDate = toZonedTime(fromUnixTime(timestamp / 1000), timeZone)
+
+    // 格式化日期
+    return format(zonedDate, formatStr)
+  } catch (error) {
+    console.error('日期格式化错误:', error)
+    return null
+  }
+}
+
+/**
+ * 将时间戳转换为 Date 对象
+ * @param {number|string} timestamp - 时间戳(毫秒)
+ * @param {string} [timeZone='Asia/Shanghai'] - 目标时区
+ * @returns {Date} Date 对象
+ */
+export const unitToDate = (timestamp: number | string, timeZone?: string): Date => {
+  if (!timestamp) {
+    throw new Error('时间戳不能为空')
+  }
+  if (typeof timestamp === 'string') timestamp = Number(timestamp)
+
+  if (!timeZone) {
+    timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
+  }
+  // console.log(timeZone)
+  // console.log(toZonedTime(fromUnixTime(timestamp / 1000), timeZone), 'toZonedTime')
+  return toZonedTime(fromUnixTime(timestamp / 1000), timeZone)
+}
+
+/**
+ * 将时间字符串转换为北京时间的时间戳
+ * @param {string} dateStr - 日期时间字符串
+ * @param {string} [formatStr='yyyy-MM-dd HH:mm:ss'] - 日期时间字符串的格式
+ * @returns {number | null} 北京时间的时间戳(毫秒),如果无法转换则返回 null
+ */
+export const convertToTimestamp = (dateStr: string, formatStr: string = 'yyyy-MM-dd HH:mm:ss'): number | null => {
+  // 检查是否为空字符串或 '--'
+  if (!dateStr || dateStr.trim() === '--') {
+    return null
+  }
+
+  try {
+    // 将输入的字符串解析为 Date 对象
+    const parsedDate = parse(dateStr, formatStr, new Date())
+
+    // 检查解析后的日期是否有效
+    if (!isValid(parsedDate)) {
+      console.warn('无效的日期字符串:', dateStr)
+      return null
+    }
+
+    // 时间戳不需要转换时区
+    // const beijingDate = toZonedTime(parsedDate, 'Asia/Shanghai')
+
+    // 返回北京时间的时间戳(毫秒)
+    return parsedDate.getTime()
+  } catch (error) {
+    console.error('时间戳转换错误:', error)
+    return null
+  }
+}
+
+/**
+ * 将时间对象转换为特定时区的时间戳
+ * @param {Date} date - 时间对象
+ * @param {string} [timeZone='Asia/Shanghai'] - 目标时区
+ * @returns {number} 特定时区的时间戳(毫秒)
+ */
+export const dateToTimestamp = (date: Date, timeZone: string = 'Asia/Shanghai') => {
+  if (!(date instanceof Date) || !date) {
+    return null
+  }
+  return date.getTime()
+}
+/**
+ * 处理域名变化
+ * @param {string} newVal - 新的域名值
+ * @param {any} form - 表单对象
+ * @param {string} [name='hostDomain'] - 表单字段名称
+ */
+export const handleDomainChange = (newVal: string, form: any, name: string = 'hostDomain') => {
+  // 使用URL构造函数解析输入
+  try {
+    const url = new URL(newVal)
+    // 提取主机名(域名)
+    form[name] = url.hostname
+  } catch (error) {
+    const domainMatch = newVal.match(/^(https?:\/\/)?(www\.)?([\w.-]+)/i)
+    form[name] = domainMatch ? (domainMatch[2] || '') + domainMatch[3] : newVal
+  }
+}
+/**
+ * 处理URL变化
+ * @param {string} newVal - 新的Url值
+ * @param {any} form - 表单对象
+ * @param {string} [name='targetUrl'] - 表单字段名称
+ */
+export const handleUrlChange = (newVal: string, form: any, name: string = 'targetUrl') => {
+  let urlString = newVal.trim()
+
+  if (!/^https?:\/\//i.test(urlString)) {
+    urlString = `https://${urlString}`
+  }
+
+  try {
+    const url = new URL(urlString)
+    console.log(url)
+
+    form[name] = url.href.replace(/\/$/, '')
+  } catch (error) {
+    form[name] = newVal
+  }
+}
+/**
+ * 处理域名变化
+ * @param {string} newVal - 新的域名值
+ * @param {any} form - 表单对象
+ * @param {string} [name='hostDomain'] - 表单字段名称
+ */
+export const handleNumberChange = (
+  newVal: number,
+  form: any,
+  name: string = 'hostPort',
+  defaultmax = 65535,
+  defaultmin = 0
+) => {
+  let result = Number(newVal)
+  console.log(result)
+  if (result > defaultmax) {
+    result = defaultmax
+  } else if (result < 0) {
+    result = defaultmin
+  }
+  form[name] = result
+}
+/**
+ * Limits a string to its last 10 characters. If the string is shorter than the limit,
+ * the original string is returned. If the value is not a string, it is returned as is.
+ *
+ * @param {string} value - The string to be limited.
+ * @param {number} limit - The maximum length of the string.
+ * @return {string} The limited string or the original string if it is shorter than the limit.
+ */
+export const limitToLast10Digits = (value: string, limit: number = 10) => {
+  if (typeof value === 'string' && value.length > limit) {
+    return `...${value.slice(-limit)}`
+  }
+  return value
+}
+
+/**
+ * Limits a string to show the first 4 digits and the last 6 digits.
+ * If the string is longer than 10 characters, it adds an ellipsis in between.
+ * If the string is 10 characters or shorter, it returns the original string.
+ *
+ * @param {string} value - The string to be limited.
+ * @return {string} The limited string or the original string if it's 10 characters or shorter.
+ */
+export const limitToFirstAndLastDigits = (value: string, firstLimit: number = 4, lastLimit: number = 6): string => {
+  if (typeof value === 'string' && value.length > firstLimit + lastLimit) {
+    return `${value.slice(0, firstLimit)}...${value.slice(-lastLimit)}`
+  }
+  return value
+}
+
+export function getYesterday() {
+  const today = new Date()
+  const yesterday = new Date(today)
+  yesterday.setDate(today.getDate() - 1)
+  return yesterday
+}
+
+export const disableDateFn = (time: Date) => {
+  return time.getTime() < getYesterday().getTime()
+}

+ 18 - 23
src/utils/request.ts

@@ -2,7 +2,7 @@ import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse
 import { ElMessage } from 'element-plus'
 import { ElMessage } from 'element-plus'
 import { getToken } from './auth'
 import { getToken } from './auth'
 import { useUserStore } from '@/store/user'
 import { useUserStore } from '@/store/user'
-import type { ApiResponse } from '@/types'
+import type { BaseResponse } from '@/types'
 
 
 const service: AxiosInstance = axios.create({
 const service: AxiosInstance = axios.create({
   baseURL: import.meta.env.VITE_APP_BASE_API || '/api',
   baseURL: import.meta.env.VITE_APP_BASE_API || '/api',
@@ -29,32 +29,27 @@ service.interceptors.request.use(
 
 
 // 响应拦截器
 // 响应拦截器
 service.interceptors.response.use(
 service.interceptors.response.use(
-  (response: AxiosResponse<ApiResponse>) => {
+  (response: AxiosResponse<BaseResponse>) => {
     const res = response.data
     const res = response.data
-    const code = res.code || 200
 
 
     // 二进制数据直接返回
     // 二进制数据直接返回
     if (response.request.responseType === 'blob' || response.request.responseType === 'arraybuffer') {
     if (response.request.responseType === 'blob' || response.request.responseType === 'arraybuffer') {
       return response.data
       return response.data
     }
     }
 
 
-    if (code === 401) {
-      // 登录过期,直接跳转到登录页
-      const userStore = useUserStore()
-      userStore.resetToken()
-      ElMessage.warning('登录已过期,请重新登录')
-      location.href = '/login'
-      return Promise.reject(new Error('登录已过期'))
-    }
-
-    if (code === 500) {
-      ElMessage.error(res.message || '服务器错误')
-      return Promise.reject(new Error(res.message || '服务器错误'))
-    }
+    // 新响应格式:使用 success 字段判断
+    if (res.success === false) {
+      // 认证失败
+      if (res.errCode === 'UNAUTHORIZED' || res.errCode === '401') {
+        const userStore = useUserStore()
+        userStore.resetToken()
+        ElMessage.warning('登录已过期,请重新登录')
+        location.href = '/login'
+        return Promise.reject(new Error('登录已过期'))
+      }
 
 
-    if (code !== 200) {
-      ElMessage.error(res.message || '请求失败')
-      return Promise.reject(new Error(res.message || '请求失败'))
+      // 其他错误:不在拦截器中显示消息,由调用方处理
+      // 直接返回响应,让调用方根据 success 判断
     }
     }
 
 
     return res as any
     return res as any
@@ -82,19 +77,19 @@ service.interceptors.response.use(
   }
   }
 )
 )
 
 
-export function get<T = any>(url: string, params?: object, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
+export function get<T = any>(url: string, params?: object, config?: AxiosRequestConfig): Promise<T> {
   return service.get(url, { params, ...config })
   return service.get(url, { params, ...config })
 }
 }
 
 
-export function post<T = any>(url: string, data?: object, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
+export function post<T = any>(url: string, data?: object, config?: AxiosRequestConfig): Promise<T> {
   return service.post(url, data, config)
   return service.post(url, data, config)
 }
 }
 
 
-export function put<T = any>(url: string, data?: object, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
+export function put<T = any>(url: string, data?: object, config?: AxiosRequestConfig): Promise<T> {
   return service.put(url, data, config)
   return service.put(url, data, config)
 }
 }
 
 
-export function del<T = any>(url: string, params?: object, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
+export function del<T = any>(url: string, params?: object, config?: AxiosRequestConfig): Promise<T> {
   return service.delete(url, { params, ...config })
   return service.delete(url, { params, ...config })
 }
 }
 
 

+ 18 - 0
src/utils/showErrorMessage.ts

@@ -0,0 +1,18 @@
+import { ElMessage } from 'element-plus' // 确保已正确安装并导入 Element Plus
+
+/**
+ * 显示错误消息
+ * @param {Object} res - 响应对象,包含 errMessage 和 errCode
+ */
+function showErrorMessage(res: any) {
+  if (res) {
+    ElMessage({
+      message: `${res.errMessage} ----- [${res.errCode}]`,
+      type: 'error'
+    })
+  } else {
+    console.error('Invalid response object')
+  }
+}
+
+export default showErrorMessage

+ 568 - 0
src/views/account/addUserDialog.vue

@@ -0,0 +1,568 @@
+<template>
+  <el-dialog v-model="rulesDialogVisible" class="tabs-dialog" destroy-on-close @closed="onClosed">
+    <el-tabs v-model="activeTab" class="m_tabs">
+      <el-tab-pane :label="currentUserId ? '更新用户' : '新增用户'" name="template_detail" class="m_tabs_pane">
+        <el-form class="single-form" :model="queryForm" ref="ruleFormRef" status-icon :rules="getRules">
+          <el-form-item label="用户名" prop="username">
+            <el-input maxlength="16" v-model="queryForm.username" auto-complete="off" :disabled="!!currentUserId" />
+          </el-form-item>
+          <el-form-item :label="currentUserId ? t('login.editpassWord') : t('login.passWord') + ' :'" prop="password">
+            <el-input v-model="queryForm.password" type="password" auto-complete="off" show-password />
+          </el-form-item>
+          <el-form-item :label="t('login.confirmPassword') + ' :'" prop="passwordRety">
+            <el-input v-model="queryForm.passwordRety" type="password" show-password />
+          </el-form-item>
+          <el-form-item :label="t('pop.systemIdentity') + ' :'">
+            <div class="checked">
+              <el-checkbox v-model="checkAll" @change="handleCheckAllChange" :indeterminate="isIndeterminate">
+                {{ t('input.SelectAll') }}
+              </el-checkbox>
+              <el-checkbox-group v-model="queryForm.roleIds" @change="handleCheckedCitiesChange">
+                <el-checkbox v-for="role in rolesList" :key="role" :label="role">
+                  {{ role }}
+                </el-checkbox>
+              </el-checkbox-group>
+            </div>
+          </el-form-item>
+          <el-form-item :label="t('table.mobilePhoneNumber') + ' :'" prop="phone">
+            <MobileCountry
+              style="width: 300px"
+              v-model:mobile-country-code="queryForm.cc"
+              v-model:mobile="queryForm.phone"
+            />
+          </el-form-item>
+          <el-form-item :label="t('pop.secretkey') + ' :'" v-if="currentUserId">
+            <div class="secret-key">
+              <div class="one">
+                <el-input disabled class="onee" v-model="currentGoogleSecret"></el-input>
+              </div>
+              <div class="two">
+                <el-button @click="getSecret">{{ t('pop.getsecretkey') }}</el-button>
+              </div>
+            </div>
+          </el-form-item>
+          <el-form-item :label="t('table.qrcode') + ' :'" v-if="showQrcode">
+            <div class="QR-code">
+              <div>
+                <qrcode-vue :value="qrCode" :size="128" level="H" />
+              </div>
+              <p>{{ t('pop.ScanQRcode') }}</p>
+            </div>
+          </el-form-item>
+        </el-form>
+        <div class="submit-wrapper" v-if="showSubmitBar">
+          <div class="btn">
+            <el-button @click="rulesDialogVisible = false">取消</el-button>
+            <el-button type="primary" :loading="isSumitFormLoading" @click="submitForm(ruleFormRef)">提交</el-button>
+          </div>
+        </div>
+      </el-tab-pane>
+    </el-tabs>
+  </el-dialog>
+</template>
+
+<script lang="ts" setup>
+// import i18n from '@/locales'
+// const { t } = i18n.global
+// import { ElMessage, type CheckboxValueType, type FormInstance } from 'element-plus'
+// import { reactive, ref } from 'vue'
+// // import { reqRoleDAll } from '@/api/system/roles/index'
+// import type { InternalRuleItem } from '@/components/mForm/rule'
+// import QrcodeVue from 'qrcode.vue'
+// import { reqGoogle, reqSystemUserAccountList, reqSystemUserCreate, reqSystemUserEdit } from '@/api/system/account'
+// // import type {
+// //   IReqGoogleQueryParams,
+// //   IReqSystemUserAccountListData,
+// //   IReqSystemUserAccountListQueryParams,
+// //   IReqSystemUserCreateQueryParams,
+// //   IReqSystemUserEditQueryParams
+// // } from '@/api/system/account/types'
+// import showErrorMessage from '@/utils/showErrorMessage'
+
+// const emits = defineEmits(['onClose'])
+// const ruleFormRef = ref<FormInstance>()
+// const isSumitFormLoading = ref(false)
+// const activeTab = ref('template_detail')
+// const rulesDialogVisible = ref(false)
+// const currentUserId = ref<number>()
+// const passwordRety = ref('')
+// const showSubmitBar = computed(() => {
+//   return activeTab.value === 'template_detail'
+// })
+
+// //区分点击的是新增账号还是编辑账号   如果为真 则是编辑
+// //控制弹出框表单 身分 是否全选
+// const checkAll = ref(false)
+// //弹出框中  哪些系统身份被勾选
+// const checkedCities = ref<string[]>()
+// //弹出框中  有哪些系统身份
+// const rolesList = ref<string[]>()
+
+// //弹出框中  点击全选的回调
+// const isIndeterminate = ref(false)
+// const handleCheckAllChange = (val: CheckboxValueType) => {
+//   queryForm.roleIds = val ? rolesList.value : []
+//   isIndeterminate.value = false
+// }
+// const handleCheckedCitiesChange = (value: CheckboxValueType[]) => {
+//   const checkedCount = value.length
+//   checkAll.value = checkedCount === rolesList.value.length
+//   isIndeterminate.value = checkedCount > 0 && checkedCount < rolesList.value.length
+// }
+
+// //账号状态
+// const accountState = ref()
+// //是否显示微信二维码
+// const showQrcode = ref(false)
+// const currentGoogleSecret = ref()
+// const qrCode = ref()
+// //获取密钥
+// const getSecret = async () => {
+//   if (!queryForm.cc) {
+//     ElMessage({
+//       type: 'warning',
+//       message: '请选择国家手机区号'
+//     })
+//     return
+//   }
+//   if (!queryForm.phone) {
+//     ElMessage({
+//       type: 'warning',
+//       message: '请添加手机号'
+//     })
+//     return
+//   }
+//   let data: IReqGoogleQueryParams = {
+//     accountId: currentUserId.value?.toString()!,
+//     cc: queryForm.cc,
+//     phone: queryForm.phone
+//   }
+//   console.log(data)
+//   let res = await reqGoogle(data)
+//   if (res.success) {
+//     ElMessage({
+//       message: t('pop.getsecretkey') + t('errorCode.0'),
+//       type: 'success'
+//     })
+//     currentGoogleSecret.value = res.data?.googleSecret
+//     qrCode.value = res.data?.qrCode
+//     showQrcode.value = true
+//   } else {
+//     showErrorMessage(res)
+//   }
+// }
+
+// const onClosed = () => {
+//   resetForm()
+//   emits('onClose')
+//   rulesDialogVisible.value = false
+// }
+
+// const checkUsernameExsit = (rule: InternalRuleItem, value: any, callback: (error?: string | Error) => void) => {
+//   // 编辑
+//   if (currentUserId.value) {
+//     callback()
+//   } else {
+//     if (value) {
+//       // 新增
+//       const query: IReqSystemUserAccountListQueryParams = {
+//         page: 1,
+//         size: 15,
+//         username: value
+//       }
+//       reqSystemUserAccountList(query).then((res) => {
+//         if (res.success) {
+//           if (res.data.list.length > 0) {
+//             callback(new Error('用户名已经存在'))
+//           } else {
+//             callback()
+//           }
+//         }
+//       })
+//     }
+//   }
+// }
+
+// const validatePass = (rule: any, value: any, callback: any) => {
+//   // 编辑
+//   if (currentUserId.value) {
+//     if (queryForm.passwordRety) {
+//       if (queryForm.password !== queryForm.passwordRety) {
+//         callback(new Error('两次密码不一致'))
+//       }
+//     }
+//   } else {
+//     // 新增
+//     if (!value) {
+//       callback(new Error('请输入密码'))
+//     }
+//   }
+//   callback()
+// }
+
+// const validatePassRety = (rule: any, value: any, callback: any) => {
+//   // 编辑
+//   if (currentUserId.value) {
+//     if (value) {
+//       if (queryForm.password !== queryForm.passwordRety) {
+//         callback(new Error('两次密码不一致'))
+//       }
+//     }
+//   } else {
+//     // 新增
+//     if (queryForm.password !== queryForm.passwordRety) {
+//       callback(new Error('两次密码不一致'))
+//     }
+//   }
+//   callback()
+// }
+
+// const getRules = computed(() => {
+//   return currentUserId.value ? editRules : newRules
+// })
+
+// const newRules = reactive({
+//   username: [
+//     { required: true, message: '用户名不能为空' },
+//     {
+//       validator: checkUsernameExsit
+//     }
+//   ],
+//   password: [{ required: true, message: '密码不能为空' }, { validator: validatePass }],
+//   passwordRety: [{ required: true, message: '请再次输入密码' }, { validator: validatePassRety }],
+//   phone: [
+//     {
+//       validator: (rule: any, value: any, callback: any) => {
+//         if (!value) {
+//           callback()
+//         } else {
+//           if (value.length > 10) {
+//             callback(new Error('最多10位电话号码'))
+//           } else {
+//             callback()
+//           }
+//         }
+//       }
+//     }
+//   ]
+// })
+
+// const editRules = reactive({
+//   password: [
+//     {
+//       validator: {
+//         validator: (rule: any, value: any, callback: any) => {
+//           if (value) {
+//             if (queryForm.passwordRety) {
+//               if (value === queryForm.passwordRety) {
+//                 callback()
+//               } else {
+//                 callback(new Error('两次密码不一致'))
+//               }
+//             } else {
+//               //
+//               callback(new Error('请再次输入密码'))
+//             }
+//           } else {
+//             callback()
+//           }
+//         }
+//       }
+//     }
+//   ],
+//   passwordRety: [
+//     {
+//       validator: (rule: any, value: any, callback: any) => {
+//         if (queryForm.password) {
+//           if (!value) {
+//             // 请再次输入密码
+//             callback(new Error('请再次输入密码'))
+//           } else {
+//             if (value === queryForm.password) {
+//               callback()
+//             } else {
+//               callback(new Error('两次密码不一致'))
+//             }
+//           }
+//         } else {
+//           callback()
+//         }
+//       }
+//     }
+//   ],
+//   phone: [
+//     {
+//       validator: (rule: any, value: any, callback: any) => {
+//         if (!value) {
+//           callback()
+//         } else {
+//           if (value.length > 10) {
+//             callback(new Error('最多10位电话号码'))
+//           } else {
+//             callback()
+//           }
+//         }
+//       }
+//     }
+//   ]
+// })
+
+// const initQueryForm = (): IReqSystemUserCreateQueryParams => ({
+//   username: '',
+//   password: '',
+//   cc: '',
+//   phone: '',
+//   roleIds: [],
+//   passwordRety: ''
+// })
+
+// const queryForm = reactive<IReqSystemUserCreateQueryParams>(initQueryForm())
+
+// const resetForm = () => {
+//   ruleFormRef.value?.resetFields()
+//   currentUserId.value = undefined
+//   currentGoogleSecret.value = ''
+//   qrCode.value = ''
+//   showQrcode.value = false
+//   Object.assign(queryForm, initQueryForm())
+// }
+
+// const submitForm = async (formEl: FormInstance | undefined) => {
+//   if (!formEl) return
+//   await formEl.validate(async (valid, fields) => {
+//     if (valid) {
+//       try {
+//         isSumitFormLoading.value = true
+//         let res
+//         // update template
+//         if (currentUserId.value) {
+//           // 更新
+//           const queryEdit: IReqSystemUserEditQueryParams = {
+//             ...queryForm,
+//             id: currentUserId.value,
+//             phone: queryForm.phone
+//           }
+//           if (!queryEdit.password) {
+//             delete queryEdit.password
+//           }
+
+//           if (currentGoogleSecret.value === queryEdit.googleSecret) {
+//             delete queryEdit.googleSecret
+//           }
+
+//           delete queryEdit.passwordRety
+//           res = await reqSystemUserEdit(queryEdit)
+//         } else {
+//           //创建
+//           const query: IReqSystemUserCreateQueryParams = {
+//             ...queryForm
+//           }
+
+//           delete query.passwordRety
+//           res = await reqSystemUserCreate(query)
+//         }
+//         if (res.success) {
+//           ElMessage({
+//             message: currentUserId.value ? '更新成功' : '已添加',
+//             type: 'success'
+//           })
+//           // 关闭dailog
+//           rulesDialogVisible.value = false
+//           activeTab.value = 'template_detail'
+//           resetForm()
+//         } else {
+//           ElMessage({
+//             message: res.errMessage,
+//             type: 'error'
+//           })
+//         }
+//       } catch (error) {
+//         ElMessage({
+//           message: '提交出错',
+//           type: 'error'
+//         })
+//       } finally {
+//         isSumitFormLoading.value = false
+//       }
+//     } else {
+//       console.info('error submit!', fields)
+//     }
+//   })
+// }
+
+// const openDailog = (row?: IReqSystemUserAccountListData) => {
+//   rulesDialogVisible.value = true
+//   activeTab.value = 'template_detail'
+//   getRolesAll()
+//   if (row?.id) {
+//     Object.assign(queryForm, row)
+//     queryForm.phone = row.originPhone
+//     currentGoogleSecret.value = row.googleSecret
+//     currentUserId.value = row.id
+//     if (row?.roleIds?.length > 0) {
+//       isIndeterminate.value = true
+//     }
+//   } else {
+//     resetForm()
+//     currentUserId.value = undefined
+//   }
+// }
+// export interface IExpose {
+//   openDailog: (row?: IReqSystemUserAccountListData) => void
+// }
+// // 分发方法
+// defineExpose<IExpose>({
+//   openDailog
+// })
+
+// const fetchData = async (siteId) => {
+//   // isLoading.value = true
+//   // try {
+//   //   // getClientWebsiteConfig
+//   //   const query: IReqAdvertisementGetOneQueryParams = {
+//   //     siteId
+//   //   }
+//   //   const res = await reqAdvertisementGetOne(query)
+//   //   if (res.success && res.data) {
+//   //     Object.assign(queryForm, res.data)
+//   //     createTime.value = res.data?.createTime
+//   //     currentUserId.value = res.data.siteId
+//   //     currentSiteName.value = res.data.siteName
+//   //   } else {
+//   //     ElMessage({
+//   //       type: 'warning',
+//   //       message: '数据读取失败'
+//   //     })
+//   //   }
+//   // } catch (error) {
+//   //   console.error(error)
+//   // } finally {
+//   //   isLoading.value = false
+//   // }
+// }
+
+// const getRolesAll = async () => {
+//   let res = await reqRoleDAll()
+//   if (res.success) {
+//     rolesList.value = res.data?.roles
+//   }
+// }
+</script>
+
+<style lang="less" scoped>
+.input-sitename {
+  width: 100%;
+}
+
+.template_id_text {
+  display: flex;
+  justify-content: center;
+}
+
+.submit-wrapper {
+  display: flex;
+  align-items: flex-end;
+  justify-content: flex-end;
+  padding: 0;
+}
+
+//  custom-tabs-dialog
+.custom-tabs-dialog {
+  margin-top: 80px;
+  padding: 10px;
+  height: calc(100vh - 8.5rem);
+  width: 78%;
+  position: relative;
+  border-radius: 0.6rem;
+}
+
+.custom-tabs-dialog .el-dialog__header {
+  padding: 0;
+  position: absolute;
+  right: -15px;
+  top: -15px;
+  z-index: 1;
+  border-radius: 100%;
+  background: #e4e7ed;
+  height: 40px;
+  width: 40px;
+}
+
+.custom-tabs-dialog .el-dialog__header .el-dialog__headerbtn {
+  height: 40px;
+  width: 40px;
+  line-height: 1;
+}
+
+.custom-tabs-dialog .el-dialog__header .el-dialog__headerbtn .el-dialog__close {
+  color: red;
+}
+
+.custom-tabs-dialog .el-dialog__body {
+  overflow: hidden;
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+}
+
+.custom-tabs-dialog .el-dialog__body .el-tabs {
+  display: flex;
+  flex-direction: column;
+  overflow: auto;
+  flex: 1;
+}
+
+.custom-tabs-dialog .el-dialog__body .el-tabs .el-tabs__content {
+  flex: 1;
+  overflow-y: auto;
+}
+
+.checked {
+  .el-checkbox {
+    width: 100px;
+  }
+}
+
+.phone {
+  display: flex;
+
+  p {
+    font-size: 22px;
+  }
+
+  .one {
+    margin: 0 10px;
+
+    .onee {
+      width: 80px;
+    }
+  }
+
+  .two {
+    .twoo {
+      width: 180px;
+    }
+  }
+}
+
+.secret-key {
+  display: flex;
+
+  .one {
+    .onee {
+      width: 200px;
+      margin-right: 10px;
+    }
+  }
+}
+
+.single-form {
+  flex: 1;
+  overflow: auto;
+}
+
+.is-no-required .el-form-item__label::before {
+  color: white !important;
+}
+</style>

+ 46 - 0
src/views/account/data.ts

@@ -0,0 +1,46 @@
+import type { TableOptions } from '@/components/mTable/types'
+
+// 年份 期数 图纸名称 报纸名称 系列名称 彩种 发布用户 评论内容
+//Table需要的数据
+export const options: TableOptions[] = [
+  {
+    prop: 'username',
+    label: '账号',
+    slot: 'username',
+    columAttr: {
+      width: 180
+    }
+  },
+  {
+    prop: 'roleIds',
+    label: '角色身份',
+    slot: 'roleIds',
+    columAttr: {
+      minWidth: 240
+    }
+  },
+  {
+    prop: 'phone',
+    label: '手机号',
+    slot: 'phone',
+    columAttr: {
+      width: 155
+    }
+  },
+  {
+    prop: 'state',
+    label: '状态',
+    slot: 'state',
+    columAttr: {
+      width: 80
+    }
+  },
+  {
+    prop: 'id',
+    label: '操作',
+    slot: 'id',
+    columAttr: {
+      width: 160
+    }
+  }
+]

+ 276 - 0
src/views/account/index.vue

@@ -0,0 +1,276 @@
+<template>
+  <section class="wrapper">
+    <section ref="tableRef">
+      <el-form
+        ref="queryRef"
+        :inline="true"
+        @keyup.enter="fetchData({ page: 1 })"
+        :model="queryForm"
+        class="custom-el-form no-rule"
+        label-width="70px"
+      >
+        <el-form-item label="" prop="username">
+          <el-input v-model="queryForm.username" placeholder="查询内容" />
+        </el-form-item>
+
+        <el-form-item label="" prop="state">
+          <el-select v-model="queryForm.state" placeholder="状态" style="width: 100px">
+            <el-option label="全部" :value="-1" />
+            <el-option label="启用" :value="1" />
+            <el-option label="关闭" :value="0" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" :icon="Search" @click="fetchData({ page: 1 })" :loading="isLoading">查询</el-button>
+          <el-button type="primary" :icon="Refresh" @click="resetButton" plain>重置</el-button>
+          <el-button type="success" :icon="Plus" @click="() => openNewuserDialog()">新增帐号</el-button>
+        </el-form-item>
+      </el-form>
+    </section>
+    <section class="custom-table">
+      <m-table
+        :options="options"
+        :data="tableData"
+        :isLoading="isLoading"
+        pagination
+        stripe
+        border
+        :total="total"
+        :currentPage="queryForm.page"
+        :pageSize="queryForm.size"
+        @size-change="handleSizeChange"
+        @current-change="handleCurrentChange"
+      >
+        <template #username="{ scope }">
+          <div>
+            {{ scope.row.username }}
+            <copy :content="scope.row.username" :title="t('table.accountNumber')" />
+          </div>
+        </template>
+        <template #roleIds="{ scope }">
+          <el-tooltip :content="scope.row.roleIds.join(' | ')" placement="top" v-if="scope.row.roleIds">
+            <div class="account-role">
+              {{ scope.row.roleIds.join(' | ') }}
+            </div>
+          </el-tooltip>
+        </template>
+
+        <template #phone="{ scope }">
+          <span v-if="scope.row.cc">{{ scope.row.cc }}&nbsp;&nbsp;&nbsp;</span>
+          <span>{{ scope.row.phone }}</span>
+        </template>
+        <template #state="{ scope }">
+          <div :class="{ active: scope.row.state === 1, inactive: scope.row.state === 0 }">
+            {{ scope.row.state && scope.row.state === 1 ? '启用' : '禁用' }}
+          </div>
+        </template>
+
+        <template #id="{ scope }">
+          <el-button
+            :disabled="per.includes('account:edit') ? false : true"
+            type="primary"
+            @click="openNewuserDialog(scope.row)"
+          >
+            {{ t('button.edit') }}
+          </el-button>
+          <el-popconfirm
+            :confirm-button-text="t('button.confirm')"
+            :cancel-button-text="t('button.cancel')"
+            icon-color="#f5c357"
+            :title="
+              t('button.whether') +
+              (scope.row.state == 1 ? t('button.disable') : t('button.enable')) +
+              t('button.selectedSsers')
+            "
+            @confirm="confirmEvent(scope.row)"
+            width="250"
+          >
+            <template #reference>
+              <el-button
+                :disabled="per.includes('account:forbidden') ? false : true"
+                :type="scope.row.state == 1 ? 'danger' : 'success'"
+              >
+                {{ scope.row.state == 1 ? t('button.disable') : t('button.enable') }}
+              </el-button>
+            </template>
+          </el-popconfirm>
+        </template>
+      </m-table>
+    </section>
+
+    <AddUserDialog ref="AddUserDialogRef" @onClose="handleClose" />
+  </section>
+</template>
+
+<script lang="ts" setup>
+// import { ref } from 'vue'
+// import i18n from '@/locales'
+// const { t } = i18n.global
+// import showErrorMessage from '@/utils/showErrorMessage'
+// import { Search, Refresh, Plus } from '@element-plus/icons-vue'
+// import { reqForbidden, reqSystemUserAccountList, reqSystemUserEdit } from '@/api/system/account'
+// import { options } from '@/views/account/data'
+// // import type {
+// //   IReqSystemUserAccountListData,
+// //   IReqSystemUserAccountListQueryParams,
+// //   IReqForbiddenQueryParams
+// // } from '@/api/system/account/types'
+// // import { usePermissionsStore } from '@/stores'
+
+// // const permissionStore = usePermissionsStore()
+// // const per = computed(() => permissionStore.menupermissions)
+// const tableRef = ref()
+// const queryRef = ref()
+// const AddUserDialogRef = ref()
+// const tableData = ref<any[]>([])
+
+// const total = ref<number>(0)
+
+// const initFormQuery = (): any => {
+//   return {
+//     page: 1,
+//     size: 15,
+//     sortName: '',
+//     sortOrder: 'DESC',
+//     username: '',
+//     state: -1 //全部
+//   }
+// }
+
+// const queryForm = reactive<any>(initFormQuery())
+
+// //loading 按钮
+// const isLoading = ref(false)
+
+// const openNewuserDialog = (row?: any) => {
+//   AddUserDialogRef.value.openDailog(row)
+// }
+
+// const confirmEvent = async (row: any) => {
+//   try {
+//     const query: any = {
+//       id: row.id.toString(),
+//       state: row.state === 1 ? 0 : 1
+//     }
+//     const res = await reqForbidden(query)
+//     if (res.success) {
+//       ElMessage.success('修改成功')
+//       fetchData()
+//     }
+//   } catch (error) {}
+// }
+
+// const handleSizeChange = (val: number) => {
+//   queryForm.size = val
+//   queryForm.page = 1
+//   fetchData()
+// }
+
+// //分页页数改变
+// const handleCurrentChange = (val: number) => {
+//   queryForm.page = val
+//   fetchData()
+// }
+
+// //页面初始化获取数据
+// const fetchData = async (_queryForm?: Partial<IReqSystemUserAccountListQueryParams>) => {
+//   isLoading.value = true
+//   try {
+//     const query: IReqSystemUserAccountListQueryParams = {
+//       ...queryForm,
+//       ..._queryForm
+//     }
+
+//     if (query.state === -1) {
+//       delete query.state
+//     }
+
+//     Object.assign(queryForm, _queryForm)
+
+//     const response = await reqSystemUserAccountList(query)
+//     if (response.success) {
+//       tableData.value = response.data.list
+//       total.value = Number(response.data.total)
+//     } else {
+//       tableData.value = []
+//       total.value = 0
+//       showErrorMessage(response)
+//     }
+//   } catch (error) {
+//     console.error('Error fetching data:', error)
+//   } finally {
+//     isLoading.value = false
+//   }
+// }
+
+// // // 更新状态
+// // const onchange = async (row: IReqSystemUserAccountListData, val) => {
+// //   console.log('🚀 ~ beforeChange ~ val:', row, val)
+// //   try {
+// //     if (row.id) {
+// //       isLoading.value = true
+// //       const query: IReqForbiddenQueryParams = {
+// //         id: row.id,
+// //         word: row,
+// //         status: val
+// //       }
+// //       const response = await reqSystemUserEdit(query)
+// //       if (response.success) {
+// //         fetchData()
+// //         ElMessage.success('操作成功')
+// //       } else {
+// //         showErrorMessage(response)
+// //       }
+// //     }
+// //   } catch (error) {
+// //     throw new Error('更新错误')
+// //   } finally {
+// //     isLoading.value = false
+// //   }
+// // }
+
+// const handleClose = () => {
+//   Object.assign(queryForm, initFormQuery())
+//   fetchData()
+// }
+
+// const resetButton = () => {
+//   queryRef.value.resetFields()
+//   Object.assign(queryForm, initFormQuery())
+//   fetchData()
+// }
+
+// onMounted(() => {
+//   fetchData()
+// })
+</script>
+
+<style lang="less" scoped>
+.custom-table {
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  flex: 1;
+}
+.wrapper {
+  height: calc(100vh - 101px);
+  display: flex;
+  flex-direction: column;
+}
+
+.switch-custom .el-switch__label.is-active {
+  color: #303133;
+}
+
+.active {
+  color: rgb(103, 194, 58);
+  font-size: 16px;
+  font-weight: bold;
+}
+
+.inactive {
+  color: rgb(245, 108, 108);
+  font-size: 16px;
+  font-weight: bold;
+}
+</style>

+ 4 - 4
src/views/audit/index.vue

@@ -256,11 +256,11 @@ async function getList() {
     }
     }
 
 
     const res = await getAuditLogs(params)
     const res = await getAuditLogs(params)
-    if (res.code === 200) {
-      auditList.value = res.data?.rows || []
-      total.value = res.data?.total || 0
+    if (res.success) {
+      auditList.value = res.data.list || []
+      total.value = Number(res.data.total) || 0
     } else {
     } else {
-      ElMessage.error(res.message || '获取审计日志失败')
+      ElMessage.error(res.errMessage || '获取审计日志失败')
     }
     }
   } catch (error) {
   } catch (error) {
     console.error('Failed to load audit logs:', error)
     console.error('Failed to load audit logs:', error)

+ 1 - 1
src/views/camera/channel.vue

@@ -73,7 +73,7 @@ async function getList() {
   loading.value = true
   loading.value = true
   try {
   try {
     const res = await getCamera(cameraId)
     const res = await getCamera(cameraId)
-    if (res.code === 200) {
+    if (res.success) {
       cameraInfo.value = res.data
       cameraInfo.value = res.data
     }
     }
   } finally {
   } finally {

+ 8 - 8
src/views/camera/index.vue

@@ -255,8 +255,8 @@ const rules: FormRules = {
 async function getMachines() {
 async function getMachines() {
   try {
   try {
     const res = await listMachines()
     const res = await listMachines()
-    if (res.code === 200) {
-      machineList.value = res.data
+    if (res.success) {
+      machineList.value = res.data.list
     }
     }
   } catch (error) {
   } catch (error) {
     console.error('获取机器列表失败', error)
     console.error('获取机器列表失败', error)
@@ -267,8 +267,8 @@ async function getList() {
   loading.value = true
   loading.value = true
   try {
   try {
     const res = await adminListCameras(queryParams.machineId || undefined)
     const res = await adminListCameras(queryParams.machineId || undefined)
-    if (res.code === 200) {
-      cameraList.value = res.data
+    if (res.success) {
+      cameraList.value = res.data.list
     }
     }
   } finally {
   } finally {
     loading.value = false
     loading.value = false
@@ -327,7 +327,7 @@ function handleChannel(row: CameraInfoDTO) {
 async function handleCheck(row: CameraInfoDTO) {
 async function handleCheck(row: CameraInfoDTO) {
   try {
   try {
     const res = await adminCheckCamera(row.id)
     const res = await adminCheckCamera(row.id)
-    if (res.code === 200) {
+    if (res.success) {
       if (res.data) {
       if (res.data) {
         ElMessage.success('摄像头连接正常')
         ElMessage.success('摄像头连接正常')
       } else {
       } else {
@@ -345,7 +345,7 @@ async function handleDelete(row: CameraInfoDTO) {
       type: 'warning'
       type: 'warning'
     })
     })
     const res = await adminDeleteCamera(row.id)
     const res = await adminDeleteCamera(row.id)
-    if (res.code === 200) {
+    if (res.success) {
       ElMessage.success('删除成功')
       ElMessage.success('删除成功')
       getList()
       getList()
     }
     }
@@ -377,7 +377,7 @@ async function handleSubmit() {
             enabled: form.enabled
             enabled: form.enabled
           }
           }
           const res = await adminUpdateCamera(updateData)
           const res = await adminUpdateCamera(updateData)
-          if (res.code === 200) {
+          if (res.success) {
             ElMessage.success('修改成功')
             ElMessage.success('修改成功')
             dialogVisible.value = false
             dialogVisible.value = false
             getList()
             getList()
@@ -395,7 +395,7 @@ async function handleSubmit() {
             machineId: form.machineId || undefined
             machineId: form.machineId || undefined
           }
           }
           const res = await adminAddCamera(addData)
           const res = await adminAddCamera(addData)
-          if (res.code === 200) {
+          if (res.success) {
             ElMessage.success('新增成功')
             ElMessage.success('新增成功')
             dialogVisible.value = false
             dialogVisible.value = false
             getList()
             getList()

+ 2 - 2
src/views/dashboard/index.vue

@@ -232,11 +232,11 @@ async function loadDashboardData() {
   loading.value = true
   loading.value = true
   try {
   try {
     const res = await getDashboardStats()
     const res = await getDashboardStats()
-    if (res.code === 200) {
+    if (res.success) {
       dashboardData.value = res.data
       dashboardData.value = res.data
       lastUpdate.value = new Date()
       lastUpdate.value = new Date()
     } else {
     } else {
-      ElMessage.error(res.message || t('获取统计数据失败'))
+      ElMessage.error(res.errMessage || t('获取统计数据失败'))
     }
     }
   } catch (error) {
   } catch (error) {
     console.error('Failed to load dashboard data:', error)
     console.error('Failed to load dashboard data:', error)

+ 2 - 2
src/views/login/index.vue

@@ -223,13 +223,13 @@ async function handleLogin() {
   loading.value = true
   loading.value = true
   try {
   try {
     const res = await userStore.loginAction(loginForm)
     const res = await userStore.loginAction(loginForm)
-    if (res.code === 200) {
+    if (res.success) {
       saveLoginInfo()
       saveLoginInfo()
       ElMessage.success(t('登录成功'))
       ElMessage.success(t('登录成功'))
       const redirect = (route.query.redirect as string) || '/'
       const redirect = (route.query.redirect as string) || '/'
       router.push(redirect)
       router.push(redirect)
     } else {
     } else {
-      ElMessage.error(res.message || t('登录失败'))
+      ElMessage.error(res.errMessage || t('登录失败'))
     }
     }
   } catch (error: any) {
   } catch (error: any) {
     ElMessage.error(error.message || t('登录失败,请检查网络'))
     ElMessage.error(error.message || t('登录失败,请检查网络'))

+ 5 - 5
src/views/machine/index.vue

@@ -319,8 +319,8 @@ async function getList() {
   loading.value = true
   loading.value = true
   try {
   try {
     const res = await listMachines()
     const res = await listMachines()
-    if (res.code === 200) {
-      machineList.value = res.data
+    if (res.success) {
+      machineList.value = res.data.list
     }
     }
   } finally {
   } finally {
     loading.value = false
     loading.value = false
@@ -416,7 +416,7 @@ async function handleDelete(row: MachineDTO) {
     })
     })
     deleteLoading.value = true
     deleteLoading.value = true
     const res = await deleteMachine(row.id)
     const res = await deleteMachine(row.id)
-    if (res.code === 200) {
+    if (res.success) {
       ElMessage.success(t('删除成功'))
       ElMessage.success(t('删除成功'))
       getList()
       getList()
     }
     }
@@ -445,7 +445,7 @@ async function handleSubmit() {
             enabled: form.enabled
             enabled: form.enabled
           }
           }
           const res = await updateMachine(updateData)
           const res = await updateMachine(updateData)
-          if (res.code === 200) {
+          if (res.success) {
             ElMessage.success(t('修改成功'))
             ElMessage.success(t('修改成功'))
             dialogVisible.value = false
             dialogVisible.value = false
             getList()
             getList()
@@ -458,7 +458,7 @@ async function handleSubmit() {
             description: form.description || undefined
             description: form.description || undefined
           }
           }
           const res = await addMachine(addData)
           const res = await addMachine(addData)
-          if (res.code === 200) {
+          if (res.success) {
             ElMessage.success(t('新增成功'))
             ElMessage.success(t('新增成功'))
             dialogVisible.value = false
             dialogVisible.value = false
             getList()
             getList()

+ 2 - 2
src/views/stats/index.vue

@@ -170,11 +170,11 @@ async function loadStats() {
   loading.value = true
   loading.value = true
   try {
   try {
     const res = await getDashboardStats()
     const res = await getDashboardStats()
-    if (res.code === 200) {
+    if (res.success) {
       statsData.value = res.data
       statsData.value = res.data
       lastUpdate.value = new Date()
       lastUpdate.value = new Date()
     } else {
     } else {
-      ElMessage.error(res.message || '获取统计数据失败')
+      ElMessage.error(res.errMessage || '获取统计数据失败')
     }
     }
   } catch (error) {
   } catch (error) {
     console.error('Failed to load stats:', error)
     console.error('Failed to load stats:', error)

+ 425 - 0
src/views/test/m-table-demo.vue

@@ -0,0 +1,425 @@
+<template>
+  <div class="page-container">
+    <!-- 搜索表单 -->
+    <div class="search-form">
+      <el-form :model="searchForm" inline>
+        <el-form-item label="用户名">
+          <el-input v-model="searchForm.username" placeholder="请输入用户名" clearable @keyup.enter="handleSearch" />
+        </el-form-item>
+        <el-form-item label="邮箱">
+          <el-input v-model="searchForm.email" placeholder="请输入邮箱" clearable @keyup.enter="handleSearch" />
+        </el-form-item>
+        <el-form-item label="状态">
+          <el-select v-model="searchForm.status" placeholder="全部" clearable>
+            <el-option label="全部" value="" />
+            <el-option label="正常" value="1" />
+            <el-option label="禁用" value="0" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="角色">
+          <el-select v-model="searchForm.role" placeholder="全部" clearable>
+            <el-option label="全部" value="" />
+            <el-option label="管理员" value="admin" />
+            <el-option label="用户" value="user" />
+            <el-option label="访客" value="guest" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="创建时间">
+          <el-date-picker
+            v-model="searchForm.dateRange"
+            type="daterange"
+            range-separator="至"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+            value-format="YYYY-MM-DD"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button>
+          <el-button :icon="RefreshRight" @click="handleReset">重置</el-button>
+          <el-button type="primary" :icon="Plus" @click="handleAdd">新增</el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+
+    <!-- 数据表格 -->
+    <div class="table-wrapper">
+      <m-table
+        :options="tableOptions"
+        :data="tableData"
+        :isLoading="isLoading"
+        pagination
+        stripe
+        border
+        :total="total"
+        :currentPage="queryForm.page"
+        :pageSize="queryForm.size"
+        @size-change="handleSizeChange"
+        @current-change="handleCurrentChange"
+      >
+        <!-- 自定义用户名列 -->
+        <template #username="{ scope }">
+          <el-link type="primary" @click="handleEdit(scope.row)">
+            {{ scope.row.username }}
+          </el-link>
+        </template>
+
+        <!-- 自定义状态列 -->
+        <template #status="{ scope }">
+          <el-tag :type="scope.row.status === '1' ? 'success' : 'danger'">
+            {{ scope.row.status === '1' ? '正常' : '禁用' }}
+          </el-tag>
+        </template>
+
+        <!-- 自定义角色列 -->
+        <template #role="{ scope }">
+          <el-tag :type="getRoleTagType(scope.row.role)">
+            {{ getRoleLabel(scope.row.role) }}
+          </el-tag>
+        </template>
+
+        <!-- 操作列 -->
+        <template #action="{ scope }">
+          <el-button type="primary" link :icon="Edit" @click="handleEdit(scope.row)">编辑</el-button>
+          <el-button type="danger" link :icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
+        </template>
+      </m-table>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Plus, Edit, Delete, Search, RefreshRight } from '@element-plus/icons-vue'
+import MTable from '@/components/mTable/index.vue'
+import type { TableOptions } from '@/components/mTable/types'
+
+// 搜索表单
+const searchForm = reactive({
+  username: '',
+  email: '',
+  status: '',
+  role: '',
+  dateRange: null as [string, string] | null
+})
+
+// 分页参数
+const queryForm = reactive({
+  page: 1,
+  size: 15
+})
+
+const isLoading = ref(false)
+const total = ref(0)
+const tableData = ref<any[]>([])
+
+// 表格配置
+const tableOptions: TableOptions[] = [
+  {
+    prop: 'id',
+    label: 'ID',
+    width: 80,
+    align: 'center'
+  },
+  {
+    prop: 'username',
+    label: '用户名',
+    minWidth: 120,
+    slot: 'username'
+  },
+  {
+    prop: 'email',
+    label: '邮箱',
+    minWidth: 180,
+    showOmission: true
+  },
+  {
+    prop: 'phone',
+    label: '手机号',
+    width: 130
+  },
+  {
+    prop: 'role',
+    label: '角色',
+    width: 100,
+    align: 'center',
+    slot: 'role'
+  },
+  {
+    prop: 'status',
+    label: '状态',
+    width: 80,
+    align: 'center',
+    slot: 'status'
+  },
+  {
+    prop: 'createdAt',
+    label: '创建时间',
+    width: 180,
+    align: 'center',
+    dateName: 'createdAt',
+    formatStr: 'yyyy-MM-dd HH:mm:ss'
+  },
+  {
+    prop: 'remark',
+    label: '备注',
+    minWidth: 150,
+    truncateText: true,
+    truncateTextLength: 20
+  },
+  {
+    label: '操作',
+    width: 150,
+    align: 'center',
+    fixed: 'right',
+    action: true
+  }
+]
+
+// 生成 Mock 数据
+function generateMockData() {
+  const roles = ['admin', 'user', 'guest']
+  const statuses = ['0', '1']
+  const remarks = [
+    '这是一段很长的备注信息用于测试文本截断功能',
+    '普通备注',
+    '系统管理员账号',
+    '测试用户',
+    '',
+    '需要审核的用户账号',
+    'VIP用户'
+  ]
+
+  const data: any[] = []
+  for (let i = 1; i <= 58; i++) {
+    data.push({
+      id: i,
+      username: `user_${String(i).padStart(3, '0')}`,
+      email: `user${i}@example.com`,
+      phone: `138${String(Math.floor(Math.random() * 100000000)).padStart(8, '0')}`,
+      role: roles[Math.floor(Math.random() * roles.length)],
+      status: statuses[Math.floor(Math.random() * statuses.length)],
+      createdAt: Date.now() - Math.floor(Math.random() * 30 * 24 * 60 * 60 * 1000),
+      remark: remarks[Math.floor(Math.random() * remarks.length)]
+    })
+  }
+  return data
+}
+
+// 全部数据
+const allData = ref<any[]>([])
+
+// 加载数据
+async function loadData() {
+  isLoading.value = true
+
+  // 模拟接口延迟
+  await new Promise((resolve) => setTimeout(resolve, 500))
+
+  // 过滤数据
+  let filtered = [...allData.value]
+
+  if (searchForm.username) {
+    filtered = filtered.filter((item) => item.username.toLowerCase().includes(searchForm.username.toLowerCase()))
+  }
+  if (searchForm.email) {
+    filtered = filtered.filter((item) => item.email.toLowerCase().includes(searchForm.email.toLowerCase()))
+  }
+  if (searchForm.status) {
+    filtered = filtered.filter((item) => item.status === searchForm.status)
+  }
+  if (searchForm.role) {
+    filtered = filtered.filter((item) => item.role === searchForm.role)
+  }
+
+  total.value = filtered.length
+
+  // 分页
+  const start = (queryForm.page - 1) * queryForm.size
+  const end = start + queryForm.size
+  tableData.value = filtered.slice(start, end)
+
+  isLoading.value = false
+}
+
+// 角色标签类型
+function getRoleTagType(role: string) {
+  const map: Record<string, string> = {
+    admin: 'danger',
+    user: 'primary',
+    guest: 'info'
+  }
+  return map[role] || 'info'
+}
+
+// 角色标签文本
+function getRoleLabel(role: string) {
+  const map: Record<string, string> = {
+    admin: '管理员',
+    user: '用户',
+    guest: '访客'
+  }
+  return map[role] || role
+}
+
+// 查询
+function handleSearch() {
+  queryForm.page = 1
+  loadData()
+}
+
+// 重置
+function handleReset() {
+  searchForm.username = ''
+  searchForm.email = ''
+  searchForm.status = ''
+  searchForm.role = ''
+  searchForm.dateRange = null
+  queryForm.page = 1
+  loadData()
+}
+
+// 新增
+function handleAdd() {
+  ElMessage.info('点击了新增按钮')
+}
+
+// 编辑
+function handleEdit(row: any) {
+  ElMessage.info(`编辑用户: ${row.username}`)
+}
+
+// 删除
+async function handleDelete(row: any) {
+  try {
+    await ElMessageBox.confirm(`确定要删除用户 "${row.username}" 吗?`, '提示', {
+      type: 'warning'
+    })
+    ElMessage.success(`删除用户 ${row.username} 成功`)
+    loadData()
+  } catch {
+    // 取消删除
+  }
+}
+
+// 分页大小改变
+function handleSizeChange(val: number) {
+  queryForm.size = val
+  queryForm.page = 1
+  loadData()
+}
+
+// 页码改变
+function handleCurrentChange(val: number) {
+  queryForm.page = val
+  loadData()
+}
+
+onMounted(() => {
+  allData.value = generateMockData()
+  loadData()
+})
+</script>
+
+<style lang="scss" scoped>
+.page-container {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  padding: 1rem;
+  box-sizing: border-box;
+  overflow: hidden;
+}
+
+.search-form {
+  flex-shrink: 0;
+  margin-bottom: 16px;
+  padding: 16px;
+  background: #f5f7fa;
+
+  :deep(.el-form-item) {
+    margin-bottom: 0;
+    margin-right: 16px;
+  }
+
+  :deep(.el-input),
+  :deep(.el-select) {
+    width: 160px;
+  }
+
+  :deep(.el-date-editor--daterange) {
+    width: 280px;
+
+    .el-range-input {
+      width: 90px;
+    }
+
+    .el-range-separator {
+      width: 30px;
+    }
+  }
+
+  :deep(.el-button--primary) {
+    background-color: #4f46e5;
+    border-color: #4f46e5;
+
+    &:hover,
+    &:focus {
+      background-color: #6366f1;
+      border-color: #6366f1;
+    }
+  }
+}
+
+.table-wrapper {
+  flex: 1;
+  min-height: 0;
+  overflow: hidden;
+
+  :deep(.el-table) {
+    --el-table-row-hover-bg-color: #f0f0ff;
+
+    .el-table__row--striped td.el-table__cell {
+      background-color: #f8f9fc;
+    }
+
+    .el-table__header th {
+      background-color: #f5f7fa;
+      color: #333;
+      font-weight: 600;
+    }
+
+    .el-link--primary {
+      color: #4f46e5;
+
+      &:hover {
+        color: #6366f1;
+      }
+    }
+
+    .el-button--primary.is-link {
+      color: #4f46e5;
+
+      &:hover {
+        color: #6366f1;
+      }
+    }
+  }
+}
+
+:deep(.pagination) {
+  padding-top: 16px;
+
+  .el-pagination {
+    .el-pager li.is-active {
+      background-color: #4f46e5;
+      color: #fff;
+    }
+
+    .el-pager li:not(.is-active):hover {
+      color: #4f46e5;
+    }
+  }
+}
+</style>

+ 6 - 6
src/views/user/index.vue

@@ -217,9 +217,9 @@ async function getList() {
     if (queryParams.role) params.role = queryParams.role
     if (queryParams.role) params.role = queryParams.role
 
 
     const res = await listUsers(params)
     const res = await listUsers(params)
-    if (res.code === 200) {
-      userList.value = res.data?.rows
-      total.value = res.data?.total
+    if (res.success) {
+      userList.value = res.data.list
+      total.value = Number(res.data.total)
     }
     }
   } finally {
   } finally {
     loading.value = false
     loading.value = false
@@ -271,7 +271,7 @@ async function handleDelete(row: User) {
       type: 'warning'
       type: 'warning'
     })
     })
     const res = await deleteUser(row.id)
     const res = await deleteUser(row.id)
-    if (res.code === 200) {
+    if (res.success) {
       ElMessage.success('删除成功')
       ElMessage.success('删除成功')
       getList()
       getList()
     }
     }
@@ -296,7 +296,7 @@ async function handleSubmit() {
         } else {
         } else {
           res = await createUser(form)
           res = await createUser(form)
         }
         }
-        if (res.code === 200) {
+        if (res.success) {
           ElMessage.success(form.id ? '修改成功' : '新增成功')
           ElMessage.success(form.id ? '修改成功' : '新增成功')
           dialogVisible.value = false
           dialogVisible.value = false
           getList()
           getList()
@@ -316,7 +316,7 @@ async function handleResetSubmit() {
       resetLoading.value = true
       resetLoading.value = true
       try {
       try {
         const res = await resetPassword(currentUserId.value, resetForm.password)
         const res = await resetPassword(currentUserId.value, resetForm.password)
-        if (res.code === 200) {
+        if (res.success) {
           ElMessage.success('密码重置成功')
           ElMessage.success('密码重置成功')
           resetDialogVisible.value = false
           resetDialogVisible.value = false
         }
         }

+ 34 - 23
tests/fixtures/index.ts

@@ -3,14 +3,7 @@
  * 提供各模块的模拟数据
  * 提供各模块的模拟数据
  */
  */
 
 
-import type {
-  AdminInfo,
-  LoginResponse,
-  MachineDTO,
-  CameraInfoDTO,
-  ChannelInfoDTO,
-  DashboardStatsDTO
-} from '@/types'
+import type { AdminInfo, LoginResponse, MachineDTO, CameraInfoDTO, ChannelInfoDTO, DashboardStatsDTO } from '@/types'
 
 
 // ==================== 用户/认证数据 ====================
 // ==================== 用户/认证数据 ====================
 
 
@@ -184,28 +177,46 @@ export const mockDashboardStats: DashboardStatsDTO = {
 
 
 // ==================== API 响应包装 ====================
 // ==================== API 响应包装 ====================
 
 
-export function wrapResponse<T>(data: T, code = 200, message = 'success') {
+/**
+ * 包装单个数据响应 (IBaseResponse<T>)
+ */
+export function wrapResponse<T>(data: T, success = true, errMessage?: string) {
   return {
   return {
-    code,
-    message,
+    success,
     data,
     data,
-    timestamp: Date.now(),
-    traceId: `trace-${Date.now()}`
+    errCode: success ? undefined : 'ERROR',
+    errMessage: success ? undefined : errMessage
   }
   }
 }
 }
 
 
-export function wrapPageResponse<T>(rows: T[], total?: number) {
-  return wrapResponse({
-    rows,
-    total: total ?? rows.length
-  })
+/**
+ * 包装分页列表响应 (IResponse<T>)
+ */
+export function wrapListResponse<T>(list: T[], total?: number) {
+  return {
+    success: true,
+    data: {
+      list,
+      total: String(total ?? list.length)
+    }
+  }
 }
 }
 
 
-export function wrapErrorResponse(message: string, code = 400) {
+/**
+ * 包装错误响应
+ */
+export function wrapErrorResponse(errMessage: string, errCode = 'ERROR') {
   return {
   return {
-    code,
-    message,
-    data: null,
-    timestamp: Date.now()
+    success: false,
+    errCode,
+    errMessage,
+    data: undefined
   }
   }
 }
 }
+
+/**
+ * @deprecated 使用 wrapListResponse 代替
+ */
+export function wrapPageResponse<T>(rows: T[], total?: number) {
+  return wrapListResponse(rows, total)
+}

+ 25 - 25
tests/mocks/api.ts

@@ -36,18 +36,18 @@ export function mockLoginApi(request: MockedRequest) {
       if (data?.username === 'admin' && data?.password === '123456') {
       if (data?.username === 'admin' && data?.password === '123456') {
         return Promise.resolve(fixtures.wrapResponse(fixtures.mockLoginResponse))
         return Promise.resolve(fixtures.wrapResponse(fixtures.mockLoginResponse))
       }
       }
-      return Promise.resolve(fixtures.wrapErrorResponse('用户名或密码错误', 401))
+      return Promise.resolve(fixtures.wrapErrorResponse('用户名或密码错误'))
     }
     }
     if (url === '/admin/auth/logout') {
     if (url === '/admin/auth/logout') {
       return Promise.resolve(fixtures.wrapResponse(null))
       return Promise.resolve(fixtures.wrapResponse(null))
     }
     }
     if (url === '/admin/auth/password') {
     if (url === '/admin/auth/password') {
       if (data?.oldPassword === '123456') {
       if (data?.oldPassword === '123456') {
-        return Promise.resolve(fixtures.wrapResponse(null, 200, '密码修改成功'))
+        return Promise.resolve(fixtures.wrapResponse(null))
       }
       }
-      return Promise.resolve(fixtures.wrapErrorResponse('原密码错误', 400))
+      return Promise.resolve(fixtures.wrapErrorResponse('原密码错误'))
     }
     }
-    return Promise.resolve(fixtures.wrapErrorResponse('Not Found', 404))
+    return Promise.resolve(fixtures.wrapErrorResponse('Not Found'))
   })
   })
 
 
   // getInfo
   // getInfo
@@ -55,7 +55,7 @@ export function mockLoginApi(request: MockedRequest) {
     if (url === '/admin/auth/info') {
     if (url === '/admin/auth/info') {
       return Promise.resolve(fixtures.wrapResponse(fixtures.mockAdminInfo))
       return Promise.resolve(fixtures.wrapResponse(fixtures.mockAdminInfo))
     }
     }
-    return Promise.resolve(fixtures.wrapErrorResponse('Not Found', 404))
+    return Promise.resolve(fixtures.wrapErrorResponse('Not Found'))
   })
   })
 }
 }
 
 
@@ -65,16 +65,16 @@ export function mockLoginApi(request: MockedRequest) {
 export function mockMachineApi(request: MockedRequest) {
 export function mockMachineApi(request: MockedRequest) {
   request.get.mockImplementation((url: string, params?: any) => {
   request.get.mockImplementation((url: string, params?: any) => {
     if (url === '/admin/machines/list') {
     if (url === '/admin/machines/list') {
-      return Promise.resolve(fixtures.wrapResponse(fixtures.mockMachines))
+      return Promise.resolve(fixtures.wrapListResponse(fixtures.mockMachines))
     }
     }
     if (url === '/admin/machines/detail') {
     if (url === '/admin/machines/detail') {
       const machine = fixtures.mockMachines.find((m) => m.id === params?.id)
       const machine = fixtures.mockMachines.find((m) => m.id === params?.id)
       if (machine) {
       if (machine) {
         return Promise.resolve(fixtures.wrapResponse(machine))
         return Promise.resolve(fixtures.wrapResponse(machine))
       }
       }
-      return Promise.resolve(fixtures.wrapErrorResponse('机器不存在', 404))
+      return Promise.resolve(fixtures.wrapErrorResponse('机器不存在'))
     }
     }
-    return Promise.resolve(fixtures.wrapErrorResponse('Not Found', 404))
+    return Promise.resolve(fixtures.wrapErrorResponse('Not Found'))
   })
   })
 
 
   request.post.mockImplementation((url: string, data?: any, _config?: any) => {
   request.post.mockImplementation((url: string, data?: any, _config?: any) => {
@@ -97,20 +97,20 @@ export function mockMachineApi(request: MockedRequest) {
       if (machine) {
       if (machine) {
         return Promise.resolve(fixtures.wrapResponse({ ...machine, ...data }))
         return Promise.resolve(fixtures.wrapResponse({ ...machine, ...data }))
       }
       }
-      return Promise.resolve(fixtures.wrapErrorResponse('机器不存在', 404))
+      return Promise.resolve(fixtures.wrapErrorResponse('机器不存在'))
     }
     }
     if (url === '/admin/machines/delete') {
     if (url === '/admin/machines/delete') {
       const id = _config?.params?.id
       const id = _config?.params?.id
       const machine = fixtures.mockMachines.find((m) => m.id === id)
       const machine = fixtures.mockMachines.find((m) => m.id === id)
       if (machine) {
       if (machine) {
         if (machine.cameraCount > 0) {
         if (machine.cameraCount > 0) {
-          return Promise.resolve(fixtures.wrapErrorResponse('该机器下存在摄像头,无法删除', 400))
+          return Promise.resolve(fixtures.wrapErrorResponse('该机器下存在摄像头,无法删除'))
         }
         }
         return Promise.resolve(fixtures.wrapResponse(null))
         return Promise.resolve(fixtures.wrapResponse(null))
       }
       }
-      return Promise.resolve(fixtures.wrapErrorResponse('机器不存在', 404))
+      return Promise.resolve(fixtures.wrapErrorResponse('机器不存在'))
     }
     }
-    return Promise.resolve(fixtures.wrapErrorResponse('Not Found', 404))
+    return Promise.resolve(fixtures.wrapErrorResponse('Not Found'))
   })
   })
 }
 }
 
 
@@ -124,17 +124,17 @@ export function mockCameraApi(request: MockedRequest) {
       if (params?.machineId) {
       if (params?.machineId) {
         cameras = cameras.filter((c) => c.machineId === params.machineId)
         cameras = cameras.filter((c) => c.machineId === params.machineId)
       }
       }
-      return Promise.resolve(fixtures.wrapResponse(cameras))
+      return Promise.resolve(fixtures.wrapListResponse(cameras))
     }
     }
     if (url === '/admin/cameras/detail') {
     if (url === '/admin/cameras/detail') {
       const camera = fixtures.mockCameras.find((c) => c.id === params?.id)
       const camera = fixtures.mockCameras.find((c) => c.id === params?.id)
       if (camera) {
       if (camera) {
         return Promise.resolve(fixtures.wrapResponse(camera))
         return Promise.resolve(fixtures.wrapResponse(camera))
       }
       }
-      return Promise.resolve(fixtures.wrapErrorResponse('摄像头不存在', 404))
+      return Promise.resolve(fixtures.wrapErrorResponse('摄像头不存在'))
     }
     }
     if (url === '/camera/list') {
     if (url === '/camera/list') {
-      return Promise.resolve(fixtures.wrapResponse(fixtures.mockCameras))
+      return Promise.resolve(fixtures.wrapListResponse(fixtures.mockCameras))
     }
     }
     if (url.startsWith('/camera/') && !url.includes('/ptz')) {
     if (url.startsWith('/camera/') && !url.includes('/ptz')) {
       const cameraId = url.split('/')[2]
       const cameraId = url.split('/')[2]
@@ -146,7 +146,7 @@ export function mockCameraApi(request: MockedRequest) {
     if (url === '/camera/current') {
     if (url === '/camera/current') {
       return Promise.resolve(fixtures.wrapResponse(fixtures.mockChannels[0]))
       return Promise.resolve(fixtures.wrapResponse(fixtures.mockChannels[0]))
     }
     }
-    return Promise.resolve(fixtures.wrapErrorResponse('Not Found', 404))
+    return Promise.resolve(fixtures.wrapErrorResponse('Not Found'))
   })
   })
 
 
   request.post.mockImplementation((url: string, data?: any, config?: any) => {
   request.post.mockImplementation((url: string, data?: any, config?: any) => {
@@ -166,7 +166,7 @@ export function mockCameraApi(request: MockedRequest) {
       if (camera) {
       if (camera) {
         return Promise.resolve(fixtures.wrapResponse({ ...camera, ...data }))
         return Promise.resolve(fixtures.wrapResponse({ ...camera, ...data }))
       }
       }
-      return Promise.resolve(fixtures.wrapErrorResponse('摄像头不存在', 404))
+      return Promise.resolve(fixtures.wrapErrorResponse('摄像头不存在'))
     }
     }
     if (url === '/admin/cameras/delete') {
     if (url === '/admin/cameras/delete') {
       return Promise.resolve(fixtures.wrapResponse(null))
       return Promise.resolve(fixtures.wrapResponse(null))
@@ -179,7 +179,7 @@ export function mockCameraApi(request: MockedRequest) {
       if (channel) {
       if (channel) {
         return Promise.resolve(fixtures.wrapResponse(channel))
         return Promise.resolve(fixtures.wrapResponse(channel))
       }
       }
-      return Promise.resolve(fixtures.wrapErrorResponse('通道不存在', 404))
+      return Promise.resolve(fixtures.wrapErrorResponse('通道不存在'))
     }
     }
     if (url.includes('/ptz/start')) {
     if (url.includes('/ptz/start')) {
       return Promise.resolve(fixtures.wrapResponse(null))
       return Promise.resolve(fixtures.wrapResponse(null))
@@ -187,7 +187,7 @@ export function mockCameraApi(request: MockedRequest) {
     if (url.includes('/ptz/stop')) {
     if (url.includes('/ptz/stop')) {
       return Promise.resolve(fixtures.wrapResponse(null))
       return Promise.resolve(fixtures.wrapResponse(null))
     }
     }
-    return Promise.resolve(fixtures.wrapErrorResponse('Not Found', 404))
+    return Promise.resolve(fixtures.wrapErrorResponse('Not Found'))
   })
   })
 }
 }
 
 
@@ -199,7 +199,7 @@ export function mockStatsApi(request: MockedRequest) {
     if (url === '/admin/stats/dashboard') {
     if (url === '/admin/stats/dashboard') {
       return Promise.resolve(fixtures.wrapResponse(fixtures.mockDashboardStats))
       return Promise.resolve(fixtures.wrapResponse(fixtures.mockDashboardStats))
     }
     }
-    return Promise.resolve(fixtures.wrapErrorResponse('Not Found', 404))
+    return Promise.resolve(fixtures.wrapErrorResponse('Not Found'))
   })
   })
 }
 }
 
 
@@ -214,7 +214,7 @@ export function mockAllApis(request: MockedRequest) {
     }
     }
     // Machines
     // Machines
     if (url === '/admin/machines/list') {
     if (url === '/admin/machines/list') {
-      return Promise.resolve(fixtures.wrapResponse(fixtures.mockMachines))
+      return Promise.resolve(fixtures.wrapListResponse(fixtures.mockMachines))
     }
     }
     if (url === '/admin/machines/detail') {
     if (url === '/admin/machines/detail') {
       const machine = fixtures.mockMachines.find((m) => m.id === params?.id)
       const machine = fixtures.mockMachines.find((m) => m.id === params?.id)
@@ -222,16 +222,16 @@ export function mockAllApis(request: MockedRequest) {
     }
     }
     // Cameras
     // Cameras
     if (url === '/admin/cameras/list') {
     if (url === '/admin/cameras/list') {
-      return Promise.resolve(fixtures.wrapResponse(fixtures.mockCameras))
+      return Promise.resolve(fixtures.wrapListResponse(fixtures.mockCameras))
     }
     }
     if (url === '/camera/list') {
     if (url === '/camera/list') {
-      return Promise.resolve(fixtures.wrapResponse(fixtures.mockCameras))
+      return Promise.resolve(fixtures.wrapListResponse(fixtures.mockCameras))
     }
     }
     // Stats
     // Stats
     if (url === '/admin/stats/dashboard') {
     if (url === '/admin/stats/dashboard') {
       return Promise.resolve(fixtures.wrapResponse(fixtures.mockDashboardStats))
       return Promise.resolve(fixtures.wrapResponse(fixtures.mockDashboardStats))
     }
     }
-    return Promise.resolve(fixtures.wrapErrorResponse('Not Found', 404))
+    return Promise.resolve(fixtures.wrapErrorResponse('Not Found'))
   })
   })
 
 
   request.post.mockImplementation((url: string, data?: any) => {
   request.post.mockImplementation((url: string, data?: any) => {
@@ -239,7 +239,7 @@ export function mockAllApis(request: MockedRequest) {
       if (data?.username === 'admin' && data?.password === '123456') {
       if (data?.username === 'admin' && data?.password === '123456') {
         return Promise.resolve(fixtures.wrapResponse(fixtures.mockLoginResponse))
         return Promise.resolve(fixtures.wrapResponse(fixtures.mockLoginResponse))
       }
       }
-      return Promise.resolve(fixtures.wrapErrorResponse('用户名或密码错误', 401))
+      return Promise.resolve(fixtures.wrapErrorResponse('用户名或密码错误'))
     }
     }
     if (url === '/admin/auth/logout') {
     if (url === '/admin/auth/logout') {
       return Promise.resolve(fixtures.wrapResponse(null))
       return Promise.resolve(fixtures.wrapResponse(null))

+ 9 - 9
tests/unit/api/login.spec.ts

@@ -25,7 +25,7 @@ describe('Login API', () => {
         username: 'admin',
         username: 'admin',
         password: 'password123'
         password: 'password123'
       })
       })
-      expect(result.code).toBe(200)
+      expect(result.success).toBe(true)
       expect(result.data.token).toBe(mockLoginResponse.token)
       expect(result.data.token).toBe(mockLoginResponse.token)
     })
     })
 
 
@@ -35,8 +35,8 @@ describe('Login API', () => {
 
 
       const result = await login({ username: 'wrong', password: 'wrong' })
       const result = await login({ username: 'wrong', password: 'wrong' })
 
 
-      expect(result.code).toBe(401)
-      expect(result.message).toBe('用户名或密码错误')
+      expect(result.success).toBe(false)
+      expect(result.errMessage).toBe('用户名或密码错误')
     })
     })
   })
   })
 
 
@@ -48,7 +48,7 @@ describe('Login API', () => {
       const result = await getInfo()
       const result = await getInfo()
 
 
       expect(request.get).toHaveBeenCalledWith('/admin/auth/info')
       expect(request.get).toHaveBeenCalledWith('/admin/auth/info')
-      expect(result.code).toBe(200)
+      expect(result.success).toBe(true)
       expect(result.data.username).toBe(mockAdminInfo.username)
       expect(result.data.username).toBe(mockAdminInfo.username)
     })
     })
   })
   })
@@ -61,13 +61,13 @@ describe('Login API', () => {
       const result = await logout()
       const result = await logout()
 
 
       expect(request.post).toHaveBeenCalledWith('/admin/auth/logout')
       expect(request.post).toHaveBeenCalledWith('/admin/auth/logout')
-      expect(result.code).toBe(200)
+      expect(result.success).toBe(true)
     })
     })
   })
   })
 
 
   describe('changePassword', () => {
   describe('changePassword', () => {
     it('should call POST /admin/auth/password with password data', async () => {
     it('should call POST /admin/auth/password with password data', async () => {
-      const mockResponse = wrapResponse(null, 200, '密码修改成功')
+      const mockResponse = wrapResponse(null)
       vi.mocked(request.post).mockResolvedValue(mockResponse)
       vi.mocked(request.post).mockResolvedValue(mockResponse)
 
 
       const result = await changePassword({
       const result = await changePassword({
@@ -79,7 +79,7 @@ describe('Login API', () => {
         oldPassword: 'oldpass',
         oldPassword: 'oldpass',
         newPassword: 'newpass'
         newPassword: 'newpass'
       })
       })
-      expect(result.code).toBe(200)
+      expect(result.success).toBe(true)
     })
     })
 
 
     it('should handle wrong old password', async () => {
     it('should handle wrong old password', async () => {
@@ -91,8 +91,8 @@ describe('Login API', () => {
         newPassword: 'newpass'
         newPassword: 'newpass'
       })
       })
 
 
-      expect(result.code).toBe(400)
-      expect(result.message).toBe('原密码错误')
+      expect(result.success).toBe(false)
+      expect(result.errMessage).toBe('原密码错误')
     })
     })
   })
   })
 })
 })

+ 15 - 15
tests/unit/api/machine.spec.ts

@@ -1,7 +1,7 @@
 import { describe, it, expect, vi, beforeEach } from 'vitest'
 import { describe, it, expect, vi, beforeEach } from 'vitest'
 import { listMachines, getMachine, addMachine, updateMachine, deleteMachine } from '@/api/machine'
 import { listMachines, getMachine, addMachine, updateMachine, deleteMachine } from '@/api/machine'
 import * as request from '@/utils/request'
 import * as request from '@/utils/request'
-import { mockMachines, wrapResponse, wrapErrorResponse } from '../../fixtures'
+import { mockMachines, wrapResponse, wrapListResponse, wrapErrorResponse } from '../../fixtures'
 
 
 vi.mock('@/utils/request', () => ({
 vi.mock('@/utils/request', () => ({
   get: vi.fn(),
   get: vi.fn(),
@@ -15,15 +15,15 @@ describe('Machine API', () => {
 
 
   describe('listMachines', () => {
   describe('listMachines', () => {
     it('should call GET /admin/machines/list', async () => {
     it('should call GET /admin/machines/list', async () => {
-      const mockResponse = wrapResponse(mockMachines)
+      const mockResponse = wrapListResponse(mockMachines)
       vi.mocked(request.get).mockResolvedValue(mockResponse)
       vi.mocked(request.get).mockResolvedValue(mockResponse)
 
 
       const result = await listMachines()
       const result = await listMachines()
 
 
       expect(request.get).toHaveBeenCalledWith('/admin/machines/list')
       expect(request.get).toHaveBeenCalledWith('/admin/machines/list')
-      expect(result.code).toBe(200)
-      expect(result.data).toHaveLength(mockMachines.length)
-      expect(result.data[0].machineId).toBe(mockMachines[0].machineId)
+      expect(result.success).toBe(true)
+      expect(result.data.list).toHaveLength(mockMachines.length)
+      expect(result.data.list[0].machineId).toBe(mockMachines[0].machineId)
     })
     })
   })
   })
 
 
@@ -36,7 +36,7 @@ describe('Machine API', () => {
       const result = await getMachine(machine.id)
       const result = await getMachine(machine.id)
 
 
       expect(request.get).toHaveBeenCalledWith('/admin/machines/detail', { id: machine.id })
       expect(request.get).toHaveBeenCalledWith('/admin/machines/detail', { id: machine.id })
-      expect(result.code).toBe(200)
+      expect(result.success).toBe(true)
       expect(result.data.id).toBe(machine.id)
       expect(result.data.id).toBe(machine.id)
     })
     })
   })
   })
@@ -50,7 +50,7 @@ describe('Machine API', () => {
         name: '新机器',
         name: '新机器',
         cameraCount: 0
         cameraCount: 0
       }
       }
-      const mockResponse = wrapResponse(newMachine, 200, '新增成功')
+      const mockResponse = wrapResponse(newMachine)
       vi.mocked(request.post).mockResolvedValue(mockResponse)
       vi.mocked(request.post).mockResolvedValue(mockResponse)
 
 
       const addData = {
       const addData = {
@@ -62,7 +62,7 @@ describe('Machine API', () => {
       const result = await addMachine(addData)
       const result = await addMachine(addData)
 
 
       expect(request.post).toHaveBeenCalledWith('/admin/machines/add', addData)
       expect(request.post).toHaveBeenCalledWith('/admin/machines/add', addData)
-      expect(result.code).toBe(200)
+      expect(result.success).toBe(true)
       expect(result.data.machineId).toBe(newMachine.machineId)
       expect(result.data.machineId).toBe(newMachine.machineId)
     })
     })
   })
   })
@@ -71,7 +71,7 @@ describe('Machine API', () => {
     it('should call POST /admin/machines/update with data', async () => {
     it('should call POST /admin/machines/update with data', async () => {
       const machine = mockMachines[0]
       const machine = mockMachines[0]
       const updatedMachine = { ...machine, name: '更新后名称', location: '三楼' }
       const updatedMachine = { ...machine, name: '更新后名称', location: '三楼' }
-      const mockResponse = wrapResponse(updatedMachine, 200, '修改成功')
+      const mockResponse = wrapResponse(updatedMachine)
       vi.mocked(request.post).mockResolvedValue(mockResponse)
       vi.mocked(request.post).mockResolvedValue(mockResponse)
 
 
       const updateData = {
       const updateData = {
@@ -84,14 +84,14 @@ describe('Machine API', () => {
       const result = await updateMachine(updateData)
       const result = await updateMachine(updateData)
 
 
       expect(request.post).toHaveBeenCalledWith('/admin/machines/update', updateData)
       expect(request.post).toHaveBeenCalledWith('/admin/machines/update', updateData)
-      expect(result.code).toBe(200)
+      expect(result.success).toBe(true)
       expect(result.data.name).toBe('更新后名称')
       expect(result.data.name).toBe('更新后名称')
     })
     })
   })
   })
 
 
   describe('deleteMachine', () => {
   describe('deleteMachine', () => {
     it('should call POST /admin/machines/delete with id', async () => {
     it('should call POST /admin/machines/delete with id', async () => {
-      const mockResponse = wrapResponse(null, 200, '删除成功')
+      const mockResponse = wrapResponse(null)
       vi.mocked(request.post).mockResolvedValue(mockResponse)
       vi.mocked(request.post).mockResolvedValue(mockResponse)
 
 
       const result = await deleteMachine(mockMachines[2].id) // Use machine with 0 cameras
       const result = await deleteMachine(mockMachines[2].id) // Use machine with 0 cameras
@@ -99,17 +99,17 @@ describe('Machine API', () => {
       expect(request.post).toHaveBeenCalledWith('/admin/machines/delete', undefined, {
       expect(request.post).toHaveBeenCalledWith('/admin/machines/delete', undefined, {
         params: { id: mockMachines[2].id }
         params: { id: mockMachines[2].id }
       })
       })
-      expect(result.code).toBe(200)
+      expect(result.success).toBe(true)
     })
     })
 
 
     it('should handle delete failure when machine has cameras', async () => {
     it('should handle delete failure when machine has cameras', async () => {
-      const mockResponse = wrapErrorResponse('该机器下存在摄像头,无法删除', 400)
+      const mockResponse = wrapErrorResponse('该机器下存在摄像头,无法删除')
       vi.mocked(request.post).mockResolvedValue(mockResponse)
       vi.mocked(request.post).mockResolvedValue(mockResponse)
 
 
       const result = await deleteMachine(mockMachines[0].id) // Machine with cameras
       const result = await deleteMachine(mockMachines[0].id) // Machine with cameras
 
 
-      expect(result.code).toBe(400)
-      expect(result.message).toBe('该机器下存在摄像头,无法删除')
+      expect(result.success).toBe(false)
+      expect(result.errMessage).toBe('该机器下存在摄像头,无法删除')
     })
     })
   })
   })
 })
 })

+ 4 - 4
tests/unit/store/user.spec.ts

@@ -32,7 +32,7 @@ describe('User Store', () => {
       const store = useUserStore()
       const store = useUserStore()
       const result = await store.loginAction({ username: 'admin', password: 'password' })
       const result = await store.loginAction({ username: 'admin', password: 'password' })
 
 
-      expect(result.code).toBe(200)
+      expect(result.success).toBe(true)
       expect(store.token).toBe(mockLoginResponse.token)
       expect(store.token).toBe(mockLoginResponse.token)
       expect(store.userInfo?.username).toBe(mockLoginResponse.admin.username)
       expect(store.userInfo?.username).toBe(mockLoginResponse.admin.username)
       // setToken now takes (token, expiresIn)
       // setToken now takes (token, expiresIn)
@@ -40,13 +40,13 @@ describe('User Store', () => {
     })
     })
 
 
     it('should not store token on login failure', async () => {
     it('should not store token on login failure', async () => {
-      const mockResponse = wrapErrorResponse('用户名或密码错误', 401) as any
+      const mockResponse = wrapErrorResponse('用户名或密码错误') as any
       vi.mocked(loginApi.login).mockResolvedValue(mockResponse)
       vi.mocked(loginApi.login).mockResolvedValue(mockResponse)
 
 
       const store = useUserStore()
       const store = useUserStore()
       const result = await store.loginAction({ username: 'wrong', password: 'wrong' })
       const result = await store.loginAction({ username: 'wrong', password: 'wrong' })
 
 
-      expect(result.code).toBe(401)
+      expect(result.success).toBe(false)
       expect(store.token).toBe('')
       expect(store.token).toBe('')
       expect(auth.setToken).not.toHaveBeenCalled()
       expect(auth.setToken).not.toHaveBeenCalled()
     })
     })
@@ -60,7 +60,7 @@ describe('User Store', () => {
       const store = useUserStore()
       const store = useUserStore()
       const result = await store.getUserInfo()
       const result = await store.getUserInfo()
 
 
-      expect(result.code).toBe(200)
+      expect(result.success).toBe(true)
       expect(store.userInfo?.username).toBe(mockAdminInfo.username)
       expect(store.userInfo?.username).toBe(mockAdminInfo.username)
       expect(store.userInfo?.role).toBe(mockAdminInfo.role)
       expect(store.userInfo?.role).toBe(mockAdminInfo.role)
     })
     })

+ 4 - 4
tests/unit/views/audit/index.spec.ts

@@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'
 import { mount, flushPromises } from '@vue/test-utils'
 import { mount, flushPromises } from '@vue/test-utils'
 import { createPinia, setActivePinia } from 'pinia'
 import { createPinia, setActivePinia } from 'pinia'
 import AuditView from '@/views/audit/index.vue'
 import AuditView from '@/views/audit/index.vue'
-import { wrapPageResponse, wrapResponse } from '../../../fixtures'
+import { wrapListResponse, wrapResponse } from '../../../fixtures'
 
 
 // Mock element-plus
 // Mock element-plus
 vi.mock('element-plus', () => ({
 vi.mock('element-plus', () => ({
@@ -63,7 +63,7 @@ describe('Audit View', () => {
   beforeEach(() => {
   beforeEach(() => {
     setActivePinia(createPinia())
     setActivePinia(createPinia())
     vi.clearAllMocks()
     vi.clearAllMocks()
-    mockGetAuditLogs.mockResolvedValue(wrapPageResponse(mockAuditLogs, 3))
+    mockGetAuditLogs.mockResolvedValue(wrapListResponse(mockAuditLogs, 3))
   })
   })
 
 
   const mountAudit = () => {
   const mountAudit = () => {
@@ -281,7 +281,7 @@ describe('Audit View', () => {
 
 
   describe('错误处理', () => {
   describe('错误处理', () => {
     it('API 返回错误码应该正确处理', async () => {
     it('API 返回错误码应该正确处理', async () => {
-      mockGetAuditLogs.mockResolvedValue(wrapResponse({ rows: [], total: 0 }, 500, '获取审计日志失败'))
+      mockGetAuditLogs.mockResolvedValue(wrapResponse(null, false, '获取审计日志失败'))
 
 
       mountAudit()
       mountAudit()
       await flushPromises()
       await flushPromises()
@@ -290,7 +290,7 @@ describe('Audit View', () => {
     })
     })
 
 
     it('API 返回空数据应该正确处理', async () => {
     it('API 返回空数据应该正确处理', async () => {
-      mockGetAuditLogs.mockResolvedValue(wrapPageResponse([], 0))
+      mockGetAuditLogs.mockResolvedValue(wrapListResponse([], 0))
 
 
       mountAudit()
       mountAudit()
       await flushPromises()
       await flushPromises()

+ 4 - 4
tests/unit/views/camera/index.spec.ts

@@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'
 import { mount, flushPromises } from '@vue/test-utils'
 import { mount, flushPromises } from '@vue/test-utils'
 import { createPinia, setActivePinia } from 'pinia'
 import { createPinia, setActivePinia } from 'pinia'
 import CameraView from '@/views/camera/index.vue'
 import CameraView from '@/views/camera/index.vue'
-import { wrapResponse, mockCameras, mockMachines } from '../../../fixtures'
+import { wrapResponse, wrapListResponse, mockCameras, mockMachines } from '../../../fixtures'
 
 
 // Mock vue-router
 // Mock vue-router
 const mockPush = vi.fn()
 const mockPush = vi.fn()
@@ -49,8 +49,8 @@ describe('Camera View', () => {
   beforeEach(() => {
   beforeEach(() => {
     setActivePinia(createPinia())
     setActivePinia(createPinia())
     vi.clearAllMocks()
     vi.clearAllMocks()
-    mockAdminListCameras.mockResolvedValue(wrapResponse(mockCameras))
-    mockListMachines.mockResolvedValue(wrapResponse(mockMachines))
+    mockAdminListCameras.mockResolvedValue(wrapListResponse(mockCameras))
+    mockListMachines.mockResolvedValue(wrapListResponse(mockMachines))
   })
   })
 
 
   const mountCamera = () => {
   const mountCamera = () => {
@@ -282,7 +282,7 @@ describe('Camera View', () => {
 
 
   describe('错误处理', () => {
   describe('错误处理', () => {
     it('API 返回错误码应该正确处理', async () => {
     it('API 返回错误码应该正确处理', async () => {
-      mockAdminListCameras.mockResolvedValue(wrapResponse([], 500, '获取失败'))
+      mockAdminListCameras.mockResolvedValue(wrapResponse([], false, '获取失败'))
 
 
       const wrapper = mountCamera()
       const wrapper = mountCamera()
       await flushPromises()
       await flushPromises()

+ 2 - 2
tests/unit/views/dashboard/index.spec.ts

@@ -269,7 +269,7 @@ describe('Dashboard View', () => {
   describe('错误处理', () => {
   describe('错误处理', () => {
     it('获取数据失败应该显示错误消息', async () => {
     it('获取数据失败应该显示错误消息', async () => {
       const { ElMessage } = await import('element-plus')
       const { ElMessage } = await import('element-plus')
-      mockGetDashboardStats.mockResolvedValue(wrapResponse(null, 500, '服务器错误'))
+      mockGetDashboardStats.mockResolvedValue(wrapResponse(null, false, '服务器错误'))
 
 
       mountDashboard()
       mountDashboard()
       await flushPromises()
       await flushPromises()
@@ -279,7 +279,7 @@ describe('Dashboard View', () => {
     })
     })
 
 
     it('网络错误应该显示错误消息', async () => {
     it('网络错误应该显示错误消息', async () => {
-      mockGetDashboardStats.mockResolvedValue(wrapResponse(null, 500, '网络错误'))
+      mockGetDashboardStats.mockResolvedValue(wrapResponse(null, false, '网络错误'))
 
 
       mountDashboard()
       mountDashboard()
       await flushPromises()
       await flushPromises()

+ 4 - 4
tests/unit/views/machine/index.spec.ts

@@ -3,7 +3,7 @@ import { mount, flushPromises } from '@vue/test-utils'
 import { createPinia, setActivePinia } from 'pinia'
 import { createPinia, setActivePinia } from 'pinia'
 import { createI18n } from 'vue-i18n'
 import { createI18n } from 'vue-i18n'
 import MachineView from '@/views/machine/index.vue'
 import MachineView from '@/views/machine/index.vue'
-import { wrapResponse, mockMachines } from '../../../fixtures'
+import { wrapResponse, wrapListResponse, mockMachines } from '../../../fixtures'
 
 
 // Create i18n instance for tests
 // Create i18n instance for tests
 const i18n = createI18n({
 const i18n = createI18n({
@@ -45,7 +45,7 @@ describe('Machine View', () => {
   beforeEach(() => {
   beforeEach(() => {
     setActivePinia(createPinia())
     setActivePinia(createPinia())
     vi.clearAllMocks()
     vi.clearAllMocks()
-    mockListMachines.mockResolvedValue(wrapResponse(mockMachines))
+    mockListMachines.mockResolvedValue(wrapListResponse(mockMachines))
   })
   })
 
 
   const mountMachine = () => {
   const mountMachine = () => {
@@ -354,7 +354,7 @@ describe('Machine View', () => {
 
 
   describe('错误处理', () => {
   describe('错误处理', () => {
     it('API 返回错误码应该正确处理', async () => {
     it('API 返回错误码应该正确处理', async () => {
-      mockListMachines.mockResolvedValue(wrapResponse([], 500, '获取失败'))
+      mockListMachines.mockResolvedValue(wrapResponse([], false, '获取失败'))
 
 
       const wrapper = mountMachine()
       const wrapper = mountMachine()
       await flushPromises()
       await flushPromises()
@@ -363,7 +363,7 @@ describe('Machine View', () => {
     })
     })
 
 
     it('新增返回错误应该处理', async () => {
     it('新增返回错误应该处理', async () => {
-      mockAddMachine.mockResolvedValue(wrapResponse(null, 400, '新增失败'))
+      mockAddMachine.mockResolvedValue(wrapResponse(null, false, '新增失败'))
 
 
       const wrapper = mountMachine()
       const wrapper = mountMachine()
       await flushPromises()
       await flushPromises()

+ 1 - 1
tests/unit/views/stats/index.spec.ts

@@ -188,7 +188,7 @@ describe('Stats View', () => {
 
 
   describe('错误处理', () => {
   describe('错误处理', () => {
     it('API 返回错误码应该正确处理', async () => {
     it('API 返回错误码应该正确处理', async () => {
-      mockGetDashboardStats.mockResolvedValue(wrapResponse(null, 500, '服务器错误'))
+      mockGetDashboardStats.mockResolvedValue(wrapResponse(null, false, '服务器错误'))
 
 
       mountStats()
       mountStats()
       await flushPromises()
       await flushPromises()