设备维修统计报表
This commit is contained in:
Vendored
+14
-15
@@ -1,19 +1,18 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>设备管理系统</title>
|
<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/rolldown-runtime-hePW80VL.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/runtime-core.esm-bundler-DZ3WZ9y9.js">
|
<link rel="modulepreload" crossorigin href="/assets/runtime-core.esm-bundler-DZ3WZ9y9.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/message-ddUl4JIs.js">
|
<link rel="modulepreload" crossorigin href="/assets/message-ddUl4JIs.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-BpBnxyAz.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-BpBnxyAz.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<!-- Vite 入口,指向 src/main.js,不要删除 -->
|
<!-- Vite 入口,指向 src/main.js,不要删除 -->␍
|
||||||
|
</body>
|
||||||
</body>
|
</html>
|
||||||
</html>
|
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
+11
-11
@@ -1,49 +1,49 @@
|
|||||||
{
|
{
|
||||||
"hash": "b6c330c5",
|
"hash": "a6f0f194",
|
||||||
"configHash": "e57f8d4c",
|
"configHash": "147f2811",
|
||||||
"lockfileHash": "74076ca5",
|
"lockfileHash": "77198382",
|
||||||
"browserHash": "ed3d8378",
|
"browserHash": "ee71f75c",
|
||||||
"optimized": {
|
"optimized": {
|
||||||
"@element-plus/icons-vue": {
|
"@element-plus/icons-vue": {
|
||||||
"src": "../../@element-plus/icons-vue/dist/index.js",
|
"src": "../../@element-plus/icons-vue/dist/index.js",
|
||||||
"file": "@element-plus_icons-vue.js",
|
"file": "@element-plus_icons-vue.js",
|
||||||
"fileHash": "de153aba",
|
"fileHash": "9553abbf",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"axios": {
|
"axios": {
|
||||||
"src": "../../axios/index.js",
|
"src": "../../axios/index.js",
|
||||||
"file": "axios.js",
|
"file": "axios.js",
|
||||||
"fileHash": "c19fdb3e",
|
"fileHash": "3e018dd2",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"echarts": {
|
"echarts": {
|
||||||
"src": "../../echarts/index.js",
|
"src": "../../echarts/index.js",
|
||||||
"file": "echarts.js",
|
"file": "echarts.js",
|
||||||
"fileHash": "a8d7111b",
|
"fileHash": "275a5510",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus": {
|
"element-plus": {
|
||||||
"src": "../../element-plus/es/index.mjs",
|
"src": "../../element-plus/es/index.mjs",
|
||||||
"file": "element-plus.js",
|
"file": "element-plus.js",
|
||||||
"fileHash": "10cb780f",
|
"fileHash": "cc57a904",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"pinia": {
|
"pinia": {
|
||||||
"src": "../../pinia/dist/pinia.js",
|
"src": "../../pinia/dist/pinia.js",
|
||||||
"file": "pinia.js",
|
"file": "pinia.js",
|
||||||
"fileHash": "82d3c424",
|
"fileHash": "0a5304b8",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"vue-router": {
|
"vue-router": {
|
||||||
"src": "../../vue-router/dist/vue-router.js",
|
"src": "../../vue-router/dist/vue-router.js",
|
||||||
"file": "vue-router.js",
|
"file": "vue-router.js",
|
||||||
"fileHash": "1b68fa4e",
|
"fileHash": "0ef5b6fe",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"vue": {
|
"vue": {
|
||||||
"src": "../../vue/dist/vue.runtime.esm-bundler.js",
|
"src": "../../vue/dist/vue.runtime.esm-bundler.js",
|
||||||
"file": "vue.js",
|
"file": "vue.js",
|
||||||
"fileHash": "7e296374",
|
"fileHash": "52e89e85",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -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')
|
||||||
|
}
|
||||||
@@ -109,6 +109,24 @@ const routes = [
|
|||||||
component: () => import('@/views/inspection/report/Index.vue'),
|
component: () => import('@/views/inspection/report/Index.vue'),
|
||||||
meta: { title: '统计报表', icon: 'DataLine' }
|
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',
|
path: 'bigscreen',
|
||||||
name: 'BigScreen',
|
name: 'BigScreen',
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -1,14 +1,45 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page-container">
|
<div class="page-container">
|
||||||
<el-card>
|
<el-tabs v-model="activeTab" @tab-change="onTabChange">
|
||||||
<template #header>
|
<el-tab-pane label="设备统计报表" name="device">
|
||||||
<span>统计报表</span>
|
<DeviceStatistics />
|
||||||
</template>
|
</el-tab-pane>
|
||||||
<el-empty description="统计报表 - 功能开发中" />
|
<el-tab-pane label="设备维护报表" name="maintenance">
|
||||||
</el-card>
|
<MaintenanceReport />
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="维修类报表" name="repair">
|
||||||
|
<RepairReport />
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<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>
|
</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>
|
||||||
Reference in New Issue
Block a user