/* AI_CHANGELOG | 日期 | Prompt | 变更 | |------|--------|------| | 2026-03-23 | 角色路由+页面权限守卫 | onShow 添加 checkPageAccess 权限守卫 | | 2026-03-27 | board-finance-integration T3.1-T3.3 | _loadGiftRows→_loadData 绑定全部 6 板块;筛选联动调 _loadData;areaOptions 9 项 | | 2026-03-27 | board-finance-integration T3.1 fix | _loadData 移除 formatMoney,所有金额/百分比字段传原始数字,由 WXS 负责格式化,修复双重格式化导致 NaN | | 2026-07-22 | 财务看板多问题修复 | discountRate/balanceRate ×100 修复百分比计算(后端返回小数,WXS 期望百分比数字) | | 2026-03-28 | 环比修复 | _loadData 绑定所有板块的 is_down/is_flat;mapExpenseItems/mapCoachTable 读取后端真实涨跌方向 | */ // 财务看板页 — 忠于 H5 原型结构 import { checkPageAccess, getVisibleBoardTabs } from '../../utils/auth-guard' import { getRandomAiColor } from '../../utils/ai-color' import { fetchBoardFinance, fetchAICache } from '../../services/api' function isCurrentMonthFilter(selectedTime: string): boolean { return selectedTime === 'month' && new Date().getDate() <= 5 } // 2026-04-22 小程序轻量 Markdown 内联解析:支持 **加粗** / *倾斜* / _倾斜_ / ***加粗倾斜*** // - 不支持块级(标题、列表、代码块),AI 洞察文案为单段纯文本,足够覆盖 // - 输出片段数组,供 WXML 通过 循环渲染不同样式 interface MarkdownSeg { text: string bold?: boolean italic?: boolean } function parseMarkdownInline(text: string): MarkdownSeg[] { if (!text) return [] const segs: MarkdownSeg[] = [] // 先匹配 ***x***(加粗倾斜)、再匹配 **x**(加粗)、再匹配 *x* / _x_(倾斜)、__x__(加粗) const re = /\*\*\*([^*]+)\*\*\*|\*\*([^*]+)\*\*|__([^_]+)__|\*([^*\s][^*]*)\*|_([^_\s][^_]*)_/g let last = 0 let m: RegExpExecArray | null while ((m = re.exec(text)) !== null) { if (m.index > last) { segs.push({ text: text.slice(last, m.index) }) } if (m[1] !== undefined) segs.push({ text: m[1], bold: true, italic: true }) else if (m[2] !== undefined) segs.push({ text: m[2], bold: true }) else if (m[3] !== undefined) segs.push({ text: m[3], bold: true }) else if (m[4] !== undefined) segs.push({ text: m[4], italic: true }) else if (m[5] !== undefined) segs.push({ text: m[5], italic: true }) last = m.index + m[0].length } if (last < text.length) { segs.push({ text: text.slice(last) }) } return segs.length ? segs : [{ text }] } interface TocItem { emoji: string title: string sectionId: string } const tipContents: Record = { occurrence: { title: '发生额/正价', content: '所有消费项目按标价计算的总金额,不扣除任何优惠。\n\n即"如果没有任何折扣,客户应付多少"。' }, discount: { title: '总优惠', content: '包含会员折扣、赠送卡抵扣、团购差价等所有优惠金额。\n\n优惠越高,实际收入越低。' }, confirmed: { title: '成交/确认收入', content: '发生额减去总优惠后的实际入账金额。\n\n成交收入 = 发生额 - 总优惠' }, cashIn: { title: '实收/现金流入', content: '实际到账的现金,包含消费直接支付和储值充值。\n\n往期为已结算金额,本期为截至当前的发生额。' }, cashOut: { title: '现金支出', content: '包含人工、房租、水电、进货等所有经营支出。' }, balance: { title: '现金结余', content: '现金流入减去现金支出。\n\n现金结余 = 现金流入 - 现金支出' }, rechargeActual: { title: '储值卡充值实收', content: '会员储值卡首充和续费的实际到账金额。\n\n不含赠送金额。' }, firstCharge: { title: '首充', content: '新会员首次充值的金额。' }, renewCharge: { title: '续费', content: '老会员续费充值的金额。' }, consume: { title: '消耗', content: '会员使用储值卡消费的金额。' }, cardBalance: { title: '储值卡总余额', content: '所有储值卡的剩余可用余额。' }, allCardBalance: { title: '全类别会员卡余额合计', content: '储值卡 + 赠送卡(酒水卡、台费卡、抵用券)的总余额。\n\n仅供经营参考,非财务属性。' }, } Page({ data: { pageState: 'normal' as 'loading' | 'empty' | 'error' | 'normal', aiColorClass: '', boardTabs: [] as Array<{ key: string; label: string; active: boolean }>, selectedTime: 'month', selectedTimeText: '本月', timeOptions: [ { value: 'month', text: '本月' }, { value: 'lastMonth', text: '上月' }, { value: 'week', text: '本周' }, { value: 'lastWeek', text: '上周' }, { value: 'quarter3', text: '前3个月 不含本月' }, { value: 'quarter', text: '本季度' }, { value: 'lastQuarter', text: '上季度' }, { value: 'half6', text: '最近6个月不含本月' }, ], selectedArea: 'all', selectedAreaText: '全部区域', areaOptions: [ { value: 'all', text: '全部区域' }, { value: 'hall', text: '大厅' }, { value: 'hallA', text: 'A区' }, { value: 'hallB', text: 'B区' }, { value: 'hallC', text: 'C区' }, { value: 'vip', text: '台球包厢' }, { value: 'snooker', text: '斯诺克' }, { value: 'mahjong', text: '麻将房' }, { value: 'ktv', text: '团建房' }, ], compareEnabled: false, isCurrentMonth: true, // 2026-04-22 改版:拆成 title/body 两段,title 在 wxml 渲染为 ai-insight-dim(灰色标题),body 为正文 aiInsights: [] as Array<{ title: string; body: string }>, // 2026-04-22 seq11/12 置顶:从 aiInsights 派生,供"本期总结"卡片渲染 aiInsightSummary: { evaluation: null as null | { title: string; body: string }, tracking: null as null | { title: string; body: string }, }, aiInsightDetails: [] as Array<{ title: string; body: string }>, summaryLightType: '' as '' | 'green' | 'yellow' | 'red', summaryLightLabel: '', aiInsightsModalVisible: false, // 查看全部弹窗可见性 tocVisible: false, tocItems: [ { emoji: '📈', title: '经营一览', sectionId: 'section-overview' }, { emoji: '💳', title: '预收资产', sectionId: 'section-recharge' }, { emoji: '💰', title: '应计收入确认', sectionId: 'section-revenue' }, { emoji: '🧾', title: '现金流入', sectionId: 'section-cashflow' }, { emoji: '📤', title: '现金流出', sectionId: 'section-expense' }, { emoji: '🎱', title: '助教分析', sectionId: 'section-coach' }, ] as TocItem[], currentSectionIndex: 0, stickyHeaderVisible: false, stickyHeaderEmoji: '', stickyHeaderTitle: '', stickyHeaderDesc: '', tipVisible: false, tipTitle: '', tipContent: '', /** 经营一览 */ overview: { occurrence: 0 as any, occurrenceCompare: '', occurrenceDown: false, occurrenceFlat: false, discount: 0 as any, discountCompare: '', discountDown: false, discountFlat: false, discountRate: 0 as any, discountRateCompare: '', discountRateDown: false, discountRateFlat: false, confirmedRevenue: 0 as any, confirmedCompare: '', confirmedDown: false, confirmedFlat: false, cashIn: 0 as any, cashInCompare: '', cashInDown: false, cashInFlat: false, cashOut: 0 as any, cashOutCompare: '', cashOutDown: false, cashOutFlat: false, cashBalance: 0 as any, cashBalanceCompare: '', cashBalanceDown: false, cashBalanceFlat: false, balanceRate: 0 as any, balanceRateCompare: '', balanceRateDown: false, balanceRateFlat: false, }, /** 预收资产 */ recharge: { actualIncome: 0 as any, actualCompare: '', actualDown: false, firstCharge: 0 as any, firstChargeCompare: '', firstChargeDown: false, renewCharge: 0 as any, renewChargeCompare: '', renewChargeDown: false, consumed: 0 as any, consumedCompare: '', consumedDown: false, cardBalance: 0 as any, cardBalanceCompare: '', cardBalanceDown: false, allCardBalance: 0 as any, allCardBalanceCompare: '', allCardBalanceDown: false, giftRows: [] as Array<{ label: string; total: any; totalCompare: string; wine: any; wineCompare: string; table: any; tableCompare: string; coupon: any; couponCompare: string; }>, }, /** 应计收入确认 */ revenue: { structureRows: [] as any[], priceItems: [] as any[], totalOccurrence: 0 as any, totalOccurrenceCompare: '', discountItems: [] as any[], discountTotal: 0, confirmedTotal: 0 as any, confirmedTotalCompare: '', channelItems: [] as any[], }, /** 现金流入 */ cashflow: { consumeItems: [] as any[], rechargeItems: [] as any[], total: 0 as any, totalCompare: '', }, /** 现金流出 */ expense: { operationItems: [] as any[], fixedItems: [] as any[], coachItems: [] as any[], platformItems: [] as any[], total: 0 as any, totalCompare: '', totalDown: false, totalFlat: false, }, /** 助教分析 */ coachAnalysis: { basic: { totalPay: 0 as any, totalPayCompare: '', totalPayDown: false, totalShare: 0 as any, totalShareCompare: '', totalShareDown: false, avgHourly: 0 as any, avgHourlyCompare: '', avgHourlyFlat: false, rows: [] as any[], }, incentive: { totalPay: 0 as any, totalPayCompare: '', totalPayDown: false, totalShare: 0 as any, totalShareCompare: '', totalShareDown: false, avgHourly: 0 as any, avgHourlyCompare: '', avgHourlyFlat: false, rows: [] as any[], }, }, }, onLoad() { const aiColor = getRandomAiColor() this.setData({ aiColorClass: aiColor.className }) }, onShow() { checkPageAccess('pages/board-finance/board-finance').then((allowed) => { if (!allowed) return const TAB_LABELS: Record = { finance: '财务', customer: '客户', coach: '助教' } const visibleKeys = getVisibleBoardTabs() this.setData({ boardTabs: visibleKeys.map(k => ({ key: k, label: TAB_LABELS[k] || k, active: k === 'finance' })), }) const tabBar = this.getTabBar?.() if (tabBar) tabBar.setData({ active: 'board' }) this._loadData() }) }, onReady() { this._cacheSectionPositions() }, onPageScroll(e: { scrollTop: number }) { const now = Date.now() if (now - this._lastScrollTime < 100) return this._lastScrollTime = now const scrollTop = e.scrollTop const isScrollingDown = scrollTop > this._lastScrollTop this._lastScrollTop = scrollTop if (this._sectionTops.length === 0) return const offset = 100 let currentIdx = 0 for (let i = this._sectionTops.length - 1; i >= 0; i--) { if (scrollTop + offset >= this._sectionTops[i]) { currentIdx = i break } } if (scrollTop < 80) { if (this.data.stickyHeaderVisible) { this.setData({ stickyHeaderVisible: false }) } return } const toc = this.data.tocItems[currentIdx] if (isScrollingDown && !this.data.stickyHeaderVisible) { this.setData({ stickyHeaderVisible: true, stickyHeaderEmoji: toc?.emoji || '', stickyHeaderTitle: toc?.title || '', stickyHeaderDesc: toc ? (this._getSectionDesc(currentIdx) || '') : '', currentSectionIndex: currentIdx, }) } else if (!isScrollingDown && this.data.stickyHeaderVisible) { this.setData({ stickyHeaderVisible: false }) } else if (this.data.stickyHeaderVisible && currentIdx !== this.data.currentSectionIndex) { this.setData({ stickyHeaderEmoji: toc?.emoji || '', stickyHeaderTitle: toc?.title || '', stickyHeaderDesc: toc ? (this._getSectionDesc(currentIdx) || '') : '', currentSectionIndex: currentIdx, }) } }, _sectionTops: [] as number[], _lastScrollTop: 0, _lastScrollTime: 0, _sectionDescs: [ '快速了解收入与现金流的整体健康度', '会员卡充值与余额 掌握资金沉淀', '从发生额到入账收入的全流程', '实际到账的资金来源明细', '清晰呈现各类开销与结构', '全部助教服务收入与分成的平均值', ] as string[], _getSectionDesc(index: number): string { return this._sectionDescs[index] || '' }, _cacheSectionPositions() { const sectionIds = this.data.tocItems.map(item => item.sectionId) const query = wx.createSelectorQuery().in(this) sectionIds.forEach(id => { query.select(`#${id}`).boundingClientRect() }) query.exec((results: Array) => { if (!results) return this._sectionTops = results.map(r => (r ? r.top : 0)) }) }, // CHANGE 2026-03-28 | 环比修复 | 绑定所有板块 is_down/is_flat,修复箭头方向硬编码问题 async _loadData() { wx.showLoading({ title: '加载中' }) this.setData({ pageState: 'loading' }) try { const data = await fetchBoardFinance({ time: this.data.selectedTime, area: this.data.selectedArea, compare: this.data.compareEnabled ? 1 : 0, }) // 1. 经营一览 const ov = data.overview || {} as any this.setData({ 'overview.occurrence': ov.occurrence ?? 0, 'overview.discount': ov.discount ?? 0, 'overview.discountRate': (ov.discountRate ?? 0) * 100, 'overview.confirmedRevenue': ov.confirmedRevenue ?? 0, 'overview.cashIn': ov.cashIn ?? 0, 'overview.cashOut': ov.cashOut ?? 0, 'overview.cashBalance': ov.cashBalance ?? 0, 'overview.balanceRate': (ov.balanceRate ?? 0) * 100, 'overview.occurrenceCompare': ov.occurrenceCompare || '', 'overview.occurrenceDown': ov.occurrenceDown ?? false, 'overview.occurrenceFlat': ov.occurrenceFlat ?? false, 'overview.discountCompare': ov.discountCompare || '', 'overview.discountDown': ov.discountDown ?? false, 'overview.discountFlat': ov.discountFlat ?? false, 'overview.discountRateCompare': ov.discountRateCompare || '', 'overview.discountRateDown': ov.discountRateDown ?? false, 'overview.discountRateFlat': ov.discountRateFlat ?? false, 'overview.confirmedCompare': ov.confirmedRevenueCompare || '', 'overview.confirmedDown': ov.confirmedRevenueDown ?? false, 'overview.confirmedFlat': ov.confirmedRevenueFlat ?? false, 'overview.cashInCompare': ov.cashInCompare || '', 'overview.cashInDown': ov.cashInDown ?? false, 'overview.cashInFlat': ov.cashInFlat ?? false, 'overview.cashOutCompare': ov.cashOutCompare || '', 'overview.cashOutDown': ov.cashOutDown ?? false, 'overview.cashOutFlat': ov.cashOutFlat ?? false, 'overview.cashBalanceCompare': ov.cashBalanceCompare || '', 'overview.cashBalanceDown': ov.cashBalanceDown ?? false, 'overview.cashBalanceFlat': ov.cashBalanceFlat ?? false, 'overview.balanceRateCompare': ov.balanceRateCompare || '', 'overview.balanceRateDown': ov.balanceRateDown ?? false, 'overview.balanceRateFlat': ov.balanceRateFlat ?? false, }) // 2. 预收资产(area=all 时后端才返回) if (data.recharge) { const rc = data.recharge as any const giftRows = (rc.giftRows || []).map((row: any) => ({ label: row.label || '', total: row.total?.value ?? 0, totalCompare: row.total?.compare || '', wine: row.liquor?.value ?? 0, wineCompare: row.liquor?.compare || '', table: row.tableFee?.value ?? 0, tableCompare: row.tableFee?.compare || '', coupon: row.voucher?.value ?? 0, couponCompare: row.voucher?.compare || '', })) this.setData({ 'recharge.actualIncome': rc.actualIncome ?? 0, 'recharge.firstCharge': rc.firstCharge ?? 0, 'recharge.renewCharge': rc.renewCharge ?? 0, 'recharge.consumed': rc.consumed ?? 0, 'recharge.cardBalance': rc.cardBalance ?? 0, 'recharge.allCardBalance': rc.allCardBalance ?? 0, 'recharge.giftRows': giftRows, 'recharge.actualCompare': rc.actualIncomeCompare || '', 'recharge.actualDown': rc.actualIncomeDown ?? false, 'recharge.firstChargeCompare': rc.firstChargeCompare || '', 'recharge.firstChargeDown': rc.firstChargeDown ?? false, 'recharge.renewChargeCompare': rc.renewChargeCompare || '', 'recharge.renewChargeDown': rc.renewChargeDown ?? false, 'recharge.consumedCompare': rc.consumedCompare || '', 'recharge.consumedDown': rc.consumedDown ?? false, 'recharge.cardBalanceCompare': rc.cardBalanceCompare || '', 'recharge.cardBalanceDown': rc.cardBalanceDown ?? false, 'recharge.allCardBalanceCompare': rc.allCardBalanceCompare || '', 'recharge.allCardBalanceDown': rc.allCardBalanceDown ?? false, }) } // 3. 应计收入确认 const rv = data.revenue || {} as any const structureRows = (rv.structureRows || []).map((r: any) => ({ id: r.id || '', name: r.name || '', desc: r.desc || '', isSub: r.isSub || false, amount: r.amount ?? 0, discount: r.discount ?? 0, booked: r.booked ?? 0, bookedCompare: r.bookedCompare || '', })) const priceItems = (rv.priceItems || []).map((r: any) => ({ name: r.label || '', value: r.amount ?? 0, compare: r.compare || '' })) const discountItems = (rv.discountItems || []).map((r: any) => ({ name: r.label || '', desc: r.desc || '', value: r.amount ?? 0, compare: r.compare || '' })) const channelItems = (rv.channelItems || []).map((r: any) => ({ name: r.label || '', desc: r.desc || '', value: r.amount ?? 0, compare: r.compare || '' })) this.setData({ 'revenue.structureRows': structureRows, 'revenue.priceItems': priceItems, 'revenue.totalOccurrence': rv.totalOccurrence ?? 0, // CHANGE 2026-03-28 | 环比字段映射 'revenue.totalOccurrenceCompare': rv.totalOccurrenceCompare || '', 'revenue.discountItems': discountItems, 'revenue.discountTotal': rv.discountTotal ?? 0, 'revenue.confirmedTotal': rv.confirmedTotal ?? 0, 'revenue.confirmedTotalCompare': rv.confirmedTotalCompare || '', 'revenue.channelItems': channelItems, }) // 4. 现金流入 const cf = data.cashflow || {} as any const consumeItems = (cf.consumeItems || []).map((r: any) => ({ name: r.label || '', desc: r.desc || '', value: r.amount ?? 0, compare: r.compare || '', isDown: r.down ?? false, })) const rechargeItems = (cf.rechargeItems || []).map((r: any) => ({ name: r.label || '', desc: r.desc || '', value: r.amount ?? 0, compare: r.compare || '', })) this.setData({ 'cashflow.consumeItems': consumeItems, 'cashflow.rechargeItems': rechargeItems, 'cashflow.total': cf.total ?? 0, // CHANGE 2026-03-28 | 环比字段映射 'cashflow.totalCompare': cf.totalCompare || '', }) // 5. 现金流出 const ex = data.expense || {} as any const mapExpenseItems = (items: any[]) => (items || []).map((r: any) => ({ name: r.label || '', value: r.amount ?? 0, compare: r.compare || '', isDown: r.down ?? false, isFlat: r.flat ?? false, })) this.setData({ 'expense.operationItems': mapExpenseItems(ex.operationItems), 'expense.fixedItems': mapExpenseItems(ex.fixedItems), 'expense.coachItems': mapExpenseItems(ex.coachItems), 'expense.platformItems': mapExpenseItems(ex.platformItems), 'expense.total': ex.total ?? 0, 'expense.totalCompare': ex.totalCompare || '', 'expense.totalDown': ex.totalDown ?? false, 'expense.totalFlat': ex.totalFlat ?? false, }) // 6. 助教分析 const ca = data.coachAnalysis || {} as any const mapCoachTable = (table: any) => { if (!table) return { totalPay: 0, totalPayCompare: '', totalPayDown: false, totalShare: 0, totalShareCompare: '', totalShareDown: false, avgHourly: 0, avgHourlyCompare: '', avgHourlyFlat: false, rows: [] } return { totalPay: table.totalPay ?? 0, totalPayCompare: table.totalPayCompare || '', totalPayDown: table.totalPayDown ?? false, totalShare: table.totalShare ?? 0, totalShareCompare: table.totalShareCompare || '', totalShareDown: table.totalShareDown ?? false, avgHourly: table.avgHourly ?? 0, avgHourlyCompare: table.avgHourlyCompare || '', avgHourlyFlat: table.avgHourlyFlat ?? false, rows: (table.rows || []).map((r: any) => ({ level: r.level || '', pay: r.pay ?? 0, payCompare: r.payCompare || '', payDown: r.payDown ?? false, share: r.share ?? 0, shareCompare: r.shareCompare || '', shareDown: r.shareDown ?? false, hourly: r.hourly ?? 0, hourlyCompare: r.hourlyCompare || '', hourlyFlat: r.hourlyFlat ?? false, })), } } this.setData({ coachAnalysis: { basic: mapCoachTable(ca.basic), incentive: mapCoachTable(ca.incentive), }, }) // 后端不返回 aiInsights,仅在有值时覆盖(避免重载时清空旧缓存洞察) const dataToSet: Record = { pageState: 'normal' } if (Array.isArray(data.aiInsights) && data.aiInsights.length > 0) { dataToSet.aiInsights = data.aiInsights } this.setData(dataToSet) // Phase 2.5:独立加载 AI 财务洞察缓存(不阻塞主流程) // 2026-04-21:缓存按 time__area 粒度预热(cron 每日 10:00 × 72 组合),按当前筛选读缓存 this._loadAIInsights(this.data.selectedTime, this.data.selectedArea).catch(() => {}) } catch (err) { console.error('[board-finance] 数据加载失败', err) this.setData({ pageState: 'error' }) wx.showToast({ title: '数据加载失败', icon: 'none', duration: 2000 }) } finally { wx.hideLoading() } }, async _loadAIInsights(selectedTime: string, selectedArea: string) { // 2026-04-21:缓存粒度 time__area;后端 dispatcher._app2_target_id 同款拼装规则 // 2026-04-23:按 area 切 cache_type(全域 app2_finance / 区域 app2a_finance_area) const TIME_MAP: Record = { month: 'this_month', lastMonth: 'last_month', week: 'this_week', lastWeek: 'last_week', quarter: 'this_quarter', lastQuarter: 'last_quarter', quarter3: 'last_3_months', half6: 'last_6_months', } const timeKey = TIME_MAP[selectedTime] if (!timeKey) return const areaKey = selectedArea || 'all' const targetId = `${timeKey}__${areaKey}` const cacheType = areaKey === 'all' ? 'app2_finance' : 'app2a_finance_area' // 切换筛选前先清空当前展示的洞察,避免 cache miss 时仍显示上个区域的陈旧数据 this.setData({ aiInsights: [], aiInsightSummary: { evaluation: null, tracking: null }, aiInsightDetails: [], summaryLightType: '', summaryLightLabel: '', }) const cache = await fetchAICache(cacheType, targetId) if (!cache?.result_json) return const rj = cache.result_json as any // 2026-04-22:忠于 demo 两段式渲染(dim 标题 + 白色正文),拆分 title/body 存到 data // 百炼返回字段 content(兜底 body);seq 字段用于 _extractSummary 精确定位 seq11/12 const insights = Array.isArray(rj.insights) ? rj.insights.map((item: any, idx: number) => { const title = (item.title || '').trim() const body = (item.content || item.body || '').trim() const seq = Number(item.seq) || (idx + 1) return { seq, title, body, titleSegs: parseMarkdownInline(title), bodySegs: parseMarkdownInline(body), } }).filter((i: { title: string; body: string }) => i.title.length > 0 || i.body.length > 0) : [] if (insights.length === 0) return // 2026-04-22 seq11/12 置顶:识别"本期总结"(健康度评级 + 跟踪指标) // - 优先按 seq 字段精确匹配(seq=11 健康度 · seq=12 跟踪指标) // - 失败时回退到"末两条启发式",兼容旧缓存或输出顺序错位 const summary = this._extractSummary(insights) this.setData({ aiInsights: insights, aiInsightSummary: summary.summary, aiInsightDetails: summary.details, summaryLightType: summary.lightType, summaryLightLabel: summary.lightLabel, }) }, /** * 识别 seq11(健康度) + seq12(跟踪指标),并解析三色灯类型。 * 规则: * 1. 优先按 item.seq === 11 / 12 精确匹配 * 2. 找不到时回退"末两条启发式"(数组长度 ≥ 4) * 3. 数组过短直接降级为全量明细无总结 */ _extractSummary(insights: Array<{ seq?: number; title: string; body: string }>) { const empty = { summary: { evaluation: null, tracking: null }, details: insights, lightType: '' as '' | 'green' | 'yellow' | 'red', lightLabel: '', } if (insights.length < 4) return empty // 优先按 seq 精确匹配 let evaluation = insights.find(i => i.seq === 11) || null let tracking = insights.find(i => i.seq === 12) || null let details: typeof insights if (evaluation && tracking) { // seq 精确匹配成功 → 过滤掉 seq 11/12 details = insights.filter(i => i.seq !== 11 && i.seq !== 12) } else { // 回退:末两条启发式 evaluation = insights[insights.length - 2] tracking = insights[insights.length - 1] details = insights.slice(0, insights.length - 2) } // 三色灯识别:优先匹配 emoji,其次匹配文案 const text = `${evaluation.title} ${evaluation.body}` let lightType: '' | 'green' | 'yellow' | 'red' = '' let lightLabel = '' if (/🔴|红灯|警告/.test(text)) { lightType = 'red' lightLabel = '🔴 红灯警告' } else if (/🟡|黄灯|观察/.test(text)) { lightType = 'yellow' lightLabel = '🟡 黄灯观察' } else if (/🟢|绿灯|健康/.test(text)) { lightType = 'green' lightLabel = '🟢 绿灯健康' } return { summary: { evaluation, tracking }, details, lightType, lightLabel, } }, openAllInsights() { this.setData({ aiInsightsModalVisible: true }) }, closeAllInsights() { this.setData({ aiInsightsModalVisible: false }) }, /** 弹窗遮罩点击穿透阻断(避免滚动到 page 底) */ _noop() {}, onPullDownRefresh() { this._loadData() setTimeout(() => wx.stopPullDownRefresh(), 500) }, // CHANGE 2026-03-29 | P1:redirectTo 替代 navigateTo onTabChange(e: WechatMiniprogram.TouchEvent) { const tab = e.currentTarget.dataset.tab as string if (tab === 'finance') return const routes: Record = { finance: { url: '/pages/board-finance/board-finance', isTab: true }, customer: { url: '/pages/board-customer/board-customer', isTab: false }, coach: { url: '/pages/board-coach/board-coach', isTab: false }, } const route = routes[tab] if (!route) return if (route.isTab) { wx.switchTab({ url: route.url }) } else { wx.redirectTo({ url: route.url }) } }, onTimeChange(e: WechatMiniprogram.CustomEvent<{ value: string }>) { const value = e.detail.value const option = this.data.timeOptions.find(o => o.value === value) this.setData({ selectedTime: value, selectedTimeText: option?.text || '本月', isCurrentMonth: isCurrentMonthFilter(value) }) this._loadData() }, onAreaChange(e: WechatMiniprogram.CustomEvent<{ value: string }>) { const value = e.detail.value const option = this.data.areaOptions.find(o => o.value === value) this.setData({ selectedArea: value, selectedAreaText: option?.text || '全部区域' }) this._loadData() setTimeout(() => this._cacheSectionPositions(), 300) }, toggleCompare() { this.setData({ compareEnabled: !this.data.compareEnabled }) this._loadData() }, toggleToc() { this.setData({ tocVisible: !this.data.tocVisible }) }, closeToc() { this.setData({ tocVisible: false }) }, onTocItemTap(e: WechatMiniprogram.TouchEvent) { const index = e.currentTarget.dataset.index as number const sectionId = this.data.tocItems[index]?.sectionId if (sectionId) { this.setData({ tocVisible: false, currentSectionIndex: index }) wx.createSelectorQuery().in(this).select(`#${sectionId}`).boundingClientRect((rect) => { if (rect) wx.pageScrollTo({ scrollTop: rect.top + (this._lastScrollTop || 0) - 140, duration: 300 }) }).exec() } }, onHelpTap(e: WechatMiniprogram.TouchEvent) { const key = e.currentTarget.dataset.key as string const tip = tipContents[key] if (tip) this.setData({ tipVisible: true, tipTitle: tip.title, tipContent: tip.content }) }, closeTip() { this.setData({ tipVisible: false }) }, onRetry() { this.setData({ pageState: 'normal' }) this._loadData() }, })