import { computed, onMounted, ref, watch } from 'vue' import BudgetTrendChart from '../../components/charts/BudgetTrendChart.vue' import { fetchEmployeeMeta } from '../../services/employees.js' import { BUDGET_CONTROL_ACTION_OPTIONS, BUDGET_EXPENSE_TYPE_OPTIONS, BUDGET_QUARTER_OPTIONS, BUDGET_STATUS_OPTIONS, BUDGET_WARNING_OPTIONS, BUDGET_YEAR_OPTIONS, buildBudgetOntologyContext, formatBudgetPeriod, resolveBudgetExpenseTypeLabel } from '../../utils/budgetOntology.js' const FALLBACK_DEPARTMENTS = [ { code: 'MARKET-DEPT', name: '市场部', costCenter: 'CC-4100' }, { code: 'FINANCE-DEPT', name: '财务部', costCenter: 'CC-2100' }, { code: 'TECH-DEPT', name: '技术部', costCenter: 'CC-6100' }, { code: 'HR-DEPT', name: '人力资源部', costCenter: 'CC-3200' }, { code: 'PRODUCTION-DEPT', name: '生产部', costCenter: 'CC-7200' }, { code: 'PRESIDENT-OFFICE', name: '总裁办', costCenter: 'CC-1000' } ] const EXPENSE_BUDGET_SEED = { travel: { total: 600000, used: 242300, occupied: 150000, warning: 80, action: '提醒' }, hotel: { total: 360000, used: 139800, occupied: 84000, warning: 80, action: '提醒' }, transport: { total: 280000, used: 104600, occupied: 56000, warning: 75, action: '提醒' }, meal: { total: 420000, used: 168200, occupied: 118000, warning: 80, action: '管控' }, meeting: { total: 260000, used: 84500, occupied: 52000, warning: 75, action: '提醒' }, marketing: { total: 500000, used: 186400, occupied: 120000, warning: 80, action: '提醒' }, office: { total: 300000, used: 68500, occupied: 60000, warning: 70, action: '正常' }, training: { total: 200000, used: 42300, occupied: 20000, warning: 70, action: '正常' }, software: { total: 600000, used: 249500, occupied: 240800, warning: 80, action: '管控' }, communication: { total: 120000, used: 38600, occupied: 18000, warning: 70, action: '正常' }, welfare: { total: 240000, used: 96500, occupied: 42000, warning: 75, action: '提醒' } } const DEFAULT_EXPENSE_BUDGET = { total: 100000, used: 0, occupied: 0, warning: 70, action: '正常' } const EXPENSE_BLUEPRINTS = BUDGET_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 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] 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 buildTrendData(rows) { const total = rows.reduce((sum, item) => sum + item.totalAmount, 0) const used = rows.reduce((sum, item) => sum + item.usedAmount + item.occupiedAmount, 0) 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) ) } } export default { name: 'BudgetCenterView', components: { BudgetTrendChart }, setup() { const departments = ref(FALLBACK_DEPARTMENTS) const activeDepartmentCode = ref(FALLBACK_DEPARTMENTS[0].code) const departmentKeyword = ref('') const filters = ref({ year: '2026', quarter: 'Q1', expenseType: '全部', status: '全部' }) const budgetPage = ref(1) const budgetPageSize = ref(5) const budgetEditOpen = ref(false) const budgetEditForm = ref({ budgetYear: '2026', budgetQuarter: 'Q1', budgetPeriod: '2026年Q1', departmentCode: FALLBACK_DEPARTMENTS[0].code, costCenter: FALLBACK_DEPARTMENTS[0].costCenter, budgetOwner: '张晓明', budgetVersion: 'V1.0(初始版本)', budgetStatus: '编制中', budgetDescription: '' }) const budgetEditRows = ref([]) const activeDepartment = computed(() => departments.value.find((item) => item.code === activeDepartmentCode.value) || departments.value[0] ) const activeDepartmentName = computed(() => activeDepartment.value?.name || '市场部') const departmentRows = computed(() => buildDepartmentRows(activeDepartment.value?.code || activeDepartmentCode.value) ) const filteredBudgetRows = computed(() => departmentRows.value .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 totalBudgetRows = computed(() => filteredBudgetRows.value.length) const totalBudgetPages = computed(() => Math.max(1, Math.ceil(totalBudgetRows.value / Number(budgetPageSize.value || 5))) ) const currentBudgetPage = computed(() => Math.min(Math.max(1, budgetPage.value), totalBudgetPages.value) ) const budgetPageNumbers = computed(() => Array.from({ length: totalBudgetPages.value }, (_, index) => index + 1) ) const visibleBudgetRows = computed(() => { const pageSize = Number(budgetPageSize.value || 5) 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) return { total, used, occupied, left: Math.max(total - used - occupied, 0) } }) const budgetMetrics = computed(() => [ { label: '预算总额', value: `¥${currency(totals.value.total)}`, yoy: comparison('+8.42%', 'up'), mom: comparison('+2.16%', 'up'), tone: 'green', icon: 'mdi mdi-wallet-outline' }, { label: '已发生', value: `¥${currency(totals.value.used)}`, yoy: comparison('+12.68%', 'up'), mom: comparison('+4.35%', 'up'), tone: 'blue', icon: 'mdi mdi-chart-line' }, { label: '已占用', value: `¥${currency(totals.value.occupied)}`, yoy: comparison('+6.37%', 'up'), mom: comparison('-1.84%', 'down'), tone: 'orange', icon: 'mdi mdi-briefcase-check-outline' }, { label: '剩余可用', value: `¥${currency(totals.value.left)}`, yoy: comparison('-3.26%', 'down'), mom: comparison('-2.08%', 'down'), tone: 'green', 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' })) }) 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' })) ) const trendData = computed(() => buildTrendData(departmentRows.value)) const budgetEditTotal = computed(() => currency( budgetEditRows.value.reduce( (sum, row) => sum + parseBudgetAmount(row.budgetAmount), 0 ) ) ) const budgetOntologyContext = computed(() => buildBudgetOntologyContext({ form: budgetEditForm.value, rows: budgetEditRows.value, departments: departments.value }) ) function buildEditableRows() { return departmentRows.value.map((row) => ({ id: makeBudgetRowId(), budgetSubject: row.expenseType, budgetSubjectCode: row.budgetSubjectCode || '', budgetAmount: currency(row.totalAmount), warningThreshold: `${row.warning}%`, controlAction: row.action, budgetRemark: `${row.expenseType}相关费用` })) } function resolveNextExpenseTypeOption() { const usedCodes = new Set(budgetEditRows.value.map((row) => row.budgetSubjectCode)) return ( BUDGET_EXPENSE_TYPE_OPTIONS.find((item) => !usedCodes.has(item.value)) || BUDGET_EXPENSE_TYPE_OPTIONS[0] ) } function syncBudgetRowSubject(row) { row.budgetSubject = resolveBudgetExpenseTypeLabel(row.budgetSubjectCode, row.budgetSubject) } function openBudgetEditDialog() { const department = activeDepartment.value const budgetPeriod = formatBudgetPeriod(filters.value.year, filters.value.quarter) budgetEditForm.value = { budgetYear: filters.value.year, budgetQuarter: filters.value.quarter, budgetPeriod, departmentCode: department?.code || activeDepartmentCode.value, costCenter: department?.costCenter || '', budgetOwner: '张晓明', budgetVersion: 'V1.0(初始版本)', budgetStatus: '编制中', budgetDescription: `${department?.name || '当前部门'}2026年度预算编制,用于指导费用支出及控制成本,确保资源合理使用。` } budgetEditRows.value = buildEditableRows() budgetEditOpen.value = true } function closeBudgetEditDialog() { budgetEditOpen.value = false } function addBudgetDetailRow() { const option = resolveNextExpenseTypeOption() budgetEditRows.value.push({ id: makeBudgetRowId(), budgetSubject: option.label, budgetSubjectCode: option.value, budgetAmount: '0.00', warningThreshold: '70%', controlAction: '正常', budgetRemark: '' }) } function removeBudgetDetailRow(rowId) { if (budgetEditRows.value.length <= 1) return budgetEditRows.value = budgetEditRows.value.filter((row) => row.id !== rowId) } function goToBudgetPage(page) { budgetPage.value = Math.min(Math.max(Number(page) || 1, 1), totalBudgetPages.value) } function changeBudgetPage(direction) { goToBudgetPage(currentBudgetPage.value + direction) } function saveBudgetDraft() { budgetEditForm.value.budgetStatus = '编制中' closeBudgetEditDialog() } function publishBudget() { budgetEditForm.value.budgetStatus = '已发布' closeBudgetEditDialog() } async function loadDepartments() { try { const payload = await fetchEmployeeMeta() const options = Array.isArray(payload?.organizationOptions) ? payload.organizationOptions : [] const nextDepartments = options .filter((item) => item?.code && item?.name) .map((item) => ({ code: String(item.code), name: String(item.name), costCenter: String(item.costCenter || '') })) if (nextDepartments.length) { departments.value = nextDepartments if (!nextDepartments.some((item) => item.code === activeDepartmentCode.value)) { activeDepartmentCode.value = nextDepartments[0].code } } } catch (error) { console.warn('Failed to load budget departments from employee meta:', error) } } onMounted(() => { void loadDepartments() }) watch( [ activeDepartmentCode, budgetPageSize, () => filters.value.year, () => filters.value.quarter, () => filters.value.expenseType, () => filters.value.status ], () => { budgetPage.value = 1 } ) watch(totalBudgetPages, (pages) => { if (budgetPage.value > pages) { budgetPage.value = pages } }) return { activeDepartmentCode, activeDepartmentName, addBudgetDetailRow, budgetEditForm, budgetEditOpen, budgetEditRows, budgetEditTotal, budgetMetrics, budgetOntologyContext, budgetPage: currentBudgetPage, budgetPageNumbers, budgetPageSize, budgetPageSizeOptions: BUDGET_PAGE_SIZE_OPTIONS, closeBudgetEditDialog, controlActionOptions: BUDGET_CONTROL_ACTION_OPTIONS, changeBudgetPage, departmentKeyword, departments, expenseTypeOptions: BUDGET_EXPENSE_TYPE_OPTIONS, expenseTypes: ['全部', ...EXPENSE_BLUEPRINTS.map((item) => item.expenseType)], filters, openBudgetEditDialog, quarters: BUDGET_QUARTER_OPTIONS, publishBudget, removeBudgetDetailRow, saveBudgetDraft, statusOptions: BUDGET_STATUS_OPTIONS, statuses: ['全部', '正常', '预警', '管控'], syncBudgetRowSubject, goToBudgetPage, totalBudgetPages, totalBudgetRows, trendData, visibleBudgetRows, visibleDepartments, warningOptions: BUDGET_WARNING_OPTIONS, warnings, years: BUDGET_YEAR_OPTIONS } } }