Explorar o código

feat(lss): refactor LSS view and introduce composables for better organization

- Refactored the LSS management view by removing unused imports and consolidating functionality into composable functions for camera and LSS management.
- Introduced new composables: `useCameraList`, `useLssList`, `useCredentials`, `useScanDevices`, and `useFormatters` to enhance code organization and reusability.
- Improved the structure of the LSS view, making it more maintainable and easier to understand.
- Added functionality for managing camera credentials and scanning devices, enhancing user interaction and experience.
yb hai 1 día
pai
achega
3a332239f9

+ 454 - 0
src/views/lss/composables/useCameraList.ts

@@ -0,0 +1,454 @@
+import { ref, reactive, computed } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import type { FormInstance, FormRules } from 'element-plus'
+import { useI18n } from 'vue-i18n'
+import { useRouter } from 'vue-router'
+import { adminListCameras, adminAddCamera, adminUpdateCamera, adminDeleteCamera, adminGetCamera } from '@/api/camera'
+import { listCameraVendors } from '@/api/camera-vendor'
+import { isValidJson } from './useFormatters'
+import type {
+  LssNodeDTO,
+  CameraInfoDTO,
+  CameraAddRequest,
+  CameraUpdateRequest,
+  CameraVendorDTO,
+  CameraHeartbeatStatus
+} from '@/types'
+
+export function useCameraList(currentLss: { value: LssNodeDTO | null }) {
+  const { t } = useI18n({ useScope: 'global' })
+  const router = useRouter()
+
+  // 设备列表抽屉
+  const cameraDrawerVisible = ref(false)
+  const cameraLoading = ref(false)
+  const deviceActiveTab = ref('camera')
+  const cameraList = ref<CameraInfoDTO[]>([])
+  const cameraVendorList = ref<CameraVendorDTO[]>([])
+
+  // 分页
+  const cameraCurrentPage = ref(1)
+  const cameraPageSize = ref(15)
+  const cameraTotal = ref(0)
+
+  const cameraTableHeight = computed(() => 'calc(100vh - 238px)')
+
+  // 搜索表单
+  const cameraSearchForm = reactive({
+    cameraId: '',
+    cameraName: '',
+    status: '' as CameraHeartbeatStatus | ''
+  })
+
+  // 编辑弹窗
+  const cameraDialogVisible = ref(false)
+  const cameraFormRef = ref<FormInstance>()
+  const isEditCamera = ref(false)
+  const cameraSubmitting = ref(false)
+  const currentCamera = ref<CameraInfoDTO | null>(null)
+
+  // 参数配置弹窗
+  const paramsDialogVisible = ref(false)
+  const paramsDialogTitle = ref('')
+  const paramsDialogType = ref<'config' | 'run'>('config')
+  const paramsContent = ref('')
+  const paramsSubmitting = ref(false)
+  const paramsCamera = ref<CameraInfoDTO | null>(null)
+
+  // 表单
+  const cameraForm = reactive({
+    selectedVendorId: null as number | null,
+    cameraId: '',
+    cameraName: '',
+    vendorName: '',
+    model: '',
+    ip: '',
+    port: 80,
+    username: '',
+    password: '',
+    brand: '',
+    capability: 'switch_only' as 'switch_only' | 'ptz_enabled',
+    rtspUrl: '',
+    channelNo: '',
+    remark: '',
+    enabled: true,
+    paramConfig: '',
+    runtimeParams: '',
+    createdAt: '',
+    updatedAt: ''
+  })
+
+  const cameraRules = computed<FormRules>(() => ({
+    cameraId: [{ required: true, message: t('请输入设备ID'), trigger: 'blur' }]
+  }))
+
+  async function loadCameraList() {
+    if (!currentLss.value) return
+    cameraLoading.value = true
+    cameraList.value = []
+    try {
+      const params: any = {
+        lssId: currentLss.value.lssId,
+        page: cameraCurrentPage.value,
+        size: cameraPageSize.value
+      }
+      if (cameraSearchForm.cameraId) params.cameraId = cameraSearchForm.cameraId
+      if (cameraSearchForm.cameraName) params.cameraName = cameraSearchForm.cameraName
+      if (cameraSearchForm.status) params.status = cameraSearchForm.status
+
+      const res = await adminListCameras(params)
+      if (res.success && res.data) {
+        cameraList.value = res.data.list || []
+        cameraTotal.value = res.data.total || 0
+      } else {
+        ElMessage.error(res.errMessage || '获取摄像头列表失败')
+      }
+    } catch (error) {
+      console.error('获取摄像头列表失败', error)
+      ElMessage.error('获取摄像头列表失败')
+    } finally {
+      cameraLoading.value = false
+    }
+  }
+
+  function handleCameraSearch() {
+    cameraCurrentPage.value = 1
+    loadCameraList()
+  }
+
+  function handleCameraReset() {
+    cameraSearchForm.cameraId = ''
+    cameraSearchForm.cameraName = ''
+    cameraSearchForm.status = ''
+    cameraCurrentPage.value = 1
+    loadCameraList()
+  }
+
+  function handleCameraSizeChange(val: number) {
+    cameraPageSize.value = val
+    cameraCurrentPage.value = 1
+    loadCameraList()
+  }
+
+  function handleCameraPageChange(val: number) {
+    cameraCurrentPage.value = val
+    loadCameraList()
+  }
+
+  function resetCameraForm() {
+    cameraForm.selectedVendorId = null
+    cameraForm.cameraId = ''
+    cameraForm.cameraName = ''
+    cameraForm.vendorName = ''
+    cameraForm.model = ''
+    cameraForm.ip = ''
+    cameraForm.port = 80
+    cameraForm.username = ''
+    cameraForm.password = ''
+    cameraForm.brand = ''
+    cameraForm.capability = 'switch_only'
+    cameraForm.rtspUrl = ''
+    cameraForm.channelNo = ''
+    cameraForm.remark = ''
+    cameraForm.enabled = true
+    cameraForm.paramConfig = ''
+    cameraForm.runtimeParams = ''
+    cameraForm.createdAt = ''
+    cameraForm.updatedAt = ''
+    cameraFormRef.value?.clearValidate()
+  }
+
+  async function loadCameraVendorList() {
+    try {
+      const res = await listCameraVendors({ enabled: true })
+      if (res.success && res.data) {
+        cameraVendorList.value = res.data.list || []
+      }
+    } catch (error) {
+      console.error('获取厂商列表失败', error)
+    }
+  }
+
+  function handleVendorSelect(vendorId: number) {
+    const vendor = cameraVendorList.value.find((v) => v.id === vendorId)
+    if (vendor) {
+      cameraForm.brand = vendor.code
+      if (vendor.defaultPort) cameraForm.port = vendor.defaultPort
+      cameraForm.capability = vendor.supportPtz ? 'ptz_enabled' : 'switch_only'
+    }
+  }
+
+  function generateCameraId() {
+    const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
+    const prefix = 'CAM-'
+    let id = ''
+    for (let i = 0; i < 6; i++) {
+      id += chars.charAt(Math.floor(Math.random() * chars.length))
+    }
+    cameraForm.cameraId = prefix + id
+  }
+
+  async function handleAddCamera() {
+    isEditCamera.value = false
+    currentCamera.value = null
+    resetCameraForm()
+    await loadCameraVendorList()
+    cameraDialogVisible.value = true
+  }
+
+  async function handleEditCamera(row: CameraInfoDTO) {
+    isEditCamera.value = true
+    try {
+      const res = await adminGetCamera({ id: row.id })
+      if (!res.success || !res.data) {
+        ElMessage.error(res.errMessage || '获取摄像头详情失败')
+        return
+      }
+      const camera = res.data
+      currentCamera.value = camera
+      cameraForm.selectedVendorId = null
+      cameraForm.cameraId = camera.cameraId
+      cameraForm.cameraName = camera.cameraName
+      cameraForm.vendorName = camera.vendorName || ''
+      cameraForm.model = camera.model || ''
+      cameraForm.ip = camera.ip
+      cameraForm.port = camera.port || 80
+      cameraForm.username = camera.username || ''
+      cameraForm.password = ''
+      cameraForm.brand = camera.brand || ''
+      cameraForm.capability = camera.capability || 'switch_only'
+      cameraForm.rtspUrl = camera.rtspUrl || ''
+      cameraForm.model = camera.model || ''
+      cameraForm.channelNo = camera.channelNo || ''
+      cameraForm.remark = camera.remark || ''
+      cameraForm.createdAt = camera.createdAt || ''
+      cameraForm.updatedAt = camera.updatedAt || ''
+      cameraForm.enabled = camera.enabled
+      cameraForm.paramConfig = camera.paramConfig || ''
+      cameraForm.runtimeParams = camera.runtimeParams || ''
+      await loadCameraVendorList()
+      cameraDialogVisible.value = true
+    } catch (error) {
+      console.error('获取摄像头详情失败', error)
+      ElMessage.error('获取摄像头详情失败')
+    }
+  }
+
+  async function handleSubmitCamera() {
+    if (!cameraFormRef.value) return
+    await cameraFormRef.value.validate(async (valid) => {
+      if (!valid) return
+      if (!isValidJson(cameraForm.paramConfig)) {
+        ElMessage.error('参数配置格式错误,请输入有效的 JSON')
+        return
+      }
+      if (!isValidJson(cameraForm.runtimeParams)) {
+        ElMessage.error('设备运行参数格式错误,请输入有效的 JSON')
+        return
+      }
+      cameraSubmitting.value = true
+      try {
+        if (isEditCamera.value) {
+          if (!currentCamera.value) {
+            ElMessage.error('摄像头信息错误')
+            return
+          }
+          const data: CameraUpdateRequest = {
+            id: currentCamera.value.id,
+            cameraName: cameraForm.cameraName,
+            vendorName: cameraForm.vendorName,
+            model: cameraForm.model,
+            port: cameraForm.port,
+            username: cameraForm.username,
+            brand: cameraForm.brand,
+            capability: cameraForm.capability,
+            lssId: currentLss.value?.lssId,
+            rtspUrl: cameraForm.rtspUrl,
+            channelNo: cameraForm.channelNo,
+            remark: cameraForm.remark,
+            enabled: cameraForm.enabled,
+            paramConfig: cameraForm.paramConfig,
+            runtimeParams: cameraForm.runtimeParams
+          }
+          if (cameraForm.password) data.password = cameraForm.password
+          const res = await adminUpdateCamera(data)
+          if (res.success) {
+            ElMessage.success('更新成功')
+            cameraDialogVisible.value = false
+            loadCameraList()
+          } else {
+            ElMessage.error(res.errMessage || '更新失败')
+          }
+        } else {
+          const data: CameraAddRequest = {
+            cameraId: cameraForm.cameraId,
+            cameraName: cameraForm.cameraName,
+            vendorName: cameraForm.vendorName,
+            model: cameraForm.model,
+            paramConfig: cameraForm.paramConfig,
+            runtimeParams: cameraForm.runtimeParams,
+            lssId: currentLss.value?.lssId
+          }
+          const res = await adminAddCamera(data)
+          if (res.success) {
+            ElMessage.success('添加成功')
+            cameraDialogVisible.value = false
+            loadCameraList()
+          } else {
+            ElMessage.error(res.errMessage || '添加失败')
+          }
+        }
+      } catch (error) {
+        console.error('保存摄像头失败', error)
+        ElMessage.error('操作失败')
+      } finally {
+        cameraSubmitting.value = false
+      }
+    })
+  }
+
+  async function handleDeleteCamera(row: CameraInfoDTO) {
+    try {
+      await ElMessageBox.confirm(
+        `你确定要删除这个设备吗?<br/>设备ID:${row.cameraId}<br/>设备名称:${row.cameraName}`,
+        '提示',
+        { type: 'warning', dangerouslyUseHTMLString: true }
+      )
+      const res = await adminDeleteCamera({ id: row.id })
+      if (res.success) {
+        ElMessage.success('删除成功')
+        loadCameraList()
+      } else {
+        ElMessage.error(res.errMessage || '删除失败')
+      }
+    } catch (error) {
+      if (error !== 'cancel') {
+        console.error('删除摄像头失败', error)
+        ElMessage.error('删除失败')
+      }
+    }
+  }
+
+  async function handleViewCamera(row: CameraInfoDTO) {
+    if (!row.streamSn) {
+      try {
+        await ElMessageBox.confirm(t('请先新增 Live Stream,才能进行后续操作。'), t('尚未建立 Live Stream'), {
+          confirmButtonText: t('新增 Live Stream'),
+          cancelButtonText: t('取消'),
+          type: 'warning',
+          center: true,
+          customClass: 'live-stream-dialog',
+          distinguishCancelAndClose: true
+        })
+        router.push(`/live-stream-manage/list?cameraId=${row.cameraId}&lssId=${row.lssId}&action=create`)
+      } catch {
+        // 用户取消
+      }
+      return
+    }
+    router.push(`/live-stream-manage/list?cameraId=${row.cameraId}`)
+  }
+
+  // 参数配置
+  function handleViewConfig(row: CameraInfoDTO) {
+    paramsCamera.value = row
+    paramsDialogType.value = 'config'
+    paramsDialogTitle.value = `参数配置 - ${row.cameraName}`
+    paramsContent.value = row.paramConfig || ''
+    paramsDialogVisible.value = true
+  }
+
+  function handleViewRunParams(row: CameraInfoDTO) {
+    paramsCamera.value = row
+    paramsDialogType.value = 'run'
+    paramsDialogTitle.value = `运行参数 - ${row.cameraName}`
+    paramsContent.value = row.runtimeParams || ''
+    paramsDialogVisible.value = true
+  }
+
+  async function handleSaveParams() {
+    if (!paramsCamera.value) return
+    paramsSubmitting.value = true
+    try {
+      const data: CameraUpdateRequest = { id: paramsCamera.value.id }
+      if (paramsDialogType.value === 'config') {
+        data.paramConfig = paramsContent.value
+      } else {
+        data.runtimeParams = paramsContent.value
+      }
+      const res = await adminUpdateCamera(data)
+      if (res.success) {
+        ElMessage.success('保存成功')
+        paramsDialogVisible.value = false
+        if (paramsDialogType.value === 'config') {
+          paramsCamera.value.configParams = paramsContent.value
+        } else {
+          paramsCamera.value.runParams = paramsContent.value
+        }
+      } else {
+        ElMessage.error(res.errMessage || '保存失败')
+      }
+    } catch (error) {
+      console.error('保存参数失败', error)
+      ElMessage.error('保存失败')
+    } finally {
+      paramsSubmitting.value = false
+    }
+  }
+
+  function handleCameraList(row: LssNodeDTO) {
+    cameraSearchForm.cameraId = ''
+    cameraSearchForm.cameraName = ''
+    cameraSearchForm.status = ''
+    deviceActiveTab.value = 'camera'
+    cameraDrawerVisible.value = true
+    loadCameraList()
+  }
+
+  function resetCameraSearch() {
+    cameraSearchForm.cameraId = ''
+    cameraSearchForm.cameraName = ''
+    cameraSearchForm.status = ''
+  }
+
+  return {
+    cameraDrawerVisible,
+    cameraLoading,
+    deviceActiveTab,
+    cameraList,
+    cameraVendorList,
+    cameraCurrentPage,
+    cameraPageSize,
+    cameraTotal,
+    cameraTableHeight,
+    cameraSearchForm,
+    cameraDialogVisible,
+    cameraFormRef,
+    isEditCamera,
+    cameraSubmitting,
+    cameraForm,
+    cameraRules,
+    paramsDialogVisible,
+    paramsDialogTitle,
+    paramsDialogType,
+    paramsContent,
+    paramsSubmitting,
+    loadCameraList,
+    handleCameraSearch,
+    handleCameraReset,
+    handleCameraSizeChange,
+    handleCameraPageChange,
+    handleVendorSelect,
+    generateCameraId,
+    handleAddCamera,
+    handleEditCamera,
+    handleSubmitCamera,
+    handleDeleteCamera,
+    handleViewCamera,
+    handleViewConfig,
+    handleViewRunParams,
+    handleSaveParams,
+    handleCameraList,
+    resetCameraSearch
+  }
+}

