feat: 完善预算中心图表与确认对话框交互
后端预算服务增加汇总查询和辅助计算,前端预算中心优化趋 势图组件和数据展示,增强确认对话框通用性和样式,完善预 算编辑对话框布局,补充预算端点单元测试。
This commit is contained in:
@@ -68,6 +68,10 @@ const comparison = (value, direction) => ({
|
||||
const parseBudgetAmount = (value) => Number(String(value || '').replace(/[^\d.-]/g, '')) || 0
|
||||
const makeBudgetRowId = () => `budget-row-${Date.now()}-${Math.random().toString(16).slice(2)}`
|
||||
const BUDGET_PAGE_SIZE_OPTIONS = [5, 10]
|
||||
const ALERT_DATE_FORMATTER = new Intl.DateTimeFormat('zh-CN', {
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
})
|
||||
|
||||
const normalizePeriodKey = (year, quarter) => {
|
||||
const normalizedYear = String(year || '').replace(/[^\d]/g, '') || '2026'
|
||||
@@ -130,52 +134,36 @@ function normalizeBudgetAllocationRow(item) {
|
||||
}
|
||||
}
|
||||
|
||||
function buildDepartmentRows(departmentCode) {
|
||||
const seed = Array.from(String(departmentCode || '')).reduce(
|
||||
(sum, char) => sum + char.charCodeAt(0),
|
||||
0
|
||||
)
|
||||
const factor = 0.88 + (seed % 18) / 100
|
||||
|
||||
return EXPENSE_BLUEPRINTS.map((item, index) => {
|
||||
const totalAmount = Math.round(item.total * factor)
|
||||
const usedAmount = Math.round(item.used * (0.9 + ((seed + index) % 12) / 100))
|
||||
const occupiedAmount = Math.round(
|
||||
item.occupied * (0.92 + ((seed + index * 3) % 10) / 100)
|
||||
)
|
||||
const leftAmount = Math.max(totalAmount - usedAmount - occupiedAmount, 0)
|
||||
const rate = Number((((usedAmount + occupiedAmount) / totalAmount) * 100).toFixed(2))
|
||||
|
||||
return {
|
||||
...item,
|
||||
totalAmount,
|
||||
usedAmount,
|
||||
occupiedAmount,
|
||||
leftAmount,
|
||||
rate,
|
||||
rateTone: rate >= item.warning ? 'danger' : rate >= item.warning - 12 ? 'warn' : 'ok',
|
||||
warningTone: item.warning >= 80 ? 'budget-warning-red' : 'budget-warning-yellow',
|
||||
warningLine: `${item.warning}%`,
|
||||
total: currency(totalAmount),
|
||||
used: currency(usedAmount),
|
||||
occupied: currency(occupiedAmount),
|
||||
left: currency(leftAmount)
|
||||
}
|
||||
})
|
||||
function normalizeBudgetUsageData(rows) {
|
||||
const source = Array.isArray(rows) ? rows : []
|
||||
return {
|
||||
labels: source.map((item) => item.expenseType || '未分类'),
|
||||
budget: source.map((item) => Number(item.totalAmount || 0)),
|
||||
used: source.map((item) => Number(item.usedAmount || 0)),
|
||||
occupied: source.map((item) => Number(item.occupiedAmount || 0)),
|
||||
available: source.map((item) => Math.max(Number(item.leftAmount || 0), 0))
|
||||
}
|
||||
}
|
||||
|
||||
function buildTrendData(rows) {
|
||||
const total = rows.reduce((sum, item) => sum + item.totalAmount, 0)
|
||||
const used = rows.reduce((sum, item) => sum + item.usedAmount + item.occupiedAmount, 0)
|
||||
function formatAlertDate(value) {
|
||||
if (!value) return ''
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return ''
|
||||
return ALERT_DATE_FORMATTER.format(date)
|
||||
}
|
||||
|
||||
function normalizeBudgetWarning(item) {
|
||||
const subjectName = item?.subject_name || resolveBudgetExpenseTypeLabel(item?.subject_code, item?.subject_code)
|
||||
const departmentName = item?.department_name || ''
|
||||
const usageRate = Number(item?.usage_rate || 0)
|
||||
const warningThreshold = Number(item?.warning_threshold || 0)
|
||||
const tone = item?.severity === 'danger' ? 'danger' : 'warn'
|
||||
return {
|
||||
labels: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
|
||||
budget: [0.05, 0.18, 0.25, 0.34, 0.45, 0.52, 0.68, 0.76, 0.84, 0.91, 0.96, 1].map((ratio) =>
|
||||
Math.round(total * ratio)
|
||||
),
|
||||
used: [0.03, 0.1, 0.13, 0.22, 0.3, 0.37, 0.51, 0.59, 0.69, 0.73, 0.86, 0.96].map((ratio) =>
|
||||
Math.round(used * ratio)
|
||||
)
|
||||
id: item?.allocation_id || `${departmentName}-${subjectName}-${item?.period_key || ''}`,
|
||||
title: departmentName ? `${departmentName} · ${subjectName}` : subjectName,
|
||||
desc: item?.message || `使用率已达 ${usageRate}%,达到预警线 ${warningThreshold}%。`,
|
||||
date: formatAlertDate(item?.occurred_at),
|
||||
tone
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,10 +192,12 @@ export default {
|
||||
const budgetPage = ref(1)
|
||||
const budgetPageSize = ref(5)
|
||||
const budgetRows = ref([])
|
||||
const budgetSummary = ref(null)
|
||||
const budgetLoading = ref(false)
|
||||
const budgetError = ref('')
|
||||
const budgetSaving = ref(false)
|
||||
const budgetEditOpen = ref(false)
|
||||
const confirmSaveOpen = ref(false)
|
||||
const budgetEditForm = ref({
|
||||
budgetYear: '2026',
|
||||
budgetQuarter: 'Q1',
|
||||
@@ -323,19 +313,13 @@ export default {
|
||||
})
|
||||
|
||||
const warnings = computed(() =>
|
||||
departmentRows.value
|
||||
.slice()
|
||||
.sort((a, b) => b.rate - a.rate)
|
||||
.slice(0, 4)
|
||||
.map((row, index) => ({
|
||||
title: row.expenseType,
|
||||
desc: `使用率已达 ${row.rate}%,${row.rate >= row.warning ? '已超过预警线' : '接近预警线'}(${row.warningLine})`,
|
||||
date: index < 2 ? '2026-05-12' : '2026-05-10',
|
||||
tone: row.rate >= row.warning ? 'danger' : row.rate >= row.warning - 12 ? 'warn' : 'ok'
|
||||
}))
|
||||
(Array.isArray(budgetSummary.value?.warnings) ? budgetSummary.value.warnings : [])
|
||||
.map(normalizeBudgetWarning)
|
||||
)
|
||||
|
||||
const trendData = computed(() => buildTrendData(departmentRows.value))
|
||||
const budgetUsageData = computed(() =>
|
||||
normalizeBudgetUsageData(departmentRows.value)
|
||||
)
|
||||
const budgetEditTotal = computed(() =>
|
||||
currency(
|
||||
budgetEditRows.value.reduce(
|
||||
@@ -402,6 +386,7 @@ export default {
|
||||
}
|
||||
|
||||
function closeBudgetEditDialog() {
|
||||
confirmSaveOpen.value = false
|
||||
budgetEditOpen.value = false
|
||||
}
|
||||
|
||||
@@ -440,6 +425,16 @@ export default {
|
||||
confirmDeleteOpen.value = false
|
||||
}
|
||||
|
||||
function requestSaveBudget() {
|
||||
if (!canEditBudget.value || budgetSaving.value) return
|
||||
confirmSaveOpen.value = true
|
||||
}
|
||||
|
||||
function cancelSaveBudget() {
|
||||
if (budgetSaving.value) return
|
||||
confirmSaveOpen.value = false
|
||||
}
|
||||
|
||||
function goToBudgetPage(page) {
|
||||
budgetPage.value = Math.min(Math.max(Number(page) || 1, 1), totalBudgetPages.value)
|
||||
}
|
||||
@@ -551,9 +546,11 @@ export default {
|
||||
cost_center: department.costCenter || ''
|
||||
})
|
||||
const allocations = Array.isArray(payload?.allocations) ? payload.allocations : []
|
||||
budgetSummary.value = payload || null
|
||||
budgetRows.value = allocations.map(normalizeBudgetAllocationRow)
|
||||
} catch (error) {
|
||||
budgetError.value = error?.message || 'Failed to load budget data'
|
||||
budgetSummary.value = null
|
||||
budgetRows.value = []
|
||||
console.warn('Failed to load budget data:', error)
|
||||
} finally {
|
||||
@@ -561,9 +558,11 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
async function publishBudgetAction() {
|
||||
budgetEditForm.value.budgetStatus = BUDGET_STATUS_OPTIONS[1]
|
||||
await saveBudgetRows('published')
|
||||
async function confirmSaveBudget() {
|
||||
if (!canEditBudget.value || budgetSaving.value) return
|
||||
budgetEditForm.value.budgetStatus = BUDGET_STATUS_OPTIONS[0]
|
||||
await saveBudgetRows('saved')
|
||||
confirmSaveOpen.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -609,6 +608,7 @@ export default {
|
||||
budgetLoading,
|
||||
budgetMetrics,
|
||||
budgetOntologyContext,
|
||||
budgetSaving,
|
||||
budgetPage: currentBudgetPage,
|
||||
budgetPageNumbers,
|
||||
budgetPageSize,
|
||||
@@ -618,6 +618,8 @@ export default {
|
||||
closeBudgetEditDialog,
|
||||
controlActionOptions: BUDGET_CONTROL_ACTION_OPTIONS,
|
||||
changeBudgetPage,
|
||||
confirmSaveBudget,
|
||||
confirmSaveOpen,
|
||||
departmentKeyword,
|
||||
departments,
|
||||
expenseTypeOptions: BUDGET_VISIBLE_EXPENSE_TYPE_OPTIONS,
|
||||
@@ -630,14 +632,15 @@ export default {
|
||||
confirmDeleteOpen,
|
||||
confirmDeleteRow,
|
||||
cancelDeleteRow,
|
||||
publishBudget: publishBudgetAction,
|
||||
cancelSaveBudget,
|
||||
requestSaveBudget,
|
||||
statusOptions: BUDGET_STATUS_OPTIONS,
|
||||
statuses: ['全部', '正常', '预警', '管控'],
|
||||
syncBudgetRowSubject,
|
||||
goToBudgetPage,
|
||||
totalBudgetPages,
|
||||
totalBudgetRows,
|
||||
trendData,
|
||||
budgetUsageData,
|
||||
visibleBudgetRows,
|
||||
visibleDepartments,
|
||||
warningOptions: BUDGET_WARNING_OPTIONS,
|
||||
|
||||
Reference in New Issue
Block a user