|
|
@@ -1,726 +1,785 @@
|
|
|
<template>
|
|
|
- <div class="prototype-container">
|
|
|
- <!-- 头部区域 -->
|
|
|
- <div class="app-header">
|
|
|
- <div class="title-section">
|
|
|
- <h1>
|
|
|
- <i class="fas fa-file-alt"></i> 应急预案在线审批机制
|
|
|
- </h1>
|
|
|
- <p>领导 · 专家评审修正 | 科学高效 · 快速响应</p>
|
|
|
+ <div class="lifeline-home" v-loading="loading">
|
|
|
+ <header class="home-header">
|
|
|
+ <div class="header-copy">
|
|
|
+ <span class="header-kicker">城市生命线运行态势</span>
|
|
|
+ <h1>预警联动管理首页</h1>
|
|
|
+ <p>汇集供水、排水、燃气与窨井预警,集中掌握风险和处置进度。</p>
|
|
|
</div>
|
|
|
- <div class="badge-area">
|
|
|
- <i class="fas fa-check-circle"></i> 先进性·科学性·高效性
|
|
|
+ <div class="header-tools">
|
|
|
+ <div class="date-block">
|
|
|
+ <strong>{{ currentDateLabel }}</strong>
|
|
|
+ <span>{{ currentTimeLabel }} · 数据更新 {{ lastUpdated || '未更新' }}</span>
|
|
|
+ </div>
|
|
|
+ <el-tooltip content="刷新首页数据" placement="bottom">
|
|
|
+ <el-button circle type="primary" :loading="loading" aria-label="刷新首页数据" @click="loadData">
|
|
|
+ <el-icon v-if="!loading"><Refresh /></el-icon>
|
|
|
+ </el-button>
|
|
|
+ </el-tooltip>
|
|
|
</div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="dashboard-grid">
|
|
|
- <!-- 左侧面板:审批流程设置 + 预案调整管理 -->
|
|
|
- <div class="left-panel">
|
|
|
- <!-- 1. 审批流程设置模块 -->
|
|
|
- <div class="section-card">
|
|
|
- <div class="section-header">
|
|
|
- <h2><i class="fas fa-diagram-project"></i> 审批流程设置</h2>
|
|
|
- <span class="tag">动态流转</span>
|
|
|
+ </header>
|
|
|
+
|
|
|
+ <el-alert
|
|
|
+ v-if="errorMessage"
|
|
|
+ :title="errorMessage"
|
|
|
+ :type="allRequestsFailed ? 'error' : 'warning'"
|
|
|
+ show-icon
|
|
|
+ closable
|
|
|
+ class="data-alert"
|
|
|
+ @close="errorMessage = ''"
|
|
|
+ >
|
|
|
+ <template #default>
|
|
|
+ <el-button link type="primary" @click="loadData">重新加载</el-button>
|
|
|
+ </template>
|
|
|
+ </el-alert>
|
|
|
+
|
|
|
+ <section class="metric-grid" aria-label="今日预警核心指标">
|
|
|
+ <button
|
|
|
+ v-for="metric in metricCards"
|
|
|
+ :key="metric.key"
|
|
|
+ type="button"
|
|
|
+ class="metric-card"
|
|
|
+ :style="{ '--tone': metric.color, '--tone-soft': metric.softColor }"
|
|
|
+ @click="navigate(metric.routeName)"
|
|
|
+ >
|
|
|
+ <span class="metric-icon"><el-icon><component :is="metric.icon" /></el-icon></span>
|
|
|
+ <span class="metric-content">
|
|
|
+ <span class="metric-label">{{ metric.label }}</span>
|
|
|
+ <strong>{{ formatCount(metric.value) }}</strong>
|
|
|
+ <small>{{ metric.note }}</small>
|
|
|
+ </span>
|
|
|
+ <el-icon class="metric-arrow"><ArrowRight /></el-icon>
|
|
|
+ </button>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <div class="analysis-grid">
|
|
|
+ <section class="panel trend-panel">
|
|
|
+ <div class="panel-header">
|
|
|
+ <div>
|
|
|
+ <h2>近 7 日预警趋势</h2>
|
|
|
+ <p>四类设施非草稿预警的每日变化</p>
|
|
|
</div>
|
|
|
- <div class="section-content">
|
|
|
- <div class="flow-steps">
|
|
|
- <div
|
|
|
- v-for="(step, idx) in flowSteps"
|
|
|
- :key="idx"
|
|
|
- class="flow-node"
|
|
|
- :class="{ 'active': step.status === 'active', 'pending': step.status === 'pending' }"
|
|
|
- >
|
|
|
- <div class="node-icon"><i :class="step.icon"></i></div>
|
|
|
- <div class="node-info">
|
|
|
- <div class="node-title">{{ step.title }}</div>
|
|
|
- <div class="node-desc">{{ step.desc }}</div>
|
|
|
- </div>
|
|
|
- <div class="node-badge">
|
|
|
- <i :class="step.status === 'active' ? 'fas fa-check-circle' : 'fas fa-clock'"></i>
|
|
|
- {{ step.statusText }}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div class="add-flow-btn">
|
|
|
- <i class="fas fa-plus-circle"></i> 自定义审批节点 / 会签设置
|
|
|
- </div>
|
|
|
- <div class="helper-text">
|
|
|
- <i class="fas fa-info-circle"></i> 支持串签、并签、转审,全审批流转记录留痕
|
|
|
- </div>
|
|
|
+ <el-button link type="primary" @click="navigate('WarningTrend')">
|
|
|
+ 趋势分析<el-icon><ArrowRight /></el-icon>
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ <div ref="trendChartRef" class="chart-box" />
|
|
|
+ <el-empty v-if="!trendRows.length && !loading" description="近 7 日暂无预警数据" :image-size="74" />
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section class="panel status-panel">
|
|
|
+ <div class="panel-header">
|
|
|
+ <div>
|
|
|
+ <h2>近 30 日处置状态</h2>
|
|
|
+ <p>各处置阶段预警数量分布</p>
|
|
|
</div>
|
|
|
+ <el-button link type="primary" @click="navigate('WarningDisposeStatus')">
|
|
|
+ 查看分析<el-icon><ArrowRight /></el-icon>
|
|
|
+ </el-button>
|
|
|
</div>
|
|
|
+ <div ref="statusChartRef" class="chart-box" />
|
|
|
+ <el-empty v-if="!statusRows.length && !loading" description="当前周期暂无状态数据" :image-size="74" />
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
|
|
|
- <!-- 3. 预案调整管理 -->
|
|
|
- <div class="section-card">
|
|
|
- <div class="section-header">
|
|
|
- <h2><i class="fas fa-sliders-h"></i> 预案调整管理</h2>
|
|
|
- <i class="fas fa-sync-alt" style="color:#6c86a3;"></i>
|
|
|
- </div>
|
|
|
- <div class="section-content">
|
|
|
- <div
|
|
|
- v-for="adjust in adjustments"
|
|
|
- :key="adjust.id"
|
|
|
- class="adjust-item"
|
|
|
- >
|
|
|
- <div class="adjust-title">
|
|
|
- <span><i class="fas fa-pen-nib"></i> {{ adjust.name }}</span>
|
|
|
- <span class="status-badge" :class="adjust.statusClass">{{ adjust.statusText }}</span>
|
|
|
- </div>
|
|
|
- <div class="adjust-detail">
|
|
|
- {{ adjust.detail }}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div class="add-flow-btn" style="background:#f1f5f9; margin-top:10px;">
|
|
|
- <i class="fas fa-edit"></i> 基于审批结果快速调整预案版本
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ <section class="panel facility-panel">
|
|
|
+ <div class="panel-header">
|
|
|
+ <div>
|
|
|
+ <h2>设施预警分布</h2>
|
|
|
+ <p>历史非草稿预警累计 {{ formatCount(facilityTotal) }} 条</p>
|
|
|
</div>
|
|
|
+ <el-button link type="primary" @click="navigate('WarningHistoryOverview')">
|
|
|
+ 历史预警概况<el-icon><ArrowRight /></el-icon>
|
|
|
+ </el-button>
|
|
|
</div>
|
|
|
-
|
|
|
- <!-- 右侧面板:预案在线审批 + 预案发布 -->
|
|
|
- <div class="right-panel">
|
|
|
- <!-- 2. 预案在线审批模块 -->
|
|
|
- <div class="section-card">
|
|
|
- <div class="section-header">
|
|
|
- <h2><i class="fas fa-check-double"></i> 预案在线审批</h2>
|
|
|
- <span class="tag">待办{{ pendingCount }}项</span>
|
|
|
+ <div class="facility-grid">
|
|
|
+ <article
|
|
|
+ v-for="facility in facilityStats"
|
|
|
+ :key="facility.value"
|
|
|
+ class="facility-card"
|
|
|
+ :style="{ '--facility-color': facility.color, '--facility-soft': facility.softColor }"
|
|
|
+ >
|
|
|
+ <div class="facility-title">
|
|
|
+ <span class="facility-mark">{{ facility.shortLabel }}</span>
|
|
|
+ <div><strong>{{ facility.label }}</strong><small>历史预警</small></div>
|
|
|
</div>
|
|
|
- <div class="section-content">
|
|
|
- <table class="data-table">
|
|
|
- <thead>
|
|
|
- <tr>
|
|
|
- <th>预案名称</th>
|
|
|
- <th>审批人员</th>
|
|
|
- <th>审批记录/意见</th>
|
|
|
- <th>状态</th>
|
|
|
- <th>操作</th>
|
|
|
- </tr>
|
|
|
- </thead>
|
|
|
- <tbody>
|
|
|
- <tr v-for="item in approvalList" :key="item.id">
|
|
|
- <td>
|
|
|
- <strong>{{ item.name }}</strong><br>
|
|
|
- <span class="version-text">{{ item.version }}</span>
|
|
|
- </td>
|
|
|
- <td>{{ item.approvers }}</td>
|
|
|
- <td class="comment-text">
|
|
|
- <i class="fas fa-comment-dots"></i> {{ item.comment }}
|
|
|
- </td>
|
|
|
- <td>
|
|
|
- <span class="status-badge" :class="item.statusClass">{{ item.statusText }}</span>
|
|
|
- </td>
|
|
|
- <td>
|
|
|
- <i class="fas fa-eye btn-icon"></i>
|
|
|
- <i class="fas fa-check-circle btn-icon" style="color:#2c7da0;"></i>
|
|
|
- </td>
|
|
|
- </tr>
|
|
|
- </tbody>
|
|
|
- </table>
|
|
|
- <div class="helper-text" style="text-align: right; margin-top: 12px;">
|
|
|
- <i class="fas fa-archive"></i> 完整审批日志可追溯 | 所有意见留痕
|
|
|
- </div>
|
|
|
+ <div class="facility-value">
|
|
|
+ <strong>{{ formatCount(facility.count) }}</strong>
|
|
|
+ <span>{{ formatPercent(facility.percentage) }}</span>
|
|
|
</div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 4. 预案发布模块 -->
|
|
|
- <div class="section-card">
|
|
|
- <div class="section-header">
|
|
|
- <h2><i class="fas fa-rocket"></i> 预案发布</h2>
|
|
|
- <i class="fas fa-globe-asia" style="color:#3083a2;"></i>
|
|
|
- </div>
|
|
|
- <div class="section-content">
|
|
|
- <div class="publish-card">
|
|
|
- <div class="publish-info">
|
|
|
- <h4><i class="fas fa-check-circle" style="color:#15803d;"></i> 已发布预案 (应急处置可用)</h4>
|
|
|
- <p style="font-size:0.8rem; margin-top: 8px;">
|
|
|
- {{ publishedPlans.map(p => p.name).join(' · ') || '暂无已发布预案' }}
|
|
|
- </p>
|
|
|
- </div>
|
|
|
- <div class="publish-btn disabled">仅展示已发布</div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div style="margin-top: 20px;">
|
|
|
- <div class="pending-publish-card">
|
|
|
- <div>
|
|
|
- <i class="fas fa-spinner fa-pulse" style="color:#e6b422;"></i>
|
|
|
- <strong>待发布预案(审批完毕)</strong>
|
|
|
- <br>
|
|
|
- <span class="pending-list">{{ pendingPublish.map(p => p.name).join(' | ') || '无' }}</span>
|
|
|
- </div>
|
|
|
- <span class="publish-action" @click="publishPlan">
|
|
|
- 一键发布 <i class="fas fa-arrow-right"></i>
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="helper-text" style="margin-top: 20px; background:#eef6fb; padding: 12px; border-radius: 18px;">
|
|
|
- <i class="fas fa-bolt"></i> <strong>快速响应保障:</strong> 已发布的预案将在应急指挥中心实时同步,支持一键启动应急预案、调集资源。
|
|
|
- </div>
|
|
|
+ <div class="facility-progress"><i :style="{ width: `${facility.percentage}%` }" /></div>
|
|
|
+ </article>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <div class="operations-grid">
|
|
|
+ <section class="panel warning-panel">
|
|
|
+ <div class="panel-header">
|
|
|
+ <div>
|
|
|
+ <h2>重点未处置预警</h2>
|
|
|
+ <p>按预警级别和等待时长优先展示,共 {{ formatCount(unprocessedTotal) }} 条</p>
|
|
|
</div>
|
|
|
+ <el-button link type="primary" @click="navigate('WarningTodayUnprocessed')">
|
|
|
+ 查看全部<el-icon><ArrowRight /></el-icon>
|
|
|
+ </el-button>
|
|
|
</div>
|
|
|
-
|
|
|
- <!-- 评审过程 & 结果记录 -->
|
|
|
- <div class="section-card" style="margin-bottom:0;">
|
|
|
- <div class="section-header">
|
|
|
- <h2><i class="fas fa-clipboard-list"></i> 评审过程 & 结果记录</h2>
|
|
|
- </div>
|
|
|
- <div class="section-content">
|
|
|
- <div style="display: flex; gap: 12px; flex-wrap: wrap;">
|
|
|
- <div class="record-card" v-for="record in reviewRecords" :key="record.id">
|
|
|
- <i class="fas fa-calendar-alt"></i> <strong>{{ record.title }}</strong>
|
|
|
- <div class="record-desc">{{ record.content }}</div>
|
|
|
+ <el-table
|
|
|
+ :data="unprocessedRows"
|
|
|
+ row-key="warningId"
|
|
|
+ class="warning-table"
|
|
|
+ empty-text="当前暂无未处置预警"
|
|
|
+ @row-click="openDetail"
|
|
|
+ >
|
|
|
+ <el-table-column label="级别" width="122">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag :type="warningLevelTag(row)" effect="light" size="small">
|
|
|
+ {{ warningLevelText(row) }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="预警事项" min-width="190" show-overflow-tooltip>
|
|
|
+ <template #default="{ row }">
|
|
|
+ <div class="warning-name">
|
|
|
+ <i :class="`level-${warningLevelCode(row)}`" />
|
|
|
+ <span>{{ listValue(row, 'warningName', 'warning_name') || '未命名预警' }}</span>
|
|
|
</div>
|
|
|
- </div>
|
|
|
- <div class="helper-text" style="margin-top: 12px;">
|
|
|
- <i class="fas fa-database"></i> 所有评审节点自动保存至历史版本库,支持追溯、对比、复盘。
|
|
|
- </div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="设施类型" width="110">
|
|
|
+ <template #default="{ row }">{{ warningTypeText(row) }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="权属单位" min-width="150" show-overflow-tooltip>
|
|
|
+ <template #default="{ row }">{{ listValue(row, 'ownershipUnit', 'ownership_unit') || '未设置' }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="等待时长" width="105">
|
|
|
+ <template #default="{ row }"><span class="waiting-time">{{ waitingText(row) }}</span></template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="状态" width="94" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag :type="warningStatusTag(row)" effect="plain" size="small">
|
|
|
+ {{ warningStatusText(row) }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="84" fixed="right" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-button link type="primary" @click.stop="openDetail(row)">详情</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <aside class="panel quick-panel">
|
|
|
+ <div class="panel-header">
|
|
|
+ <div>
|
|
|
+ <h2>常用功能</h2>
|
|
|
+ <p>快速进入预警查询与处置工作</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
+ <div class="quick-list">
|
|
|
+ <button
|
|
|
+ v-for="action in quickActions"
|
|
|
+ :key="action.routeName"
|
|
|
+ type="button"
|
|
|
+ class="quick-action"
|
|
|
+ @click="navigate(action.routeName)"
|
|
|
+ >
|
|
|
+ <span class="quick-icon" :style="{ color: action.color, background: action.softColor }">
|
|
|
+ <el-icon><component :is="action.icon" /></el-icon>
|
|
|
+ </span>
|
|
|
+ <span><strong>{{ action.label }}</strong><small>{{ action.note }}</small></span>
|
|
|
+ <el-icon class="quick-arrow"><ArrowRight /></el-icon>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </aside>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 底部信息 -->
|
|
|
- <div class="footer-bar">
|
|
|
- <div style="display: flex; gap: 24px;">
|
|
|
- <span><i class="fas fa-chart-line" style="color:#1e6f5c;"></i> 先进性 · 动态评审机制</span>
|
|
|
- <span><i class="fas fa-flask"></i> 科学性 · 专家逐条修正</span>
|
|
|
- <span><i class="fas fa-tachometer-alt"></i> 高效性 · 快速响应闭环</span>
|
|
|
- </div>
|
|
|
- <div style="font-size:0.7rem; color:#66809a;">
|
|
|
- <i class="far fa-clock"></i> 审批流转引擎就绪 · 提升应急处置敏捷度
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ <WarningDetailDrawer v-model="detailVisible" :row="selectedRow" />
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { reactive, computed } from 'vue'
|
|
|
-
|
|
|
-// 审批流程步骤配置
|
|
|
-const flowSteps = reactive([
|
|
|
- {
|
|
|
- title: '环节 1:预案起草人提交',
|
|
|
- desc: '审批人: 安全总监 · 权限: 初审/退回',
|
|
|
- icon: 'fas fa-user-tie',
|
|
|
- status: 'active',
|
|
|
- statusText: '已激活'
|
|
|
- },
|
|
|
- {
|
|
|
- title: '环节 2:专家技术评审',
|
|
|
- desc: '审批人: 专家组(3人) · 权限: 修正建议/通过',
|
|
|
- icon: 'fas fa-chalkboard-user',
|
|
|
- status: 'active',
|
|
|
- statusText: '进行中'
|
|
|
- },
|
|
|
- {
|
|
|
- title: '环节 3:领导层终审',
|
|
|
- desc: '审批人: 应急总指挥 · 权限: 批准/驳回',
|
|
|
- icon: 'fas fa-landmark',
|
|
|
- status: 'pending',
|
|
|
- statusText: '待触发'
|
|
|
- },
|
|
|
- {
|
|
|
- title: '环节 4:签发与归档',
|
|
|
- desc: '办公室自动化发布 · 记录存档',
|
|
|
- icon: 'fas fa-cloud-upload-alt',
|
|
|
- status: 'pending',
|
|
|
- statusText: '流程终点'
|
|
|
- }
|
|
|
-])
|
|
|
+import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
|
|
|
+import { useRouter } from 'vue-router'
|
|
|
+import { ElMessage } from 'element-plus'
|
|
|
+import {
|
|
|
+ ArrowRight,
|
|
|
+ Bell,
|
|
|
+ CircleCheck,
|
|
|
+ Clock,
|
|
|
+ DataAnalysis,
|
|
|
+ Refresh,
|
|
|
+ Setting,
|
|
|
+ TrendCharts,
|
|
|
+ Warning
|
|
|
+} from '@element-plus/icons-vue'
|
|
|
+import * as echarts from 'echarts'
|
|
|
+import {
|
|
|
+ getWarningTodayOverview,
|
|
|
+ pageWarningHistory,
|
|
|
+ pageWarningTodayUnprocessed
|
|
|
+} from '@/api/warning/dashboard'
|
|
|
+import { getDisposeStatusStat, getWarningTrendByDay } from '@/api/warning/statistics'
|
|
|
+import WarningDetailDrawer from '../lOverviewWarning/WarningDetailDrawer.vue'
|
|
|
+import {
|
|
|
+ dashboardData,
|
|
|
+ listValue,
|
|
|
+ pageRecords,
|
|
|
+ pageTotal,
|
|
|
+ warningLevelCode,
|
|
|
+ warningLevelTag,
|
|
|
+ warningLevelText,
|
|
|
+ warningStatusTag,
|
|
|
+ warningStatusText,
|
|
|
+ warningTypeText
|
|
|
+} from '../lOverviewWarning/dashboardShared'
|
|
|
+import { labelOf, responseData, statusLabels } from '../lstatistics/statisticsShared'
|
|
|
+
|
|
|
+const router = useRouter()
|
|
|
+const loading = ref(false)
|
|
|
+const errorMessage = ref('')
|
|
|
+const allRequestsFailed = ref(false)
|
|
|
+const lastUpdated = ref('')
|
|
|
+const now = ref(new Date())
|
|
|
+const todayOverview = ref({})
|
|
|
+const unprocessedRows = ref([])
|
|
|
+const unprocessedTotal = ref(null)
|
|
|
+const trendRows = ref([])
|
|
|
+const statusRows = ref([])
|
|
|
+const detailVisible = ref(false)
|
|
|
+const selectedRow = ref(null)
|
|
|
+const trendChartRef = ref()
|
|
|
+const statusChartRef = ref()
|
|
|
+let trendChart
|
|
|
+let statusChart
|
|
|
+let clockTimer
|
|
|
+let resizeObserver
|
|
|
+
|
|
|
+const facilityDefinitions = [
|
|
|
+ { value: 'WATER_PIPE', label: '供水管网', shortLabel: '供', color: '#2563eb', softColor: '#eff6ff' },
|
|
|
+ { value: 'SEWER_PIPE', label: '排水管网', shortLabel: '排', color: '#0f766e', softColor: '#ecfdf5' },
|
|
|
+ { value: 'GAS_PIPE', label: '燃气管网', shortLabel: '燃', color: '#d97706', softColor: '#fffbeb' },
|
|
|
+ { value: 'MANHOLE', label: '窨井', shortLabel: '井', color: '#64748b', softColor: '#f1f5f9' }
|
|
|
+]
|
|
|
+
|
|
|
+const facilityCounts = ref(Object.fromEntries(facilityDefinitions.map(item => [item.value, null])))
|
|
|
+
|
|
|
+const facilityTotal = computed(() => {
|
|
|
+ const values = Object.values(facilityCounts.value).filter(value => Number.isFinite(value))
|
|
|
+ return values.length ? values.reduce((total, value) => total + value, 0) : null
|
|
|
+})
|
|
|
|
|
|
-// 预案调整数据
|
|
|
-const adjustments = reactive([
|
|
|
- {
|
|
|
- id: 1,
|
|
|
- name: '洪涝灾害应急预案 V2.1',
|
|
|
- statusText: '基于专家意见调整',
|
|
|
- statusClass: 'secondary',
|
|
|
- detail: '调整内容: 根据刘宏专家评审意见,优化预警阈值及撤离路线;增加物资调度流程。评审结果: “科学性提升,建议补充应急通讯保障” — 已修正。'
|
|
|
- },
|
|
|
- {
|
|
|
- id: 2,
|
|
|
- name: '危化品泄漏专项预案',
|
|
|
- statusText: '修改中',
|
|
|
- statusClass: 'warning',
|
|
|
- detail: '根据领导层反馈: 需强化现场指挥架构,新增“紧急疏散图”附件,当前版本迭代至3.0。'
|
|
|
- },
|
|
|
- {
|
|
|
- id: 3,
|
|
|
- name: '地震应急响应预案',
|
|
|
- statusText: '审批通过待调整细节',
|
|
|
- statusClass: '',
|
|
|
- detail: '最终评审批示: 修正响应分级条件,将震级启动标准细化,已按专家组合议完成修订。'
|
|
|
- }
|
|
|
-])
|
|
|
+const facilityStats = computed(() => facilityDefinitions.map(item => {
|
|
|
+ const count = facilityCounts.value[item.value]
|
|
|
+ const percentage = Number.isFinite(count) && facilityTotal.value > 0
|
|
|
+ ? Number(((count / facilityTotal.value) * 100).toFixed(1))
|
|
|
+ : 0
|
|
|
+ return { ...item, count, percentage }
|
|
|
+}))
|
|
|
|
|
|
-// 审批列表数据
|
|
|
-const approvalList = reactive([
|
|
|
+const metricCards = computed(() => [
|
|
|
{
|
|
|
- id: 1,
|
|
|
- name: '洪涝灾害应急预案',
|
|
|
- version: 'v2.2',
|
|
|
- approvers: '专家组: 王宏,李敏,张启\n领导: 陈卫国',
|
|
|
- comment: '专家李敏: 响应等级描述建议更精确,已采纳。领导陈卫国: 同意专家组意见,补充响应时效。',
|
|
|
- statusText: '终审中',
|
|
|
- statusClass: ''
|
|
|
+ key: 'today', label: '今日预警', value: numericValue(todayOverview.value.todayTotal),
|
|
|
+ note: '今日非草稿预警', routeName: 'WarningTodayOverview', icon: Bell,
|
|
|
+ color: '#2563eb', softColor: '#eff6ff'
|
|
|
},
|
|
|
{
|
|
|
- id: 2,
|
|
|
- name: '危化品泄漏专项预案',
|
|
|
- version: 'v3.1',
|
|
|
- approvers: '安全总监: 赵岩\n外部专家: 刘景明',
|
|
|
- comment: '审批记录: 安全总监初审通过, 专家提出“新增化学洗消流程”修正要求,预案调整中。',
|
|
|
- statusText: '待修正',
|
|
|
- statusClass: 'warning'
|
|
|
+ key: 'unresolved', label: '当前未解除', value: numericValue(todayOverview.value.unresolvedCount),
|
|
|
+ note: '截至当前仍在跟进', routeName: 'WarningTodayOverview', icon: Warning,
|
|
|
+ color: '#dc2626', softColor: '#fef2f2'
|
|
|
},
|
|
|
{
|
|
|
- id: 3,
|
|
|
- name: '公共卫生事件应急预案',
|
|
|
- version: 'v1.5',
|
|
|
- approvers: '应急办: 周敏, 疾控专家: 何秀英',
|
|
|
- comment: '审批意见: 何秀英专家评审通过;领导复核要求更新隔离标准,修正后重新提交审批。',
|
|
|
- statusText: '二次评审',
|
|
|
- statusClass: 'secondary'
|
|
|
+ key: 'resolved', label: '今日已解除', value: numericValue(todayOverview.value.resolvedCount),
|
|
|
+ note: '今日完成风险解除', routeName: 'WarningTodayOverview', icon: CircleCheck,
|
|
|
+ color: '#059669', softColor: '#ecfdf5'
|
|
|
},
|
|
|
{
|
|
|
- id: 4,
|
|
|
- name: '大面积停电应急预案',
|
|
|
- version: 'v2.0',
|
|
|
- approvers: '专家组: 电力工程师+总指挥',
|
|
|
- comment: '审批记录: 全票通过先进性评估,目前进入发布前最终确认环节。',
|
|
|
- statusText: '审批通过',
|
|
|
- statusClass: ''
|
|
|
+ key: 'unprocessed', label: '未处置预警', value: unprocessedTotal.value,
|
|
|
+ note: '需要重点督办', routeName: 'WarningTodayUnprocessed', icon: Clock,
|
|
|
+ color: '#d97706', softColor: '#fffbeb'
|
|
|
}
|
|
|
])
|
|
|
|
|
|
-// 已发布的预案
|
|
|
-const publishedPlans = reactive([
|
|
|
- { id: 1, name: '地震应急响应预案 (2025-01-发布)' }
|
|
|
-])
|
|
|
+const quickActions = [
|
|
|
+ { label: '今日预警概况', note: '查看今日警情明细', routeName: 'WarningTodayOverview', icon: Bell, color: '#2563eb', softColor: '#eff6ff' },
|
|
|
+ { label: '今日未处置预警', note: '跟进待处理警情', routeName: 'WarningTodayUnprocessed', icon: Warning, color: '#dc2626', softColor: '#fef2f2' },
|
|
|
+ { label: '历史预警概况', note: '查看设施预警分布', routeName: 'WarningHistoryOverview', icon: DataAnalysis, color: '#0f766e', softColor: '#ecfdf5' },
|
|
|
+ { label: '处置状态分析', note: '分析处置进度', routeName: 'WarningDisposeStatus', icon: TrendCharts, color: '#7c3aed', softColor: '#f5f3ff' },
|
|
|
+ { label: '预警事项管理', note: '维护预警清单配置', routeName: 'WarningItemManage', icon: Setting, color: '#475569', softColor: '#f1f5f9' }
|
|
|
+]
|
|
|
|
|
|
-// 待发布预案(审批完毕但尚未发布)
|
|
|
-const pendingPublish = reactive([
|
|
|
- { id: 4, name: '大面积停电应急预案' },
|
|
|
- { id: 3, name: '公共卫生事件应急预案' }
|
|
|
-])
|
|
|
+const currentDateLabel = computed(() => new Intl.DateTimeFormat('zh-CN', {
|
|
|
+ year: 'numeric', month: 'long', day: 'numeric', weekday: 'long'
|
|
|
+}).format(now.value))
|
|
|
|
|
|
-// 评审记录
|
|
|
-const reviewRecords = reactive([
|
|
|
- {
|
|
|
- id: 1,
|
|
|
- title: '2025-04-10 专家评审会',
|
|
|
- content: '评审意见:预案结构科学,响应衔接效率高,建议增加辅助决策地图。→ 记录已归档,预案已完成修正。'
|
|
|
- },
|
|
|
- {
|
|
|
- id: 2,
|
|
|
- title: '领导终审记录',
|
|
|
- content: '最终结果:同意发布,强调实战演练与预案结合。结果存入档案,提升先进性指标。'
|
|
|
- }
|
|
|
-])
|
|
|
-
|
|
|
-// 计算待办数量(状态为终审中/待修正/二次评审的数量)
|
|
|
-const pendingCount = computed(() => {
|
|
|
- return approvalList.filter(item =>
|
|
|
- item.statusText === '终审中' || item.statusText === '待修正' || item.statusText === '二次评审'
|
|
|
- ).length
|
|
|
-})
|
|
|
-
|
|
|
-// 发布预案功能:将待发布预案移至已发布
|
|
|
-const publishPlan = () => {
|
|
|
- if (pendingPublish.length === 0) {
|
|
|
- alert('当前没有待发布的预案')
|
|
|
- return
|
|
|
- }
|
|
|
- // 将待发布预案添加到已发布列表
|
|
|
- pendingPublish.forEach(plan => {
|
|
|
- if (!publishedPlans.some(p => p.name === plan.name)) {
|
|
|
- publishedPlans.push({ id: plan.id, name: `${plan.name} (刚刚发布)` })
|
|
|
- }
|
|
|
- })
|
|
|
- // 清空待发布列表
|
|
|
- pendingPublish.splice(0, pendingPublish.length)
|
|
|
-
|
|
|
- // 同时更新审批列表中的对应预案状态为“已发布”
|
|
|
- approvalList.forEach(item => {
|
|
|
- if (item.name === '大面积停电应急预案' || item.name === '公共卫生事件应急预案') {
|
|
|
- item.statusText = '已发布'
|
|
|
- item.statusClass = ''
|
|
|
- }
|
|
|
- })
|
|
|
+const currentTimeLabel = computed(() => new Intl.DateTimeFormat('zh-CN', {
|
|
|
+ hour: '2-digit', minute: '2-digit', hour12: false
|
|
|
+}).format(now.value))
|
|
|
|
|
|
- // 可选:提示发布成功
|
|
|
- alert('预案已成功发布,应急处置中可立即启用!')
|
|
|
+function numericValue(value) {
|
|
|
+ if (value === null || value === undefined || value === '') return null
|
|
|
+ const number = Number(value)
|
|
|
+ return Number.isFinite(number) ? number : null
|
|
|
}
|
|
|
-</script>
|
|
|
|
|
|
-<style scoped>
|
|
|
-* {
|
|
|
- margin: 0;
|
|
|
- padding: 0;
|
|
|
- box-sizing: border-box;
|
|
|
+function formatCount(value) {
|
|
|
+ return Number.isFinite(value) ? new Intl.NumberFormat('zh-CN').format(value) : '-'
|
|
|
}
|
|
|
|
|
|
-.prototype-container {
|
|
|
- max-width: 1440px;
|
|
|
- margin: 0 auto;
|
|
|
- background: #ffffff;
|
|
|
- border-radius: 32px;
|
|
|
- box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.15);
|
|
|
- overflow: hidden;
|
|
|
- font-family: 'Inter', 'Nunito', system-ui, -apple-system, sans-serif;
|
|
|
+function formatPercent(value) {
|
|
|
+ return facilityTotal.value > 0 ? `${Number(value || 0).toFixed(1)}%` : '-'
|
|
|
}
|
|
|
|
|
|
-.app-header {
|
|
|
- background: linear-gradient(135deg, #0f2b3d 0%, #1b4a6e 100%);
|
|
|
- padding: 24px 32px;
|
|
|
- color: white;
|
|
|
- display: flex;
|
|
|
- justify-content: space-between;
|
|
|
- align-items: center;
|
|
|
- flex-wrap: wrap;
|
|
|
- gap: 16px;
|
|
|
+function pad(value) {
|
|
|
+ return String(value).padStart(2, '0')
|
|
|
}
|
|
|
|
|
|
-.title-section h1 {
|
|
|
- font-size: 1.8rem;
|
|
|
- font-weight: 700;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 12px;
|
|
|
+function formatApiDate(date) {
|
|
|
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
|
|
}
|
|
|
|
|
|
-.title-section h1 i {
|
|
|
- font-size: 1.8rem;
|
|
|
- color: #ffd966;
|
|
|
+function rangeForDays(days) {
|
|
|
+ const end = new Date()
|
|
|
+ const start = new Date(end)
|
|
|
+ start.setHours(0, 0, 0, 0)
|
|
|
+ start.setDate(start.getDate() - days + 1)
|
|
|
+ return { startTime: formatApiDate(start), endTime: formatApiDate(end) }
|
|
|
}
|
|
|
|
|
|
-.title-section p {
|
|
|
- font-size: 0.85rem;
|
|
|
- opacity: 0.85;
|
|
|
- margin-top: 6px;
|
|
|
+function recentDateKeys(days = 7) {
|
|
|
+ const result = []
|
|
|
+ const cursor = new Date()
|
|
|
+ cursor.setHours(0, 0, 0, 0)
|
|
|
+ cursor.setDate(cursor.getDate() - days + 1)
|
|
|
+ for (let index = 0; index < days; index += 1) {
|
|
|
+ result.push(`${cursor.getFullYear()}-${pad(cursor.getMonth() + 1)}-${pad(cursor.getDate())}`)
|
|
|
+ cursor.setDate(cursor.getDate() + 1)
|
|
|
+ }
|
|
|
+ return result
|
|
|
}
|
|
|
|
|
|
-.badge-area {
|
|
|
- background: rgba(255, 255, 240, 0.15);
|
|
|
- backdrop-filter: blur(4px);
|
|
|
- padding: 8px 20px;
|
|
|
- border-radius: 40px;
|
|
|
- font-size: 0.9rem;
|
|
|
- font-weight: 500;
|
|
|
+function warningCount(row) {
|
|
|
+ return Number(row.warning_count ?? row.warningCount ?? row.status_count ?? row.statusCount ?? 0)
|
|
|
}
|
|
|
|
|
|
-.dashboard-grid {
|
|
|
- display: grid;
|
|
|
- grid-template-columns: 1fr 1.4fr;
|
|
|
- gap: 0;
|
|
|
- border-top: 1px solid #e9edf2;
|
|
|
+function renderTrendChart() {
|
|
|
+ if (!trendChartRef.value) return
|
|
|
+ trendChart ||= echarts.init(trendChartRef.value)
|
|
|
+ if (!trendRows.value.length) {
|
|
|
+ trendChart.clear()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const dates = recentDateKeys()
|
|
|
+ const totals = Object.fromEntries(dates.map(date => [date, 0]))
|
|
|
+ trendRows.value.forEach(row => {
|
|
|
+ const date = String(row.stat_date ?? row.statDate ?? '').slice(0, 10)
|
|
|
+ if (date in totals) totals[date] += warningCount(row)
|
|
|
+ })
|
|
|
+ trendChart.setOption({
|
|
|
+ animationDuration: 450,
|
|
|
+ color: ['#2563eb'],
|
|
|
+ tooltip: { trigger: 'axis', valueFormatter: value => `${value} 条` },
|
|
|
+ grid: { left: 48, right: 22, top: 24, bottom: 38 },
|
|
|
+ xAxis: {
|
|
|
+ type: 'category', boundaryGap: false, data: dates.map(date => date.slice(5)),
|
|
|
+ axisLine: { lineStyle: { color: '#d9e1ea' } }, axisLabel: { color: '#64748b' }
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ type: 'value', minInterval: 1, axisLabel: { color: '#64748b' },
|
|
|
+ splitLine: { lineStyle: { color: '#edf1f5' } }
|
|
|
+ },
|
|
|
+ series: [{
|
|
|
+ name: '预警数量', type: 'line', smooth: true, symbol: 'circle', symbolSize: 7,
|
|
|
+ lineStyle: { width: 3 }, itemStyle: { borderColor: '#ffffff', borderWidth: 2 },
|
|
|
+ areaStyle: { color: 'rgba(37, 99, 235, 0.10)' }, data: dates.map(date => totals[date])
|
|
|
+ }]
|
|
|
+ }, true)
|
|
|
+}
|
|
|
+
|
|
|
+function renderStatusChart() {
|
|
|
+ if (!statusChartRef.value) return
|
|
|
+ statusChart ||= echarts.init(statusChartRef.value)
|
|
|
+ const data = statusRows.value.map(row => ({
|
|
|
+ name: labelOf(statusLabels, row.status), value: warningCount(row), status: String(row.status || '').toUpperCase()
|
|
|
+ })).filter(item => item.value > 0)
|
|
|
+ if (!data.length) {
|
|
|
+ statusChart.clear()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const colorByStatus = {
|
|
|
+ PENDING: '#dc2626', PROCESSING: '#d97706', RELEASED: '#2563eb',
|
|
|
+ HANDLED: '#0f766e', CLOSED: '#059669', RESOLVED: '#059669'
|
|
|
+ }
|
|
|
+ statusChart.setOption({
|
|
|
+ tooltip: { trigger: 'item', formatter: '{b}<br/>{c} 条({d}%)' },
|
|
|
+ legend: { type: 'scroll', bottom: 0, left: 'center', textStyle: { color: '#64748b' } },
|
|
|
+ series: [{
|
|
|
+ type: 'pie', radius: ['46%', '68%'], center: ['50%', '43%'], avoidLabelOverlap: true,
|
|
|
+ itemStyle: { borderColor: '#ffffff', borderWidth: 3 }, label: { show: false },
|
|
|
+ emphasis: { label: { show: true, fontSize: 14, fontWeight: 600 } },
|
|
|
+ data: data.map(item => ({
|
|
|
+ name: item.name, value: item.value,
|
|
|
+ itemStyle: { color: colorByStatus[item.status] || '#64748b' }
|
|
|
+ }))
|
|
|
+ }]
|
|
|
+ }, true)
|
|
|
+}
|
|
|
+
|
|
|
+function waitingText(row) {
|
|
|
+ const hours = numericValue(listValue(row, 'waitingHours', 'waiting_hours'))
|
|
|
+ if (hours === null) return '-'
|
|
|
+ if (hours < 1) return '< 1 小时'
|
|
|
+ if (hours < 24) return `${hours.toFixed(hours >= 10 ? 0 : 1)} 小时`
|
|
|
+ return `${Math.floor(hours / 24)} 天 ${Math.round(hours % 24)} 小时`
|
|
|
+}
|
|
|
+
|
|
|
+function openDetail(row) {
|
|
|
+ selectedRow.value = row
|
|
|
+ detailVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+function navigate(routeName) {
|
|
|
+ const routeCandidates = router.getRoutes().filter(route => route.name === routeName)
|
|
|
+ if (!routeCandidates.length) {
|
|
|
+ ElMessage.warning('当前账号暂无该功能菜单权限')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ // Legacy databases can temporarily expose the same route name at root level
|
|
|
+ // and under the subsystem. Prefer the scoped route when both are available.
|
|
|
+ const preferredRoute = routeCandidates.sort((left, right) => {
|
|
|
+ const leftScoped = left.path.startsWith('/subSystem/') ? 1 : 0
|
|
|
+ const rightScoped = right.path.startsWith('/subSystem/') ? 1 : 0
|
|
|
+ return rightScoped - leftScoped || right.path.length - left.path.length
|
|
|
+ })[0]
|
|
|
+ router.push(preferredRoute.path).catch(() => {})
|
|
|
+}
|
|
|
+
|
|
|
+async function loadData() {
|
|
|
+ if (loading.value) return
|
|
|
+ loading.value = true
|
|
|
+ errorMessage.value = ''
|
|
|
+ const requests = [
|
|
|
+ { key: 'today', label: '今日概况', promise: getWarningTodayOverview() },
|
|
|
+ { key: 'unprocessed', label: '未处置清单', promise: pageWarningTodayUnprocessed({ pageNum: 1, pageSize: 6 }) },
|
|
|
+ { key: 'trend', label: '预警趋势', promise: getWarningTrendByDay(rangeForDays(7)) },
|
|
|
+ { key: 'status', label: '处置状态', promise: getDisposeStatusStat(rangeForDays(30)) },
|
|
|
+ ...facilityDefinitions.map(item => ({
|
|
|
+ key: item.value, label: item.label,
|
|
|
+ promise: pageWarningHistory({ pageNum: 1, pageSize: 1, warningType: item.value })
|
|
|
+ }))
|
|
|
+ ]
|
|
|
+
|
|
|
+ try {
|
|
|
+ const results = await Promise.allSettled(requests.map(item => item.promise))
|
|
|
+ const failedLabels = []
|
|
|
+ let successCount = 0
|
|
|
+ let generatedAt = ''
|
|
|
+
|
|
|
+ results.forEach((result, index) => {
|
|
|
+ const request = requests[index]
|
|
|
+ if (result.status === 'rejected') {
|
|
|
+ failedLabels.push(request.label)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ successCount += 1
|
|
|
+ if (request.key === 'today') {
|
|
|
+ todayOverview.value = dashboardData(result.value, {})
|
|
|
+ generatedAt = todayOverview.value.generatedAt || generatedAt
|
|
|
+ } else if (request.key === 'unprocessed') {
|
|
|
+ const data = dashboardData(result.value, {})
|
|
|
+ unprocessedRows.value = pageRecords(result.value)
|
|
|
+ unprocessedTotal.value = pageTotal(result.value)
|
|
|
+ generatedAt = data.generatedAt || generatedAt
|
|
|
+ } else if (request.key === 'trend') {
|
|
|
+ trendRows.value = responseData(result.value, [])
|
|
|
+ } else if (request.key === 'status') {
|
|
|
+ statusRows.value = responseData(result.value, [])
|
|
|
+ } else {
|
|
|
+ facilityCounts.value = { ...facilityCounts.value, [request.key]: pageTotal(result.value) }
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ allRequestsFailed.value = successCount === 0
|
|
|
+ if (successCount > 0) lastUpdated.value = generatedAt || formatApiDate(new Date())
|
|
|
+ if (failedLabels.length) errorMessage.value = `${failedLabels.join('、')}加载失败,其他可用数据已正常展示。`
|
|
|
+ await nextTick()
|
|
|
+ renderTrendChart()
|
|
|
+ renderStatusChart()
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-.left-panel {
|
|
|
- background: #fafcff;
|
|
|
- border-right: 1px solid #eef2f6;
|
|
|
- padding: 28px 24px;
|
|
|
+function resizeCharts() {
|
|
|
+ trendChart?.resize()
|
|
|
+ statusChart?.resize()
|
|
|
}
|
|
|
|
|
|
-.right-panel {
|
|
|
- background: #ffffff;
|
|
|
- padding: 28px 24px;
|
|
|
-}
|
|
|
+onMounted(() => {
|
|
|
+ clockTimer = window.setInterval(() => { now.value = new Date() }, 30000)
|
|
|
+ resizeObserver = new ResizeObserver(resizeCharts)
|
|
|
+ if (trendChartRef.value) resizeObserver.observe(trendChartRef.value)
|
|
|
+ if (statusChartRef.value) resizeObserver.observe(statusChartRef.value)
|
|
|
+ loadData()
|
|
|
+})
|
|
|
|
|
|
-.section-card {
|
|
|
- background: white;
|
|
|
- border-radius: 24px;
|
|
|
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05), 0 0 0 1px rgba(0, 0, 0, 0.02);
|
|
|
- margin-bottom: 32px;
|
|
|
- overflow: hidden;
|
|
|
-}
|
|
|
+onUnmounted(() => {
|
|
|
+ window.clearInterval(clockTimer)
|
|
|
+ resizeObserver?.disconnect()
|
|
|
+ trendChart?.dispose()
|
|
|
+ statusChart?.dispose()
|
|
|
+})
|
|
|
+</script>
|
|
|
|
|
|
-.section-header {
|
|
|
- padding: 18px 24px 12px 24px;
|
|
|
- border-bottom: 2px solid #f1f5f9;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: space-between;
|
|
|
- flex-wrap: wrap;
|
|
|
+<style scoped lang="scss">
|
|
|
+.lifeline-home {
|
|
|
+ min-height: calc(100vh - 84px);
|
|
|
+ padding: 20px 22px 28px;
|
|
|
+ background: #f3f6fa;
|
|
|
+ color: #1f2937;
|
|
|
}
|
|
|
|
|
|
-.section-header h2 {
|
|
|
- font-size: 1.35rem;
|
|
|
- font-weight: 600;
|
|
|
+.home-header {
|
|
|
display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 10px;
|
|
|
- color: #0f2b3d;
|
|
|
-}
|
|
|
-
|
|
|
-.tag {
|
|
|
- font-size: 12px;
|
|
|
- background: #eef2ff;
|
|
|
- padding: 4px 12px;
|
|
|
- border-radius: 30px;
|
|
|
+ align-items: flex-end;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 24px;
|
|
|
+ padding: 2px 2px 18px;
|
|
|
+ border-bottom: 1px solid #dfe6ee;
|
|
|
}
|
|
|
|
|
|
-.section-content {
|
|
|
- padding: 20px 24px 24px 24px;
|
|
|
-}
|
|
|
+.header-kicker { color: #2563eb; font-size: 12px; font-weight: 700; }
|
|
|
+.header-copy h1 { margin: 6px 0 5px; font-size: 26px; line-height: 1.25; }
|
|
|
+.header-copy p, .panel-header p { margin: 0; color: #718096; }
|
|
|
+.header-tools { display: flex; align-items: center; gap: 14px; }
|
|
|
|
|
|
-.flow-steps {
|
|
|
+.date-block {
|
|
|
display: flex;
|
|
|
+ min-width: 245px;
|
|
|
flex-direction: column;
|
|
|
- gap: 16px;
|
|
|
+ align-items: flex-end;
|
|
|
+ gap: 5px;
|
|
|
}
|
|
|
|
|
|
-.flow-node {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 16px;
|
|
|
- background: #f8fafc;
|
|
|
- padding: 14px 18px;
|
|
|
- border-radius: 20px;
|
|
|
- border-left: 5px solid #cbd5e1;
|
|
|
- transition: all 0.2s;
|
|
|
-}
|
|
|
+.date-block strong { font-size: 14px; }
|
|
|
+.date-block span { color: #8492a6; font-size: 12px; }
|
|
|
+.data-alert { margin-top: 16px; }
|
|
|
|
|
|
-.flow-node.active {
|
|
|
- border-left-color: #2c7da0;
|
|
|
- background: #f1f9fe;
|
|
|
+.metric-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
|
+ gap: 14px;
|
|
|
+ margin: 18px 0 16px;
|
|
|
}
|
|
|
|
|
|
-.node-icon {
|
|
|
- width: 44px;
|
|
|
- height: 44px;
|
|
|
- background: #eef2ff;
|
|
|
- border-radius: 30px;
|
|
|
+.metric-card {
|
|
|
+ position: relative;
|
|
|
display: flex;
|
|
|
+ min-width: 0;
|
|
|
+ min-height: 122px;
|
|
|
align-items: center;
|
|
|
- justify-content: center;
|
|
|
- font-size: 1.4rem;
|
|
|
- color: #1e4a6b;
|
|
|
-}
|
|
|
-
|
|
|
-.node-info {
|
|
|
- flex: 1;
|
|
|
-}
|
|
|
-
|
|
|
-.node-title {
|
|
|
- font-weight: 700;
|
|
|
- font-size: 1rem;
|
|
|
-}
|
|
|
-
|
|
|
-.node-desc {
|
|
|
- font-size: 0.8rem;
|
|
|
- color: #475569;
|
|
|
- margin-top: 4px;
|
|
|
-}
|
|
|
-
|
|
|
-.node-badge {
|
|
|
- background: #e2e8f0;
|
|
|
- padding: 4px 12px;
|
|
|
- border-radius: 50px;
|
|
|
- font-size: 0.75rem;
|
|
|
- font-weight: 600;
|
|
|
- color: #1e293b;
|
|
|
-}
|
|
|
-
|
|
|
-.add-flow-btn {
|
|
|
- margin-top: 12px;
|
|
|
- background: white;
|
|
|
- border: 1px dashed #94a3b8;
|
|
|
- border-radius: 50px;
|
|
|
- padding: 10px;
|
|
|
- text-align: center;
|
|
|
- color: #2c7da0;
|
|
|
- font-weight: 500;
|
|
|
- font-size: 0.85rem;
|
|
|
- cursor: default;
|
|
|
-}
|
|
|
-
|
|
|
-.data-table {
|
|
|
- width: 100%;
|
|
|
- border-collapse: collapse;
|
|
|
-}
|
|
|
-
|
|
|
-.data-table th {
|
|
|
+ gap: 15px;
|
|
|
+ padding: 20px;
|
|
|
+ border: 1px solid #e1e7ef;
|
|
|
+ border-top: 3px solid var(--tone);
|
|
|
+ border-radius: 8px;
|
|
|
+ background: #ffffff;
|
|
|
+ color: inherit;
|
|
|
text-align: left;
|
|
|
- padding: 12px 8px 12px 0;
|
|
|
- font-weight: 600;
|
|
|
- font-size: 0.8rem;
|
|
|
- color: #5b6e8c;
|
|
|
- border-bottom: 1px solid #eef2f6;
|
|
|
-}
|
|
|
-
|
|
|
-.data-table td {
|
|
|
- padding: 14px 8px 14px 0;
|
|
|
- border-bottom: 1px solid #f1f5f9;
|
|
|
- font-size: 0.85rem;
|
|
|
- vertical-align: middle;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: border-color 0.2s, box-shadow 0.2s, transform 0.2s;
|
|
|
}
|
|
|
|
|
|
-.version-text {
|
|
|
- font-size: 11px;
|
|
|
- color: #54708f;
|
|
|
+.metric-card:hover {
|
|
|
+ border-color: var(--tone);
|
|
|
+ box-shadow: 0 8px 20px rgba(31, 41, 55, 0.08);
|
|
|
+ transform: translateY(-1px);
|
|
|
}
|
|
|
|
|
|
-.status-badge {
|
|
|
- background: #dcfce7;
|
|
|
- color: #15803d;
|
|
|
- padding: 4px 10px;
|
|
|
- border-radius: 30px;
|
|
|
- font-size: 0.7rem;
|
|
|
- font-weight: 600;
|
|
|
- display: inline-block;
|
|
|
+.metric-icon, .quick-icon {
|
|
|
+ display: inline-flex;
|
|
|
+ width: 42px;
|
|
|
+ height: 42px;
|
|
|
+ flex: 0 0 42px;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ border-radius: 7px;
|
|
|
}
|
|
|
|
|
|
-.status-badge.warning {
|
|
|
- background: #fff3e3;
|
|
|
- color: #b45309;
|
|
|
-}
|
|
|
+.metric-icon { background: var(--tone-soft); color: var(--tone); font-size: 21px; }
|
|
|
+.metric-content { display: flex; min-width: 0; flex: 1; flex-direction: column; }
|
|
|
+.metric-label { color: #64748b; font-size: 13px; }
|
|
|
+.metric-content strong { margin: 4px 0 2px; color: #172033; font-size: 34px; line-height: 1.05; }
|
|
|
|
|
|
-.status-badge.secondary {
|
|
|
- background: #eef2ff;
|
|
|
- color: #1e40af;
|
|
|
-}
|
|
|
-
|
|
|
-.btn-icon {
|
|
|
- background: none;
|
|
|
- border: none;
|
|
|
- color: #5b6e8c;
|
|
|
- cursor: default;
|
|
|
- font-size: 1rem;
|
|
|
- margin: 0 4px;
|
|
|
+.metric-content small {
|
|
|
+ overflow: hidden;
|
|
|
+ color: #94a3b8;
|
|
|
+ font-size: 12px;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
-.comment-text {
|
|
|
- font-size: 0.8rem;
|
|
|
- max-width: 180px;
|
|
|
- white-space: normal;
|
|
|
- word-break: break-word;
|
|
|
- line-height: 1.4;
|
|
|
-}
|
|
|
+.metric-arrow { color: #b6c0cd; }
|
|
|
|
|
|
-.adjust-item {
|
|
|
- background: #fefce8;
|
|
|
- border-left: 4px solid #facc15;
|
|
|
- padding: 14px 16px;
|
|
|
- border-radius: 16px;
|
|
|
- margin-bottom: 14px;
|
|
|
-}
|
|
|
-
|
|
|
-.adjust-title {
|
|
|
- font-weight: 700;
|
|
|
- display: flex;
|
|
|
- justify-content: space-between;
|
|
|
- flex-wrap: wrap;
|
|
|
- gap: 8px;
|
|
|
- margin-bottom: 6px;
|
|
|
+.analysis-grid, .operations-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: minmax(0, 1.7fr) minmax(320px, 0.8fr);
|
|
|
+ gap: 16px;
|
|
|
+ margin-bottom: 16px;
|
|
|
}
|
|
|
|
|
|
-.adjust-detail {
|
|
|
- font-size: 0.8rem;
|
|
|
- color: #3b3f46;
|
|
|
- margin-top: 8px;
|
|
|
+.panel {
|
|
|
+ min-width: 0;
|
|
|
+ border: 1px solid #e1e7ef;
|
|
|
+ border-radius: 8px;
|
|
|
+ background: #ffffff;
|
|
|
+ box-shadow: 0 2px 8px rgba(31, 41, 55, 0.035);
|
|
|
}
|
|
|
|
|
|
-.publish-card, .pending-publish-card {
|
|
|
- background: linear-gradient(115deg, #eef6fc 0%, #ffffff 100%);
|
|
|
- border-radius: 20px;
|
|
|
- padding: 20px;
|
|
|
+.panel-header {
|
|
|
display: flex;
|
|
|
- justify-content: space-between;
|
|
|
+ min-height: 72px;
|
|
|
align-items: center;
|
|
|
- flex-wrap: wrap;
|
|
|
+ justify-content: space-between;
|
|
|
gap: 16px;
|
|
|
+ padding: 17px 20px 14px;
|
|
|
+ border-bottom: 1px solid #edf1f5;
|
|
|
}
|
|
|
|
|
|
-.publish-info h4 {
|
|
|
- font-weight: 700;
|
|
|
-}
|
|
|
+.panel-header h2 { margin: 0 0 5px; font-size: 17px; line-height: 1.25; }
|
|
|
+.panel-header p { font-size: 12px; }
|
|
|
+.panel-header .el-button { flex: 0 0 auto; }
|
|
|
+.chart-box { width: 100%; height: 292px; }
|
|
|
+.trend-panel, .status-panel { position: relative; min-height: 365px; }
|
|
|
+.trend-panel .el-empty, .status-panel .el-empty { position: absolute; inset: 84px 0 8px; }
|
|
|
+.facility-panel { margin-bottom: 16px; }
|
|
|
|
|
|
-.publish-btn {
|
|
|
- background: #0f2b3d;
|
|
|
- border: none;
|
|
|
- padding: 10px 24px;
|
|
|
- border-radius: 40px;
|
|
|
- color: white;
|
|
|
- font-weight: 600;
|
|
|
- font-size: 0.85rem;
|
|
|
- cursor: default;
|
|
|
+.facility-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
|
+ gap: 14px;
|
|
|
+ padding: 16px 20px 20px;
|
|
|
}
|
|
|
|
|
|
-.publish-btn.disabled {
|
|
|
- background: #94a3b8;
|
|
|
- cursor: not-allowed;
|
|
|
+.facility-card {
|
|
|
+ min-width: 0;
|
|
|
+ padding: 16px;
|
|
|
+ border: 1px solid #e7ebf1;
|
|
|
+ border-radius: 7px;
|
|
|
+ background: #ffffff;
|
|
|
}
|
|
|
|
|
|
-.pending-publish-card {
|
|
|
- background: #ffffff;
|
|
|
- border: 1px solid #eef2f6;
|
|
|
+.facility-title, .facility-value {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
justify-content: space-between;
|
|
|
+ gap: 10px;
|
|
|
}
|
|
|
|
|
|
-.pending-list {
|
|
|
- font-size: 12px;
|
|
|
- color: #2c3e66;
|
|
|
-}
|
|
|
+.facility-title { justify-content: flex-start; }
|
|
|
+.facility-title > div { display: flex; min-width: 0; flex-direction: column; gap: 3px; }
|
|
|
+.facility-title strong { overflow: hidden; font-size: 14px; text-overflow: ellipsis; white-space: nowrap; }
|
|
|
+.facility-title small, .facility-value span { color: #94a3b8; font-size: 12px; }
|
|
|
|
|
|
-.publish-action {
|
|
|
- background: #2c7da0;
|
|
|
- padding: 6px 18px;
|
|
|
- border-radius: 40px;
|
|
|
- color: white;
|
|
|
- font-size: 0.8rem;
|
|
|
- font-weight: 600;
|
|
|
- cursor: pointer;
|
|
|
- transition: 0.2s;
|
|
|
+.facility-mark {
|
|
|
+ display: inline-flex;
|
|
|
+ width: 36px;
|
|
|
+ height: 36px;
|
|
|
+ flex: 0 0 36px;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ border-radius: 7px;
|
|
|
+ background: var(--facility-soft);
|
|
|
+ color: var(--facility-color);
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 700;
|
|
|
}
|
|
|
|
|
|
-.publish-action:hover {
|
|
|
- background: #1f5e7e;
|
|
|
- transform: scale(0.98);
|
|
|
-}
|
|
|
+.facility-value { margin: 14px 0 10px; align-items: flex-end; }
|
|
|
+.facility-value strong { font-size: 27px; line-height: 1; }
|
|
|
+.facility-progress { height: 5px; overflow: hidden; border-radius: 3px; background: #eef2f6; }
|
|
|
|
|
|
-.record-card {
|
|
|
- flex: 1;
|
|
|
- background: #f9fafb;
|
|
|
- border-radius: 20px;
|
|
|
- padding: 12px;
|
|
|
+.facility-progress i {
|
|
|
+ display: block;
|
|
|
+ height: 100%;
|
|
|
+ border-radius: inherit;
|
|
|
+ background: var(--facility-color);
|
|
|
+ transition: width 0.35s ease;
|
|
|
}
|
|
|
|
|
|
-.record-desc {
|
|
|
- font-size: 13px;
|
|
|
- margin-top: 8px;
|
|
|
- color: #334155;
|
|
|
-}
|
|
|
+.warning-table { width: calc(100% - 32px); margin: 0 16px 16px; cursor: pointer; }
|
|
|
+.warning-name { display: flex; min-width: 0; align-items: center; gap: 8px; }
|
|
|
|
|
|
-.helper-text {
|
|
|
- font-size: 0.7rem;
|
|
|
- color: #6c86a3;
|
|
|
- margin-top: 12px;
|
|
|
- text-align: center;
|
|
|
+.warning-name i {
|
|
|
+ width: 8px;
|
|
|
+ height: 8px;
|
|
|
+ flex: 0 0 8px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: #94a3b8;
|
|
|
}
|
|
|
|
|
|
-.footer-bar {
|
|
|
- background: #f9fef9;
|
|
|
- border-top: 1px solid #e2ecf5;
|
|
|
- padding: 16px 32px;
|
|
|
+.warning-name i.level-1 { background: #2563eb; }
|
|
|
+.warning-name i.level-2 { background: #eab308; }
|
|
|
+.warning-name i.level-3 { background: #f97316; }
|
|
|
+.warning-name i.level-4 { background: #dc2626; }
|
|
|
+.warning-name span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
|
+.waiting-time { color: #b45309; font-weight: 600; }
|
|
|
+.quick-panel { align-self: stretch; }
|
|
|
+.quick-list { display: flex; flex-direction: column; padding: 8px 14px 14px; }
|
|
|
+
|
|
|
+.quick-action {
|
|
|
display: flex;
|
|
|
- justify-content: space-between;
|
|
|
+ width: 100%;
|
|
|
+ min-height: 64px;
|
|
|
align-items: center;
|
|
|
- flex-wrap: wrap;
|
|
|
- gap: 10px;
|
|
|
+ gap: 12px;
|
|
|
+ padding: 10px 8px;
|
|
|
+ border: 0;
|
|
|
+ border-bottom: 1px solid #edf1f5;
|
|
|
+ background: transparent;
|
|
|
+ color: inherit;
|
|
|
+ text-align: left;
|
|
|
+ cursor: pointer;
|
|
|
}
|
|
|
|
|
|
+.quick-action:last-child { border-bottom: 0; }
|
|
|
+.quick-action:hover { background: #f8fafc; }
|
|
|
|
|
|
-@media (max-width: 980px) {
|
|
|
- .dashboard-grid {
|
|
|
- grid-template-columns: 1fr;
|
|
|
- }
|
|
|
- .left-panel {
|
|
|
- border-right: none;
|
|
|
- border-bottom: 1px solid #eef2f6;
|
|
|
- }
|
|
|
- body {
|
|
|
- padding: 20px;
|
|
|
- }
|
|
|
+.quick-action > span:nth-child(2) {
|
|
|
+ display: flex;
|
|
|
+ min-width: 0;
|
|
|
+ flex: 1;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 4px;
|
|
|
}
|
|
|
-</style>
|
|
|
+
|
|
|
+.quick-action strong, .quick-action small {
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+.quick-action strong { font-size: 14px; }
|
|
|
+.quick-action small { color: #94a3b8; font-size: 12px; }
|
|
|
+.quick-icon { width: 38px; height: 38px; flex-basis: 38px; font-size: 18px; }
|
|
|
+.quick-arrow { flex: 0 0 auto; color: #b6c0cd; }
|
|
|
+
|
|
|
+@media (max-width: 1180px) {
|
|
|
+ .metric-grid, .facility-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
|
+ .analysis-grid, .operations-grid { grid-template-columns: 1fr; }
|
|
|
+ .quick-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 16px; }
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 680px) {
|
|
|
+ .lifeline-home { padding: 14px 12px 22px; }
|
|
|
+ .home-header { align-items: flex-start; flex-direction: column; }
|
|
|
+ .header-tools { width: 100%; justify-content: space-between; }
|
|
|
+ .date-block { min-width: 0; align-items: flex-start; }
|
|
|
+ .metric-grid, .facility-grid, .quick-list { grid-template-columns: 1fr; }
|
|
|
+ .metric-card { min-height: 110px; }
|
|
|
+ .panel-header { align-items: flex-start; }
|
|
|
+ .chart-box { height: 260px; }
|
|
|
+ .trend-panel, .status-panel { min-height: 335px; }
|
|
|
+ .facility-grid { padding: 14px; }
|
|
|
+}
|
|
|
+</style>
|