|
|
@@ -1,233 +1,1027 @@
|
|
|
<template>
|
|
|
- <div class="dashboard">
|
|
|
- <!-- 欢迎卡片 -->
|
|
|
- <div class="welcome-card">
|
|
|
- <div class="welcome-info">
|
|
|
- <h2>欢迎回来,{{ userStore.nickname }} 👋</h2>
|
|
|
- <p>{{ greeting }},祝您工作愉快!</p>
|
|
|
+ <div class="dashboard-page">
|
|
|
+ <section class="overview-strip">
|
|
|
+ <div>
|
|
|
+ <div class="eyebrow">运营首页</div>
|
|
|
+ <h1>艾灸椅业务总览</h1>
|
|
|
+ <p>设备投产、圣手服务、审批待办与异常报警集中看板</p>
|
|
|
</div>
|
|
|
- <div class="welcome-date">
|
|
|
- <div class="date">{{ currentDate }}</div>
|
|
|
- <div class="time">{{ currentTime }}</div>
|
|
|
+ <div class="overview-actions">
|
|
|
+ <el-tag effect="plain" type="success">系统运行正常</el-tag>
|
|
|
+ <el-button :icon="Refresh" type="primary" plain>刷新数据</el-button>
|
|
|
</div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 统计卡片 -->
|
|
|
- <el-row :gutter="20" class="stat-row">
|
|
|
- <el-col :xs="24" :sm="12" :lg="6" v-for="stat in stats" :key="stat.title">
|
|
|
- <div class="stat-card" :style="{ '--card-color': stat.color }">
|
|
|
- <div class="stat-icon">
|
|
|
- <el-icon><component :is="stat.icon" /></el-icon>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section class="metric-grid">
|
|
|
+ <article
|
|
|
+ v-for="item in metrics"
|
|
|
+ :key="item.title"
|
|
|
+ class="metric-card"
|
|
|
+ :style="{ '--accent': item.color, '--accent-soft': item.softColor }"
|
|
|
+ >
|
|
|
+ <div class="metric-icon">
|
|
|
+ <el-icon><component :is="item.icon" /></el-icon>
|
|
|
+ </div>
|
|
|
+ <div class="metric-body">
|
|
|
+ <span>{{ item.title }}</span>
|
|
|
+ <strong>{{ item.value }}</strong>
|
|
|
+ <em>{{ item.tip }}</em>
|
|
|
+ </div>
|
|
|
+ </article>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section class="main-grid">
|
|
|
+ <div class="panel map-panel">
|
|
|
+ <div class="panel-header">
|
|
|
+ <div>
|
|
|
+ <h2>设备分布图</h2>
|
|
|
+ <p>按中国省份级区域统计投产设备数量</p>
|
|
|
+ </div>
|
|
|
+ <el-tag type="info" effect="plain">合计 {{ totalDeviceCount }} 台</el-tag>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="distribution-layout">
|
|
|
+ <div class="province-map" aria-label="中国省份设备分布">
|
|
|
+ <div
|
|
|
+ v-for="province in provinceData"
|
|
|
+ :key="province.name"
|
|
|
+ :class="['province-tile', heatClass(province.count)]"
|
|
|
+ :style="{ gridColumn: province.col, gridRow: province.row }"
|
|
|
+ >
|
|
|
+ <span>{{ province.name }}</span>
|
|
|
+ <strong>{{ province.count }}</strong>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="province-rank">
|
|
|
+ <div class="legend">
|
|
|
+ <span class="legend-item level-1"></span>
|
|
|
+ <span>1-9</span>
|
|
|
+ <span class="legend-item level-2"></span>
|
|
|
+ <span>10-29</span>
|
|
|
+ <span class="legend-item level-3"></span>
|
|
|
+ <span>30+</span>
|
|
|
+ </div>
|
|
|
+ <div class="rank-list">
|
|
|
+ <div v-for="province in topProvinces" :key="province.name" class="rank-item">
|
|
|
+ <span>{{ province.name }}</span>
|
|
|
+ <div class="rank-track">
|
|
|
+ <i :style="{ width: `${province.percent}%` }"></i>
|
|
|
+ </div>
|
|
|
+ <strong>{{ province.count }}</strong>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="panel device-panel">
|
|
|
+ <div class="panel-header">
|
|
|
+ <div>
|
|
|
+ <h2>设备统计图</h2>
|
|
|
+ <p>艾灸椅设备与其他设备占比</p>
|
|
|
+ </div>
|
|
|
+ <el-icon><PieChart /></el-icon>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="donut-wrap">
|
|
|
+ <div class="donut-chart">
|
|
|
+ <span>100%</span>
|
|
|
+ <small>艾灸椅设备</small>
|
|
|
+ </div>
|
|
|
+ <div class="device-summary">
|
|
|
+ <div class="device-row">
|
|
|
+ <span><i class="dot primary"></i>艾灸椅设备</span>
|
|
|
+ <strong>{{ totalDeviceCount }} 台</strong>
|
|
|
+ </div>
|
|
|
+ <el-progress :percentage="100" :stroke-width="10" :show-text="false" />
|
|
|
+ <div class="device-row muted">
|
|
|
+ <span><i class="dot empty"></i>其他设备</span>
|
|
|
+ <strong>0 台</strong>
|
|
|
+ </div>
|
|
|
+ <p class="panel-note">其他设备暂未维护,当前仅展示艾灸椅设备。</p>
|
|
|
</div>
|
|
|
- <div class="stat-info">
|
|
|
- <div class="stat-value">{{ stat.value }}</div>
|
|
|
- <div class="stat-title">{{ stat.title }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section class="panel chart-panel">
|
|
|
+ <div class="panel-header">
|
|
|
+ <div>
|
|
|
+ <h2>圣手模式统计</h2>
|
|
|
+ <p>统计圣手沟通数量与发布圣手方案数量</p>
|
|
|
+ </div>
|
|
|
+ <el-radio-group v-model="period" size="small">
|
|
|
+ <el-radio-button label="week">周</el-radio-button>
|
|
|
+ <el-radio-button label="month">月</el-radio-button>
|
|
|
+ <el-radio-button label="quarter">季度</el-radio-button>
|
|
|
+ </el-radio-group>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="bar-chart">
|
|
|
+ <div v-for="item in chartBars" :key="item.label" class="bar-group">
|
|
|
+ <div class="bar-stack">
|
|
|
+ <el-tooltip :content="`沟通 ${item.communicate} 次`" placement="top">
|
|
|
+ <span class="bar communicate" :style="{ height: `${item.communicateHeight}%` }"></span>
|
|
|
+ </el-tooltip>
|
|
|
+ <el-tooltip :content="`发布方案 ${item.plan} 个`" placement="top">
|
|
|
+ <span class="bar plan" :style="{ height: `${item.planHeight}%` }"></span>
|
|
|
+ </el-tooltip>
|
|
|
+ </div>
|
|
|
+ <span class="bar-label">{{ item.label }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="chart-legend">
|
|
|
+ <span><i class="bar-dot communicate"></i>圣手沟通数量</span>
|
|
|
+ <span><i class="bar-dot plan"></i>发布圣手方案数量</span>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section class="bottom-grid">
|
|
|
+ <div class="panel list-panel">
|
|
|
+ <div class="panel-header">
|
|
|
+ <div>
|
|
|
+ <h2>待办事项</h2>
|
|
|
+ <p>管理员审批任务</p>
|
|
|
+ </div>
|
|
|
+ <el-badge :value="todoTotal" type="warning">
|
|
|
+ <el-icon><Checked /></el-icon>
|
|
|
+ </el-badge>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="work-list">
|
|
|
+ <div v-for="todo in todoList" :key="todo.title" class="work-item">
|
|
|
+ <div class="work-icon todo">
|
|
|
+ <el-icon><component :is="todo.icon" /></el-icon>
|
|
|
+ </div>
|
|
|
+ <div class="work-content">
|
|
|
+ <div class="work-title">
|
|
|
+ <strong>{{ todo.title }}</strong>
|
|
|
+ <el-tag :type="todo.type" size="small" effect="plain">{{ todo.status }}</el-tag>
|
|
|
+ </div>
|
|
|
+ <p>{{ todo.desc }}</p>
|
|
|
+ </div>
|
|
|
+ <div class="work-count">
|
|
|
+ <strong>{{ todo.count }}</strong>
|
|
|
+ <span>项</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="panel list-panel">
|
|
|
+ <div class="panel-header">
|
|
|
+ <div>
|
|
|
+ <h2>异常报警</h2>
|
|
|
+ <p>设备故障与用户报修信息</p>
|
|
|
</div>
|
|
|
+ <el-badge :value="alarmList.length" type="danger">
|
|
|
+ <el-icon><WarningFilled /></el-icon>
|
|
|
+ </el-badge>
|
|
|
</div>
|
|
|
- </el-col>
|
|
|
- </el-row>
|
|
|
-
|
|
|
- <!-- 框架信息 -->
|
|
|
- <el-row :gutter="20">
|
|
|
- <el-col :span="24">
|
|
|
- <el-card class="info-card" header="框架技术栈">
|
|
|
- <el-row :gutter="20">
|
|
|
- <el-col :xs="24" :sm="12" :md="6" v-for="tech in techStack" :key="tech.name">
|
|
|
- <div class="tech-item">
|
|
|
- <div class="tech-name">{{ tech.name }}</div>
|
|
|
- <div class="tech-version">{{ tech.version }}</div>
|
|
|
- <div class="tech-desc">{{ tech.desc }}</div>
|
|
|
+
|
|
|
+ <div class="work-list alarm-list">
|
|
|
+ <div v-for="alarm in alarmList" :key="alarm.id" class="work-item">
|
|
|
+ <div :class="['work-icon', alarm.level]">
|
|
|
+ <el-icon><BellFilled /></el-icon>
|
|
|
+ </div>
|
|
|
+ <div class="work-content">
|
|
|
+ <div class="work-title">
|
|
|
+ <strong>{{ alarm.title }}</strong>
|
|
|
+ <el-tag :type="alarm.tagType" size="small">{{ alarm.levelText }}</el-tag>
|
|
|
</div>
|
|
|
- </el-col>
|
|
|
- </el-row>
|
|
|
- </el-card>
|
|
|
- </el-col>
|
|
|
- </el-row>
|
|
|
+ <p>{{ alarm.device }} · {{ alarm.location }}</p>
|
|
|
+ </div>
|
|
|
+ <time>{{ alarm.time }}</time>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|
|
-import { User, Document, Odometer, DataLine } from '@element-plus/icons-vue'
|
|
|
-import { useUserStore } from '@/store/user'
|
|
|
-
|
|
|
-const userStore = useUserStore()
|
|
|
-
|
|
|
-// 当前时间
|
|
|
-const currentTime = ref('')
|
|
|
-const currentDate = ref('')
|
|
|
-let timer = null
|
|
|
-
|
|
|
-// 问候语
|
|
|
-const greeting = computed(() => {
|
|
|
- const hour = new Date().getHours()
|
|
|
- if (hour < 6) return '凌晨好'
|
|
|
- if (hour < 12) return '上午好'
|
|
|
- if (hour < 14) return '中午好'
|
|
|
- if (hour < 18) return '下午好'
|
|
|
- return '晚上好'
|
|
|
-})
|
|
|
+import { computed, ref } from 'vue'
|
|
|
+import {
|
|
|
+ BellFilled,
|
|
|
+ Checked,
|
|
|
+ ChatDotRound,
|
|
|
+ DocumentChecked,
|
|
|
+ GoldMedal,
|
|
|
+ Monitor,
|
|
|
+ Notebook,
|
|
|
+ PieChart,
|
|
|
+ Promotion,
|
|
|
+ Refresh,
|
|
|
+ UserFilled,
|
|
|
+ WarningFilled
|
|
|
+} from '@element-plus/icons-vue'
|
|
|
|
|
|
-// 统计卡片数据(实际项目中从后端接口获取)
|
|
|
-const stats = [
|
|
|
- { title: '今日访问量', value: '--', icon: 'DataLine', color: '#1890ff' },
|
|
|
- { title: '用户总数', value: '--', icon: 'User', color: '#52c41a' },
|
|
|
- { title: '操作日志', value: '--', icon: 'Document', color: '#faad14' },
|
|
|
- { title: '系统状态', value: '正常', icon: 'Odometer', color: '#13c2c2' }
|
|
|
+const metrics = [
|
|
|
+ {
|
|
|
+ title: '当前使用用户数量',
|
|
|
+ value: '12,836',
|
|
|
+ tip: '较昨日 +328',
|
|
|
+ icon: UserFilled,
|
|
|
+ color: '#2f80ed',
|
|
|
+ softColor: 'rgba(47, 128, 237, 0.12)'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '布衣圣手数量',
|
|
|
+ value: '286',
|
|
|
+ tip: '本月新增 18',
|
|
|
+ icon: GoldMedal,
|
|
|
+ color: '#b7791f',
|
|
|
+ softColor: 'rgba(183, 121, 31, 0.14)'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '投产设备数量',
|
|
|
+ value: '395',
|
|
|
+ tip: '在线率 92.4%',
|
|
|
+ icon: Monitor,
|
|
|
+ color: '#0f9f8f',
|
|
|
+ softColor: 'rgba(15, 159, 143, 0.12)'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '圣手方案数量',
|
|
|
+ value: '1,248',
|
|
|
+ tip: '待审核 23',
|
|
|
+ icon: Notebook,
|
|
|
+ color: '#c05621',
|
|
|
+ softColor: 'rgba(192, 86, 33, 0.13)'
|
|
|
+ }
|
|
|
]
|
|
|
|
|
|
-// 技术栈信息
|
|
|
-const techStack = [
|
|
|
- { name: 'Spring Boot', version: '2.7.18', desc: '后端基础框架' },
|
|
|
- { name: 'MyBatis Plus', version: '3.5.3', desc: 'ORM 框架' },
|
|
|
- { name: 'Redis', version: '3.x', desc: '缓存 & Token 存储' },
|
|
|
- { name: 'Vue 3', version: '3.x + Vite', desc: '前端框架' }
|
|
|
+const provinceData = [
|
|
|
+ { name: '新疆', count: 2, col: '1 / span 2', row: '2' },
|
|
|
+ { name: '内蒙古', count: 2, col: '4 / span 3', row: '1' },
|
|
|
+ { name: '黑龙江', count: 4, col: '9 / span 2', row: '1' },
|
|
|
+ { name: '吉林', count: 3, col: '9', row: '2' },
|
|
|
+ { name: '辽宁', count: 6, col: '8', row: '2' },
|
|
|
+ { name: '甘肃', count: 3, col: '4', row: '2' },
|
|
|
+ { name: '宁夏', count: 1, col: '4', row: '3' },
|
|
|
+ { name: '北京', count: 14, col: '7', row: '2' },
|
|
|
+ { name: '天津', count: 5, col: '8', row: '3' },
|
|
|
+ { name: '河北', count: 9, col: '7', row: '3' },
|
|
|
+ { name: '山西', count: 5, col: '6', row: '3' },
|
|
|
+ { name: '青海', count: 1, col: '3', row: '3' },
|
|
|
+ { name: '西藏', count: 0, col: '2 / span 2', row: '5' },
|
|
|
+ { name: '陕西', count: 8, col: '5', row: '4' },
|
|
|
+ { name: '山东', count: 31, col: '8', row: '4' },
|
|
|
+ { name: '河南', count: 21, col: '6', row: '4' },
|
|
|
+ { name: '江苏', count: 39, col: '9', row: '4' },
|
|
|
+ { name: '上海', count: 13, col: '10', row: '5' },
|
|
|
+ { name: '安徽', count: 9, col: '8', row: '5' },
|
|
|
+ { name: '湖北', count: 18, col: '6', row: '5' },
|
|
|
+ { name: '重庆', count: 10, col: '5', row: '5' },
|
|
|
+ { name: '四川', count: 24, col: '4', row: '5' },
|
|
|
+ { name: '贵州', count: 4, col: '5', row: '6' },
|
|
|
+ { name: '湖南', count: 16, col: '6', row: '6' },
|
|
|
+ { name: '江西', count: 6, col: '7', row: '6' },
|
|
|
+ { name: '浙江', count: 42, col: '9', row: '6' },
|
|
|
+ { name: '福建', count: 15, col: '8', row: '7' },
|
|
|
+ { name: '广东', count: 68, col: '7', row: '7' },
|
|
|
+ { name: '广西', count: 7, col: '6', row: '7' },
|
|
|
+ { name: '云南', count: 6, col: '4 / span 2', row: '7' },
|
|
|
+ { name: '海南', count: 3, col: '7', row: '8' },
|
|
|
+ { name: '台湾', count: 0, col: '9', row: '7' },
|
|
|
+ { name: '香港', count: 0, col: '8', row: '8' },
|
|
|
+ { name: '澳门', count: 0, col: '6', row: '8' }
|
|
|
]
|
|
|
|
|
|
-// 更新时间
|
|
|
-function updateTime() {
|
|
|
- const now = new Date()
|
|
|
- const pad = n => String(n).padStart(2, '0')
|
|
|
- currentTime.value = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
|
|
|
- currentDate.value = `${now.getFullYear()}年${now.getMonth() + 1}月${now.getDate()}日 ${['日', '一', '二', '三', '四', '五', '六'][now.getDay()]}曜日`
|
|
|
+const period = ref('week')
|
|
|
+
|
|
|
+const chartSource = {
|
|
|
+ week: [
|
|
|
+ { label: '周一', communicate: 96, plan: 18 },
|
|
|
+ { label: '周二', communicate: 128, plan: 24 },
|
|
|
+ { label: '周三', communicate: 116, plan: 21 },
|
|
|
+ { label: '周四', communicate: 152, plan: 35 },
|
|
|
+ { label: '周五', communicate: 141, plan: 28 },
|
|
|
+ { label: '周六', communicate: 86, plan: 16 },
|
|
|
+ { label: '周日', communicate: 73, plan: 11 }
|
|
|
+ ],
|
|
|
+ month: [
|
|
|
+ { label: '第1周', communicate: 642, plan: 104 },
|
|
|
+ { label: '第2周', communicate: 718, plan: 126 },
|
|
|
+ { label: '第3周', communicate: 803, plan: 142 },
|
|
|
+ { label: '第4周', communicate: 756, plan: 137 }
|
|
|
+ ],
|
|
|
+ quarter: [
|
|
|
+ { label: '一月', communicate: 2380, plan: 392 },
|
|
|
+ { label: '二月', communicate: 2194, plan: 361 },
|
|
|
+ { label: '三月', communicate: 2616, plan: 448 }
|
|
|
+ ]
|
|
|
}
|
|
|
|
|
|
-onMounted(() => {
|
|
|
- updateTime()
|
|
|
- timer = setInterval(updateTime, 1000)
|
|
|
+const todoList = [
|
|
|
+ {
|
|
|
+ title: '圣手申请审批',
|
|
|
+ desc: '新提交资质材料待管理员审核',
|
|
|
+ count: 12,
|
|
|
+ status: '高优先级',
|
|
|
+ type: 'warning',
|
|
|
+ icon: Promotion
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '圣手方案发布审批',
|
|
|
+ desc: '圣手方案发布前内容与步骤确认',
|
|
|
+ count: 23,
|
|
|
+ status: '待处理',
|
|
|
+ type: 'primary',
|
|
|
+ icon: DocumentChecked
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '用户反馈复核',
|
|
|
+ desc: '用户对服务体验与设备使用的反馈',
|
|
|
+ count: 7,
|
|
|
+ status: '跟进中',
|
|
|
+ type: 'info',
|
|
|
+ icon: ChatDotRound
|
|
|
+ }
|
|
|
+]
|
|
|
+
|
|
|
+const alarmList = [
|
|
|
+ {
|
|
|
+ id: 'ALM001',
|
|
|
+ title: '温控模块异常',
|
|
|
+ device: 'AJY-2026-GD-0318',
|
|
|
+ location: '广东省广州市',
|
|
|
+ time: '10分钟前',
|
|
|
+ level: 'critical',
|
|
|
+ levelText: '严重',
|
|
|
+ tagType: 'danger'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'ALM002',
|
|
|
+ title: '用户报修待派单',
|
|
|
+ device: 'AJY-2025-ZJ-0094',
|
|
|
+ location: '浙江省杭州市',
|
|
|
+ time: '32分钟前',
|
|
|
+ level: 'warning',
|
|
|
+ levelText: '报修',
|
|
|
+ tagType: 'warning'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'ALM003',
|
|
|
+ title: '设备离线超过 24 小时',
|
|
|
+ device: 'AJY-2026-SC-0211',
|
|
|
+ location: '四川省成都市',
|
|
|
+ time: '1小时前',
|
|
|
+ level: 'offline',
|
|
|
+ levelText: '离线',
|
|
|
+ tagType: 'info'
|
|
|
+ }
|
|
|
+]
|
|
|
+
|
|
|
+const totalDeviceCount = computed(() => provinceData.reduce((sum, item) => sum + item.count, 0))
|
|
|
+
|
|
|
+const topProvinces = computed(() => {
|
|
|
+ const max = Math.max(...provinceData.map(item => item.count))
|
|
|
+ return [...provinceData]
|
|
|
+ .sort((a, b) => b.count - a.count)
|
|
|
+ .slice(0, 6)
|
|
|
+ .map(item => ({
|
|
|
+ ...item,
|
|
|
+ percent: Math.max(8, Math.round((item.count / max) * 100))
|
|
|
+ }))
|
|
|
})
|
|
|
|
|
|
-onUnmounted(() => {
|
|
|
- clearInterval(timer)
|
|
|
+const chartBars = computed(() => {
|
|
|
+ const data = chartSource[period.value]
|
|
|
+ const max = Math.max(...data.flatMap(item => [item.communicate, item.plan]))
|
|
|
+ return data.map(item => ({
|
|
|
+ ...item,
|
|
|
+ communicateHeight: Math.max(8, Math.round((item.communicate / max) * 100)),
|
|
|
+ planHeight: Math.max(8, Math.round((item.plan / max) * 100))
|
|
|
+ }))
|
|
|
})
|
|
|
+
|
|
|
+const todoTotal = computed(() => todoList.reduce((sum, item) => sum + item.count, 0))
|
|
|
+
|
|
|
+function heatClass(count) {
|
|
|
+ if (count >= 30) return 'heat-3'
|
|
|
+ if (count >= 10) return 'heat-2'
|
|
|
+ if (count > 0) return 'heat-1'
|
|
|
+ return 'heat-0'
|
|
|
+}
|
|
|
</script>
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
-.dashboard {
|
|
|
+.dashboard-page {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
- gap: 20px;
|
|
|
+ gap: 18px;
|
|
|
+ min-height: calc(100vh - 104px);
|
|
|
+ color: #1f2933;
|
|
|
}
|
|
|
|
|
|
-// 欢迎卡片
|
|
|
-.welcome-card {
|
|
|
- background: linear-gradient(135deg, #1890ff, #36cfc9);
|
|
|
- border-radius: 12px;
|
|
|
- padding: 28px 32px;
|
|
|
+.overview-strip,
|
|
|
+.panel,
|
|
|
+.metric-card {
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid #edf0f5;
|
|
|
+ box-shadow: 0 8px 24px rgba(29, 41, 57, 0.06);
|
|
|
+}
|
|
|
+
|
|
|
+.overview-strip {
|
|
|
display: flex;
|
|
|
+ align-items: center;
|
|
|
justify-content: space-between;
|
|
|
+ gap: 16px;
|
|
|
+ padding: 22px 24px;
|
|
|
+ border-radius: 8px;
|
|
|
+
|
|
|
+ .eyebrow {
|
|
|
+ margin-bottom: 6px;
|
|
|
+ color: #0f9f8f;
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 700;
|
|
|
+ }
|
|
|
+
|
|
|
+ h1 {
|
|
|
+ margin: 0;
|
|
|
+ color: #172033;
|
|
|
+ font-size: 24px;
|
|
|
+ line-height: 1.25;
|
|
|
+ font-weight: 700;
|
|
|
+ letter-spacing: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ p {
|
|
|
+ margin-top: 8px;
|
|
|
+ color: #697586;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.overview-actions {
|
|
|
+ display: flex;
|
|
|
align-items: center;
|
|
|
- color: #fff;
|
|
|
+ gap: 10px;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
|
|
|
- .welcome-info {
|
|
|
- h2 {
|
|
|
- font-size: 22px;
|
|
|
- font-weight: 600;
|
|
|
- margin: 0 0 8px;
|
|
|
- }
|
|
|
- p {
|
|
|
- margin: 0;
|
|
|
- opacity: 0.85;
|
|
|
- font-size: 14px;
|
|
|
+.metric-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
|
+ gap: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.metric-card {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 16px;
|
|
|
+ min-height: 116px;
|
|
|
+ padding: 20px;
|
|
|
+ border-radius: 8px;
|
|
|
+
|
|
|
+ .metric-icon {
|
|
|
+ width: 52px;
|
|
|
+ height: 52px;
|
|
|
+ display: grid;
|
|
|
+ place-items: center;
|
|
|
+ flex-shrink: 0;
|
|
|
+ border-radius: 8px;
|
|
|
+ background: var(--accent-soft);
|
|
|
+ color: var(--accent);
|
|
|
+
|
|
|
+ .el-icon {
|
|
|
+ font-size: 26px;
|
|
|
}
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
- .welcome-date {
|
|
|
- text-align: right;
|
|
|
- .date {
|
|
|
- font-size: 14px;
|
|
|
- opacity: 0.85;
|
|
|
- }
|
|
|
- .time {
|
|
|
- font-size: 32px;
|
|
|
- font-weight: 200;
|
|
|
- letter-spacing: 2px;
|
|
|
- font-variant-numeric: tabular-nums;
|
|
|
- }
|
|
|
+.metric-body {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ min-width: 0;
|
|
|
+
|
|
|
+ span {
|
|
|
+ color: #697586;
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+
|
|
|
+ strong {
|
|
|
+ margin-top: 8px;
|
|
|
+ color: #111827;
|
|
|
+ font-size: 28px;
|
|
|
+ line-height: 1;
|
|
|
+ font-weight: 760;
|
|
|
+ font-variant-numeric: tabular-nums;
|
|
|
+ }
|
|
|
+
|
|
|
+ em {
|
|
|
+ margin-top: 9px;
|
|
|
+ color: #7c8798;
|
|
|
+ font-size: 12px;
|
|
|
+ font-style: normal;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// 统计卡片行
|
|
|
-.stat-row {
|
|
|
- margin-bottom: 0 !important;
|
|
|
+.main-grid,
|
|
|
+.bottom-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: minmax(0, 1.7fr) minmax(320px, 0.8fr);
|
|
|
+ gap: 18px;
|
|
|
}
|
|
|
|
|
|
-.stat-card {
|
|
|
- background: #fff;
|
|
|
- border-radius: 12px;
|
|
|
- padding: 24px;
|
|
|
+.bottom-grid {
|
|
|
+ grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
+}
|
|
|
+
|
|
|
+.panel {
|
|
|
+ padding: 20px;
|
|
|
+ border-radius: 8px;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.panel-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 14px;
|
|
|
+ margin-bottom: 18px;
|
|
|
+
|
|
|
+ h2 {
|
|
|
+ margin: 0;
|
|
|
+ color: #172033;
|
|
|
+ font-size: 17px;
|
|
|
+ font-weight: 700;
|
|
|
+ letter-spacing: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ p {
|
|
|
+ margin-top: 6px;
|
|
|
+ color: #7c8798;
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+
|
|
|
+ > .el-icon {
|
|
|
+ color: #0f9f8f;
|
|
|
+ font-size: 22px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.distribution-layout {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: minmax(0, 1fr) 230px;
|
|
|
+ gap: 18px;
|
|
|
+ align-items: stretch;
|
|
|
+}
|
|
|
+
|
|
|
+.province-map {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(10, minmax(44px, 1fr));
|
|
|
+ grid-template-rows: repeat(8, 52px);
|
|
|
+ gap: 6px;
|
|
|
+ min-height: 450px;
|
|
|
+ padding: 18px;
|
|
|
+ border-radius: 8px;
|
|
|
+ background:
|
|
|
+ linear-gradient(135deg, rgba(15, 159, 143, 0.08), rgba(47, 128, 237, 0.08)),
|
|
|
+ #f7fafc;
|
|
|
+}
|
|
|
+
|
|
|
+.province-tile {
|
|
|
display: flex;
|
|
|
+ flex-direction: column;
|
|
|
align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ min-width: 0;
|
|
|
+ border-radius: 7px;
|
|
|
+ border: 1px solid rgba(15, 159, 143, 0.14);
|
|
|
+ color: #1f2933;
|
|
|
+ text-align: center;
|
|
|
+
|
|
|
+ span {
|
|
|
+ max-width: 100%;
|
|
|
+ overflow: hidden;
|
|
|
+ color: inherit;
|
|
|
+ font-size: 12px;
|
|
|
+ line-height: 1.2;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ strong {
|
|
|
+ margin-top: 4px;
|
|
|
+ font-size: 16px;
|
|
|
+ line-height: 1;
|
|
|
+ font-variant-numeric: tabular-nums;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.heat-0 {
|
|
|
+ color: #98a2b3;
|
|
|
+ background: #eef2f6;
|
|
|
+}
|
|
|
+
|
|
|
+.heat-1 {
|
|
|
+ background: #d8f3ed;
|
|
|
+}
|
|
|
+
|
|
|
+.heat-2 {
|
|
|
+ color: #0f433b;
|
|
|
+ background: #86dbc9;
|
|
|
+}
|
|
|
+
|
|
|
+.heat-3 {
|
|
|
+ color: #fff;
|
|
|
+ background: #0f9f8f;
|
|
|
+ box-shadow: 0 8px 20px rgba(15, 159, 143, 0.22);
|
|
|
+}
|
|
|
+
|
|
|
+.province-rank {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
gap: 16px;
|
|
|
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
|
- transition: box-shadow 0.2s;
|
|
|
+}
|
|
|
+
|
|
|
+.legend {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: auto 1fr auto 1fr auto 1fr;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px;
|
|
|
+ color: #697586;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.legend-item {
|
|
|
+ width: 18px;
|
|
|
+ height: 10px;
|
|
|
+ border-radius: 999px;
|
|
|
+}
|
|
|
+
|
|
|
+.level-1 {
|
|
|
+ background: #d8f3ed;
|
|
|
+}
|
|
|
+
|
|
|
+.level-2 {
|
|
|
+ background: #86dbc9;
|
|
|
+}
|
|
|
+
|
|
|
+.level-3 {
|
|
|
+ background: #0f9f8f;
|
|
|
+}
|
|
|
+
|
|
|
+.rank-list {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.rank-item {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 42px 1fr 34px;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+ color: #4b5563;
|
|
|
+ font-size: 13px;
|
|
|
+
|
|
|
+ strong {
|
|
|
+ color: #172033;
|
|
|
+ text-align: right;
|
|
|
+ font-variant-numeric: tabular-nums;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.rank-track {
|
|
|
+ height: 8px;
|
|
|
+ overflow: hidden;
|
|
|
+ border-radius: 999px;
|
|
|
+ background: #eef2f6;
|
|
|
|
|
|
- &:hover {
|
|
|
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
|
|
+ i {
|
|
|
+ display: block;
|
|
|
+ height: 100%;
|
|
|
+ border-radius: inherit;
|
|
|
+ background: linear-gradient(90deg, #0f9f8f, #2f80ed);
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
- .stat-icon {
|
|
|
- width: 52px;
|
|
|
- height: 52px;
|
|
|
- border-radius: 12px;
|
|
|
- background: color-mix(in srgb, var(--card-color) 15%, transparent);
|
|
|
+.donut-wrap {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ gap: 24px;
|
|
|
+ padding-top: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.donut-chart {
|
|
|
+ width: 186px;
|
|
|
+ height: 186px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ border-radius: 50%;
|
|
|
+ background:
|
|
|
+ radial-gradient(circle at center, #fff 0 56%, transparent 57%),
|
|
|
+ conic-gradient(#2f80ed 0 100%, #e5e7eb 0);
|
|
|
+ box-shadow: inset 0 0 0 1px rgba(47, 128, 237, 0.08);
|
|
|
+
|
|
|
+ span {
|
|
|
+ color: #172033;
|
|
|
+ font-size: 34px;
|
|
|
+ font-weight: 760;
|
|
|
+ line-height: 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ small {
|
|
|
+ margin-top: 8px;
|
|
|
+ color: #697586;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.device-summary {
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.device-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ color: #344054;
|
|
|
+
|
|
|
+ span {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
- justify-content: center;
|
|
|
- flex-shrink: 0;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
|
|
|
- .el-icon {
|
|
|
- font-size: 24px;
|
|
|
- color: var(--card-color);
|
|
|
+ strong {
|
|
|
+ color: #172033;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.muted {
|
|
|
+ margin-top: 16px;
|
|
|
+ color: #98a2b3;
|
|
|
+
|
|
|
+ strong {
|
|
|
+ color: #98a2b3;
|
|
|
}
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
- .stat-value {
|
|
|
- font-size: 24px;
|
|
|
- font-weight: 600;
|
|
|
- color: #262626;
|
|
|
+.dot {
|
|
|
+ width: 9px;
|
|
|
+ height: 9px;
|
|
|
+ border-radius: 50%;
|
|
|
+}
|
|
|
+
|
|
|
+.dot.primary {
|
|
|
+ background: #2f80ed;
|
|
|
+}
|
|
|
+
|
|
|
+.dot.empty {
|
|
|
+ background: #d0d5dd;
|
|
|
+}
|
|
|
+
|
|
|
+.panel-note {
|
|
|
+ margin-top: 16px;
|
|
|
+ padding: 12px;
|
|
|
+ border-radius: 7px;
|
|
|
+ background: #f8fafc;
|
|
|
+ color: #697586;
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+
|
|
|
+.bar-chart {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(7, minmax(56px, 1fr));
|
|
|
+ gap: 18px;
|
|
|
+ height: 300px;
|
|
|
+ padding: 18px 10px 8px;
|
|
|
+ border-radius: 8px;
|
|
|
+ background:
|
|
|
+ repeating-linear-gradient(to top, #edf0f5 0, #edf0f5 1px, transparent 1px, transparent 58px),
|
|
|
+ #fbfcfe;
|
|
|
+}
|
|
|
+
|
|
|
+.bar-group {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: flex-end;
|
|
|
+ min-width: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.bar-stack {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-end;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 8px;
|
|
|
+ width: 100%;
|
|
|
+ height: 240px;
|
|
|
+}
|
|
|
+
|
|
|
+.bar {
|
|
|
+ width: 18px;
|
|
|
+ min-height: 16px;
|
|
|
+ border-radius: 8px 8px 3px 3px;
|
|
|
+}
|
|
|
+
|
|
|
+.bar.communicate {
|
|
|
+ background: linear-gradient(180deg, #2f80ed, #74aef7);
|
|
|
+}
|
|
|
+
|
|
|
+.bar.plan {
|
|
|
+ background: linear-gradient(180deg, #c05621, #f6ad55);
|
|
|
+}
|
|
|
+
|
|
|
+.bar-label {
|
|
|
+ margin-top: 12px;
|
|
|
+ color: #697586;
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-legend {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 24px;
|
|
|
+ margin-top: 14px;
|
|
|
+ color: #697586;
|
|
|
+ font-size: 13px;
|
|
|
+
|
|
|
+ span {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.bar-dot {
|
|
|
+ width: 10px;
|
|
|
+ height: 10px;
|
|
|
+ border-radius: 50%;
|
|
|
+}
|
|
|
+
|
|
|
+.bar-dot.communicate {
|
|
|
+ background: #2f80ed;
|
|
|
+}
|
|
|
+
|
|
|
+.bar-dot.plan {
|
|
|
+ background: #c05621;
|
|
|
+}
|
|
|
+
|
|
|
+.work-list {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.work-item {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 44px minmax(0, 1fr) auto;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ padding: 14px;
|
|
|
+ border-radius: 8px;
|
|
|
+ background: #f8fafc;
|
|
|
+}
|
|
|
+
|
|
|
+.work-icon {
|
|
|
+ width: 44px;
|
|
|
+ height: 44px;
|
|
|
+ display: grid;
|
|
|
+ place-items: center;
|
|
|
+ border-radius: 8px;
|
|
|
+ color: #fff;
|
|
|
+ background: #2f80ed;
|
|
|
+
|
|
|
+ .el-icon {
|
|
|
+ font-size: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.todo {
|
|
|
+ background: #0f9f8f;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.critical {
|
|
|
+ background: #d92d20;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.warning {
|
|
|
+ background: #c05621;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.offline {
|
|
|
+ background: #667085;
|
|
|
}
|
|
|
+}
|
|
|
+
|
|
|
+.work-content {
|
|
|
+ min-width: 0;
|
|
|
|
|
|
- .stat-title {
|
|
|
+ p {
|
|
|
+ margin-top: 6px;
|
|
|
+ overflow: hidden;
|
|
|
+ color: #7c8798;
|
|
|
font-size: 13px;
|
|
|
- color: #8c8c8c;
|
|
|
- margin-top: 2px;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// 信息卡片
|
|
|
-.info-card {
|
|
|
- border-radius: 12px;
|
|
|
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
|
+.work-title {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ min-width: 0;
|
|
|
|
|
|
- :deep(.el-card__header) {
|
|
|
- font-weight: 600;
|
|
|
- color: #262626;
|
|
|
+ strong {
|
|
|
+ overflow: hidden;
|
|
|
+ color: #172033;
|
|
|
+ font-size: 14px;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-.tech-item {
|
|
|
- padding: 16px;
|
|
|
- border-radius: 8px;
|
|
|
- background: #f8f9fb;
|
|
|
- margin-bottom: 16px;
|
|
|
- border-left: 3px solid #1890ff;
|
|
|
+.work-count {
|
|
|
+ min-width: 48px;
|
|
|
+ text-align: right;
|
|
|
|
|
|
- .tech-name {
|
|
|
- font-weight: 600;
|
|
|
- color: #262626;
|
|
|
- margin-bottom: 4px;
|
|
|
+ strong {
|
|
|
+ display: block;
|
|
|
+ color: #172033;
|
|
|
+ font-size: 22px;
|
|
|
+ line-height: 1;
|
|
|
+ font-variant-numeric: tabular-nums;
|
|
|
}
|
|
|
|
|
|
- .tech-version {
|
|
|
+ span {
|
|
|
+ color: #98a2b3;
|
|
|
font-size: 12px;
|
|
|
- color: #1890ff;
|
|
|
- margin-bottom: 6px;
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
- .tech-desc {
|
|
|
- font-size: 12px;
|
|
|
- color: #8c8c8c;
|
|
|
+.alarm-list time {
|
|
|
+ color: #7c8798;
|
|
|
+ font-size: 12px;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 1400px) {
|
|
|
+ .metric-grid {
|
|
|
+ grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
+ }
|
|
|
+
|
|
|
+ .main-grid,
|
|
|
+ .bottom-grid {
|
|
|
+ grid-template-columns: 1fr;
|
|
|
+ }
|
|
|
+
|
|
|
+ .distribution-layout {
|
|
|
+ grid-template-columns: 1fr;
|
|
|
+ }
|
|
|
+
|
|
|
+ .province-rank {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1fr 2fr;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 900px) {
|
|
|
+ .overview-strip {
|
|
|
+ align-items: flex-start;
|
|
|
+ flex-direction: column;
|
|
|
+ }
|
|
|
+
|
|
|
+ .overview-actions {
|
|
|
+ width: 100%;
|
|
|
+ justify-content: space-between;
|
|
|
+ }
|
|
|
+
|
|
|
+ .metric-grid {
|
|
|
+ grid-template-columns: 1fr;
|
|
|
+ }
|
|
|
+
|
|
|
+ .province-map {
|
|
|
+ grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
|
+ grid-template-rows: none;
|
|
|
+ min-height: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .province-tile {
|
|
|
+ grid-column: auto !important;
|
|
|
+ grid-row: auto !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ .province-rank {
|
|
|
+ grid-template-columns: 1fr;
|
|
|
+ }
|
|
|
+
|
|
|
+ .bar-chart {
|
|
|
+ grid-template-columns: repeat(4, minmax(56px, 1fr));
|
|
|
+ height: auto;
|
|
|
+ overflow-x: auto;
|
|
|
+ }
|
|
|
+
|
|
|
+ .bar-stack {
|
|
|
+ height: 200px;
|
|
|
}
|
|
|
}
|
|
|
</style>
|