|
|
@@ -0,0 +1,468 @@
|
|
|
+<!-- 终端设备远程测控(三级报警控远程手动控制) -->
|
|
|
+<template>
|
|
|
+ <div class="remote-ctl-page">
|
|
|
+ <!-- 查询条件:仅设备编码、设备名称 -->
|
|
|
+ <el-card shadow="never" class="alarm-filter-card">
|
|
|
+ <el-form :model="query" inline class="alarm-filters" @submit.prevent="handleSearch">
|
|
|
+ <el-form-item label="设备编码">
|
|
|
+ <el-input v-model="query.equipmentCode" placeholder="请输入设备编码" clearable style="width: 220px" @keyup.enter="handleSearch" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="设备名称">
|
|
|
+ <el-input v-model="query.equipmentName" placeholder="请输入设备名称" clearable style="width: 220px" @keyup.enter="handleSearch" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button>
|
|
|
+ <el-button :icon="Refresh" @click="handleReset">重置</el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- 设备列表 -->
|
|
|
+ <section class="alarm-table-panel">
|
|
|
+ <div class="alarm-toolbar">
|
|
|
+ <h3>终端设备列表</h3>
|
|
|
+ <span class="toolbar-right">
|
|
|
+ <span class="muted">共 {{ total }} 条</span>
|
|
|
+ <el-button type="primary" plain size="small" :loading="loading" @click="loadList">刷新状态</el-button>
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ <el-table v-loading="loading" :data="list" border stripe size="small" :header-cell-style="{ background: '#f5f7fa', color: '#606266' }">
|
|
|
+ <el-table-column prop="equipmentCode" label="设备编号" min-width="160" show-overflow-tooltip />
|
|
|
+ <el-table-column prop="equipmentName" label="设备名称" min-width="160" show-overflow-tooltip />
|
|
|
+ <el-table-column prop="equipmentTypeName" label="设备类型" min-width="150" show-overflow-tooltip />
|
|
|
+ <el-table-column label="片区" min-width="90">
|
|
|
+ <template #default="{ row }">{{ districtName(row.district) }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="设备状态" min-width="150">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <span class="tag" :class="isOnline(row) ? 'tag-green' : 'tag-gray'"><i class="dot"></i>{{ isOnline(row) ? '在线' : '离线' }}</span>
|
|
|
+ <span v-if="isAlarm(row)" class="tag tag-red"><i class="dot"></i>报警</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="实时数据" min-width="220" show-overflow-tooltip>
|
|
|
+ <template #default="{ row }">
|
|
|
+ <span :class="{ 'cell-alarm': hasAlarmInLatest(row) }">{{ latestMetricsText(row) }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="最后通信" min-width="170">
|
|
|
+ <template #default="{ row }">{{ profileMap[row.equipmentId]?.lastCommTime || '—' }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="100" fixed="right">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-button type="primary" link size="small" @click="openControl(row)">远程控制</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <template #empty><el-empty description="没有符合条件的设备" /></template>
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <pagination v-show="total > 0" v-model:page="query.pageNum" v-model:limit="query.pageSize" :total="total" @pagination="loadList" />
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <!-- ============ 远程控制抽屉 ============ -->
|
|
|
+ <el-drawer
|
|
|
+ v-model="drawerOpen"
|
|
|
+ :title="drawerTitle"
|
|
|
+ size="720px"
|
|
|
+ append-to-body
|
|
|
+ direction="rtl"
|
|
|
+ @opened="startDrawerTimer"
|
|
|
+ @closed="onDrawerClosed"
|
|
|
+ >
|
|
|
+ <div v-loading="profileLoading">
|
|
|
+ <template v-if="currentProfile.base">
|
|
|
+ <!-- 基础信息 -->
|
|
|
+ <div class="ctl-dev">
|
|
|
+ <div class="ctl-dev-grid">
|
|
|
+ <div>设备编号:<b>{{ currentProfile.base.equipmentCode }}</b></div>
|
|
|
+ <div>设备类型:<b>{{ currentProfile.base.equipmentTypeName || '—' }}</b></div>
|
|
|
+ <div>管辖片区:<b>{{ districtName(currentProfile.base.district) }}</b></div>
|
|
|
+ <div>安装位置:<b>{{ currentProfile.base.equipmentLocation || '—' }}</b></div>
|
|
|
+ <div class="full">最后通信:<b>{{ currentProfile.lastCommTime || '—' }}</b></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 下发指令 -->
|
|
|
+ <div class="form-item">
|
|
|
+ <span>上报间隔</span>
|
|
|
+ <div class="row gap10">
|
|
|
+ <el-input-number v-model="reportInterval" :min="5" :max="3600" :step="1" controls-position="right" style="width: 160px" />
|
|
|
+ <span class="unit">秒</span>
|
|
|
+ </div>
|
|
|
+ <div class="quick-steps">
|
|
|
+ <button
|
|
|
+ v-for="s in REPORT_STEPS"
|
|
|
+ :key="s"
|
|
|
+ type="button"
|
|
|
+ class="chip"
|
|
|
+ :class="{ active: reportInterval === s }"
|
|
|
+ @click="reportInterval = s"
|
|
|
+ >{{ s }}s</button>
|
|
|
+ </div>
|
|
|
+ <div class="form-tip">
|
|
|
+ 可设置范围 5 ~ 3600 秒,步进 1 秒;当前上报间隔 {{ currentReportInterval }} 秒。上报间隔越短数据越及时,通信流量与设备功耗越高。
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="drawer-actions">
|
|
|
+ <el-button type="primary" @click="sendCommand">下发指令</el-button>
|
|
|
+ <el-button @click="drawerOpen = false">取消</el-button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 指令下发记录(本会话留痕,纯前端) -->
|
|
|
+ <div v-if="cmdLogs.length" class="form-item">
|
|
|
+ <span>指令下发记录</span>
|
|
|
+ <el-table :data="cmdLogs" size="small" border>
|
|
|
+ <el-table-column prop="time" label="时间" width="170" />
|
|
|
+ <el-table-column prop="deviceName" label="设备" min-width="120" show-overflow-tooltip />
|
|
|
+ <el-table-column prop="param" label="参数" min-width="120" />
|
|
|
+ <el-table-column label="结果" width="90">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <span class="tag" :class="row.result === '成功' ? 'tag-green' : 'tag-red'"><i class="dot"></i>{{ row.result }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 采集记录(最近 50 条,与 realTimeShow 一致) -->
|
|
|
+ <div class="form-item">
|
|
|
+ <span>采集记录(最近 50 条)</span>
|
|
|
+ <div class="hist-wrap">
|
|
|
+ <el-table :data="pagedRecordRows" max-height="420" size="small">
|
|
|
+ <el-table-column
|
|
|
+ v-for="c in recordViewColumns"
|
|
|
+ :key="c.key"
|
|
|
+ :label="c.label + (c.unit ? '(' + c.unit + ')' : '')"
|
|
|
+ :min-width="c.key === 'create_time' ? 168 : (c.key === 'filepath' ? 170 : 104)"
|
|
|
+ >
|
|
|
+ <template #default="{ row, $index }">
|
|
|
+ <span v-if="c.key === 'create_time'">{{ row.createTime || '—' }}</span>
|
|
|
+ <span v-else-if="c.key === 'filepath'" class="rec-audio-cell">
|
|
|
+ <audio
|
|
|
+ v-if="valueOf(row, c.key) && valueOf(row, c.key) !== '—'"
|
|
|
+ class="rec-audio"
|
|
|
+ controls
|
|
|
+ preload="none"
|
|
|
+ :src="buildMinioAttachmentUrl(valueOf(row, c.key))"
|
|
|
+ @play="pauseOtherAudios"
|
|
|
+ ></audio>
|
|
|
+ <span v-else>—</span>
|
|
|
+ </span>
|
|
|
+ <span v-else :class="{ 'cell-alarm': alarmOf(row, c.key) }">{{ valueOf(row, c.key) }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <template #empty><div class="rec-empty">暂无采集记录</div></template>
|
|
|
+ </el-table>
|
|
|
+ <el-pagination
|
|
|
+ v-if="recordRows.length"
|
|
|
+ class="rec-pager"
|
|
|
+ layout="prev, pager, next"
|
|
|
+ :total="recordRows.length"
|
|
|
+ :page-size="recordPageSize"
|
|
|
+ small
|
|
|
+ v-model:current-page="recordPage"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <el-empty v-else description="未找到设备档案" />
|
|
|
+ </div>
|
|
|
+ </el-drawer>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
|
|
+import { Search, Refresh } from '@element-plus/icons-vue'
|
|
|
+import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
+import { pagePerceptionEquipment, getEquipmentProfile } from '@/api/pipeNetwork/perception'
|
|
|
+import { buildMinioAttachmentUrl } from '@/utils/minioAttachment'
|
|
|
+
|
|
|
+const loading = ref(false)
|
|
|
+const list = ref([])
|
|
|
+const total = ref(0)
|
|
|
+const query = reactive({
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ equipmentCode: '',
|
|
|
+ equipmentName: ''
|
|
|
+})
|
|
|
+
|
|
|
+// 设备档案缓存(key=equipmentId),列表实时数据列与抽屉共用,复用 getEquipmentProfile
|
|
|
+const profileMap = reactive({})
|
|
|
+
|
|
|
+const drawerOpen = ref(false)
|
|
|
+const drawerTitle = ref('')
|
|
|
+const profileLoading = ref(false)
|
|
|
+const currentDevice = ref(null)
|
|
|
+const currentProfile = ref({ base: null, lastCommTime: '', recordView: { columns: [], rows: [] } })
|
|
|
+let drawerTimer = null
|
|
|
+
|
|
|
+// 下发指令(假操作)相关
|
|
|
+const REPORT_STEPS = [5, 10, 30, 60, 300, 600, 1800, 3600]
|
|
|
+const reportInterval = ref(30)
|
|
|
+const currentReportInterval = ref(30)
|
|
|
+const cmdLogs = ref([])
|
|
|
+
|
|
|
+// 采集记录分页(每页 10 条)
|
|
|
+const recordPage = ref(1)
|
|
|
+const recordPageSize = 10
|
|
|
+
|
|
|
+const districtOptions = [
|
|
|
+ { value: 0, label: '城南' },
|
|
|
+ { value: 1, label: '城北' },
|
|
|
+ { value: 2, label: '太常' }
|
|
|
+]
|
|
|
+
|
|
|
+function districtName(district) {
|
|
|
+ const found = districtOptions.find((d) => d.value === district)
|
|
|
+ return found ? found.label : '—'
|
|
|
+}
|
|
|
+function isOnline(d) { return d.onlineStatus === 1 }
|
|
|
+function isAlarm(d) { return d.alarmStatus === 1 }
|
|
|
+
|
|
|
+// ============ 实时数据列(采集记录最新一条的指标项) ============
|
|
|
+function profileOf(row) {
|
|
|
+ return profileMap[row.equipmentId] || null
|
|
|
+}
|
|
|
+function latestRowOf(row) {
|
|
|
+ const p = profileOf(row)
|
|
|
+ const rows = p?.recordView?.rows || []
|
|
|
+ return rows.length ? rows[0] : null
|
|
|
+}
|
|
|
+function columnLabelMap(p) {
|
|
|
+ const map = {}
|
|
|
+ ;(p?.recordView?.columns || []).forEach((c) => { map[c.key] = c })
|
|
|
+ return map
|
|
|
+}
|
|
|
+function latestMetricsText(row) {
|
|
|
+ if (!isOnline(row)) return '—' // 离线设备不展示实时数据
|
|
|
+ const p = profileOf(row)
|
|
|
+ const first = latestRowOf(row)
|
|
|
+ if (!first) return '—'
|
|
|
+ const colMap = columnLabelMap(p)
|
|
|
+ return (first.cells || [])
|
|
|
+ .filter((c) => c.key !== 'filepath')
|
|
|
+ .map((c) => {
|
|
|
+ const label = colMap[c.key]?.label || c.key
|
|
|
+ const v = c.value && c.value !== '' ? c.value : (c.display || '—')
|
|
|
+ return `${label}:${v}`
|
|
|
+ })
|
|
|
+ .join(' ')
|
|
|
+}
|
|
|
+function hasAlarmInLatest(row) {
|
|
|
+ const first = latestRowOf(row)
|
|
|
+ return (first?.cells || []).some((c) => c.alarm)
|
|
|
+}
|
|
|
+
|
|
|
+// ============ 采集记录渲染(与 realTimeShow 一致) ============
|
|
|
+const recordView = computed(() => currentProfile.value.recordView || { columns: [], rows: [] })
|
|
|
+const recordViewColumns = computed(() => recordView.value.columns || [])
|
|
|
+const recordRows = computed(() => recordView.value.rows || [])
|
|
|
+const pagedRecordRows = computed(() => {
|
|
|
+ const start = (recordPage.value - 1) * recordPageSize
|
|
|
+ return recordRows.value.slice(start, start + recordPageSize)
|
|
|
+})
|
|
|
+function valueOf(row, key) {
|
|
|
+ const cells = row.cells || []
|
|
|
+ const c = cells.find((x) => x.key === key)
|
|
|
+ if (!c) return '—'
|
|
|
+ if (recordView.value.layout === 'long') return c.display
|
|
|
+ return c.value && c.value !== '' ? c.value : c.display
|
|
|
+}
|
|
|
+function alarmOf(row, key) {
|
|
|
+ const cells = row.cells || []
|
|
|
+ const c = cells.find((x) => x.key === key)
|
|
|
+ return c ? !!c.alarm : false
|
|
|
+}
|
|
|
+
|
|
|
+// 采集记录音频:行内原生播放器,一次只播一条
|
|
|
+function pauseOtherAudios(e) {
|
|
|
+ document.querySelectorAll('audio.rec-audio').forEach((el) => {
|
|
|
+ if (el !== e.target) el.pause()
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// ============ 数据加载 ============
|
|
|
+function loadList() {
|
|
|
+ loading.value = true
|
|
|
+ return pagePerceptionEquipment({
|
|
|
+ pageNum: query.pageNum,
|
|
|
+ pageSize: query.pageSize,
|
|
|
+ equipmentCode: query.equipmentCode || undefined,
|
|
|
+ equipmentName: query.equipmentName || undefined,
|
|
|
+ rootTypeId: undefined
|
|
|
+ })
|
|
|
+ .then((res) => {
|
|
|
+ const page = res.data || {}
|
|
|
+ list.value = page.records || []
|
|
|
+ total.value = page.total || 0
|
|
|
+ // 批量拉取档案,填充列表实时数据列;复用 getEquipmentProfile,不新增接口
|
|
|
+ return Promise.all(
|
|
|
+ list.value.map((d) =>
|
|
|
+ getEquipmentProfile(d.equipmentId)
|
|
|
+ .then((p) => { profileMap[d.equipmentId] = p.data || null })
|
|
|
+ .catch(() => { profileMap[d.equipmentId] = null })
|
|
|
+ )
|
|
|
+ )
|
|
|
+ })
|
|
|
+ .finally(() => { loading.value = false })
|
|
|
+}
|
|
|
+function handleSearch() {
|
|
|
+ query.pageNum = 1
|
|
|
+ loadList()
|
|
|
+}
|
|
|
+function handleReset() {
|
|
|
+ query.equipmentCode = ''
|
|
|
+ query.equipmentName = ''
|
|
|
+ query.pageNum = 1
|
|
|
+ loadList()
|
|
|
+}
|
|
|
+
|
|
|
+// ============ 远程控制抽屉 ============
|
|
|
+function openControl(row) {
|
|
|
+ currentDevice.value = row
|
|
|
+ drawerTitle.value = '远程手动控制 · ' + row.equipmentName
|
|
|
+ drawerOpen.value = true
|
|
|
+ recordPage.value = 1
|
|
|
+ reportInterval.value = currentReportInterval.value = 30
|
|
|
+ profileLoading.value = true
|
|
|
+ const cached = profileMap[row.equipmentId]
|
|
|
+ if (cached) {
|
|
|
+ currentProfile.value = cached
|
|
|
+ profileLoading.value = false
|
|
|
+ } else {
|
|
|
+ getEquipmentProfile(row.equipmentId)
|
|
|
+ .then((res) => { currentProfile.value = res.data || { base: null, lastCommTime: '', recordView: { columns: [], rows: [] } } })
|
|
|
+ .finally(() => { profileLoading.value = false })
|
|
|
+ }
|
|
|
+}
|
|
|
+function startDrawerTimer() {
|
|
|
+ stopDrawerTimer()
|
|
|
+ drawerTimer = setInterval(() => {
|
|
|
+ if (currentDevice.value) {
|
|
|
+ getEquipmentProfile(currentDevice.value.equipmentId)
|
|
|
+ .then((res) => {
|
|
|
+ const p = res.data || null
|
|
|
+ if (p) { profileMap[currentDevice.value.equipmentId] = p; currentProfile.value = p }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }, 3000)
|
|
|
+}
|
|
|
+function stopDrawerTimer() {
|
|
|
+ if (drawerTimer) clearInterval(drawerTimer)
|
|
|
+ drawerTimer = null
|
|
|
+}
|
|
|
+function onDrawerClosed() {
|
|
|
+ stopDrawerTimer()
|
|
|
+ document.querySelectorAll('audio.rec-audio').forEach((el) => el.pause())
|
|
|
+}
|
|
|
+
|
|
|
+// 下发指令:纯前端模拟,不调用后端接口
|
|
|
+function sendCommand() {
|
|
|
+ const num = Number(reportInterval.value)
|
|
|
+ if (!num || num < 5 || num > 3600) {
|
|
|
+ ElMessage.error('上报间隔须为 5 ~ 3600 秒之间的整数')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const d = currentDevice.value
|
|
|
+ const cur = currentReportInterval.value
|
|
|
+ if (d.onlineStatus !== 1) {
|
|
|
+ ElMessage.error(`指令下发失败:${d.equipmentName} 处于离线状态,指令将被拦截`)
|
|
|
+ cmdLogs.value.unshift({ time: nowText(), deviceName: d.equipmentName, param: `上报间隔 ${num} 秒`, result: '失败' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ ElMessageBox.confirm(
|
|
|
+ `将向设备 <b>${d.equipmentName}</b>(${d.equipmentCode})下发指令:上报间隔 ${cur} 秒 → <b>${num} 秒</b>`,
|
|
|
+ '确认下发控制指令',
|
|
|
+ { confirmButtonText: '确认下发', cancelButtonText: '取消', type: 'warning', dangerouslyUseHTMLString: true }
|
|
|
+ ).then(() => {
|
|
|
+ const btnText = '下发中...'
|
|
|
+ ElMessage.info(btnText)
|
|
|
+ setTimeout(() => {
|
|
|
+ currentReportInterval.value = num
|
|
|
+ if (profileMap[d.equipmentId]) profileMap[d.equipmentId].currentReportInterval = num
|
|
|
+ cmdLogs.value.unshift({ time: nowText(), deviceName: d.equipmentName, param: `上报间隔 ${num} 秒`, result: '成功' })
|
|
|
+ ElMessage.success(`上报间隔已更新为 ${num} 秒:${d.equipmentName}`)
|
|
|
+ }, 600)
|
|
|
+ }).catch(() => {})
|
|
|
+}
|
|
|
+function nowText() {
|
|
|
+ const dt = new Date()
|
|
|
+ const p = (n) => String(n).padStart(2, '0')
|
|
|
+ return `${dt.getFullYear()}-${p(dt.getMonth() + 1)}-${p(dt.getDate())} ${p(dt.getHours())}:${p(dt.getMinutes())}:${p(dt.getSeconds())}`
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => { loadList() })
|
|
|
+onBeforeUnmount(() => { stopDrawerTimer() })
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.remote-ctl-page {
|
|
|
+ padding: 16px;
|
|
|
+ background: #f0f3f8;
|
|
|
+ min-height: calc(100vh - 84px);
|
|
|
+}
|
|
|
+.title-sub { font-size: 12px; color: #97a3b7; font-weight: 400; }
|
|
|
+
|
|
|
+.alarm-filter-card { margin-bottom: 16px; }
|
|
|
+.alarm-filters { display: flex; flex-wrap: wrap; align-items: center; }
|
|
|
+/* 单行查询表单:清掉 el-form-item 默认的 18px 底部间距,保证卡片上下留白一致 */
|
|
|
+.alarm-filter-card :deep(.el-form-item) { margin-bottom: 0; }
|
|
|
+.alarm-table-panel { padding: 15px 20px 20px; margin-bottom: 20px; background: #fff; border: 1px solid var(--el-card-border-color); border-radius: 8px; }
|
|
|
+.alarm-toolbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
|
|
|
+.alarm-toolbar h3 { font-size: 15px; font-weight: 600; margin: 0; display: flex; align-items: center; gap: 8px; }
|
|
|
+.alarm-toolbar h3:before { content: ""; width: 3px; height: 15px; border-radius: 2px; background: #1677ff; }
|
|
|
+.toolbar-right { display: flex; align-items: center; gap: 12px; }
|
|
|
+.muted { color: #909399; font-size: 13px; }
|
|
|
+
|
|
|
+/* 状态标签(与 realTimeShow 一致) */
|
|
|
+.tag { display: inline-flex; align-items: center; gap: 5px; padding: 2px 9px; border-radius: 20px; font-size: 12px; font-weight: 500; }
|
|
|
+.tag-green { background: #dcfce7; color: #16a34a; }
|
|
|
+.tag-gray { background: #f1f5f9; color: #64748b; }
|
|
|
+.tag-red { background: #fee2e2; color: #ef4444; }
|
|
|
+.tag .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; display: inline-block; }
|
|
|
+.cell-alarm { color: #ef4444; font-weight: 600; }
|
|
|
+
|
|
|
+/* 抽屉内基础信息 / 表单项(复用 realTimeShow 样式) */
|
|
|
+.ctl-dev { background: #f8fafc; border: 1px solid #e3e9f2; border-radius: 8px; padding: 14px; margin-bottom: 16px; }
|
|
|
+.ctl-dev-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px 14px; font-size: 12px; color: #5e6d82; }
|
|
|
+.ctl-dev-grid .full { grid-column: 1 / -1; }
|
|
|
+.ctl-dev-grid b { color: #0a1628; font-weight: 500; }
|
|
|
+.form-item { display: block; margin-bottom: 14px; }
|
|
|
+.form-item > span { display: block; font-size: 13px; color: #5e6d82; margin-bottom: 6px; font-weight: 500; }
|
|
|
+.row { display: flex; align-items: center; }
|
|
|
+.row.gap10 { gap: 10px; }
|
|
|
+.unit { font-size: 13px; color: #5e6d82; }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+.quick-steps { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
|
|
|
+.chip { border: 1px solid #d6e6ff; background: #eef5ff; color: #1677ff; border-radius: 16px; padding: 4px 12px; font-size: 12px; cursor: pointer; transition: 0.15s; }
|
|
|
+.chip:hover { background: #d6e6ff; }
|
|
|
+.chip.active { background: #1677ff; color: #fff; border-color: #1677ff; }
|
|
|
+.form-tip { margin-top: 8px; font-size: 12px; color: #97a3b7; line-height: 1.6; }
|
|
|
+
|
|
|
+.drawer-actions { display: flex; gap: 10px; margin: 16px 0; }
|
|
|
+.drawer-actions :deep(.el-button) { flex: 1; height: 36px; }
|
|
|
+
|
|
|
+/* 采集记录表格(与 realTimeShow 一致) */
|
|
|
+.hist-wrap { overflow: auto; border-radius: 8px; background: #fff; }
|
|
|
+.hist-wrap :deep(.el-table) { font-size: 12px; border: 1px solid #e3e9f2; border-radius: 8px; }
|
|
|
+.hist-wrap :deep(.el-table th.el-table__cell) { background: #f7f9fc; color: #5e6d82; font-weight: 600; }
|
|
|
+.hist-wrap :deep(.el-table td.el-table__cell) { padding: 6px 0; border-bottom: 1px solid #eef2f7; }
|
|
|
+.hist-wrap :deep(.el-table .cell) { padding: 0 10px; line-height: 1.5; }
|
|
|
+.hist-wrap :deep(.el-table td.el-table__cell .cell) { white-space: nowrap; }
|
|
|
+.hist-wrap :deep(.el-table__header th.el-table__cell) { border-bottom: 1px solid #e3e9f2; }
|
|
|
+.hist-wrap :deep(.el-table::before),
|
|
|
+.hist-wrap :deep(.el-table::after),
|
|
|
+.hist-wrap :deep(.el-table__inner-wrapper::before),
|
|
|
+.hist-wrap :deep(.el-table__inner-wrapper::after) { display: none; }
|
|
|
+.hist-wrap :deep(.el-table .el-table__row:last-child td.el-table__cell) { border-bottom: none; }
|
|
|
+.rec-empty { color: #9aa7b8; font-size: 12px; padding: 28px 0; }
|
|
|
+.rec-audio-cell { display: flex; align-items: center; overflow: hidden; }
|
|
|
+.rec-audio { width: 150px; max-width: 100%; height: 26px; vertical-align: middle; }
|
|
|
+.rec-pager { margin-top: 10px; justify-content: flex-end; display: flex; }
|
|
|
+
|
|
|
+:deep(.el-drawer__header) { margin-bottom: 0; padding: 10px 20px; border-bottom: 1px solid #e3e9f2; color: #0a1628; }
|
|
|
+:deep(.el-drawer__body) { padding: 10px 20px 20px; }
|
|
|
+</style>
|