const QUARTER_NAME_MAP = { 1: 'Q1', 2: 'Q2', 3: 'Q3', 4: 'Q4' } const CHINESE_QUARTER_MAP = { 一: 1, 二: 2, 三: 3, 四: 4 } const BUDGET_REPORT_COLORS = { travel: 'var(--theme-primary)', meal: 'var(--theme-secondary)', office: 'var(--warning)', communication: 'var(--info)' } const PREVIOUS_QUARTER_SPEND = [ { key: 'travel', name: '差旅', value: 468000, previousValue: 395600, recommendedBudget: 550000, color: BUDGET_REPORT_COLORS.travel, drivers: ['客户现场实施增加', '跨区域项目支持', '住宿单价上浮'], risk: 'Q2 差旅占比最高,Q3 如果继续集中出差,建议把提醒阈值放在 75%。', suggestion: '建议 Q3 编制 52-56 万,优先锁定核心项目差旅,非项目型出行走事前说明。' }, { key: 'meal', name: '招待费', value: 286000, previousValue: 255100, recommendedBudget: 320000, color: BUDGET_REPORT_COLORS.meal, drivers: ['重点客户拜访', '渠道活动增多', '单次人均金额偏高'], risk: '招待费增长快于整体费用,容易触发合规说明和客户关联材料补充。', suggestion: '建议 Q3 编制 30-32 万,并按客户拓展活动拆分额度,避免月底集中消耗。' }, { key: 'office', name: '办公用品', value: 181600, previousValue: 172000, recommendedBudget: 200000, color: BUDGET_REPORT_COLORS.office, drivers: ['新员工入职物资', '会议设备补充', '季度集中采购'], risk: '办公用品整体平稳,但集中采购会造成单月占用偏高。', suggestion: '建议 Q3 编制 19-20 万,采用月度采购节奏,把一次性采购纳入占用预留。' }, { key: 'communication', name: '通信', value: 98000, previousValue: 102800, recommendedBudget: 110000, color: BUDGET_REPORT_COLORS.communication, drivers: ['固定通讯补贴', '项目远程协同', '少量专线费用'], risk: '通信费用占比较低且略有下降,适合维持刚性额度。', suggestion: '建议 Q3 编制 10-11 万,保留 8% 弹性池用于项目临时通讯支出。' } ] const currency = (value) => Number(value || 0).toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 0 }) const compactCurrency = (value) => `¥${(Number(value || 0) / 10000).toFixed(1)}万` const percent = (value, total) => { const denominator = Number(total || 0) if (!denominator) return '0.0%' return `${((Number(value || 0) / denominator) * 100).toFixed(1)}%` } function normalizeBudgetText(rawText) { return String(rawText || '') .replace(/\s+/g, '') .toLowerCase() } function parseQuarter(rawText) { const text = String(rawText || '') const qMatch = text.match(/[qQ]\s*([1-4])/) if (qMatch) return Number(qMatch[1]) const numberMatch = text.match(/第?\s*([1-4])\s*(?:季|季度)/) if (numberMatch) return Number(numberMatch[1]) const chineseMatch = text.match(/第?\s*([一二三四])\s*(?:季|季度)/) if (chineseMatch) return CHINESE_QUARTER_MAP[chineseMatch[1]] || 0 return 0 } function parseYear(rawText) { const match = String(rawText || '').match(/(20\d{2})/) return match ? Number(match[1]) : 2026 } function hasExplicitYear(rawText) { return /(20\d{2})/.test(String(rawText || '')) } function resolvePreviousPeriod(year, quarter) { if (quarter > 1) { return { year, quarter: quarter - 1 } } return { year: year - 1, quarter: 4 } } export function shouldUseBudgetCompileReport(rawText, options = {}) { if (String(options.sessionType || '').trim() !== 'budget') { return false } const text = normalizeBudgetText(rawText) const hasTargetPeriod = parseQuarter(rawText) || hasExplicitYear(rawText) return Boolean( text && /(预算|budget)/.test(text) && /(编制|制定|测算|生成|规划|预算一下|compile|create|plan)/.test(text) && hasTargetPeriod ) } export function buildBudgetCompileReport(rawText, user = {}) { const targetYear = parseYear(rawText) const parsedQuarter = parseQuarter(rawText) const isAnnualBudget = !parsedQuarter const targetQuarter = parsedQuarter || 1 const previous = isAnnualBudget ? { year: targetYear - 1, quarter: 0 } : resolvePreviousPeriod(targetYear, targetQuarter) const periodMultiplier = isAnnualBudget ? 4 : 1 const totalSpend = PREVIOUS_QUARTER_SPEND.reduce((sum, item) => sum + item.value * periodMultiplier, 0) const totalBudget = 1320000 * periodMultiplier const recommendedTotal = PREVIOUS_QUARTER_SPEND.reduce((sum, item) => sum + item.recommendedBudget * periodMultiplier, 0) const departmentName = String(user.departmentName || user.department || '').trim() || '当前部门' const items = PREVIOUS_QUARTER_SPEND.map((item) => { const value = item.value * periodMultiplier const previousValue = item.previousValue * periodMultiplier const recommendedBudget = item.recommendedBudget * periodMultiplier const trendValue = item.previousValue ? ((value - previousValue) / previousValue) * 100 : 0 return { ...item, value, previousValue, recommendedBudget, amountDisplay: compactCurrency(value), display: percent(value, totalSpend), share: percent(value, totalSpend), trend: `${trendValue >= 0 ? '+' : ''}${trendValue.toFixed(1)}%`, trendTone: trendValue >= 10 ? 'risk' : trendValue >= 0 ? 'warn' : 'stable', recommendedDisplay: compactCurrency(recommendedBudget), editableBudget: recommendedBudget, reminderThreshold: item.key === 'communication' || item.key === 'office' ? 60 : 70, alertThreshold: item.key === 'communication' || item.key === 'office' ? 70 : 80, riskThreshold: item.key === 'communication' || item.key === 'office' ? 80 : 90, editNote: item.suggestion } }) const topItem = [...items].sort((a, b) => b.value - a.value)[0] const growthItem = [...items].sort((a, b) => { const aGrowth = a.previousValue ? (a.value - a.previousValue) / a.previousValue : 0 const bGrowth = b.previousValue ? (b.value - b.previousValue) / b.previousValue : 0 return bGrowth - aGrowth })[0] return { type: 'budget_compile_analysis', title: isAnnualBudget ? `${targetYear}年度预算编制前置分析报告` : `${targetYear}年${targetQuarter}季度预算编制前置分析报告`, subtitle: isAnnualBudget ? `基于${previous.year}年度预算执行模拟数据` : `基于${previous.year}年${previous.quarter}季度预算执行模拟数据`, departmentName, targetPeriod: isAnnualBudget ? `${targetYear}年度` : `${targetYear}年${QUARTER_NAME_MAP[targetQuarter]}`, basePeriod: isAnnualBudget ? `${previous.year}年度` : `${previous.year}年${QUARTER_NAME_MAP[previous.quarter]}`, periodType: isAnnualBudget ? '年度预算' : '季度预算', centerValue: compactCurrency(totalSpend), centerLabel: isAnnualBudget ? '去年开销' : '上季度开销', summary: { totalBudget: compactCurrency(totalBudget), totalSpend: compactCurrency(totalSpend), usageRate: percent(totalSpend, totalBudget), recommendedTotal: compactCurrency(recommendedTotal) }, macroInsights: [ `${isAnnualBudget ? `${previous.year}年度` : `${previous.year}年${previous.quarter}季度`}实际开销 ${compactCurrency(totalSpend)},预算使用率 ${percent(totalSpend, totalBudget)},整体仍在可控区间。`, `${topItem.name}是最大开销项,占 ${topItem.share},建议作为${isAnnualBudget ? `${targetYear}年度` : `${targetYear}年${targetQuarter}季度`}预算编制的第一优先级。`, `${growthItem.name}环比增长 ${growthItem.trend},需要在预算说明中提前解释业务驱动,避免后续报销阶段反复补充材料。` ], items, editableDraft: { status: 'editing', rows: items.map((item) => ({ key: item.key, name: item.name, budgetAmount: item.editableBudget, reminderThreshold: item.reminderThreshold, alertThreshold: item.alertThreshold, riskThreshold: item.riskThreshold, note: item.editNote })) }, recommendations: [ `建议${isAnnualBudget ? `${targetYear}年度` : `${targetYear}年${targetQuarter}季度`}总预算先按 ${compactCurrency(recommendedTotal)} 编制,再预留 5%-8% 部门机动池。`, '差旅和招待费采用更早的提醒阈值,通信和办公用品保持稳定额度,避免把预算过度分散到低波动项目。', '正式编制时建议把重点项目、客户活动和集中采购计划写入预算说明,后续费用控制会更容易解释。' ], generatedAt: '模拟数据 · 用于 Demo 预览' } } export async function handleBudgetCompileReportSubmit(runtime) { const { adjustComposerTextareaHeight, clearAttachedFiles, completeFlowStep, composerBusinessTimeDraftTouched, composerBusinessTimeTags, composerDraft, createMessage, currentUser, fileInputRef, fileNames, messages, nextTick, options, persistSessionState, rawText, replaceMessage, resetFlowRun, scrollToBottom, startFlowStep, submitting, userText } = runtime const analysisStartedAt = Date.now() resetFlowRun() startFlowStep('budget-prior-quarter-analysis', { title: '上季度预算开销分析', tool: 'budget.analysis.previous_quarter', detail: '正在汇总上季度费用占比、增长点和下一季度编制建议...' }) startFlowStep('budget-compile-guidance', { title: '预算编制建议生成', tool: 'budget.compile.recommendation', detail: '正在生成预算编制前置分析报告...' }) if (!options.skipUserMessage) { messages.value.push(createMessage('user', userText, fileNames)) } const pendingMessage = createMessage( 'assistant', '我先不直接进入预算表单,先执行上季度预算开销结构分析,再给您一版下一季度预算编制建议。', [], { meta: ['预算分析中'] } ) messages.value.push(pendingMessage) composerDraft.value = '' composerBusinessTimeTags.value = [] composerBusinessTimeDraftTouched.value = false clearAttachedFiles() if (fileInputRef.value) { fileInputRef.value.value = '' } submitting.value = true nextTick(() => { adjustComposerTextareaHeight() scrollToBottom() }) persistSessionState() try { await new Promise((resolve) => setTimeout(resolve, 360)) const budgetReport = buildBudgetCompileReport(rawText, currentUser.value || {}) completeFlowStep( 'budget-prior-quarter-analysis', '已完成上季度费用占比、增长点和风险点分析', Date.now() - analysisStartedAt ) completeFlowStep( 'budget-compile-guidance', '已生成下一季度预算编制建议', Date.now() - analysisStartedAt ) replaceMessage(pendingMessage.id, createMessage( 'assistant', '下面先按上季度模拟数据做一版预算编制前置分析。正式接入预算池后,这里会替换成真实部门预算和报销消耗数据。', [], { meta: ['预算分析报告', '模拟数据'], budgetReport } )) persistSessionState() } finally { submitting.value = false nextTick(scrollToBottom) } return null }