From 9f48020dfd3e29d1c0049a3da9df3203c40bc264 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期三, 26 八月 2026 17:13:17 +0800
Subject: [PATCH] Revert "feat(device): use adapter discovery backend"

---
 /dev/null                    |   83 --------
 src/stores/deviceCabinets.ts |   76 +++++++
 src/views/DeviceCabinet.vue  |  373 ++++++++++++++++++++----------------
 src/router/index.ts          |   12 +
 4 files changed, 291 insertions(+), 253 deletions(-)

diff --git a/src/api/deviceAdapters.ts b/src/api/deviceAdapters.ts
deleted file mode 100644
index 2a698ea..0000000
--- a/src/api/deviceAdapters.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import { request, type ApiResponse } from './request'
-
-export type AdapterConnectionStatus = 'DISCONNECTED' | 'CONNECTED' | 'ERROR'
-export type ChannelOnlineStatus = 'ONLINE' | 'OFFLINE' | 'UNKNOWN'
-
-export interface DeviceAdapterRecord {
-  adapterId: number
-  adapterCode: string
-  adapterName: string
-  adapterType: 'BTS'
-  host: string
-  port: number
-  pollIntervalMs: number
-  timezoneId: string
-  readOnly: boolean
-  status: '0' | '1'
-  connectionStatus: AdapterConnectionStatus
-  lastErrorCode: string | null
-  lastErrorMessage: string | null
-  lastAttemptAtUtc: string | null
-  lastObservedAtUtc: string | null
-  remark: string | null
-}
-
-export interface DeviceAdapterPayload {
-  adapterId?: number
-  adapterCode: string
-  adapterName: string
-  adapterType: 'BTS'
-  host: string
-  port: number
-  pollIntervalMs: number
-  timezoneId: string
-  readOnly: true
-  remark: string
-}
-
-export interface DetectionChannelRecord {
-  channelId: number
-  adapterId: number
-  cabinetId: number
-  devtype: number
-  devid: number
-  subdevid: number
-  chlid: number
-  displayNo: string
-  onlineStatus: ChannelOnlineStatus
-  workStatus: string
-  lastSeenAtUtc: string | null
-}
-
-interface AdapterListResponse extends ApiResponse { total: number; rows: DeviceAdapterRecord[] }
-interface DataResponse<T> extends ApiResponse { data: T }
-
-export async function getDeviceAdapters(pageNum: number, pageSize: number) {
-  const result = await request<AdapterListResponse>(`/system/deviceAdapter/list?pageNum=${pageNum}&pageSize=${pageSize}`)
-  return { total: Number(result.total) || 0, rows: result.rows || [] }
-}
-
-export async function createDeviceAdapter(payload: DeviceAdapterPayload) {
-  await request<ApiResponse>('/system/deviceAdapter', { method: 'POST', body: JSON.stringify(payload) })
-}
-
-export async function updateDeviceAdapter(payload: DeviceAdapterPayload) {
-  await request<ApiResponse>('/system/deviceAdapter', { method: 'PUT', body: JSON.stringify(payload) })
-}
-
-export async function changeDeviceAdapterStatus(adapterId: number, enabled: boolean) {
-  await request<ApiResponse>(`/system/deviceAdapter/${adapterId}/status`, {
-    method: 'PUT', body: JSON.stringify({ enabled }),
-  })
-}
-
-export async function discoverDeviceAdapter(adapterId: number) {
-  return request<DataResponse<{ cabinetCount: number; channelCount: number; observedAtUtc: string }>>(
-    `/system/deviceAdapter/${adapterId}/discover`, { method: 'POST' },
-  )
-}
-
-export async function getDeviceAdapterChannels(adapterId: number) {
-  const result = await request<DataResponse<DetectionChannelRecord[]>>(`/system/deviceAdapter/${adapterId}/channels`)
-  return result.data || []
-}
diff --git a/src/router/index.ts b/src/router/index.ts
index b744377..d42c59b 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -119,6 +119,18 @@
           meta: { title: '检测柜' },
         },
         {
+          path: 'device-cabinet/new',
+          name: 'device-cabinet-create',
+          component: () => import('@/views/DeviceCabinetForm.vue'),
+          meta: { title: '新增检测柜' },
+        },
+        {
+          path: 'device-cabinet/:id/edit',
+          name: 'device-cabinet-edit',
+          component: () => import('@/views/DeviceCabinetForm.vue'),
+          meta: { title: '编辑检测柜' },
+        },
+        {
           path: 'device-collector',
           name: 'device-collector',
           component: () => import('@/views/DeviceCollector.vue'),
diff --git a/src/stores/deviceCabinets.ts b/src/stores/deviceCabinets.ts
index 97a51ce..f8ff636 100644
--- a/src/stores/deviceCabinets.ts
+++ b/src/stores/deviceCabinets.ts
@@ -1,3 +1,5 @@
+import { ref } from 'vue'
+
 export type CabinetOnlineStatus = '在线' | '离线'
 export type CabinetTestStatus = '待检测' | '测试中' | '空闲'
 export type CabinetStatus = '启用' | '禁用'
@@ -20,6 +22,74 @@
   createdAt: string
 }
 
