Files
prop-ai-hr/mobile-uni/src/services/summary-card-image.ts
T

129 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
};