diff --git a/dashboard-frontend/src/features/boss-dashboard/archive.ts b/dashboard-frontend/src/features/boss-dashboard/archive.ts new file mode 100644 index 0000000..3d52a56 --- /dev/null +++ b/dashboard-frontend/src/features/boss-dashboard/archive.ts @@ -0,0 +1,215 @@ +import { formatMetricValue, formatMoney, formatNumber } from '../../utils/format' +import type { DashboardOverview, MetricStatus, RankItem, RiskAlert, RiskLevel, SnapshotSlot, TodaySnapshot } from './types' + +const snapshotTitle: Record = { + '1015': '上午抢购快报', + '1455': '下午寄卖/转卖快报', +} + +const snapshotDescription: Record = { + '1015': '用户集中抢购上一天用户寄卖的商品,重点看成交、付款和采购用户是否达标。', + '1455': '用户把上午抢到的商品继续寄卖或转卖,重点看新增寄售供给和奖金变化是否正常。', +} + +const metricStatusText: Record = { + normal: '正常', + success: '达标', + warning: '关注', + danger: '异常', +} + +const riskLevelText: Record = { + red: '红色', + yellow: '黄色', + gray: '灰色', +} + +function escapeHtml(value: unknown): string { + return String(value ?? '') + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", ''') +} + +function serializeStaticData(data: DashboardOverview): string { + return JSON.stringify(data, null, 2).replaceAll('<', '\\u003c').replaceAll('>', '\\u003e') +} + +function renderMetricGrid(metrics: DashboardOverview['kpis']): string { + return metrics + .map( + (metric) => ` +
+ ${escapeHtml(metricStatusText[metric.status])} +

${escapeHtml(metric.title)}

+ ${escapeHtml(formatMetricValue(metric.value, metric.unit))} + ${metric.trendLabel ? `

${escapeHtml(metric.trendLabel)} ${escapeHtml(metric.trendValue ?? '')}%

` : ''} +
`, + ) + .join('') +} + +function renderSnapshots(snapshots: TodaySnapshot[]): string { + return snapshots + .map((snapshot) => { + const bonusChange = Number(snapshot.selfBonusChange) + Number(snapshot.shareBonusChange) + return ` +
+
+
+ ${escapeHtml(snapshot.slot)} +

${escapeHtml(snapshotTitle[snapshot.slot])}

+
+ ${escapeHtml(snapshot.status)} +
+

${escapeHtml(snapshotDescription[snapshot.slot])}

+

${escapeHtml(snapshot.message)}

+ ${snapshot.generatedAt ? `生成时间:${escapeHtml(snapshot.generatedAt)}` : ''} +
+ 用户${escapeHtml(formatNumber(snapshot.purchaseUsers))}人 + 订单${escapeHtml(formatNumber(snapshot.orderCount))}单 + 成交额${escapeHtml(formatMoney(snapshot.dealAmount))} + 已支付${escapeHtml(formatMoney(snapshot.paidAmount))} + 商品${escapeHtml(formatNumber(snapshot.newMerchandiseCount))}件 + 奖金${escapeHtml(formatMoney(bonusChange))} +
+
` + }) + .join('') +} + +function renderRanks(title: string, ranks: RankItem[]): string { + return ` +
+

${escapeHtml(title)}

+
+ ${ranks + .map( + (rank, index) => ` +
+ ${index + 1} + + ${escapeHtml(rank.name)} + ${escapeHtml(rank.description)} + + ${escapeHtml(formatMoney(rank.value))} +
`, + ) + .join('')} +
+
` +} + +function renderRisks(risks: RiskAlert[]): string { + return risks + .map( + (risk) => ` +
+
+ ${escapeHtml(riskLevelText[risk.level])} + ${escapeHtml(risk.type)} + +
+ ${escapeHtml(risk.title)} +

${escapeHtml(risk.description)}

+
`, + ) + .join('') +} + +export function buildDailyReportArchiveHtml(data: DashboardOverview): string { + const generatedAt = new Date().toLocaleString('zh-CN', { hour12: false }) + + return ` + + + + + 经营日报归档 - ${escapeHtml(data.businessDate)} + + + +
+
+

Daily Report Archive

+

经营日报归档 ${escapeHtml(data.businessDate)}

+

${escapeHtml(data.summary)}

+
+ 业务日期:${escapeHtml(data.businessDate)} + 数据生成:${escapeHtml(data.generatedAt)} + 归档生成:${escapeHtml(generatedAt)} +
+
+

核心指标

${renderMetricGrid(data.kpis)}
+

资金池摘要

${renderMetricGrid(data.fundPool)}
+

今日快报

${renderSnapshots(data.snapshots)}
+
+

最近趋势

+
+ ${data.trends + .map( + (trend) => ` +
+ ${escapeHtml(trend.date)} + 成交 ${escapeHtml(formatMoney(trend.amount))} + 订单 ${escapeHtml(formatNumber(trend.orders))} 单 + 奖金 ${escapeHtml(formatMoney(trend.bonus))} +
`, + ) + .join('')} +
+
+ ${renderRanks('高价值用户', data.userRanks)} + ${renderRanks('团队贡献排行', data.teamRanks)} + ${renderRanks('高货值未成交商品', data.productRanks)} +

风险预警

${renderRisks(data.risks)}
+ +
+ +` +} diff --git a/dashboard-frontend/src/features/boss-dashboard/pages/OperationsPages.tsx b/dashboard-frontend/src/features/boss-dashboard/pages/OperationsPages.tsx index 26b37e0..9247b64 100644 --- a/dashboard-frontend/src/features/boss-dashboard/pages/OperationsPages.tsx +++ b/dashboard-frontend/src/features/boss-dashboard/pages/OperationsPages.tsx @@ -3,7 +3,8 @@ import { useMemo, useState } from 'react' import { MiniTrendChart } from '../../../components/charts/MiniTrendChart' import { KpiCard } from '../../../components/kpi/KpiCard' import { formatMoney, formatNumber } from '../../../utils/format' -import { downloadDailyReportArchive, useDashboardOverview } from '../api' +import { useDashboardOverview } from '../api' +import { buildDailyReportArchiveHtml } from '../archive' import type { DashboardOverview, RiskLevel, SnapshotSlot, TodaySnapshot } from '../types' const snapshotStatusMeta = { @@ -187,7 +188,8 @@ export function DailyReportPage() { const handleArchive = async () => { try { setIsArchiving(true) - const blob = await downloadDailyReportArchive(data.businessDate) + const html = buildDailyReportArchiveHtml(data) + const blob = new Blob([html], { type: 'text/html;charset=utf-8' }) const url = URL.createObjectURL(blob) const link = document.createElement('a') link.href = url