+ 178 - 0
src/views/lss/composables/useCredentials.ts

@@ -0,0 +1,178 @@
+import { ref, reactive, computed, watch } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import type { FormInstance, FormRules } from 'element-plus'
+import { useI18n } from 'vue-i18n'
+import { getCredentials, addCredential, updateCredential, deleteCredential } from '@/api/camera-scan'
+import type { CameraCredentialDTO } from '@/types'
+
+export function useCredentials() {
+  const { t } = useI18n({ useScope: 'global' })
+
+  const credentialDrawerVisible = ref(false)
+  const credentialLoading = ref(false)
+  const credentials = ref<CameraCredentialDTO[]>([])
+  const credentialSearchForm = reactive({ username: '', password: '' })
+
+  const filteredCredentials = computed(() => {
+    let list = credentials.value
+    if (credentialSearchForm.username) {
+      list = list.filter((c) => c.username.includes(credentialSearchForm.username))
+    }
+    if (credentialSearchForm.password) {
+      list = list.filter((c) => c.password.includes(credentialSearchForm.password))
+    }
+    return list
+  })
+
+  async function loadCredentials() {
+    credentialLoading.value = true
+    try {
+      const res = await getCredentials()
+      if (res.success) {
+        credentials.value = res.data || []
+      } else {
+        ElMessage.error(res.errMessage || t('获取凭证列表失败'))
+      }
+    } catch (error) {
+      console.error('获取凭证列表失败', error)
+    } finally {
+      credentialLoading.value = false
+    }
+  }
+
+  function handleCredentialReset() {
+    credentialSearchForm.username = ''
+    credentialSearchForm.password = ''
+  }
+
+  // 凭证编辑
+  const credentialDialogVisible = ref(false)
+  const isEditCredential = ref(false)
+  const credentialSubmitting = ref(false)
+  const currentCredential = ref<CameraCredentialDTO | null>(null)
+  const credentialFormRef = ref<FormInstance>()
+  const credentialForm = reactive({
+    name: '',
+    username: '',
+    password: '',
+    vendor: '',
+    priority: 0,
+    enabled: true,
+    remark: ''
+  })
+
+  const credentialRules = computed<FormRules>(() => ({
+    name: [{ required: true, message: t('请输入凭证名称'), trigger: 'blur' }],
+    username: [{ required: true, message: t('请输入用户名'), trigger: 'blur' }],
+    password: [{ required: true, message: t('请输入密码'), trigger: 'blur' }]
+  }))
+
+  function resetCredentialForm() {
+    credentialForm.name = ''
+    credentialForm.username = ''
+    credentialForm.password = ''
+    credentialForm.vendor = ''
+    credentialForm.priority = 0
+    credentialForm.enabled = true
+    credentialForm.remark = ''
+    credentialFormRef.value?.clearValidate()
+  }
+
+  function handleAddCredential() {
+    isEditCredential.value = false
+    currentCredential.value = null
+    resetCredentialForm()
+    credentialDialogVisible.value = true
+  }
+
+  function handleEditCredential(row: CameraCredentialDTO) {
+    isEditCredential.value = true
+    currentCredential.value = row
+    credentialForm.name = row.name
+    credentialForm.username = row.username
+    credentialForm.password = row.password
+    credentialForm.vendor = row.vendor || ''
+    credentialForm.priority = row.priority || 0
+    credentialForm.enabled = row.enabled
+    credentialForm.remark = row.remark || ''
+    credentialDialogVisible.value = true
+  }
+
+  async function handleSubmitCredential() {
+    if (!credentialFormRef.value) return
+    await credentialFormRef.value.validate(async (valid) => {
+      if (!valid) return
+      credentialSubmitting.value = true
+      try {
+        if (isEditCredential.value && currentCredential.value) {
+          const res = await updateCredential({
+            id: currentCredential.value.id,
+            ...credentialForm
+          })
+          if (res.success) {
+            ElMessage.success(t('更新成功'))
+            credentialDialogVisible.value = false
+            loadCredentials()
+          } else {
+            ElMessage.error(res.errMessage || t('更新失败'))
+          }
+        } else {
+          const res = await addCredential({ ...credentialForm })
+          if (res.success) {
+            ElMessage.success(t('新增成功'))
+            credentialDialogVisible.value = false
+            loadCredentials()
+          } else {
+            ElMessage.error(res.errMessage || t('新增失败'))
+          }
+        }
+      } catch (error) {
+        console.error('保存凭证失败', error)
+        ElMessage.error(t('操作失败'))
+      } finally {
+        credentialSubmitting.value = false
+      }
+    })
+  }
+
+  async function handleDeleteCredential(row: CameraCredentialDTO) {
+    try {
+      await ElMessageBox.confirm(t('确定要删除该凭证吗?'), t('提示'), { type: 'warning' })
+      const res = await deleteCredential(row.id)
+      if (res.success) {
+        ElMessage.success(t('删除凭证成功'))
+        loadCredentials()
+      } else {
+        ElMessage.error(res.errMessage || t('删除失败'))
+      }
+    } catch (error) {
+      if (error !== 'cancel') {
+        console.error('删除凭证失败', error)
+      }
+    }
+  }
+
+  // 打开抽屉时自动加载
+  watch(credentialDrawerVisible, (val) => {
+    if (val) loadCredentials()
+  })
+
+  return {
+    credentialDrawerVisible,
+    credentialLoading,
+    credentialSearchForm,
+    filteredCredentials,
+    credentialDialogVisible,
+    isEditCredential,
+    credentialSubmitting,
+    credentialFormRef,
+    credentialForm,
+    credentialRules,
+    loadCredentials,
+    handleCredentialReset,
+    handleAddCredential,
+    handleEditCredential,
+    handleSubmitCredential,
+    handleDeleteCredential
+  }
+}

