소스 검색

refactor(camera): streamline camera API and PTZ control integration

- Removed deprecated PTZ service references and integrated PTZ control directly into the camera API.
- Updated API endpoints for camera actions, including listing, switching channels, and managing presets.
- Refactored related components to utilize the new API structure, enhancing consistency and maintainability.
- Improved error handling and response formatting across camera and PTZ functionalities.
yb 2 일 전
부모
커밋
e52282d045

+ 1 - 4
.env.development

@@ -5,8 +5,5 @@ VITE_APP_TITLE=摄像头管理系统
 
 VITE_APP_LANG=zh-cn
 
-# API 基础路径 (直接使用远程地址,不使用 Vite proxy)
+# API 基础路径
 VITE_APP_BASE_API=/api
-
-# PTZ 服务地址 (独立服务)
-VITE_APP_PTZ_API=http://localhost:3002

+ 0 - 3
.env.production

@@ -7,6 +7,3 @@ VITE_APP_LANG=en
 
 # API 基础路径 (生产环境使用完整 URL,因为 Cloudflare Pages 没有代理)
 VITE_APP_BASE_API=https://tg-live-game.pwtk.cc/api
-
-# PTZ 服务地址 (独立服务,需要配置生产环境地址)
-VITE_APP_PTZ_API=https://tg-live-game-ptz.pwtk.cc

+ 105 - 148
src/api/camera.ts