-export type CabinetPayload = Pick<DeviceCabinetRecord,
-  'deviceCode' | 'ip' | 'mac' | 'name' | 'manufacturer' | 'model' | 'channelCount' |
-  'specs' | 'remark' | 'onlineStatus' | 'testStatus' | 'currentTest' | 'status'>
+export type CabinetPayload = Pick<DeviceCabinetRecord, 'deviceCode' | 'ip' | 'mac' | 'name' | 'manufacturer' | 'model' | 'channelCount' | 'specs' | 'remark' | 'onlineStatus' | 'testStatus' | 'currentTest' | 'status'>
+
+const STORAGE_KEY = 'battery_detection_device_cabinets_v1'
+
+const seedRecords: DeviceCabinetRecord[] = [
+  { id: 4012, deviceCode: '45643166', ip: '192.1.1.152', mac: '41:F5:D6:N1', name: '测试柜1', manufacturer: 'XXX', model: 'A1', channelCount: 12, specs: 120, remark: '一号实验区', onlineStatus: '在线', testStatus: '待检测', currentTest: 'SP2电池寿命循环', status: '启用', createdAt: '2026-08-05 09:26' },
+  { id: 4011, deviceCode: '45643167', ip: '192.1.1.153', mac: '41:F5:D6:N2', name: '测试柜2', manufacturer: 'XXX', model: 'A1', channelCount: 12, specs: 120, remark: '一号实验区', onlineStatus: '在线', testStatus: '测试中', currentTest: '高温循环性能测试', status: '启用', createdAt: '2026-08-04 16:18' },
+  { id: 4010, deviceCode: '45643168', ip: '192.1.1.154', mac: '41:F5:D6:N3', name: '测试柜3', manufacturer: 'XXX', model: 'A2', channelCount: 24, specs: 240, remark: '二号实验区', onlineStatus: '在线', testStatus: '空闲', currentTest: '-', status: '启用', createdAt: '2026-08-04 10:42' },
+  { id: 4009, deviceCode: '45643169', ip: '192.1.1.155', mac: '41:F5:D6:N4', name: '测试柜4', manufacturer: 'XXX', model: 'A2', channelCount: 24, specs: 240, remark: '二号实验区', onlineStatus: '离线', testStatus: '空闲', currentTest: '-', status: '禁用', createdAt: '2026-08-03 15:06' },
+  { id: 4008, deviceCode: '45643170', ip: '192.1.1.156', mac: '41:F5:D6:N5', name: '测试柜5', manufacturer: 'YYY', model: 'B1', channelCount: 8, specs: 80, remark: '三号实验区', onlineStatus: '在线', testStatus: '待检测', currentTest: '-', status: '启用', createdAt: '2026-08-02 11:35' },
+  { id: 4007, deviceCode: '45643171', ip: '192.1.1.157', mac: '41:F5:D6:N6', name: '测试柜6', manufacturer: 'YYY', model: 'B1', channelCount: 8, specs: 80, remark: '三号实验区', onlineStatus: '在线', testStatus: '空闲', currentTest: '-', status: '启用', createdAt: '2026-08-01 09:18' },
+  { id: 4006, deviceCode: '45643172', ip: '192.1.1.158', mac: '41:F5:D6:N7', name: '测试柜7', manufacturer: 'YYY', model: 'B2', channelCount: 16, specs: 160, remark: '四号实验区', onlineStatus: '离线', testStatus: '待检测', currentTest: '-', status: '禁用', createdAt: '2026-07-30 14:20' },
+  { id: 4005, deviceCode: '45643173', ip: '192.1.1.159', mac: '41:F5:D6:N8', name: '测试柜8', manufacturer: 'YYY', model: 'B2', channelCount: 16, specs: 160, remark: '四号实验区', onlineStatus: '在线', testStatus: '测试中', currentTest: '低温充放电测试', status: '启用', createdAt: '2026-07-29 17:12' },
+  { id: 4004, deviceCode: '45643174', ip: '192.1.1.160', mac: '41:F5:D6:N9', name: '测试柜9', manufacturer: 'ZZZ', model: 'C1', channelCount: 32, specs: 320, remark: '五号实验区', onlineStatus: '在线', testStatus: '空闲', currentTest: '-', status: '启用', createdAt: '2026-07-28 13:46' },
+  { id: 4003, deviceCode: '45643175', ip: '192.1.1.161', mac: '41:F5:D6:N0', name: '测试柜10', manufacturer: 'ZZZ', model: 'C1', channelCount: 32, specs: 320, remark: '五号实验区', onlineStatus: '离线', testStatus: '空闲', currentTest: '-', status: '禁用', createdAt: '2026-07-26 10:16' },
+]
+
+function loadRecords() {
+  try {
+    const stored = localStorage.getItem(STORAGE_KEY)
+    if (stored) {
+      const parsed = JSON.parse(stored) as Array<Omit<DeviceCabinetRecord, 'specs'> & { specs: number | string }>
+      return parsed.map(item => ({
+        ...item,
+        specs: typeof item.specs === 'number' ? item.specs : Number(item.specs) || item.channelCount * 10,
+      }))
+    }
+  } catch {
+    localStorage.removeItem(STORAGE_KEY)
+  }
+  return seedRecords
+}
+
+const records = ref<DeviceCabinetRecord[]>(loadRecords())
+
+function persist() {
+  localStorage.setItem(STORAGE_KEY, JSON.stringify(records.value))
+}
+
+function nowString() {
+  const date = new Date()
+  const pad = (value: number) => String(value).padStart(2, '0')
+  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
+}
+
+export function useDeviceCabinets() {
+  function createCabinet(data: CabinetPayload) {
+    const id = Math.max(4000, ...records.value.map(item => item.id)) + 1
+    records.value.unshift({ ...data, id, createdAt: nowString() })
+    persist()
+  }
+
+  function updateCabinet(id: number, data: CabinetPayload) {
+    const target = records.value.find(item => item.id === id)
+    if (!target) return false
+    Object.assign(target, data)
+    persist()
+    return true
+  }
+
+  function removeCabinet(id: number) {
+    records.value = records.value.filter(item => item.id !== id)
+    persist()
+  }
+
+  function saveChanges() {
+    persist()
+  }
+
+  return { records, createCabinet, updateCabinet, removeCabinet, saveChanges }
+}
diff --git a/src/views/DeviceCabinet.vue b/src/views/DeviceCabinet.vue
index f3bfa16..db4c83e 100644
--- a/src/views/DeviceCabinet.vue
+++ b/src/views/DeviceCabinet.vue
@@ -1,198 +1,237 @@
 <script setup lang="ts">
 import { onMounted, reactive, ref } from 'vue'