+ 90 - 0
src/views/lss/composables/useFormatters.ts

@@ -0,0 +1,90 @@
+import { formatTime } from '@/utils/dayjs'
+import type { LssNodeDTO, LssNodeStatus, LssHeartbeatStatus, CameraInfoDTO } from '@/types'
+
+// 格式化状态显示
+export function formatStatus(status: LssNodeStatus | undefined): string {
+  switch (status) {
+    case 'active':
+      return '在线'
+    case 'hold':
+      return '离线'
+    case 'dead':
+      return '离线'
+    default:
+      return '离线'
+  }
+}
+
+// 获取状态标签类型
+export function getStatusTagType(status: LssNodeStatus | undefined): 'success' | 'danger' | 'warning' | 'info' {
+  switch (status) {
+    case 'active':
+      return 'success'
+    case 'hold':
+      return 'danger'
+    case 'dead':
+      return 'warning'
+    default:
+      return 'info'
+  }
+}
+
+// 格式化摄像头状态
+export function formatCameraStatus(row: CameraInfoDTO): string {
+  if (row.status === 'active') {
+    return `active [${formatTime(row.updatedAt)}]`
+  }
+  if (row.status === 'hold') {
+    return `hold [${formatTime(row.updatedAt)}]`
+  }
+  return `dead (离线)`
+}
+
+// 格式化心跳状态
+export function formatHeartbeat(lss: LssNodeDTO | null | undefined): string {
+  if (!lss) return '-'
+  const status = lss.heartbeat || (lss.status === 'active' ? 'active' : lss.status === 'hold' ? 'hold' : 'dead')
+  const time = lss.heartbeatTime || lss.updatedAt
+  if (status === 'active') {
+    return `active [${formatTime(time)}]`
+  }
+  if (status === 'hold') {
+    return `hold [${formatTime(time)}]`
+  }
+  return `dead (离线)`
+}
+
+// 获取心跳状态样式类
+export function getHeartbeatClass(status: LssHeartbeatStatus | undefined): string {
+  switch (status) {
+    case 'active':
+      return 'status-active'
+    case 'hold':
+      return 'status-hold'
+    case 'dead':
+    default:
+      return 'status-dead'
+  }
+}
+
+// 格式化品牌
+export function formatBrand(brand: string | undefined): string {
+  const brandMap: Record<string, string> = {
+    hikvision: '海康威视',
+    dahua: '大华',
+    uniview: '宇视',
+    other: '其他'
+  }
+  return brand ? brandMap[brand] || brand.toUpperCase() : '-'
+}
+
+// 验证 JSON 格式
+export function isValidJson(str: string): boolean {
+  if (!str || !str.trim()) return true
+  try {
+    JSON.parse(str)
+    return true
+  } catch {
+    return false
+  }
+}

+ 229 - 0
src/views/lss/composables/useLssList.ts