@@ -1,4 +1,4 @@
-import { get, post, ptzGet, ptzPost } from '@/utils/request'
+import { get, post } from '@/utils/request'
 import type {
   IBaseResponse,
   IListResponse,
@@ -10,214 +10,171 @@ import type {
   CameraAddRequest,
   CameraUpdateRequest,
   CameraListRequest,
-  SwitchChannelRequest,
-  PTZAction,
-  PTZControlRequest,
-  AdminPTZRequest
+  SwitchChannelRequest
 } from '@/types'
 
-// ==================== Controller APIs (MVP) ====================
+// ==================== Controller APIs ====================
 
-// 获取摄像头列表 (POST)
-export function listCameras(machineId?: string): Promise<IListResponse<CameraDTO>> {
-  return post('/camera/list', machineId ? { machineId } : {})
+// 获取摄像头列表
+export interface ListCamerasRequest {
+  machineId?: string
 }
 
-// 获取摄像头信息 (主API)
-export function getCamera(cameraId: string): Promise<IBaseResponse<CameraDTO>> {
-  return get(`/cameras/${cameraId}`)
+export function listCameras(data?: ListCamerasRequest): Promise<IListResponse<CameraDTO>> {
+  return post('/camera/list', data || {})
 }
 
-// 切换摄像头通道 (MVP核心)
-export function switchChannel(data: SwitchChannelRequest): Promise<IBaseResponse<ChannelDTO>> {
-  return post('/camera/switch', data)
+// 获取摄像头信息
+export interface GetCameraRequest {
+  cameraId: string
 }
 
-// 获取当前活动通道
-export function getCurrentChannel(machineId: string): Promise<IBaseResponse<ChannelDTO>> {
-  return get('/camera/current', { machineId })
+export function getCamera(data: GetCameraRequest): Promise<IBaseResponse<CameraDTO>> {
+  return get(`/cameras/${data.cameraId}`)
 }
 
-// 开始PTZ控制 (PTZ服务)
-// 使用 /ptz/control 端点,通过 body 传递 cameraId
-export function ptzStart(cameraId: string, action: PTZAction, speed: number = 50): Promise<BaseResponse> {
-  // 映射 action 到 PTZ 服务支持的 command
-  const commandMap: Record<PTZAction, string> = {
-    up: 'up',
-    down: 'down',
-    left: 'left',
-    right: 'right',
-    zoom_in: 'zoom_in',
-    zoom_out: 'zoom_out',
-    stop: 'stop'
-  }
-  return ptzPost('/ptz/control', {
-    host: cameraId,
-    command: commandMap[action] || 'stop',
-    speed
-  })
+// 切换摄像头通道
+export function switchChannel(data: SwitchChannelRequest): Promise<IBaseResponse<ChannelDTO>> {
+  return post('/camera/switch', data)
 }
 
-// 停止PTZ控制 (PTZ服务)
-export function ptzStop(cameraId: string): Promise<BaseResponse> {
-  return ptzPost('/ptz/control', {
-    host: cameraId,
-    command: 'stop'
-  })
+// 获取当前活动通道
+export interface GetCurrentChannelRequest {
+  machineId: string
 }
 
-// PTZ 直接控制 (pan/tilt/zoom 方式, PTZ服务)
-export function ptzDirectControl(cameraId: string, data: PTZControlRequest): Promise<BaseResponse> {
-  return ptzPost('/ptz/control', {
-    host: cameraId,
-    ...data
-  })
+export function getCurrentChannel(data: GetCurrentChannelRequest): Promise<IBaseResponse<ChannelDTO>> {
+  return get('/camera/current', data)
 }
 
-// 获取预置位列表 (PTZ服务)
-export function presetList(cameraId: string): Promise<BaseResponse> {
-  return ptzGet(`/${cameraId}/preset/list`)
-}
+// ==================== PTZ 控制 (代理到 PTZ 服务) ====================
 
-// 获取PTZ能力 (PTZ服务)
-export function getPTZCapabilities(cameraId: string): Promise<BaseResponse> {
-  return ptzGet(`/${cameraId}/ptz/capabilities`)
+// PTZ 控制
+export interface PTZControlRequest {
+  cameraId: string
+  command: string
+  speed?: number
 }
 
-// 跳转到预置位 (PTZ服务)
-export function presetGoto(cameraId: string, presetId: number): Promise<BaseResponse> {
-  return ptzPost(`/${cameraId}/preset/goto`, { presetId })
+export function ptzControl(data: PTZControlRequest): Promise<BaseResponse> {
+  return post(`/camera/control/${data.cameraId}/ptz/control`, data)
 }
 
-// 设置预置位 (PTZ服务)
-export function presetSet(cameraId: string, presetId: number, presetName?: string): Promise<BaseResponse> {
-  return ptzPost(`/${cameraId}/preset/set`, { presetId, presetName })
+// PTZ 能力
+export interface PTZCapabilitiesRequest {
+  cameraId: string
 }
 
-// 删除预置位 (PTZ服务)
-export function presetRemove(cameraId: string, presetId: number): Promise<BaseResponse> {
-  return ptzPost(`/${cameraId}/preset/remove`, { presetId })
+export function getPTZCapabilities(data: PTZCapabilitiesRequest): Promise<BaseResponse> {
+  return post(`/camera/control/${data.cameraId}/ptz/capabilities`, data)
 }
 
-// ==================== Admin APIs ====================
+// ==================== 预置位 ====================
 
-// 获取摄像头列表 (管理后台,分页)
-export function adminListCameras(params?: CameraListRequest): Promise<IPageResponse<CameraInfoDTO>> {
-  return post('/admin/cameras/list', params || {})
+export interface PresetInfo {
+  id: string
+  name: string
+  time: number
 }
 
-// 获取摄像头列表 (全部,不分页)
-export function adminListAllCameras(machineId?: string): Promise<IListResponse<CameraInfoDTO>> {
-  return get('/admin/cameras/listAll', machineId ? { machineId } : undefined)
+// 获取预置位列表
+export interface PresetListRequest {
+  cameraId: string
 }
 
-// 获取摄像头详情
-export function adminGetCamera(id: number): Promise<IBaseResponse<CameraInfoDTO>> {
-  return get('/admin/cameras/detail', { id })
+export function presetList(data: PresetListRequest): Promise<IBaseResponse<PresetInfo[]>> {
+  return post(`/camera/control/${data.cameraId}/preset/list`, data)
 }
 
-// 添加摄像头
-export function adminAddCamera(data: CameraAddRequest): Promise<IBaseResponse<CameraInfoDTO>> {
-  return post('/admin/cameras/add', data)
+// 跳转预置位
+export interface PresetGotoRequest {
+  cameraId: string
+  presetId: number
 }
 
-// 更新摄像头
-export function adminUpdateCamera(data: CameraUpdateRequest): Promise<IBaseResponse<CameraInfoDTO>> {
-  return post('/admin/cameras/update', data)
+export function presetGoto(data: PresetGotoRequest): Promise<IBaseResponse<boolean>> {
+  return post(`/camera/control/${data.cameraId}/preset/goto`, data)
 }
 
-// 删除摄像头
-export function adminDeleteCamera(id: number): Promise<BaseResponse> {
-  return post('/admin/cameras/delete', undefined, {
-    params: { id }
-  })
+// 设置预置位
+export interface PresetSetRequest {
+  cameraId: string
+  presetId: number
+  presetName?: string
 }
 
-// 检测摄像头连通性
-export function adminCheckCamera(id: number): Promise<IBaseResponse<boolean>> {
-  return post('/admin/cameras/check', undefined, {
-    params: { id }
-  })
+export function presetSet(data: PresetSetRequest): Promise<BaseResponse> {
+  return post(`/camera/control/${data.cameraId}/preset/set`, data)
 }
 
-// 获取摄像头快照
-export function adminGetSnapshot(id: number): Promise<IBaseResponse<Blob>> {
-  return get('/admin/cameras/snapshot', { id }, { responseType: 'blob' })
+// 删除预置位
+export interface PresetRemoveRequest {
+  cameraId: string
+  presetId: number
 }
 
-// PTZ 控制 (Admin)
-export function adminPTZControl(data: AdminPTZRequest): Promise<IBaseResponse<boolean>> {
-  return post('/admin/cameras/ptz', data)
+export function presetRemove(data: PresetRemoveRequest): Promise<BaseResponse> {
+  return post(`/camera/control/${data.cameraId}/preset/remove`, data)
 }
 
-// ==================== Preset APIs (PTZ服务) ====================
+// ==================== Admin APIs ====================
 
-// 预置位类型
-export interface PresetInfo {
-  token: string
-  name: string
+// 获取摄像头列表 (分页)
+export function adminListCameras(data?: CameraListRequest): Promise<IPageResponse<CameraInfoDTO>> {
+  return post('/admin/cameras/list', data || {})
 }
 
-// 获取预置位列表 (兼容旧API)
-export function getPresets(cameraId: string): Promise<IListResponse<PresetInfo>> {
-  return ptzGet(`/${cameraId}/preset/list`)
+// 获取摄像头列表 (全部)
+export interface AdminListAllCamerasRequest {
+  machineId?: string
 }
 
-// 跳转到预置位 (兼容旧API)
-export function gotoPreset(cameraId: string, presetToken: string): Promise<BaseResponse> {
-  return ptzPost(`/${cameraId}/preset/goto`, { presetId: parseInt(presetToken) || 1 })
+export function adminListAllCameras(data?: AdminListAllCamerasRequest): Promise<IListResponse<CameraInfoDTO>> {
+  return get('/admin/cameras/listAll', data)
 }
 
-// 设置预置位 (保存当前位置, 兼容旧API)
-export function setPreset(cameraId: string, presetName?: string): Promise<IBaseResponse<PresetInfo>> {
-  // 旧API不指定presetId,这里使用一个默认值
-  return ptzPost(`/${cameraId}/preset/set`, { presetId: 1, presetName })
+// 获取摄像头详情
+export interface AdminGetCameraRequest {
+  id: number
 }
 
-// 删除预置位 (兼容旧API)
-export function removePreset(cameraId: string, presetToken: string): Promise<BaseResponse> {
-  return ptzPost(`/${cameraId}/preset/remove`, { presetId: parseInt(presetToken) || 1 })
+export function adminGetCamera(data: AdminGetCameraRequest): Promise<IBaseResponse<CameraInfoDTO>> {
+  return get('/admin/cameras/detail', data)
 }
 
-// ==================== 兼容旧代码的别名 ====================
+// 添加摄像头
+export function adminAddCamera(data: CameraAddRequest): Promise<IBaseResponse<CameraInfoDTO>> {
+  return post('/admin/cameras/add', data)
+}
 
-// 获取设备列表 (兼容 - 使用不分页接口)
-export const listDevice = adminListAllCameras
+// 更新摄像头
+export function adminUpdateCamera(data: CameraUpdateRequest): Promise<IBaseResponse<CameraInfoDTO>> {
+  return post('/admin/cameras/update', data)
+}
 
-// 获取设备详情 (兼容)
-export const getDevice = (deviceId: string) => getCamera(deviceId)
+// 删除摄像头
+export interface AdminDeleteCameraRequest {
+  id: number
+}
 
-// 添加设备 (兼容)
-export const addDevice = adminAddCamera
+export function adminDeleteCamera(data: AdminDeleteCameraRequest): Promise<BaseResponse> {
+  return post('/admin/cameras/delete', undefined, { params: data })
+}
 
-// 修改设备 (兼容)
-export const updateDevice = adminUpdateCamera
+// 检测摄像头连通性
+export interface AdminCheckCameraRequest {
+  id: number
+}
 
-// 删除设备 (兼容)
-export const delDevice = (id: number) => adminDeleteCamera(id)
+export function adminCheckCamera(data: AdminCheckCameraRequest): Promise<IBaseResponse<boolean>> {
+  return post('/admin/cameras/check', undefined, { params: data })
+}
 
-// PTZ控制 (兼容旧API)
-export function ptzControl(
-  _deviceId: string,
-  channelId: string,
-  command: string,
-  horizonSpeed?: number,
-  _verticalSpeed?: number,
-  _zoomSpeed?: number
-): Promise<BaseResponse> {
-  // 映射旧的命令到新的 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 interface AdminGetSnapshotRequest {
+  id: number
+}
 
-  // 使用 channelId 关联的 cameraId 进行 PTZ 控制
-  return ptzStart(channelId, action, speed)
+export function adminGetSnapshot(data: AdminGetSnapshotRequest): Promise<IBaseResponse<Blob>> {
+  return get('/admin/cameras/snapshot', data, { responseType: 'blob' })
 }

+ 5 - 6
src/components/PTZController.vue

@@ -1,7 +1,6 @@
 <script setup lang="ts">
 import { ref } from 'vue'
-import { ptzStart, ptzStop } from '@/api/camera'
-import type { PTZAction } from '@/types'
+import { ptzControl } from '@/api/camera'
 
 const props = withDefaults(
   defineProps<{
@@ -17,7 +16,7 @@ const isMoving = ref(false)
 const currentDirection = ref<string | null>(null)
 
 // 方向映射
-const directionToAction: Record<string, PTZAction> = {
+const directionToCommand: Record<string, string> = {
   UP: 'up',
   DOWN: 'down',
   LEFT: 'left',
@@ -33,8 +32,8 @@ async function handleStart(
   isMoving.value = true
   currentDirection.value = direction
 
-  const action = directionToAction[direction] || 'stop'
-  const result = await ptzStart(props.cameraId, action)
+  const command = directionToCommand[direction] || 'stop'
+  const result = await ptzControl({ cameraId: props.cameraId, command })
   if (!result.success) {
     console.error('PTZ 控制失败:', result.errMsg)
   }
@@ -44,7 +43,7 @@ async function handleStart(
 async function handleStop() {
   if (!isMoving.value || !props.cameraId) return
 
-  const result = await ptzStop(props.cameraId)
+  const result = await ptzControl({ cameraId: props.cameraId, command: 'stop' })
   if (!result.success) {
     console.error('PTZ 停止失败:', result.errMsg)
   }

+ 10 - 11
src/components/monitor/PtzOverlay.vue

@@ -107,8 +107,7 @@ import {
   ZoomIn,
   ZoomOut
 } from '@element-plus/icons-vue'
-import { ptzStart, ptzStop } from '@/api/camera'
-import type { PTZAction } from '@/types'
+import { ptzControl } from '@/api/camera'
 
 interface Props {
   cameraId?: string
@@ -127,7 +126,7 @@ const zoomValue = ref(0)
 const ptzSpeed = 50
 
 // 方向映射
-const directionToAction: Record<string, PTZAction> = {
+const directionToCommand: Record<string, string> = {
   UP: 'up',
   DOWN: 'down',
   LEFT: 'left',
@@ -142,33 +141,33 @@ const directionToAction: Record<string, PTZAction> = {
 async function handleDirection(direction: string) {
   if (!props.cameraId) return
   emit('ptz-action', 'direction', { direction })
-  const action = directionToAction[direction] || 'stop'
-  await ptzStart(props.cameraId, action, ptzSpeed)
+  const command = directionToCommand[direction] || 'stop'
+  await ptzControl({ cameraId: props.cameraId, command, speed: ptzSpeed })
 }
 
 async function handleDirectionStop() {
   if (!props.cameraId) return
   emit('ptz-action', 'stop')
-  await ptzStop(props.cameraId)
+  await ptzControl({ cameraId: props.cameraId, command: 'stop' })
 }
 
 async function handleZoomChange(val: number) {
   if (!props.cameraId) return
   if (val === 0) {
-    await ptzStop(props.cameraId)
+    await ptzControl({ cameraId: props.cameraId, command: 'stop' })
     return
   }
 
-  const action: PTZAction = val > 0 ? 'zoom_in' : 'zoom_out'
+  const command = val > 0 ? 'zoom_in' : 'zoom_out'
   const speed = Math.abs(val)
-  emit('ptz-action', 'zoom', { action, speed })
-  await ptzStart(props.cameraId, action, speed)
+  emit('ptz-action', 'zoom', { command, speed })
+  await ptzControl({ cameraId: props.cameraId, command, speed })
 }
 
 async function handleZoomRelease() {
   zoomValue.value = 0
   if (!props.cameraId) return
-  await ptzStop(props.cameraId)
+  await ptzControl({ cameraId: props.cameraId, command: 'stop' })
 }
 </script>
 

+ 2 - 0
src/types/index.ts

@@ -11,6 +11,8 @@ export interface IBaseResponse<T> {
   success: boolean
   errCode?: string
   errMessage?: string
+  code?: number
+  msg?: string
 }
 
 // API 响应类型 - 分页列表响应

+ 0 - 56
src/utils/request.ts

@@ -13,15 +13,6 @@ const service: AxiosInstance = axios.create({
   }
 })
 
-// PTZ 服务 (独立服务,不走主后端)
-const ptzService: AxiosInstance = axios.create({
-  baseURL: import.meta.env.VITE_APP_PTZ_API || 'http://localhost:3002',
-  timeout: 10000,
-  headers: {
-    'Content-Type': 'application/json;charset=utf-8'
-  }
-})
-
 // 请求拦截器
 service.interceptors.request.use(
   (config) => {
@@ -103,51 +94,4 @@ export function del<T = any>(url: string, params?: object, config?: AxiosRequest
   return service.delete(url, { params, ...config })
 }
 
-// ==================== PTZ 服务请求方法 ====================
-
-// PTZ 服务响应格式转换
-// PTZ 服务返回: {code, msg, data}
-// 前端期望: {success, errMsg, data}
-interface PTZServiceResponse {
-  code: number
-  msg: string
-  data: unknown
-}
-
-// PTZ 服务响应拦截器
-ptzService.interceptors.response.use(
-  (response: AxiosResponse<PTZServiceResponse>) => {
-    const res = response.data
-    // 转换格式: {code, msg, data} -> {success, errMsg, data}
-    return {
-      success: res.code === 200,
-      errMsg: res.code !== 200 ? res.msg : undefined,
-      data: res.data
-    } as any
-  },
-  (error) => {
-    let { message } = error
-    if (message === 'Network Error') {
-      message = 'PTZ服务连接异常'
-    } else if (message.includes('timeout')) {
-      message = 'PTZ服务请求超时'
-    }
-    console.error('PTZ Service Error:', message)
-    // 返回统一格式的错误响应
-    return {
-      success: false,
-      errMsg: message,
-      data: null
-    }
-  }
-)
-
-export function ptzGet<T = any>(url: string, params?: object, config?: AxiosRequestConfig): Promise<T> {
-  return ptzService.get(url, { params, ...config })
-}
-
-export function ptzPost<T = any>(url: string, data?: object, config?: AxiosRequestConfig): Promise<T> {
-  return ptzService.post(url, data, config)
-}
-
 export default service

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

@@ -72,7 +72,7 @@ const filteredChannels = computed<ChannelDTO[]>(() => {
 async function getList() {
   loading.value = true
   try {
-    const res = await getCamera(cameraId)
+    const res = await getCamera({ cameraId })
     if (res.success) {
       cameraInfo.value = res.data
     }

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

@@ -563,7 +563,7 @@ function handleChannel(row: CameraInfoDTO) {
 
 async function handleCheck(row: CameraInfoDTO) {
   try {
-    const res = await adminCheckCamera(row.id)
+    const res = await adminCheckCamera({ id: row.id })
     if (res.success) {
       if (res.data) {
         ElMessage.success(t('摄像头连接正常'))
@@ -584,7 +584,7 @@ async function handleDelete(row: CameraInfoDTO) {
       type: 'warning'
     })
     deleteLoading.value = true
-    const res = await adminDeleteCamera(row.id)
+    const res = await adminDeleteCamera({ id: row.id })
     if (res.success) {
       ElMessage.success(t('删除成功'))
       getList()

+ 9 - 10
src/views/demo/cloudflareStream.vue

@@ -222,8 +222,7 @@ import {
   ZoomOut
 } from '@element-plus/icons-vue'
 import VideoPlayer from '@/components/VideoPlayer.vue'
-import { ptzStart, ptzStop } from '@/api/camera'
-import type { PTZAction } from '@/types'
+import { ptzControl } from '@/api/camera'
 
 const playerRef = ref<InstanceType<typeof VideoPlayer>>()
 
@@ -363,7 +362,7 @@ function onError(error: any) {
 }
 
 // PTZ 方向映射
-const directionToAction: Record<string, PTZAction> = {
+const directionToCommand: Record<string, string> = {
   UP: 'up',
   DOWN: 'down',
   LEFT: 'left',
@@ -382,8 +381,8 @@ async function handlePTZ(direction: string) {
     return
   }
 
-  const action = directionToAction[direction] || 'stop'
-  const result = await ptzStart(ptzCameraId.value, action, ptzSpeed.value)
+  const command = directionToCommand[direction] || 'stop'
+  const result = await ptzControl({ cameraId: ptzCameraId.value, command, speed: ptzSpeed.value })
 
   if (result.success) {
     addLog(`PTZ 移动: ${direction} (速度: ${ptzSpeed.value})`, 'info')
@@ -395,7 +394,7 @@ async function handlePTZ(direction: string) {
 async function handlePTZStop() {
   if (!ptzCameraId.value) return
 
-  const result = await ptzStop(ptzCameraId.value)
+  const result = await ptzControl({ cameraId: ptzCameraId.value, command: 'stop' })
 
   if (!result.success) {
     addLog(`PTZ 停止失败: ${result.errMsg}`, 'error')
@@ -412,20 +411,20 @@ async function handleZoomChange(val: number) {
   if (!ptzCameraId.value) return
 
   if (val === 0) {
-    await ptzStop(ptzCameraId.value)
+    await ptzControl({ cameraId: ptzCameraId.value, command: 'stop' })
     return
   }
 
-  const action: PTZAction = val > 0 ? 'zoom_in' : 'zoom_out'
+  const command = val > 0 ? 'zoom_in' : 'zoom_out'
   const speed = Math.abs(val)
-  await ptzStart(ptzCameraId.value, action, speed)
+  await ptzControl({ cameraId: ptzCameraId.value, command, speed })
 }
 
 async function handleZoomRelease() {
   zoomValue.value = 0
   if (!ptzCameraId.value) return
 
-  await ptzStop(ptzCameraId.value)
+  await ptzControl({ cameraId: ptzCameraId.value, command: 'stop' })
   addLog('缩放停止', 'info')
 }
 </script>

+ 23 - 72
src/views/live-stream/index.vue

@@ -506,7 +506,7 @@
                     <div
                       v-for="preset in ptzPresetList"
                       :key="preset.id"
-                      :class="['preset-item', { active: activePresetId === preset.id }]"
+                      :class="['preset-item', { active: activePresetId === preset.id.toString() }]"
                     >
                       <span class="preset-index">{{ preset.id }}</span>
                       <span class="preset-name">{{ preset.name || `Preset ${preset.id}` }}</span>
@@ -639,16 +639,13 @@ import { startStreamTask, stopStreamTask, getStreamPlayback } from '@/api/stream
 import VideoPlayer from '@/components/VideoPlayer.vue'
 import CodeEditor from '@/components/CodeEditor.vue'
 import {
-  getPresets,
-  gotoPreset,
   type PresetInfo,
   presetList,
   presetGoto,
   presetSet,
   presetRemove,
   getPTZCapabilities,
-  ptzStart,
-  ptzStop
+  ptzControl
 } from '@/api/camera'
 import type { LiveStreamDTO, LiveStreamStatus, LssNodeDTO, CameraInfoDTO, StreamChannelDTO, PTZAction } from '@/types'
 
@@ -694,9 +691,7 @@ const ptzSpeed = ref(50)
 const zoomValue = ref(0)
 
 // 预置位
-const presetListData = ref<PresetInfo[]>([])
 const presetsLoading = ref(false)
-const activePresetToken = ref<string | null>(null)
 
 // PTZ 预置位 (camera API)
 interface PTZPresetInfo {
@@ -707,7 +702,7 @@ interface PTZCapabilities {
   maxPresetNum?: number
   [key: string]: unknown
 }
-const ptzPresetList = ref<PTZPresetInfo[]>([])
+const ptzPresetList = ref<PresetInfo[]>([])
 const activePresetId = ref<string | null>(null)
 const cameraCapabilities = ref<PTZCapabilities | null>(null)
 const capabilitiesLoading = ref(false)
@@ -1349,8 +1344,8 @@ async function handlePTZ(direction: string) {
   }
 
   try {
-    const action = directionToAction[direction] || 'stop'
-    const res = await ptzStart(cameraId, action, ptzSpeed.value)
+    const command = directionToAction[direction] || 'stop'
+    const res = await ptzControl({ cameraId, command, speed: ptzSpeed.value })
     if (!res.success) {
       console.error('PTZ 控制失败', res.errMsg)
     }
@@ -1364,7 +1359,7 @@ async function handlePTZStop() {
   if (!cameraId) return
 
   try {
-    await ptzStop(cameraId)
+    await ptzControl({ cameraId, command: 'stop' })
   } catch (error) {
     console.error('PTZ 停止失败', error)
   }
@@ -1381,19 +1376,19 @@ async function handleZoomChange(val: number) {
   if (!cameraId) return
 
   if (val === 0) {
-    await ptzStop(cameraId)
+    await ptzControl({ cameraId, command: 'stop' })
     return
   }
 
-  const action: PTZAction = val > 0 ? 'zoom_in' : 'zoom_out'
-  await ptzStart(cameraId, action, Math.abs(val))
+  const command = val > 0 ? 'zoom_in' : 'zoom_out'
+  await ptzControl({ cameraId, command, speed: Math.abs(val) })
 }
 
 async function handleZoomRelease() {
   zoomValue.value = 0
   const cameraId = currentMediaStream.value?.cameraId
   if (!cameraId) return
-  await ptzStop(cameraId)
+  await ptzControl({ cameraId, command: 'stop' })
 }
 
 // 缩放按钮控制
@@ -1404,7 +1399,7 @@ async function handleZoomIn() {
     return
   }
   try {
-    const res = await ptzStart(cameraId, 'zoom_in', ptzSpeed.value)
+    const res = await ptzControl({ cameraId, command: 'zoom_in', speed: ptzSpeed.value })
     if (!res.success) {
       console.error('Zoom in 失败', res.errMsg)
     }
@@ -1420,7 +1415,7 @@ async function handleZoomOut() {
     return
   }
   try {
-    const res = await ptzStart(cameraId, 'zoom_out', ptzSpeed.value)
+    const res = await ptzControl({ cameraId, command: 'zoom_out', speed: ptzSpeed.value })
     if (!res.success) {
       console.error('Zoom out 失败', res.errMsg)
     }
@@ -1429,50 +1424,6 @@ async function handleZoomOut() {
   }
 }
 
-// 加载预置位列表
-async function loadPresets() {
-  if (!currentMediaStream.value?.cameraId) {
-    ElMessage.warning(t('未配置摄像头'))
-    return
-  }
-
-  presetsLoading.value = true
-  try {
-    const res = await getPresets(currentMediaStream.value.cameraId)
-    if (res.success && res.data) {
-      presetList.value = res.data
-    } else {
-      presetList.value = []
-    }
-  } catch (error) {
-    console.error('加载预置位失败', error)
-    presetList.value = []
-  } finally {
-    presetsLoading.value = false
-  }
-}
-
-// 跳转到预置位
-async function handleGotoPreset(preset: PresetInfo) {
-  if (!currentMediaStream.value?.cameraId) {
-    ElMessage.warning(t('未配置摄像头'))
-    return
-  }
-
-  try {
-    activePresetToken.value = preset.token
-    const res = await gotoPreset(currentMediaStream.value.cameraId, preset.token)
-    if (res.success) {
-      ElMessage.success(`${t('已跳转到预置位')}: ${preset.name || preset.token}`)
-    } else {
-      ElMessage.error(res.errMessage || t('跳转失败'))
-    }
-  } catch (error) {
-    console.error('跳转预置位失败', error)
-    ElMessage.error(t('跳转失败'))
-  }
-}
-
 // ==================== PTZ 直连 API ====================
 
 // 检查摄像头连接配置
@@ -1490,9 +1441,9 @@ async function loadPTZPresets() {
 
   presetsLoading.value = true
   try {
-    const res = await presetList(cameraId)
-    if (res.success && res.data) {
-      ptzPresetList.value = res.data as PTZPresetInfo[]
+    const res = await presetList({ cameraId })
+    if (res.code === 200 && res.data) {
+      ptzPresetList.value = res.data as PresetInfo[]
     } else {
       ptzPresetList.value = []
       if (!res.success) {
@@ -1508,7 +1459,7 @@ async function loadPTZPresets() {
 }
 
 // 跳转到 PTZ 预置位 (通过 camera API)
-async function handleGotoPTZPreset(preset: PTZPresetInfo) {
+async function handleGotoPTZPreset(preset: PresetInfo) {
   const cameraId = currentMediaStream.value?.cameraId
   if (!cameraId) {
     ElMessage.warning(t('请先配置摄像头连接'))
@@ -1517,8 +1468,8 @@ async function handleGotoPTZPreset(preset: PTZPresetInfo) {
 
   try {
     activePresetId.value = preset.id
-    const res = await presetGoto(cameraId, parseInt(preset.id))
-    if (res.success) {
+    const res = await presetGoto({ cameraId, presetId: parseInt(preset.id) })
+    if (res.code === 200) {
       ElMessage.success(`${t('已跳转到预置位')}: ${preset.name || preset.id}`)
     } else {
       ElMessage.error(res.errMsg || t('跳转失败'))
@@ -1548,7 +1499,7 @@ async function handleDeletePreset(preset: PTZPresetInfo) {
       type: 'warning'
     })
 
-    const res = await presetRemove(cameraId, parseInt(preset.id))
+    const res = await presetRemove({ cameraId, presetId: parseInt(preset.id) })
     if (res.success) {
       ElMessage.success(t('删除成功'))
       // 刷新预置位列表
@@ -1573,7 +1524,7 @@ async function loadCameraCapabilities() {
 
   capabilitiesLoading.value = true
   try {
-    const res = await getPTZCapabilities(cameraId)
+    const res = await getPTZCapabilities({ cameraId })
     if (res.success && res.data) {
       cameraCapabilities.value = res.data as PTZCapabilities
     } else {
@@ -1693,7 +1644,7 @@ function selectPoint(point: TimelinePoint) {
   // 如果已有预置位,跳转到该位置
   const cameraId = currentMediaStream.value?.cameraId
   if (point.presetId && cameraId) {
-    presetGoto(cameraId, point.presetId).then((res) => {
+    presetGoto({ cameraId, presetId: point.presetId }).then((res) => {
       if (res.success) {
         ElMessage.success(`${t('已跳转到')}: ${point.presetName || `Point ${point.id}`}`)
       }
@@ -1718,7 +1669,7 @@ async function saveCurrentPoint() {
 
   savingPreset.value = true
   try {
-    const res = await presetSet(cameraId, presetIdNum, presetName)
+    const res = await presetSet({ cameraId, presetId: presetIdNum, presetName })
     if (res.success) {
       point.presetId = presetIdNum
       point.presetName = presetName
@@ -1820,7 +1771,7 @@ async function playTimeline() {
 
       // 跳转到该预置位
       if (point.presetId) {
-        await presetGoto(cameraId, point.presetId)
+        await presetGoto({ cameraId, presetId: point.presetId })
         selectedPoint.value = point
       }
 

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

@@ -1213,7 +1213,7 @@ async function handleEditCamera(row: CameraInfoDTO) {
 
   try {
     // 通过 API 获取摄像头详情
-    const res = await adminGetCamera(row.id)
+    const res = await adminGetCamera({ id: row.id })
     if (!res.success || !res.data) {
       ElMessage.error(res.errMessage || '获取摄像头详情失败')
       return
@@ -1341,7 +1341,7 @@ async function handleDeleteCamera(row: CameraInfoDTO) {
         dangerouslyUseHTMLString: true
       }
     )
-    const res = await adminDeleteCamera(row.id)
+    const res = await adminDeleteCamera({ id: row.id })
     if (res.success) {
       ElMessage.success('删除成功')
       loadCameraList()

+ 31 - 53
tests/unit/api/camera.spec.ts

@@ -4,8 +4,7 @@ import {
   getCamera,
   switchChannel,
   getCurrentChannel,
-  ptzStart,
-  ptzStop,
+  ptzControl,
   adminListCameras,
   adminGetCamera,
   adminAddCamera,
@@ -18,9 +17,7 @@ import { mockCameras, mockChannels, wrapResponse } from '../../fixtures'
 
 vi.mock('@/utils/request', () => ({
   get: vi.fn(),
-  post: vi.fn(),
-  ptzGet: vi.fn(),
-  ptzPost: vi.fn()
+  post: vi.fn()
 }))
 
 describe('Camera API', () => {
@@ -43,7 +40,7 @@ describe('Camera API', () => {
       it('should call with machineId filter', async () => {
         vi.mocked(request.post).mockResolvedValue(wrapResponse([]))
 
-        await listCameras('machine-001')
+        await listCameras({ machineId: 'machine-001' })
 
         expect(request.post).toHaveBeenCalledWith('/camera/list', { machineId: 'machine-001' })
       })
@@ -55,7 +52,7 @@ describe('Camera API', () => {
         const mockResponse = wrapResponse(camera)
         vi.mocked(request.get).mockResolvedValue(mockResponse)
 
-        const result = await getCamera(camera.cameraId)
+        const result = await getCamera({ cameraId: camera.cameraId })
 
         expect(request.get).toHaveBeenCalledWith(`/cameras/${camera.cameraId}`)
         expect(result.data.cameraId).toBe(camera.cameraId)
@@ -68,12 +65,10 @@ describe('Camera API', () => {
         const mockResponse = wrapResponse(channel)
         vi.mocked(request.post).mockResolvedValue(mockResponse)
 
-        const result = await switchChannel({ machineId: 'machine-001', channelId: channel.channelId })
+        const data = { machineId: 'machine-001', channelId: channel.channelId }
+        const result = await switchChannel(data)
 
-        expect(request.post).toHaveBeenCalledWith('/camera/switch', {
-          machineId: 'machine-001',
-          channelId: channel.channelId
-        })
+        expect(request.post).toHaveBeenCalledWith('/camera/switch', data)
         expect(result.data.channelId).toBe(channel.channelId)
       })
     })
@@ -84,39 +79,23 @@ describe('Camera API', () => {
         const mockResponse = wrapResponse(channel)
         vi.mocked(request.get).mockResolvedValue(mockResponse)
 
-        const result = await getCurrentChannel('machine-001')
+        const data = { machineId: 'machine-001' }
+        const result = await getCurrentChannel(data)
 
-        expect(request.get).toHaveBeenCalledWith('/camera/current', { machineId: 'machine-001' })
+        expect(request.get).toHaveBeenCalledWith('/camera/current', data)
         expect(result.data.channelId).toBe(channel.channelId)
       })
     })
 
-    describe('ptzStart', () => {
-      it('should call POST /ptz/control with PTZ service', async () => {
-        const mockResponse = wrapResponse(null)
-        vi.mocked(request.ptzPost).mockResolvedValue(mockResponse)
-
-        await ptzStart('cam-001', 'up', 50)
-
-        expect(request.ptzPost).toHaveBeenCalledWith('/ptz/control', {
-          host: 'cam-001',
-          command: 'up',
-          speed: 50
-        })
-      })
-    })
-
-    describe('ptzStop', () => {
-      it('should call POST /ptz/control with stop command', async () => {
+    describe('ptzControl', () => {
+      it('should call POST /camera/control/:cameraId/ptz/control', async () => {
         const mockResponse = wrapResponse(null)
-        vi.mocked(request.ptzPost).mockResolvedValue(mockResponse)
+        vi.mocked(request.post).mockResolvedValue(mockResponse)
 
-        await ptzStop('cam-001')
+        const data = { cameraId: 'cam-001', command: 'up', speed: 50 }
+        await ptzControl(data)
 
-        expect(request.ptzPost).toHaveBeenCalledWith('/ptz/control', {
-          host: 'cam-001',
-          command: 'stop'
-        })
+        expect(request.post).toHaveBeenCalledWith('/camera/control/cam-001/ptz/control', data)
       })
     })
   })
@@ -148,9 +127,10 @@ describe('Camera API', () => {
         const mockResponse = wrapResponse(camera)
         vi.mocked(request.get).mockResolvedValue(mockResponse)
 
-        const result = await adminGetCamera(camera.id)
+        const data = { id: camera.id }
+        const result = await adminGetCamera(data)
 
-        expect(request.get).toHaveBeenCalledWith('/admin/cameras/detail', { id: camera.id })
+        expect(request.get).toHaveBeenCalledWith('/admin/cameras/detail', data)
         expect(result.data.id).toBe(camera.id)
       })
     })
@@ -161,15 +141,15 @@ describe('Camera API', () => {
         const mockResponse = wrapResponse(newCamera)
         vi.mocked(request.post).mockResolvedValue(mockResponse)
 
-        const addData = {
+        const data = {
           cameraId: 'cam-new',
           name: '新摄像头',
           ip: '192.168.1.100',
           port: 80
         }
-        const result = await adminAddCamera(addData)
+        const result = await adminAddCamera(data)
 
-        expect(request.post).toHaveBeenCalledWith('/admin/cameras/add', addData)
+        expect(request.post).toHaveBeenCalledWith('/admin/cameras/add', data)
         expect(result.data.cameraId).toBe('cam-new')
       })
     })
@@ -180,10 +160,10 @@ describe('Camera API', () => {
         const mockResponse = wrapResponse(updatedCamera)
         vi.mocked(request.post).mockResolvedValue(mockResponse)
 
-        const updateData = { id: mockCameras[0].id, name: '更新后名称' }
-        const result = await adminUpdateCamera(updateData)
+        const data = { id: mockCameras[0].id, name: '更新后名称' }
+        const result = await adminUpdateCamera(data)
 
-        expect(request.post).toHaveBeenCalledWith('/admin/cameras/update', updateData)
+        expect(request.post).toHaveBeenCalledWith('/admin/cameras/update', data)
         expect(result.data.name).toBe('更新后名称')
       })
     })
@@ -193,11 +173,10 @@ describe('Camera API', () => {
         const mockResponse = wrapResponse(null)
         vi.mocked(request.post).mockResolvedValue(mockResponse)
 
-        await adminDeleteCamera(1)
+        const data = { id: 1 }
+        await adminDeleteCamera(data)
 
-        expect(request.post).toHaveBeenCalledWith('/admin/cameras/delete', undefined, {
-          params: { id: 1 }
-        })
+        expect(request.post).toHaveBeenCalledWith('/admin/cameras/delete', undefined, { params: data })
       })
     })
 
@@ -206,11 +185,10 @@ describe('Camera API', () => {
         const mockResponse = wrapResponse(true)
         vi.mocked(request.post).mockResolvedValue(mockResponse)
 
-        const result = await adminCheckCamera(1)
+        const data = { id: 1 }
+        const result = await adminCheckCamera(data)
 
-        expect(request.post).toHaveBeenCalledWith('/admin/cameras/check', undefined, {
-          params: { id: 1 }
-        })
+        expect(request.post).toHaveBeenCalledWith('/admin/cameras/check', undefined, { params: data })
         expect(result.data).toBe(true)
       })
     })

+ 6 - 6
vite.config.ts

@@ -61,17 +61,17 @@ export default defineConfig({
     port: 3000,
     open: true,
     proxy: {
+      // PTZ 摄像头控制 (必须放在 /api 之前,更具体的路径优先匹配)
+      '/api/camera/control': {
+        target: 'http://localhost:3002',
+        changeOrigin: true,
+        rewrite: (path) => path.replace(/^\/api\/camera\/control/, '')
+      },
       // 后端 API
       '/api': {
         target: 'https://tg-live-game.pwtk.cc',
         changeOrigin: true,
         secure: false // 禁用 SSL 证书验证(开发环境)
-      },
-      // PTZ 摄像头控制
-      '/camera/control': {
-        target: 'http://localhost:3002',
-        changeOrigin: true,
-        rewrite: (path) => path.replace(/^\/camera\/control/, '')
       }
     }
   },