v4.0.14概览和日报
CI / verify (pull_request) Waiting to run

This commit is contained in:
2026-09-02 21:11:58 +08:00
parent e5fe8b903a
commit 68a4b7f984
41 changed files with 3815 additions and 1098 deletions
+6 -44
View File
@@ -147,50 +147,12 @@ export type DashboardStats = {
ordersByStatus: Array<{ status: string; count: number }>;
};
export type DashboardAnalytics = {
summary: {
users: number;
orders: number;
payingUsers: number;
partners: number;
stores: number;
redeems: number;
redeemAmount: number;
};
byDate: Array<{
date: string;
users: number;
orders: number;
partners: number;
stores: number;
redeems: number;
redeemAmount: number;
}>;
byCity: Array<{
cityId: string;
cityName: string;
users: number;
orders: number;
partners: number;
stores: number;
redeems: number;
redeemAmount: number;
}>;
byPromo: Array<{
promoCodeId: string | null;
code: string;
name: string;
users: number;
orders: number;
}>;
byPartner: Array<{
partnerAccountId: string;
companyName: string;
stores: number;
redeems: number;
redeemAmount: number;
}>;
};
export type {
DashboardAnalytics,
DashboardGranularity,
DashboardLineChart,
DashboardLineHref,
} from '@dukang/shared-types';
export type SystemVersion = {
id: string;
@@ -0,0 +1,94 @@
import type { ECharts } from 'echarts';
const TITLE_FONT = 'bold 32px "Microsoft YaHei","PingFang SC","Noto Sans SC",sans-serif';
const SUB_FONT = '22px "Microsoft YaHei","PingFang SC","Noto Sans SC",sans-serif';
type ChartShot = {
title: string;
instance: ECharts | undefined;
};
function loadImage(src: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => reject(new Error('图表截图失败'));
img.src = src;
});
}
async function composePage(params: {
chartPng: string;
title: string;
subtitle: string;
}): Promise<{ dataUrl: string; width: number; height: number }> {
const img = await loadImage(params.chartPng);
const header = 96;
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height + header;
const ctx = canvas.getContext('2d');
if (!ctx) throw new Error('无法生成 PDF 画布');
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.textBaseline = 'top';
ctx.fillStyle = '#1f1f1f';
ctx.font = TITLE_FONT;
ctx.fillText(params.title, 24, 16, canvas.width - 48);
ctx.fillStyle = '#8c8c8c';
ctx.font = SUB_FONT;
ctx.fillText(params.subtitle, 24, 56, canvas.width - 48);
ctx.drawImage(img, 0, header);
return {
dataUrl: canvas.toDataURL('image/jpeg', 0.92),
width: canvas.width,
height: canvas.height,
};
}
/** 把当前已渲染折线图打成横向 A4 PDF(不含 KPI / 待办)。 */
export async function downloadDashboardChartsPdf(params: {
filename: string;
subtitle: string;
charts: ChartShot[];
}): Promise<void> {
const ready = params.charts.filter((c): c is { title: string; instance: ECharts } => !!c.instance);
if (ready.length === 0) {
throw new Error('暂无可下载的图');
}
if (ready.length !== params.charts.length) {
throw new Error('图表尚未渲染完成,请稍后再试');
}
const { jsPDF } = await import('jspdf');
const pdf = new jsPDF({ orientation: 'landscape', unit: 'mm', format: 'a4' });
const pageW = pdf.internal.pageSize.getWidth();
const pageH = pdf.internal.pageSize.getHeight();
const margin = 10;
for (let i = 0; i < ready.length; i += 1) {
const chart = ready[i];
const png = chart.instance.getDataURL({
type: 'png',
pixelRatio: 2,
backgroundColor: '#ffffff',
});
const page = await composePage({
chartPng: png,
title: chart.title,
subtitle: params.subtitle,
});
const maxW = pageW - margin * 2;
const maxH = pageH - margin * 2;
const ratio = Math.min(maxW / page.width, maxH / page.height);
const w = page.width * ratio;
const h = page.height * ratio;
const x = (pageW - w) / 2;
const y = margin + (maxH - h) / 2;
if (i > 0) pdf.addPage();
pdf.addImage(page.dataUrl, 'JPEG', x, y, w, h);
}
const filename = params.filename.endsWith('.pdf') ? params.filename : `${params.filename}.pdf`;
pdf.save(filename);
}