@@ -0,0 +1,229 @@
+import { ref, reactive, computed, watch } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import type { FormInstance } from 'element-plus'
+import { useI18n } from 'vue-i18n'
+import { listLssNodes, deleteLssNode, setLssNodeEnabled, updateLssNode } from '@/api/lss'
+import type { LssNodeDTO, LssNodeStatus, LssNodeListRequest } from '@/types'
+
+export function useLssList() {
+  const { t } = useI18n({ useScope: 'global' })
+
+  const loading = ref(false)
+  const lssList = ref<(LssNodeDTO & { _switching?: boolean })[]>([])
+  const tableRef = ref()
+
+  // 抽屉状态
+  const detailDrawerVisible = ref(false)
+  const currentLss = ref<LssNodeDTO | null>(null)
+
+  // LSS 编辑抽屉状态
+  const lssEditDrawerVisible = ref(false)
+  const lssUpdating = ref(false)
+  const editActiveTab = ref('detail')
+  const lssEditFormRef = ref<FormInstance>()
+  const lssEditForm = reactive({
+    lssName: '',
+    address: '',
+    ip: '',
+    ably: ''
+  })
+
+  const editDrawerSize = computed(() => {
+    return editActiveTab.value === 'detail' ? '800px' : '80%'
+  })
+
+  // 排序状态
+  const sortState = reactive<{
+    sortBy: string
+    sortDir: 'ASC' | 'DESC' | undefined
+  }>({
+    sortBy: '',
+    sortDir: undefined
+  })
+
+  // 搜索表单
+  const searchForm = reactive<{
+    lssId: string
+    lssName: string
+    status: LssNodeStatus | ''
+  }>({
+    lssId: '',
+    lssName: '',
+    status: ''
+  })
+
+  // 分页
+  const currentPage = ref(1)
+  const pageSize = ref(15)
+  const total = ref(0)
+
+  async function getList() {
+    loading.value = true
+    try {
+      const params: LssNodeListRequest = {
+        page: currentPage.value,
+        size: pageSize.value
+      }
+      if (searchForm.lssId) params.lssId = searchForm.lssId
+      if (searchForm.lssName) params.lssName = searchForm.lssName
+      if (searchForm.status) params.status = searchForm.status
+      if (sortState.sortBy) {
+        params.sortBy = sortState.sortBy
+        params.sortDir = sortState.sortDir
+      }
+
+      const res = await listLssNodes(params)
+      if (res.success && res.data) {
+        lssList.value = res.data.list
+        total.value = res.data.total || 0
+      } else {
+        ElMessage.error(res.errMessage || '获取列表失败')
+      }
+    } catch (error) {
+      console.error('获取 LSS 列表失败', error)
+      ElMessage.error('获取列表失败')
+    } finally {
+      loading.value = false
+    }
+  }
+
+  function handleSearch() {
+    currentPage.value = 1
+    getList()
+  }
+
+  function handleReset() {
+    searchForm.lssId = ''
+    searchForm.lssName = ''
+    searchForm.status = ''
+    sortState.sortBy = ''
+    sortState.sortDir = undefined
+    currentPage.value = 1
+    getList()
+  }
+
+  function handleSortChange({ prop, order }: { prop: string; order: 'ascending' | 'descending' | null }) {
+    sortState.sortBy = prop || ''
+    sortState.sortDir = order === 'ascending' ? 'ASC' : order === 'descending' ? 'DESC' : undefined
+    getList()
+  }
+
+  function handleSizeChange(val: number) {
+    pageSize.value = val
+    currentPage.value = 1
+    getList()
+  }
+
+  function handleCurrentChange(val: number) {
+    currentPage.value = val
+    getList()
+  }
+
+  function handleViewDetail(row: LssNodeDTO) {
+    currentLss.value = row
+    detailDrawerVisible.value = true
+  }
+
+  function handleEdit(row: LssNodeDTO, tab: 'detail' | 'camera' | 'pusher', onOpen?: () => void) {
+    currentLss.value = row
+    lssEditForm.lssName = row.lssName || ''
+    lssEditForm.address = row.address || ''
+    lssEditForm.ably = JSON.stringify(row.ably)
+    editActiveTab.value = tab
+    lssEditDrawerVisible.value = true
+    onOpen?.()
+  }
+
+  async function handleUpdateLss() {
+    if (!currentLss.value) return
+    lssUpdating.value = true
+    try {
+      const res = await updateLssNode({
+        lssId: currentLss.value.lssId,
+        lssName: lssEditForm.lssName,
+        address: lssEditForm.address,
+        ablyInfo: lssEditForm.ably
+      })
+      if (res.success) {
+        ElMessage.success('更新成功')
+        lssEditDrawerVisible.value = false
+        getList()
+      } else {
+        ElMessage.error(res.errMessage || '更新失败')
+      }
+    } catch (error) {
+      console.error('更新 LSS 失败', error)
+      ElMessage.error('更新失败')
+    } finally {
+      lssUpdating.value = false
+    }
+  }
+
+  async function handleToggleEnabled(row: LssNodeDTO & { _switching?: boolean }, enabled: boolean) {
+    row._switching = true
+    try {
+      const res = await setLssNodeEnabled(row.lssId, enabled)
+      if (res.success) {
+        ElMessage.success(enabled ? '已启用' : '已禁用')
+      } else {
+        row.enabled = !enabled
+        ElMessage.error(res.errMessage || '操作失败')
+      }
+    } catch (error) {
+      row.enabled = !enabled
+      console.error('切换启用状态失败', error)
+      ElMessage.error('操作失败')
+    } finally {
+      row._switching = false
+    }
+  }
+
+  async function handleDelete(row: LssNodeDTO) {
+    try {
+      await ElMessageBox.confirm(`确定要删除 LSS 节点 "${row.lssName}" 吗?`, '提示', {
+        type: 'warning'
+      })
+      const res = await deleteLssNode(row.lssId)
+      if (res.success) {
+        ElMessage.success('删除成功')
+        getList()
+      } else {
+        ElMessage.error(res.errMessage || '删除失败')
+      }
+    } catch (error) {
+      if (error !== 'cancel') {
+        console.error('删除失败', error)
+        ElMessage.error('删除失败')
+      }
+    }
+  }
+
+  return {
+    loading,
+    lssList,
+    tableRef,
+    detailDrawerVisible,
+    currentLss,
+    lssEditDrawerVisible,
+    lssUpdating,
+    editActiveTab,
+    lssEditFormRef,
+    lssEditForm,
+    editDrawerSize,
+    searchForm,
+    currentPage,
+    pageSize,
+    total,
+    getList,
+    handleSearch,
+    handleReset,
+    handleSortChange,
+    handleSizeChange,
+    handleCurrentChange,
+    handleViewDetail,
+    handleEdit,
+    handleUpdateLss,
+    handleToggleEnabled,
+    handleDelete
+  }
+}

+ 121 - 0
src/views/lss/composables/useScanDevices.ts

@@ -0,0 +1,121 @@
+import { ref } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { useI18n } from 'vue-i18n'
+import { scanDevices, getDiscoveredDevices, triggerMatch } from '@/api/camera-scan'
+import type { LssNodeDTO, DiscoveredCameraDTO } from '@/types'
+
+export function useScanDevices() {
+  const { t } = useI18n({ useScope: 'global' })
+
+  const scanDrawerVisible = ref(false)
+  const scanLoading = ref(false)
+  const matchLoading = ref(false)
+  const scanMatched = ref(false)
+  const discoveredDevices = ref<DiscoveredCameraDTO[]>([])
+  const scanLssId = ref('')
+
+  async function handleScanDevices(row: LssNodeDTO) {
+    scanLssId.value = row.lssId
+    scanMatched.value = false
+
+    // 先尝试获取已有的发现设备
+    try {
+      const res = await getDiscoveredDevices(row.lssId)
+      if (res.success && res.data && res.data.length > 0) {
+        discoveredDevices.value = res.data
+        scanDrawerVisible.value = true
+        return
+      }
+    } catch (error) {
+      console.error('获取发现设备失败', error)
+    }
+
+    // 没有扫描结果,弹确认框
+    try {
+      await ElMessageBox.confirm(t('当前 LSS 节点尚未扫描设备,是否开启扫描?'), t('扫描设备'), {
+        confirmButtonText: t('开始扫描'),
+        cancelButtonText: t('取消'),
+        type: 'info'
+      })
+    } catch {
+      return
+    }
+
+    // 用户确认,触发扫描
+    scanDrawerVisible.value = true
+    scanLoading.value = true
+    try {
+      const res = await scanDevices(row.lssId)
+      if (res.success) {
+        ElMessage.success(t('扫描完成'))
+      } else {
+        ElMessage.error(res.errMessage || t('扫描失败'))
+      }
+      await loadDiscoveredDevices()
+    } catch (error) {
+      console.error('扫描失败', error)
+      ElMessage.error(t('扫描失败'))
+    } finally {
+      scanLoading.value = false
+    }
+  }
+
+  async function loadDiscoveredDevices() {
+    if (!scanLssId.value) return
+    scanLoading.value = true
+    try {
+      const res = await getDiscoveredDevices(scanLssId.value)
+      if (res.success) {
+        discoveredDevices.value = res.data || []
+      } else {
+        ElMessage.error(res.errMessage || t('获取发现设备失败'))
+      }
+    } catch (error) {
+      console.error('获取发现设备失败', error)
+    } finally {
+      scanLoading.value = false
+    }
+  }
+
+  async function handleTriggerMatch() {
+    if (!scanLssId.value) return
+    matchLoading.value = true
+    try {
+      const res = await triggerMatch(scanLssId.value)
+      if (res.success) {
+        ElMessage.success(t('匹配完成'))
+        scanMatched.value = true
+        await loadDiscoveredDevices()
+      } else {
+        ElMessage.error(res.errMessage || t('匹配失败'))
+      }
+    } catch (error) {
+      console.error('匹配失败', error)
+      ElMessage.error(t('匹配失败'))
+    } finally {
+      matchLoading.value = false
+    }
+  }
+
+  async function handleRematch() {
+    scanMatched.value = false
+    await handleTriggerMatch()
+  }
+
+  async function handleBindDevice(row: DiscoveredCameraDTO) {
+    console.log('bind device', row)
+    ElMessage.info('绑定功能开发中')
+  }
+
+  return {
+    scanDrawerVisible,
+    scanLoading,
+    matchLoading,
+    scanMatched,
+    discoveredDevices,
+    handleScanDevices,
+    handleTriggerMatch,
+    handleRematch,
+    handleBindDevice
+  }
+}

+ 119 - 1016
src/views/lss/index.vue

@@ -663,7 +663,7 @@
           <el-table-column prop="ip" label="IP" min-width="120" show-overflow-tooltip />
           <el-table-column prop="port" :label="t('端口')" width="80" align="center" />
           <el-table-column prop="deviceName" :label="t('设备名称')" min-width="140" show-overflow-tooltip />
-          <el-table-column v-if="scanMatched" :label="t('匹配状态')" width="100" align="center">
+          <el-table-column :label="t('匹配状态')" width="100" align="center">
             <template #default="{ row }">
               <Icon
                 v-if="row.matchStatus === 'MATCHED'"
@@ -689,7 +689,7 @@
               <span v-else style="color: #909399">{{ t('待匹配') }}</span>
             </template>
           </el-table-column>
