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((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); };