<template>
|
<div class="app-container">
|
<el-row :gutter="20" class="stat-cards">
|
<el-col :span="6">
|
<el-card>
|
<div class="stat-item">
|
<div class="stat-icon" style="background: #67c23a;"><el-icon><CircleCheck /></el-icon></div>
|
<div class="stat-info">
|
<span class="stat-value">{{ todayStats.completionRate }}%</span>
|
<span class="stat-label">当日巡检完成率</span>
|
</div>
|
</div>
|
</el-card>
|
</el-col>
|
<el-col :span="6">
|
<el-card>
|
<div class="stat-item">
|
<div class="stat-icon" style="background: #409eff;"><el-icon><User /></el-icon></div>
|
<div class="stat-info">
|
<span class="stat-value">{{ todayStats.inspectorCount }}</span>
|
<span class="stat-label">巡检人员数</span>
|
</div>
|
</div>
|
</el-card>
|
</el-col>
|
<el-col :span="6">
|
<el-card>
|
<div class="stat-item">
|
<div class="stat-icon" style="background: #e6a23c;"><el-icon><Warning /></el-icon></div>
|
<div class="stat-info">
|
<span class="stat-value">{{ todayStats.abnormalCount }}</span>
|
<span class="stat-label">异常记录数</span>
|
</div>
|
</div>
|
</el-card>
|
</el-col>
|
<el-col :span="6">
|
<el-card>
|
<div class="stat-item">
|
<div class="stat-icon" style="background: #f56c6c;"><el-icon><CircleClose /></el-icon></div>
|
<div class="stat-info">
|
<span class="stat-value">{{ todayStats.missedCount }}</span>
|
<span class="stat-label">漏检记录数</span>
|
</div>
|
</div>
|
</el-card>
|
</el-col>
|
</el-row>
|
|
<el-row :gutter="20" class="chart-row">
|
<el-col :span="12">
|
<el-card>
|
<template #header>
|
<span>巡检任务执行趋势</span>
|
</template>
|
<div ref="trendChartRef" style="height: 300px;"></div>
|
</el-card>
|
</el-col>
|
<el-col :span="12">
|
<el-card>
|
<template #header>
|
<span>巡检类型与频率分布</span>
|
</template>
|
<div ref="typeChartRef" style="height: 300px;"></div>
|
</el-card>
|
</el-col>
|
</el-row>
|
|
<el-card class="table-card">
|
<template #header>
|
<span>巡检记录明细</span>
|
</template>
|
<el-form :model="filters" :inline="true">
|
<el-form-item label="巡检日期">
|
<el-date-picker v-model="filters.dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD" />
|
</el-form-item>
|
<el-form-item label="巡检人员">
|
<el-input v-model="filters.inspector" placeholder="请输入巡检人员" clearable style="width: 200px" />
|
</el-form-item>
|
<el-form-item label="状态">
|
<el-select v-model="filters.status" placeholder="请选择" clearable style="width: 150px">
|
<el-option label="正常" :value="0" />
|
<el-option label="异常" :value="1" />
|
<el-option label="漏检" :value="2" />
|
<el-option label="未执行" :value="3" />
|
</el-select>
|
</el-form-item>
|
<el-form-item>
|
<el-button type="primary" @click="getData">搜索</el-button>
|
<el-button @click="resetFilters">重置</el-button>
|
<el-button type="success" @click="handleSyncData" icon="Refresh">同步数据</el-button>
|
</el-form-item>
|
</el-form>
|
<PIMTable :column="columns" :tableData="dataList" :page="pagination" @pagination="changePage" :tableLoading="tableLoading" />
|
</el-card>
|
</div>
|
</template>
|
|
<script setup>
|
import { ref, reactive, onMounted, nextTick } from "vue";
|
import PIMTable from "@/components/PIMTable/PIMTable.vue";
|
import * as echarts from "echarts";
|
import {
|
getInspectionRecordList,
|
getTodayStatistics,
|
getTrendStatistics,
|
getTypeStatistics,
|
syncInspectionData
|
} from "@/api/safetyManagement/inspectionReport.js";
|
import { ElMessage } from "element-plus";
|
import dayjs from "dayjs";
|
|
defineOptions({
|
name: "巡检数据报表",
|
});
|
|
const todayStats = reactive({
|
completionRate: 0,
|
inspectorCount: 0,
|
abnormalCount: 0,
|
missedCount: 0,
|
});
|
|
const filters = reactive({
|
dateRange: [],
|
inspector: "",
|
status: null,
|
});
|
|
const dataList = ref([]);
|
const pagination = reactive({ current: 1, size: 10, total: 0 });
|
const tableLoading = ref(false);
|
|
const columns = [
|
{ label: "巡检时间", prop: "inspectionTime", align: "center" },
|
{ label: "巡检人员", prop: "inspector", align: "center" },
|
{ label: "巡检区域", prop: "area", align: "center" },
|
{
|
label: "巡检类型",
|
prop: "type",
|
align: "center",
|
formatData: (val) => {
|
const typeMap = {
|
'daily': '日常巡检',
|
'equipment': '设备巡检',
|
'fire': '消防巡检',
|
'night': '夜间巡检',
|
'safety': '安全巡检',
|
'environment': '环境巡检'
|
};
|
return typeMap[val] || val;
|
}
|
},
|
{
|
label: "状态",
|
prop: "status",
|
align: "center",
|
dataType: "tag",
|
formatType: (val) => {
|
if (val === 0) return 'success';
|
if (val === 1) return 'danger';
|
if (val === 2) return 'warning';
|
return 'info';
|
},
|
formatData: (val) => {
|
const statusMap = { 0: '正常', 1: '异常', 2: '漏检', 3: '未执行' };
|
return statusMap[val] || val;
|
}
|
},
|
{ label: "异常情况", prop: "abnormalDesc", align: "center" },
|
];
|
|
const trendChartRef = ref(null);
|
const typeChartRef = ref(null);
|
let trendChart = null;
|
let typeChart = null;
|
|
onMounted(() => {
|
getTodayStats();
|
getData();
|
nextTick(() => {
|
initTrendChart();
|
initTypeChart();
|
});
|
});
|
|
// 获取今日统计
|
const getTodayStats = async () => {
|
try {
|
const res = await getTodayStatistics();
|
if (res.code === 200) {
|
Object.assign(todayStats, res.data);
|
}
|
} catch (error) {
|
console.error('获取今日统计失败', error);
|
}
|
};
|
|
// 初始化趋势图表
|
const initTrendChart = async () => {
|
if (!trendChartRef.value) return;
|
|
trendChart = echarts.init(trendChartRef.value);
|
|
try {
|
const endDate = dayjs().format('YYYY-MM-DD');
|
const startDate = dayjs().subtract(6, 'day').format('YYYY-MM-DD');
|
const res = await getTrendStatistics({ startDate, endDate });
|
|
let dates = [];
|
let completedData = [];
|
let abnormalData = [];
|
let missedData = [];
|
let unexecutedData = [];
|
|
if (res.code === 200 && Array.isArray(res.data)) {
|
// 按日期排序
|
const sortedData = res.data.sort((a, b) => a.statDate.localeCompare(b.statDate));
|
dates = sortedData.map(item => dayjs(item.statDate).format('MM-DD'));
|
completedData = sortedData.map(item => item.completedCount || 0);
|
abnormalData = sortedData.map(item => item.abnormalCount || 0);
|
missedData = sortedData.map(item => item.missedCount || 0);
|
unexecutedData = sortedData.map(item => item.unexecutedCount || 0);
|
} else {
|
// 使用默认数据
|
for (let i = 6; i >= 0; i--) {
|
dates.push(dayjs().subtract(i, 'day').format('MM-DD'));
|
}
|
completedData = [2, 1, 2, 2, 1, 4, 3];
|
abnormalData = [1, 0, 0, 1, 0, 1, 0];
|
missedData = [0, 0, 1, 0, 0, 1, 0];
|
unexecutedData = [0, 1, 0, 0, 1, 0, 1];
|
}
|
|
const option = {
|
tooltip: { trigger: 'axis' },
|
xAxis: {
|
type: "category",
|
data: dates,
|
},
|
yAxis: { type: "value" },
|
series: [
|
{ name: "已完成", type: "line", data: completedData, smooth: true },
|
{ name: "异常", type: "line", data: abnormalData, smooth: true },
|
{ name: "漏检", type: "line", data: missedData, smooth: true },
|
{ name: "未执行", type: "line", data: unexecutedData, smooth: true },
|
],
|
legend: { data: ["已完成", "异常", "漏检", "未执行"], bottom: 0 },
|
grid: { left: "3%", right: "4%", bottom: "15%", containLabel: true },
|
};
|
trendChart.setOption(option);
|
} catch (error) {
|
console.error('获取趋势数据失败', error);
|
}
|
};
|
|
// 类型映射
|
const typeMap = {
|
'daily': '日常巡检',
|
'equipment': '设备巡检',
|
'fire': '消防巡检',
|
'night': '夜间巡检',
|
'safety': '安全巡检',
|
'environment': '环境巡检'
|
};
|
|
// 初始化类型图表
|
const initTypeChart = async () => {
|
if (!typeChartRef.value) return;
|
|
typeChart = echarts.init(typeChartRef.value);
|
|
try {
|
const endDate = dayjs().format('YYYY-MM-DD');
|
const startDate = dayjs().subtract(30, 'day').format('YYYY-MM-DD');
|
const res = await getTypeStatistics({ startDate, endDate });
|
|
let pieData = [];
|
|
if (res.code === 200 && Array.isArray(res.data)) {
|
pieData = res.data.map(item => ({
|
value: item.count || 0,
|
name: typeMap[item.type] || item.type
|
}));
|
} else {
|
// 使用默认数据
|
pieData = [
|
{ value: 8, name: "日常巡检" },
|
{ value: 4, name: "设备巡检" },
|
{ value: 3, name: "消防巡检" },
|
{ value: 3, name: "夜间巡检" },
|
];
|
}
|
|
const option = {
|
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
|
series: [
|
{
|
type: "pie",
|
radius: ["40%", "70%"],
|
data: pieData,
|
label: {
|
formatter: '{b}\n{c}次 ({d}%)'
|
}
|
},
|
],
|
legend: { orient: "vertical", left: "left" },
|
};
|
typeChart.setOption(option);
|
} catch (error) {
|
console.error('获取类型统计失败', error);
|
}
|
};
|
|
// 获取巡检记录列表
|
const getData = async () => {
|
tableLoading.value = true;
|
try {
|
const params = {
|
pageNum: pagination.current,
|
pageSize: pagination.size,
|
inspector: filters.inspector,
|
status: filters.status,
|
};
|
|
if (filters.dateRange && filters.dateRange.length === 2) {
|
params.startDate = filters.dateRange[0];
|
params.endDate = filters.dateRange[1];
|
}
|
|
const res = await getInspectionRecordList(params);
|
if (res.code === 200) {
|
dataList.value = res.data.rows || res.data.records || [];
|
pagination.total = res.data.total || 0;
|
}
|
} catch (error) {
|
ElMessage.error('获取巡检记录失败');
|
} finally {
|
tableLoading.value = false;
|
}
|
};
|
|
const resetFilters = () => {
|
filters.dateRange = [];
|
filters.inspector = "";
|
filters.status = null;
|
pagination.current = 1;
|
getData();
|
};
|
|
const changePage = ({ page, limit }) => {
|
pagination.current = page;
|
pagination.size = limit;
|
getData();
|
};
|
|
// 同步巡检数据
|
const handleSyncData = async () => {
|
try {
|
const res = await syncInspectionData();
|
if (res.code === 200) {
|
ElMessage.success(`同步成功,新增 ${res.data?.count || 0} 条记录`);
|
// 刷新列表和统计
|
getData();
|
getTodayStats();
|
initTrendChart();
|
initTypeChart();
|
} else {
|
ElMessage.warning(res.msg || '同步失败');
|
}
|
} catch (error) {
|
ElMessage.error(error.response?.data?.msg || '同步失败');
|
}
|
};
|
|
// 窗口大小改变时重新渲染图表
|
window.addEventListener('resize', () => {
|
trendChart && trendChart.resize();
|
typeChart && typeChart.resize();
|
});
|
</script>
|
|
<style lang="scss" scoped>
|
.stat-cards {
|
margin-bottom: 20px;
|
.stat-item {
|
display: flex;
|
align-items: center;
|
.stat-icon {
|
width: 60px;
|
height: 60px;
|
border-radius: 50%;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
color: #fff;
|
font-size: 28px;
|
margin-right: 15px;
|
}
|
.stat-info {
|
display: flex;
|
flex-direction: column;
|
.stat-value {
|
font-size: 24px;
|
font-weight: bold;
|
color: #333;
|
}
|
.stat-label {
|
font-size: 14px;
|
color: #666;
|
margin-top: 5px;
|
}
|
}
|
}
|
}
|
|
.chart-row {
|
margin-bottom: 20px;
|
}
|
|
.table-card {
|
:deep(.el-card__header) {
|
font-weight: bold;
|
}
|
}
|
</style>
|