-import { ElMessage } from 'element-plus'
-import {
-  changeDeviceAdapterStatus, createDeviceAdapter, discoverDeviceAdapter,
-  getDeviceAdapterChannels, getDeviceAdapters, updateDeviceAdapter,
-  type DetectionChannelRecord, type DeviceAdapterPayload, type DeviceAdapterRecord,
-} from '@/api/deviceAdapters'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { useRouter } from 'vue-router'
+import { changeDeviceCabinetStatus, deleteDeviceCabinets, getDeviceCabinetList } from '@/api/deviceCabinets'
+import type { CabinetOnlineStatus, CabinetStatus, CabinetTestStatus, DeviceCabinetRecord } from '@/stores/deviceCabinets'
 
-const adapters = ref<DeviceAdapterRecord[]>([])
-const channels = ref<DetectionChannelRecord[]>([])
+interface FilterState {
+  device: string
+  dateRange: [string, string] | null
+  onlineStatus: string
+  testStatus: string
+  status: string
+}
+
+const router = useRouter()
+const onlineStatuses: CabinetOnlineStatus[] = ['在线', '离线']
+const testStatuses: CabinetTestStatus[] = ['测试中', '空闲']
+const statuses: CabinetStatus[] = ['启用', '禁用']
+const filters = reactive<FilterState>({ device: '', dateRange: null, onlineStatus: '', testStatus: '', status: '' })
+const records = ref<DeviceCabinetRecord[]>([])
 const total = ref(0)
-const pageNum = ref(1)
-const pageSize = 10
-const selectedId = ref<number | null>(null)
 const loading = ref(false)
-const channelLoading = ref(false)
 const submittingId = ref<number | null>(null)
-const dialogVisible = ref(false)
-const saving = ref(false)
-const editingId = ref<number | null>(null)
-const form = reactive<DeviceAdapterPayload>({
-  adapterCode: '', adapterName: '', adapterType: 'BTS', host: '', port: 502,
-  pollIntervalMs: 5000, timezoneId: 'Asia/Hong_Kong', readOnly: true, remark: '',
-})
+const currentPage = ref(1)
+const pageSize = ref(10)
 