-          <el-table-column v-if="scanMatched" :label="t('操作')" width="80" align="center">
+          <el-table-column :label="t('操作')" width="80" align="center">
             <template #default="{ row }">
               <el-button
                 v-if="row.matchStatus === 'MATCHED' && !row.bound"
@@ -838,1039 +838,142 @@
 </template>
 
 <script setup lang="ts">
-import { ref, reactive, onMounted, computed, watch } from 'vue'
-// Element Plus icons removed - using Iconify instead
-import { ElMessage, ElMessageBox } from 'element-plus'
+import { onMounted, watch } from 'vue'
 import { Icon } from '@iconify/vue'
-import type { FormInstance, FormRules } from 'element-plus'
 import { useI18n } from 'vue-i18n'
 import { formatTime } from '@/utils/dayjs'
-import { useRouter } from 'vue-router'
-import { listLssNodes, deleteLssNode, setLssNodeEnabled, updateLssNode } from '@/api/lss'
-import { adminListCameras, adminAddCamera, adminUpdateCamera, adminDeleteCamera, adminGetCamera } from '@/api/camera'
-import { listCameraVendors } from '@/api/camera-vendor'
-import {
-  scanDevices,
-  getDiscoveredDevices,
-  triggerMatch,
-  bindDevice,
-  getCredentials,
-  addCredential,
-  updateCredential,
-  deleteCredential
-} from '@/api/camera-scan'
 import CodeEditor from '@/components/CodeEditor.vue'
-import type {
-  LssNodeDTO,
-  LssNodeStatus,
-  LssNodeListRequest,
-  LssHeartbeatStatus,
-  CameraInfoDTO,
-  CameraAddRequest,
-  CameraUpdateRequest,
-  CameraVendorDTO,
-  IAbly,
-  CameraHeartbeatStatus,
-  DiscoveredCameraDTO,
-  CameraCredentialDTO
-} from '@/types'
+import {
+  formatStatus,
+  getStatusTagType,
+  formatCameraStatus,
+  formatHeartbeat,
+  getHeartbeatClass,
+  formatBrand
+} from './composables/useFormatters'
+import { useLssList } from './composables/useLssList'
+import { useCameraList } from './composables/useCameraList'
+import { useScanDevices } from './composables/useScanDevices'
+import { useCredentials } from './composables/useCredentials'
 
 const { t } = useI18n({ useScope: 'global' })
 
