129 lines
4.5 KiB
TypeScript
129 lines
4.5 KiB
TypeScript
import type { SummaryCardResponse } from '@/types/api';
|
||
|
||
const WIDTH = 1080;
|
||
const PADDING = 84;
|
||
const CONTENT_WIDTH = WIDTH - PADDING * 2;
|
||
|
||
const safeFileName = (value: string) =>
|
||
(value || 'SOP总结卡').replace(/[\\/:*?"<>|\r\n]+/g, '-').slice(0, 48);
|
||
|
||
const wrapText = (context: CanvasRenderingContext2D, text: string, maxWidth: number) => {
|
||
const lines: string[] = [];
|
||
let current = '';
|
||
for (const char of String(text || '').trim()) {
|
||
const next = current + char;
|
||
if (current && context.measureText(next).width > maxWidth) {
|
||
lines.push(current);
|
||
current = char;
|
||
} else {
|
||
current = next;
|
||
}
|
||
}
|
||
if (current) lines.push(current);
|
||
return lines.length ? lines : [''];
|
||
};
|
||
|
||
export const downloadSummaryCardImage = async (card: SummaryCardResponse) => {
|
||
if (typeof document === 'undefined' || typeof URL === 'undefined') {
|
||
throw new Error('当前终端暂不支持下载图片');
|
||
}
|
||
if (document.fonts?.ready) await document.fonts.ready;
|
||
|
||
const canvas = document.createElement('canvas');
|
||
canvas.width = WIDTH;
|
||
canvas.height = 1;
|
||
const measureContext = canvas.getContext('2d');
|
||
if (!measureContext) throw new Error('图片生成失败');
|
||
|
||
measureContext.font = '800 56px system-ui, sans-serif';
|
||
const titleLines = wrapText(measureContext, card.title || '工作总结卡', CONTENT_WIDTH);
|
||
const sectionRows = [
|
||
['核心步骤', (card.steps || []).map((item) => `${item.label}:${item.desc}`)],
|
||
['推荐话术', card.scripts || []],
|
||
['常见异议与回应', (card.objections || []).map((item) => `${item.concern};回应:${item.response}`)],
|
||
['师傅提醒', card.reminders || []]
|
||
] as const;
|
||
measureContext.font = '400 28px system-ui, sans-serif';
|
||
const sections = sectionRows
|
||
.filter(([, rows]) => rows.length)
|
||
.map(([title, rows]) => ({
|
||
title,
|
||
rows: rows.map((row) => wrapText(measureContext, row, CONTENT_WIDTH - 64))
|
||
}));
|
||
|
||
let measuredY = 118 + 80 + titleLines.length * 76 + 14 + 70;
|
||
sections.forEach((section) => {
|
||
measuredY += 48;
|
||
section.rows.forEach((lines) => {
|
||
measuredY += Math.max(54, lines.length * 44 + 18);
|
||
});
|
||
measuredY += 36;
|
||
});
|
||
const height = Math.max(720, measuredY + 100);
|
||
canvas.height = height;
|
||
const context = canvas.getContext('2d');
|
||
if (!context) throw new Error('图片生成失败');
|
||
|
||
context.fillStyle = '#f7f9fc';
|
||
context.fillRect(0, 0, WIDTH, height);
|
||
context.fillStyle = '#ffffff';
|
||
context.fillRect(36, 36, WIDTH - 72, height - 72);
|
||
|
||
let y = 118;
|
||
context.fillStyle = '#e60012';
|
||
context.font = '700 28px system-ui, sans-serif';
|
||
context.fillText('银城数字师傅 · SOP 总结卡', PADDING, y);
|
||
y += 80;
|
||
context.fillStyle = '#172033';
|
||
context.font = '800 56px system-ui, sans-serif';
|
||
titleLines.forEach((line) => {
|
||
context.fillText(line, PADDING, y);
|
||
y += 76;
|
||
});
|
||
y += 14;
|
||
context.fillStyle = '#e60012';
|
||
context.fillRect(PADDING, y, 88, 8);
|
||
y += 70;
|
||
|
||
sections.forEach((section) => {
|
||
context.fillStyle = '#172033';
|
||
context.font = '800 34px system-ui, sans-serif';
|
||
context.fillText(section.title, PADDING, y);
|
||
y += 48;
|
||
section.rows.forEach((lines, index) => {
|
||
context.fillStyle = '#fff0f1';
|
||
context.beginPath();
|
||
context.arc(PADDING + 18, y + 19, 18, 0, Math.PI * 2);
|
||
context.fill();
|
||
context.fillStyle = '#e60012';
|
||
context.font = '800 20px system-ui, sans-serif';
|
||
context.textAlign = 'center';
|
||
context.fillText(String(index + 1), PADDING + 18, y + 27);
|
||
context.textAlign = 'left';
|
||
context.fillStyle = '#3f4a5c';
|
||
context.font = '400 28px system-ui, sans-serif';
|
||
lines.forEach((line, lineIndex) => context.fillText(line, PADDING + 56, y + 28 + lineIndex * 44));
|
||
y += Math.max(54, lines.length * 44 + 18);
|
||
});
|
||
y += 36;
|
||
});
|
||
|
||
context.fillStyle = '#8a94a5';
|
||
context.font = '400 22px system-ui, sans-serif';
|
||
context.fillText('内容由数字师傅基于已授权 SOP 生成,请结合现场情况执行。', PADDING, y + 10);
|
||
|
||
const blob = await new Promise<Blob>((resolve, reject) => {
|
||
canvas.toBlob((value) => value ? resolve(value) : reject(new Error('图片生成失败')), 'image/png');
|
||
});
|
||
canvas.width = 1;
|
||
canvas.height = 1;
|
||
const url = URL.createObjectURL(blob);
|
||
const link = document.createElement('a');
|
||
link.href = url;
|
||
link.download = `${safeFileName(card.title)}.png`;
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
link.remove();
|
||
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||
};
|