设备维修统计报表

This commit is contained in:
“hsc”
2026-09-02 14:30:09 +08:00
parent dbb3fedb97
commit 6d888ddaee
16 changed files with 827 additions and 40 deletions
+1 -2
View File
@@ -5,7 +5,7 @@
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>设备管理系统</title>
<script type="module" crossorigin src="/assets/index-D7ykTT7f.js"></script>
<script type="module" crossorigin src="/assets/index-B0bC4XlZ.js"></script>
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
<link rel="modulepreload" crossorigin href="/assets/runtime-core.esm-bundler-DZ3WZ9y9.js">
<link rel="modulepreload" crossorigin href="/assets/message-ddUl4JIs.js">
@@ -14,6 +14,5 @@
<body>
<div id="app"></div>
<!-- Vite 入口,指向 src/main.js,不要删除 -->
</body>
</html>
File diff suppressed because one or more lines are too long
+11 -11
View File
@@ -1,49 +1,49 @@
{
"hash": "b6c330c5",
"configHash": "e57f8d4c",
"lockfileHash": "74076ca5",
"browserHash": "ed3d8378",
"hash": "a6f0f194",
"configHash": "147f2811",
"lockfileHash": "77198382",
"browserHash": "ee71f75c",
"optimized": {
"@element-plus/icons-vue": {
"src": "../../@element-plus/icons-vue/dist/index.js",
"file": "@element-plus_icons-vue.js",
"fileHash": "de153aba",
"fileHash": "9553abbf",
"needsInterop": false
},
"axios": {
"src": "../../axios/index.js",
"file": "axios.js",
"fileHash": "c19fdb3e",
"fileHash": "3e018dd2",
"needsInterop": false
},
"echarts": {
"src": "../../echarts/index.js",
"file": "echarts.js",
"fileHash": "a8d7111b",
"fileHash": "275a5510",
"needsInterop": false
},
"element-plus": {
"src": "../../element-plus/es/index.mjs",
"file": "element-plus.js",
"fileHash": "10cb780f",
"fileHash": "cc57a904",
"needsInterop": false
},
"pinia": {
"src": "../../pinia/dist/pinia.js",
"file": "pinia.js",
"fileHash": "82d3c424",
"fileHash": "0a5304b8",
"needsInterop": false
},
"vue-router": {
"src": "../../vue-router/dist/vue-router.js",
"file": "vue-router.js",
"fileHash": "1b68fa4e",
"fileHash": "0ef5b6fe",
"needsInterop": false
},
"vue": {
"src": "../../vue/dist/vue.runtime.esm-bundler.js",
"file": "vue.js",
"fileHash": "7e296374",
"fileHash": "52e89e85",
"needsInterop": false
}
},
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+52
View File
@@ -0,0 +1,52 @@
import request from '@/utils/request'
// ==================== 设备统计报表 ====================
// 设备统计概览
export function getDeviceStatistics() {
return request.get('/inspection/report/device/statistics')
}
// 按分类统计
export function getCategoryStatistics() {
return request.get('/inspection/report/device/category')
}
// 按部门统计
export function getDepartmentStatistics() {
return request.get('/inspection/report/device/department')
}
// ==================== 设备维护报表 ====================
// 维护统计概览
export function getMaintenanceStatistics() {
return request.get('/inspection/report/maintenance/statistics')
}
// 维护超期明细
export function getMaintenanceOverdueDetails() {
return request.get('/inspection/report/maintenance/overdue')
}
// ==================== 维修类报表 ====================
// 维修统计概览
export function getRepairStatistics() {
return request.get('/inspection/report/repair/statistics')
}
// 故障类型分布
export function getFaultTypeDistribution() {
return request.get('/inspection/report/repair/fault-type')
}
// 维修工时分析
export function getRepairHoursAnalysis() {
return request.get('/inspection/report/repair/hours-analysis')
}
// 维修记录明细
export function getRepairRecordDetails() {
return request.get('/inspection/report/repair/records')
}
+18
View File
@@ -109,6 +109,24 @@ const routes = [
component: () => import('@/views/inspection/report/Index.vue'),
meta: { title: '统计报表', icon: 'DataLine' }
},
{
path: 'report/device',
name: 'DeviceStatistics',
component: () => import('@/views/inspection/report/DeviceStatistics.vue'),
meta: { title: '设备统计报表', icon: 'Monitor' }
},
{
path: 'report/maintenance',
name: 'MaintenanceReport',
component: () => import('@/views/inspection/report/MaintenanceReport.vue'),
meta: { title: '设备维护报表', icon: 'Setting' }
},
{
path: 'report/repair',
name: 'RepairReport',
component: () => import('@/views/inspection/report/RepairReport.vue'),
meta: { title: '维修类报表', icon: 'Tools' }
},
{
path: 'bigscreen',
name: 'BigScreen',
+60
View File
@@ -0,0 +1,60 @@
/**
* 通用格式化工具(后端返回的日期为 ISO 字符串,如 "2026-08-28T10:30:00"
*/
// 补零:5 → "05"
const pad = n => String(n).padStart(2, '0')
/**
* 格式化为日期:2026-08-28
* @param {string|Date} value 日期/时间值
* @returns {string} 空值返回 ''
*/
export function fmtDate(value) {
if (!value) return ''
const d = new Date(value)
if (isNaN(d.getTime())) return String(value).slice(0, 10)
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
}
/**
* 格式化为日期时间:2026-08-28 10:30:00
* @param {string|Date} value 日期/时间值
* @returns {string} 空值返回 ''
*/
export function fmtDateTime(value) {
if (!value) return ''
const d = new Date(value)
if (isNaN(d.getTime())) return String(value).replace('T', ' ').slice(0, 19)
return `${fmtDate(d)} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
}
/**
* 格式化文件大小:1024 → "1.00 KB"
* @param {number} bytes 字节数
* @returns {string} 空值返回 ''
*/
export function fmtFileSize(bytes) {
if (bytes === null || bytes === undefined || isNaN(bytes)) return ''
if (bytes < 1024) return `${bytes} B`
const units = ['KB', 'MB', 'GB', 'TB']
let size = bytes
let i = -1
do {
size /= 1024
i++
} while (size >= 1024 && i < units.length - 1)
return `${size.toFixed(2)} ${units[i]}`
}
/**
* 判断日期是否已超期(小于当前日期)
* @param {string|Date} value 日期值
* @returns {boolean}
*/
export function isOverdue(value) {
if (!value) return false
const d = new Date(value)
if (isNaN(d.getTime())) return false
return d < new Date()
}
@@ -0,0 +1,220 @@
<template>
<div class="report-page">
<!-- 统计卡片 -->
<el-row :gutter="16" class="stat-cards">
<el-col :span="4">
<el-card shadow="hover" class="stat-card">
<div class="stat-value">{{ deviceStats.Total }}</div>
<div class="stat-label">设备总数</div>
</el-card>
</el-col>
<el-col :span="4">
<el-card shadow="hover" class="stat-card in-use">
<div class="stat-value">{{ deviceStats.InUse }}</div>
<div class="stat-label">在用数量</div>
</el-card>
</el-col>
<el-col :span="4">
<el-card shadow="hover" class="stat-card idle">
<div class="stat-value">{{ deviceStats.Idle }}</div>
<div class="stat-label">闲置数量</div>
</el-card>
</el-col>
<el-col :span="4">
<el-card shadow="hover" class="stat-card scrapped">
<div class="stat-value">{{ deviceStats.Scrapped }}</div>
<div class="stat-label">报废数量</div>
</el-card>
</el-col>
<el-col :span="4">
<el-card shadow="hover" class="stat-card repairing">
<div class="stat-value">{{ deviceStats.Repairing }}</div>
<div class="stat-label">维修中</div>
</el-card>
</el-col>
<el-col :span="4">
<el-card shadow="hover" class="stat-card other">
<div class="stat-value">{{ deviceStats.Other }}</div>
<div class="stat-label">其他</div>
</el-card>
</el-col>
</el-row>
<!-- 图表区域 -->
<el-row :gutter="16" class="chart-row">
<el-col :span="12">
<el-card>
<template #header><span>设备状态分布</span></template>
<div ref="statusChartRef" class="chart-container"></div>
</el-card>
</el-col>
<el-col :span="12">
<el-card>
<template #header><span>按分类统计</span></template>
<div ref="categoryChartRef" class="chart-container"></div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="16" class="chart-row">
<el-col :span="24">
<el-card>
<template #header><span>按部门统计</span></template>
<div ref="deptChartRef" class="chart-container" style="height: 320px;"></div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
import * as echarts from 'echarts'
import { getDeviceStatistics, getCategoryStatistics, getDepartmentStatistics } from '@/api/statisticsReport'
const deviceStats = ref({ Total: 0, InUse: 0, Idle: 0, Scrapped: 0, Repairing: 0, Suspended: 0, Other: 0 })
const statusChartRef = ref(null)
const categoryChartRef = ref(null)
const deptChartRef = ref(null)
let statusChart = null
let categoryChart = null
let deptChart = null
const statusColors = {
InUse: '#409EFF',
Idle: '#E6A23C',
Scrapped: '#909399',
Repairing: '#F56C6C',
Suspended: '#606266',
Other: '#909399'
}
const statusLabels = {
InUse: '在用',
Idle: '闲置',
Scrapped: '报废',
Repairing: '维修中',
Suspended: '已停用',
Other: '其他'
}
async function loadData() {
try {
deviceStats.value = await getDeviceStatistics() || deviceStats.value
} catch (e) { /* 拦截器已提示 */ }
await nextTick()
renderStatusChart()
await loadCategoryData()
await loadDepartmentData()
}
function renderStatusChart() {
if (!statusChartRef.value) return
if (statusChart) statusChart.dispose()
statusChart = echarts.init(statusChartRef.value)
const data = [
{ value: deviceStats.value.InUse, name: '在用', itemStyle: { color: '#409EFF' } },
{ value: deviceStats.value.Idle, name: '闲置', itemStyle: { color: '#E6A23C' } },
{ value: deviceStats.value.Scrapped, name: '报废', itemStyle: { color: '#909399' } },
{ value: deviceStats.value.Repairing, name: '维修中', itemStyle: { color: '#F56C6C' } },
{ value: deviceStats.value.Suspended, name: '已停用', itemStyle: { color: '#606266' } },
{ value: deviceStats.value.Other, name: '其他', itemStyle: { color: '#A8ABB2' } }
].filter(d => d.value > 0)
statusChart.setOption({
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
legend: { bottom: 0 },
series: [{
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: true,
itemStyle: { borderRadius: 6, borderColor: '#fff', borderWidth: 2 },
label: { show: true, formatter: '{b}\n{c}台' },
data
}]
})
}
async function loadCategoryData() {
try {
const list = await getCategoryStatistics() || []
if (!categoryChartRef.value) return
if (categoryChart) categoryChart.dispose()
categoryChart = echarts.init(categoryChartRef.value)
categoryChart.setOption({
tooltip: { trigger: 'axis' },
grid: { left: 80, right: 30, top: 20, bottom: 20 },
xAxis: { type: 'value' },
yAxis: { type: 'category', data: list.map(i => i.CategoryName || '未分类'), axisLabel: { width: 80, overflow: 'truncate' } },
series: [{
type: 'bar',
data: list.map(i => i.Count),
itemStyle: { color: '#409EFF', borderRadius: [0, 4, 4, 0] },
label: { show: true, position: 'right' }
}]
})
} catch (e) { /* 拦截器已提示 */ }
}
async function loadDepartmentData() {
try {
const list = await getDepartmentStatistics() || []
if (!deptChartRef.value) return
if (deptChart) deptChart.dispose()
deptChart = echarts.init(deptChartRef.value)
deptChart.setOption({
tooltip: { trigger: 'axis' },
grid: { left: 100, right: 30, top: 20, bottom: 20 },
xAxis: { type: 'value' },
yAxis: { type: 'category', data: list.map(i => i.Department || '未分配'), axisLabel: { width: 80, overflow: 'truncate' } },
series: [{
type: 'bar',
data: list.map(i => i.Count),
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: '#409EFF' },
{ offset: 1, color: '#79BBFF' }
]),
borderRadius: [0, 4, 4, 0]
},
label: { show: true, position: 'right' }
}]
})
} catch (e) { /* 拦截器已提示 */ }
}
function handleResize() {
statusChart?.resize()
categoryChart?.resize()
deptChart?.resize()
}
onMounted(loadData)
onMounted(() => window.addEventListener('resize', handleResize))
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
statusChart?.dispose()
categoryChart?.dispose()
deptChart?.dispose()
})
</script>
<style scoped>
.report-page { padding: 0; }
.stat-cards { margin-bottom: 16px; }
.stat-card { text-align: center; }
.stat-card .stat-value { font-size: 28px; font-weight: bold; color: #303133; }
.stat-card .stat-label { font-size: 13px; color: #909399; margin-top: 4px; }
.stat-card.in-use .stat-value { color: #409EFF; }
.stat-card.idle .stat-value { color: #E6A23C; }
.stat-card.scrapped .stat-value { color: #909399; }
.stat-card.repairing .stat-value { color: #F56C6C; }
.stat-card.other .stat-value { color: #A8ABB2; }
.chart-row { margin-bottom: 16px; }
.chart-container { height: 300px; }
</style>
+38 -7
View File
@@ -1,14 +1,45 @@
<template>
<div class="page-container">
<el-card>
<template #header>
<span>统计报表</span>
</template>
<el-empty description="统计报表 - 功能开发中" />
</el-card>
<el-tabs v-model="activeTab" @tab-change="onTabChange">
<el-tab-pane label="设备统计报表" name="device">
<DeviceStatistics />
</el-tab-pane>
<el-tab-pane label="设备维护报表" name="maintenance">
<MaintenanceReport />
</el-tab-pane>
<el-tab-pane label="维修类报表" name="repair">
<RepairReport />
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup>
// TODO: 实现统计报表功能
import { ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import DeviceStatistics from './DeviceStatistics.vue'
import MaintenanceReport from './MaintenanceReport.vue'
import RepairReport from './RepairReport.vue'
const router = useRouter()
const route = useRoute()
const activeTab = ref('device')
function onTabChange(name) {
router.push({ name: `${name.charAt(0).toUpperCase() + name.slice(1)}Report` })
}
onMounted(() => {
// 根据路由参数切换Tab
const path = route.path
if (path.includes('maintenance')) activeTab.value = 'maintenance'
else if (path.includes('repair')) activeTab.value = 'repair'
else activeTab.value = 'device'
})
</script>
<style scoped>
.page-container { padding: 0; }
:deep(.el-tabs__header) { margin-bottom: 0; }
:deep(.el-tabs__content) { padding-top: 16px; }
</style>
@@ -0,0 +1,205 @@
<template>
<div class="report-page">
<!-- 统计卡片 -->
<el-row :gutter="16" class="stat-cards">
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-value">{{ maintStats.Total }}</div>
<div class="stat-label">设备总数</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card normal">
<div class="stat-value">{{ maintStats.CalibrationNormal }}</div>
<div class="stat-label">校准正常</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card overdue">
<div class="stat-value">{{ maintStats.CalibrationOverdue }}</div>
<div class="stat-label">校准超期</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card rate">
<div class="stat-value">{{ maintStats.CalibrationTimelyRate }}%</div>
<div class="stat-label">校准及时率</div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="16" class="stat-cards">
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-value">{{ maintStats.MaintenanceNormal }}</div>
<div class="stat-label">保养正常</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card overdue">
<div class="stat-value">{{ maintStats.MaintenanceOverdue }}</div>
<div class="stat-label">保养超期</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card rate">
<div class="stat-value">{{ maintStats.MaintenanceTimelyRate }}%</div>
<div class="stat-label">保养及时率</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-value">{{ overdueList.length }}</div>
<div class="stat-label">超期记录数</div>
</el-card>
</el-col>
</el-row>
<!-- 图表 -->
<el-row :gutter="16" class="chart-row">
<el-col :span="12">
<el-card>
<template #header><span>校准状态分布</span></template>
<div ref="calChartRef" class="chart-container"></div>
</el-card>
</el-col>
<el-col :span="12">
<el-card>
<template #header><span>保养状态分布</span></template>
<div ref="maintChartRef" class="chart-container"></div>
</el-card>
</el-col>
</el-row>
<!-- 超期明细表 -->
<el-card>
<template #header>
<div class="card-header">
<span>维护超期明细{{ overdueList.length }} </span>
</div>
</template>
<el-table :data="overdueList" v-loading="loading" stripe>
<el-table-column prop="EquipmentCode" label="设备编号" width="120" show-overflow-tooltip />
<el-table-column prop="EquipmentName" label="设备名称" min-width="140" show-overflow-tooltip />
<el-table-column prop="Department" label="部门" width="120" show-overflow-tooltip />
<el-table-column prop="OverdueType" label="超期类型" width="100">
<template #default="{ row }">
<el-tag :type="row.OverdueType === '校准超期' ? 'warning' : 'danger'">{{ row.OverdueType }}</el-tag>
</template>
</el-table-column>
<el-table-column label="到期日期" width="120">
<template #default="{ row }">{{ fmtTime(row.ExpireDate) }}</template>
</el-table-column>
<el-table-column prop="OverdueDays" label="超期天数" width="100" align="center">
<template #default="{ row }">
<el-tag type="danger" effect="dark">{{ row.OverdueDays }} </el-tag>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
import * as echarts from 'echarts'
import { getMaintenanceStatistics, getMaintenanceOverdueDetails } from '@/api/statisticsReport'
const maintStats = ref({
Total: 0, CalibrationNormal: 0, CalibrationOverdue: 0,
MaintenanceNormal: 0, MaintenanceOverdue: 0,
CalibrationTimelyRate: 0, MaintenanceTimelyRate: 0
})
const overdueList = ref([])
const loading = ref(false)
const calChartRef = ref(null)
const maintChartRef = ref(null)
let calChart = null
let maintChart = null
const fmtTime = (t) => t ? String(t).replace('T', ' ').slice(0, 10) : '-'
async function loadData() {
loading.value = true
try {
maintStats.value = await getMaintenanceStatistics() || maintStats.value
overdueList.value = await getMaintenanceOverdueDetails() || []
} catch (e) { /* 拦截器已提示 */ } finally {
loading.value = false
}
await nextTick()
renderCalChart()
renderMaintChart()
}
function renderCalChart() {
if (!calChartRef.value) return
if (calChart) calChart.dispose()
calChart = echarts.init(calChartRef.value)
calChart.setOption({
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
legend: { bottom: 0 },
series: [{
type: 'pie',
radius: ['40%', '70%'],
itemStyle: { borderRadius: 6, borderColor: '#fff', borderWidth: 2 },
label: { show: true, formatter: '{b}\n{c}台' },
data: [
{ value: maintStats.value.CalibrationNormal, name: '正常', itemStyle: { color: '#67C23A' } },
{ value: maintStats.value.CalibrationOverdue, name: '超期', itemStyle: { color: '#F56C6C' } }
].filter(d => d.value > 0)
}]
})
}
function renderMaintChart() {
if (!maintChartRef.value) return
if (maintChart) maintChart.dispose()
maintChart = echarts.init(maintChartRef.value)
maintChart.setOption({
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
legend: { bottom: 0 },
series: [{
type: 'pie',
radius: ['40%', '70%'],
itemStyle: { borderRadius: 6, borderColor: '#fff', borderWidth: 2 },
label: { show: true, formatter: '{b}\n{c}台' },
data: [
{ value: maintStats.value.MaintenanceNormal, name: '正常', itemStyle: { color: '#67C23A' } },
{ value: maintStats.value.MaintenanceOverdue, name: '超期', itemStyle: { color: '#F56C6C' } }
].filter(d => d.value > 0)
}]
})
}
function handleResize() {
calChart?.resize()
maintChart?.resize()
}
onMounted(loadData)
onMounted(() => window.addEventListener('resize', handleResize))
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
calChart?.dispose()
maintChart?.dispose()
})
</script>
<style scoped>
.report-page { padding: 0; }
.stat-cards { margin-bottom: 16px; }
.stat-card { text-align: center; }
.stat-card .stat-value { font-size: 28px; font-weight: bold; color: #303133; }
.stat-card .stat-label { font-size: 13px; color: #909399; margin-top: 4px; }
.stat-card.normal .stat-value { color: #67C23A; }
.stat-card.overdue .stat-value { color: #F56C6C; }
.stat-card.rate .stat-value { color: #409EFF; }
.chart-row { margin-bottom: 16px; }
.chart-container { height: 280px; }
.card-header { display: flex; justify-content: space-between; align-items: center; }
</style>
@@ -0,0 +1,202 @@
<template>
<div class="report-page">
<!-- 统计卡片 -->
<el-row :gutter="16" class="stat-cards">
<el-col :span="4">
<el-card shadow="hover" class="stat-card">
<div class="stat-value">{{ repairStats.Total }}</div>
<div class="stat-label">维修总数</div>
</el-card>
</el-col>
<el-col :span="4">
<el-card shadow="hover" class="stat-card completed">
<div class="stat-value">{{ repairStats.Completed }}</div>
<div class="stat-label">已完成</div>
</el-card>
</el-col>
<el-col :span="4">
<el-card shadow="hover" class="stat-card pending">
<div class="stat-value">{{ repairStats.Pending }}</div>
<div class="stat-label">待处理</div>
</el-card>
</el-col>
<el-col :span="4">
<el-card shadow="hover" class="stat-card in-progress">
<div class="stat-value">{{ repairStats.InProgress }}</div>
<div class="stat-label">维修中</div>
</el-card>
</el-col>
<el-col :span="4">
<el-card shadow="hover" class="stat-card hours">
<div class="stat-value">{{ repairStats.AvgRepairHours }}</div>
<div class="stat-label">平均工时(h)</div>
</el-card>
</el-col>
<el-col :span="4">
<el-card shadow="hover" class="stat-card cost">
<div class="stat-value">¥{{ repairStats.TotalRepairCost?.toFixed(0) || 0 }}</div>
<div class="stat-label">总费用</div>
</el-card>
</el-col>
</el-row>
<!-- 图表 -->
<el-row :gutter="16" class="chart-row">
<el-col :span="12">
<el-card>
<template #header><span>故障类型分布</span></template>
<div ref="faultChartRef" class="chart-container"></div>
</el-card>
</el-col>
<el-col :span="12">
<el-card>
<template #header><span>维修工时分析按故障类型</span></template>
<div ref="hoursChartRef" class="chart-container"></div>
</el-card>
</el-col>
</el-row>
<!-- 维修记录明细表 -->
<el-card>
<template #header>
<div class="card-header">
<span>维修记录明细{{ recordList.length }} </span>
</div>
</template>
<el-table :data="recordList" v-loading="loading" stripe max-height="500">
<el-table-column prop="EquipmentCode" label="设备编号" width="120" show-overflow-tooltip />
<el-table-column prop="EquipmentName" label="设备名称" min-width="140" show-overflow-tooltip />
<el-table-column prop="FaultType" label="故障类型" width="120">
<template #default="{ row }">
<el-tag size="small">{{ row.FaultType || '-' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="FaultDescription" label="故障描述" min-width="180" show-overflow-tooltip />
<el-table-column label="报修日期" width="120">
<template #default="{ row }">{{ fmtTime(row.ReportDate) }}</template>
</el-table-column>
<el-table-column label="完成日期" width="120">
<template #default="{ row }">{{ fmtTime(row.RepairEndDate) }}</template>
</el-table-column>
<el-table-column prop="RepairHours" label="工时(h)" width="90" align="center" />
<el-table-column prop="RepairPerson" label="维修人员" width="100" show-overflow-tooltip />
<el-table-column prop="RepairCost" label="费用(元)" width="100" align="right">
<template #default="{ row }">{{ row.RepairCost ? `¥${row.RepairCost.toFixed(2)}` : '-' }}</template>
</el-table-column>
<el-table-column prop="RepairStatus" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="statusTag(row.RepairStatus)" size="small">{{ row.RepairStatus || '-' }}</el-tag>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
import * as echarts from 'echarts'
import { getRepairStatistics, getFaultTypeDistribution, getRepairHoursAnalysis, getRepairRecordDetails } from '@/api/statisticsReport'
const repairStats = ref({ Total: 0, Completed: 0, InProgress: 0, Pending: 0, AvgRepairHours: 0, TotalRepairHours: 0, TotalRepairCost: 0 })
const recordList = ref([])
const loading = ref(false)
const faultChartRef = ref(null)
const hoursChartRef = ref(null)
let faultChart = null
let hoursChart = null
const fmtTime = (t) => t ? String(t).replace('T', ' ').slice(0, 10) : '-'
const statusTag = (s) => ({ '已完成': 'success', '维修中': 'warning', '待维修': 'info' }[s] || 'info')
async function loadData() {
loading.value = true
try {
repairStats.value = await getRepairStatistics() || repairStats.value
recordList.value = await getRepairRecordDetails() || []
} catch (e) { /* 拦截器已提示 */ } finally {
loading.value = false
}
await nextTick()
await loadFaultChart()
await loadHoursChart()
}
async function loadFaultChart() {
if (!faultChartRef.value) return
if (faultChart) faultChart.dispose()
faultChart = echarts.init(faultChartRef.value)
try {
const list = await getFaultTypeDistribution() || []
const colors = ['#F56C6C', '#E6A23C', '#409EFF', '#67C23A', '#909399', '#A8ABB2']
faultChart.setOption({
tooltip: { trigger: 'item', formatter: '{b}: {c}次 ({d}%)' },
legend: { bottom: 0 },
series: [{
type: 'pie',
radius: ['40%', '70%'],
itemStyle: { borderRadius: 6, borderColor: '#fff', borderWidth: 2 },
label: { show: true, formatter: '{b}\n{c}次' },
data: list.map((i, idx) => ({
value: i.Count, name: i.FaultType || '未知',
itemStyle: { color: colors[idx % colors.length] }
}))
}]
})
} catch (e) { /* 拦截器已提示 */ }
}
async function loadHoursChart() {
if (!hoursChartRef.value) return
if (hoursChart) hoursChart.dispose()
hoursChart = echarts.init(hoursChartRef.value)
try {
const list = await getRepairHoursAnalysis() || []
hoursChart.setOption({
tooltip: { trigger: 'axis' },
legend: { data: ['平均工时', '最大工时', '最小工时'], bottom: 0 },
grid: { left: 80, right: 30, top: 20, bottom: 50 },
xAxis: { type: 'category', data: list.map(i => i.FaultType || '未知'), axisLabel: { rotate: 30 } },
yAxis: { type: 'value', name: '小时' },
series: [
{ name: '平均工时', type: 'bar', data: list.map(i => Number(i.AvgHours.toFixed(1))), itemStyle: { color: '#409EFF' } },
{ name: '最大工时', type: 'bar', data: list.map(i => Number(i.MaxHours.toFixed(1))), itemStyle: { color: '#F56C6C' } },
{ name: '最小工时', type: 'bar', data: list.map(i => Number(i.MinHours.toFixed(1))), itemStyle: { color: '#67C23A' } }
]
})
} catch (e) { /* 拦截器已提示 */ }
}
function handleResize() {
faultChart?.resize()
hoursChart?.resize()
}
onMounted(loadData)
onMounted(() => window.addEventListener('resize', handleResize))
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
faultChart?.dispose()
hoursChart?.dispose()
})
</script>
<style scoped>
.report-page { padding: 0; }
.stat-cards { margin-bottom: 16px; }
.stat-card { text-align: center; }
.stat-card .stat-value { font-size: 24px; font-weight: bold; color: #303133; }
.stat-card .stat-label { font-size: 13px; color: #909399; margin-top: 4px; }
.stat-card.completed .stat-value { color: #67C23A; }
.stat-card.pending .stat-value { color: #E6A23C; }
.stat-card.in-progress .stat-value { color: #409EFF; }
.stat-card.hours .stat-value { color: #909399; }
.stat-card.cost .stat-value { color: #F56C6C; }
.chart-row { margin-bottom: 16px; }
.chart-container { height: 300px; }
.card-header { display: flex; justify-content: space-between; align-items: center; }
</style>