|
|
@@ -0,0 +1,715 @@
|
|
|
+<template>
|
|
|
+ <div class="page-container">
|
|
|
+ <!-- 搜索表单 -->
|
|
|
+ <div class="search-form">
|
|
|
+ <el-form :model="searchForm" inline data-id="search-form">
|
|
|
+ <el-form-item>
|
|
|
+ <el-input
|
|
|
+ v-model.trim="searchForm.keyword"
|
|
|
+ placeholder="LSS ID / 名称"
|
|
|
+ clearable
|
|
|
+ data-id="search-keyword"
|
|
|
+ @keyup.enter="handleSearch"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-select v-model="searchForm.heartbeat" placeholder="心跳状态" clearable data-id="search-heartbeat">
|
|
|
+ <el-option label="全部" value="" />
|
|
|
+ <el-option label="active" value="active" />
|
|
|
+ <el-option label="hold" value="hold" />
|
|
|
+ <el-option label="dead" value="dead" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-button type="primary" :icon="Search" data-id="btn-search" @click="handleSearch">
|
|
|
+ {{ t('查询') }}
|
|
|
+ </el-button>
|
|
|
+ <el-button :icon="RefreshRight" data-id="btn-reset" @click="handleReset">{{ t('重置') }}</el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 数据表格 -->
|
|
|
+ <div class="table-wrapper">
|
|
|
+ <el-table
|
|
|
+ ref="tableRef"
|
|
|
+ v-loading="loading"
|
|
|
+ :data="sortedList"
|
|
|
+ stripe
|
|
|
+ size="default"
|
|
|
+ data-id="lss-table"
|
|
|
+ height="100%"
|
|
|
+ @sort-change="handleSortChange"
|
|
|
+ >
|
|
|
+ <el-table-column prop="lssId" label="LSS ID" min-width="120" sortable="custom" show-overflow-tooltip />
|
|
|
+ <el-table-column prop="name" :label="t('名称')" min-width="140" sortable="custom" show-overflow-tooltip />
|
|
|
+ <el-table-column prop="address" :label="t('地址')" min-width="180" sortable="custom" show-overflow-tooltip />
|
|
|
+ <el-table-column prop="publicIp" :label="t('公网IP')" min-width="130" sortable="custom" show-overflow-tooltip />
|
|
|
+ <el-table-column prop="heartbeat" :label="t('心跳')" min-width="200" sortable="custom">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <span :class="getHeartbeatClass(row.heartbeat)">
|
|
|
+ {{ formatHeartbeat(row) }}
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="詳情" width="70" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-button type="primary" link :icon="View" @click="handleViewDetail(row)" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="設備列表" width="90" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-button type="primary" link :icon="List" @click="handleViewDevices(row)" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="ablyInfo" label="ably信息" min-width="120" show-overflow-tooltip />
|
|
|
+ <el-table-column :label="t('操作')" width="90" align="center" fixed="right">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-button type="primary" link :icon="Edit" @click="handleEdit(row)" />
|
|
|
+ <el-button type="danger" link :icon="Delete" @click="handleDelete(row)" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- LSS 详情抽屉 -->
|
|
|
+ <el-drawer v-model="detailDrawerVisible" title="LSS 详情" direction="rtl" size="500px" destroy-on-close>
|
|
|
+ <el-descriptions :column="1" border>
|
|
|
+ <el-descriptions-item label="LSS ID">{{ currentLss?.lssId }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="名称">{{ currentLss?.name }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="地址">{{ currentLss?.address }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="公网IP">{{ currentLss?.publicIp }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="心跳状态">
|
|
|
+ <span :class="getHeartbeatClass(currentLss?.heartbeat || 'dead')">
|
|
|
+ {{ formatHeartbeat(currentLss) }}
|
|
|
+ </span>
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="ably信息">{{ currentLss?.ablyInfo || '-' }}</el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+ </el-drawer>
|
|
|
+
|
|
|
+ <!-- 设备列表抽屉 -->
|
|
|
+ <el-drawer v-model="deviceDrawerVisible" title="設備列表" direction="rtl" size="80%" destroy-on-close>
|
|
|
+ <template #header>
|
|
|
+ <div class="drawer-header">
|
|
|
+ <span>設備列表</span>
|
|
|
+ <el-button type="primary" :icon="Plus" size="small" @click="handleAddDevice">新增</el-button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <el-table :data="deviceList" stripe size="default" height="100%">
|
|
|
+ <el-table-column prop="localIp" label="本地IP" min-width="100" />
|
|
|
+ <el-table-column prop="deviceId" label="設備ID" min-width="100" />
|
|
|
+ <el-table-column prop="name" label="名稱" min-width="100" />
|
|
|
+ <el-table-column prop="heartbeat" label="狀態(心跳)" min-width="180">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <span :class="getHeartbeatClass(row.heartbeat)">
|
|
|
+ {{ formatDeviceHeartbeat(row) }}
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="參數配置" width="80" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-link type="primary" @click="handleViewConfig(row)">查看</el-link>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="運行參數" width="80" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-link type="primary" @click="handleViewRunParams(row)">查看</el-link>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="manufacturer" label="廠商" min-width="100" />
|
|
|
+ <el-table-column prop="model" label="型號" min-width="140" />
|
|
|
+ <el-table-column prop="addTime" label="添加時間" min-width="150" />
|
|
|
+ <el-table-column label="設備控制" width="100" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-button type="primary" link :icon="Edit" @click="handleEditDevice(row)" />
|
|
|
+ <el-button type="danger" link :icon="Delete" @click="handleDeleteDevice(row)" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </el-drawer>
|
|
|
+
|
|
|
+ <!-- 分页 -->
|
|
|
+ <div class="pagination-container">
|
|
|
+ <el-pagination
|
|
|
+ v-model:current-page="currentPage"
|
|
|
+ v-model:page-size="pageSize"
|
|
|
+ :page-sizes="[10, 20, 50, 100]"
|
|
|
+ :total="total"
|
|
|
+ layout="total, sizes, prev, pager, next, jumper"
|
|
|
+ background
|
|
|
+ @size-change="handleSizeChange"
|
|
|
+ @current-change="handleCurrentChange"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref, reactive, onMounted, computed } from 'vue'
|
|
|
+import { Search, RefreshRight, Edit, Delete, View, List, Plus } from '@element-plus/icons-vue'
|
|
|
+import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
+import { listLss } from '@/api/lss'
|
|
|
+import type { LssDTO, LssHeartbeatStatus } from '@/types'
|
|
|
+import dayjs from 'dayjs'
|
|
|
+import { useI18n } from 'vue-i18n'
|
|
|
+
|
|
|
+const { t } = useI18n({ useScope: 'global' })
|
|
|
+
|
|
|
+// 格式化心跳显示
|
|
|
+function formatHeartbeat(row: LssDTO | null | undefined): string {
|
|
|
+ if (!row) return '-'
|
|
|
+ if (row.heartbeat === 'active') {
|
|
|
+ const time = row.heartbeatTime ? dayjs(row.heartbeatTime).format('YY-MM-DD HH:mm:ss') : ''
|
|
|
+ return `active [${time}]`
|
|
|
+ } else if (row.heartbeat === 'hold') {
|
|
|
+ return 'hold(5分鐘內)'
|
|
|
+ } else if (row.heartbeat === 'dead') {
|
|
|
+ return 'dead (超過5分)'
|
|
|
+ }
|
|
|
+ return row.heartbeat || '-'
|
|
|
+}
|
|
|
+
|
|
|
+// 获取心跳状态样式类
|
|
|
+function getHeartbeatClass(status: LssHeartbeatStatus): string {
|
|
|
+ switch (status) {
|
|
|
+ case 'active':
|
|
|
+ return 'heartbeat-active'
|
|
|
+ case 'hold':
|
|
|
+ return 'heartbeat-hold'
|
|
|
+ case 'dead':
|
|
|
+ return 'heartbeat-dead'
|
|
|
+ default:
|
|
|
+ return ''
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const loading = ref(false)
|
|
|
+const lssList = ref<LssDTO[]>([])
|
|
|
+const tableRef = ref()
|
|
|
+
|
|
|
+// 抽屉状态
|
|
|
+const detailDrawerVisible = ref(false)
|
|
|
+const deviceDrawerVisible = ref(false)
|
|
|
+const currentLss = ref<LssDTO | null>(null)
|
|
|
+
|
|
|
+interface DeviceItem {
|
|
|
+ id: number
|
|
|
+ localIp: string
|
|
|
+ deviceId: string
|
|
|
+ name: string
|
|
|
+ heartbeat: LssHeartbeatStatus
|
|
|
+ heartbeatTime?: string
|
|
|
+ manufacturer: string
|
|
|
+ model: string
|
|
|
+ addTime: string
|
|
|
+}
|
|
|
+
|
|
|
+const deviceList = ref<DeviceItem[]>([])
|
|
|
+
|
|
|
+// 排序状态
|
|
|
+const sortState = reactive<{
|
|
|
+ prop: string
|
|
|
+ order: 'ascending' | 'descending' | null
|
|
|
+}>({
|
|
|
+ prop: '',
|
|
|
+ order: null
|
|
|
+})
|
|
|
+
|
|
|
+// 搜索表单
|
|
|
+const searchForm = reactive<{
|
|
|
+ keyword: string
|
|
|
+ heartbeat: LssHeartbeatStatus | ''
|
|
|
+}>({
|
|
|
+ keyword: '',
|
|
|
+ heartbeat: ''
|
|
|
+})
|
|
|
+
|
|
|
+// 分页相关
|
|
|
+const currentPage = ref(1)
|
|
|
+const pageSize = ref(20)
|
|
|
+const total = ref(0)
|
|
|
+
|
|
|
+// 排序后的数据
|
|
|
+const sortedList = computed(() => {
|
|
|
+ const list = [...lssList.value]
|
|
|
+ if (sortState.prop && sortState.order) {
|
|
|
+ list.sort((a, b) => {
|
|
|
+ const aVal = a[sortState.prop as keyof LssDTO]
|
|
|
+ const bVal = b[sortState.prop as keyof LssDTO]
|
|
|
+
|
|
|
+ if (aVal == null && bVal == null) return 0
|
|
|
+ if (aVal == null) return sortState.order === 'ascending' ? -1 : 1
|
|
|
+ if (bVal == null) return sortState.order === 'ascending' ? 1 : -1
|
|
|
+
|
|
|
+ let result = 0
|
|
|
+ if (typeof aVal === 'number' && typeof bVal === 'number') {
|
|
|
+ result = aVal - bVal
|
|
|
+ } else {
|
|
|
+ result = String(aVal).localeCompare(String(bVal))
|
|
|
+ }
|
|
|
+
|
|
|
+ return sortState.order === 'ascending' ? result : -result
|
|
|
+ })
|
|
|
+ }
|
|
|
+ return list
|
|
|
+})
|
|
|
+
|
|
|
+// 模拟测试数据
|
|
|
+const mockData: LssDTO[] = [
|
|
|
+ {
|
|
|
+ id: 1,
|
|
|
+ lssId: 'L001',
|
|
|
+ name: '现场-初台1',
|
|
|
+ address: '西新宿初台3-3-2',
|
|
|
+ publicIp: '10.72.44.56',
|
|
|
+ heartbeat: 'active',
|
|
|
+ heartbeatTime: '2026-01-18T12:33:31',
|
|
|
+ ablyInfo: 'ably-channel-001'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 2,
|
|
|
+ lssId: 'L002',
|
|
|
+ name: '现场-�的谷2',
|
|
|
+ address: '涩谷区道玄坂1-2-3',
|
|
|
+ publicIp: '10.72.44.57',
|
|
|
+ heartbeat: 'active',
|
|
|
+ heartbeatTime: '2026-01-18T12:35:22',
|
|
|
+ ablyInfo: 'ably-channel-002'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 3,
|
|
|
+ lssId: 'L003',
|
|
|
+ name: '现场-新宿3',
|
|
|
+ address: '新宿区歌舞伎町1-1-1',
|
|
|
+ publicIp: '10.72.44.58',
|
|
|
+ heartbeat: 'hold',
|
|
|
+ heartbeatTime: '2026-01-18T12:30:00',
|
|
|
+ ablyInfo: 'ably-channel-003'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 4,
|
|
|
+ lssId: 'L004',
|
|
|
+ name: '现场-池袋4',
|
|
|
+ address: '�的島区南池袋2-5-6',
|
|
|
+ publicIp: '10.72.44.59',
|
|
|
+ heartbeat: 'dead',
|
|
|
+ heartbeatTime: '2026-01-18T12:00:00',
|
|
|
+ ablyInfo: ''
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 5,
|
|
|
+ lssId: 'L005',
|
|
|
+ name: '现场-秋叶原5',
|
|
|
+ address: '千代田区外神田4-4-4',
|
|
|
+ publicIp: '10.72.44.60',
|
|
|
+ heartbeat: 'active',
|
|
|
+ heartbeatTime: '2026-01-18T12:36:10',
|
|
|
+ ablyInfo: 'ably-channel-005'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 6,
|
|
|
+ lssId: 'L006',
|
|
|
+ name: '现场-银座6',
|
|
|
+ address: '中央区银座5-5-5',
|
|
|
+ publicIp: '10.72.44.61',
|
|
|
+ heartbeat: 'hold',
|
|
|
+ heartbeatTime: '2026-01-18T12:32:00'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 7,
|
|
|
+ lssId: 'L007',
|
|
|
+ name: '现场-上野7',
|
|
|
+ address: '台东区上野公园7-7',
|
|
|
+ publicIp: '10.72.44.62',
|
|
|
+ heartbeat: 'dead',
|
|
|
+ heartbeatTime: '2026-01-18T11:50:00',
|
|
|
+ ablyInfo: 'ably-channel-007'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 8,
|
|
|
+ lssId: 'L008',
|
|
|
+ name: '现场-品川8',
|
|
|
+ address: '港区高轮3-3-3',
|
|
|
+ publicIp: '10.72.44.63',
|
|
|
+ heartbeat: 'active',
|
|
|
+ heartbeatTime: '2026-01-18T12:36:55',
|
|
|
+ ablyInfo: 'ably-channel-008'
|
|
|
+ }
|
|
|
+]
|
|
|
+
|
|
|
+async function getList() {
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ // TODO: 替换为真实 API 调用
|
|
|
+ // const params: Record<string, any> = {
|
|
|
+ // page: currentPage.value,
|
|
|
+ // size: pageSize.value
|
|
|
+ // }
|
|
|
+ // if (searchForm.keyword) {
|
|
|
+ // params.keyword = searchForm.keyword
|
|
|
+ // }
|
|
|
+ // if (searchForm.heartbeat) {
|
|
|
+ // params.heartbeat = searchForm.heartbeat
|
|
|
+ // }
|
|
|
+ // const res = await listLss(params)
|
|
|
+ // if (res.success) {
|
|
|
+ // lssList.value = res.data.list
|
|
|
+ // total.value = res.data.total || 0
|
|
|
+ // }
|
|
|
+
|
|
|
+ // 使用模拟数据
|
|
|
+ await new Promise((resolve) => setTimeout(resolve, 300))
|
|
|
+ let filtered = [...mockData]
|
|
|
+
|
|
|
+ // 关键词过滤
|
|
|
+ if (searchForm.keyword) {
|
|
|
+ const kw = searchForm.keyword.toLowerCase()
|
|
|
+ filtered = filtered.filter(
|
|
|
+ (item) => item.lssId.toLowerCase().includes(kw) || item.name.toLowerCase().includes(kw)
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ // 心跳状态过滤
|
|
|
+ if (searchForm.heartbeat) {
|
|
|
+ filtered = filtered.filter((item) => item.heartbeat === searchForm.heartbeat)
|
|
|
+ }
|
|
|
+
|
|
|
+ total.value = filtered.length
|
|
|
+ const start = (currentPage.value - 1) * pageSize.value
|
|
|
+ lssList.value = filtered.slice(start, start + pageSize.value)
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function handleSearch() {
|
|
|
+ currentPage.value = 1
|
|
|
+ getList()
|
|
|
+}
|
|
|
+
|
|
|
+function handleReset() {
|
|
|
+ searchForm.keyword = ''
|
|
|
+ searchForm.heartbeat = ''
|
|
|
+ currentPage.value = 1
|
|
|
+ sortState.prop = ''
|
|
|
+ sortState.order = null
|
|
|
+ getList()
|
|
|
+}
|
|
|
+
|
|
|
+function handleSortChange({ prop, order }: { prop: string; order: 'ascending' | 'descending' | null }) {
|
|
|
+ sortState.prop = prop || ''
|
|
|
+ sortState.order = order
|
|
|
+ getList()
|
|
|
+}
|
|
|
+
|
|
|
+function handleSizeChange(val: number) {
|
|
|
+ pageSize.value = val
|
|
|
+ currentPage.value = 1
|
|
|
+ getList()
|
|
|
+}
|
|
|
+
|
|
|
+function handleCurrentChange(val: number) {
|
|
|
+ currentPage.value = val
|
|
|
+ getList()
|
|
|
+}
|
|
|
+
|
|
|
+function handleViewDetail(row: LssDTO) {
|
|
|
+ currentLss.value = row
|
|
|
+ detailDrawerVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+function handleViewDevices(row: LssDTO) {
|
|
|
+ currentLss.value = row
|
|
|
+ // 模拟设备列表数据
|
|
|
+ deviceList.value = [
|
|
|
+ {
|
|
|
+ id: 1,
|
|
|
+ localIp: '192.168.0.64',
|
|
|
+ deviceId: 'CAM-069',
|
|
|
+ name: '攝像頭名稱1',
|
|
|
+ heartbeat: 'active',
|
|
|
+ heartbeatTime: '2026-01-16T14:33:33',
|
|
|
+ manufacturer: 'HIKVISION',
|
|
|
+ model: 'DS-2DE2A404IW-DE3',
|
|
|
+ addTime: '26-01-16 14:33:33'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 2,
|
|
|
+ localIp: '10.0.2.102',
|
|
|
+ deviceId: 'CAM-069',
|
|
|
+ name: '攝像頭名稱1',
|
|
|
+ heartbeat: 'hold',
|
|
|
+ heartbeatTime: '2026-01-16T14:30:00',
|
|
|
+ manufacturer: 'HIKVISION',
|
|
|
+ model: 'DS-2DE2A404IW-DE3',
|
|
|
+ addTime: '26-01-16 14:33:33'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 3,
|
|
|
+ localIp: '10.0.2.102',
|
|
|
+ deviceId: 'CAM-069',
|
|
|
+ name: '攝像頭名稱1',
|
|
|
+ heartbeat: 'dead',
|
|
|
+ heartbeatTime: '2026-01-16T14:00:00',
|
|
|
+ manufacturer: 'HIKVISION',
|
|
|
+ model: 'DS-2DE2A404IW-DE3',
|
|
|
+ addTime: '26-01-16 14:33:33'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 4,
|
|
|
+ localIp: '10.0.2.102',
|
|
|
+ deviceId: 'CAM-069',
|
|
|
+ name: '攝像頭名稱1',
|
|
|
+ heartbeat: 'active',
|
|
|
+ heartbeatTime: '2026-01-16T14:33:33',
|
|
|
+ manufacturer: 'HIKVISION',
|
|
|
+ model: 'DS-2DE2A404IW-DE3',
|
|
|
+ addTime: '26-01-16 14:33:33'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 5,
|
|
|
+ localIp: '10.0.2.102',
|
|
|
+ deviceId: 'CAM-069',
|
|
|
+ name: '攝像頭名稱1',
|
|
|
+ heartbeat: 'hold',
|
|
|
+ heartbeatTime: '2026-01-16T14:30:00',
|
|
|
+ manufacturer: 'HIKVISION',
|
|
|
+ model: 'DS-2DE2A404IW-DE3',
|
|
|
+ addTime: '26-01-16 14:33:33'
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ deviceDrawerVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+// 格式化设备心跳显示
|
|
|
+function formatDeviceHeartbeat(row: DeviceItem): string {
|
|
|
+ if (row.heartbeat === 'active') {
|
|
|
+ const time = row.heartbeatTime ? dayjs(row.heartbeatTime).format('YY-MM-DD HH:mm:ss') : ''
|
|
|
+ return `active [${time}]`
|
|
|
+ } else if (row.heartbeat === 'hold') {
|
|
|
+ return 'hold(5分鐘內)'
|
|
|
+ } else if (row.heartbeat === 'dead') {
|
|
|
+ return 'dead (超過5分)'
|
|
|
+ }
|
|
|
+ return row.heartbeat || '-'
|
|
|
+}
|
|
|
+
|
|
|
+function handleAddDevice() {
|
|
|
+ ElMessage.info('新增设备')
|
|
|
+}
|
|
|
+
|
|
|
+function handleViewConfig(row: DeviceItem) {
|
|
|
+ ElMessage.info(`查看参数配置: ${row.deviceId}`)
|
|
|
+}
|
|
|
+
|
|
|
+function handleViewRunParams(row: DeviceItem) {
|
|
|
+ ElMessage.info(`查看运行参数: ${row.deviceId}`)
|
|
|
+}
|
|
|
+
|
|
|
+function handleEditDevice(row: DeviceItem) {
|
|
|
+ ElMessage.info(`编辑设备: ${row.deviceId}`)
|
|
|
+}
|
|
|
+
|
|
|
+async function handleDeleteDevice(row: DeviceItem) {
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm(`确定要删除设备 "${row.name}" 吗?`, '提示', {
|
|
|
+ type: 'warning'
|
|
|
+ })
|
|
|
+ ElMessage.success('删除成功')
|
|
|
+ } catch (error) {
|
|
|
+ if (error !== 'cancel') {
|
|
|
+ console.error('删除失败', error)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function handleEdit(row: LssDTO) {
|
|
|
+ // TODO: 实现编辑功能
|
|
|
+ ElMessage.info(`编辑 ${row.name}`)
|
|
|
+}
|
|
|
+
|
|
|
+async function handleDelete(row: LssDTO) {
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm(`确定要删除 LSS "${row.name}" 吗?`, '提示', {
|
|
|
+ type: 'warning'
|
|
|
+ })
|
|
|
+ // TODO: 调用删除 API
|
|
|
+ ElMessage.success('删除成功')
|
|
|
+ getList()
|
|
|
+ } catch (error) {
|
|
|
+ if (error !== 'cancel') {
|
|
|
+ console.error('删除失败', error)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ getList()
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.page-container {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ height: 100%;
|
|
|
+ padding: 1rem;
|
|
|
+ box-sizing: border-box;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.search-form {
|
|
|
+ flex-shrink: 0;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ padding: 16px 16px 4px 16px;
|
|
|
+ background: #f5f7fa;
|
|
|
+
|
|
|
+ :deep(.el-form-item) {
|
|
|
+ margin-bottom: 12px;
|
|
|
+ margin-right: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.el-input),
|
|
|
+ :deep(.el-select) {
|
|
|
+ width: 160px;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.el-button--primary) {
|
|
|
+ background-color: #4f46e5;
|
|
|
+ border-color: #4f46e5;
|
|
|
+
|
|
|
+ &:hover,
|
|
|
+ &:focus {
|
|
|
+ background-color: #6366f1;
|
|
|
+ border-color: #6366f1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.table-wrapper {
|
|
|
+ flex: 1;
|
|
|
+ min-height: 0;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.pagination-container {
|
|
|
+ flex-shrink: 0;
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ padding-top: 16px;
|
|
|
+
|
|
|
+ :deep(.el-pagination) {
|
|
|
+ .el-pager li.is-active {
|
|
|
+ background-color: #4f46e5;
|
|
|
+ color: #fff;
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-pager li:not(.is-active):hover {
|
|
|
+ color: #4f46e5;
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn-prev:hover,
|
|
|
+ .btn-next:hover {
|
|
|
+ color: #4f46e5;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 心跳状态样式
|
|
|
+.heartbeat-active {
|
|
|
+ color: #67c23a;
|
|
|
+}
|
|
|
+
|
|
|
+.heartbeat-hold {
|
|
|
+ color: #e6a23c;
|
|
|
+}
|
|
|
+
|
|
|
+.heartbeat-dead {
|
|
|
+ color: #f56c6c;
|
|
|
+}
|
|
|
+
|
|
|
+// 抽屉样式
|
|
|
+.drawer-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ width: 100%;
|
|
|
+
|
|
|
+ span {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.el-button--primary) {
|
|
|
+ background-color: #4f46e5;
|
|
|
+ border-color: #4f46e5;
|
|
|
+
|
|
|
+ &:hover,
|
|
|
+ &:focus {
|
|
|
+ background-color: #6366f1;
|
|
|
+ border-color: #6366f1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-drawer) {
|
|
|
+ .el-drawer__header {
|
|
|
+ margin-bottom: 0;
|
|
|
+ padding: 16px 20px;
|
|
|
+ border-bottom: 1px solid #e5e7eb;
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-drawer__body {
|
|
|
+ padding: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-descriptions {
|
|
|
+ .el-descriptions__label {
|
|
|
+ width: 100px;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-link--primary {
|
|
|
+ color: #4f46e5;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ color: #6366f1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 表格样式
|
|
|
+:deep(.el-table) {
|
|
|
+ --el-table-row-hover-bg-color: #f0f0ff;
|
|
|
+
|
|
|
+ .el-table__row--striped td.el-table__cell {
|
|
|
+ background-color: #f8f9fc;
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-table__header th {
|
|
|
+ background-color: #f5f7fa;
|
|
|
+ color: #333;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-button--primary.is-link {
|
|
|
+ color: #4f46e5;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ color: #6366f1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .caret-wrapper {
|
|
|
+ .sort-caret.ascending {
|
|
|
+ border-bottom-color: #4f46e5;
|
|
|
+ }
|
|
|
+
|
|
|
+ .sort-caret.descending {
|
|
|
+ border-top-color: #4f46e5;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|