import type { EChartsOption } from 'echarts' import type { AnalysisContext, AnalysisPageDefinition, ChartDefinition } from '../types/analysis' import { buildAeMonthlyByOccurrence, buildComplaintPareto, buildComplianceMatching, buildGlobalMonthlyPpm, buildInjuryDeviceMatrix, buildProvinceAeCounts, buildPsurTopProducts, buildReportingLagDays, buildSaeMonthlyByOccurrence, buildSaeShareMonthly, buildTopDeviceFailures, } from '../lib/aggregate' import { buildBatchComplaintVsAeCounts, buildComplaintBatchTop, buildComplaintCloseCycleDays, buildComplaintMonthly, buildCompensationDist, buildDefectConfirmedProductFault, buildFaultByRegisterMonthMatrix, buildHospitalAeVsAdmissionScatter, buildHospitalComplaintVsAdmissionQty, buildHospitalProductComplaintRate, buildInvestigationConclusionDist, buildIsAeRateByProduct, buildProductComplaintHalfYearGrowth, buildProvinceComplaintHalfYearGrowth, buildTopFaultTypes, buildTopHospitalsComplaints, buildTopProductsComplaints, } from '../lib/quality-aggregate' import { COMPLIANCE_DOC_REF, COMPLIANCE_FOOTNOTE_AUDIT, COMPLIANCE_FOOTNOTE_PPM, COMPLIANCE_FOOTNOTE_SHORT, } from './compliance-baseline' import { QUALITY_DOC_REF, QLT_ADMISSION_DENOM, QLT_TIME_AXIS } from './quality-baseline' import { buildDealerComplaintRatePer1000, buildDealerOpWrongRatePer1000, buildOpWrongDealerCounts, buildOpWrongHospitalTop, buildProductOpWrongSharePct, buildProvinceComplaintCounts, buildProvinceComplaintRatePer1000, } from '../lib/marketing-aggregate' import { MARKETING_DOC_REF, MKT_ADMISSION_DENOM, MKT_HOSPITAL_MAP, MKT_TIME_AXIS } from './marketing-baseline' const MOCK_DATA_SCOPE = '数据源:前端 fetch 加载 public/mock 下 ae.json、complaint.json、admission.json 的全量记录;未接入 Pinia 全局年份/BU 筛选(上线后与数仓条件对齐)。' const ex = (extraScope: string, analysisLogic: string) => ({ dataScope: [MOCK_DATA_SCOPE, extraScope].filter(Boolean).join(' '), analysisLogic, }) const placeholderPair = (prefix: string, hint = '见主策略对应小节与落地文档指标编码。'): ChartDefinition[] => [ { id: `${prefix}-1`, title: '图表位 A(待接数据)', placeholder: true, chartExplain: ex('暂无聚合结果。', `占位图表;实现时替换为 optionBuilder,并满足「取数范围 + 分析逻辑」说明要求。${hint}`), }, { id: `${prefix}-2`, title: '图表位 B(待接数据)', placeholder: true, chartExplain: ex('暂无聚合结果。', `同上。${hint}`), }, ] const lineOption = ( title: string, xAxis: string[], seriesData: (number | null)[], seriesName = '事件数', subtext?: string, ): EChartsOption => ({ title: { text: title, subtext, left: 'center', textStyle: { fontSize: 14 }, subtextStyle: { fontSize: 11, color: '#64748b' }, }, tooltip: { trigger: 'axis' }, grid: { top: subtext ? 78 : 60, right: 24, bottom: 40, left: 40 }, xAxis: { type: 'category', data: xAxis }, yAxis: { type: 'value' }, series: [{ name: seriesName, type: 'line', smooth: true, data: seriesData }], }) const barOption = (title: string, labels: string[], values: number[], subtext?: string): EChartsOption => ({ title: { text: title, subtext, left: 'center', textStyle: { fontSize: 14 }, subtextStyle: { fontSize: 11, color: '#64748b' }, }, tooltip: { trigger: 'axis' }, grid: { top: subtext ? 78 : 60, right: 24, bottom: 60, left: 45 }, xAxis: { type: 'category', data: labels, axisLabel: { rotate: 20 } }, yAxis: { type: 'value' }, series: [{ type: 'bar', data: values, itemStyle: { borderRadius: [4, 4, 0, 0] } }], }) const injuryDeviceHeatmapOption = (saeOnly: boolean, title: string, subtext: string) => { return (context: AnalysisContext): EChartsOption => { const { injuries, devices, cells } = buildInjuryDeviceMatrix(context, saeOnly) const data = cells.map((c) => [devices.indexOf(c.device), injuries.indexOf(c.injury), c.value]) const max = Math.max(1, ...cells.map((c) => c.value)) return { title: { text: title, subtext, left: 'center', textStyle: { fontSize: 14 }, subtextStyle: { fontSize: 11, color: '#64748b' }, }, tooltip: { position: 'top' }, grid: { top: 88, height: Math.max(220, injuries.length * 28), left: 80, right: 24, bottom: 48 }, xAxis: { type: 'category', data: devices, splitArea: { show: true } }, yAxis: { type: 'category', data: injuries, splitArea: { show: true } }, visualMap: { min: 0, max, calculable: true, orient: 'horizontal', left: 'center', bottom: 4, inRange: { color: ['#eef2ff', '#312e81'] }, }, series: [ { name: '报告条数', type: 'heatmap', data, label: { show: true }, emphasis: { itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0,0,0,0.3)' } }, }, ], } } } const reportingLagHistogramOption = (context: AnalysisContext): EChartsOption => { const lags = buildReportingLagDays(context).filter((d) => d >= 0) const bins = ['0–1', '2–3', '4–5', '6–7', '8–10', '>10'] const counts = [0, 0, 0, 0, 0, 0] lags.forEach((d) => { if (d <= 1) counts[0] += 1 else if (d <= 3) counts[1] += 1 else if (d <= 5) counts[2] += 1 else if (d <= 7) counts[3] += 1 else if (d <= 10) counts[4] += 1 else counts[5] += 1 }) return { title: { text: '上报滞后天数分布(审核日期 − 登记日期)', subtext: `${COMPLIANCE_DOC_REF}|CMP-AE-701`, left: 'center', textStyle: { fontSize: 14 }, subtextStyle: { fontSize: 11, color: '#64748b' }, }, tooltip: { trigger: 'axis' }, grid: { top: 88, right: 24, bottom: 40, left: 40 }, xAxis: { type: 'category', data: bins }, yAxis: { type: 'value', name: 'AE 条数' }, series: [{ type: 'bar', data: counts, itemStyle: { borderRadius: [4, 4, 0, 0] } }], } } const complianceAuditSummaryOption = (context: AnalysisContext): EChartsOption => { const m = buildComplianceMatching(context) return { title: { text: '上报合规审计摘要', subtext: COMPLIANCE_FOOTNOTE_AUDIT, left: 'center', textStyle: { fontSize: 14 }, subtextStyle: { fontSize: 11, color: '#64748b' }, }, tooltip: { trigger: 'axis' }, legend: { top: 72, data: ['占比%'] }, grid: { top: 110, right: 24, bottom: 56, left: 48 }, xAxis: { type: 'category', data: ['漏报率', '匹配率', '时效(登记)', '时效(C3)'], axisLabel: { interval: 0, rotate: 0 }, }, yAxis: { type: 'value', name: '%', max: 100 }, series: [ { name: '占比%', type: 'bar', data: [m.leakRate, m.matchRate, m.timelinessFromRegistrationPct, m.timelinessFromC3Pct], itemStyle: { borderRadius: [4, 4, 0, 0] }, }, ], } } const complianceAuditCountsOption = (context: AnalysisContext): EChartsOption => { const m = buildComplianceMatching(context) return { title: { text: '应报样本量(投诉 isAe=是)', subtext: `基准 ${m.flaggedTotal} 条|匹配 ${m.matched}|疑似漏报 ${m.missed}|CMP-CV-101~104`, left: 'center', textStyle: { fontSize: 14 }, subtextStyle: { fontSize: 11, color: '#64748b' }, }, tooltip: { trigger: 'item' }, series: [ { type: 'pie', radius: ['36%', '62%'], label: { formatter: '{b}: {c} ({d}%)' }, data: [ { name: '已匹配', value: m.matched }, { name: '疑似漏报', value: m.missed }, ], }, ], } } const paretoOption = (context: AnalysisContext): EChartsOption => { const pareto = buildComplaintPareto(context) return { title: { text: '产品×故障 Pareto', left: 'center', textStyle: { fontSize: 14 } }, tooltip: { trigger: 'axis' }, legend: { top: 30, data: ['投诉数', '累计占比%'] }, grid: { top: 70, right: 50, bottom: 70, left: 45 }, xAxis: { type: 'category', data: pareto.labels, axisLabel: { rotate: 25 } }, yAxis: [{ type: 'value', name: '投诉数' }, { type: 'value', name: '累计%', max: 100 }], series: [ { name: '投诉数', type: 'bar', data: pareto.bars }, { name: '累计占比%', type: 'line', yAxisIndex: 1, smooth: true, data: pareto.line }, ], } } const faultSeasonalityHeatmapOption = (context: AnalysisContext): EChartsOption => { const { faults, months, cells } = buildFaultByRegisterMonthMatrix(context) const max = Math.max(1, ...cells.map((c) => c.value)) const data = cells.map((c) => [months.indexOf(c.month), faults.indexOf(c.fault), c.value]) return { title: { text: '故障类型 × C3登记月(投诉热力)', subtext: `${QUALITY_DOC_REF}|主策略 §5.2 与 QLT-BTH-203 月份桶一致`, left: 'center', textStyle: { fontSize: 14 }, subtextStyle: { fontSize: 11, color: '#64748b' }, }, tooltip: { position: 'top' }, grid: { top: 88, height: Math.max(220, faults.length * 26), left: 100, right: 24, bottom: 48 }, xAxis: { type: 'category', data: months, splitArea: { show: true } }, yAxis: { type: 'category', data: faults, splitArea: { show: true } }, visualMap: { min: 0, max, calculable: true, orient: 'horizontal', left: 'center', bottom: 4, inRange: { color: ['#ecfdf5', '#065f46'] }, }, series: [{ name: '投诉条数', type: 'heatmap', data, label: { show: true } }], } } const closeCycleHistogramOption = (context: AnalysisContext): EChartsOption => { const days = buildComplaintCloseCycleDays(context) const bins = ['0–7', '8–14', '15–30', '31–60', '>60'] const counts = [0, 0, 0, 0, 0] days.forEach((d) => { if (d <= 7) counts[0] += 1 else if (d <= 14) counts[1] += 1 else if (d <= 30) counts[2] += 1 else if (d <= 60) counts[3] += 1 else counts[4] += 1 }) return { title: { text: '投诉关闭周期分布', subtext: 'QLT-RPT-802|关闭日期 − C3登记日期', left: 'center', textStyle: { fontSize: 14 }, subtextStyle: { fontSize: 11, color: '#64748b' }, }, tooltip: { trigger: 'axis' }, grid: { top: 80, right: 24, bottom: 40, left: 44 }, xAxis: { type: 'category', data: bins }, yAxis: { type: 'value', name: '投诉条数' }, series: [{ type: 'bar', data: counts, itemStyle: { borderRadius: [4, 4, 0, 0] } }], } } const hospitalAeScatterOption = (context: AnalysisContext): EChartsOption => { const pts = buildHospitalAeVsAdmissionScatter(context) const data = pts.map((p) => [p.cyQty, p.aeCount, p.hospital]) return { title: { text: '医院:AE 条数 vs 入院 cyQty 汇总', subtext: 'QLT-AE-1003|AE 按单位名称计数;入院量按医院汇总', left: 'center', textStyle: { fontSize: 14 }, subtextStyle: { fontSize: 11, color: '#64748b' }, }, tooltip: { formatter: (params: unknown) => { const p = params as { value?: [number, number, string] } const v = p.value if (!v) return '' return `${v[2]}
cyQty:${v[0]}
AE:${v[1]}` }, }, grid: { top: 88, right: 24, bottom: 44, left: 52 }, xAxis: { type: 'value', name: 'cyQty', scale: true }, yAxis: { type: 'value', name: 'AE 条数', scale: true }, series: [{ type: 'scatter', symbolSize: 12, data }], } } const hospitalComplaintQtyScatterOption = (context: AnalysisContext): EChartsOption => { const pts = buildHospitalComplaintVsAdmissionQty(context) const data = pts.map((p) => [p.cyQty, p.complaints, p.hospital]) return { title: { text: '医院:投诉条数 vs 入院 cyQty', subtext: QLT_TIME_AXIS, left: 'center', textStyle: { fontSize: 14 }, subtextStyle: { fontSize: 11, color: '#64748b' }, }, tooltip: { formatter: (params: unknown) => { const p = params as { value?: [number, number, string] } const v = p.value if (!v) return '' return `${v[2]}
cyQty:${v[0]}
投诉:${v[1]}` }, }, grid: { top: 88, right: 24, bottom: 44, left: 52 }, xAxis: { type: 'value', name: 'cyQty', scale: true }, yAxis: { type: 'value', name: '投诉条数', scale: true }, series: [{ type: 'scatter', symbolSize: 12, data }], } } const productHalfYearBarOption = (context: AnalysisContext): EChartsOption => { const h1 = new Map() const h2 = new Map() for (const c of context.complaints) { const mo = Number.parseInt(c.registerDate.slice(5, 7), 10) const p = c.productName if (mo <= 6) h1.set(p, (h1.get(p) ?? 0) + 1) else h2.set(p, (h2.get(p) ?? 0) + 1) } const products = [...new Set([...h1.keys(), ...h2.keys()])].sort() return { title: { text: '产品投诉量:上半年 vs 下半年', subtext: 'C3登记月份 1–6 vs 7–12|QLT-TRN-703 对比窗演示', left: 'center', textStyle: { fontSize: 14 }, subtextStyle: { fontSize: 11, color: '#64748b' }, }, tooltip: { trigger: 'axis' }, legend: { top: 72 }, grid: { top: 100, right: 24, bottom: 56, left: 44 }, xAxis: { type: 'category', data: products, axisLabel: { rotate: 22 } }, yAxis: { type: 'value', name: '投诉条数' }, series: [ { name: '上半年(1–6月)', type: 'bar', data: products.map((p) => h1.get(p) ?? 0) }, { name: '下半年(7–12月)', type: 'bar', data: products.map((p) => h2.get(p) ?? 0) }, ], } } const productGrowthAlertBarOption = (context: AnalysisContext): EChartsOption => { const alertRows = buildProductComplaintHalfYearGrowth(context) const h1 = new Map() const h2 = new Map() for (const c of context.complaints) { const mo = Number.parseInt(c.registerDate.slice(5, 7), 10) if (mo <= 6) h1.set(c.productName, (h1.get(c.productName) ?? 0) + 1) else h2.set(c.productName, (h2.get(c.productName) ?? 0) + 1) } const labels = alertRows.length ? alertRows.map((r) => r.product) : [...new Set(context.complaints.map((c) => c.productName))].sort() return { title: { text: '产品投诉增速预警(下半年相对上半年 ≥20%)', subtext: alertRows.length ? 'QLT-TRN-703' : 'QLT-TRN-703|当前样本无达阈产品,展示全产品上下半年对照', left: 'center', textStyle: { fontSize: 14 }, subtextStyle: { fontSize: 11, color: '#64748b' }, }, tooltip: { trigger: 'axis' }, legend: { top: 72 }, grid: { top: 100, right: 24, bottom: 56, left: 44 }, xAxis: { type: 'category', data: labels, axisLabel: { rotate: 22 } }, yAxis: { type: 'value', name: '投诉条数' }, series: [ { name: '上半年', type: 'bar', data: labels.map((p) => h1.get(p) ?? 0) }, { name: '下半年', type: 'bar', data: labels.map((p) => h2.get(p) ?? 0) }, ], } } const provinceHalfYearBarOption = (context: AnalysisContext): EChartsOption => { const rows = buildProvinceComplaintHalfYearGrowth(context) const labels = rows.map((r) => r.province) return { title: { text: '省份投诉量:上半年 vs 下半年', subtext: '医院→省映射取自入院量表众数|QLT-TRN-704', left: 'center', textStyle: { fontSize: 14 }, subtextStyle: { fontSize: 11, color: '#64748b' }, }, tooltip: { trigger: 'axis' }, legend: { top: 72 }, grid: { top: 100, right: 24, bottom: 48, left: 44 }, xAxis: { type: 'category', data: labels }, yAxis: { type: 'value', name: '投诉条数' }, series: [ { name: '上半年', type: 'bar', data: rows.map((r) => r.firstHalf) }, { name: '下半年', type: 'bar', data: rows.map((r) => r.secondHalf) }, ], } } const complaintRateBarOption = (context: AnalysisContext): EChartsOption => { const rows = buildHospitalProductComplaintRate(context).slice(0, 10) const labels = rows.map((r) => `${r.hospital} / ${r.product}`) return { title: { text: '医院×产品 投诉率 Top(每千件入院量)', subtext: `QLT-SAL-901|${QLT_ADMISSION_DENOM}`, left: 'center', textStyle: { fontSize: 14 }, subtextStyle: { fontSize: 11, color: '#64748b' }, }, tooltip: { trigger: 'axis' }, grid: { top: 88, right: 24, bottom: 8, left: 8, containLabel: true }, xAxis: { type: 'value', name: '件/千件' }, yAxis: { type: 'category', data: labels, axisLabel: { fontSize: 10 } }, series: [{ type: 'bar', data: rows.map((r) => r.ratePer1000), itemStyle: { borderRadius: [0, 4, 4, 0] } }], } } const isAeRateBarOption = (context: AnalysisContext): EChartsOption => { const rows = buildIsAeRateByProduct(context) return { title: { text: '按产品的投诉升级率(是否不良事件=是)', subtext: 'QLT-AE-1001', left: 'center', textStyle: { fontSize: 14 }, subtextStyle: { fontSize: 11, color: '#64748b' }, }, tooltip: { trigger: 'axis' }, grid: { top: 72, right: 24, bottom: 48, left: 44 }, xAxis: { type: 'category', data: rows.map((r) => r.product), axisLabel: { rotate: 18 } }, yAxis: { type: 'value', name: '%', max: 100 }, series: [{ type: 'bar', data: rows.map((r) => r.ratePct), itemStyle: { borderRadius: [4, 4, 0, 0] } }], } } const investigationPieOption = (context: AnalysisContext): EChartsOption => { const dist = buildInvestigationConclusionDist(context) return { title: { text: '调查结论分布', subtext: 'QLT-INV-501', left: 'center', textStyle: { fontSize: 14 }, subtextStyle: { fontSize: 11, color: '#64748b' }, }, tooltip: { trigger: 'item' }, series: [ { type: 'pie', radius: ['34%', '60%'], label: { formatter: '{b}: {c} ({d}%)' }, data: dist.map(([name, value]) => ({ name, value })), }, ], } } const compensationPieOption = (context: AnalysisContext): EChartsOption => { const dist = buildCompensationDist(context) return { title: { text: '赔付结论分布', subtext: 'QLT-CST-601', left: 'center', textStyle: { fontSize: 14 }, subtextStyle: { fontSize: 11, color: '#64748b' }, }, tooltip: { trigger: 'item' }, series: [ { type: 'pie', radius: ['34%', '60%'], label: { formatter: '{b}: {c} ({d}%)' }, data: dist.map(([name, value]) => ({ name, value })), }, ], } } const defectProductFaultBarOption = (context: AnalysisContext): EChartsOption => { const d = buildDefectConfirmedProductFault(context) return barOption('产品缺陷成立:产品×故障 Top', d.labels, d.values, 'QLT-INV-502') } const batchComplaintAeBarOption = (context: AnalysisContext): EChartsOption => { const rows = buildBatchComplaintVsAeCounts(context).filter((r) => r.complaints + r.aeReports > 0) return { title: { text: '批号:投诉条数 vs AE 条数', subtext: 'QLT-BTH-104|同批号字符串对照(非逐条匹配)', left: 'center', textStyle: { fontSize: 14 }, subtextStyle: { fontSize: 11, color: '#64748b' }, }, tooltip: { trigger: 'axis' }, legend: { top: 72 }, grid: { top: 100, right: 24, bottom: 48, left: 44 }, xAxis: { type: 'category', data: rows.map((r) => r.batchNo), axisLabel: { rotate: 18 } }, yAxis: { type: 'value', name: '条数' }, series: [ { name: '投诉', type: 'bar', data: rows.map((r) => r.complaints) }, { name: 'AE', type: 'bar', data: rows.map((r) => r.aeReports) }, ], } } const complaintMonthlyLineOption = (context: AnalysisContext): EChartsOption => { const t = buildComplaintMonthly(context) return lineOption('投诉月度趋势(C3登记月)', t.months, t.values, '投诉条数', `${QUALITY_DOC_REF}|QLT 时间轴`) } const marketingHBarOption = ( title: string, subtext: string, labels: string[], values: number[], xAxisName: string, ): EChartsOption => ({ title: { text: title, subtext, left: 'center', textStyle: { fontSize: 14 }, subtextStyle: { fontSize: 11, color: '#64748b' }, }, tooltip: { trigger: 'axis' }, grid: { top: 88, right: 28, bottom: 12, left: 12, containLabel: true }, xAxis: { type: 'value', name: xAxisName }, yAxis: { type: 'category', data: labels, axisLabel: { fontSize: 10 } }, series: [{ type: 'bar', data: values, itemStyle: { borderRadius: [0, 4, 4, 0] } }], }) const mktOpWrongHospitalOption = (context: AnalysisContext): EChartsOption => { const d = buildOpWrongHospitalTop(context, 14) return barOption('操作不当投诉:医院 Top', d.labels, d.values, 'MKT-OP-101') } const mktOpWrongDealerCountOption = (context: AnalysisContext): EChartsOption => { const d = buildOpWrongDealerCounts(context, 14) return barOption('操作不当投诉:经销商(条数)', d.labels, d.values, 'MKT-OP-102') } const mktDealerOpWrongRateOption = (context: AnalysisContext): EChartsOption => { const d = buildDealerOpWrongRatePer1000(context, 14) return marketingHBarOption( '经销商操作不当率(件/千件入院量)', `MKT-OP-103|${MARKETING_DOC_REF}`, d.labels, d.values, '件/千件', ) } const mktProductOpWrongShareOption = (context: AnalysisContext): EChartsOption => { const d = buildProductOpWrongSharePct(context, 12) return marketingHBarOption( '产品:操作不当占该产品投诉比例(%)', 'MKT-OP-104|子集=调查结论为「操作不当」', d.labels, d.values, '%', ) } const mktProvinceComplaintRateOption = (context: AnalysisContext): EChartsOption => { const d = buildProvinceComplaintRatePer1000(context, 16) return marketingHBarOption( '省级投诉率(件/千件入院量)', `MKT-RG-201|${MKT_ADMISSION_DENOM}`, d.labels, d.values, '件/千件', ) } const mktDealerComplaintRateOption = (context: AnalysisContext): EChartsOption => { const d = buildDealerComplaintRatePer1000(context, 16) return marketingHBarOption( '经销商投诉率(件/千件入院量)', `MKT-RG-202|${MARKETING_DOC_REF}`, d.labels, d.values, '件/千件', ) } const mktProvinceComplaintCountOption = (context: AnalysisContext): EChartsOption => { const d = buildProvinceComplaintCounts(context, 14) return barOption('省级投诉条数 Top(绝对量)', d.labels, d.values, 'MKT-RG-203') } const configs: Record = { 'compliance-psur': { key: 'compliance-psur', title: 'PSUR 监管分析', pillar: 'monitor', strategyRef: '合规方向分析 §2', subtitle: '监测层:按《合规方向分析》CMP-AE-101/201/301/401/501/601/701 等演示实现。', sections: [ { id: 'psur-trend', title: '总体趋势与暴露', strategyRefs: ['CMP-AE-101', 'CMP-AE-201', 'CMP-AE-301'], charts: [ { id: 'psur-monthly', title: '不良事件月度趋势(按发生日期)', description: COMPLIANCE_FOOTNOTE_SHORT, chartExplain: ex( '不良事件表;时间过滤=发生日期 occurrenceDate;统计单元=每条 AE 记录计 1。', 'CMP-AE-101:按公历月对报告条数求和,用于 PSUR「总体趋势」与异常上升监测。', ), optionBuilder: (context) => { const trend = buildAeMonthlyByOccurrence(context) return lineOption( 'CMP-AE-101 不良事件报告数', trend.months, trend.values, '事件数', `${COMPLIANCE_DOC_REF}|主时间轴=发生日期|单元=报告条数`, ) }, }, { id: 'psur-ppm', title: '全局月度 PPM(入院量 cyQty 分母)', description: COMPLIANCE_FOOTNOTE_PPM, chartExplain: ex( '分子=AE 按发生月计数;分母=入院量表按业务年月汇总 cyQty(演示未按产品拆分全量暴露)。', 'CMP-AE-201:PPM = AE数/入院量×1e6;分母为 0 时在序列中用 null 表示不可算。', ), optionBuilder: (context) => { const ppm = buildGlobalMonthlyPpm(context) return lineOption( 'CMP-AE-201 演示 PPM', ppm.months, ppm.ppm, 'PPM', `${COMPLIANCE_DOC_REF}|分母=入院量 cyQty 按年月汇总`, ) }, }, { id: 'psur-device-top', title: '器械故障表现 Top(占比)', description: COMPLIANCE_FOOTNOTE_SHORT, chartExplain: ex( '不良事件表 deviceFailure 字段;全量计数。', 'CMP-AE-301:按故障表现归类计数取 Top;tooltip 可扩展占比=该类条数/当期 AE 总数。', ), optionBuilder: (context) => { const top = buildTopDeviceFailures(context, 10) return barOption( 'CMP-AE-301 器械故障 Top', top.labels, top.values, '条数=报告条数;占比见 tooltip', ) }, }, { id: 'psur-product-top', title: '产品事件 Top 分布', description: COMPLIANCE_FOOTNOTE_SHORT, chartExplain: ex('不良事件表 productName 全量。', '按产品聚合 AE 报告条数取 Top,用于 PSUR 产品章节与管理驾驶舱分产品线视图。'), optionBuilder: (context) => { const top = buildPsurTopProducts(context) return barOption('产品 AE 条数 Top', top.labels, top.values, COMPLIANCE_DOC_REF) }, }, ], }, { id: 'psur-serious', title: '严重事件、区域与上报时效', strategyRefs: ['CMP-AE-501', 'CMP-AE-502', 'CMP-AE-601', 'CMP-AE-701'], charts: [ { id: 'psur-sae-monthly', title: 'SAE 报告数(按发生月)', description: 'SAE:死亡/危及生命/残疾/住院或延长住院 任一(演示字段)', chartExplain: ex( '不良事件表;SAE 子集由四个布尔字段任一为真判定(与合规方向分析 §1.4 一致)。', 'CMP-AE-501:在 SAE 子集上按发生月计数,用于严重事件专章趋势。', ), optionBuilder: (context) => { const s = buildSaeMonthlyByOccurrence(context) return lineOption('CMP-AE-501 SAE 条数', s.months, s.values, 'SAE 条数', COMPLIANCE_DOC_REF) }, }, { id: 'psur-sae-share', title: 'SAE 占全部 AE 比例(按发生月)', description: 'CMP-AE-502', chartExplain: ex('同月内 SAE 条数与全部 AE 条数之比,时间轴=发生月。', 'CMP-AE-502:观察严重事件是否在总量上升同时占比上升。'), optionBuilder: (context) => { const s = buildSaeShareMonthly(context) return lineOption('CMP-AE-502 SAE%', s.months, s.shares, '占比%', COMPLIANCE_DOC_REF) }, }, { id: 'psur-province', title: '省级 AE 报告数', description: 'CMP-AE-601', chartExplain: ex('不良事件表 province 字段;全期按省聚合。', 'CMP-AE-601:省级报告条数分布,用于地理聚集识别(地图可视化可后续接入)。'), optionBuilder: (context) => { const p = buildProvinceAeCounts(context) return barOption('CMP-AE-601 省分布', p.labels, p.values, COMPLIANCE_DOC_REF) }, }, { id: 'psur-lag-histogram', title: '上报滞后天数分布', description: 'Lag = 审核日期 − 登记日期|CMP-AE-701', chartExplain: ex( 'AE 表 reviewDate、registrationDate 均可解析的行;Lag=审核日期−登记日期(天)。', 'CMP-AE-701:对滞后天数做直方分箱,支撑上报时效分布与内控阈值讨论。', ), optionBuilder: reportingLagHistogramOption, }, ], }, ], }, 'compliance-audit': { key: 'compliance-audit', title: '上报合规性审计', pillar: 'diagnose', strategyRef: '合规方向分析 §3', subtitle: '诊断层:CMP-CV-101~104 与 CMP-AE-701/702 双口径演示。', sections: [ { id: 'audit-core', title: '审计核心指标', strategyRefs: ['CMP-CV-101', 'CMP-CV-104', 'CMP-AE-702'], charts: [ { id: 'audit-summary-bar', title: '漏报 / 匹配 / 时效(双口径)', description: COMPLIANCE_FOOTNOTE_AUDIT, chartExplain: ex( '基准=投诉 isAe=true;匹配规则=医院+产品+型号一致且 AE 发生日与 C3登记日相差≤30天。', '汇总漏报率、匹配率及匹配子集上「审核−登记」「审核−C3」≤15 天占比(演示阈值)。', ), optionBuilder: complianceAuditSummaryOption, }, { id: 'audit-counts-pie', title: '应报样本结构', description: '投诉 isAe=是', chartExplain: ex( '同上基准集合;在可匹配与疑似漏报二分结构上展示样本量。', '饼图用于审计抽样框说明:已匹配 vs 需人工复核的疑似漏报。', ), optionBuilder: complianceAuditCountsOption, }, ], }, ], }, 'quality-concentration': { key: 'quality-concentration', title: '投诉集中性与 Pareto', pillar: 'diagnose', strategyRef: '质量方向分析 §2–§3', subtitle: '诊断层:QLT-PRD-401 / QLT-BTH-101 / QLT-CNC 系列(集中性)。', sections: [ { id: 'quality-pareto', title: '集中性与批号', strategyRefs: ['QLT-PRD-401', 'QLT-BTH-101', 'QLT-CNC-301~303'], charts: [ { id: 'quality-pareto-chart', title: '产品×故障 Pareto(二八)', description: 'QLT-PRD-401', chartExplain: ex( `质量投诉表;${QLT_TIME_AXIS};取 Top8 产品×故障组合。`, '对「产品名称×故障类型」计数并降序;同时给出累计占比曲线,用于识别贡献约 80% 投诉的组合(Pareto)。', ), optionBuilder: paretoOption, }, { id: 'quality-batch-chart', title: '批号投诉条数排名', description: 'QLT-BTH-101', chartExplain: ex( '质量投诉表 batchNo 字段;空批号归入「(未填批号)」桶。', '在统计期内按批号分组对投诉条数计数并取 Top,用于触发批次调查(问题批次识别)。', ), optionBuilder: (context) => { const batch = buildComplaintBatchTop(context, 8) return barOption('批号投诉 Top', batch.labels, batch.values, QUALITY_DOC_REF) }, }, { id: 'quality-hospital-top', title: '医院投诉条数 Top', description: 'QLT-CNC-301', chartExplain: ex( '质量投诉表;按医院名称聚合。', '统计各医院投诉条数并取 Top,用于识别投诉集中医院(现场调查/培训优先级)。', ), optionBuilder: (context) => { const t = buildTopHospitalsComplaints(context, 8) return barOption('医院投诉 Top', t.labels, t.values, QUALITY_DOC_REF) }, }, { id: 'quality-product-fault-top', title: '故障类型 Top', description: 'QLT-CNC-303', chartExplain: ex('质量投诉表 faultType 字段。', '按故障类型计数取 Top,与 Pareto 组合图互补的一维缺陷雷达。'), optionBuilder: (context) => { const t = buildTopFaultTypes(context, 8) return barOption('故障类型 Top', t.labels, t.values, QUALITY_DOC_REF) }, }, ], }, ], }, 'quality-trends': { key: 'quality-trends', title: '趋势分析', pillar: 'monitor', strategyRef: '质量方向分析 §2.2 / §4.1', subtitle: '监测层:月度趋势与上下半年对比、区域对比(QLT-TRN-703/704)。', sections: [ { id: 'trend-core', title: '时间与对比窗', strategyRefs: ['QLT-TRN-703', 'QLT-TRN-704'], charts: [ { id: 'trend-monthly', title: '投诉月度趋势', description: 'C3登记月', chartExplain: ex( `质量投诉表;${QLT_TIME_AXIS};按公历年月桶聚合。`, '对每个自然月统计投诉条数,观察总量是否异常上升(与主策略 §3.2 趋势主题一致)。', ), optionBuilder: complaintMonthlyLineOption, }, { id: 'trend-product-half', title: '产品:上/下半年投诉量', description: 'QLT-TRN-703', chartExplain: ex( '质量投诉表;以登记日期的月份区分上半年(1–6)与下半年(7–12)。', '按产品对两个半年窗口分别计数,用于与增速预警表对照阅读。', ), optionBuilder: productHalfYearBarOption, }, { id: 'trend-product-alert', title: '产品投诉增速预警', description: 'QLT-TRN-703', chartExplain: ex( '同上、对比窗为同一公历年的上下半年。', '当 (下半年−上半年)/上半年 ≥ 20%(且上半年>0)或「上半年零且下半年≥2」时纳入预警柱图。', ), optionBuilder: productGrowthAlertBarOption, }, { id: 'trend-province-half', title: '省份:上/下半年投诉量', description: 'QLT-TRN-704', chartExplain: ex( '投诉侧按医院映射至省份:映射表由入院量数据中「医院→省」出现频次众数生成。', '将投诉按登记月拆上下半年,在省级桶内计数,用于区域波动监测。', ), optionBuilder: provinceHalfYearBarOption, }, ], }, ], }, 'quality-complaint-sales': { key: 'quality-complaint-sales', title: '投诉行为:销售与入院关联', pillar: 'diagnose', strategyRef: '质量方向分析 §4.3', subtitle: '诊断层:投诉量与入院量关系、医院×产品投诉率(QLT-SAL-901)。', sections: [ { id: 'qc-sales', title: '入院与投诉率', strategyRefs: ['QLT-SAL-901', '主策略 3.3 a'], charts: [ { id: 'qc-sales-scatter', title: '医院:投诉条数 vs 入院 cyQty', description: '散点', chartExplain: ex( '投诉按医院计数;入院量表按 HospitalName 汇总 cyQty(跨月相加,演示口径)。', '用于观察「入院量大是否必然投诉多」;偏离对角带可为异常医院信号(需结合床位/产品结构)。', ), optionBuilder: hospitalComplaintQtyScatterOption, }, { id: 'qc-sales-rate', title: '医院×产品 投诉率 Top(每千件)', description: 'QLT-SAL-901', chartExplain: ex( `分子=统计期内医院+产品投诉条数;分母=入院量表同键 cyQty 汇总。${QLT_ADMISSION_DENOM}。`, '投诉率 = 投诉条数 / cyQty × 1000;取 Top10 横向条形图,突出相对暴露下的高频组合。', ), optionBuilder: complaintRateBarOption, }, ], }, ], }, 'quality-complaint-investigation': { key: 'quality-complaint-investigation', title: '投诉行为:调查与赔付关系', pillar: 'act', strategyRef: '质量方向分析 §3.3–§3.4', subtitle: '决策层:调查结论池、缺陷成立组合、赔付结构(QLT-INV / QLT-CST)。', sections: [ { id: 'qc-investigation', title: '调查与成本', strategyRefs: ['QLT-INV-501', 'QLT-INV-502', 'QLT-CST-601'], charts: [ { id: 'qc-inv-pie', title: '调查结论分布', description: 'QLT-INV-501', chartExplain: ex( '质量投诉表 conclusion(调查结论)字段全量有效行。', '对结论类别计数并计算占比,观察「产品缺陷成立」等结构是否恶化。', ), optionBuilder: investigationPieOption, }, { id: 'qc-defect-bar', title: '产品缺陷成立:产品×故障 Top', description: 'QLT-INV-502', chartExplain: ex( '子集:调查结论=「产品缺陷成立」的投诉记录。', '在子集内按「产品×故障」计数排序,形成已确认质量问题池,供 CAPA/设计变更引用。', ), optionBuilder: defectProductFaultBarOption, }, { id: 'qc-comp-pie', title: '赔付结论分布', description: 'QLT-CST-601', chartExplain: ex('质量投诉表 compensation(赔付结论)字段。', '统计各类赔付方式占比,作为质量失败外显成本(COQ)结构的入口指标。'), optionBuilder: compensationPieOption, }, { id: 'qc-product-top', title: '产品投诉条数 Top', description: 'QLT-CNC-302', chartExplain: ex('质量投诉表 productName。', '按产品聚合投诉条数,与调查/赔付饼图联读以定位重点产品。'), optionBuilder: (context) => { const t = buildTopProductsComplaints(context, 8) return barOption('产品投诉 Top', t.labels, t.values, QUALITY_DOC_REF) }, }, ], }, ], }, 'quality-complaint-batch': { key: 'quality-complaint-batch', title: '投诉行为:批次与稳定性', pillar: 'diagnose', strategyRef: '质量方向分析 §2', subtitle: '诊断层:批号与 AE 双通道对照、关闭周期(QLT-BTH-104 / QLT-RPT-802)。', sections: [ { id: 'qc-batch', title: '批次与闭环', strategyRefs: ['QLT-BTH-104', 'QLT-RPT-802'], charts: [ { id: 'qc-batch-ae', title: '批号:投诉 vs AE 条数', description: 'QLT-BTH-104', chartExplain: ex( '投诉侧 batchNo;AE 侧 productBatch(batchNo);剔除双方均为 0 的批号。', '同一批号字符串在投诉表 batchNo 与 AE 表 batchNo 上分别计数,用于市场反馈与监管报告双通道对照(非逐条匹配)。', ), optionBuilder: batchComplaintAeBarOption, }, { id: 'qc-close-cycle', title: '投诉关闭周期分布', description: 'QLT-RPT-802', chartExplain: ex( '仅统计 closeDate、registerDate 均可解析且差分≥0 的投诉行。', '关闭周期=关闭日期−C3登记日期,按天分段聚合为直方图,用于流程效率与结案 SLA 讨论。', ), optionBuilder: closeCycleHistogramOption, }, ], }, ], }, 'quality-complaint-text': { key: 'quality-complaint-text', title: '投诉行为:文本与流程类', pillar: 'goal', strategyRef: '3.3 p-x', subtitle: '目标层,占位展示文本挖掘与流程效率方向。', sections: [{ id: 'qc-text', title: '主题组 D', strategyRefs: ['3.3 p-x'], charts: [...placeholderPair('qc-text-a'), ...placeholderPair('qc-text-b')] }], }, 'quality-ae-cause': { key: 'quality-ae-cause', title: 'AE 报告原因推测', pillar: 'diagnose', strategyRef: '质量方向分析 §4.4', subtitle: '诊断层:AE 与入院量散点、按产品升级率(QLT-AE-1001/1003)。', sections: [ { id: 'ae-cause', title: '报告行为与升级', strategyRefs: ['QLT-AE-1003', 'QLT-AE-1001'], charts: [ { id: 'ae-scatter-hosp', title: '医院:AE 条数 vs 入院 cyQty', description: 'QLT-AE-1003', chartExplain: ex( 'AE 表按 unitName 计数报告条数;入院表按 HospitalName 汇总 cyQty;AE 未按投诉时间轴过滤。', '用于发现「相对入院量异常高报告」的医院点;解读需结合合规口径(发生日期)与报告动机,不等同于质量差。', ), optionBuilder: hospitalAeScatterOption, }, { id: 'ae-upgrade-rate', title: '按产品的投诉升级率', description: 'QLT-AE-1001', chartExplain: ex( '质量投诉表 isAe(是否不良事件)与 productName。', '升级率 = 该产品 isAe=true 的投诉条数 / 该产品总投诉条数 ×100%,比较各产品线升级意愿差异。', ), optionBuilder: isAeRateBarOption, }, ], }, ], }, 'quality-ae-only': { key: 'quality-ae-only', title: '报告不投诉分析', pillar: 'monitor', strategyRef: '3.4.2', subtitle: '监测层:AE 器械故障分布(演示)。', sections: [ { id: 'ae-only', title: '仅 AE 报告', strategyRefs: ['3.4.2'], charts: [ { id: 'ae-only-failure', title: 'AE 器械故障表现分布', description: '演示', chartExplain: ex( '不良事件表全量;字段 deviceFailure。', '对仅走监管通道的报告,按器械故障表现聚类,辅助理解「未并发投诉」的事件形态(主策略 §3.4.2 占位深化方向)。', ), optionBuilder: (context) => { const top = buildTopDeviceFailures(context, 8) return barOption('AE 故障 Top', top.labels, top.values, COMPLIANCE_DOC_REF) }, }, { id: 'ae-only-monthly', title: 'AE 月度条数(发生日期)', description: '演示', chartExplain: ex('不良事件表 occurrenceDate 按月桶。', '与投诉月度趋势对照时须在业务上区分两条时间轴。'), optionBuilder: (context) => { const t = buildAeMonthlyByOccurrence(context) return lineOption('AE 月度', t.months, t.values, 'AE 条数', COMPLIANCE_DOC_REF) }, }, ], }, ], }, 'quality-ae-with-complaint': { key: 'quality-ae-with-complaint', title: '报告且投诉分析', pillar: 'diagnose', strategyRef: '3.4.3', subtitle: '诊断层:已升级投诉(isAe=true)的故障类型分布(演示)。', sections: [ { id: 'ae-with-complaint', title: '报告+投诉交集', strategyRefs: ['3.4.3'], charts: [ { id: 'ae-with-fault', title: '已升级投诉的故障类型分布', description: 'isAe=true', chartExplain: ex( '子集:质量投诉表 isAe=true。', '在「已升级」子集上按 faultType 计数,用于与 AE 表伤害/故障矩阵叙事衔接(细粒度匹配规则见合规审计主题)。', ), optionBuilder: (context) => { const sub = context.complaints.filter((c) => c.isAe) const m = new Map() sub.forEach((c) => m.set(c.faultType, (m.get(c.faultType) ?? 0) + 1)) const sorted = [...m.entries()].sort((a, b) => b[1] - a[1]) return barOption( '升级投诉故障类型', sorted.map((i) => i[0]), sorted.map((i) => i[1]), QUALITY_DOC_REF, ) }, }, { id: 'ae-with-monthly', title: '已升级投诉的登记月份趋势', description: 'C3登记月', chartExplain: ex('子集:isAe=true;时间字段 registerDate。', '观察升级投诉在登记时间上的分布,用于与 AE 发生月趋势对照。'), optionBuilder: (context) => { const sub = context.complaints.filter((c) => c.isAe) const counter = new Map() sub.forEach((c) => { const k = c.registerDate.slice(0, 7) counter.set(k, (counter.get(k) ?? 0) + 1) }) const months = [...counter.keys()].sort() return lineOption('升级投诉月度', months, months.map((x) => counter.get(x) ?? 0), '条数', QUALITY_DOC_REF) }, }, ], }, ], }, 'marketing-operation-training': { key: 'marketing-operation-training', title: '操作不当与培训', pillar: 'act', strategyRef: '营销方向分析 §2', subtitle: '决策层:MKT-OP-101~104,支撑医院清单、经销商辅导与培训素材优先级。', sections: [ { id: 'marketing-operation', title: '培训与使用支持', strategyRefs: ['MKT-OP-101', 'MKT-OP-102', 'MKT-OP-103', 'MKT-OP-104'], charts: [ { id: 'mkt-op-hospital', title: '操作不当投诉:医院分布', description: 'MKT-OP-101', chartExplain: ex( `投诉表子集:调查结论=「操作不当」;${MKT_TIME_AXIS};全量 mock。`, '按医院名称计数操作不当投诉条数并取 Top,用于院内培训与跟台优先级(主策略 §4.1)。', ), optionBuilder: mktOpWrongHospitalOption, }, { id: 'mkt-op-dealer-count', title: '操作不当投诉:经销商条数', description: 'MKT-OP-102', chartExplain: ex( `同上子集;${MKT_HOSPITAL_MAP} 将投诉归入经销商后计数。`, '识别操作不当投诉绝对量高的经销商,作为渠道侧约谈与联合巡院名单输入。', ), optionBuilder: mktOpWrongDealerCountOption, }, { id: 'mkt-op-dealer-rate', title: '经销商操作不当率(入院量分母)', description: 'MKT-OP-103', chartExplain: ex( `分子=各经销商子集内操作不当投诉条数;分母=入院量表同 DealerName 的 CY Qty 汇总;${MKT_ADMISSION_DENOM}。`, '率=分子/分母×1000(件/千件),用于发现「相对销量操作不当偏多」的经销商(主策略 §4.1)。', ), optionBuilder: mktDealerOpWrongRateOption, }, { id: 'mkt-op-product-share', title: '产品:操作不当占比', description: 'MKT-OP-104', chartExplain: ex( '全量投诉按产品计数分母;分子为该产品调查结论=操作不当的条数。', '占比=操作不当条数/该产品投诉总条数×100%,用于说明书、操作视频与数字化推广素材的迭代优先级。', ), optionBuilder: mktProductOpWrongShareOption, }, ], }, ], }, 'marketing-region-dealer': { key: 'marketing-region-dealer', title: '区域与经销商投诉率', pillar: 'monitor', strategyRef: '营销方向分析 §3', subtitle: '监测层:MKT-RG-201~203,区分相对率与绝对量。', sections: [ { id: 'marketing-region', title: '区域与渠道', strategyRefs: ['MKT-RG-201', 'MKT-RG-202', 'MKT-RG-203'], charts: [ { id: 'mkt-rg-province-rate', title: '省级投诉率(件/千件入院量)', description: 'MKT-RG-201', chartExplain: ex( `投诉经医院映射至省(${MKT_HOSPITAL_MAP});分母为入院量同省 CY Qty 汇总;${MKT_TIME_AXIS}。`, '投诉率=投诉条数/入院量×1000,用于区域活动与资源排序(主策略 §4.2)。', ), optionBuilder: mktProvinceComplaintRateOption, }, { id: 'mkt-rg-dealer-rate', title: '经销商投诉率(件/千件入院量)', description: 'MKT-RG-202', chartExplain: ex( `投诉经医院映射至经销商众数;分母为入院量表 DealerName 维度 CY Qty 汇总。`, '用于渠道储运、覆盖密度与客户支持资源的再分配假设验证(主策略 §4.2)。', ), optionBuilder: mktDealerComplaintRateOption, }, { id: 'mkt-rg-province-count', title: '省级投诉条数 Top(绝对量)', description: 'MKT-RG-203', chartExplain: ex( '全量投诉按省计数,不按入院量标准化。', '与省级投诉率对照,区分「人口/销量大省带来的绝对量大」与「相对率异常高」。', ), optionBuilder: mktProvinceComplaintCountOption, }, ], }, ], }, 'substance-matrix': { key: 'substance-matrix', title: '伤害 × 故障矩阵', pillar: 'diagnose', strategyRef: '合规方向分析 §2.4 / 主策略 5.1', subtitle: '诊断层:CMP-AE-401 全量与 CMP-AE-402 SAE 子集对照。', sections: [ { id: 'substance-matrix', title: '关联矩阵', strategyRefs: ['CMP-AE-401', 'CMP-AE-402'], charts: [ { id: 'matrix-all', title: '伤害 × 器械故障(全量 AE)', description: COMPLIANCE_FOOTNOTE_SHORT, chartExplain: ex( '不良事件表 injuryExpression × deviceFailure;时间过滤默认全量(可按发生日期窗扩展)。', 'CMP-AE-401:二维计数矩阵,识别高频伤害-故障组合以支撑 CAPA 优先级。', ), optionBuilder: injuryDeviceHeatmapOption( false, 'CMP-AE-401 伤害-故障热力图', `${COMPLIANCE_DOC_REF}|按发生日期纳入全集`, ), }, { id: 'matrix-sae', title: '伤害 × 器械故障(SAE 子集)', description: 'CMP-AE-402', chartExplain: ex( '在 SAE 子集(四严重标准任一)上重复 CMP-AE-401 的矩阵统计。', 'CMP-AE-402:聚焦严重结局组合模式,用于紧急医学评估与对外报告要点。', ), optionBuilder: injuryDeviceHeatmapOption( true, 'CMP-AE-402 SAE 子集', '死亡/危及生命/残疾/住院或延长住院 任一', ), }, ], }, ], }, 'substance-seasonality': { key: 'substance-seasonality', title: '故障季节性分析', pillar: 'monitor', strategyRef: '主策略 §5.2 / 质量方向分析 §5', subtitle: '监测层:投诉侧「故障类型×登记月」热力图(与质量方向月度桶一致)。', sections: [ { id: 'substance-seasonality', title: '季节性模式', strategyRefs: ['5.2', 'QLT-BTH-203'], charts: [ { id: 'seasonality-heatmap', title: '故障类型 × C3登记月', description: '投诉热力', chartExplain: ex( '质量投诉表 faultType × registerDate 公历月份;全量 mock。', '单元格=该月该故障类型的投诉条数;颜色越深投诉越多,用于观察高温/雨季等是否与某类故障同向波动(探索性,需外部气候数据佐证)。', ), optionBuilder: faultSeasonalityHeatmapOption, }, { id: 'seasonality-monthly-total', title: '投诉月度总量(对照)', description: 'QLT 时间轴', chartExplain: ex('质量投诉表 registerDate 按月聚合。', '给出各月投诉总量曲线,与热力图行合计一致,便于读趋势峰值月份。'), optionBuilder: complaintMonthlyLineOption, }, ], }, ], }, 'portfolio-persona': { key: 'portfolio-persona', title: '安全维度客户画像', pillar: 'goal', strategyRef: '6', subtitle: '目标层,占位展示高投诉高复购客户画像。', sections: [{ id: 'portfolio-persona', title: '客户画像', strategyRefs: ['6'], charts: placeholderPair('portfolio-persona') }], }, 'portfolio-integrated': { key: 'portfolio-integrated', title: '综合联动分析', pillar: 'act', strategyRef: '7', subtitle: '决策层,支撑监管风险热区与优先级矩阵。', sections: [{ id: 'portfolio-integrated', title: '综合联动', strategyRefs: ['7'], charts: placeholderPair('portfolio-integrated') }], }, } export function getAnalysisDefinition(key: string): AnalysisPageDefinition | undefined { return configs[key] }