-async function loadAdapters() {
+const onlineStatusValues: Record<CabinetOnlineStatus, string> = { 在线: '0', 离线: '1' }
+const testStatusValues: Record<CabinetTestStatus, string> = { 空闲: '0', 测试中: '1', 待检测: '0' }
+const statusValues: Record<CabinetStatus, string> = { 启用: '0', 禁用: '1' }
+
+async function loadRecords(showResult = false) {
   loading.value = true
   try {
-    const result = await getDeviceAdapters(pageNum.value, pageSize)
-    adapters.value = result.rows
+    const result = await getDeviceCabinetList({
+      pageNum: currentPage.value,
+      pageSize: pageSize.value,
+      device: filters.device.trim() || undefined,
+      beginCreateTime: filters.dateRange?.[0] ? `${filters.dateRange[0]} 00:00:00` : undefined,
+      endCreateTime: filters.dateRange?.[1] ? `${filters.dateRange[1]} 23:59:59` : undefined,
+      onlineStatus: filters.onlineStatus && filters.onlineStatus !== '全部' ? onlineStatusValues[filters.onlineStatus as CabinetOnlineStatus] : undefined,
+      testStatus: filters.testStatus && filters.testStatus !== '全部' ? testStatusValues[filters.testStatus as CabinetTestStatus] : undefined,
+      status: filters.status && filters.status !== '全部' ? statusValues[filters.status as CabinetStatus] : undefined,
+    })
+    records.value = result.rows
     total.value = result.total
-    if (selectedId.value && !result.rows.some(item => item.adapterId === selectedId.value)) selectedId.value = null
-    if (!selectedId.value && result.rows.length) selectedId.value = result.rows[0].adapterId
-    await loadChannels()
+    if (showResult) ElMessage.success(`共找到 ${result.total} 个检测柜`)
   } catch (error) {
-    adapters.value = []
-    channels.value = []
-    ElMessage.error(error instanceof Error ? error.message : '设备适配器加载失败')
-  } finally { loading.value = false }
-}
-
-async function loadChannels() {
-  if (!selectedId.value) { channels.value = []; return }
-  channelLoading.value = true
-  try { channels.value = await getDeviceAdapterChannels(selectedId.value) }
-  catch (error) {
-    channels.value = []
-    ElMessage.error(error instanceof Error ? error.message : '检测柜通道加载失败')
-  } finally { channelLoading.value = false }
-}
-
-function resetForm() {
-  Object.assign(form, {
-    adapterCode: '', adapterName: '', adapterType: 'BTS', host: '', port: 502,
-    pollIntervalMs: 5000, timezoneId: 'Asia/Hong_Kong', readOnly: true, remark: '',
-  })
-  editingId.value = null
-}
-function openCreate() { resetForm(); dialogVisible.value = true }
-function openEdit(item: DeviceAdapterRecord) {
-  editingId.value = item.adapterId
-  Object.assign(form, {
-    adapterId: item.adapterId, adapterCode: item.adapterCode, adapterName: item.adapterName,
-    adapterType: 'BTS', host: item.host, port: item.port, pollIntervalMs: item.pollIntervalMs,
-    timezoneId: item.timezoneId, readOnly: true, remark: item.remark || '',
-  })
-  dialogVisible.value = true
-}
-
-async function saveAdapter() {
-  if (!form.adapterCode.trim() || !form.adapterName.trim() || !form.host.trim()) {
-    ElMessage.warning('请填写适配器编码、名称和主机')
-    return
+    records.value = []
+    total.value = 0
+    ElMessage.error(error instanceof Error ? error.message : '检测柜列表加载失败')
+  } finally {
+    loading.value = false
   }
-  saving.value = true
+}
+
+function applySearch() {
+  currentPage.value = 1
+  loadRecords(true)
+}
+
+function resetSearch() {
+  Object.assign(filters, { device: '', dateRange: null, onlineStatus: '', testStatus: '', status: '' })
+  currentPage.value = 1
+  loadRecords()
+}
+
+function handlePageChange(page: number) {
+  currentPage.value = page
+  loadRecords()
+}
+
+function handleSizeChange(size: number) {
+  pageSize.value = size
+  currentPage.value = 1
+  loadRecords()
+}
+
+function createCabinet() {
+  router.push('/device-cabinet/new')
+}
+
+function editCabinet(id: number) {
+  router.push(`/device-cabinet/${id}/edit`)
+}
+
+async function changeStatus(item: DeviceCabinetRecord) {
+  const nextStatus: CabinetStatus = item.status === '启用' ? '禁用' : '启用'
   try {
-    const payload = { ...form, adapterId: editingId.value || undefined }
-    if (editingId.value) await updateDeviceAdapter(payload)
-    else await createDeviceAdapter(payload)
-    dialogVisible.value = false
-    ElMessage.success(editingId.value ? '设备适配器已更新' : '设备适配器已新增')
-    await loadAdapters()
-  } catch (error) { ElMessage.error(error instanceof Error ? error.message : '设备适配器保存失败') }
-  finally { saving.value = false }
-}
-
-async function toggleStatus(item: DeviceAdapterRecord, enabled: boolean) {
-  submittingId.value = item.adapterId
-  try {
-    await changeDeviceAdapterStatus(item.adapterId, enabled)
-    ElMessage.success(enabled ? '设备适配器已启用' : '设备适配器已停用')
-    await loadAdapters()
-  } catch (error) { ElMessage.error(error instanceof Error ? error.message : '启停操作失败') }
-  finally { submittingId.value = null }
-}
-
-function handleStatusChange(item: DeviceAdapterRecord, value: string | number | boolean) {
-  toggleStatus(item, Boolean(value))
-}
-
-function handlePageChange(value: number) {
-  pageNum.value = value
-  loadAdapters()
-}
-
-async function discover(item: DeviceAdapterRecord) {
-  submittingId.value = item.adapterId
-  selectedId.value = item.adapterId
-  try {
-    const result = await discoverDeviceAdapter(item.adapterId)
-    ElMessage.success(`发现 ${result.data.channelCount} 个检测柜通道`)
-    await loadAdapters()
+    await ElMessageBox.confirm(
+      `确认${nextStatus}检测柜“${item.name}”吗?`,
+      `${nextStatus}确认`,
+      { type: 'warning', confirmButtonText: '确认', cancelButtonText: '取消' },
+    )
+    submittingId.value = item.id
+    await changeDeviceCabinetStatus(item.id, nextStatus)
+    ElMessage.success(`检测柜已${nextStatus}`)
+    await loadRecords()
   } catch (error) {
-    await loadAdapters()
-    ElMessage.error(error instanceof Error ? error.message : '设备发现失败')
-  } finally { submittingId.value = null }
+    if (error !== 'cancel' && error !== 'close') {
+      ElMessage.error(error instanceof Error ? error.message : `${nextStatus}检测柜失败`)
+    }
+  } finally {
+    submittingId.value = null
+  }
 }
 
