|
|
@@ -0,0 +1,748 @@
|
|
|
+<template>
|
|
|
+ <div class="water-page">
|
|
|
+ <div class="page-heading">
|
|
|
+ <div class="page-title">
|
|
|
+ <span class="page-title-icon"><el-icon><WarningFilled/></el-icon></span>
|
|
|
+ <div>
|
|
|
+ <h2>设备异常管理</h2>
|
|
|
+ <span class="muted">供水监测设备异常态势只读看板,覆盖报警中、离线与维修中</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <el-button class="heading-refresh" :icon="Refresh" :loading="loading" @click="loadData">刷新</el-button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-card shadow="never" class="filter-card">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-title">
|
|
|
+ <el-icon><Search/></el-icon>
|
|
|
+ <span>异常筛选</span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <el-form :inline="true" :model="query" @submit.prevent="handleQuery">
|
|
|
+ <el-form-item label="异常类型">
|
|
|
+ <el-select v-model="query.type" class="select--type" @change="handleQuery">
|
|
|
+ <el-option
|
|
|
+ v-for="option in abnormalTypeOptions"
|
|
|
+ :key="option.value"
|
|
|
+ :label="option.label"
|
|
|
+ :value="option.value"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="设备编码">
|
|
|
+ <el-input v-model="query.equipmentCode" clearable placeholder="请输入设备编码"
|
|
|
+ @keyup.enter="handleQuery"/>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="设备名称">
|
|
|
+ <el-input v-model="query.equipmentName" clearable placeholder="请输入设备名称"
|
|
|
+ @keyup.enter="handleQuery"/>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-button type="primary" :icon="Search" :loading="loading" @click="handleQuery">查询</el-button>
|
|
|
+ <el-button :icon="RefreshLeft" @click="resetQuery">重置</el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <el-alert
|
|
|
+ v-if="loadError"
|
|
|
+ class="page-error"
|
|
|
+ type="error"
|
|
|
+ show-icon
|
|
|
+ :closable="false"
|
|
|
+ title="异常设备加载失败"
|
|
|
+ description="请检查后端服务或网络连接后重试。"
|
|
|
+ >
|
|
|
+ <el-button size="small" type="primary" @click="loadData">重试</el-button>
|
|
|
+ </el-alert>
|
|
|
+
|
|
|
+ <div class="metric-grid">
|
|
|
+ <div v-for="item in abnormalMetrics" :key="item.key" class="metric">
|
|
|
+ <div class="metric-icon" :class="item.iconClass">
|
|
|
+ <el-icon><component :is="item.icon"/></el-icon>
|
|
|
+ </div>
|
|
|
+ <div class="metric-body">
|
|
|
+ <span>{{ item.label }}</span>
|
|
|
+ <div class="metric-value">
|
|
|
+ <strong :class="{ danger: item.danger }">{{ item.value }}</strong>
|
|
|
+ <small>{{ item.unit }}</small>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-card shadow="never" class="table-card">
|
|
|
+ <template #header>
|
|
|
+ <div class="table-toolbar">
|
|
|
+ <div class="card-title">
|
|
|
+ <el-icon><List/></el-icon>
|
|
|
+ <span>异常设备清单</span>
|
|
|
+ <span class="muted">共 {{ filteredRows.length }} 台</span>
|
|
|
+ </div>
|
|
|
+ <el-button
|
|
|
+ type="success"
|
|
|
+ plain
|
|
|
+ :icon="Download"
|
|
|
+ :disabled="!filteredRows.length"
|
|
|
+ @click="exportData"
|
|
|
+ >批量导出</el-button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <el-table :data="pagedRows" stripe border v-loading="loading" @row-click="openDetail">
|
|
|
+ <template #empty>
|
|
|
+ <el-empty :description="loadError ? '暂无法获取异常设备' : '当前筛选下没有异常设备'"/>
|
|
|
+ </template>
|
|
|
+ <el-table-column type="index" label="#" width="55"/>
|
|
|
+ <el-table-column prop="equipmentCode" label="设备编码" min-width="150" show-overflow-tooltip/>
|
|
|
+ <el-table-column prop="equipmentName" label="设备名称" min-width="150" show-overflow-tooltip/>
|
|
|
+ <el-table-column prop="equipmentTypeName" label="设备类型" min-width="130" show-overflow-tooltip/>
|
|
|
+ <el-table-column label="所属片区" width="100">
|
|
|
+ <template #default="{ row }">{{ districtLabel(row.district) }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="设备图片" width="92">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-image
|
|
|
+ v-if="row.equipmentImageUrl"
|
|
|
+ class="table-thumb"
|
|
|
+ :src="row.equipmentImageUrl"
|
|
|
+ :preview-src-list="[row.equipmentImageUrl]"
|
|
|
+ preview-teleported
|
|
|
+ fit="cover"
|
|
|
+ @click.stop
|
|
|
+ />
|
|
|
+ <span v-else class="muted">-</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="异常原因" min-width="130">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag v-for="type in row.abnormalTypes" :key="type" class="reason-tag"
|
|
|
+ size="small" :type="abnormalTypeTagType(type)">
|
|
|
+ {{ abnormalTypeLabel(type) }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="设备当前状态" width="115">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag size="small" :type="row.currentStatus === 1 ? 'warning' : 'info'">
|
|
|
+ {{ row.currentStatusText }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="设备报警状态" width="115">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag size="small" :type="row.alarmStatus === 1 ? 'danger' : 'success'">
|
|
|
+ {{ row.alarmStatusText }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="在线状态" width="95">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag size="small" :type="row.onlineStatus === 1 ? 'success' : 'info'">
|
|
|
+ {{ row.onlineStatusText }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="关联未办结工单" min-width="160" show-overflow-tooltip>
|
|
|
+ <template #default="{ row }">
|
|
|
+ <span>{{ row.relatedOrderNo }}</span>
|
|
|
+ <span v-if="row.workOrderStatusCode !== null" class="muted">
|
|
|
+ · {{ row.workOrderStatusText }}
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="90" fixed="right">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-button link type="primary" :icon="View" @click.stop="openDetail(row)">详情</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ <el-pagination
|
|
|
+ v-model:current-page="query.pageNum"
|
|
|
+ v-model:page-size="query.pageSize"
|
|
|
+ :total="filteredRows.length"
|
|
|
+ layout="total, sizes, prev, pager, next"
|
|
|
+ :page-sizes="[10, 20, 50]"
|
|
|
+ />
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <el-drawer
|
|
|
+ v-model="detailVisible"
|
|
|
+ class="detail-drawer"
|
|
|
+ direction="rtl"
|
|
|
+ size="480px"
|
|
|
+ :title="detailRow?.equipmentName || '设备详情'"
|
|
|
+ >
|
|
|
+ <div v-loading="detailLoading" class="detail-body">
|
|
|
+ <div v-if="detailRow" class="detail-header">
|
|
|
+ <el-image
|
|
|
+ v-if="detailImageUrl"
|
|
|
+ class="detail-media"
|
|
|
+ :src="detailImageUrl"
|
|
|
+ :preview-src-list="[detailImageUrl]"
|
|
|
+ preview-teleported
|
|
|
+ fit="cover"
|
|
|
+ />
|
|
|
+ <div v-else class="detail-media detail-media--empty">暂无设备图片</div>
|
|
|
+ <div class="detail-header-text">
|
|
|
+ <div class="detail-name">{{ detailRow.equipmentName || '未命名设备' }}</div>
|
|
|
+ <div class="muted">{{ detailRow.equipmentCode }} · {{ detailRow.equipmentTypeName || '未分类' }}</div>
|
|
|
+ <div class="reason-tags">
|
|
|
+ <el-tag v-for="type in detailRow.abnormalTypes" :key="type" size="small"
|
|
|
+ :type="abnormalTypeTagType(type)">
|
|
|
+ {{ abnormalTypeLabel(type) }}
|
|
|
+ </el-tag>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-alert
|
|
|
+ v-if="detailWarning"
|
|
|
+ type="warning"
|
|
|
+ show-icon
|
|
|
+ :closable="false"
|
|
|
+ :title="detailWarning"
|
|
|
+ >
|
|
|
+ <el-button size="small" link type="primary" @click="loadDetailEnrichment">重试</el-button>
|
|
|
+ </el-alert>
|
|
|
+
|
|
|
+ <div class="detail-section-title">设备状态</div>
|
|
|
+ <el-descriptions :column="1" border size="small">
|
|
|
+ <el-descriptions-item label="设备当前状态">{{ detailRow.currentStatusText }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="设备报警状态">{{ detailRow.alarmStatusText }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="在线状态">{{ detailRow.onlineStatusText }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="异常原因">{{ detailRow.abnormalReasonText }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item v-if="detailRow.alarmLevelText !== '未分级'" label="报警等级">
|
|
|
+ {{ detailRow.alarmLevelText }}
|
|
|
+ </el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+
|
|
|
+ <div class="detail-section-title">关联未办结工单</div>
|
|
|
+ <el-descriptions :column="1" border size="small">
|
|
|
+ <el-descriptions-item label="工单编号">{{ detailRow.relatedOrderNo }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="工单状态">
|
|
|
+ {{ orderStatusText(detailRow.workOrderStatusCode) }}
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="接单责任人">{{ detailRow.relatedWorkOrderPerson }}</el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+
|
|
|
+ <div class="detail-section-title">设备信息</div>
|
|
|
+ <el-descriptions :column="1" border size="small">
|
|
|
+ <el-descriptions-item label="所属片区">{{ districtLabel(detailRow.district) }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="安装位置">{{ detailRow.equipmentLocation || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="最近上报">{{ detailRow.lastDataTime || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="设备负责人">{{ detailRow.maintainer || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="联系电话">{{ detailRow.maintainerPhone || '-' }}</el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+ </div>
|
|
|
+ </el-drawer>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import {computed, onMounted, ref} from 'vue'
|
|
|
+import {
|
|
|
+ BellFilled,
|
|
|
+ CircleClose,
|
|
|
+ Download,
|
|
|
+ List,
|
|
|
+ Monitor,
|
|
|
+ Refresh,
|
|
|
+ RefreshLeft,
|
|
|
+ Search,
|
|
|
+ Tools,
|
|
|
+ View,
|
|
|
+ WarningFilled
|
|
|
+} from '@element-plus/icons-vue'
|
|
|
+import {ElMessage} from 'element-plus'
|
|
|
+import {saveAs} from 'file-saver'
|
|
|
+import {
|
|
|
+ getEquipmentDetail,
|
|
|
+ getEquipmentStatusStatistics,
|
|
|
+ listEquipment
|
|
|
+} from '@/api/pipeNetwork/pdeviceInform'
|
|
|
+import {getDeviceGisDetail} from '@/api/pipeNetwork/waterSupply'
|
|
|
+import {normalizeDeviceStatistics} from '@/utils/waterHomeModel'
|
|
|
+import {normalizePage, unwrapResponse} from '@/utils/waterSupplyModel'
|
|
|
+import {getRequestErrorMessage} from '@/utils/waterFacilityQuery'
|
|
|
+import {
|
|
|
+ ABNORMAL_TYPE_OPTIONS,
|
|
|
+ WATER_DEVICE_ROOT_TYPE_ID,
|
|
|
+ abnormalTypeLabel,
|
|
|
+ abnormalTypeTagType,
|
|
|
+ buildAbnormalCsv,
|
|
|
+ buildAbnormalListQuery,
|
|
|
+ computeAbnormalSummary,
|
|
|
+ filterAbnormalDevices,
|
|
|
+ normalizeAbnormalRow
|
|
|
+} from '@/utils/waterDeviceAbnormalModel'
|
|
|
+import {orderStatusText} from '@/views/subSystem/pipeNetwork/ops/workOrder/workOrderDict.js'
|
|
|
+
|
|
|
+const ABNORMAL_TYPE_FALLBACK = 'all'
|
|
|
+
|
|
|
+const districtOptions = [
|
|
|
+ {label: '城南', value: 0},
|
|
|
+ {label: '城北', value: 1},
|
|
|
+ {label: '太常', value: 2}
|
|
|
+]
|
|
|
+
|
|
|
+const abnormalTypeOptions = ABNORMAL_TYPE_OPTIONS
|
|
|
+
|
|
|
+const query = ref(createQuery())
|
|
|
+const rows = ref([])
|
|
|
+const statistics = ref({total: 0, online: 0, offline: 0, alarm: 0, onlineRate: 0})
|
|
|
+const loading = ref(false)
|
|
|
+const loadError = ref('')
|
|
|
+
|
|
|
+const detailVisible = ref(false)
|
|
|
+const detailLoading = ref(false)
|
|
|
+const detailWarning = ref('')
|
|
|
+const detailRow = ref(null)
|
|
|
+const detailImageUrl = ref('')
|
|
|
+
|
|
|
+function createQuery() {
|
|
|
+ return {
|
|
|
+ type: ABNORMAL_TYPE_FALLBACK,
|
|
|
+ equipmentCode: '',
|
|
|
+ equipmentName: '',
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 10
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function districtLabel(code) {
|
|
|
+ const hit = districtOptions.find(item => item.value === Number(code))
|
|
|
+ return hit ? hit.label : '-'
|
|
|
+}
|
|
|
+
|
|
|
+// 异常指标带取后端聚合的设备总量/在线/离线/报警,维修中由本地全量结果补齐
|
|
|
+const summary = computed(() => computeAbnormalSummary(rows.value))
|
|
|
+const abnormalMetrics = computed(() => [
|
|
|
+ {
|
|
|
+ key: 'total',
|
|
|
+ label: '供水设备总数',
|
|
|
+ value: statistics.value.total,
|
|
|
+ unit: '台',
|
|
|
+ icon: Monitor,
|
|
|
+ iconClass: 'metric-icon--blue'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'alarm',
|
|
|
+ label: '报警设备',
|
|
|
+ value: statistics.value.alarm,
|
|
|
+ unit: '台',
|
|
|
+ icon: BellFilled,
|
|
|
+ iconClass: 'metric-icon--red',
|
|
|
+ danger: true
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'offline',
|
|
|
+ label: '离线设备',
|
|
|
+ value: statistics.value.offline,
|
|
|
+ unit: '台',
|
|
|
+ icon: CircleClose,
|
|
|
+ iconClass: 'metric-icon--slate'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'maintenance',
|
|
|
+ label: '维修中设备',
|
|
|
+ value: summary.value.maintenance,
|
|
|
+ unit: '台',
|
|
|
+ icon: Tools,
|
|
|
+ iconClass: 'metric-icon--orange'
|
|
|
+ }
|
|
|
+])
|
|
|
+const filteredRows = computed(() => filterAbnormalDevices(rows.value, {type: query.value.type}))
|
|
|
+const pagedRows = computed(() => {
|
|
|
+ const start = (query.value.pageNum - 1) * query.value.pageSize
|
|
|
+ return filteredRows.value.slice(start, start + query.value.pageSize)
|
|
|
+})
|
|
|
+
|
|
|
+async function loadData() {
|
|
|
+ loading.value = true
|
|
|
+ loadError.value = ''
|
|
|
+ try {
|
|
|
+ const statsResponse = await getEquipmentStatusStatistics(WATER_DEVICE_ROOT_TYPE_ID)
|
|
|
+ statistics.value = normalizeDeviceStatistics(unwrapResponse(statsResponse) || {})
|
|
|
+
|
|
|
+ const listResponse = await listEquipment(buildAbnormalListQuery({
|
|
|
+ total: statistics.value.total || 1,
|
|
|
+ equipmentCode: query.value.equipmentCode,
|
|
|
+ equipmentName: query.value.equipmentName
|
|
|
+ }))
|
|
|
+ const page = normalizePage(listResponse)
|
|
|
+ rows.value = page.records
|
|
|
+ .map(normalizeAbnormalRow)
|
|
|
+ .filter(row => row.abnormalTypes.length > 0)
|
|
|
+ if (query.value.pageNum > Math.max(Math.ceil(filteredRows.value.length / query.value.pageSize), 1)) {
|
|
|
+ query.value.pageNum = 1
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ loadError.value = getRequestErrorMessage(error, '异常设备加载失败,请点击重试')
|
|
|
+ console.warn('[设备异常管理] 数据加载失败', error)
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function handleQuery() {
|
|
|
+ query.value.pageNum = 1
|
|
|
+ loadData()
|
|
|
+}
|
|
|
+
|
|
|
+function resetQuery() {
|
|
|
+ query.value = createQuery()
|
|
|
+ loadData()
|
|
|
+}
|
|
|
+
|
|
|
+async function openDetail(row) {
|
|
|
+ detailRow.value = { ...row }
|
|
|
+ detailImageUrl.value = row.equipmentImageUrl || ''
|
|
|
+ detailWarning.value = ''
|
|
|
+ detailVisible.value = true
|
|
|
+ await loadDetailEnrichment()
|
|
|
+}
|
|
|
+
|
|
|
+// 统一设备详情接口给出权威台账,供水 GIS 详情补关联未办结工单与最近上报
|
|
|
+async function loadDetailEnrichment() {
|
|
|
+ const equipmentId = detailRow.value?.equipmentId
|
|
|
+ if (!equipmentId) return
|
|
|
+ detailLoading.value = true
|
|
|
+ detailWarning.value = ''
|
|
|
+ try {
|
|
|
+ const detail = unwrapResponse(await getEquipmentDetail(equipmentId)) || {}
|
|
|
+ const info = detail.equipmentInfo || {}
|
|
|
+ const status = detail.statusInfo || {}
|
|
|
+ detailRow.value = normalizeAbnormalRow({
|
|
|
+ ...detailRow.value,
|
|
|
+ ...info,
|
|
|
+ equipmentImage: info.equipmentImage || detailRow.value?.equipmentImage,
|
|
|
+ currentStatus: status.currentStatus ?? detailRow.value?.currentStatus,
|
|
|
+ alarmStatus: status.alarmStatus ?? detailRow.value?.alarmStatus,
|
|
|
+ onlineStatus: status.onlineStatus ?? detailRow.value?.onlineStatus
|
|
|
+ })
|
|
|
+ detailImageUrl.value = detailRow.value.equipmentImageUrl || ''
|
|
|
+ } catch (error) {
|
|
|
+ detailWarning.value = '设备详情加载失败,已显示列表数据'
|
|
|
+ console.warn('[设备异常管理] 设备详情加载失败', error)
|
|
|
+ } finally {
|
|
|
+ detailLoading.value = false
|
|
|
+ }
|
|
|
+ await loadDetailWorkOrder(equipmentId)
|
|
|
+}
|
|
|
+
|
|
|
+async function loadDetailWorkOrder(equipmentId) {
|
|
|
+ try {
|
|
|
+ const enrichment = unwrapResponse(await getDeviceGisDetail(equipmentId)) || {}
|
|
|
+ detailRow.value = normalizeAbnormalRow({
|
|
|
+ ...detailRow.value,
|
|
|
+ abnormalReason: enrichment.abnormalReason || detailRow.value?.abnormalReason,
|
|
|
+ alertLevel: enrichment.alertLevel ?? detailRow.value?.alertLevel,
|
|
|
+ lastDataTime: enrichment.lastDataTime || detailRow.value?.lastDataTime,
|
|
|
+ relatedWorkOrderNo: enrichment.relatedWorkOrderNo,
|
|
|
+ relatedWorkOrderStatus: enrichment.relatedWorkOrderStatus,
|
|
|
+ relatedWorkOrderPerson: enrichment.relatedWorkOrderPerson
|
|
|
+ })
|
|
|
+ } catch (error) {
|
|
|
+ detailWarning.value = '关联工单加载失败,请点击重试'
|
|
|
+ console.warn('[设备异常管理] 关联工单加载失败', error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function exportData() {
|
|
|
+ try {
|
|
|
+ const csv = buildAbnormalCsv(filteredRows.value)
|
|
|
+ const blob = new Blob([csv], {type: 'text/csv;charset=utf-8'})
|
|
|
+ saveAs(blob, `设备异常清单_${new Date().toISOString().slice(0, 10)}.csv`)
|
|
|
+ ElMessage.success('导出完成')
|
|
|
+ } catch (error) {
|
|
|
+ console.warn('[设备异常管理] 导出失败', error)
|
|
|
+ ElMessage.error('导出失败,请稍后重试')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(loadData)
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss">
|
|
|
+ @use './waterSupplyPageTheme.scss';
|
|
|
+</style>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+ .water-page {
|
|
|
+ padding: 20px;
|
|
|
+ background: #f3f7fb;
|
|
|
+ min-height: calc(100vh - 84px)
|
|
|
+ }
|
|
|
+
|
|
|
+ .page-heading {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ gap: 16px;
|
|
|
+ margin-bottom: 16px
|
|
|
+ }
|
|
|
+
|
|
|
+ .page-title {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ min-width: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .page-title-icon {
|
|
|
+ display: grid;
|
|
|
+ place-items: center;
|
|
|
+ width: 42px;
|
|
|
+ height: 42px;
|
|
|
+ border-radius: 8px;
|
|
|
+ color: #fff;
|
|
|
+ background: #2563eb;
|
|
|
+ box-shadow: 0 8px 16px rgba(37, 99, 235, 0.2);
|
|
|
+ }
|
|
|
+
|
|
|
+ .page-heading h2 {
|
|
|
+ margin: 0 0 4px;
|
|
|
+ font-size: 20px;
|
|
|
+ color: #1e293b
|
|
|
+ }
|
|
|
+
|
|
|
+ .muted {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #64748b
|
|
|
+ }
|
|
|
+
|
|
|
+ .heading-refresh {
|
|
|
+ border-color: #bfdbfe;
|
|
|
+ color: #2563eb;
|
|
|
+ background: #fff;
|
|
|
+ }
|
|
|
+
|
|
|
+ .filter-card {
|
|
|
+ margin-bottom: 16px;
|
|
|
+ border: 1px solid #e2e8f0;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 6px 14px rgba(15, 23, 42, 0.04)
|
|
|
+ }
|
|
|
+
|
|
|
+ .filter-card :deep(.el-form-item) {
|
|
|
+ margin-top: 8px;
|
|
|
+ margin-right: 16px;
|
|
|
+ margin-bottom: 8px
|
|
|
+ }
|
|
|
+
|
|
|
+ .filter-card :deep(.el-form-item__label) {
|
|
|
+ color: #475569
|
|
|
+ }
|
|
|
+
|
|
|
+ .select--type {
|
|
|
+ width: 150px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .page-error {
|
|
|
+ margin-bottom: 16px
|
|
|
+ }
|
|
|
+
|
|
|
+ .metric-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
|
+ gap: 14px;
|
|
|
+ margin-bottom: 16px
|
|
|
+ }
|
|
|
+
|
|
|
+ .metric {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 14px;
|
|
|
+ min-width: 0;
|
|
|
+ padding: 16px 18px;
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid #e2e8f0;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 6px 14px rgba(15, 23, 42, 0.04)
|
|
|
+ }
|
|
|
+
|
|
|
+ .metric-icon {
|
|
|
+ position: static;
|
|
|
+ flex: 0 0 42px;
|
|
|
+ display: grid;
|
|
|
+ place-items: center;
|
|
|
+ width: 42px;
|
|
|
+ height: 42px;
|
|
|
+ border-radius: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .metric-icon .el-icon {
|
|
|
+ font-size: 22px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .metric-icon--blue {
|
|
|
+ color: #2563eb;
|
|
|
+ background: #e6f4ff;
|
|
|
+ }
|
|
|
+
|
|
|
+ .metric-icon--red {
|
|
|
+ color: #dc2626;
|
|
|
+ background: #fff1f2;
|
|
|
+ }
|
|
|
+
|
|
|
+ .metric-icon--orange {
|
|
|
+ color: #ea580c;
|
|
|
+ background: #fff7ed;
|
|
|
+ }
|
|
|
+
|
|
|
+ .metric-icon--slate {
|
|
|
+ color: #475569;
|
|
|
+ background: #f1f5f9;
|
|
|
+ }
|
|
|
+
|
|
|
+ .metric-body {
|
|
|
+ min-width: 0;
|
|
|
+ text-align: left;
|
|
|
+ }
|
|
|
+
|
|
|
+ .metric-body span {
|
|
|
+ display: block;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ margin-bottom: 3px;
|
|
|
+ color: #64748b;
|
|
|
+ font-size: 13px
|
|
|
+ }
|
|
|
+
|
|
|
+ .metric-value {
|
|
|
+ display: flex;
|
|
|
+ align-items: baseline;
|
|
|
+ gap: 5px;
|
|
|
+ min-width: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .metric-value strong {
|
|
|
+ font-size: 24px;
|
|
|
+ line-height: 1.2;
|
|
|
+ color: #1e293b;
|
|
|
+ }
|
|
|
+
|
|
|
+ .metric-value small {
|
|
|
+ color: #94a3b8
|
|
|
+ }
|
|
|
+
|
|
|
+ .danger {
|
|
|
+ color: #ef4444 !important
|
|
|
+ }
|
|
|
+
|
|
|
+ .table-card {
|
|
|
+ min-height: 420px;
|
|
|
+ border: 1px solid #e2e8f0;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 6px 14px rgba(15, 23, 42, 0.04)
|
|
|
+ }
|
|
|
+
|
|
|
+ .card-title {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ color: #1e293b;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+
|
|
|
+ .card-title .el-icon {
|
|
|
+ color: #2563eb;
|
|
|
+ font-size: 18px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .table-toolbar {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ }
|
|
|
+
|
|
|
+ .table-card :deep(.el-table) {
|
|
|
+ --el-table-border-color: #e2e8f0;
|
|
|
+ --el-table-header-bg-color: #f8fafc;
|
|
|
+ --el-table-header-text-color: #475569;
|
|
|
+ --el-table-row-hover-bg-color: #eff6ff;
|
|
|
+ }
|
|
|
+
|
|
|
+ .table-card :deep(.el-table__row) {
|
|
|
+ cursor: pointer;
|
|
|
+ }
|
|
|
+
|
|
|
+ .table-card :deep(.el-pagination) {
|
|
|
+ justify-content: flex-end;
|
|
|
+ margin-top: 16px
|
|
|
+ }
|
|
|
+
|
|
|
+ .table-thumb {
|
|
|
+ width: 46px;
|
|
|
+ height: 34px;
|
|
|
+ border: 1px solid #e2e8f0;
|
|
|
+ border-radius: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .reason-tag {
|
|
|
+ margin-right: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .reason-tags {
|
|
|
+ display: flex;
|
|
|
+ gap: 6px;
|
|
|
+ margin-top: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .detail-body {
|
|
|
+ min-height: 320px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .detail-header {
|
|
|
+ display: flex;
|
|
|
+ gap: 14px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .detail-media {
|
|
|
+ width: 104px;
|
|
|
+ height: 78px;
|
|
|
+ flex: 0 0 104px;
|
|
|
+ border: 1px solid #e2e8f0;
|
|
|
+ border-radius: 6px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .detail-media--empty {
|
|
|
+ display: grid;
|
|
|
+ place-items: center;
|
|
|
+ color: #94a3b8;
|
|
|
+ font-size: 12px;
|
|
|
+ background: #f8fafc;
|
|
|
+ }
|
|
|
+
|
|
|
+ .detail-header-text {
|
|
|
+ min-width: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .detail-name {
|
|
|
+ margin-bottom: 4px;
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #1e293b;
|
|
|
+ }
|
|
|
+
|
|
|
+ .detail-section-title {
|
|
|
+ margin: 18px 0 10px;
|
|
|
+ padding-left: 8px;
|
|
|
+ border-left: 3px solid #2563eb;
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #1e293b;
|
|
|
+ }
|
|
|
+
|
|
|
+ .detail-body :deep(.el-alert) {
|
|
|
+ margin-bottom: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ @media (max-width: 900px) {
|
|
|
+ .metric-grid {
|
|
|
+ grid-template-columns: repeat(2, minmax(0, 1fr))
|
|
|
+ }
|
|
|
+ }
|
|
|
+</style>
|