feat: 财务看板口径重构与半年模拟数据及报销状态注册表
- 重构 finance_dashboard 口径计算,新增模拟公司画像数据生成与筛选 - 引入 expense_claim_status_registry 统一报销状态流转 - 完善报销草稿流程、Item Sync 与本体解析器 - 优化总览页趋势图、分页组件与请求进度步骤 - 增强报销申请快速预览、本体工具与详情展示 - 新增半年报销模拟数据种子脚本与状态审计工具 - 补充财务看板、报销状态注册与模拟数据测试覆盖
This commit is contained in:
@@ -1,9 +1,7 @@
|
||||
<template>
|
||||
<div class="trend-chart">
|
||||
<div class="chart-legend">
|
||||
<span><i :style="{ background: chartColors.primary }"></i>申请量(单)</span>
|
||||
<span><i :style="{ background: chartColors.blue }"></i>审批完成量(单)</span>
|
||||
<span><i :style="{ background: chartColors.purple }"></i>平均审批时长(小时)</span>
|
||||
<span><i :style="{ background: activeColor }"></i>{{ legendLabel }}</span>
|
||||
</div>
|
||||
<div ref="chartElement" class="chart-body" role="img" :aria-label="ariaLabel"></div>
|
||||
</div>
|
||||
@@ -23,22 +21,44 @@ use([GridComponent, TooltipComponent, EChartsBarChart, EChartsLineChart, CanvasR
|
||||
|
||||
const props = defineProps({
|
||||
labels: { type: Array, required: true },
|
||||
applications: { type: Array, required: true },
|
||||
approved: { type: Array, required: true },
|
||||
avgHours: { type: Array, required: true }
|
||||
mode: { type: String, default: 'amount' },
|
||||
claimCount: { type: Array, default: () => [] },
|
||||
claimAmount: { type: Array, default: () => [] },
|
||||
applications: { type: Array, default: () => [] },
|
||||
approved: { type: Array, default: () => [] },
|
||||
avgHours: { type: Array, default: () => [] }
|
||||
})
|
||||
|
||||
const chartElement = shallowRef(null)
|
||||
const themeColors = useThemeColors()
|
||||
const chartColors = computed(() => ({
|
||||
primary: themeColors.value.chartPrimary,
|
||||
blue: themeColors.value.chartBlue,
|
||||
purple: themeColors.value.chartPurple
|
||||
blue: themeColors.value.chartBlue
|
||||
}))
|
||||
const isCountMode = computed(() => props.mode === 'count')
|
||||
|
||||
const claimCountSeries = computed(() => (
|
||||
props.claimCount.length ? props.claimCount : props.applications
|
||||
))
|
||||
const claimAmountSeries = computed(() => (
|
||||
props.claimAmount.length ? props.claimAmount : props.approved
|
||||
))
|
||||
const activeSeries = computed(() => (
|
||||
isCountMode.value ? claimCountSeries.value : claimAmountSeries.value
|
||||
))
|
||||
const activeColor = computed(() => (
|
||||
isCountMode.value ? chartColors.value.primary : chartColors.value.blue
|
||||
))
|
||||
const legendLabel = computed(() => (
|
||||
isCountMode.value ? '报销数量(单)' : '报销金额(元)'
|
||||
))
|
||||
const maxValue = computed(() => Math.max(...activeSeries.value.map((value) => Number(value || 0)), 1))
|
||||
|
||||
const ariaLabel = computed(() =>
|
||||
props.labels.map((label, index) => (
|
||||
`${label}申请${props.applications[index] || 0}单,审批${props.approved[index] || 0}单,平均${props.avgHours[index] || 0}小时`
|
||||
isCountMode.value
|
||||
? `${label}报销${claimCountSeries.value[index] || 0}单`
|
||||
: `${label}报销金额${formatCurrency(claimAmountSeries.value[index] || 0)}`
|
||||
)).join(';')
|
||||
)
|
||||
|
||||
@@ -51,7 +71,7 @@ const chartOptions = computed(() => ({
|
||||
animationEasingUpdate: 'linear',
|
||||
grid: {
|
||||
top: 18,
|
||||
right: 38,
|
||||
right: 24,
|
||||
bottom: 22,
|
||||
left: 36,
|
||||
containLabel: true
|
||||
@@ -83,72 +103,46 @@ const chartOptions = computed(() => ({
|
||||
fontWeight: 700
|
||||
}
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
min: 0,
|
||||
max: 250,
|
||||
splitNumber: 5,
|
||||
axisLabel: {
|
||||
color: '#64748b',
|
||||
fontSize: 11,
|
||||
fontWeight: 700
|
||||
},
|
||||
splitLine: { lineStyle: { color: 'rgba(226, 232, 240, 0.75)' } }
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
max: Math.ceil(maxValue.value * 1.2),
|
||||
splitNumber: 5,
|
||||
name: isCountMode.value ? '单' : '元',
|
||||
nameTextStyle: {
|
||||
color: '#64748b',
|
||||
fontSize: 11,
|
||||
fontWeight: 700
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
min: 0,
|
||||
max: 15,
|
||||
splitNumber: 5,
|
||||
axisLabel: {
|
||||
color: '#64748b',
|
||||
fontSize: 11,
|
||||
fontWeight: 700
|
||||
},
|
||||
splitLine: { show: false }
|
||||
}
|
||||
],
|
||||
axisLabel: {
|
||||
color: '#64748b',
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
formatter: (value) => (isCountMode.value ? `${Math.round(value)}` : formatAxisCurrency(value))
|
||||
},
|
||||
splitLine: { lineStyle: { color: 'rgba(226, 232, 240, 0.75)' } }
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '申请量(单)',
|
||||
type: 'bar',
|
||||
data: props.applications,
|
||||
barWidth: 12,
|
||||
barGap: '28%',
|
||||
itemStyle: {
|
||||
color: chartColors.value.primary,
|
||||
borderRadius: [4, 4, 0, 0]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '审批完成量(单)',
|
||||
type: 'bar',
|
||||
data: props.approved,
|
||||
barWidth: 12,
|
||||
itemStyle: {
|
||||
color: chartColors.value.blue,
|
||||
borderRadius: [4, 4, 0, 0]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '平均审批时长(小时)',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: props.avgHours,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
name: legendLabel.value,
|
||||
type: isCountMode.value ? 'line' : 'bar',
|
||||
data: activeSeries.value,
|
||||
barWidth: 16,
|
||||
smooth: isCountMode.value,
|
||||
symbol: isCountMode.value ? 'circle' : 'none',
|
||||
symbolSize: 7,
|
||||
lineStyle: {
|
||||
width: 2.5,
|
||||
color: chartColors.value.purple
|
||||
color: activeColor.value
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#ffffff',
|
||||
borderColor: chartColors.value.purple,
|
||||
borderWidth: 2.5
|
||||
color: isCountMode.value ? '#ffffff' : activeColor.value,
|
||||
borderColor: activeColor.value,
|
||||
borderWidth: isCountMode.value ? 2.5 : 0,
|
||||
borderRadius: [4, 4, 0, 0]
|
||||
},
|
||||
areaStyle: {
|
||||
opacity: isCountMode.value ? 1 : 0,
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
@@ -156,10 +150,15 @@ const chartOptions = computed(() => ({
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: toRgba(chartColors.value.purple, 0.14) },
|
||||
{ offset: 1, color: toRgba(chartColors.value.purple, 0.02) }
|
||||
{ offset: 0, color: toRgba(activeColor.value, 0.14) },
|
||||
{ offset: 1, color: toRgba(activeColor.value, 0.02) }
|
||||
]
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
valueFormatter: (value) => (
|
||||
isCountMode.value ? `${Number(value || 0)} 单` : formatCurrency(value)
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -178,6 +177,20 @@ function toRgba(color, alpha) {
|
||||
}
|
||||
return `rgba(58, 124, 165, ${alpha})`
|
||||
}
|
||||
|
||||
function formatCurrency(value) {
|
||||
const number = Number(value || 0)
|
||||
if (number >= 1000000) return `¥${(number / 1000000).toFixed(1)}M`
|
||||
if (number >= 1000) return `¥${(number / 1000).toFixed(1)}K`
|
||||
return `¥${Math.round(number)}`
|
||||
}
|
||||
|
||||
function formatAxisCurrency(value) {
|
||||
const number = Number(value || 0)
|
||||
if (number >= 1000000) return `${(number / 1000000).toFixed(1)}M`
|
||||
if (number >= 1000) return `${(number / 1000).toFixed(0)}K`
|
||||
return `${Math.round(number)}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -38,19 +38,36 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<EnterpriseSelect
|
||||
v-if="showPageSize"
|
||||
class="page-size-select"
|
||||
:model-value="pageSize"
|
||||
:options="pageSizeOptions"
|
||||
size="small"
|
||||
@change="setPageSize"
|
||||
/>
|
||||
<div class="page-tools">
|
||||
<EnterpriseSelect
|
||||
v-if="showPageSize"
|
||||
class="page-size-select"
|
||||
:model-value="pageSize"
|
||||
:options="pageSizeOptions"
|
||||
size="small"
|
||||
@change="setPageSize"
|
||||
/>
|
||||
|
||||
<div class="page-jump">
|
||||
<span>跳至</span>
|
||||
<input
|
||||
:value="pageInput"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
pattern="[0-9]*"
|
||||
aria-label="输入页码跳转"
|
||||
@blur="commitPageInput"
|
||||
@input="updatePageInput"
|
||||
@keydown.enter.prevent="commitPageInput"
|
||||
/>
|
||||
<span>页</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import EnterpriseSelect from './EnterpriseSelect.vue'
|
||||
|
||||
@@ -73,12 +90,15 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['update:currentPage', 'update:pageSize', 'page-size-change'])
|
||||
|
||||
const pageInput = ref(String(props.currentPage || 1))
|
||||
|
||||
const pageItems = computed(() => {
|
||||
if (props.pages.length) {
|
||||
return props.pages
|
||||
const total = Math.max(1, Number(props.totalPages) || 1)
|
||||
if (total <= 4) {
|
||||
return Array.from({ length: total }, (_, index) => index + 1)
|
||||
}
|
||||
|
||||
return Array.from({ length: props.totalPages }, (_, index) => index + 1)
|
||||
return [1, 2, 3, 'ellipsis', total]
|
||||
})
|
||||
|
||||
const summaryText = computed(() => {
|
||||
@@ -104,4 +124,21 @@ function setPageSize(size) {
|
||||
emit('update:pageSize', size)
|
||||
emit('page-size-change', size)
|
||||
}
|
||||
|
||||
function updatePageInput(event) {
|
||||
pageInput.value = String(event.target.value || '').replace(/\D/g, '')
|
||||
}
|
||||
|
||||
function commitPageInput() {
|
||||
const nextPage = Math.min(Math.max(Number(pageInput.value) || props.currentPage || 1, 1), props.totalPages)
|
||||
pageInput.value = String(nextPage)
|
||||
setPage(nextPage)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.currentPage,
|
||||
(page) => {
|
||||
pageInput.value = String(page || 1)
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user