瀏覽代碼

终端设备远程测控开发

zengy 2 天之前
父節點
當前提交
0c7752a8fb

+ 17 - 42
src/views/subSystem/pipeNetwork/pinteControl/realTimeShow.vue

@@ -210,21 +210,19 @@
                   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' ? 180 : 104)"
+                  :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'">
-                      <el-button
+                    <span v-else-if="c.key === 'filepath'" class="rec-audio-cell">
+                      <audio
                         v-if="valueOf(row, c.key) && valueOf(row, c.key) !== '—'"
-                        class="rec-play-btn"
-                        size="small"
-                        circle
-                        :type="isPlayingCell($index, c.key) ? 'primary' : 'default'"
-                        @click="toggleAudio($index, c.key, row)"
-                      >
-                        <el-icon><VideoPause v-if="isPlayingCell($index, c.key)" /><VideoPlay v-else /></el-icon>
-                      </el-button>
+                        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>
@@ -243,7 +241,6 @@
                 small
                 v-model:current-page="recordPage"
               />
-              <audio ref="recordAudio" class="rec-audio-single" @ended="onAudioEnded"></audio>
             </div>
           </div>
 
@@ -404,38 +401,15 @@ function alarmOf(row, key) {
   return c ? !!c.alarm : false;
 }
 
-// 采集记录音频播放:单例 audio,一次只播一条
-const recordAudio = ref(null);
-const playingKey = ref("");
-
-function cellPlayKey(index, key) {
-  return index + ":" + key;
-}
-function isPlayingCell(index, key) {
-  return playingKey.value === cellPlayKey(index, key);
-}
-function toggleAudio(index, key, row) {
-  const el = recordAudio.value;
-  if (!el) return;
-  const k = cellPlayKey(index, key);
-  if (playingKey.value === k) {
-    el.pause();
-    playingKey.value = "";
-    return;
-  }
-  const v = valueOf(row, key);
-  if (!v || v === "—") return;
-  el.src = buildMinioAttachmentUrl(v);
-  el.play().catch(() => {});
-  playingKey.value = k;
-}
-function onAudioEnded() {
-  playingKey.value = "";
+// 采集记录音频:行内原生播放器,一次只播一条
+function pauseOtherAudios(e) {
+  document.querySelectorAll("audio.rec-audio").forEach((el) => {
+    if (el !== e.target) el.pause();
+  });
 }
 function onDrawerClosed() {
   stopProfileTimer();
-  if (recordAudio.value) recordAudio.value.pause();
-  playingKey.value = "";
+  document.querySelectorAll("audio.rec-audio").forEach((el) => el.pause());
 }
 
 // 报警等级配色(与系统 alarm 模块保持一致:1红 2橙 3蓝 4绿)
@@ -800,7 +774,8 @@ onBeforeUnmount(() => {
 .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-single { display: none; }
+.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; }
 
 @keyframes pulse {

+ 468 - 0
src/views/subSystem/pipeNetwork/pinteControl/remoteControl.vue

@@ -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>