-const router = useRouter()
-
-// 格式化状态显示
-function formatStatus(status: LssNodeStatus | undefined): string {
-  switch (status) {
-    case 'active':
-      return '在线'
-    case 'hold':
-      return '离线'
-    case 'dead':
-      return '离线'
-    default:
-      return '离线'
-  }
-}
-
-// 获取状态标签类型
-function getStatusTagType(status: LssNodeStatus | undefined): 'success' | 'danger' | 'warning' | 'info' {
-  switch (status) {
-    case 'active':
-      return 'success'
-    case 'hold':
-      return 'danger'
-    case 'dead':
-      return 'warning'
-    default:
-      return 'info'
-  }
-}
-
-// 格式化摄像头状态
-function formatCameraStatus(row: CameraInfoDTO): string {
-  if (row.status === 'active') {
-    // 大约5秒钟
-    return `active [${formatTime(row.updatedAt)}]`
-  }
-  if (row.status === 'hold') {
-    // 大约5分钟没有返回
-    return `hold [${formatTime(row.updatedAt)}]`
-  }
-  // 大约10分钟没有返回
-  return `dead (离线)`
-}
-
-// 当前激活的摄像头 ID
-const activeCameraId = ref<number | null>(null)
-
-async function handleViewCamera(row: CameraInfoDTO) {
-  // 如果没有 streamSn,显示提示对话框
-  if (!row.streamSn) {
-    try {
-      await ElMessageBox.confirm(t('请先新增 Live Stream,才能进行后续操作。'), t('尚未建立 Live Stream'), {
-        confirmButtonText: t('新增 Live Stream'),
-        cancelButtonText: t('取消'),
-        type: 'warning',
-        center: true,
-        customClass: 'live-stream-dialog',
-        distinguishCancelAndClose: true
-      })
-      router.push(`/live-stream-manage/list?cameraId=${row.cameraId}&lssId=${row.lssId}&action=create`)
-    } catch {
-      // 用户点击了取消,不做任何操作
-    }
-    return
-  }
-  // 有 streamSn,正常跳转
-  router.push(`/live-stream-manage/list?cameraId=${row.cameraId}`)
-}
-
-// 格式化品牌
-function formatBrand(brand: string | undefined): string {
-  const brandMap: Record<string, string> = {
-    hikvision: '海康威视',
-    dahua: '大华',
-    uniview: '宇视',
-    other: '其他'
-  }
-  return brand ? brandMap[brand] || brand.toUpperCase() : '-'
-}
-
-// 验证 JSON 格式
-function isValidJson(str: string): boolean {
-  if (!str || !str.trim()) return true // 空值视为有效
-  try {
-    JSON.parse(str)
-    return true
-  } catch {
-    return false
-  }
-}
-
-// 格式化心跳状态
-function formatHeartbeat(lss: LssNodeDTO | null | undefined): string {
-  if (!lss) return '-'
-  const status = lss.heartbeat || (lss.status === 'active' ? 'active' : lss.status === 'hold' ? 'hold' : 'dead')
-  const time = lss.heartbeatTime || lss.updatedAt
-  if (status === 'active') {
-    return `active [${formatTime(time)}]`
-  }
-  if (status === 'hold') {
-    return `hold [${formatTime(time)}]`
-  }
-  return `dead (离线)`
-}
-
-// 获取心跳状态样式类
-function getHeartbeatClass(status: LssHeartbeatStatus | undefined): string {
-  switch (status) {
-    case 'active':
-      return 'status-active'
-    case 'hold':
-      return 'status-hold'
-    case 'dead':
-    default:
-      return 'status-dead'
-  }
-}
-
-// 查看参数配置
-function handleViewConfig(row: CameraInfoDTO) {
-  paramsCamera.value = row
-  paramsDialogType.value = 'config'
-  paramsDialogTitle.value = `参数配置 - ${row.cameraName}`
-  paramsContent.value = row.paramConfig || ''
-  paramsDialogVisible.value = true
-}
-
-// 查看运行参数
-function handleViewRunParams(row: CameraInfoDTO) {
-  paramsCamera.value = row
-  paramsDialogType.value = 'run'
-  paramsDialogTitle.value = `运行参数 - ${row.cameraName}`
-  paramsContent.value = row.runtimeParams || ''
-  paramsDialogVisible.value = true
-}
-
-// 保存参数配置/运行参数
-async function handleSaveParams() {
-  if (!paramsCamera.value) return
-
-  paramsSubmitting.value = true
-  try {
-    const data: CameraUpdateRequest = {
-      id: paramsCamera.value.id
-    }
-    if (paramsDialogType.value === 'config') {
-      data.paramConfig = paramsContent.value
-    } else {
-      data.runtimeParams = paramsContent.value
-    }
-
-    const res = await adminUpdateCamera(data)
-    if (res.success) {
-      ElMessage.success('保存成功')
-      paramsDialogVisible.value = false
-      // 更新本地数据
-      if (paramsDialogType.value === 'config') {
-        paramsCamera.value.configParams = paramsContent.value
-      } else {
-        paramsCamera.value.runParams = paramsContent.value
-      }
-    } else {
-      ElMessage.error(res.errMessage || '保存失败')
-    }
-  } catch (error) {
-    console.error('保存参数失败', error)
-    ElMessage.error('保存失败')
-  } finally {
-    paramsSubmitting.value = false
-  }
-}
-
-const loading = ref(false)
-const lssList = ref<(LssNodeDTO & { _switching?: boolean })[]>([])
-const tableRef = ref()
-
-// 抽屉状态
-const detailDrawerVisible = ref(false)
-const currentLss = ref<LssNodeDTO | null>(null)
-
-// LSS 编辑抽屉状态
-const lssEditDrawerVisible = ref(false)
-const lssUpdating = ref(false)
-const editActiveTab = ref('detail')
-const lssEditFormRef = ref<FormInstance>()
-const lssEditForm = reactive({
-  lssName: '',
-  address: '',
-  ip: '',
-  ably: ''
-})
-
-// 根据当前 tab 计算抽屉宽度
-const editDrawerSize = computed(() => {
-  return editActiveTab.value === 'detail' ? '800px' : '80%'
-})
-
-// 设备列表抽屉状态
-const cameraDrawerVisible = ref(false)
-const cameraLoading = ref(false)
-const deviceActiveTab = ref('camera')
-const cameraList = ref<CameraInfoDTO[]>([])
-const cameraVendorList = ref<CameraVendorDTO[]>([])
-
-// 摄像头分页
-const cameraCurrentPage = ref(1)
-const cameraPageSize = ref(15)
-const cameraTotal = ref(0)
-
-// 摄像头表格高度 (视口高度 - 顶部导航 - tabs - 搜索栏 - 分页 - padding)
-const cameraTableHeight = computed(() => {
-  return 'calc(100vh - 238px)'
-})
-
-// 摄像头搜索表单
-const cameraSearchForm = reactive({
-  cameraId: '',
-  cameraName: '',
-  status: '' as CameraHeartbeatStatus | ''
-})
-
-// 摄像头编辑弹窗状态
-const cameraDialogVisible = ref(false)
-const cameraFormRef = ref<FormInstance>()
-const isEditCamera = ref(false)
-const cameraSubmitting = ref(false)
-const currentCamera = ref<CameraInfoDTO | null>(null)
-
-// 参数配置/运行参数弹窗状态
-const paramsDialogVisible = ref(false)
-const paramsDialogTitle = ref('')
-const paramsDialogType = ref<'config' | 'run'>('config')
-const paramsContent = ref('')
-const paramsSubmitting = ref(false)
-const paramsCamera = ref<CameraInfoDTO | null>(null)
-
-// 摄像头表单
-const cameraForm = reactive({
-  selectedVendorId: null as number | null,
-  cameraId: '',
-  cameraName: '',
-  vendorName: '',
-  model: '',
-  ip: '',
-  port: 80,
-  username: '',
-  password: '',
-  brand: '',
-  capability: 'switch_only' as 'switch_only' | 'ptz_enabled',
-  rtspUrl: '',
-  channelNo: '',
-  remark: '',
-  enabled: true,
-  paramConfig: '',
-  runtimeParams: '',
-  createdAt: '',
-  updatedAt: ''
-})
-
-// 摄像头表单验证规则(动态)
-const cameraRules = computed<FormRules>(() => ({
-  cameraId: [{ required: true, message: t('请输入设备ID'), trigger: 'blur' }]
-}))
-
-// 排序状态
-const sortState = reactive<{
-  sortBy: string
-  sortDir: 'ASC' | 'DESC' | undefined
-}>({
-  sortBy: '',
-  sortDir: undefined
-})
-
-// 搜索表单
-const searchForm = reactive<{
-  lssId: string
-  lssName: string
-  status: LssNodeStatus | ''
-}>({
-  lssId: '',
-  lssName: '',
-  status: ''
-})
-
-// 分页相关
-const currentPage = ref(1)
-const pageSize = ref(15)
-const total = ref(0)
-
-async function getList() {
-  loading.value = true
-  try {
-    const params: LssNodeListRequest = {
-      page: currentPage.value,
-      size: pageSize.value
-    }
-
-    if (searchForm.lssId) {
-      params.lssId = searchForm.lssId
-    }
-
-    if (searchForm.lssName) {
-      params.lssName = searchForm.lssName
-    }
-
-    if (searchForm.status) {
-      params.status = searchForm.status
-    }
-
-    if (sortState.sortBy) {
-      params.sortBy = sortState.sortBy
-      params.sortDir = sortState.sortDir
-    }
-
-    const res = await listLssNodes(params)
-    if (res.success && res.data) {
-      lssList.value = res.data.list
-      total.value = res.data.total || 0
-    } else {
-      ElMessage.error(res.errMessage || '获取列表失败')
-    }
-  } catch (error) {
-    console.error('获取 LSS 列表失败', error)
-    ElMessage.error('获取列表失败')
-  } finally {
-    loading.value = false
-  }
-}
-
-function handleSearch() {
-  currentPage.value = 1
-  getList()
-}
-
-function handleReset() {
-  searchForm.lssId = ''
-  searchForm.lssName = ''
-  searchForm.status = ''
-  sortState.sortBy = ''
-  sortState.sortDir = undefined
-  currentPage.value = 1
-  getList()
-}
-
-function handleSortChange({ prop, order }: { prop: string; order: 'ascending' | 'descending' | null }) {
-  sortState.sortBy = prop || ''
-  sortState.sortDir = order === 'ascending' ? 'ASC' : order === 'descending' ? 'DESC' : undefined
-  getList()
-}
-
-function handleSizeChange(val: number) {
-  pageSize.value = val
-  currentPage.value = 1
-  getList()
-}
-
-function handleCurrentChange(val: number) {
-  currentPage.value = val
-  getList()
-}
-
-function handleViewDetail(row: LssNodeDTO) {
-  currentLss.value = row
-  detailDrawerVisible.value = true
-}
-
-// ==================== 扫描设备相关 ====================
-const scanDrawerVisible = ref(false)
-const scanLoading = ref(false)
-const matchLoading = ref(false)
-const scanMatched = ref(false)
-const discoveredDevices = ref<DiscoveredCameraDTO[]>([])
-const scanLssId = ref('')
-
-async function handleScanDevices(row: LssNodeDTO) {
-  scanLssId.value = row.lssId
-  scanMatched.value = false
-
-  // 先尝试获取已有的发现设备
-  try {
-    const res = await getDiscoveredDevices(row.lssId)
-    if (res.success && res.data && res.data.length > 0) {
-      // 已有扫描结果,直接打开抽屉
-      discoveredDevices.value = res.data
-      scanDrawerVisible.value = true
-      return
-    }
-  } catch (error) {
-    console.error('获取发现设备失败', error)
-  }
-
-  // 没有扫描结果,弹确认框询问是否开启扫描
-  try {
-    await ElMessageBox.confirm(t('当前 LSS 节点尚未扫描设备,是否开启扫描?'), t('扫描设备'), {
-      confirmButtonText: t('开始扫描'),
-      cancelButtonText: t('取消'),
-      type: 'info'
-    })
-  } catch {
-    // 用户取消
-    return
-  }
-
-  // 用户确认,触发扫描
-  scanDrawerVisible.value = true
-  scanLoading.value = true
-  try {
-    const res = await scanDevices(row.lssId)
-    if (res.success) {
-      ElMessage.success(t('扫描完成'))
-    } else {
-      ElMessage.error(res.errMessage || t('扫描失败'))
-    }
-    // 刷新列表
-    await loadDiscoveredDevices()
-  } catch (error) {
-    console.error('扫描失败', error)
-    ElMessage.error(t('扫描失败'))
-  } finally {
-    scanLoading.value = false
-  }
-}
-
-async function loadDiscoveredDevices() {
-  if (!scanLssId.value) return
-  scanLoading.value = true
-  try {
-    const res = await getDiscoveredDevices(scanLssId.value)
-    if (res.success) {
-      discoveredDevices.value = res.data || []
-    } else {
-      ElMessage.error(res.errMessage || t('获取发现设备失败'))
-    }
-  } catch (error) {
-    console.error('获取发现设备失败', error)
-  } finally {
-    scanLoading.value = false
-  }
-}
-
-async function handleTriggerMatch() {
-  if (!scanLssId.value) return
-  matchLoading.value = true
-  try {
-    const res = await triggerMatch(scanLssId.value)
-    if (res.success) {
-      ElMessage.success(t('匹配完成'))
-      scanMatched.value = true
-      await loadDiscoveredDevices()
-    } else {
-      ElMessage.error(res.errMessage || t('匹配失败'))
-    }
-  } catch (error) {
-    console.error('匹配失败', error)
-    ElMessage.error(t('匹配失败'))
-  } finally {
-    matchLoading.value = false
-  }
-}
-
-async function handleRematch() {
-  scanMatched.value = false
-  await handleTriggerMatch()
-}
-
-async function handleBindDevice(row: DiscoveredCameraDTO) {
-  // 占位:后续接入真实 bind 接口
-  console.log('bind device', row)
-  ElMessage.info('绑定功能开发中')
-}
-
-// ==================== 凭证管理相关 ====================
-const credentialDrawerVisible = ref(false)
-const credentialLoading = ref(false)
-const credentials = ref<CameraCredentialDTO[]>([])
-const credentialSearchForm = reactive({ username: '', password: '' })
-
-const filteredCredentials = computed(() => {
-  let list = credentials.value
-  if (credentialSearchForm.username) {
-    list = list.filter((c) => c.username.includes(credentialSearchForm.username))
-  }
-  if (credentialSearchForm.password) {
-    list = list.filter((c) => c.password.includes(credentialSearchForm.password))
-  }
-  return list
-})
-
-async function loadCredentials() {
-  credentialLoading.value = true
-  try {
-    const res = await getCredentials()
-    if (res.success) {
-      credentials.value = res.data || []
-    } else {
-      ElMessage.error(res.errMessage || t('获取凭证列表失败'))
-    }
-  } catch (error) {
-    console.error('获取凭证列表失败', error)
-  } finally {
-    credentialLoading.value = false
-  }
-}
-
-function handleCredentialReset() {
-  credentialSearchForm.username = ''
-  credentialSearchForm.password = ''
-}
-
-// 凭证编辑
-const credentialDialogVisible = ref(false)
-const isEditCredential = ref(false)
-const credentialSubmitting = ref(false)
-const currentCredential = ref<CameraCredentialDTO | null>(null)
-const credentialFormRef = ref<FormInstance>()
-const credentialForm = reactive({
-  name: '',
-  username: '',
-  password: '',
-  vendor: '',
-  priority: 0,
-  enabled: true,
-  remark: ''
-})
-
-const credentialRules = computed<FormRules>(() => ({
-  name: [{ required: true, message: t('请输入凭证名称'), trigger: 'blur' }],
-  username: [{ required: true, message: t('请输入用户名'), trigger: 'blur' }],
-  password: [{ required: true, message: t('请输入密码'), trigger: 'blur' }]
-}))
-
-function resetCredentialForm() {
-  credentialForm.name = ''
-  credentialForm.username = ''
-  credentialForm.password = ''
-  credentialForm.vendor = ''
-  credentialForm.priority = 0
-  credentialForm.enabled = true
-  credentialForm.remark = ''
-  credentialFormRef.value?.clearValidate()
-}
-
-function handleAddCredential() {
-  isEditCredential.value = false
-  currentCredential.value = null
-  resetCredentialForm()
-  credentialDialogVisible.value = true
-}
-
-function handleEditCredential(row: CameraCredentialDTO) {
-  isEditCredential.value = true
-  currentCredential.value = row
-  credentialForm.name = row.name
-  credentialForm.username = row.username
-  credentialForm.password = row.password
-  credentialForm.vendor = row.vendor || ''
-  credentialForm.priority = row.priority || 0
-  credentialForm.enabled = row.enabled
-  credentialForm.remark = row.remark || ''
-  credentialDialogVisible.value = true
-}
-
-async function handleSubmitCredential() {
-  if (!credentialFormRef.value) return
-  await credentialFormRef.value.validate(async (valid) => {
-    if (!valid) return
-    credentialSubmitting.value = true
-    try {
-      if (isEditCredential.value && currentCredential.value) {
-        const res = await updateCredential({
-          id: currentCredential.value.id,
-          ...credentialForm
-        })
-        if (res.success) {
-          ElMessage.success(t('更新成功'))
-          credentialDialogVisible.value = false
-          loadCredentials()
-        } else {
-          ElMessage.error(res.errMessage || t('更新失败'))
-        }
-      } else {
-        const res = await addCredential({ ...credentialForm })
-        if (res.success) {
-          ElMessage.success(t('新增成功'))
-          credentialDialogVisible.value = false
-          loadCredentials()
-        } else {
-          ElMessage.error(res.errMessage || t('新增失败'))
-        }
-      }
-    } catch (error) {
-      console.error('保存凭证失败', error)
-      ElMessage.error(t('操作失败'))
-    } finally {
-      credentialSubmitting.value = false
-    }
-  })
-}
-
-async function handleDeleteCredential(row: CameraCredentialDTO) {
-  try {
-    await ElMessageBox.confirm(t('确定要删除该凭证吗?'), t('提示'), { type: 'warning' })
-    const res = await deleteCredential(row.id)
-    if (res.success) {
-      ElMessage.success(t('删除凭证成功'))
-      loadCredentials()
-    } else {
-      ElMessage.error(res.errMessage || t('删除失败'))
-    }
-  } catch (error) {
-    if (error !== 'cancel') {
-      console.error('删除凭证失败', error)
-    }
-  }
-}
-
-// 打开凭证抽屉时加载数据
-watch(credentialDrawerVisible, (val) => {
-  if (val) loadCredentials()
-})
-
-function handleEdit(row: LssNodeDTO, tab: 'detail' | 'camera' | 'pusher') {
+// ==================== LSS 列表 ====================
+const {
+  loading,
+  lssList,
+  tableRef,
+  detailDrawerVisible,
+  currentLss,
+  lssEditDrawerVisible,
+  lssUpdating,
+  editActiveTab,
+  lssEditFormRef,
+  lssEditForm,
+  editDrawerSize,
+  searchForm,
+  currentPage,
+  pageSize,
+  total,
+  getList,
+  handleSearch,
+  handleReset,
+  handleSortChange,
+  handleSizeChange,
+  handleCurrentChange,
+  handleUpdateLss,
+  handleDelete
+} = useLssList()
+
+// ==================== 摄像头列表 ====================
+const {
+  cameraDrawerVisible,
+  cameraLoading,
+  deviceActiveTab,
+  cameraList,
+  cameraCurrentPage,
+  cameraPageSize,
+  cameraTotal,
+  cameraTableHeight,
+  cameraSearchForm,
+  cameraDialogVisible,
+  cameraFormRef,
+  isEditCamera,
+  cameraSubmitting,
+  cameraForm,
+  cameraRules,
+  paramsDialogVisible,
+  paramsDialogTitle,
+  paramsDialogType,
+  paramsContent,
+  paramsSubmitting,
+  loadCameraList,
+  handleCameraSearch,
+  handleCameraReset,
+  handleCameraSizeChange,
+  handleCameraPageChange,
+  handleVendorSelect,
+  generateCameraId,
+  handleAddCamera,
+  handleEditCamera,
+  handleSubmitCamera,
+  handleDeleteCamera,
+  handleViewCamera,
+  handleViewConfig,
+  handleViewRunParams,
+  handleSaveParams,
+  resetCameraSearch
+} = useCameraList(currentLss)
+
+// ==================== 扫描设备 ====================
+const {
+  scanDrawerVisible,
+  scanLoading,
+  matchLoading,
+  scanMatched,
+  discoveredDevices,
+  handleScanDevices,
+  handleTriggerMatch,
+  handleRematch,
+  handleBindDevice
+} = useScanDevices()
+
+// ==================== 凭证管理 ====================
+const {
+  credentialDrawerVisible,
+  credentialLoading,
+  credentialSearchForm,
+  filteredCredentials,
+  credentialDialogVisible,
+  isEditCredential,
+  credentialSubmitting,
+  credentialFormRef,
+  credentialForm,
+  credentialRules,
+  loadCredentials,
+  handleCredentialReset,
+  handleAddCredential,
+  handleEditCredential,
+  handleSubmitCredential,
+  handleDeleteCredential
+} = useCredentials()
+
+// ==================== 页面级编排 ====================
+function handleEdit(row: any, tab: 'detail' | 'camera' | 'pusher') {
   currentLss.value = row
   lssEditForm.lssName = row.lssName || ''
   lssEditForm.address = row.address || ''
   lssEditForm.ably = JSON.stringify(row.ably)
   editActiveTab.value = tab
   lssEditDrawerVisible.value = true
-  // 每次打开抽屉都刷新摄像头列表
-  cameraSearchForm.cameraId = ''
-  cameraSearchForm.cameraName = ''
-  cameraSearchForm.status = ''
-  loadCameraList()
-}
-
-async function handleCameraList(row: LssNodeDTO) {
-  currentLss.value = row
-  cameraSearchForm.cameraId = ''
-  cameraSearchForm.cameraName = ''
-  cameraSearchForm.status = ''
-  deviceActiveTab.value = 'camera'
-  cameraDrawerVisible.value = true
-  await loadCameraList()
-}
-
-async function handleUpdateLss() {
-  if (!currentLss.value) return
-
-  lssUpdating.value = true
-  try {
-    const res = await updateLssNode({
-      lssId: currentLss.value.lssId,
-      lssName: lssEditForm.lssName,
-      address: lssEditForm.address,
-      ablyInfo: lssEditForm.ably
-    })
-    if (res.success) {
-      ElMessage.success('更新成功')
-      lssEditDrawerVisible.value = false
-      getList()
-    } else {
-      ElMessage.error(res.errMessage || '更新失败')
-    }
-  } catch (error) {
-    console.error('更新 LSS 失败', error)
-    ElMessage.error('更新失败')
-  } finally {
-    lssUpdating.value = false
-  }
-}
-
-async function loadCameraList() {
-  if (!currentLss.value) return
-  cameraLoading.value = true
-  cameraList.value = []
-
-  try {
-    const params: any = {
-      lssId: currentLss.value.lssId,
-      page: cameraCurrentPage.value,
-      size: cameraPageSize.value
-    }
-
-    if (cameraSearchForm.cameraId) {
-      params.cameraId = cameraSearchForm.cameraId
-    }
-
-    if (cameraSearchForm.cameraName) {
-      params.cameraName = cameraSearchForm.cameraName
-    }
-
-    if (cameraSearchForm.status) {
-      params.status = cameraSearchForm.status
-    }
-
-    const res = await adminListCameras(params)
-    if (res.success && res.data) {
-      cameraList.value = res.data.list || []
-      cameraTotal.value = res.data.total || 0
-    } else {
-      ElMessage.error(res.errMessage || '获取摄像头列表失败')
-    }
-  } catch (error) {
-    console.error('获取摄像头列表失败', error)
-    ElMessage.error('获取摄像头列表失败')
-  } finally {
-    cameraLoading.value = false
-  }
-}
-
-function handleCameraSearch() {
-  cameraCurrentPage.value = 1
-  loadCameraList()
-}
-
-function handleCameraReset() {
-  cameraSearchForm.cameraId = ''
-  cameraSearchForm.cameraName = ''
-  cameraSearchForm.status = ''
-  cameraCurrentPage.value = 1
-  loadCameraList()
-}
-
-function handleCameraSizeChange(val: number) {
-  cameraPageSize.value = val
-  cameraCurrentPage.value = 1
+  resetCameraSearch()
   loadCameraList()
 }
 
-function handleCameraPageChange(val: number) {
-  cameraCurrentPage.value = val
-  loadCameraList()
-}
-
-function resetCameraForm() {
-  cameraForm.selectedVendorId = null
-  cameraForm.cameraId = ''
-  cameraForm.cameraName = ''
-  cameraForm.vendorName = ''
-  cameraForm.model = ''
-  cameraForm.ip = ''
-  cameraForm.port = 80
-  cameraForm.username = ''
-  cameraForm.password = ''
-  cameraForm.brand = ''
-  cameraForm.capability = 'switch_only'
-  cameraForm.rtspUrl = ''
-  cameraForm.model = ''
-  cameraForm.channelNo = ''
-  cameraForm.remark = ''
-  cameraForm.enabled = true
-  cameraForm.paramConfig = ''
-  cameraForm.runtimeParams = ''
-  cameraForm.createdAt = ''
-  cameraForm.updatedAt = ''
-  cameraFormRef.value?.clearValidate()
-}
-
-async function loadCameraVendorList() {
-  try {
-    const res = await listCameraVendors({ enabled: true })
-    if (res.success && res.data) {
-      cameraVendorList.value = res.data.list || []
-    }
-  } catch (error) {
-    console.error('获取厂商列表失败', error)
-  }
-}
-
-function handleVendorSelect(vendorId: number) {
-  const vendor = cameraVendorList.value.find((v) => v.id === vendorId)
-  if (vendor) {
-    cameraForm.brand = vendor.code
-    // 设置厂商默认端口
-    if (vendor.defaultPort) {
-      cameraForm.port = vendor.defaultPort
-    }
-    // 根据厂商设置默认能力
-    cameraForm.capability = vendor.supportPtz ? 'ptz_enabled' : 'switch_only'
-  }
-}
-
-function generateCameraId() {
-  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
-  const prefix = 'CAM-'
-  let id = ''
-  for (let i = 0; i < 6; i++) {
-    id += chars.charAt(Math.floor(Math.random() * chars.length))
-  }
-  cameraForm.cameraId = prefix + id
-}
-
-async function handleAddCamera() {
-  isEditCamera.value = false
-  currentCamera.value = null
-  resetCameraForm()
-  await loadCameraVendorList()
-  cameraDialogVisible.value = true
-}
-
-async function handleEditCamera(row: CameraInfoDTO) {
-  isEditCamera.value = true
-
-  try {
-    // 通过 API 获取摄像头详情
-    const res = await adminGetCamera({ id: row.id })
-    if (!res.success || !res.data) {
-      ElMessage.error(res.errMessage || '获取摄像头详情失败')
-      return
-    }
-
-    const camera = res.data
-    currentCamera.value = camera
-    cameraForm.selectedVendorId = null
-    cameraForm.cameraId = camera.cameraId
-    cameraForm.cameraName = camera.cameraName
-    cameraForm.vendorName = camera.vendorName || ''
-    cameraForm.model = camera.model || ''
-    cameraForm.ip = camera.ip
-    cameraForm.port = camera.port || 80
-    cameraForm.username = camera.username || ''
-    cameraForm.password = ''
-    cameraForm.brand = camera.brand || ''
-    cameraForm.capability = camera.capability || 'switch_only'
-    cameraForm.rtspUrl = camera.rtspUrl || ''
-    cameraForm.model = camera.model || ''
-    cameraForm.channelNo = camera.channelNo || ''
-    cameraForm.remark = camera.remark || ''
-    cameraForm.createdAt = camera.createdAt || ''
-    cameraForm.updatedAt = camera.updatedAt || ''
-    cameraForm.enabled = camera.enabled
-    cameraForm.paramConfig = camera.paramConfig || ''
-    cameraForm.runtimeParams = camera.runtimeParams || ''
-
-    await loadCameraVendorList()
-    cameraDialogVisible.value = true
-  } catch (error) {
-    console.error('获取摄像头详情失败', error)
-    ElMessage.error('获取摄像头详情失败')
-  }
-}
-
-async function handleSubmitCamera() {
-  if (!cameraFormRef.value) return
-
-  await cameraFormRef.value.validate(async (valid) => {
-    if (!valid) return
-
-    // 验证 JSON 格式
-    if (!isValidJson(cameraForm.paramConfig)) {
-      ElMessage.error('参数配置格式错误,请输入有效的 JSON')
-      return
-    }
-    if (!isValidJson(cameraForm.runtimeParams)) {
-      ElMessage.error('设备运行参数格式错误,请输入有效的 JSON')
-      return
-    }
-
-    cameraSubmitting.value = true
-    try {
-      if (isEditCamera.value) {
-        // 编辑模式:更新摄像头信息
-        if (!currentCamera.value) {
-          ElMessage.error('摄像头信息错误')
-          return
-        }
-        const data: CameraUpdateRequest = {
-          id: currentCamera.value.id,
-          cameraName: cameraForm.cameraName,
-          vendorName: cameraForm.vendorName,
-          model: cameraForm.model,
-          port: cameraForm.port,
-          username: cameraForm.username,
-          brand: cameraForm.brand,
-          capability: cameraForm.capability,
-          lssId: currentLss.value?.lssId,
-          rtspUrl: cameraForm.rtspUrl,
-          channelNo: cameraForm.channelNo,
-          remark: cameraForm.remark,
-          enabled: cameraForm.enabled,
-          paramConfig: cameraForm.paramConfig,
-          runtimeParams: cameraForm.runtimeParams
-        }
-        if (cameraForm.password) {
-          data.password = cameraForm.password
-        }
-        const res = await adminUpdateCamera(data)
-        if (res.success) {
-          ElMessage.success('更新成功')
-          cameraDialogVisible.value = false
-          loadCameraList()
-        } else {
-          ElMessage.error(res.errMessage || '更新失败')
-        }
-      } else {
-        // 新增模式:创建摄像头并绑定到当前 LSS
-        const data: CameraAddRequest = {
-          cameraId: cameraForm.cameraId,
-          cameraName: cameraForm.cameraName,
-          vendorName: cameraForm.vendorName,
-          model: cameraForm.model,
-          paramConfig: cameraForm.paramConfig,
-          runtimeParams: cameraForm.runtimeParams,
-          lssId: currentLss.value?.lssId
-        }
-        const res = await adminAddCamera(data)
-        if (res.success) {
-          ElMessage.success('添加成功')
-          cameraDialogVisible.value = false
-          loadCameraList()
-        } else {
-          ElMessage.error(res.errMessage || '添加失败')
-        }
-      }
-    } catch (error) {
-      console.error('保存摄像头失败', error)
-      ElMessage.error('操作失败')
-    } finally {
-      cameraSubmitting.value = false
-    }
-  })
-}
-
-async function handleDeleteCamera(row: CameraInfoDTO) {
-  try {
-    await ElMessageBox.confirm(
-      `你确定要删除这个设备吗?<br/>设备ID:${row.cameraId}<br/>设备名称:${row.cameraName}`,
-      '提示',
-      {
-        type: 'warning',
-        dangerouslyUseHTMLString: true
-      }
-    )
-    const res = await adminDeleteCamera({ id: row.id })
-    if (res.success) {
-      ElMessage.success('删除成功')
-      loadCameraList()
-    } else {
-      ElMessage.error(res.errMessage || '删除失败')
-    }
-  } catch (error) {
-    if (error !== 'cancel') {
-      console.error('删除摄像头失败', error)
-      ElMessage.error('删除失败')
-    }
-  }
-}
-
-async function handleToggleEnabled(row: LssNodeDTO & { _switching?: boolean }, enabled: boolean) {
-  row._switching = true
-  try {
-    const res = await setLssNodeEnabled(row.lssId, enabled)
-    if (res.success) {
-      ElMessage.success(enabled ? '已启用' : '已禁用')
-    } else {
-      // 恢复原状态
-      row.enabled = !enabled
-      ElMessage.error(res.errMessage || '操作失败')
-    }
-  } catch (error) {
-    row.enabled = !enabled
-    console.error('切换启用状态失败', error)
-    ElMessage.error('操作失败')
-  } finally {
-    row._switching = false
-  }
-}
-
-async function handleDelete(row: LssNodeDTO) {
-  try {
-    await ElMessageBox.confirm(`确定要删除 LSS 节点 "${row.lssName}" 吗?`, '提示', {
-      type: 'warning'
-    })
-    const res = await deleteLssNode(row.lssId)
-    if (res.success) {
-      ElMessage.success('删除成功')
-      getList()
-    } else {
-      ElMessage.error(res.errMessage || '删除失败')
-    }
-  } catch (error) {
-    if (error !== 'cancel') {
-      console.error('删除失败', error)
-      ElMessage.error('删除失败')
-    }
-  }
-}
-
 // 监听 tab 切换,加载对应数据
 watch(editActiveTab, (newTab) => {
   if (newTab === 'camera' && currentLss.value) {
-    cameraSearchForm.cameraId = ''
-    cameraSearchForm.cameraName = ''
-    cameraSearchForm.status = ''
+    resetCameraSearch()
     cameraCurrentPage.value = 1
     loadCameraList()
   }