import { jsPDF } from 'jspdf' import type { FamilyMember } from '@/types/family' // 中文字体需要特殊处理,这里使用基本的方式 // jsPDF 默认不支持中文,需要添加中文字体 interface MemberInfo { label: string value: string } // 格式化日期 function formatDate(dateStr?: string): string { if (!dateStr) return '' try { const date = new Date(dateStr) if (isNaN(date.getTime())) return dateStr return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日` } catch { return dateStr } } // 计算年龄 function calculateAge(birthDate?: string, deathDate?: string): string { if (!birthDate) return '' try { const birth = new Date(birthDate) const end = deathDate ? new Date(deathDate) : new Date() if (isNaN(birth.getTime())) return '' let age = end.getFullYear() - birth.getFullYear() const monthDiff = end.getMonth() - birth.getMonth() if (monthDiff < 0 || (monthDiff === 0 && end.getDate() < birth.getDate())) { age-- } if (deathDate) { return `享年${age}岁` } return `${age}岁` } catch { return '' } } // 获取成员主要信息 function getMemberInfo(member: FamilyMember, getMemberName: (id: string) => string): MemberInfo[] { const info: MemberInfo[] = [] // 基本信息 info.push({ label: '姓名', value: member.fullName || '' }) info.push({ label: '性别', value: member.gender === 'MALE' ? '男' : member.gender === 'FEMALE' ? '女' : '' }) info.push({ label: '世代', value: member.generation ? `第${member.generation}世` : '' }) info.push({ label: '排行', value: member.rank ? `第${member.rank}` : '' }) // 字辈名号 info.push({ label: '字辈', value: member.generationName || '' }) info.push({ label: '字', value: member.courtesyName || '' }) info.push({ label: '号', value: member.artName || '' }) // 日期 info.push({ label: '出生日期', value: formatDate(member.birthDate) }) info.push({ label: '逝世日期', value: formatDate(member.deathDate) }) info.push({ label: '年龄', value: calculateAge(member.birthDate, member.deathDate) }) // 地点 info.push({ label: '籍贯', value: member.ancestralHome || '' }) info.push({ label: '出生地', value: member.birthPlace || '' }) info.push({ label: '现居地', value: member.address || '' }) info.push({ label: '墓地', value: member.burialPlace || '' }) // 联系方式 info.push({ label: '手机', value: member.phone || '' }) info.push({ label: '电话', value: member.telephone || '' }) info.push({ label: '邮箱', value: member.email || '' }) // 职业 info.push({ label: '职业', value: member.occupation || '' }) // 家庭关系 const fatherName = member.fatherId ? getMemberName(member.fatherId) : '' const motherName = member.motherId ? getMemberName(member.motherId) : '' const spouseNames = member.spouseIds?.map(id => getMemberName(id)).filter(Boolean).join('、') || '' const childrenNames = member.childrenIds?.map(id => getMemberName(id)).filter(Boolean).join('、') || '' info.push({ label: '父亲', value: fatherName }) info.push({ label: '母亲', value: motherName }) info.push({ label: '配偶', value: spouseNames }) info.push({ label: '子女', value: childrenNames }) return info } // 按世代分组成员 function groupMembersByGeneration(members: FamilyMember[]): Map { const groups = new Map() members.forEach(member => { const gen = member.generation || 0 if (!groups.has(gen)) { groups.set(gen, []) } groups.get(gen)!.push(member) }) // 按世代排序 return new Map([...groups.entries()].sort((a, b) => a[0] - b[0])) } // 导出 PDF export async function exportFamilyPDF( members: FamilyMember[], getMemberName: (id: string) => string, treeName: string = '家族' ): Promise { // 创建 PDF,A4 纸张 const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' }) // 页面尺寸 const pageWidth = 210 const pageHeight = 297 const margin = 15 const contentWidth = pageWidth - margin * 2 const halfWidth = contentWidth / 2 - 5 // 按世代分组 const groupedMembers = groupMembersByGeneration(members) let isFirstPage = true // 遍历每个世代 for (const [generation, genMembers] of groupedMembers) { // 对同世代成员按排行排序 genMembers.sort((a, b) => (a.rank || 999) - (b.rank || 999)) // 每页两个成员 for (let i = 0; i < genMembers.length; i += 2) { if (!isFirstPage) { doc.addPage() } isFirstPage = false // 页眉 doc.setFontSize(12) doc.setTextColor(100) doc.text(`${treeName}族谱 - 第${generation}世`, pageWidth / 2, 10, { align: 'center' }) // 绘制分隔线 doc.setDrawColor(200) doc.line(margin, 15, pageWidth - margin, 15) // 左侧成员 const leftMember = genMembers[i] drawMemberCard(doc, leftMember, getMemberName, margin, 20, halfWidth) // 右侧成员(如果有) if (i + 1 < genMembers.length) { const rightMember = genMembers[i + 1] drawMemberCard(doc, rightMember, getMemberName, margin + halfWidth + 10, 20, halfWidth) } // 中间分隔线 doc.setDrawColor(220) doc.setLineDashPattern([2, 2], 0) doc.line(pageWidth / 2, 20, pageWidth / 2, pageHeight - 15) doc.setLineDashPattern([], 0) // 页脚 doc.setFontSize(10) doc.setTextColor(150) doc.text(`第 ${doc.getNumberOfPages()} 页`, pageWidth / 2, pageHeight - 8, { align: 'center' }) } } // 保存 PDF const fileName = `${treeName}族谱_${new Date().toISOString().split('T')[0]}.pdf` doc.save(fileName) } // 绘制成员卡片 function drawMemberCard( doc: jsPDF, member: FamilyMember, getMemberName: (id: string) => string, x: number, y: number, width: number ): void { const info = getMemberInfo(member, getMemberName) const lineHeight = 8 const labelWidth = 25 // 成员姓名标题 doc.setFontSize(14) doc.setTextColor(0) doc.setFont('helvetica', 'bold') doc.text(member.fullName || '未知', x + width / 2, y + 5, { align: 'center' }) // 状态标签 const status = member.deathDate ? '(已故)' : '(在世)' doc.setFontSize(10) doc.setTextColor(100) doc.text(status, x + width / 2, y + 12, { align: 'center' }) // 分隔线 doc.setDrawColor(200) doc.line(x, y + 16, x + width, y + 16) // 信息列表 doc.setFont('helvetica', 'normal') doc.setFontSize(9) let currentY = y + 24 info.forEach(item => { // 跳过姓名(已在标题显示) if (item.label === '姓名') return // 标签 doc.setTextColor(80) doc.text(`${item.label}:`, x + 2, currentY) // 值 doc.setTextColor(0) const value = item.value || '-' // 处理长文本换行 const maxValueWidth = width - labelWidth - 5 const lines = doc.splitTextToSize(value, maxValueWidth) doc.text(lines[0] || '-', x + labelWidth, currentY) // 如果有多行,继续绘制 if (lines.length > 1) { for (let i = 1; i < Math.min(lines.length, 2); i++) { currentY += lineHeight - 2 doc.text(lines[i], x + labelWidth, currentY) } } currentY += lineHeight }) } // 定义页面数据结构 interface PageData { leftMember: FamilyMember leftGeneration: number rightMembers: FamilyMember[] // 配偶列表(可能多个,上下排列) rightGeneration: number } // 获取完整的图片 URL function getFullImageUrl(url: string | undefined, baseUrl: string): string { if (!url) return '' // 如果已经是完整 URL,直接返回 if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('data:')) { return url } // 相对路径,拼接基础 URL return `${baseUrl}${url.startsWith('/') ? '' : '/'}${url}` } // 导出为 HTML 格式(支持中文,夫妻同页) export async function exportFamilyHTML( members: FamilyMember[], getMemberName: (id: string) => string, treeName: string = '家族', baseUrl: string = '' ): Promise { const groupedMembers = groupMembersByGeneration(members) const membersMap = new Map() members.forEach(m => membersMap.set(m.id, m)) // 已处理的成员ID(避免配偶重复出现) const processedIds = new Set() // 构建页面数据:每页一对夫妻 const pages: PageData[] = [] for (const [generation, genMembers] of groupedMembers) { // 按排行排序 genMembers.sort((a, b) => (a.rank || 999) - (b.rank || 999)) for (const member of genMembers) { // 跳过已处理的成员(作为配偶已出现) if (processedIds.has(member.id)) continue processedIds.add(member.id) // 获取配偶列表 const spouses: FamilyMember[] = [] if (member.spouseIds && member.spouseIds.length > 0) { for (const spouseId of member.spouseIds) { const spouse = membersMap.get(spouseId) if (spouse && !processedIds.has(spouseId)) { spouses.push(spouse) processedIds.add(spouseId) } } } // 获取配偶的世代(通常与主成员相同) const spouseGeneration = spouses.length > 0 ? (spouses[0].generation || generation) : generation pages.push({ leftMember: member, leftGeneration: generation, rightMembers: spouses, rightGeneration: spouseGeneration }) } } let html = ` ${treeName}族谱 ` // 遍历每一页(每页一对夫妻) const totalPages = pages.length for (let pageIndex = 0; pageIndex < totalPages; pageIndex++) { const pageData = pages[pageIndex] // 获取子女名单(夫妻共用) const childrenNames = pageData.leftMember.childrenIds?.map(id => getMemberName(id)).filter(Boolean).join('、') || '' html += `
第${pageData.leftGeneration}世
${renderPersonCard(pageData.leftMember, getMemberName, baseUrl, true)}
${pageData.rightMembers.length > 0 ? renderSpousesCards(pageData.rightMembers, getMemberName, baseUrl) : '
' }
${childrenNames ? `
子女:
${childrenNames}
` : ''}
` } html += ` ` // 创建 Blob 并下载 const blob = new Blob([html], { type: 'text/html;charset=utf-8' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = `${treeName}族谱_${new Date().toISOString().split('T')[0]}.html` document.body.appendChild(a) a.click() document.body.removeChild(a) URL.revokeObjectURL(url) } function renderMemberCardHTMLNew( member: FamilyMember, generation: number, getMemberName: (id: string) => string, baseUrl: string = '' ): string { const info = getMemberInfo(member, getMemberName) const isDead = !!member.deathDate const statusClass = isDead ? 'status-deceased' : 'status-living' const statusText = isDead ? '已故' : '在世' // 获取完整的头像 URL const avatarUrl = getFullImageUrl(member.avatarUrl, baseUrl) let html = `
${member.fullName || '未知'}
第${generation}世${member.rank ? ` · 排行第${member.rank}` : ''}
${avatarUrl ? `${member.fullName}` : `
${member.gender === 'MALE' ? '👤' : '👩'}
` }
${statusText}
基本信息
${renderInfoRow('性别', member.gender === 'MALE' ? '男' : member.gender === 'FEMALE' ? '女' : '-')} ${renderInfoRow('字辈', member.generationName)} ${renderInfoRow('字', member.courtesyName)} ${renderInfoRow('号', member.artName)}
生卒信息
${renderInfoRow('出生', formatDate(member.birthDate))} ${renderInfoRow('逝世', formatDate(member.deathDate))} ${renderInfoRow('年龄', calculateAge(member.birthDate, member.deathDate))}
地址信息
${renderInfoRow('籍贯', member.ancestralHome)} ${renderInfoRow('出生地', member.birthPlace)} ${renderInfoRow('现居地', member.address)} ${renderInfoRow('墓地', member.burialPlace)}
联系方式
${renderInfoRow('手机', member.phone)} ${renderInfoRow('电话', member.telephone)} ${renderInfoRow('邮箱', member.email)} ${renderInfoRow('职业', member.occupation)}
家庭关系
${renderInfoRow('父亲', member.fatherId ? getMemberName(member.fatherId) : '')} ${renderInfoRow('母亲', member.motherId ? getMemberName(member.motherId) : '')} ${renderInfoRow('配偶', member.spouseIds?.map(id => getMemberName(id)).filter(Boolean).join('、'))} ${renderInfoRow('子女', member.childrenIds?.map(id => getMemberName(id)).filter(Boolean).join('、'))}
` return html } function renderInfoRow(label: string, value?: string): string { const displayValue = value || '-' const emptyClass = !value ? 'empty' : '' return `
${label}: ${displayValue}
` } // 渲染配偶列(支持多配偶上下排列) function renderSpouseColumn( spouses: FamilyMember[], generation: number, getMemberName: (id: string) => string, baseUrl: string = '' ): string { if (spouses.length === 0) { return '
无配偶
' } if (spouses.length === 1) { // 单配偶,使用完整卡片 return renderSpouseCardHTML(spouses[0], generation, getMemberName, false, baseUrl) } // 多配偶,上下排列 let html = '
' spouses.forEach((spouse, index) => { html += `
配偶${index + 1}
` html += renderSpouseCardHTML(spouse, generation, getMemberName, true, baseUrl) }) html += '
' return html } // 渲染配偶卡片(简化版,用于多配偶时) function renderSpouseCardHTML( member: FamilyMember, generation: number, getMemberName: (id: string) => string, isCompact: boolean, baseUrl: string = '' ): string { const isDead = !!member.deathDate const statusClass = isDead ? 'status-deceased' : 'status-living' const statusText = isDead ? '已故' : '在世' const avatarUrl = getFullImageUrl(member.avatarUrl, baseUrl) if (isCompact) { // 紧凑版(多配偶时使用) return `
${member.fullName || '未知'}
第${generation}世
${avatarUrl ? `${member.fullName}` : `
${member.gender === 'MALE' ? '👤' : '👩'}
` }
${statusText}
${renderInfoRow('性别', member.gender === 'MALE' ? '男' : member.gender === 'FEMALE' ? '女' : '-')} ${renderInfoRow('出生', formatDate(member.birthDate))} ${renderInfoRow('逝世', formatDate(member.deathDate))} ${renderInfoRow('籍贯', member.ancestralHome)} ${renderInfoRow('职业', member.occupation)}
` } // 完整版(单配偶时使用) return `
${member.fullName || '未知'}
第${generation}世${member.rank ? ` · 排行第${member.rank}` : ''}
${avatarUrl ? `${member.fullName}` : `
${member.gender === 'MALE' ? '👤' : '👩'}
` }
${statusText}
基本信息
${renderInfoRow('性别', member.gender === 'MALE' ? '男' : member.gender === 'FEMALE' ? '女' : '-')} ${renderInfoRow('字辈', member.generationName)} ${renderInfoRow('字', member.courtesyName)} ${renderInfoRow('号', member.artName)}
生卒信息
${renderInfoRow('出生', formatDate(member.birthDate))} ${renderInfoRow('逝世', formatDate(member.deathDate))} ${renderInfoRow('年龄', calculateAge(member.birthDate, member.deathDate))}
地址信息
${renderInfoRow('籍贯', member.ancestralHome)} ${renderInfoRow('出生地', member.birthPlace)} ${renderInfoRow('现居地', member.address)} ${renderInfoRow('墓地', member.burialPlace)}
联系方式
${renderInfoRow('手机', member.phone)} ${renderInfoRow('电话', member.telephone)} ${renderInfoRow('邮箱', member.email)} ${renderInfoRow('职业', member.occupation)}
家庭关系
${renderInfoRow('父亲', member.fatherId ? getMemberName(member.fatherId) : '')} ${renderInfoRow('母亲', member.motherId ? getMemberName(member.motherId) : '')} ${renderInfoRow('子女', member.childrenIds?.map(id => getMemberName(id)).filter(Boolean).join('、'))}
` } // 渲染单个人员卡片(用于夫妻共用外框布局) function renderPersonCard( member: FamilyMember, getMemberName: (id: string) => string, baseUrl: string, isMain: boolean ): string { const isDead = !!member.deathDate const statusText = isDead ? '已故' : '在世' const statusClass = isDead ? 'status-deceased' : 'status-living' const avatarUrl = getFullImageUrl(member.avatarUrl, baseUrl) const genderText = member.gender === 'MALE' ? '男' : member.gender === 'FEMALE' ? '女' : '' const rankText = member.rank ? `排行第${member.rank}` : '' const metaItems = [genderText, rankText].filter(Boolean).join(' · ') return `
${avatarUrl ? `${member.fullName}` : `
${member.gender === 'MALE' ? '👤' : '👩'}
` }
${member.fullName || '未知'}
${metaItems}
${statusText}
${member.generationName ? renderInfoRow('字辈', member.generationName) : ''} ${member.courtesyName ? renderInfoRow('字', member.courtesyName) : ''} ${member.artName ? renderInfoRow('号', member.artName) : ''} ${renderInfoRow('出生', formatDate(member.birthDate))} ${renderInfoRow('逝世', formatDate(member.deathDate))} ${renderInfoRow('年龄', calculateAge(member.birthDate, member.deathDate))}
${renderInfoRow('籍贯', member.ancestralHome)} ${member.birthPlace ? renderInfoRow('出生地', member.birthPlace) : ''} ${member.address ? renderInfoRow('现居地', member.address) : ''} ${member.burialPlace ? renderInfoRow('墓地', member.burialPlace) : ''}
${member.phone ? renderInfoRow('手机', member.phone) : ''} ${member.email ? renderInfoRow('邮箱', member.email) : ''} ${member.occupation ? renderInfoRow('职业', member.occupation) : ''}
${isMain ? `
${renderInfoRow('父亲', member.fatherId ? getMemberName(member.fatherId) : '')} ${renderInfoRow('母亲', member.motherId ? getMemberName(member.motherId) : '')}
` : ''}
` } // 渲染配偶卡片列表 function renderSpousesCards( spouses: FamilyMember[], getMemberName: (id: string) => string, baseUrl: string ): string { if (spouses.length === 1) { return renderPersonCard(spouses[0], getMemberName, baseUrl, false) } // 多配偶,上下排列 let html = '
' spouses.forEach((spouse, index) => { html += `
配偶${index + 1}
` html += renderPersonCard(spouse, getMemberName, baseUrl, false) }) html += '
' return html }