Browse Source

feat: refactor camera API and introduce machine management

- Updated camera API to enhance functionality, including endpoints for listing, retrieving, adding, updating, and deleting cameras.
- Introduced new machine management API for handling machine-related operations.
- Improved type definitions for API responses and requests, ensuring better type safety and clarity.
- Enhanced the user interface in camera views to reflect updated data structures and improve usability.
- Updated error handling to use consistent messaging across the application.
yb 3 weeks ago
parent
commit
1d82c10c54

+ 94 - 57
src/api/camera.ts

@@ -1,87 +1,124 @@
-import { get, post, put, del } from '@/utils/request'
-import type { ApiResponse, PageResult, CameraDevice, CameraChannel, PlayResponse, RecordItem } from '@/types'
+import { get, post } from '@/utils/request'
+import type {
+  ApiResponse,
+  CameraDTO,
+  ChannelDTO,
+  CameraInfoDTO,
+  CameraAddRequest,
+  CameraUpdateRequest,
+  SwitchChannelRequest,
+  PTZAction
+} from '@/types'
 
-// 获取设备列表
-export function listDevice(params?: object): Promise<ApiResponse<PageResult<CameraDevice>>> {
-  return get('/iot/sip/device/list', params)
-}
+// ==================== Controller APIs (MVP) ====================
 