-function selectAdapter(item: DeviceAdapterRecord) { selectedId.value = item.adapterId; loadChannels() }
-function formatTime(value: string | null) { return value ? new Date(value).toLocaleString() : '-' }
-function connectionLabel(value: string) { return { CONNECTED: '已连接', ERROR: '连接异常', DISCONNECTED: '未连接' }[value] || value }
-function onlineLabel(value: string) { return { ONLINE: '在线', OFFLINE: '离线', UNKNOWN: '未知' }[value] || value }
-onMounted(loadAdapters)
+async function deleteCabinet(item: DeviceCabinetRecord) {
+  try {
+    await ElMessageBox.confirm(
+      `删除后无法恢复,确认删除检测柜“${item.name}”吗?`,
+      '删除确认',
+      { type: 'warning', confirmButtonText: '确认删除', cancelButtonText: '取消' },
+    )
+    submittingId.value = item.id
+    await deleteDeviceCabinets(item.id)
+    if (records.value.length === 1 && currentPage.value > 1) currentPage.value -= 1
+    ElMessage.success('检测柜已删除')
+    await loadRecords()
+  } catch (error) {
+    if (error !== 'cancel' && error !== 'close') {
+      ElMessage.error(error instanceof Error ? error.message : '删除检测柜失败')
+    }
+  } finally {
+    submittingId.value = null
+  }
+}
+
+function testStatusClass(status: CabinetTestStatus) {
+  return { '待检测': 'pending', '测试中': 'running', '空闲': 'idle' }[status]
+}
+
+onMounted(() => loadRecords())
 </script>
 
 <template>
-  <div class="adapter-page">
-    <section class="adapter-panel">
-      <header class="panel-heading">
-        <div><i></i><strong>设备适配器实例</strong><span>{{ total }} INSTANCES</span></div>
-        <button class="primary-button" type="button" @click="openCreate"><el-icon><Plus /></el-icon>新增</button>
-      </header>
+  <div class="cabinet-page">
+    <section class="filter-panel" aria-label="检测柜查询条件">
+      <header class="section-heading"><div><i></i><strong>查询条件</strong><span>FILTER CONDITIONS</span></div><small>共 {{ total }} 个检测柜</small></header>
+      <div class="filter-grid">
+        <label><span>设备</span><el-input v-model="filters.device" clearable placeholder="输入IP、设备编码、MAC或名称" @keyup.enter="applySearch" /></label>
+        <label class="date-filter"><span>创建时间</span><el-date-picker v-model="filters.dateRange" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" range-separator="至" unlink-panels /></label>
+        <label><span>在线状态</span><el-select v-model="filters.onlineStatus" clearable placeholder="全部状态"><el-option label="全部" value="全部" /><el-option v-for="item in onlineStatuses" :key="item" :label="item" :value="item" /></el-select></label>
+        <label><span>测试状态</span><el-select v-model="filters.testStatus" clearable placeholder="全部状态"><el-option label="全部" value="全部" /><el-option v-for="item in testStatuses" :key="item" :label="item" :value="item" /></el-select></label>
+        <label><span>状态</span><el-select v-model="filters.status" clearable placeholder="全部状态"><el-option label="全部" value="全部" /><el-option v-for="item in statuses" :key="item" :label="item" :value="item" /></el-select></label>
+        <div class="filter-actions"><button class="primary-button" type="button" @click="applySearch"><el-icon><Search /></el-icon>查询</button><button class="secondary-button" type="button" @click="resetSearch"><el-icon><RefreshLeft /></el-icon>重置</button></div>
+      </div>
+    </section>
+
+    <section class="result-panel">
+      <div class="result-toolbar"><div><i></i><strong>检测柜列表</strong><span>{{ total }} RESULTS · 按创建时间倒序</span></div><button class="primary-button" type="button" @click="createCabinet"><el-icon><Plus /></el-icon>手动添加</button></div>
       <div v-loading="loading" class="table-wrap">
-        <table class="adapter-table">
-          <thead><tr><th>实例</th><th>端点</th><th>轮询</th><th>时区</th><th>连接状态</th><th>最近观测</th><th>最近错误</th><th>启用</th><th>操作</th></tr></thead>
+        <table>
+          <thead><tr><th>设备编码</th><th>IP</th><th>MAC</th><th>名称</th><th>制造商</th><th>型号</th><th>通道数</th><th>规格/容量</th><th>备注</th><th>在线状态</th><th>测试状态</th><th>当前测试</th><th class="action-column">操作</th></tr></thead>
           <tbody>
