feat: 扩展风险规则体系、审批动态路由与预算中心列表化改造

- 新增 25+ 条风险规则(预算/报销/申请/通用类),完善风险规则模拟与反馈发布机制
- 引入费用审批动态路由、平台风险分级、预审与风险阶段管理
- 预算中心列表化改造,优化票据夹仪表盘与数字员工工作看板
- 新增 Hermes 风险线索收集器、Agent 链路追踪中心
- 扩展数字员工能力库(18 个领域 Skill)与交通费用自动预估
- 完善报销申请快速预览、权限控制与前端测试覆盖
This commit is contained in:
caoxiaozhu
2026-06-01 17:07:14 +08:00
parent 7989f3a159
commit 92444e7eae
285 changed files with 25075 additions and 2986 deletions

View File

@@ -1,13 +1,12 @@
import { computed, onMounted, ref, watch } from 'vue'
import { ElButton } from 'element-plus/es/components/button/index.mjs'
import { ElInput } from 'element-plus/es/components/input/index.mjs'
import { ElPagination } from 'element-plus/es/components/pagination/index.mjs'
import { ElTable, ElTableColumn } from 'element-plus/es/components/table/index.mjs'
import BudgetTrendChart from '../../components/charts/BudgetTrendChart.vue'
import EnterpriseDetailCard from '../../components/shared/EnterpriseDetailCard.vue'
import EnterpriseDetailPage from '../../components/shared/EnterpriseDetailPage.vue'
import EnterpriseSelect from '../../components/shared/EnterpriseSelect.vue'
import TableEmptyState from '../../components/shared/TableEmptyState.vue'
import TableLoadingState from '../../components/shared/TableLoadingState.vue'
import { fetchBudgetSummary } from '../../services/budgets.js'
import { fetchEmployeeMeta } from '../../services/employees.js'
import {
canEditBudgetCenter,
@@ -17,10 +16,19 @@ import {
} from '../../utils/accessControl.js'
import {
BUDGET_QUARTER_OPTIONS,
BUDGET_VISIBLE_EXPENSE_TYPE_OPTIONS,
BUDGET_YEAR_OPTIONS,
resolveBudgetExpenseTypeLabel
BUDGET_YEAR_OPTIONS
} from '../../utils/budgetOntology.js'
import {
BUDGET_PAGE_SIZE_OPTIONS,
BUDGET_SCOPE_ALL,
BUDGET_SCOPE_ARCHIVE,
BUDGET_SCOPE_REVIEW,
buildBudgetRows,
buildBudgetScopeTabs,
buildBudgetUsageData,
getBudgetStatusOptions,
matchesBudgetKeyword
} from './budgetCenterListModel.js'
const FALLBACK_DEPARTMENTS = [
{ code: 'MARKET-DEPT', name: '市场部', costCenter: 'CC-4100' },
@@ -31,182 +39,52 @@ const FALLBACK_DEPARTMENTS = [
{ code: 'PRESIDENT-OFFICE', name: '总裁办', costCenter: 'CC-1000' }
]
const EXPENSE_BUDGET_SEED = {
travel: { total: 600000, used: 242300, occupied: 150000, warning: 80, action: '提醒' },
communication: { total: 120000, used: 38600, occupied: 18000, warning: 70, action: '正常' },
meal: { total: 420000, used: 168200, occupied: 118000, warning: 80, action: '管控' },
office: { total: 180000, used: 68500, occupied: 32000, warning: 70, action: '正常' }
function mapOptions(values, suffix = '') {
return values.map((value) => ({
label: suffix ? `${value}${suffix}` : value,
value
}))
}
const DEFAULT_EXPENSE_BUDGET = {
total: 100000,
used: 0,
occupied: 0,
warning: 70,
action: '正常'
function resolveBudgetUpdatedAt(row) {
return row?.updatedAt || row?.submittedAt || row?.archivedAt || '-'
}
const EXPENSE_BLUEPRINTS = BUDGET_VISIBLE_EXPENSE_TYPE_OPTIONS.map((option) => ({
...DEFAULT_EXPENSE_BUDGET,
...EXPENSE_BUDGET_SEED[option.value],
budgetSubjectCode: option.value,
expenseType: option.label
}))
const currency = (value) =>
Number(value || 0).toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
})
const comparison = (value, direction) => ({
value,
tone: direction === 'down' ? 'down' : 'up',
icon: direction === 'down' ? 'mdi mdi-arrow-down' : 'mdi mdi-arrow-up'
})
const BUDGET_PAGE_SIZE_OPTIONS = [5, 10]
const ALERT_DATE_FORMATTER = new Intl.DateTimeFormat('zh-CN', {
month: '2-digit',
day: '2-digit'
})
const BUDGET_COMPILED_TIME_FORMATTER = new Intl.DateTimeFormat('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false
})
const normalizePeriodKey = (year, quarter) => {
const normalizedYear = String(year || '').replace(/[^\d]/g, '') || '2026'
const normalizedQuarter = BUDGET_QUARTER_OPTIONS.includes(String(quarter || '').trim())
? String(quarter || '').trim()
: BUDGET_QUARTER_OPTIONS[0]
return `${normalizedYear}${normalizedQuarter}`
function resolveBudgetCompiler(row) {
return row?.compiler || row?.owner || '-'
}
const parsePercent = (value, fallback = 80) => {
const parsed = Number(String(value || '').replace(/[^\d.-]/g, ''))
return Number.isFinite(parsed) ? parsed : fallback
}
const clampPercent = (value) => Math.min(100, Math.max(0, Number(value) || 0))
function buildThresholds(warning) {
const alert = clampPercent(warning)
return {
reminder: clampPercent(alert - 10),
alert,
risk: clampPercent(alert + 10)
}
}
function formatBudgetCompiledAt(value) {
if (!value) return '—'
const date = new Date(value)
if (Number.isNaN(date.getTime())) return '—'
return BUDGET_COMPILED_TIME_FORMATTER.format(date).replace(/\//g, '-')
}
function resolveBudgetCompiler(item) {
return String(
item?.compiler
|| item?.compiled_by
|| item?.compiledBy
|| item?.created_by
|| item?.createdBy
|| item?.owner_name
|| item?.ownerName
|| '预算编制助手'
).trim()
}
function resolveBudgetReviewer(item) {
return String(
item?.reviewer
|| item?.reviewed_by
|| item?.reviewedBy
|| item?.approved_by
|| item?.approvedBy
|| item?.auditor
|| item?.updated_by
|| item?.updatedBy
|| '高级财务人员'
).trim()
}
function normalizeBudgetAllocationRow(item) {
const balance = item?.balance || {}
const totalAmount = Number(balance.total_amount ?? item?.original_amount ?? 0)
const usedAmount = Number(balance.consumed_amount ?? 0)
const occupiedAmount = Number(balance.reserved_amount ?? 0)
const leftAmount = Number(balance.available_amount ?? 0)
const rate = Number(balance.usage_rate ?? 0)
const warning = parsePercent(item?.warning_threshold, 80)
const thresholds = buildThresholds(warning)
const budgetSubjectCode = String(item?.subject_code || '').trim()
const expenseType = item?.subject_name || resolveBudgetExpenseTypeLabel(budgetSubjectCode, budgetSubjectCode)
return {
allocationId: item?.id || '',
budgetNo: item?.budget_no || '',
budgetSubjectCode,
compiledAt: formatBudgetCompiledAt(item?.created_at || item?.createdAt || item?.updated_at || item?.updatedAt),
compiler: resolveBudgetCompiler(item),
reviewer: resolveBudgetReviewer(item),
expenseType,
totalAmount,
usedAmount,
occupiedAmount,
leftAmount,
rate,
rateTone: rate >= thresholds.risk ? 'danger' : rate >= thresholds.alert ? 'warn' : 'ok',
reminderThreshold: thresholds.reminder,
alertThreshold: thresholds.alert,
riskThreshold: thresholds.risk,
reminderLine: `${thresholds.reminder}%`,
alertLine: `${thresholds.alert}%`,
riskLine: `${thresholds.risk}%`,
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 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 {
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
}
function buildBudgetDetailKpis(row) {
return [
{
label: '编制人',
value: resolveBudgetCompiler(row),
unit: '',
meta: row.scope === BUDGET_SCOPE_REVIEW ? '提交草案' : '预算编制',
color: 'var(--theme-primary)'
},
{
label: '审核人',
value: row.reviewer || '-',
unit: '',
meta: '高级财务审核',
color: '#3b82f6'
},
{
label: '版本',
value: row.version || '-',
unit: '',
meta: row.periodType || '预算版本',
color: '#64748b'
},
{
label: '更新时间',
value: resolveBudgetUpdatedAt(row),
unit: '',
meta: '最近同步',
color: '#f59e0b'
}
]
}
export default {
@@ -217,91 +95,71 @@ export default {
default: () => ({})
}
},
emits: ['openAssistant'],
emits: ['openAssistant', 'detail-open-change', 'detail-topbar-change'],
components: {
BudgetTrendChart,
EnterpriseSelect,
EnterpriseDetailCard,
EnterpriseDetailPage,
TableEmptyState,
TableLoadingState,
ElButton,
ElInput,
ElPagination,
ElTable,
ElTableColumn
ElButton
},
setup(props, { emit }) {
const departments = ref(FALLBACK_DEPARTMENTS)
const activeDepartmentCode = ref(FALLBACK_DEPARTMENTS[0].code)
const departmentKeyword = ref('')
const activeBudgetScope = ref(BUDGET_SCOPE_ALL)
const budgetKeyword = ref('')
const budgetPage = ref(1)
const budgetPageSize = ref(8)
const budgetLoading = ref(true)
const budgetError = ref('')
const selectedBudgetId = ref('')
const filters = ref({
year: '2026',
quarter: 'Q1',
expenseType: '全部',
status: '全部'
})
const budgetPage = ref(1)
const budgetPageSize = ref(5)
const budgetTableKeyword = ref('')
const budgetRows = ref([])
const budgetSummary = ref(null)
const budgetLoading = ref(true)
const budgetError = ref('')
const canEditBudget = computed(() => canEditBudgetCenter(props.currentUser))
const canAuditBudgetDrafts = computed(() => canEditBudgetCenter(props.currentUser))
const canSwitchDepartments = computed(() => canSwitchBudgetDepartments(props.currentUser))
const isDepartmentBudgetMonitor = computed(
() => isBudgetMonitorUser(props.currentUser) && !canSwitchDepartments.value && !isExecutiveUser(props.currentUser)
)
const yearOptions = BUDGET_YEAR_OPTIONS.map((year) => ({ label: `${year}年度`, value: year }))
const budgetPageSizeOptions = BUDGET_PAGE_SIZE_OPTIONS.map((size) => ({ label: `${size} 条/页`, value: size }))
const departmentOptions = computed(() =>
departments.value.map((department) => ({
label: department.name,
value: department.code
}))
)
const activeDepartment = computed(() =>
departments.value.find((item) => item.code === activeDepartmentCode.value) || departments.value[0]
)
const activeDepartmentName = computed(() => activeDepartment.value?.name || '市场部')
const currentUserDepartmentName = computed(() =>
String(props.currentUser?.departmentName || props.currentUser?.department || '').trim()
)
const currentUserCostCenter = computed(() =>
String(props.currentUser?.costCenter || props.currentUser?.cost_center || '').trim()
)
const departmentRows = computed(() => budgetRows.value)
const filteredBudgetRows = computed(() => {
const keyword = budgetTableKeyword.value.trim().toLowerCase()
return departmentRows.value
.filter((row) => {
if (!keyword) return true
return [
row.compiledAt,
row.compiler,
row.reviewer,
row.expenseType,
row.total,
row.used,
row.occupied,
row.left,
`${row.rate}%`,
row.reminderLine,
row.alertLine,
row.riskLine
].some((value) => String(value || '').toLowerCase().includes(keyword))
})
.filter((row) => filters.value.expenseType === '全部' || row.expenseType === filters.value.expenseType)
.filter((row) => {
if (filters.value.status === '全部') return true
if (filters.value.status === '预警') return row.rateTone === 'warn'
if (filters.value.status === '管控') return row.rateTone === 'danger'
return row.rateTone === 'ok'
})
})
const yearOptions = mapOptions(BUDGET_YEAR_OPTIONS, '年度')
const quarterOptions = mapOptions(BUDGET_QUARTER_OPTIONS)
const budgetPageSizeOptions = BUDGET_PAGE_SIZE_OPTIONS.map((size) => ({ label: `${size} 条/页`, value: size }))
const budgetRowsByScope = computed(() =>
buildBudgetRows({
departments: departments.value,
year: filters.value.year,
quarter: filters.value.quarter
})
)
const budgetScopeTabs = computed(() => buildBudgetScopeTabs(budgetRowsByScope.value))
const activeScopeRows = computed(() => budgetRowsByScope.value[activeBudgetScope.value] || [])
const activeScopeLabel = computed(
() => budgetScopeTabs.value.find((item) => item.value === activeBudgetScope.value)?.label || '预算'
)
const statusOptions = computed(() => mapOptions(getBudgetStatusOptions(activeBudgetScope.value)))
const filteredBudgetRows = computed(() =>
activeScopeRows.value
.filter((row) => filters.value.status === '全部' || row.statusLabel === filters.value.status)
.filter((row) => matchesBudgetKeyword(row, budgetKeyword.value))
)
const totalBudgetRows = computed(() => filteredBudgetRows.value.length)
const totalBudgetPages = computed(() =>
Math.max(1, Math.ceil(totalBudgetRows.value / Number(budgetPageSize.value || 5)))
Math.max(1, Math.ceil(totalBudgetRows.value / Number(budgetPageSize.value || 8)))
)
const currentBudgetPage = computed(() =>
Math.min(Math.max(1, budgetPage.value), totalBudgetPages.value)
@@ -310,102 +168,112 @@ export default {
Array.from({ length: totalBudgetPages.value }, (_, index) => index + 1)
)
const visibleBudgetRows = computed(() => {
const pageSize = Number(budgetPageSize.value || 5)
const pageSize = Number(budgetPageSize.value || 8)
const start = (currentBudgetPage.value - 1) * pageSize
return filteredBudgetRows.value.slice(start, start + pageSize)
})
const totals = computed(() => {
const rows = departmentRows.value
const total = rows.reduce((sum, item) => sum + item.totalAmount, 0)
const used = rows.reduce((sum, item) => sum + item.usedAmount, 0)
const occupied = rows.reduce((sum, item) => sum + item.occupiedAmount, 0)
const selectedBudget = computed(() =>
activeScopeRows.value.find((row) => row.id === selectedBudgetId.value) || null
)
const detailMode = computed(() => Boolean(selectedBudget.value))
const selectedBudgetUsageData = computed(() => buildBudgetUsageData(selectedBudget.value))
const budgetDetailTopBarPayload = computed(() => {
const row = selectedBudget.value
if (!row) return null
return {
total,
used,
occupied,
left: Math.max(total - used - occupied, 0)
view: {
eyebrow: '预算详情',
title: `${row.departmentName} · ${row.periodLabel}`,
desc: `${row.budgetNo} / ${row.version} · 仅覆盖差旅、通信、招待费、办公用品`
},
alerts: [],
kpis: buildBudgetDetailKpis(row)
}
})
const selectedBudgetStatusNotes = computed(() => {
const row = selectedBudget.value
if (!row) return []
const budgetMetrics = computed(() => [
{
label: '预算总额',
value: `¥${currency(totals.value.total)}`,
yoy: comparison('+8.42%', 'up'),
mom: comparison('+2.16%', 'up'),
tone: 'primary',
icon: 'mdi mdi-wallet-outline'
},
{
label: '已发生',
value: `¥${currency(totals.value.used)}`,
yoy: comparison('+12.68%', 'up'),
mom: comparison('+4.35%', 'up'),
tone: 'info',
icon: 'mdi mdi-chart-line'
},
{
label: '已占用',
value: `¥${currency(totals.value.occupied)}`,
yoy: comparison('+6.37%', 'up'),
mom: comparison('-1.84%', 'down'),
tone: 'warning',
icon: 'mdi mdi-briefcase-check-outline'
},
{
label: '剩余可用',
value: `¥${currency(totals.value.left)}`,
yoy: comparison('-3.26%', 'down'),
mom: comparison('-2.08%', 'down'),
tone: 'primary',
icon: 'mdi mdi-cash'
}
])
const visibleDepartments = computed(() => {
const keyword = departmentKeyword.value.trim()
return departments.value
.filter((item) => !keyword || item.name.includes(keyword) || item.code.includes(keyword))
.map((item) => ({
...item,
icon: item.code === activeDepartmentCode.value ? 'mdi mdi-account-group-outline' : 'mdi mdi-domain'
}))
return [
{
label: '预算状态',
value: row.statusLabel,
tone: row.statusTone || 'ok',
desc: row.auditSummary || '当前预算状态已完成同步,可在预算中心继续追踪。'
},
{
label: '风险状态',
value: row.riskLabel,
tone: row.riskTone || 'ok',
desc: `当前已发生与已占用合计使用率为 ${row.usageRateLabel},系统按四类费用的提醒、告警和风险阈值综合判断。`
}
]
})
const showTable = computed(() => !budgetLoading.value && !budgetError.value && visibleBudgetRows.value.length > 0)
const showEmpty = computed(() => !budgetLoading.value && !budgetError.value && visibleBudgetRows.value.length === 0)
const emptyState = computed(() => ({
eyebrow: activeScopeLabel.value,
title: `暂无${activeScopeLabel.value}`,
desc: '当前筛选条件下没有匹配的预算记录。',
icon: 'mdi mdi-database-search-outline',
tone: 'blue',
artLabel: '预算列表为空',
tips: ['可以调整年度、季度、状态或关键词后重试。']
}))
const pageSummary = computed(() => `${totalBudgetRows.value} 条,目前第 ${currentBudgetPage.value}`)
const warnings = computed(() =>
(Array.isArray(budgetSummary.value?.warnings) ? budgetSummary.value.warnings : [])
.map(normalizeBudgetWarning)
)
const budgetUsageData = computed(() =>
normalizeBudgetUsageData(departmentRows.value)
)
function openBudgetAssistant() {
function openBudgetAssistant(prompt = '') {
if (!canEditBudget.value) return
emit('openAssistant', {
source: 'budget',
sessionType: 'budget',
prompt: '',
prompt,
files: [],
conversation: null
})
}
function openBudgetReviewAssistant(row) {
if (!row || !canAuditBudgetDrafts.value) {
openBudgetDetail(row)
return
}
openBudgetAssistant(
`请进入预算审核模式,审核${row.departmentName}${row.periodLabel}预算草案,重点看差旅、通信、招待费和办公用品的合理性、风险点和是否可以通过。`
)
}
function openBudgetDetail(row) {
if (!row?.id) return
selectedBudgetId.value = row.id
}
function backToList() {
selectedBudgetId.value = ''
}
function handleRowAction(row) {
if (activeBudgetScope.value === BUDGET_SCOPE_REVIEW && canAuditBudgetDrafts.value) {
openBudgetReviewAssistant(row)
return
}
openBudgetDetail(row)
}
function goToBudgetPage(page) {
budgetPage.value = Math.min(Math.max(Number(page) || 1, 1), totalBudgetPages.value)
}
function changeBudgetPage(direction) {
goToBudgetPage(currentBudgetPage.value + direction)
function changeBudgetPageSize(size) {
budgetPageSize.value = Number(size) || 8
budgetPage.value = 1
}
function resolveScopedDepartments(options) {
if (!isDepartmentBudgetMonitor.value) {
return options
}
if (!isDepartmentBudgetMonitor.value) return options
const userDepartment = currentUserDepartmentName.value
const userCostCenter = currentUserCostCenter.value
@@ -414,9 +282,7 @@ export default {
return userDepartment && item.name === userDepartment
})
if (scoped.length) {
return scoped
}
if (scoped.length) return scoped
return [
{
@@ -430,6 +296,7 @@ export default {
async function loadDepartments() {
budgetLoading.value = true
budgetError.value = ''
try {
const payload = await fetchEmployeeMeta()
const options = Array.isArray(payload?.organizationOptions) ? payload.organizationOptions : []
@@ -442,39 +309,11 @@ export default {
costCenter: String(item.costCenter || '')
}))
const scopedDepartments = resolveScopedDepartments(nextDepartments)
if (scopedDepartments.length) {
departments.value = scopedDepartments
if (!scopedDepartments.some((item) => item.code === activeDepartmentCode.value)) {
activeDepartmentCode.value = scopedDepartments[0].code
}
}
await loadBudgetData()
} catch (error) {
console.warn('Failed to load budget departments from employee meta:', error)
await loadBudgetData()
}
}
async function loadBudgetData() {
const department = activeDepartment.value || {}
budgetLoading.value = true
budgetError.value = ''
try {
const payload = await fetchBudgetSummary({
year: filters.value.year,
period: normalizePeriodKey(filters.value.year, filters.value.quarter),
department_id: department.id || '',
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 {
budgetLoading.value = false
}
@@ -485,24 +324,24 @@ export default {
})
watch(
[
activeDepartmentCode,
budgetPageSize,
() => filters.value.year,
() => filters.value.quarter,
() => filters.value.expenseType,
() => filters.value.status,
budgetTableKeyword
],
() => activeBudgetScope.value,
() => {
filters.value.status = '全部'
budgetPage.value = 1
selectedBudgetId.value = ''
}
)
watch(
[activeDepartmentCode, () => filters.value.year, () => filters.value.quarter],
[
budgetPageSize,
budgetKeyword,
() => filters.value.year,
() => filters.value.quarter,
() => filters.value.status
],
() => {
void loadBudgetData()
budgetPage.value = 1
}
)
@@ -512,37 +351,51 @@ export default {
}
})
watch(detailMode, (value) => {
emit('detail-open-change', value)
}, { immediate: true })
watch(budgetDetailTopBarPayload, (payload) => {
emit('detail-topbar-change', payload)
}, { immediate: true, deep: true })
return {
activeDepartmentCode,
activeDepartmentName,
BUDGET_SCOPE_ALL,
BUDGET_SCOPE_ARCHIVE,
BUDGET_SCOPE_REVIEW,
activeBudgetScope,
budgetError,
budgetKeyword,
budgetLoading,
budgetMetrics,
budgetPage: currentBudgetPage,
budgetPageNumbers,
budgetPageSize,
budgetPageSizeOptions,
budgetTableKeyword,
budgetScopeTabs,
backToList,
canAuditBudgetDrafts,
canEditBudget,
canSwitchDepartments,
changeBudgetPage,
departmentKeyword,
departments,
expenseTypes: ['全部', ...EXPENSE_BLUEPRINTS.map((item) => item.expenseType)],
changeBudgetPageSize,
detailMode,
emptyState,
filters,
openBudgetAssistant,
quarters: BUDGET_QUARTER_OPTIONS,
departmentOptions,
statuses: ['全部', '正常', '预警', '管控'],
goToBudgetPage,
handleRowAction,
openBudgetAssistant,
openBudgetDetail,
openBudgetReviewAssistant,
pageSummary,
quarterOptions,
selectedBudget,
selectedBudgetStatusNotes,
selectedBudgetUsageData,
showEmpty,
showTable,
statusOptions,
totalBudgetPages,
totalBudgetRows,
budgetUsageData,
visibleBudgetRows,
visibleDepartments,
warnings,
yearOptions,
years: BUDGET_YEAR_OPTIONS
yearOptions
}
}
}