2026-04-29 23:36:18 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="trend-chart">
|
|
|
|
|
|
<div class="chart-legend">
|
|
|
|
|
|
<span><i style="background:#10b981"></i>申请量(单)</span>
|
|
|
|
|
|
<span><i style="background:#3b82f6"></i>审批完成量(单)</span>
|
|
|
|
|
|
<span><i style="background:#8b5cf6"></i>平均审批时长(小时)</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="chart-body">
|
|
|
|
|
|
<Bar :data="chartData" :options="chartOptions" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { computed } from 'vue'
|
|
|
|
|
|
import { Bar } from 'vue-chartjs'
|
|
|
|
|
|
import {
|
|
|
|
|
|
Chart as ChartJS,
|
|
|
|
|
|
CategoryScale,
|
|
|
|
|
|
LinearScale,
|
|
|
|
|
|
BarElement,
|
|
|
|
|
|
PointElement,
|
|
|
|
|
|
LineElement,
|
|
|
|
|
|
Filler,
|
|
|
|
|
|
Tooltip,
|
|
|
|
|
|
Legend
|
|
|
|
|
|
} from 'chart.js'
|
2026-05-01 00:39:24 +08:00
|
|
|
|
import { useAnimationProgress } from '../../composables/useAnimationProgress.js'
|
2026-04-29 23:36:18 +08:00
|
|
|
|
|
|
|
|
|
|
ChartJS.register(CategoryScale, LinearScale, BarElement, PointElement, LineElement, Filler, Tooltip, Legend)
|
|
|
|
|
|
|
|
|
|
|
|
const props = defineProps({
|
|
|
|
|
|
labels: { type: Array, required: true },
|
|
|
|
|
|
applications: { type: Array, required: true },
|
|
|
|
|
|
approved: { type: Array, required: true },
|
|
|
|
|
|
avgHours: { type: Array, required: true }
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-05-01 00:39:24 +08:00
|
|
|
|
const progress = useAnimationProgress([
|
|
|
|
|
|
() => props.labels,
|
|
|
|
|
|
() => props.applications,
|
|
|
|
|
|
() => props.approved,
|
|
|
|
|
|
() => props.avgHours
|
|
|
|
|
|
], 1200)
|
|
|
|
|
|
|
|
|
|
|
|
const scaleSeries = (series, decimals = 0) =>
|
|
|
|
|
|
series.map((value) => Number((Number(value) * progress.value).toFixed(decimals)))
|
|
|
|
|
|
|
2026-04-29 23:36:18 +08:00
|
|
|
|
const chartData = computed(() => ({
|
|
|
|
|
|
labels: props.labels,
|
|
|
|
|
|
datasets: [
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '申请量(单)',
|
2026-05-01 00:39:24 +08:00
|
|
|
|
data: scaleSeries(props.applications),
|
2026-04-29 23:36:18 +08:00
|
|
|
|
backgroundColor: '#10b981',
|
|
|
|
|
|
borderRadius: 4,
|
|
|
|
|
|
barPercentage: 0.6,
|
|
|
|
|
|
categoryPercentage: 0.5,
|
|
|
|
|
|
order: 2
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '审批完成量(单)',
|
2026-05-01 00:39:24 +08:00
|
|
|
|
data: scaleSeries(props.approved),
|
2026-04-29 23:36:18 +08:00
|
|
|
|
backgroundColor: '#3b82f6',
|
|
|
|
|
|
borderRadius: 4,
|
|
|
|
|
|
barPercentage: 0.6,
|
|
|
|
|
|
categoryPercentage: 0.5,
|
|
|
|
|
|
order: 2
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '平均审批时长(小时)',
|
2026-05-01 00:39:24 +08:00
|
|
|
|
data: scaleSeries(props.avgHours, 1),
|
2026-04-29 23:36:18 +08:00
|
|
|
|
borderColor: '#8b5cf6',
|
|
|
|
|
|
backgroundColor: 'transparent',
|
|
|
|
|
|
borderWidth: 2,
|
|
|
|
|
|
pointBackgroundColor: '#ffffff',
|
|
|
|
|
|
pointBorderColor: '#8b5cf6',
|
|
|
|
|
|
pointBorderWidth: 2,
|
|
|
|
|
|
pointRadius: 3,
|
|
|
|
|
|
pointHoverRadius: 5,
|
|
|
|
|
|
type: 'line',
|
|
|
|
|
|
yAxisID: 'y1',
|
|
|
|
|
|
order: 1
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
|
|
const chartOptions = {
|
|
|
|
|
|
responsive: true,
|
|
|
|
|
|
maintainAspectRatio: false,
|
2026-05-01 00:39:24 +08:00
|
|
|
|
animation: {
|
|
|
|
|
|
duration: 900,
|
|
|
|
|
|
easing: 'easeOutQuart'
|
|
|
|
|
|
},
|
2026-04-29 23:36:18 +08:00
|
|
|
|
interaction: {
|
|
|
|
|
|
mode: 'index',
|
|
|
|
|
|
intersect: false
|
|
|
|
|
|
},
|
|
|
|
|
|
events: ['click', 'mousemove', 'mouseout'],
|
|
|
|
|
|
plugins: {
|
|
|
|
|
|
legend: {
|
|
|
|
|
|
display: false
|
|
|
|
|
|
},
|
|
|
|
|
|
tooltip: {
|
|
|
|
|
|
enabled: false,
|
|
|
|
|
|
external: externalTooltip
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
scales: {
|
|
|
|
|
|
x: {
|
|
|
|
|
|
grid: { display: false },
|
|
|
|
|
|
ticks: {
|
|
|
|
|
|
color: '#64748b',
|
|
|
|
|
|
font: { size: 11 }
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
y: {
|
|
|
|
|
|
beginAtZero: true,
|
|
|
|
|
|
max: 250,
|
|
|
|
|
|
grid: { color: '#f1f5f9' },
|
|
|
|
|
|
ticks: {
|
|
|
|
|
|
color: '#64748b',
|
|
|
|
|
|
font: { size: 11 },
|
|
|
|
|
|
stepSize: 50
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
y1: {
|
|
|
|
|
|
position: 'right',
|
|
|
|
|
|
beginAtZero: true,
|
|
|
|
|
|
max: 15,
|
|
|
|
|
|
grid: { display: false },
|
|
|
|
|
|
ticks: {
|
|
|
|
|
|
color: '#64748b',
|
|
|
|
|
|
font: { size: 11 },
|
|
|
|
|
|
stepSize: 3
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function externalTooltip(context) {
|
|
|
|
|
|
const { chart, tooltip } = context
|
|
|
|
|
|
|
|
|
|
|
|
let el = chart.canvas.parentNode.querySelector('.chart-tooltip')
|
|
|
|
|
|
if (!el) {
|
|
|
|
|
|
el = document.createElement('div')
|
|
|
|
|
|
el.classList.add('chart-tooltip')
|
|
|
|
|
|
el.style.cssText =
|
|
|
|
|
|
'position:absolute;background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:12px 16px;pointer-events:none;transition:opacity .15s,transform .15s;font-family:Inter,system-ui,sans-serif;font-size:13px;box-shadow:0 4px 12px rgba(0,0,0,.08);z-index:10;opacity:0;transform:translateY(4px)'
|
|
|
|
|
|
chart.canvas.parentNode.appendChild(el)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (tooltip.opacity === 0) {
|
|
|
|
|
|
el.style.opacity = '0'
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (tooltip.body) {
|
|
|
|
|
|
const titleLines = tooltip.title || []
|
|
|
|
|
|
const bodyLines = tooltip.body.map((b) => b.lines)
|
|
|
|
|
|
|
|
|
|
|
|
const colors = tooltip.labelColors
|
|
|
|
|
|
const dot = (color, text) =>
|
|
|
|
|
|
`<div style="display:flex;align-items:center;gap:6px;margin-top:4px"><span style="width:8px;height:8px;border-radius:50%;background:${color};flex-shrink:0"></span><span style="color:#64748b">${text}</span></div>`
|
|
|
|
|
|
|
|
|
|
|
|
el.innerHTML =
|
|
|
|
|
|
`<div style="font-weight:600;color:#1e293b;margin-bottom:4px">${titleLines.join('')}</div>` +
|
|
|
|
|
|
bodyLines
|
|
|
|
|
|
.map((lines, i) =>
|
|
|
|
|
|
lines.map((line) => dot(colors[i]?.backgroundColor || colors[i]?.borderColor || '#999', line))
|
|
|
|
|
|
)
|
|
|
|
|
|
.join('')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const { offsetLeft, offsetTop } = chart.canvas
|
|
|
|
|
|
const left = offsetLeft + tooltip.caretX
|
|
|
|
|
|
const top = offsetTop + tooltip.caretY
|
|
|
|
|
|
|
|
|
|
|
|
el.style.opacity = '1'
|
|
|
|
|
|
el.style.transform = 'translateY(0)'
|
|
|
|
|
|
el.style.left = `${left}px`
|
|
|
|
|
|
el.style.top = `${top - el.offsetHeight - 12}px`
|
|
|
|
|
|
el.style.transform = `translate(-50%, 0)`
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.trend-chart {
|
|
|
|
|
|
height: 280px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chart-legend {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
color: #475569;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chart-legend i {
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
width: 8px;
|
|
|
|
|
|
height: 8px;
|
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
|
margin-right: 4px;
|
|
|
|
|
|
vertical-align: middle;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chart-body {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|