-            <tr v-for="item in adapters" :key="item.adapterId" :class="{ selected: selectedId === item.adapterId }" @click="selectAdapter(item)">
-              <td><strong>{{ item.adapterName }}</strong><small>{{ item.adapterCode }} · {{ item.adapterType }} · 只读</small></td>
-              <td class="mono">{{ item.host }}:{{ item.port }}</td><td>{{ item.pollIntervalMs / 1000 }} 秒</td><td>{{ item.timezoneId }}</td>
-              <td><span class="state" :class="item.connectionStatus.toLowerCase()"><i></i>{{ connectionLabel(item.connectionStatus) }}</span></td>
-              <td>{{ formatTime(item.lastObservedAtUtc) }}</td>
-              <td class="error-cell" :title="item.lastErrorMessage || ''"><span v-if="item.lastErrorCode">{{ item.lastErrorCode }} · {{ item.lastErrorMessage }}</span><span v-else>-</span></td>
-              <td @click.stop><el-switch :model-value="item.status === '0'" :loading="submittingId === item.adapterId" @change="handleStatusChange(item, $event)" /></td>
-              <td @click.stop><div class="row-actions"><el-tooltip content="发现设备" placement="top"><button type="button" :disabled="item.status !== '0' || submittingId !== null" aria-label="发现设备" @click="discover(item)"><el-icon><Refresh /></el-icon></button></el-tooltip><el-tooltip content="编辑配置" placement="top"><button type="button" aria-label="编辑配置" @click="openEdit(item)"><el-icon><EditPen /></el-icon></button></el-tooltip></div></td>
+            <tr v-for="item in records" :key="item.id">
+              <td class="code-cell">{{ item.deviceCode }}</td><td class="code-cell">{{ item.ip }}</td><td class="code-cell">{{ item.mac }}</td><td><strong>{{ item.name }}</strong></td><td>{{ item.manufacturer }}</td><td>{{ item.model }}</td><td>{{ item.channelCount }}</td><td :title="String(item.specs)">{{ item.specs }}</td><td :title="item.remark">{{ item.remark || '-' }}</td>
+              <td><span class="online-state" :class="item.onlineStatus === '在线' ? 'online' : 'offline'"><i></i>{{ item.onlineStatus }}</span></td>
+              <td><span class="test-state" :class="testStatusClass(item.testStatus)">{{ item.testStatus }}</span></td>
+              <td :title="item.currentTest">{{ item.currentTest || '-' }}</td>
+              <td class="action-column"><div class="row-actions"><button type="button" :disabled="submittingId !== null" :class="item.status === '启用' ? 'disable-action' : 'enable-action'" @click="changeStatus(item)">{{ submittingId === item.id ? '处理中' : item.status === '启用' ? '禁用' : '启用' }}</button><button type="button" :disabled="submittingId !== null" @click="editCabinet(item.id)">编辑</button><button class="delete-action" type="button" :disabled="submittingId !== null" @click="deleteCabinet(item)">删除</button></div></td>
             </tr>
-            <tr v-if="!loading && !adapters.length"><td class="empty" colspan="9">暂无设备适配器实例</td></tr>
+            <tr v-if="!loading && !records.length"><td colspan="13" class="empty-cell">暂无符合条件的检测柜</td></tr>
           </tbody>
         </table>
       </div>
-      <footer class="pagination"><span>显示 {{ adapters.length }} 条,共 {{ total }} 条</span><el-pagination :current-page="pageNum" :page-size="pageSize" :total="total" layout="prev, pager, next" background @current-change="handlePageChange" /></footer>
+      <footer class="pagination-bar"><span>显示 {{ records.length }} 条,共 {{ total }} 条</span><el-pagination :current-page="currentPage" :page-size="pageSize" :page-sizes="[10, 20, 50]" :total="total" layout="sizes, prev, pager, next" background @current-change="handlePageChange" @size-change="handleSizeChange" /></footer>
     </section>
-
-    <section class="channel-panel">
-      <header class="panel-heading"><div><i></i><strong>检测柜通道</strong><span>{{ channels.length }} CHANNELS</span></div><button class="icon-button" type="button" :disabled="!selectedId" aria-label="刷新通道" @click="loadChannels"><el-icon><Refresh /></el-icon></button></header>
-      <div v-loading="channelLoading" class="table-wrap">
-        <table class="channel-table">
-          <thead><tr><th>通道身份</th><th>设备类型</th><th>设备号</th><th>单元号</th><th>通道号</th><th>在线状态</th><th>工作状态</th><th>最近观测</th></tr></thead>
-          <tbody>
-            <tr v-for="item in channels" :key="item.channelId"><td class="mono">{{ item.adapterId }} / {{ item.devtype }} / {{ item.devid }} / {{ item.subdevid }} / {{ item.chlid }}</td><td>{{ item.devtype }}</td><td>{{ item.devid }}</td><td>{{ item.subdevid }}</td><td>{{ item.chlid }}</td><td><span class="state" :class="item.onlineStatus.toLowerCase()"><i></i>{{ onlineLabel(item.onlineStatus) }}</span></td><td>{{ item.workStatus }}</td><td>{{ formatTime(item.lastSeenAtUtc) }}</td></tr>
-            <tr v-if="!channelLoading && !channels.length"><td class="empty" colspan="8">暂无已发现的检测柜通道</td></tr>
-          </tbody>
-        </table>
-      </div>
-    </section>
-
-    <el-dialog v-model="dialogVisible" :title="editingId ? '编辑设备适配器实例' : '新增设备适配器实例'" width="620px" destroy-on-close>
-      <div class="config-grid">
-        <label><span>实例编码</span><el-input v-model="form.adapterCode" :disabled="Boolean(editingId)" maxlength="64" /></label>
-        <label><span>实例名称</span><el-input v-model="form.adapterName" maxlength="100" /></label>
-        <label><span>类型</span><el-select v-model="form.adapterType"><el-option label="BTS" value="BTS" /></el-select></label>
-        <label><span>只读能力</span><el-switch v-model="form.readOnly" disabled inline-prompt active-text="只读" /></label>
-        <label class="wide"><span>主机</span><el-input v-model="form.host" maxlength="255" placeholder="IP 或主机名" /></label>
-        <label><span>端口</span><el-input-number v-model="form.port" :min="1" :max="65535" controls-position="right" /></label>
-        <label><span>轮询周期(毫秒)</span><el-input-number v-model="form.pollIntervalMs" :min="1000" :step="1000" controls-position="right" /></label>
-        <label><span>时区</span><el-input v-model="form.timezoneId" maxlength="64" /></label>
-        <label class="wide"><span>备注</span><el-input v-model="form.remark" type="textarea" :rows="3" maxlength="500" show-word-limit /></label>
-      </div>
-      <template #footer><button class="secondary-button" type="button" @click="dialogVisible = false">取消</button><button class="primary-button" type="button" :disabled="saving" @click="saveAdapter"><el-icon><Check /></el-icon>{{ saving ? '保存中' : '保存' }}</button></template>
-    </el-dialog>
   </div>
 </template>
 
 <style scoped>
