| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750 |
- <template>
- <div class="app-container home">
- <!-- 欢迎标题区 -->
- <div class="welcome-header mb-[20px]">
- <h2 class="text-xl font-bold text-gray-800">欢迎回来,管理员</h2>
- <p class="text-sm text-gray-500 mt-1">今日是,今天也是安全的一天</p>
- </div>
- <!-- 今日数据卡片 -->
- <el-row :gutter="16" class="mb-[20px]">
- <el-col :xs="24" :sm="12" :lg="6">
- <div class="stat-card stat-card-blue" @click="goToHazard()">
- <div class="stat-card-content">
- <div class="stat-info">
- <p class="stat-label">安全隐患总数</p>
- <p class="stat-value">{{ dashboardData.hazardTotal }}</p>
- <p class="stat-trend">
- <span class="trend-up">↑ 较前日 +3.2%</span>
- </p>
- </div>
- <div class="stat-icon">
- <svg viewBox="0 0 24 24" width="32" height="32">
- <path fill="currentColor" d="M12 2L2 7v6c0 5.5 3.8 10.7 9 12 5.2-1.3 9-6.5 9-12V7l-10-5z"/>
- </svg>
- </div>
- </div>
- </div>
- </el-col>
- <el-col :xs="24" :sm="12" :lg="6">
- <div class="stat-card stat-card-green" @click="goToHazard('已处理')">
- <div class="stat-card-content">
- <div class="stat-info">
- <p class="stat-label">已处理隐患</p>
- <p class="stat-value">{{ dashboardData.hazardProcessed }}</p>
- <p class="stat-trend">
- <span class="trend-normal">↑ 处理率 75%</span>
- </p>
- </div>
- <div class="stat-icon">
- <svg viewBox="0 0 24 24" width="32" height="32">
- <path fill="currentColor" d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
- </svg>
- </div>
- </div>
- </div>
- </el-col>
- <el-col :xs="24" :sm="12" :lg="6">
- <div class="stat-card stat-card-orange" @click="goToHazard('待处理')">
- <div class="stat-card-content">
- <div class="stat-info">
- <p class="stat-label">待处理隐患</p>
- <p class="stat-value">{{ dashboardData.hazardPending }}</p>
- <p class="stat-trend">
- <span class="trend-warn">需尽快处理</span>
- </p>
- </div>
- <div class="stat-icon">
- <svg viewBox="0 0 24 24" width="32" height="32">
- <path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
- </svg>
- </div>
- </div>
- </div>
- </el-col>
- <el-col :xs="24" :sm="12" :lg="6">
- <div class="stat-card stat-card-purple" @click="goToEquipment()">
- <div class="stat-card-content">
- <div class="stat-info">
- <p class="stat-label">特种设备数量</p>
- <p class="stat-value">{{ dashboardData.equipmentTotal }}</p>
- <p class="stat-trend">
- <span class="trend-normal">✓ 全部检验合格</span>
- </p>
- </div>
- <div class="stat-icon">
- <svg viewBox="0 0 24 24" width="32" height="32">
- <path fill="currentColor" d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/>
- </svg>
- </div>
- </div>
- </div>
- </el-col>
- </el-row>
- <!-- 图表区域 -->
- <el-row :gutter="16" class="mb-[20px]">
- <el-col :xs="24" :lg="16">
- <el-card shadow="never" class="chart-card">
- <template #header>
- <div class="chart-header">
- <span class="chart-title">月度隐患处理趋势</span>
- </div>
- </template>
- <div ref="trendChartRef" style="width: 100%; height: 320px;"></div>
- </el-card>
- </el-col>
- <el-col :xs="24" :lg="8">
- <el-card shadow="never" class="chart-card">
- <template #header>
- <div class="chart-header">
- <span class="chart-title">隐患类型分布</span>
- </div>
- </template>
- <div ref="pieChartRef" style="width: 100%; height: 320px;"></div>
- </el-card>
- </el-col>
- </el-row>
- <!-- 最新安全隐患 -->
- <el-row class="mb-[20px]">
- <el-col :span="24">
- <el-card shadow="never" class="list-card">
- <template #header>
- <div class="list-header">
- <span class="list-title">最新安全隐患</span>
- <a class="view-all" @click="goToHazard()">查看全部→</a>
- </div>
- </template>
- <el-table :data="latestHazards" stripe style="width: 100%" :show-overflow-tooltip="true">
- <el-table-column prop="hazardNo" label="隐患编号" min-width="120" />
- <el-table-column prop="hazardLocation" label="隐患位置" min-width="140" />
- <el-table-column prop="hazardType" label="隐患类型" min-width="120" />
- <el-table-column prop="riskLevel" label="风险等级" min-width="100">
- <template #default="scope">
- <el-tag :type="getRiskLevelType(scope.row.riskLevel)" size="small">{{ scope.row.riskLevel }}</el-tag>
- </template>
- </el-table-column>
- <el-table-column prop="discoverTime" label="上报时间" min-width="120" />
- <el-table-column prop="processStatus" label="状态" min-width="100">
- <template #default="scope">
- <el-tag :type="getStatusType(scope.row.processStatus)" size="small">{{ scope.row.processStatus }}</el-tag>
- </template>
- </el-table-column>
- </el-table>
- </el-card>
- </el-col>
- </el-row>
- <!-- 底部列表 -->
- <el-row :gutter="16">
- <el-col :xs="24" :lg="8">
- <el-card shadow="never" class="list-card">
- <template #header>
- <div class="list-header">
- <span class="list-title">近期安全培训</span>
- <a class="view-all" @click="goToTraining()">查看全部→</a>
- </div>
- </template>
- <div class="training-list">
- <div v-for="(item, index) in trainingList" :key="index" class="training-item" @click="goToTraining()">
- <div class="training-info">
- <p class="training-title">{{ item.trainingTitle }}</p>
- <p class="training-time">{{ item.trainingTime }}</p>
- </div>
- </div>
- <div v-if="!trainingList.length" class="empty-hint">暂无培训安排</div>
- </div>
- </el-card>
- </el-col>
- <el-col :xs="24" :lg="16">
- <el-card shadow="never" class="list-card">
- <template #header>
- <div class="list-header">
- <span class="list-title">最新公告</span>
- <a class="view-all" @click="goToNotice()">查看全部→</a>
- </div>
- </template>
- <div class="notice-list">
- <div v-for="(item, index) in noticeList" :key="index" class="notice-item" @click="goToNotice()">
- <div class="notice-content">
- <p class="notice-title">{{ item.noticeTitle }}</p>
- <p class="notice-desc">{{ item.noticeContent }}</p>
- </div>
- <span class="notice-date">{{ item.createTime }}</span>
- </div>
- <div v-if="!noticeList.length" class="empty-hint">暂无公告</div>
- </div>
- </el-card>
- </el-col>
- </el-row>
- </div>
- </template>
- <script setup name="Dashboard" lang="ts">
- import { ref, reactive, onMounted } from 'vue';
- import { useRouter } from 'vue-router';
- import * as echarts from 'echarts';
- import { getHazardStats, getHazardTrend, getHazardDistribution, getHazardLatest } from '@/api/riskctrl/hazard';
- import { getEquipmentStats } from '@/api/riskctrl/specialEquipment';
- import { getLatestTrainings } from '@/api/riskctrl/safetyTraining';
- import { getPublishedNotices } from '@/api/riskctrl/noticeAnnouncement';
- import type { HazardVO } from '@/api/riskctrl/hazard/types';
- const router = useRouter();
- // 图表引用
- const trendChartRef = ref<HTMLDivElement>();
- const pieChartRef = ref<HTMLDivElement>();
- let trendChart: echarts.ECharts | null = null;
- let pieChart: echarts.ECharts | null = null;
- // 数据(与截图保持一致)
- const dashboardData = reactive({
- hazardTotal: 128,
- hazardProcessed: 96,
- hazardPending: 32,
- equipmentTotal: 47
- });
- // 月度隐患处理趋势数据(与截图保持一致)
- const trendData = ref<any[]>([
- { month: '1月', total: 10, closed: 10 },
- { month: '2月', total: 15, closed: 12 },
- { month: '3月', total: 18, closed: 14 },
- { month: '4月', total: 13, closed: 12 },
- { month: '5月', total: 22, closed: 18 },
- { month: '6月', total: 18, closed: 15 }
- ]);
- // 隐患类型分布数据(与截图保持一致)
- const pieData = ref<any[]>([
- { type: '电气安全', count: 35 },
- { type: '消防隐患', count: 25 },
- { type: '设备安全', count: 20 },
- { type: '环境安全', count: 18 },
- { type: '人员操作', count: 30 }
- ]);
- const latestHazards = ref<HazardVO[]>([]);
- // 近期安全培训
- const trainingList = ref<Array<{ trainingTitle: string; trainingTime: string; trainingId: string | number }>>([]);
- // 最新公告
- const noticeList = ref<Array<{ noticeTitle: string; noticeContent: string; createTime: string; noticeId: string | number }>>([]);
- // 风险等级标签样式
- const getRiskLevelType = (level: string) => {
- const map: Record<string, string> = {
- '重大': 'danger',
- '较大': 'warning',
- '一般': ''
- };
- return map[level] || 'info';
- };
- // 状态标签样式
- const getStatusType = (status: string) => {
- const map: Record<string, string> = {
- '待处理': 'danger',
- '处理中': 'warning',
- '已处理': 'success'
- };
- return map[status] || 'info';
- };
- // 加载统计数据
- const loadStats = async () => {
- try {
- const [hazardRes, equipRes] = await Promise.all([
- getHazardStats(),
- getEquipmentStats()
- ]);
- const hdata = hazardRes.data as any;
- if (hdata.total) dashboardData.hazardTotal = hdata.total;
- if (hdata.processed) dashboardData.hazardProcessed = hdata.processed;
- if (hdata.unprocessed) dashboardData.hazardPending = hdata.unprocessed;
- if ((equipRes.data as any)?.total) dashboardData.equipmentTotal = (equipRes.data as any).total;
- } catch (e) {
- console.error('加载统计失败', e);
- }
- };
- // 加载最新隐患
- const loadLatestHazards = async () => {
- try {
- const res = await getHazardLatest(5);
- latestHazards.value = res.data || [];
- } catch (e) {
- console.error('加载最新隐患失败', e);
- }
- };
- // 加载趋势
- const loadTrend = async () => {
- try {
- const res = await getHazardTrend(6);
- trendData.value = res.data || [];
- initTrendChart();
- } catch (e) {
- console.error('加载趋势失败', e);
- }
- };
- // 加载分布
- const loadDistribution = async () => {
- try {
- const res = await getHazardDistribution();
- pieData.value = res.data || [];
- initPieChart();
- } catch (e) {
- console.error('加载分布失败', e);
- }
- };
- // 初始化趋势图
- const initTrendChart = () => {
- if (!trendChartRef.value) return;
- if (trendChart) trendChart.dispose();
- trendChart = echarts.init(trendChartRef.value);
- const months = trendData.value.map((item: any) => item.month);
- const totals = trendData.value.map((item: any) => item.total || 0);
- const closeds = trendData.value.map((item: any) => item.closed || 0);
- const option: echarts.EChartsOption = {
- tooltip: {
- trigger: 'axis',
- axisPointer: { type: 'cross' }
- },
- legend: {
- data: ['上报隐患', '已处理'],
- bottom: 0,
- icon: 'circle',
- itemWidth: 8,
- itemHeight: 8
- },
- grid: {
- left: '3%',
- right: '4%',
- bottom: '15%',
- top: '10%',
- containLabel: true
- },
- xAxis: {
- type: 'category',
- boundaryGap: false,
- data: months,
- axisLine: { lineStyle: { color: '#e0e0e0' } },
- axisLabel: { color: '#666', fontSize: 12 }
- },
- yAxis: {
- type: 'value',
- axisLine: { show: false },
- splitLine: { lineStyle: { color: '#f0f0f0' } },
- axisLabel: { color: '#666', fontSize: 12 }
- },
- series: [
- {
- name: '上报隐患',
- type: 'line',
- smooth: true,
- data: totals,
- itemStyle: { color: '#3b82f6' },
- lineStyle: { width: 2 },
- symbol: 'circle',
- symbolSize: 6
- },
- {
- name: '已处理',
- type: 'line',
- smooth: true,
- data: closeds,
- itemStyle: { color: '#22c55e' },
- lineStyle: { width: 2 },
- symbol: 'circle',
- symbolSize: 6
- }
- ]
- };
- trendChart.setOption(option);
- };
- // 初始化饼图
- const initPieChart = () => {
- if (!pieChartRef.value) return;
- if (pieChart) pieChart.dispose();
- pieChart = echarts.init(pieChartRef.value);
- const data = pieData.value.map((item: any) => ({
- name: item.type,
- value: item.count
- }));
- const colors = ['#3b82f6', '#ef4444', '#f59e0b', '#22c55e', '#8b5cf6'];
- const option: echarts.EChartsOption = {
- tooltip: {
- trigger: 'item',
- formatter: '{b}: {c} ({d}%)'
- },
- legend: {
- orient: 'vertical',
- right: '5%',
- top: 'center',
- itemWidth: 10,
- itemHeight: 10,
- textStyle: { fontSize: 12, color: '#666' },
- data: ['电气安全', '消防隐患', '设备安全', '环境安全', '人员操作']
- },
- color: colors,
- series: [
- {
- name: '隐患类型',
- type: 'pie',
- radius: ['45%', '70%'],
- center: ['35%', '50%'],
- avoidLabelOverlap: false,
- itemStyle: {
- borderRadius: 6,
- borderColor: '#fff',
- borderWidth: 2
- },
- label: {
- show: false
- },
- emphasis: {
- label: {
- show: true,
- fontSize: 14,
- fontWeight: 'bold'
- }
- },
- labelLine: {
- show: false
- },
- data: data.length ? data : [{ name: '暂无数据', value: 0 }]
- }
- ]
- };
- pieChart.setOption(option);
- };
- // 路由跳转
- const goToHazard = (status?: string) => {
- const query: any = {};
- if (status === '已处理') query.processStatus = '已处理';
- if (status === '待处理') query.processStatus = '待处理';
- router.push({ path: '/riskctrl/hazard', query });
- };
- const goToEquipment = () => {
- router.push('/riskctrl/specialEquipment');
- };
- const goToTraining = () => {
- router.push('/riskctrl/safetyTraining');
- };
- const goToNotice = () => {
- router.push('/riskctrl/noticeAnnouncement');
- };
- // 加载培训和公告数据
- const loadTrainingsAndNotices = async () => {
- try {
- const [trainingRes, noticeRes] = await Promise.all([
- getLatestTrainings(3),
- getPublishedNotices(3)
- ]);
- if (trainingRes.data) {
- trainingList.value = (trainingRes.data as any) || [];
- }
- if (noticeRes.data) {
- noticeList.value = (noticeRes.data as any) || [];
- }
- } catch (e) {
- console.error('加载培训/公告失败', e);
- }
- };
- // 窗口大小变化时重绘图表
- const handleResize = () => {
- trendChart?.resize();
- pieChart?.resize();
- };
- onMounted(() => {
- // 先初始化图表(使用静态数据)
- setTimeout(() => {
- initTrendChart();
- initPieChart();
- }, 100);
-
- // 尝试从后端加载数据(失败则保持静态数据)
- loadStats();
- loadLatestHazards();
- loadTrend();
- loadDistribution();
- loadTrainingsAndNotices();
- window.addEventListener('resize', handleResize);
- });
- </script>
- <style lang="scss" scoped>
- .home {
- padding: 20px;
- background-color: #f5f7fb;
- min-height: 100%;
- }
- .welcome-header {
- h2 {
- font-size: 20px;
- font-weight: 600;
- color: #1f2937;
- }
- p {
- font-size: 13px;
- color: #9ca3af;
- }
- }
- .stat-card {
- background: #fff;
- border-radius: 12px;
- padding: 20px;
- cursor: pointer;
- transition: all 0.3s ease;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
- &:hover {
- transform: translateY(-2px);
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
- }
- .stat-card-content {
- display: flex;
- align-items: center;
- justify-content: space-between;
- }
- .stat-info {
- flex: 1;
- }
- .stat-label {
- font-size: 13px;
- color: #6b7280;
- margin-bottom: 8px;
- }
- .stat-value {
- font-size: 28px;
- font-weight: 700;
- color: #1f2937;
- line-height: 1.2;
- margin-bottom: 6px;
- }
- .stat-trend {
- font-size: 12px;
- .trend-up {
- color: #22c55e;
- }
- .trend-normal {
- color: #22c55e;
- }
- .trend-warn {
- color: #f59e0b;
- }
- }
- .stat-icon {
- width: 48px;
- height: 48px;
- border-radius: 10px;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- }
- &.stat-card-blue .stat-icon {
- background-color: #eff6ff;
- color: #3b82f6;
- }
- &.stat-card-green .stat-icon {
- background-color: #f0fdf4;
- color: #22c55e;
- }
- &.stat-card-orange .stat-icon {
- background-color: #fffbeb;
- color: #f59e0b;
- }
- &.stat-card-purple .stat-icon {
- background-color: #faf5ff;
- color: #8b5cf6;
- }
- }
- .chart-card {
- border-radius: 12px;
- border: none;
- :deep(.el-card__header) {
- padding: 16px 20px;
- border-bottom: 1px solid #f3f4f6;
- }
- :deep(.el-card__body) {
- padding: 16px 20px;
- }
- .chart-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- }
- .chart-title {
- font-size: 15px;
- font-weight: 600;
- color: #1f2937;
- }
- }
- .list-card {
- border-radius: 12px;
- border: none;
- :deep(.el-card__header) {
- padding: 16px 20px;
- border-bottom: 1px solid #f3f4f6;
- }
- :deep(.el-card__body) {
- padding: 0;
- }
- .list-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- }
- .list-title {
- font-size: 15px;
- font-weight: 600;
- color: #1f2937;
- }
- .view-all {
- font-size: 13px;
- color: #3b82f6;
- cursor: pointer;
- &:hover {
- color: #2563eb;
- }
- }
- }
- .training-list {
- padding: 12px 20px;
- .training-item {
- padding: 12px 0;
- border-bottom: 1px solid #f3f4f6;
- cursor: pointer;
- &:last-child {
- border-bottom: none;
- }
- &:hover {
- .training-title {
- color: #3b82f6;
- }
- }
- .training-title {
- font-size: 14px;
- color: #374151;
- margin-bottom: 4px;
- transition: color 0.2s;
- }
- .training-time {
- font-size: 12px;
- color: #9ca3af;
- }
- }
- }
- .notice-list {
- padding: 12px 20px;
- .notice-item {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- padding: 12px 0;
- border-bottom: 1px solid #f3f4f6;
- cursor: pointer;
- &:last-child {
- border-bottom: none;
- }
- &:hover {
- .notice-title {
- color: #3b82f6;
- }
- }
- .notice-content {
- flex: 1;
- margin-right: 16px;
- }
- .notice-title {
- font-size: 14px;
- font-weight: 500;
- color: #374151;
- margin-bottom: 4px;
- transition: color 0.2s;
- }
- .notice-desc {
- font-size: 12px;
- color: #9ca3af;
- line-height: 1.5;
- overflow: hidden;
- text-overflow: ellipsis;
- display: -webkit-box;
- -webkit-line-clamp: 1;
- -webkit-box-orient: vertical;
- }
- .notice-date {
- font-size: 12px;
- color: #9ca3af;
- white-space: nowrap;
- flex-shrink: 0;
- }
- }
- }
- .empty-hint {
- text-align: center;
- padding: 20px;
- color: #9ca3af;
- font-size: 13px;
- }
- </style>
|