feat: 扩展风险规则体系、审批动态路由与预算中心列表化改造
- 新增 25+ 条风险规则(预算/报销/申请/通用类),完善风险规则模拟与反馈发布机制 - 引入费用审批动态路由、平台风险分级、预审与风险阶段管理 - 预算中心列表化改造,优化票据夹仪表盘与数字员工工作看板 - 新增 Hermes 风险线索收集器、Agent 链路追踪中心 - 扩展数字员工能力库(18 个领域 Skill)与交通费用自动预估 - 完善报销申请快速预览、权限控制与前端测试覆盖
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user