-.adapter-page { display: grid; gap: 16px; width: 100%; }
-.adapter-panel, .channel-panel { border: 1px solid #d8e1e9; background: #fff; }
-.panel-heading { min-height: 50px; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 0 16px; border-bottom: 1px solid #d8e1e9; background: var(--surface-soft); }
-.panel-heading > div { display: flex; align-items: center; gap: 8px; }.panel-heading i { width: 3px; height: 16px; background: #2b78a3; }.panel-heading strong { color: var(--ink); font-size: 15px; }.panel-heading span { color: #81909d; font-size: 10px; }
-.primary-button, .secondary-button, .icon-button { height: 34px; display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 0 13px; border: 1px solid transparent; border-radius: 3px; font-size: 13px; cursor: pointer; }.primary-button { color: #fff; border-color: var(--blue); background: var(--blue); }.secondary-button, .icon-button { color: #52606d; border-color: var(--line-strong); background: #fff; }.icon-button { width: 34px; padding: 0; }button:disabled { opacity: .5; cursor: not-allowed; }
-.table-wrap { overflow-x: auto; }table { width: 100%; border-collapse: collapse; color: #52606d; font-size: 12px; table-layout: fixed; }.adapter-table { min-width: 1320px; }.channel-table { min-width: 980px; }
-th, td { height: 48px; padding: 0 11px; border-right: 1px solid #dde7f0; border-bottom: 1px solid #dde7f0; text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }th { height: 40px; color: #52606d; background: #eaf4ff; border-color: #c7e0ff; font-weight: 600; }th:last-child, td:last-child { border-right: 0; }
-.adapter-table th:first-child { width: 180px; }.adapter-table th:nth-child(2) { width: 150px; }.adapter-table th:nth-child(5) { width: 105px; }.adapter-table th:nth-child(6) { width: 165px; }.adapter-table th:nth-child(7) { width: 210px; }.adapter-table th:nth-child(8) { width: 70px; }.adapter-table th:last-child { width: 88px; }.channel-table th:first-child { width: 230px; }.channel-table th:last-child { width: 180px; }
-tbody tr.selected { background: #edf6ff; }tbody tr:hover { background: #f7fbff; }td strong, td small { display: block; overflow: hidden; text-overflow: ellipsis; }td strong { color: #26333f; font-size: 13px; }td small { margin-top: 3px; color: #7b8794; font-size: 10px; }.mono { font-family: 'Cascadia Mono', Consolas, monospace; }
-.state { display: inline-flex; align-items: center; gap: 6px; }.state i { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }.state.connected, .state.online { color: #23865e; }.state.error, .state.offline { color: #a64a4a; }.state.disconnected, .state.unknown { color: #697682; }.error-cell { color: #a64a4a; }
-.row-actions { display: flex; justify-content: center; gap: 8px; }.row-actions button { width: 30px; height: 30px; display: inline-grid; place-items: center; border: 0; color: var(--blue); background: transparent; cursor: pointer; }.empty { height: 120px; color: #8795a3; text-align: center; }
-.pagination { min-height: 56px; display: flex; align-items: center; justify-content: space-between; padding: 0 14px; }.pagination > span { color: #7b8794; font-size: 11px; }.config-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; }.config-grid label { min-width: 0; display: grid; gap: 7px; }.config-grid label > span { color: #52606d; font-size: 12px; font-weight: 500; }.config-grid .wide { grid-column: 1 / -1; }.config-grid :deep(.el-select), .config-grid :deep(.el-input-number) { width: 100%; }
-@media (max-width: 680px) { .panel-heading { padding: 0 12px; }.panel-heading span { display: none; }.pagination { align-items: flex-start; flex-direction: column; gap: 8px; padding: 10px 12px; }.config-grid { grid-template-columns: minmax(0, 1fr); }.config-grid .wide { grid-column: auto; } }
+.cabinet-page { width: 100%; }
+.primary-button, .secondary-button { height: 34px; display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 0 13px; border: 1px solid transparent; border-radius: 3px; font-size: 13px; font-weight: 500; cursor: pointer; white-space: nowrap; }
+.primary-button { color: #fff; background: var(--blue); border-color: var(--blue); }
+.primary-button:hover { background: #176fcb; border-color: #176fcb; }
+.secondary-button { color: #52606d; background: #fff; border-color: var(--line-strong); }
+.secondary-button:hover { color: var(--blue); border-color: #9dc8f5; }
+.filter-panel, .result-panel { background: #fff; border: 1px solid #d8e1e9; }
+.filter-panel { margin-bottom: 16px; }
+.section-heading, .result-toolbar { display: flex; align-items: center; justify-content: space-between; background: var(--surface-soft); border-bottom: 1px solid var(--line); }
+.section-heading { height: 44px; padding: 0 16px; }
+.section-heading > div, .result-toolbar > div { display: flex; align-items: center; gap: 8px; }
+.section-heading > div > i, .result-toolbar > div:first-child > i { width: 3px; height: 16px; background: #2b78a3; }
+.section-heading strong, .result-toolbar strong { color: var(--ink); font-size: 15px; font-weight: 600; }
+.section-heading span, .result-toolbar span { color: #81909d; font-size: 10px; }
+.section-heading small { color: #647380; font-size: 11px; }
+.filter-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px 18px; padding: 16px; }
+.filter-grid label { width: 100%; min-width: 0; display: grid; grid-template-columns: 72px minmax(0, 1fr); align-items: center; gap: 8px; }
+.filter-grid label > span { color: #52606d; font-size: 12px; font-weight: 500; }
+.filter-grid :deep(.el-select), .filter-grid :deep(.el-date-editor) { width: 100%; }
+.filter-grid :deep(.el-input__wrapper), .filter-grid :deep(.el-select__wrapper) { min-height: 34px; border-radius: 3px; box-shadow: 0 0 0 1px #d8e1e9 inset; background: #f8fafc; }
+.filter-actions { grid-column: -2 / -1; min-width: 0; display: flex; align-items: center; justify-content: flex-end; gap: 8px; }
+.result-toolbar { height: 50px; padding: 0 16px; }
+.table-wrap { overflow-x: auto; }
+table { width: 100%; min-width: 1740px; table-layout: fixed; border-collapse: collapse; color: #52606d; font-size: 13px; }
+th, td { height: 50px; padding: 0 12px; border-right: 1px solid #dde7f0; border-bottom: 1px solid #dde7f0; text-align: left; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+th { height: 40px; color: #52606d; background: #eaf4ff; border-color: #c7e0ff; font-size: 12px; font-weight: 600; }
+th:nth-child(1) { width: 115px; } th:nth-child(2) { width: 120px; } th:nth-child(3) { width: 130px; } th:nth-child(4) { width: 130px; }
+th:nth-child(5), th:nth-child(6) { width: 100px; } th:nth-child(7) { width: 80px; } th:nth-child(8) { width: 145px; } th:nth-child(9) { width: 145px; }
+th:nth-child(10), th:nth-child(11) { width: 100px; } th:nth-child(12) { width: 180px; } th:last-child { width: 180px; }
+th:last-child, td:last-child { border-right: 0; text-align: center; }
+.action-column { position: sticky; right: 0; z-index: 2; background: #fff; overflow: visible; border-left: 0; box-shadow: inset 1px 0 0 #b4c7d9, -8px 0 14px rgba(23, 49, 74, .12); }
+th.action-column { z-index: 3; background: #eaf4ff; }
+tbody tr:hover td.action-column { background: #f7fbff; }
+tbody tr:hover { background: #f7fbff; }
+tbody tr:last-child td { border-bottom: 0; }
+td strong { color: #26333f; font-weight: 600; }
+.code-cell { color: #647380; font-family: 'Cascadia Mono', Consolas, monospace; font-size: 12px; }
+.online-state { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; }
+.online-state i { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
+.online-state.online { color: #23865e; }
+.online-state.offline { color: #a64a4a; }
+.test-state { display: inline-flex; padding: 4px 7px; border: 1px solid; font-size: 11px; line-height: 1; }
+.test-state.pending { color: #a96b16; background: #fff8e9; border-color: #efd59e; }
+.test-state.running { color: #176fcb; background: #edf6ff; border-color: #b8d9fb; }
+.test-state.idle { color: #697682; background: #f2f4f6; border-color: #ccd4da; }
+.row-actions { display: flex; align-items: center; justify-content: center; gap: 12px; }
+.row-actions button { border: 0; color: var(--blue); background: transparent; font-size: 12px; font-weight: 500; cursor: pointer; }
+.row-actions button:disabled { opacity: .55; cursor: not-allowed; }
+.row-actions .disable-action, .row-actions .delete-action { color: #ad4a4a; }
+.row-actions .enable-action { color: #23865e; }
+.empty-cell { height: 160px; color: #8795a3; text-align: center; }
+.pagination-bar { min-height: 58px; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 8px 14px 8px 16px; border-top: 1px solid var(--line); }
+.pagination-bar > span { color: #7b8794; font-size: 11px; }
+.pagination-bar :deep(.el-pagination) { --el-pagination-button-bg-color: #edf4fa; --el-pagination-hover-color: var(--blue); }
+@media (max-width: 1320px) { .filter-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
+@media (max-width: 980px) { .filter-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
+@media (max-width: 680px) {
+  .filter-grid { grid-template-columns: minmax(0, 1fr); gap: 12px; padding: 14px 12px; }
+  .filter-actions { justify-content: stretch; }
+  .filter-actions button { flex: 1; }
+  .section-heading, .result-toolbar { padding-left: 12px; }
+  .section-heading span, .result-toolbar span { display: none; }
+  .pagination-bar { align-items: flex-start; flex-direction: column; overflow-x: auto; }
+}
 </style>

--
Gitblit v1.7.1