-// 获取设备详情
-export function getDevice(deviceId: string): Promise<ApiResponse<CameraDevice>> {
-  return get(`/iot/sip/device/${deviceId}`)
+// 获取摄像头列表
+export function listCameras(machineId?: string): Promise<ApiResponse<CameraDTO[]>> {
+  return get('/camera/list', machineId ? { machineId } : undefined)
 }
 
-// 添加设备
-export function addDevice(data: Partial<CameraDevice>): Promise<ApiResponse<null>> {
-  return post('/iot/sip/device', data)
+// 获取摄像头信息
+export function getCamera(cameraId: string): Promise<ApiResponse<CameraDTO>> {
+  return get(`/camera/${cameraId}`)
 }
 
-// 修改设备
-export function updateDevice(data: Partial<CameraDevice>): Promise<ApiResponse<null>> {
-  return put('/iot/sip/device', data)
+// 切换摄像头通道 (MVP核心)
+export function switchChannel(data: SwitchChannelRequest): Promise<ApiResponse<ChannelDTO>> {
+  return post('/camera/switch', data)
 }
 
-// 删除设备
-export function delDevice(deviceId: string): Promise<ApiResponse<null>> {
-  return del(`/iot/sip/device/${deviceId}`)
+// 获取当前活动通道
+export function getCurrentChannel(machineId: string): Promise<ApiResponse<ChannelDTO>> {
+  return get('/camera/current', { machineId })
 }
 
-// 获取设备通道列表
-export function listChannel(deviceId: string, params?: object): Promise<ApiResponse<PageResult<CameraChannel>>> {
-  return get(`/iot/sip/channel/${deviceId}/list`, params)
+// 开始PTZ控制 (后台专用)
+export function ptzStart(cameraId: string, action: PTZAction, speed: number = 50): Promise<ApiResponse<null>> {
+  return post(`/camera/${cameraId}/ptz/start`, undefined, {
+    params: { action, speed }
+  })
 }
 
-// 同步通道
-export function syncChannel(deviceId: string): Promise<ApiResponse<null>> {
-  return post(`/iot/sip/device/${deviceId}/sync`)
+// 停止PTZ控制 (后台专用)
+export function ptzStop(cameraId: string): Promise<ApiResponse<null>> {
+  return post(`/camera/${cameraId}/ptz/stop`)
 }
 
-// 播放实时视频
-export function play(deviceId: string, channelId: string): Promise<ApiResponse<PlayResponse>> {
-  return get(`/iot/sip/channel/${deviceId}/${channelId}/play`)
-}
+// ==================== Admin APIs ====================
 
-// 停止播放
-export function stopPlay(deviceId: string, channelId: string): Promise<ApiResponse<null>> {
-  return get(`/iot/sip/channel/${deviceId}/${channelId}/stop`)
+// 获取摄像头列表 (管理后台)
+export function adminListCameras(machineId?: string): Promise<ApiResponse<CameraInfoDTO[]>> {
+  return get('/admin/cameras/list', machineId ? { machineId } : undefined)
 }
 
-// 播放录像回放
-export function playback(deviceId: string, channelId: string, params: { start: number; end: number }): Promise<ApiResponse<PlayResponse>> {
-  return get(`/iot/sip/channel/${deviceId}/${channelId}/playback`, params)
+// 获取摄像头详情
+export function adminGetCamera(id: number): Promise<ApiResponse<CameraInfoDTO>> {
+  return get('/admin/cameras/detail', { id })
 }
 
-// 停止回放
-export function playbackStop(deviceId: string, channelId: string): Promise<ApiResponse<null>> {
-  return get(`/iot/sip/channel/${deviceId}/${channelId}/playbackStop`)
+// 添加摄像头
+export function adminAddCamera(data: CameraAddRequest): Promise<ApiResponse<CameraInfoDTO>> {
+  return post('/admin/cameras/add', data)
 }
 
-// 暂停回放
-export function playbackPause(deviceId: string, channelId: string): Promise<ApiResponse<null>> {
-  return get(`/iot/sip/channel/${deviceId}/${channelId}/playbackPause`)
+// 更新摄像头
+export function adminUpdateCamera(data: CameraUpdateRequest): Promise<ApiResponse<CameraInfoDTO>> {
+  return post('/admin/cameras/update', data)
 }
 
-// 恢复回放
-export function playbackReplay(deviceId: string, channelId: string): Promise<ApiResponse<null>> {
-  return get(`/iot/sip/channel/${deviceId}/${channelId}/playbackReplay`)
+// 删除摄像头
+export function adminDeleteCamera(id: number): Promise<ApiResponse<null>> {
+  return post('/admin/cameras/delete', undefined, {
+    params: { id }
+  })
 }
 
-// 录像跳转
-export function playbackSeek(deviceId: string, channelId: string, params: { seek: number }): Promise<ApiResponse<null>> {
-  return get(`/iot/sip/channel/${deviceId}/${channelId}/playbackSeek`, params)
+// 检测摄像头连通性
+export function adminCheckCamera(id: number): Promise<ApiResponse<boolean>> {
+  return post('/admin/cameras/check', undefined, {
+    params: { id }
+  })
 }
 
-// 回放倍速
-export function playbackSpeed(deviceId: string, channelId: string, speed: number): Promise<ApiResponse<null>> {
-  return get(`/iot/sip/channel/${deviceId}/${channelId}/playbackSpeed/${speed}`)
-}
+// ==================== 兼容旧代码的别名 ====================
 
-// 获取设备录像
-export function getDevRecord(deviceId: string, channelId: string, params: { start: number; end: number }): Promise<ApiResponse<{ recordItems: RecordItem[] }>> {
-  return get(`/iot/sip/channel/${deviceId}/${channelId}/devRecord`, params)
-}
+// 获取设备列表 (兼容)
+export const listDevice = adminListCameras
+
+// 获取设备详情 (兼容)
+export const getDevice = (deviceId: string) => getCamera(deviceId)
+
+// 添加设备 (兼容)
+export const addDevice = adminAddCamera
+
+// 修改设备 (兼容)
+export const updateDevice = adminUpdateCamera
+
+// 删除设备 (兼容)
+export const delDevice = (id: number) => adminDeleteCamera(id)
+
+// PTZ控制 (兼容旧API)
+export function ptzControl(
+  _deviceId: string,
+  channelId: string,
+  command: string,
+  horizonSpeed?: number,
+  _verticalSpeed?: number,
+  _zoomSpeed?: number
+): Promise<ApiResponse<null>> {
+  // 映射旧的命令到新的 action
+  const actionMap: Record<string, PTZAction> = {
+    up: 'up',
+    down: 'down',
+    left: 'left',
+    right: 'right',
+    zoomin: 'zoom_in',
+    zoomout: 'zoom_out',
+    stop: 'stop'
+  }
+  const action = actionMap[command.toLowerCase()] || 'stop'
+  const speed = horizonSpeed || 50
 
-// 云台控制
-export function ptzControl(deviceId: string, channelId: string, command: string, horizonSpeed?: number, verticalSpeed?: number, zoomSpeed?: number): Promise<ApiResponse<null>> {
-  return get(`/iot/sip/channel/${deviceId}/${channelId}/ptz/${command}/${horizonSpeed || 50}/${verticalSpeed || 50}/${zoomSpeed || 50}`)
+  // 使用 channelId 关联的 cameraId 进行 PTZ 控制
+  return ptzStart(channelId, action, speed)
 }

+ 15 - 26
src/api/login.ts

@@ -1,39 +1,28 @@
 import { get, post } from '@/utils/request'
-import type { LoginParams, ApiResponse, UserInfo } from '@/types'
-
-// Hono API 认证响应类型
-interface AuthResponse {
-  accessToken: string
-  refreshToken: string
-  expiresIn: number
-  user: {
-    id: string
-    username: string
-    role: string
-  }
-}
+import type {
+  ApiResponse,
+  LoginParams,
+  LoginResponse,
+  AdminInfo,
+  ChangePasswordRequest
+} from '@/types'
 
 // 登录
-export function login(data: LoginParams): Promise<ApiResponse<AuthResponse>> {
-  return post('/auth/login', data)
+export function login(data: LoginParams): Promise<ApiResponse<LoginResponse>> {
+  return post('/admin/auth/login', data)
 }
 
-// 获取用户信息
-export function getInfo(): Promise<ApiResponse<UserInfo>> {
-  return get('/auth/me')
+// 获取当前用户信息
+export function getInfo(): Promise<ApiResponse<AdminInfo>> {
+  return get('/admin/auth/info')
 }
 
 // 退出登录
 export function logout(): Promise<ApiResponse<null>> {
-  return post('/auth/logout')
-}
-
-// 刷新 Token
-export function refreshToken(refreshToken: string): Promise<ApiResponse<AuthResponse>> {
-  return post('/auth/refresh', { refreshToken })
+  return post('/admin/auth/logout')
 }
 
 // 修改密码
-export function changePassword(data: { oldPassword: string; newPassword: string }): Promise<ApiResponse<null>> {
-  return post('/auth/change-password', data)
+export function changePassword(data: ChangePasswordRequest): Promise<ApiResponse<null>> {
+  return post('/admin/auth/password', data)
 }

+ 34 - 0
src/api/machine.ts

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

+ 5 - 64
src/api/stats.ts

@@ -1,69 +1,10 @@
 import { get } from '@/utils/request'
-import type { ApiResponse } from '@/types'
-
-// 用户统计
-export interface UserStats {
-  total: number
-  admin_count: number
-  operator_count: number
-  viewer_count: number
-  active_count: number
-  inactive_count: number
-}
-
-// 摄像头统计
-export interface CameraStats {
-  total: number
-  online_count: number
-  offline_count: number
-  error_count: number
-}
-
-// 视频统计
-export interface VideoStats {
-  total: number
-  total_views: number
-  total_duration: number
-}
-
-// 直播会话统计
-export interface LiveSessionStats {
-  total: number
-  live_count: number
-  ended_count: number
-  max_peak_viewers: number
-}
-
-// 今日统计
-export interface TodayStats {
-  today_views: number
-  today_new_users: number
-  today_sessions: number
-}
-
-// 系统状态
-export interface SystemStatus {
-  database: string
-  api_version: string
-  uptime: number
-}
-
-// 仪表盘数据
-export interface DashboardData {
-  users: UserStats
-  cameras: CameraStats
-  videos: VideoStats
-  live_sessions: LiveSessionStats
-  today: TodayStats
-  system: SystemStatus
-}
+import type { ApiResponse, DashboardStatsDTO } from '@/types'
 
 // 获取仪表盘统计数据
-export function getDashboardStats(): Promise<ApiResponse<DashboardData>> {
-  return get('/stats/dashboard')
+export function getDashboardStats(): Promise<ApiResponse<DashboardStatsDTO>> {
+  return get('/admin/stats/dashboard')
 }
 
-// 获取统计概览
-export function getStatsOverview(days = 7): Promise<ApiResponse<any>> {
-  return get('/stats/overview', { days })
-}
+// 兼容旧代码的类型导出
+export type { DashboardStatsDTO }

+ 35 - 48
src/store/user.ts

@@ -1,75 +1,62 @@
-import { defineStore } from "pinia";
-import { ref } from "vue";
-import type { UserInfo } from "@/types";
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+import type { AdminInfo } from '@/types'
 import {
   getToken,
   setToken,
   removeToken,
   setRefreshToken,
-  removeRefreshToken,
-} from "@/utils/auth";
-import { login, logout, getInfo } from "@/api/login";
-import type { LoginParams } from "@/types";
+  removeRefreshToken
+} from '@/utils/auth'
+import { login, logout, getInfo } from '@/api/login'
+import type { LoginParams } from '@/types'
 
-export const useUserStore = defineStore("user", () => {
-  const token = ref<string>(getToken() || "");
-  const userInfo = ref<UserInfo | null>(null);
+export const useUserStore = defineStore('user', () => {
+  const token = ref<string>(getToken() || '')
+  const userInfo = ref<AdminInfo | null>(null)
 
   async function loginAction(loginForm: LoginParams) {
-    const res = await login(loginForm);
+    const res = await login(loginForm)
     if (res.code === 200 && res.data) {
-      // Hono API 返回 accessToken 和 refreshToken
-      const { accessToken, refreshToken, user } = res.data;
-      token.value = accessToken;
-      setToken(accessToken);
+      // 新 API 返回 token 和 admin
+      const { token: accessToken, refreshToken, admin } = res.data
+      token.value = accessToken
+      setToken(accessToken)
       if (refreshToken) {
-        setRefreshToken(refreshToken);
+        setRefreshToken(refreshToken)
       }
-      // 直接设置用户信息
-      console.log("🚀 ~ loginAction ~ user:", user);
-      // id: "9fbe87ad2e38fe111da7fbea466ea192"
-      // role: "admin"
-      // username: "pwtk004"
-      if (user) {
-        userInfo.value = {
-          id: user.id,
-          username: user.username,
-          role: user.role,
-          created_at: 0,
-          email: "",
-          last_login: 0,
-          status: "",
-          updated_at: 0,
-        };
+      // 设置用户信息
+      if (admin) {
+        userInfo.value = admin
       }
     }
-    return res;
+    return res
   }
 
   async function getUserInfo() {
-    const res = await getInfo();
+    const res = await getInfo()
     if (res.code === 200 && res.data) {
-      userInfo.value = res.data;
+      userInfo.value = res.data
     }
-    return res;
+    return res
   }
 
   async function logoutAction() {
     try {
-      await logout();
+      await logout()
     } finally {
-      token.value = "";
-      userInfo.value = null;
-      removeToken();
-      removeRefreshToken();
+      token.value = ''
+      userInfo.value = null
+      removeToken()
+      removeRefreshToken()
     }
   }
 
   function resetToken() {
-    token.value = "";
-    userInfo.value = null;
-    removeToken();
-    removeRefreshToken();
+    token.value = ''
+    userInfo.value = null
+    removeToken()
+    removeRefreshToken()
   }
 
   return {
@@ -78,6 +65,6 @@ export const useUserStore = defineStore("user", () => {
     loginAction,
     getUserInfo,
     logoutAction,
-    resetToken,
-  };
-});
+    resetToken
+  }
+})

+ 212 - 111
src/types/index.ts

@@ -1,130 +1,231 @@
 // API 响应类型
 export interface ApiResponse<T = any> {
-  code: number;
-  msg: string;
-  data: T;
+  code: number
+  message: string
+  data: T
+  timestamp?: number
+  traceId?: string
 }
 
 // 分页参数
 export interface PageParams {
-  pageNum: number;
-  pageSize: number;
+  pageNum: number
+  pageSize: number
 }
 
 // 分页响应
 export interface PageResult<T = any> {
-  total: number;
-  rows: T[];
+  total: number
+  rows: T[]
 }
 
-// 用户信息
+// 登录参数
+export interface LoginParams {
+  username: string
+  password: string
+}
+
+// 登录响应
+export interface LoginResponse {
+  token: string
+  tokenType: string
+  expiresIn: number
+  refreshToken?: string
+  admin: AdminInfo
+}
+
+// 管理员信息
+export interface AdminInfo {
+  id: number
+  username: string
+  nickname: string
+  role: string
+  lastLoginAt?: string
+}
+
+// 用户信息 (兼容旧代码)
 export interface UserInfo {
-  created_at: number;
-  email: string;
-  id: string;
-  last_login: number;
-  role: string;
-  status: string;
-  updated_at: number;
-  username: string;
+  id: number
+  username: string
+  nickname: string
+  role: string
+  lastLoginAt?: string
 }
 
-// 登录参数
-export interface LoginParams {
-  username: string;
-  password: string;
-  code?: string;
-  uuid?: string;
-}
-
-// 摄像头设备
-export interface CameraDevice {
-  deviceId: string;
-  deviceName: string;
-  manufacturer?: string;
-  model?: string;
-  firmware?: string;
-  transport?: string;
-  streamMode?: string;
-  online: boolean;
-  registerTime?: string;
-  keepaliveTime?: string;
-  ip?: string;
-  port?: number;
-  hostAddress?: string;
-  charset?: string;
-  subscribeCycleForCatalog?: number;
-  subscribeCycleForMobilePosition?: number;
-  mobilePositionSubmissionInterval?: number;
-  subscribeCycleForAlarm?: number;
-  ssrcCheck?: boolean;
-  geoCoordSys?: string;
-  treeType?: string;
-  password?: string;
-  asMessageChannel?: boolean;
-  broadcastPushAfterAck?: boolean;
-  createTime?: string;
-  updateTime?: string;
-  channelCount?: number;
-}
-
-// 摄像头通道
-export interface CameraChannel {
-  channelId: string;
-  deviceId: string;
-  name: string;
-  manufacturer?: string;
-  model?: string;
-  owner?: string;
-  civilCode?: string;
-  block?: string;
-  address?: string;
-  parental?: number;
-  parentId?: string;
-  safetyWay?: number;
-  registerWay?: number;
-  certNum?: string;
-  certifiable?: number;
-  errCode?: number;
-  endTime?: string;
-  secrecy?: number;
-  ipAddress?: string;
-  port?: number;
-  password?: string;
-  ptzType?: number;
-  status?: boolean;
-  longitude?: number;
-  latitude?: number;
-  longitudeGcj02?: number;
-  latitudeGcj02?: number;
-  longitudeWgs84?: number;
-  latitudeWgs84?: number;
-  hasAudio?: boolean;
-  createTime?: string;
-  updateTime?: string;
-  businessGroupId?: string;
-  gpsTime?: string;
-}
-
-// 录像记录
-export interface RecordItem {
-  name?: string;
-  start: number;
-  end: number;
-  secrecy?: number;
-  type?: string;
+// 通道信息 (Controller 用)
+export interface ChannelDTO {
+  channelId: string
+  name: string
+  rtspUrl: string
+  defaultView: boolean
+  status: 'ONLINE' | 'OFFLINE'
+  cameraId: string
+}
+
+// 摄像头信息 (Controller 用)
+export interface CameraDTO {
+  cameraId: string
+  name: string
+  machineId: string
+  status: 'ONLINE' | 'OFFLINE'
+  capability: 'switch_only' | 'ptz_enabled'
+  ptzSupported: boolean
+  channels: ChannelDTO[]
+}
+
+// 通道详情 (Admin 用)
+export interface ChannelInfoDTO {
+  id: number
+  channelId: string
+  name: string
+  rtspUrl: string
+  defaultView: boolean
+  status: 'ONLINE' | 'OFFLINE'
+}
+
+// 摄像头详情 (Admin 用)
+export interface CameraInfoDTO {
+  id: number
+  cameraId: string
+  name: string
+  ip: string
+  port: number
+  username: string
+  brand: string
+  capability: 'switch_only' | 'ptz_enabled'
+  status: 'ONLINE' | 'OFFLINE'
+  machineId: string
+  machineName: string
+  enabled: boolean
+  channels: ChannelInfoDTO[]
+  createdAt: string
+  updatedAt: string
+}
+
+// 添加摄像头请求
+export interface CameraAddRequest {
+  cameraId: string
+  name: string
+  ip: string
+  port?: number
+  username?: string
+  password?: string
+  brand?: string
+  capability?: 'switch_only' | 'ptz_enabled'
+  machineId?: string
+  channels?: ChannelAddRequest[]
+}
+
+// 添加通道请求
+export interface ChannelAddRequest {
+  channelId: string
+  name: string
+  rtspUrl?: string
+  defaultView?: boolean
 }
 
-// 播放响应
+// 更新摄像头请求
+export interface CameraUpdateRequest {
+  id: number
+  name?: string
+  ip?: string
+  port?: number
+  username?: string
+  password?: string
+  brand?: string
+  capability?: 'switch_only' | 'ptz_enabled'
+  machineId?: string
+  enabled?: boolean
+  channels?: ChannelUpdateRequest[]
+}
+
+// 更新通道请求
+export interface ChannelUpdateRequest {
+  id?: number
+  channelId?: string
+  name?: string
+  rtspUrl?: string
+  defaultView?: boolean
+}
+
+// 切换通道请求
+export interface SwitchChannelRequest {
+  machineId: string
+  channelId: string
+}
+
+// 机器信息
+export interface MachineDTO {
+  id: number
+  machineId: string
+  name: string
+  location: string
+  description: string
+  enabled: boolean
+  cameraCount: number
+  createdAt: string
+  updatedAt: string
+}
+
+// 添加机器请求
+export interface MachineAddRequest {
+  machineId: string
+  name: string
+  location?: string
+  description?: string
+}
+
+// 更新机器请求
+export interface MachineUpdateRequest {
+  id: number
+  name?: string
+  location?: string
+  description?: string
+  enabled?: boolean
+}
+
+// 仪表盘统计
+export interface DashboardStatsDTO {
+  machineTotal: number
+  machineEnabled: number
+  cameraTotal: number
+  cameraOnline: number
+  cameraOffline: number
+  channelTotal: number
+}
+
+// 修改密码请求
+export interface ChangePasswordRequest {
+  oldPassword: string
+  newPassword: string
+}
+
+// PTZ 动作类型
+export type PTZAction = 'up' | 'down' | 'left' | 'right' | 'zoom_in' | 'zoom_out' | 'stop'
+
+// 兼容旧代码的类型别名
+export type CameraDevice = CameraInfoDTO
+export type CameraChannel = ChannelInfoDTO
+
+// 播放响应 (保留用于视频播放)
 export interface PlayResponse {
-  streamId: string;
-  flv: string;
-  ws_flv?: string;
-  rtsp?: string;
-  rtmp?: string;
-  hls?: string;
-  rtc?: string;
-  mediaServerId?: string;
-  deviceId?: string;
-  channelId?: string;
+  streamId: string
+  flv: string
+  ws_flv?: string
+  rtsp?: string
+  rtmp?: string
+  hls?: string
+  rtc?: string
+  mediaServerId?: string
+  deviceId?: string
+  channelId?: string
+}
+
+// 录像记录 (保留用于视频回放)
+export interface RecordItem {
+  name?: string
+  start: number
+  end: number
+  secrecy?: number
+  type?: string
 }

+ 4 - 4
src/utils/request.ts

@@ -52,13 +52,13 @@ service.interceptors.response.use(
     }
 
     if (code === 500) {
-      ElMessage.error(res.msg || '服务器错误')
-      return Promise.reject(new Error(res.msg || '服务器错误'))
+      ElMessage.error(res.message || '服务器错误')
+      return Promise.reject(new Error(res.message || '服务器错误'))
     }
 
     if (code !== 200) {
-      ElMessage.error(res.msg || '请求失败')
-      return Promise.reject(new Error(res.msg || '请求失败'))
+      ElMessage.error(res.message || '请求失败')
+      return Promise.reject(new Error(res.message || '请求失败'))
     }
 
     return res as any

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

@@ -258,7 +258,7 @@ async function getList() {
       auditList.value = res.data?.rows || []
       total.value = res.data?.total || 0
     } else {
-      ElMessage.error(res.msg || '获取审计日志失败')
+      ElMessage.error(res.message || '获取审计日志失败')
     }
   } catch (error) {
     console.error('Failed to load audit logs:', error)

+ 50 - 139
src/views/camera/channel.vue

@@ -3,200 +3,116 @@
     <!-- 返回按钮 -->
     <div class="page-header">
       <el-button :icon="ArrowLeft" @click="goBack">返回</el-button>
-      <span class="title">设备通道列表 - {{ deviceId }}</span>
+      <span class="title">通道列表 - {{ cameraInfo?.name || cameraId }}</span>
     </div>
 
-    <!-- 搜索区域 -->
-    <div class="search-form">
-      <el-form :model="queryParams" inline>
-        <el-form-item label="通道名称">
-          <el-input
-            v-model="queryParams.name"
-            placeholder="请输入通道名称"
-            clearable
-            @keyup.enter="handleQuery"
-          />
-        </el-form-item>
-        <el-form-item label="在线状态">
-          <el-select
-            v-model="queryParams.status"
-            placeholder="请选择"
-            clearable
-          >
-            <el-option label="在线" :value="true" />
-            <el-option label="离线" :value="false" />
-          </el-select>
-        </el-form-item>
-        <el-form-item>
-          <el-button type="primary" :icon="Search" @click="handleQuery"
-            >搜索</el-button
-          >
-          <el-button :icon="Refresh" @click="resetQuery">重置</el-button>
-        </el-form-item>
-      </el-form>
+    <!-- 摄像头信息 -->
+    <div v-if="cameraInfo" class="camera-info">
+      <el-descriptions :column="4" border>
+        <el-descriptions-item label="摄像头ID">{{ cameraInfo.cameraId }}</el-descriptions-item>
+        <el-descriptions-item label="名称">{{ cameraInfo.name }}</el-descriptions-item>
+        <el-descriptions-item label="所属机器">{{ cameraInfo.machineId }}</el-descriptions-item>
+        <el-descriptions-item label="状态">
+          <el-tag :type="cameraInfo.status === 'ONLINE' ? 'success' : 'danger'">
+            {{ cameraInfo.status === 'ONLINE' ? '在线' : '离线' }}
+          </el-tag>
+        </el-descriptions-item>
+      </el-descriptions>
     </div>
 
     <!-- 数据表格 -->
-    <el-table v-loading="loading" :data="channelList" border>
+    <el-table v-loading="loading" :data="filteredChannels" border>
       <el-table-column type="index" label="序号" width="60" align="center" />
       <el-table-column
         prop="channelId"
         label="通道ID"
-        min-width="180"
+        min-width="120"
         show-overflow-tooltip
       />
       <el-table-column
         prop="name"
         label="通道名称"
-        min-width="150"
-        show-overflow-tooltip
-      />
-      <el-table-column
-        prop="manufacturer"
-        label="厂商"
         min-width="120"
         show-overflow-tooltip
       />
       <el-table-column
-        prop="address"
-        label="地址"
-        min-width="150"
+        prop="rtspUrl"
+        label="RTSP地址"
+        min-width="250"
         show-overflow-tooltip
       />
-      <el-table-column
-        prop="ptzType"
-        label="云台类型"
-        width="100"
-        align="center"
-      >
+      <el-table-column prop="defaultView" label="默认视角" width="100" align="center">
         <template #default="{ row }">
-          <span>{{ getPtzType(row.ptzType) }}</span>
+          <el-tag :type="row.defaultView ? 'success' : 'info'">
+            {{ row.defaultView ? '是' : '否' }}
+          </el-tag>
         </template>
       </el-table-column>
       <el-table-column prop="status" label="状态" width="80" align="center">
         <template #default="{ row }">
-          <el-tag :type="row.status ? 'success' : 'danger'">
-            {{ row.status ? "在线" : "离线" }}
+          <el-tag :type="row.status === 'ONLINE' ? 'success' : 'danger'">
+            {{ row.status === 'ONLINE' ? '在线' : '离线' }}
           </el-tag>
         </template>
       </el-table-column>
-      <el-table-column label="操作" width="200" align="center" fixed="right">
+      <el-table-column label="操作" width="150" align="center" fixed="right">
         <template #default="{ row }">
           <el-button
             type="primary"
             link
             :icon="VideoPlay"
             @click="handlePlay(row)"
-            :disabled="!row.status"
-          >
-            实时播放
-          </el-button>
-          <el-button
-            type="success"
-            link
-            :icon="VideoCamera"
-            @click="handlePlayback(row)"
-            :disabled="!row.status"
+            :disabled="row.status !== 'ONLINE'"
           >
-            录像回
+            播放
           </el-button>
         </template>
       </el-table-column>
     </el-table>
-
-    <!-- 分页 -->
-    <el-pagination
-      v-model:current-page="queryParams.pageNum"
-      v-model:page-size="queryParams.pageSize"
-      :page-sizes="[10, 20, 50, 100]"
-      :total="total"
-      layout="total, sizes, prev, pager, next, jumper"
-      class="pagination"
-      @size-change="getList"
-      @current-change="getList"
-    />
   </div>
 </template>
 
 <script setup lang="ts">
-import { ref, reactive, onMounted } from "vue";
-import { useRoute, useRouter } from "vue-router";
-import {
-  Search,
-  Refresh,
-  ArrowLeft,
-  VideoPlay,
-  VideoCamera,
-} from "@element-plus/icons-vue";
-import { listChannel } from "@/api/camera";
-import type { CameraChannel } from "@/types";
-
-const route = useRoute();
-const router = useRouter();
+import { ref, computed, onMounted } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { ArrowLeft, VideoPlay } from '@element-plus/icons-vue'
+import { getCamera } from '@/api/camera'
+import type { CameraDTO, ChannelDTO } from '@/types'
 
-const deviceId = route.params.deviceId as string;
-const loading = ref(false);
-const channelList = ref<CameraChannel[]>([]);
-const total = ref(0);
+const route = useRoute()
+const router = useRouter()
 
-const queryParams = reactive({
-  pageNum: 1,
-  pageSize: 10,
-  name: "",
-  status: undefined as boolean | undefined,
-});
+const cameraId = route.params.cameraId as string
+const loading = ref(false)
+const cameraInfo = ref<CameraDTO | null>(null)
 
-function getPtzType(type: number | undefined): string {
-  const types: Record<number, string> = {
-    0: "未知",
-    1: "球机",
-    2: "半球",
-    3: "固定枪机",
-    4: "遥控枪机",
-  };
-  return types[type || 0] || "未知";
-}
+const filteredChannels = computed<ChannelDTO[]>(() => {
+  return cameraInfo.value?.channels || []
+})
 
 async function getList() {
-  loading.value = true;
+  loading.value = true
   try {
-    const res = await listChannel(deviceId, queryParams);
+    const res = await getCamera(cameraId)
     if (res.code === 200) {
-      channelList.value = res.data.rows;
-      total.value = res.data.total;
+      cameraInfo.value = res.data
     }
   } finally {
-    loading.value = false;
+    loading.value = false
   }
 }
 
-function handleQuery() {
-  queryParams.pageNum = 1;
-  getList();
-}
-
-function resetQuery() {
-  queryParams.pageNum = 1;
-  queryParams.name = "";
-  queryParams.status = undefined;
-  getList();
-}
-
 function goBack() {
-  router.push("/camera");
-}
-
-function handlePlay(row: CameraChannel) {
-  router.push(`/camera/video/${deviceId}/${row.channelId}?mode=live`);
+  router.push('/camera')
 }
 
-function handlePlayback(row: CameraChannel) {
-  router.push(`/camera/video/${deviceId}/${row.channelId}?mode=playback`);
+function handlePlay(row: ChannelDTO) {
+  router.push(`/camera/video/${cameraId}/${row.channelId}?mode=live`)
 }
 
 onMounted(() => {
-  getList();
-});
+  getList()
+})
 </script>
 
 <style lang="scss" scoped>
@@ -219,15 +135,10 @@ onMounted(() => {
   }
 }
 
-.search-form {
+.camera-info {
   margin-bottom: 20px;
   padding: 20px;
   background-color: #fff;
   border-radius: 4px;
 }
-
-.pagination {
-  margin-top: 20px;
-  justify-content: flex-end;
-}
 </style>

+ 337 - 180
src/views/camera/index.vue

@@ -3,30 +3,30 @@
     <!-- 搜索区域 -->
     <div class="search-form">
       <el-form :model="queryParams" inline>
-        <el-form-item label="设备ID">
-          <el-input
-            v-model="queryParams.deviceId"
-            placeholder="请输入设备ID"
-            clearable
-            @keyup.enter="handleQuery"
-          />
-        </el-form-item>
-        <el-form-item label="设备名称">
-          <el-input
-            v-model="queryParams.deviceName"
-            placeholder="请输入设备名称"
+        <el-form-item label="机器">
+          <el-select
+            v-model="queryParams.machineId"
+            placeholder="请选择机器"
             clearable
-            @keyup.enter="handleQuery"
-          />
+            @change="handleQuery"
+          >
+            <el-option
+              v-for="machine in machineList"
+              :key="machine.machineId"
+              :label="machine.name"
+              :value="machine.machineId"
+            />
+          </el-select>
         </el-form-item>
-        <el-form-item label="在线状态">
+        <el-form-item label="状态">
           <el-select
-            v-model="queryParams.online"
+            v-model="queryParams.status"
             placeholder="请选择"
             clearable
+            @change="handleQuery"
           >
-            <el-option label="在线" :value="true" />
-            <el-option label="离线" :value="false" />
+            <el-option label="在线" value="ONLINE" />
+            <el-option label="离线" value="OFFLINE" />
           </el-select>
         </el-form-item>
         <el-form-item>
@@ -41,7 +41,7 @@
     <!-- 操作按钮 -->
     <div class="table-actions">
       <el-button type="primary" :icon="Plus" @click="handleAdd"
-        >新增设备</el-button
+        >新增摄像头</el-button
       >
       <el-button type="success" :icon="Refresh" @click="getList"
         >刷新列表</el-button
@@ -49,52 +49,61 @@
     </div>
 
     <!-- 数据表格 -->
-    <el-table v-loading="loading" :data="deviceList" border>
+    <el-table v-loading="loading" :data="filteredList" border>
       <el-table-column type="index" label="序号" width="60" align="center" />
       <el-table-column
-        prop="deviceId"
-        label="设备ID"
-        min-width="180"
+        prop="cameraId"
+        label="摄像头ID"
+        min-width="120"
         show-overflow-tooltip
       />
       <el-table-column
-        prop="deviceName"
-        label="设备名称"
-        min-width="150"
+        prop="name"
+        label="名称"
+        min-width="120"
         show-overflow-tooltip
       />
       <el-table-column
-        prop="manufacturer"
-        label="厂商"
-        min-width="120"
+        prop="ip"
+        label="IP地址"
+        min-width="130"
         show-overflow-tooltip
       />
+      <el-table-column prop="port" label="端口" width="80" align="center" />
       <el-table-column
-        prop="hostAddress"
-        label="地址"
-        min-width="150"
+        prop="brand"
+        label="品牌"
+        min-width="100"
         show-overflow-tooltip
       />
       <el-table-column
-        prop="channelCount"
-        label="通道数"
-        width="80"
-        align="center"
+        prop="machineName"
+        label="所属机器"
+        min-width="100"
+        show-overflow-tooltip
       />
-      <el-table-column prop="online" label="状态" width="80" align="center">
+      <el-table-column prop="capability" label="能力" width="100" align="center">
         <template #default="{ row }">
-          <el-tag :type="row.online ? 'success' : 'danger'">
-            {{ row.online ? "在线" : "离线" }}
+          <el-tag :type="row.capability === 'ptz_enabled' ? 'success' : 'info'">
+            {{ row.capability === 'ptz_enabled' ? 'PTZ' : '仅切换' }}
           </el-tag>
         </template>
       </el-table-column>
-      <el-table-column
-        prop="registerTime"
-        label="注册时间"
-        width="160"
-        align="center"
-      />
-      <el-table-column label="操作" width="280" align="center" fixed="right">
+      <el-table-column prop="status" label="状态" width="80" align="center">
+        <template #default="{ row }">
+          <el-tag :type="row.status === 'ONLINE' ? 'success' : 'danger'">
+            {{ row.status === 'ONLINE' ? '在线' : '离线' }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column prop="enabled" label="启用" width="80" align="center">
+        <template #default="{ row }">
+          <el-tag :type="row.enabled ? 'success' : 'info'">
+            {{ row.enabled ? '是' : '否' }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="260" align="center" fixed="right">
         <template #default="{ row }">
           <el-button
             type="primary"
@@ -104,11 +113,11 @@
             >通道</el-button
           >
           <el-button
-            type="primary"
+            type="success"
             link
-            :icon="Refresh"
-            @click="handleSync(row)"
-            >同步</el-button
+            :icon="Connection"
+            @click="handleCheck(row)"
+            >检测</el-button
           >
           <el-button type="primary" link :icon="Edit" @click="handleEdit(row)"
             >编辑</el-button
@@ -124,55 +133,84 @@
       </el-table-column>
     </el-table>
 
-    <!-- 分页 -->
-    <el-pagination
-      v-model:current-page="queryParams.pageNum"
-      v-model:page-size="queryParams.pageSize"
-      :page-sizes="[10, 20, 50, 100]"
-      :total="total"
-      layout="total, sizes, prev, pager, next, jumper"
-      class="pagination"
-      @size-change="getList"
-      @current-change="getList"
-    />
-
     <!-- 新增/编辑弹窗 -->
     <el-dialog
       v-model="dialogVisible"
       :title="dialogTitle"
-      width="600px"
+      width="650px"
       destroy-on-close
     >
       <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
-        <el-form-item label="设备ID" prop="deviceId">
+        <el-form-item label="摄像头ID" prop="cameraId">
           <el-input
-            v-model="form.deviceId"
-            placeholder="请输入设备ID"
-            :disabled="!!form.id"
+            v-model="form.cameraId"
+            placeholder="请输入摄像头ID"
+            :disabled="isEdit"
           />
         </el-form-item>
-        <el-form-item label="设备名称" prop="deviceName">
-          <el-input v-model="form.deviceName" placeholder="请输入设备名称" />
+        <el-form-item label="名称" prop="name">
+          <el-input v-model="form.name" placeholder="请输入名称" />
         </el-form-item>
-        <el-form-item label="密码" prop="password">
-          <el-input
-            v-model="form.password"
-            type="password"
-            placeholder="请输入密码"
-            show-password
-          />
-        </el-form-item>
-        <el-form-item label="传输协议" prop="transport">
-          <el-select v-model="form.transport" placeholder="请选择">
-            <el-option label="UDP" value="UDP" />
-            <el-option label="TCP-PASSIVE" value="TCP-PASSIVE" />
+        <el-row :gutter="20">
+          <el-col :span="16">
+            <el-form-item label="IP地址" prop="ip">
+              <el-input v-model="form.ip" placeholder="请输入IP地址" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="端口" prop="port">
+              <el-input-number v-model="form.port" :min="1" :max="65535" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="用户名" prop="username">
+              <el-input v-model="form.username" placeholder="请输入用户名" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="密码" prop="password">
+              <el-input
+                v-model="form.password"
+                type="password"
+                placeholder="请输入密码"
+                show-password
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="品牌" prop="brand">
+              <el-select v-model="form.brand" placeholder="请选择">
+                <el-option label="海康威视" value="hikvision" />
+                <el-option label="大华" value="dahua" />
+                <el-option label="其他" value="other" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="能力" prop="capability">
+              <el-select v-model="form.capability" placeholder="请选择">
+                <el-option label="仅切换" value="switch_only" />
+                <el-option label="支持PTZ" value="ptz_enabled" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item label="所属机器" prop="machineId">
+          <el-select v-model="form.machineId" placeholder="请选择机器" clearable>
+            <el-option
+              v-for="machine in machineList"
+              :key="machine.machineId"
+              :label="machine.name"
+              :value="machine.machineId"
+            />
           </el-select>
         </el-form-item>
-        <el-form-item label="字符集" prop="charset">
-          <el-select v-model="form.charset" placeholder="请选择">
-            <el-option label="GB2312" value="GB2312" />
-            <el-option label="UTF-8" value="UTF-8" />
-          </el-select>
+        <el-form-item v-if="isEdit" label="启用状态">
+          <el-switch v-model="form.enabled" />
         </el-form-item>
       </el-form>
       <template #footer>
@@ -182,18 +220,45 @@
         >
       </template>
     </el-dialog>
+
+    <!-- 通道弹窗 -->
+    <el-dialog
+      v-model="channelDialogVisible"
+      title="通道列表"
+      width="700px"
+      destroy-on-close
+    >
+      <el-table :data="currentChannels" border>
+        <el-table-column prop="channelId" label="通道ID" min-width="100" />
+        <el-table-column prop="name" label="名称" min-width="100" />
+        <el-table-column prop="rtspUrl" label="RTSP地址" min-width="200" show-overflow-tooltip />
+        <el-table-column prop="defaultView" label="默认视角" width="90" align="center">
+          <template #default="{ row }">
+            <el-tag :type="row.defaultView ? 'success' : 'info'">
+              {{ row.defaultView ? '是' : '否' }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="status" label="状态" width="80" align="center">
+          <template #default="{ row }">
+            <el-tag :type="row.status === 'ONLINE' ? 'success' : 'danger'">
+              {{ row.status === 'ONLINE' ? '在线' : '离线' }}
+            </el-tag>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-dialog>
   </div>
 </template>
 
 <script setup lang="ts">
-import { ref, reactive, onMounted, computed } from "vue";
-import { useRouter } from "vue-router";
+import { ref, reactive, onMounted, computed } from 'vue'
 import {
   ElMessage,
   ElMessageBox,
   type FormInstance,
-  type FormRules,
-} from "element-plus";
+  type FormRules
+} from 'element-plus'
 import {
   Search,
   Refresh,
@@ -201,154 +266,251 @@ import {
   View,
   Edit,
   Delete,
-} from "@element-plus/icons-vue";
+  Connection
+} from '@element-plus/icons-vue'
 import {
-  listDevice,
-  addDevice,
-  updateDevice,
-  delDevice,
-  syncChannel,
-} from "@/api/camera";
-import type { CameraDevice } from "@/types";
+  adminListCameras,
+  adminAddCamera,
+  adminUpdateCamera,
+  adminDeleteCamera,
+  adminCheckCamera
+} from '@/api/camera'
+import { listMachines } from '@/api/machine'
+import type {
+  CameraInfoDTO,
+  ChannelInfoDTO,
+  CameraAddRequest,
+  CameraUpdateRequest,
+  MachineDTO
+} from '@/types'
 
-const router = useRouter();
-
-const loading = ref(false);
-const submitLoading = ref(false);
-const deviceList = ref<CameraDevice[]>([]);
-const total = ref(0);
-const dialogVisible = ref(false);
-const formRef = ref<FormInstance>();
+const loading = ref(false)
+const submitLoading = ref(false)
+const cameraList = ref<CameraInfoDTO[]>([])
+const machineList = ref<MachineDTO[]>([])
+const dialogVisible = ref(false)
+const channelDialogVisible = ref(false)
+const currentChannels = ref<ChannelInfoDTO[]>([])
+const formRef = ref<FormInstance>()
 
 const queryParams = reactive({
-  pageNum: 1,
-  pageSize: 10,
-  deviceId: "",
-  deviceName: "",
-  online: undefined as boolean | undefined,
-});
+  machineId: '',
+  status: '' as '' | 'ONLINE' | 'OFFLINE'
+})
+
+const form = reactive<{
+  id?: number
+  cameraId: string
+  name: string
+  ip: string
+  port: number
+  username: string
+  password: string
+  brand: string
+  capability: 'switch_only' | 'ptz_enabled'
+  machineId: string
+  enabled: boolean
+}>({
+  cameraId: '',
+  name: '',
+  ip: '',
+  port: 80,
+  username: 'admin',
+  password: '',
+  brand: 'hikvision',
+  capability: 'switch_only',
+  machineId: '',
+  enabled: true
+})
 
-const form = reactive<Partial<CameraDevice> & { id?: string }>({
-  deviceId: "",
-  deviceName: "",
-  password: "",
-  transport: "UDP",
-  charset: "GB2312",
-});
+const isEdit = computed(() => !!form.id)
+const dialogTitle = computed(() => (isEdit.value ? '编辑摄像头' : '新增摄像头'))
 
-const dialogTitle = computed(() => (form.id ? "编辑设备" : "新增设备"));
+const filteredList = computed(() => {
+  let list = cameraList.value
+  if (queryParams.status) {
+    list = list.filter(item => item.status === queryParams.status)
+  }
+  return list
+})
 
 const rules: FormRules = {
-  deviceId: [{ required: true, message: "请输入设备ID", trigger: "blur" }],
-  deviceName: [{ required: true, message: "请输入设备名称", trigger: "blur" }],
-};
+  cameraId: [{ required: true, message: '请输入摄像头ID', trigger: 'blur' }],
+  name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
+  ip: [
+    { required: true, message: '请输入IP地址', trigger: 'blur' },
+    {
+      pattern: /^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$/,
+      message: '请输入正确的IP地址',
+      trigger: 'blur'
+    }
+  ]
+}
+
+async function getMachines() {
+  try {
+    const res = await listMachines()
+    if (res.code === 200) {
+      machineList.value = res.data
+    }
+  } catch (error) {
+    console.error('获取机器列表失败', error)
+  }
+}
 
 async function getList() {
-  loading.value = true;
+  loading.value = true
   try {
-    const res = await listDevice(queryParams);
+    const res = await adminListCameras(queryParams.machineId || undefined)
     if (res.code === 200) {
-      deviceList.value = res.data.rows;
-      total.value = res.data.total;
+      cameraList.value = res.data
     }
   } finally {
-    loading.value = false;
+    loading.value = false
   }
 }
 
 function handleQuery() {
-  queryParams.pageNum = 1;
-  getList();
+  getList()
 }
 
 function resetQuery() {
-  queryParams.pageNum = 1;
-  queryParams.deviceId = "";
-  queryParams.deviceName = "";
-  queryParams.online = undefined;
-  getList();
+  queryParams.machineId = ''
+  queryParams.status = ''
+  getList()
 }
 
 function handleAdd() {
   Object.assign(form, {
     id: undefined,
-    deviceId: "",
-    deviceName: "",
-    password: "",
-    transport: "UDP",
-    charset: "GB2312",
-  });
-  dialogVisible.value = true;
+    cameraId: '',
+    name: '',
+    ip: '',
+    port: 80,
+    username: 'admin',
+    password: '',
+    brand: 'hikvision',
+    capability: 'switch_only',
+    machineId: '',
+    enabled: true
+  })
+  dialogVisible.value = true
 }
 
-function handleEdit(row: CameraDevice) {
+function handleEdit(row: CameraInfoDTO) {
   Object.assign(form, {
-    id: row.deviceId,
-    ...row,
-  });
-  dialogVisible.value = true;
+    id: row.id,
+    cameraId: row.cameraId,
+    name: row.name,
+    ip: row.ip,
+    port: row.port,
+    username: row.username,
+    password: '',
+    brand: row.brand,
+    capability: row.capability,
+    machineId: row.machineId || '',
+    enabled: row.enabled
+  })
+  dialogVisible.value = true
 }
 
-function handleChannel(row: CameraDevice) {
-  router.push(`/camera/channel/${row.deviceId}`);
+function handleChannel(row: CameraInfoDTO) {
+  currentChannels.value = row.channels || []
+  channelDialogVisible.value = true
 }
 
-async function handleSync(row: CameraDevice) {
+async function handleCheck(row: CameraInfoDTO) {
   try {
-    const res = await syncChannel(row.deviceId);
+    const res = await adminCheckCamera(row.id)
     if (res.code === 200) {
-      ElMessage.success("同步成功");
-      getList();
+      if (res.data) {
+        ElMessage.success('摄像头连接正常')
+      } else {
+        ElMessage.warning('摄像头连接失败')
+      }
     }
   } catch (error) {
-    console.error("同步失败", error);
+    console.error('检测失败', error)
   }
 }
 
-async function handleDelete(row: CameraDevice) {
+async function handleDelete(row: CameraInfoDTO) {
   try {
     await ElMessageBox.confirm(
-      `确定要删除设备 "${row.deviceName}" 吗?`,
-      "提示",
+      `确定要删除摄像头 "${row.name}" 吗?`,
+      '提示',
       {
-        type: "warning",
+        type: 'warning'
       }
-    );
-    const res = await delDevice(row.deviceId);
+    )
+    const res = await adminDeleteCamera(row.id)
     if (res.code === 200) {
-      ElMessage.success("删除成功");
-      getList();
+      ElMessage.success('删除成功')
+      getList()
     }
   } catch (error) {
-    if (error !== "cancel") {
-      console.error("删除失败", error);
+    if (error !== 'cancel') {
+      console.error('删除失败', error)
     }
   }
 }
 
 async function handleSubmit() {
-  if (!formRef.value) return;
+  if (!formRef.value) return
 
   await formRef.value.validate(async (valid) => {
     if (valid) {
-      submitLoading.value = true;
+      submitLoading.value = true
       try {
-        const res = form.id ? await updateDevice(form) : await addDevice(form);
-        if (res.code === 200) {
-          ElMessage.success(form.id ? "修改成功" : "新增成功");
-          dialogVisible.value = false;
-          getList();
+        if (isEdit.value) {
+          const updateData: CameraUpdateRequest = {
+            id: form.id!,
+            name: form.name,
+            ip: form.ip,
+            port: form.port,
+            username: form.username,
+            password: form.password || undefined,
+            brand: form.brand,
+            capability: form.capability,
+            machineId: form.machineId || undefined,
+            enabled: form.enabled
+          }
+          const res = await adminUpdateCamera(updateData)
+          if (res.code === 200) {
+            ElMessage.success('修改成功')
+            dialogVisible.value = false
+            getList()
+          }
+        } else {
+          const addData: CameraAddRequest = {
+            cameraId: form.cameraId,
+            name: form.name,
+            ip: form.ip,
+            port: form.port,
+            username: form.username,
+            password: form.password || undefined,
+            brand: form.brand,
+            capability: form.capability,
+            machineId: form.machineId || undefined
+          }
+          const res = await adminAddCamera(addData)
+          if (res.code === 200) {
+            ElMessage.success('新增成功')
+            dialogVisible.value = false
+            getList()
+          }
         }
       } finally {
-        submitLoading.value = false;
+        submitLoading.value = false
       }
     }
-  });
+  })
 }
 
 onMounted(() => {
-  getList();
-});
+  getMachines()
+  getList()
+})
 </script>
 
 <style lang="scss" scoped>
@@ -366,9 +528,4 @@ onMounted(() => {
 .table-actions {
   margin-bottom: 15px;
 }
-
-.pagination {
-  margin-top: 20px;
-  justify-content: flex-end;
-}
 </style>

+ 105 - 138
src/views/dashboard/index.vue

@@ -3,22 +3,21 @@
     <!-- 统计卡片 -->
     <el-row :gutter="20" class="stat-cards">
       <el-col :xs="24" :sm="12" :lg="6">
-        <el-card class="stat-card users" shadow="hover">
+        <el-card class="stat-card machines" shadow="hover">
           <div class="stat-content">
             <div class="stat-icon">
-              <el-icon :size="40"><User /></el-icon>
+              <el-icon :size="40"><Monitor /></el-icon>
             </div>
             <div class="stat-info">
               <div class="stat-value">
-                {{ dashboardData?.users?.total || 0 }}
+                {{ dashboardData?.machineTotal || 0 }}
               </div>
-              <div class="stat-label">用户总数</div>
+              <div class="stat-label">机器总数</div>
             </div>
           </div>
           <div class="stat-footer">
-            <span>管理员: {{ dashboardData?.users?.admin_count || 0 }}</span>
-            <span>操作员: {{ dashboardData?.users?.operator_count || 0 }}</span>
-            <span>观察者: {{ dashboardData?.users?.viewer_count || 0 }}</span>
+            <span class="online">已启用: {{ dashboardData?.machineEnabled || 0 }}</span>
+            <span class="offline">已禁用: {{ (dashboardData?.machineTotal || 0) - (dashboardData?.machineEnabled || 0) }}</span>
           </div>
         </el-card>
       </el-col>
@@ -31,159 +30,115 @@
             </div>
             <div class="stat-info">
               <div class="stat-value">
-                {{ dashboardData?.cameras?.total || 0 }}
+                {{ dashboardData?.cameraTotal || 0 }}
               </div>
               <div class="stat-label">摄像头总数</div>
             </div>
           </div>
           <div class="stat-footer">
-            <span class="online"
-              >在线: {{ dashboardData?.cameras?.online_count || 0 }}</span
-            >
-            <span class="offline"
-              >离线: {{ dashboardData?.cameras?.offline_count || 0 }}</span
-            >
-            <span class="error"
-              >异常: {{ dashboardData?.cameras?.error_count || 0 }}</span
-            >
+            <span class="online">在线: {{ dashboardData?.cameraOnline || 0 }}</span>
+            <span class="offline">离线: {{ dashboardData?.cameraOffline || 0 }}</span>
           </div>
         </el-card>
       </el-col>
 
       <el-col :xs="24" :sm="12" :lg="6">
-        <el-card class="stat-card videos" shadow="hover">
+        <el-card class="stat-card channels" shadow="hover">
           <div class="stat-content">
             <div class="stat-icon">
-              <el-icon :size="40"><Film /></el-icon>
+              <el-icon :size="40"><Connection /></el-icon>
             </div>
             <div class="stat-info">
               <div class="stat-value">
-                {{ dashboardData?.videos?.total || 0 }}
+                {{ dashboardData?.channelTotal || 0 }}
               </div>
-              <div class="stat-label">视频总数</div>
+              <div class="stat-label">通道总数</div>
             </div>
           </div>
           <div class="stat-footer">
-            <span>总播放: {{ dashboardData?.videos?.total_views || 0 }}</span>
-            <span
-              >总时长:
-              {{
-                formatDuration(dashboardData?.videos?.total_duration || 0)
-              }}</span
-            >
+            <span>可用通道数量</span>
           </div>
         </el-card>
       </el-col>
 
       <el-col :xs="24" :sm="12" :lg="6">
-        <el-card class="stat-card live" shadow="hover">
+        <el-card class="stat-card status" shadow="hover">
           <div class="stat-content">
             <div class="stat-icon">
-              <el-icon :size="40"><VideoCameraFilled /></el-icon>
+              <el-icon :size="40"><CircleCheck /></el-icon>
             </div>
             <div class="stat-info">
-              <div class="stat-value">
-                {{ dashboardData?.live_sessions?.live_count || 0 }}
+              <div class="stat-value online-rate">
+                {{ onlineRate }}%
               </div>
-              <div class="stat-label">正在直播</div>
+              <div class="stat-label">摄像头在线率</div>
             </div>
           </div>
           <div class="stat-footer">
-            <span
-              >会话总数: {{ dashboardData?.live_sessions?.total || 0 }}</span
-            >
-            <span
-              >峰值观众:
-              {{ dashboardData?.live_sessions?.max_peak_viewers || 0 }}</span
-            >
+            <span>系统运行正常</span>
           </div>
         </el-card>
       </el-col>
     </el-row>
 
-    <!-- 今日统计 -->
-    <el-row :gutter="20" class="today-stats">
+    <!-- 快捷操作 -->
+    <el-row :gutter="20" class="quick-actions">
       <el-col :span="24">
         <el-card shadow="hover">
           <template #header>
             <div class="card-header">
-              <span>今日统计</span>
+              <span>快捷操作</span>
               <el-button
                 type="primary"
                 link
                 :icon="Refresh"
                 @click="loadDashboardData"
-                >刷新</el-button
-              >
+              >刷新数据</el-button>
             </div>
           </template>
-          <el-row :gutter="40">
-            <el-col :xs="24" :sm="8">
-              <el-statistic
-                title="今日观看次数"
-                :value="dashboardData?.today?.today_views || 0"
-              >
-                <template #prefix>
-                  <el-icon><View /></el-icon>
-                </template>
-              </el-statistic>
+          <el-row :gutter="20">
+            <el-col :xs="24" :sm="8" :md="6">
+              <el-button class="action-btn" @click="goTo('/camera')">
+                <el-icon :size="24"><VideoCamera /></el-icon>
+                <span>摄像头管理</span>
+              </el-button>
             </el-col>
-            <el-col :xs="24" :sm="8">
-              <el-statistic
-                title="今日新增用户"
-                :value="dashboardData?.today?.today_new_users || 0"
-              >
-                <template #prefix>
-                  <el-icon><UserFilled /></el-icon>
-                </template>
-              </el-statistic>
+            <el-col :xs="24" :sm="8" :md="6">
+              <el-button class="action-btn" @click="goTo('/machine')">
+                <el-icon :size="24"><Monitor /></el-icon>
+                <span>机器管理</span>
+              </el-button>
             </el-col>
-            <el-col :xs="24" :sm="8">
-              <el-statistic
-                title="今日直播场次"
-                :value="dashboardData?.today?.today_sessions || 0"
-              >
-                <template #prefix>
-                  <el-icon><Videocamera /></el-icon>
-                </template>
-              </el-statistic>
+            <el-col :xs="24" :sm="8" :md="6">
+              <el-button class="action-btn" @click="goTo('/stream-test')">
+                <el-icon :size="24"><VideoPlay /></el-icon>
+                <span>Stream 测试</span>
+              </el-button>
             </el-col>
           </el-row>
         </el-card>
       </el-col>
     </el-row>
 
-    <!-- 系统状态 -->
-    <el-row :gutter="20" class="system-status">
+    <!-- 系统信息 -->
+    <el-row :gutter="20" class="system-info">
       <el-col :span="24">
         <el-card shadow="hover">
           <template #header>
             <div class="card-header">
-              <span>系统状态</span>
+              <span>系统信息</span>
             </div>
           </template>
           <el-descriptions :column="3" border>
-            <el-descriptions-item label="数据库状态">
-              <el-tag
-                :type="
-                  dashboardData?.system?.database === 'healthy'
-                    ? 'success'
-                    : 'danger'
-                "
-              >
-                {{
-                  dashboardData?.system?.database === "healthy"
-                    ? "正常"
-                    : "异常"
-                }}
-              </el-tag>
-            </el-descriptions-item>
-            <el-descriptions-item label="API 版本">
-              {{ dashboardData?.system?.api_version || "-" }}
+            <el-descriptions-item label="系统状态">
+              <el-tag type="success">正常</el-tag>
             </el-descriptions-item>
             <el-descriptions-item label="数据更新时间">
               {{ lastUpdateTime }}
             </el-descriptions-item>
+            <el-descriptions-item label="版本">
+              v1.0.0
+            </el-descriptions-item>
           </el-descriptions>
         </el-card>
       </el-col>
@@ -192,59 +147,60 @@
 </template>
 
 <script setup lang="ts">
-import { ref, onMounted, computed } from "vue";
-import { ElMessage } from "element-plus";
+import { ref, onMounted, computed } from 'vue'
+import { useRouter } from 'vue-router'
+import { ElMessage } from 'element-plus'
 import {
-  User,
+  Monitor,
   VideoCamera,
-  Film,
-  VideoCameraFilled,
+  Connection,
+  CircleCheck,
   Refresh,
-  View,
-  UserFilled,
-} from "@element-plus/icons-vue";
-import { getDashboardStats, type DashboardData } from "@/api/stats";
+  VideoPlay
+} from '@element-plus/icons-vue'
+import { getDashboardStats } from '@/api/stats'
+import type { DashboardStatsDTO } from '@/types'
 
-const loading = ref(false);
-const dashboardData = ref<DashboardData | null>(null);
-const lastUpdate = ref<Date | null>(null);
+const router = useRouter()
+const loading = ref(false)
+const dashboardData = ref<DashboardStatsDTO | null>(null)
+const lastUpdate = ref<Date | null>(null)
 
 const lastUpdateTime = computed(() => {
-  if (!lastUpdate.value) return "-";
-  return lastUpdate.value.toLocaleString("zh-CN");
-});
-
-function formatDuration(seconds: number): string {
-  if (!seconds) return "0秒";
-  const hours = Math.floor(seconds / 3600);
-  const minutes = Math.floor((seconds % 3600) / 60);
-  if (hours > 0) {
-    return `${hours}小时${minutes}分钟`;
-  }
-  return `${minutes}分钟`;
+  if (!lastUpdate.value) return '-'
+  return lastUpdate.value.toLocaleString('zh-CN')
+})
+
+const onlineRate = computed(() => {
+  if (!dashboardData.value || !dashboardData.value.cameraTotal) return 0
+  return Math.round((dashboardData.value.cameraOnline / dashboardData.value.cameraTotal) * 100)
+})
+
+function goTo(path: string) {
+  router.push(path)
 }
 
 async function loadDashboardData() {
-  loading.value = true;
+  loading.value = true
   try {
-    const res = await getDashboardStats();
+    const res = await getDashboardStats()
     if (res.code === 200) {
-      dashboardData.value = res.data;
-      lastUpdate.value = new Date();
+      dashboardData.value = res.data
+      lastUpdate.value = new Date()
     } else {
-      ElMessage.error(res.msg || "获取统计数据失败");
+      ElMessage.error(res.message || '获取统计数据失败')
     }
   } catch (error) {
-    console.error("Failed to load dashboard data:", error);
-    ElMessage.error("获取统计数据失败");
+    console.error('Failed to load dashboard data:', error)
+    ElMessage.error('获取统计数据失败')
   } finally {
-    loading.value = false;
+    loading.value = false
   }
 }
 
 onMounted(() => {
-  loadDashboardData();
-});
+  loadDashboardData()
+})
 </script>
 
 <style lang="scss" scoped>
@@ -287,6 +243,10 @@ onMounted(() => {
     font-size: 32px;
     font-weight: bold;
     line-height: 1.2;
+
+    &.online-rate {
+      color: #67c23a;
+    }
   }
 
   .stat-label {
@@ -309,12 +269,9 @@ onMounted(() => {
     .offline {
       color: #909399;
     }
-    .error {
-      color: #f56c6c;
-    }
   }
 
-  &.users .stat-icon {
+  &.machines .stat-icon {
     background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
     color: #fff;
   }
@@ -324,27 +281,37 @@ onMounted(() => {
     color: #fff;
   }
 
-  &.videos .stat-icon {
+  &.channels .stat-icon {
     background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
     color: #fff;
   }
 
-  &.live .stat-icon {
+  &.status .stat-icon {
     background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
     color: #fff;
   }
 }
 
-.today-stats {
+.quick-actions {
   margin-bottom: 20px;
 
-  .el-statistic {
-    text-align: center;
-    padding: 20px 0;
+  .action-btn {
+    width: 100%;
+    height: 80px;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    gap: 8px;
+    margin-bottom: 10px;
+
+    span {
+      font-size: 14px;
+    }
   }
 }
 
-.system-status {
+.system-info {
   margin-bottom: 20px;
 }
 

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

@@ -115,7 +115,7 @@ async function handleLogin() {
           const redirect = (route.query.redirect as string) || '/'
           router.push(redirect)
         } else {
-          ElMessage.error(res.msg || '登录失败')
+          ElMessage.error(res.message || '登录失败')
           if (captchaEnabled.value) {
             getCode()
           }

+ 191 - 284
src/views/stats/index.vue

@@ -1,17 +1,13 @@
 <template>
   <div class="stats-container">
-    <!-- 时间范围选择 -->
+    <!-- 刷新按钮 -->
     <el-card class="filter-card" shadow="hover">
       <el-form inline>
-        <el-form-item label="统计周期">
-          <el-select v-model="days" @change="loadStats">
-            <el-option label="最近 7 天" :value="7" />
-            <el-option label="最近 14 天" :value="14" />
-            <el-option label="最近 30 天" :value="30" />
-          </el-select>
+        <el-form-item label="统计概览">
+          <span class="update-time">更新时间: {{ lastUpdateTime }}</span>
         </el-form-item>
         <el-form-item>
-          <el-button type="primary" :icon="Refresh" @click="loadStats">刷新</el-button>
+          <el-button type="primary" :icon="Refresh" :loading="loading" @click="loadStats">刷新</el-button>
         </el-form-item>
       </el-form>
     </el-card>
@@ -19,253 +15,170 @@
     <!-- 总体统计 -->
     <el-row :gutter="20" class="summary-cards">
       <el-col :xs="12" :sm="6">
-        <el-card shadow="hover" class="summary-card">
-          <el-statistic title="总观看次数" :value="statsData?.total?.total_views || 0">
-            <template #prefix><el-icon><View /></el-icon></template>
+        <el-card shadow="hover" class="summary-card machines">
+          <el-statistic title="机器总数" :value="statsData?.machineTotal || 0">
+            <template #prefix><el-icon><Monitor /></el-icon></template>
           </el-statistic>
+          <div class="stat-detail">
+            已启用: {{ statsData?.machineEnabled || 0 }}
+          </div>
         </el-card>
       </el-col>
       <el-col :xs="12" :sm="6">
-        <el-card shadow="hover" class="summary-card">
-          <el-statistic title="独立用户" :value="statsData?.total?.unique_users || 0">
-            <template #prefix><el-icon><User /></el-icon></template>
+        <el-card shadow="hover" class="summary-card cameras">
+          <el-statistic title="摄像头总数" :value="statsData?.cameraTotal || 0">
+            <template #prefix><el-icon><VideoCamera /></el-icon></template>
           </el-statistic>
+          <div class="stat-detail">
+            <span class="online">在线: {{ statsData?.cameraOnline || 0 }}</span>
+            <span class="offline">离线: {{ statsData?.cameraOffline || 0 }}</span>
+          </div>
         </el-card>
       </el-col>
       <el-col :xs="12" :sm="6">
-        <el-card shadow="hover" class="summary-card">
-          <el-statistic title="独立 IP" :value="statsData?.total?.unique_ips || 0">
+        <el-card shadow="hover" class="summary-card channels">
+          <el-statistic title="通道总数" :value="statsData?.channelTotal || 0">
             <template #prefix><el-icon><Connection /></el-icon></template>
           </el-statistic>
+          <div class="stat-detail">
+            可用通道数量
+          </div>
         </el-card>
       </el-col>
       <el-col :xs="12" :sm="6">
-        <el-card shadow="hover" class="summary-card">
-          <el-statistic title="总观看时长" :value="formatDuration(statsData?.total?.total_watch_time || 0)">
-            <template #prefix><el-icon><Timer /></el-icon></template>
+        <el-card shadow="hover" class="summary-card rate">
+          <el-statistic title="摄像头在线率" :value="onlineRate" suffix="%">
+            <template #prefix><el-icon><CircleCheck /></el-icon></template>
           </el-statistic>
+          <div class="stat-detail">
+            系统运行状态
+          </div>
         </el-card>
       </el-col>
     </el-row>
 
-    <!-- 每日趋势 -->
-    <el-card class="trend-card" shadow="hover">
+    <!-- 统计图表占位 -->
+    <el-card class="chart-card" shadow="hover">
       <template #header>
         <div class="card-header">
-          <span>每日观看趋势</span>
+          <span>统计详情</span>
         </div>
       </template>
-      <div class="trend-chart" v-if="statsData?.daily_trend?.length">
-        <div class="chart-bars">
-          <div
-            v-for="item in statsData.daily_trend"
-            :key="item.date"
-            class="bar-item"
-          >
-            <div class="bar-wrapper">
-              <div
-                class="bar"
-                :style="{ height: getBarHeight(item.views) + '%' }"
-                :title="`${item.views} 次观看`"
-              ></div>
-            </div>
-            <div class="bar-label">{{ formatDate(item.date) }}</div>
-            <div class="bar-value">{{ item.views }}</div>
-          </div>
-        </div>
-      </div>
-      <el-empty v-else description="暂无数据" />
+      <el-empty description="详细统计功能开发中...">
+        <template #image>
+          <el-icon :size="60" style="color: #c0c4cc"><TrendCharts /></el-icon>
+        </template>
+      </el-empty>
     </el-card>
 
+    <!-- 设备状态概览 -->
     <el-row :gutter="20">
-      <!-- 热门视频 -->
       <el-col :xs="24" :lg="12">
-        <el-card class="rank-card" shadow="hover">
+        <el-card class="status-card" shadow="hover">
           <template #header>
             <div class="card-header">
-              <span>热门视频 TOP 10</span>
+              <span>摄像头状态分布</span>
             </div>
           </template>
-          <el-table :data="statsData?.top_videos || []" stripe size="small">
-            <el-table-column type="index" label="排名" width="60" align="center">
-              <template #default="{ $index }">
-                <el-tag :type="getRankType($index)" size="small">{{ $index + 1 }}</el-tag>
-              </template>
-            </el-table-column>
-            <el-table-column prop="title" label="视频标题" min-width="150" show-overflow-tooltip />
-            <el-table-column prop="recent_views" label="近期观看" width="100" align="center" />
-            <el-table-column prop="view_count" label="总观看" width="80" align="center" />
-          </el-table>
+          <div class="status-chart">
+            <div class="status-item online">
+              <div class="status-bar" :style="{ width: getStatusWidth('online') + '%' }"></div>
+              <div class="status-info">
+                <span class="status-label">在线</span>
+                <span class="status-value">{{ statsData?.cameraOnline || 0 }}</span>
+              </div>
+            </div>
+            <div class="status-item offline">
+              <div class="status-bar" :style="{ width: getStatusWidth('offline') + '%' }"></div>
+              <div class="status-info">
+                <span class="status-label">离线</span>
+                <span class="status-value">{{ statsData?.cameraOffline || 0 }}</span>
+              </div>
+            </div>
+          </div>
         </el-card>
       </el-col>
 
-      <!-- 热门直播 -->
       <el-col :xs="24" :lg="12">
-        <el-card class="rank-card" shadow="hover">
+        <el-card class="status-card" shadow="hover">
           <template #header>
             <div class="card-header">
-              <span>热门直播 TOP 10</span>
+              <span>机器状态分布</span>
             </div>
           </template>
-          <el-table :data="statsData?.top_sessions || []" stripe size="small">
-            <el-table-column type="index" label="排名" width="60" align="center">
-              <template #default="{ $index }">
-                <el-tag :type="getRankType($index)" size="small">{{ $index + 1 }}</el-tag>
-              </template>
-            </el-table-column>
-            <el-table-column prop="camera_name" label="摄像头" min-width="120" show-overflow-tooltip />
-            <el-table-column prop="total_views" label="观看次数" width="100" align="center" />
-            <el-table-column prop="peak_viewers" label="峰值观众" width="100" align="center" />
-          </el-table>
+          <div class="status-chart">
+            <div class="status-item enabled">
+              <div class="status-bar" :style="{ width: getMachineStatusWidth('enabled') + '%' }"></div>
+              <div class="status-info">
+                <span class="status-label">已启用</span>
+                <span class="status-value">{{ statsData?.machineEnabled || 0 }}</span>
+              </div>
+            </div>
+            <div class="status-item disabled">
+              <div class="status-bar" :style="{ width: getMachineStatusWidth('disabled') + '%' }"></div>
+              <div class="status-info">
+                <span class="status-label">已禁用</span>
+                <span class="status-value">{{ (statsData?.machineTotal || 0) - (statsData?.machineEnabled || 0) }}</span>
+              </div>
+            </div>
+          </div>
         </el-card>
       </el-col>
     </el-row>
-
-    <!-- 地区分布 -->
-    <el-card class="geo-card" shadow="hover">
-      <template #header>
-        <div class="card-header">
-          <span>观看地区分布</span>
-        </div>
-      </template>
-      <div class="geo-list" v-if="statsData?.geo_distribution?.length">
-        <div
-          v-for="(item, index) in statsData.geo_distribution.slice(0, 10)"
-          :key="item.country"
-          class="geo-item"
-        >
-          <div class="geo-info">
-            <span class="geo-rank">{{ index + 1 }}</span>
-            <span class="geo-country">{{ getCountryName(item.country) }}</span>
-          </div>
-          <div class="geo-bar-wrapper">
-            <el-progress
-              :percentage="getGeoPercentage(item.count)"
-              :stroke-width="16"
-              :show-text="false"
-            />
-          </div>
-          <span class="geo-count">{{ item.count }}</span>
-        </div>
-      </div>
-      <el-empty v-else description="暂无地区数据" />
-    </el-card>
   </div>
 </template>
 
 <script setup lang="ts">
 import { ref, computed, onMounted } from 'vue'
 import { ElMessage } from 'element-plus'
-import { Refresh, View, User, Connection, Timer } from '@element-plus/icons-vue'
-import { getStatsOverview } from '@/api/stats'
-
-interface StatsData {
-  period_days: number
-  total: {
-    total_views: number
-    unique_users: number
-    unique_ips: number
-    total_watch_time: number
-  }
-  daily_trend: Array<{
-    date: string
-    views: number
-    unique_users: number
-    watch_time: number
-  }>
-  top_videos: Array<{
-    id: string
-    title: string
-    view_count: number
-    recent_views: number
-  }>
-  top_sessions: Array<{
-    id: string
-    camera_name: string
-    peak_viewers: number
-    total_views: number
-  }>
-  geo_distribution: Array<{
-    country: string
-    count: number
-  }>
-}
+import {
+  Refresh,
+  Monitor,
+  VideoCamera,
+  Connection,
+  CircleCheck,
+  TrendCharts
+} from '@element-plus/icons-vue'
+import { getDashboardStats } from '@/api/stats'
+import type { DashboardStatsDTO } from '@/types'
 
 const loading = ref(false)
-const days = ref(7)
-const statsData = ref<StatsData | null>(null)
+const statsData = ref<DashboardStatsDTO | null>(null)
+const lastUpdate = ref<Date | null>(null)
 
-const maxViews = computed(() => {
-  if (!statsData.value?.daily_trend?.length) return 1
-  return Math.max(...statsData.value.daily_trend.map(d => d.views), 1)
+const lastUpdateTime = computed(() => {
+  if (!lastUpdate.value) return '-'
+  return lastUpdate.value.toLocaleString('zh-CN')
 })
 
-const maxGeoCount = computed(() => {
-  if (!statsData.value?.geo_distribution?.length) return 1
-  return Math.max(...statsData.value.geo_distribution.map(d => d.count), 1)
+const onlineRate = computed(() => {
+  if (!statsData.value || !statsData.value.cameraTotal) return 0
+  return Math.round((statsData.value.cameraOnline / statsData.value.cameraTotal) * 100)
 })
 
-function formatDuration(seconds: number): string {
-  if (!seconds) return '0'
-  const hours = Math.floor(seconds / 3600)
-  const minutes = Math.floor((seconds % 3600) / 60)
-  if (hours > 0) {
-    return `${hours}h ${minutes}m`
-  }
-  return `${minutes}m`
-}
-
-function formatDate(dateStr: string): string {
-  const date = new Date(dateStr)
-  return `${date.getMonth() + 1}/${date.getDate()}`
-}
-
-function getBarHeight(views: number): number {
-  return Math.max((views / maxViews.value) * 100, 5)
-}
-
-function getGeoPercentage(count: number): number {
-  return Math.round((count / maxGeoCount.value) * 100)
+function getStatusWidth(type: 'online' | 'offline'): number {
+  if (!statsData.value || !statsData.value.cameraTotal) return 0
+  const value = type === 'online' ? statsData.value.cameraOnline : statsData.value.cameraOffline
+  return Math.round((value / statsData.value.cameraTotal) * 100)
 }
 
-function getRankType(index: number): '' | 'success' | 'warning' | 'danger' | 'info' {
-  if (index === 0) return 'danger'
-  if (index === 1) return 'warning'
-  if (index === 2) return 'success'
-  return 'info'
-}
-
-function getCountryName(code: string): string {
-  const countryMap: Record<string, string> = {
-    CN: '中国',
-    US: '美国',
-    JP: '日本',
-    KR: '韩国',
-    HK: '香港',
-    TW: '台湾',
-    SG: '新加坡',
-    MY: '马来西亚',
-    TH: '泰国',
-    VN: '越南',
-    PH: '菲律宾',
-    ID: '印尼',
-    AU: '澳大利亚',
-    CA: '加拿大',
-    UK: '英国',
-    DE: '德国',
-    FR: '法国',
-  }
-  return countryMap[code] || code || '未知'
+function getMachineStatusWidth(type: 'enabled' | 'disabled'): number {
+  if (!statsData.value || !statsData.value.machineTotal) return 0
+  const value = type === 'enabled'
+    ? statsData.value.machineEnabled
+    : (statsData.value.machineTotal - statsData.value.machineEnabled)
+  return Math.round((value / statsData.value.machineTotal) * 100)
 }
 
 async function loadStats() {
   loading.value = true
   try {
-    const res = await getStatsOverview(days.value)
+    const res = await getDashboardStats()
     if (res.code === 200) {
       statsData.value = res.data
+      lastUpdate.value = new Date()
     } else {
-      ElMessage.error(res.msg || '获取统计数据失败')
+      ElMessage.error(res.message || '获取统计数据失败')
     }
   } catch (error) {
     console.error('Failed to load stats:', error)
@@ -287,6 +200,11 @@ onMounted(() => {
 
 .filter-card {
   margin-bottom: 20px;
+
+  .update-time {
+    color: #909399;
+    font-size: 14px;
+  }
 }
 
 .summary-cards {
@@ -298,126 +216,115 @@ onMounted(() => {
 
   .summary-card {
     text-align: center;
+    position: relative;
+    overflow: hidden;
+
+    &::before {
+      content: '';
+      position: absolute;
+      top: 0;
+      left: 0;
+      right: 0;
+      height: 4px;
+    }
 
-    :deep(.el-statistic__head) {
-      font-size: 14px;
+    &.machines::before {
+      background: linear-gradient(90deg, #667eea, #764ba2);
     }
 
-    :deep(.el-statistic__content) {
-      font-size: 24px;
+    &.cameras::before {
+      background: linear-gradient(90deg, #11998e, #38ef7d);
     }
-  }
-}
 
-.trend-card {
-  margin-bottom: 20px;
-}
+    &.channels::before {
+      background: linear-gradient(90deg, #f093fb, #f5576c);
+    }
 
-.trend-chart {
-  padding: 20px 0;
+    &.rate::before {
+      background: linear-gradient(90deg, #4facfe, #00f2fe);
+    }
 
-  .chart-bars {
-    display: flex;
-    justify-content: space-between;
-    align-items: flex-end;
-    height: 200px;
-    gap: 8px;
-  }
+    :deep(.el-statistic__head) {
+      font-size: 14px;
+    }
 
-  .bar-item {
-    flex: 1;
-    display: flex;
-    flex-direction: column;
-    align-items: center;
-    min-width: 30px;
-  }
+    :deep(.el-statistic__content) {
+      font-size: 28px;
+    }
 
-  .bar-wrapper {
-    width: 100%;
-    height: 150px;
-    display: flex;
-    align-items: flex-end;
-    justify-content: center;
-  }
+    .stat-detail {
+      margin-top: 10px;
+      font-size: 12px;
+      color: #909399;
 
-  .bar {
-    width: 80%;
-    max-width: 40px;
-    background: linear-gradient(180deg, #409eff 0%, #79bbff 100%);
-    border-radius: 4px 4px 0 0;
-    transition: height 0.3s ease;
-    cursor: pointer;
+      .online {
+        color: #67c23a;
+        margin-right: 10px;
+      }
 
-    &:hover {
-      background: linear-gradient(180deg, #337ecc 0%, #409eff 100%);
+      .offline {
+        color: #909399;
+      }
     }
   }
-
-  .bar-label {
-    font-size: 11px;
-    color: #909399;
-    margin-top: 8px;
-  }
-
-  .bar-value {
-    font-size: 12px;
-    color: #606266;
-    font-weight: 500;
-  }
 }
 
-.rank-card {
+.chart-card {
   margin-bottom: 20px;
 }
 
-.geo-card {
+.status-card {
   margin-bottom: 20px;
 
-  .geo-list {
-    display: flex;
-    flex-direction: column;
-    gap: 12px;
+  .status-chart {
+    padding: 10px 0;
   }
 
-  .geo-item {
-    display: flex;
-    align-items: center;
-    gap: 12px;
-  }
+  .status-item {
+    margin-bottom: 15px;
 
-  .geo-info {
-    width: 100px;
-    display: flex;
-    align-items: center;
-    gap: 8px;
-  }
+    &:last-child {
+      margin-bottom: 0;
+    }
 
-  .geo-rank {
-    width: 20px;
-    height: 20px;
-    border-radius: 50%;
-    background: #f0f2f5;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    font-size: 12px;
-    color: #606266;
-  }
+    .status-bar {
+      height: 24px;
+      border-radius: 4px;
+      transition: width 0.3s ease;
+      min-width: 20px;
+    }
 
-  .geo-country {
-    font-size: 14px;
-    color: #303133;
-  }
+    &.online .status-bar {
+      background: linear-gradient(90deg, #67c23a, #85ce61);
+    }
 
-  .geo-bar-wrapper {
-    flex: 1;
-  }
+    &.offline .status-bar {
+      background: linear-gradient(90deg, #909399, #a6a9ad);
+    }
 
-  .geo-count {
-    width: 60px;
-    text-align: right;
-    font-size: 14px;
-    color: #606266;
+    &.enabled .status-bar {
+      background: linear-gradient(90deg, #409eff, #66b1ff);
+    }
+
+    &.disabled .status-bar {
+      background: linear-gradient(90deg, #e6a23c, #ebb563);
+    }
+
+    .status-info {
+      display: flex;
+      justify-content: space-between;
+      margin-top: 5px;
+      font-size: 13px;
+    }
+
+    .status-label {
+      color: #606266;
+    }
+
+    .status-value {
+      color: #303133;
+      font-weight: 500;
+    }
   }
 }