342 lines
10 KiB
TypeScript
342 lines
10 KiB
TypeScript
import type { FamilyMember, FamilyTreeData } from "@/types/family"
|
|
import { parseGedcom } from "read-gedcom"
|
|
|
|
/**
|
|
* 将 FamilyTreeData 导出为 GEDCOM 格式
|
|
*/
|
|
export function exportToGedcom(treeData: FamilyTreeData): string {
|
|
const lines: string[] = []
|
|
|
|
// GEDCOM Header
|
|
lines.push("0 HEAD")
|
|
lines.push("1 SOUR Chinese Family Tree")
|
|
lines.push("2 VERS 1.0")
|
|
lines.push("2 NAME 华夏谱")
|
|
lines.push("1 DEST ANY")
|
|
lines.push("1 DATE " + new Date().toISOString().split('T')[0].replace(/-/g, ' '))
|
|
lines.push("1 CHAR UTF-8")
|
|
lines.push("1 GEDC")
|
|
lines.push("2 VERS 5.5.1")
|
|
lines.push("2 FORM LINEAGE-LINKED")
|
|
|
|
const members = Object.values(treeData.members)
|
|
|
|
// 个人记录 (INDI)
|
|
members.forEach(member => {
|
|
lines.push(`0 @I${member.id}@ INDI`)
|
|
lines.push(`1 NAME ${member.fullName}`)
|
|
lines.push(`2 GIVN ${member.givenName}`)
|
|
lines.push(`2 SURN ${member.surname}`)
|
|
|
|
if (member.courtesyName) {
|
|
lines.push(`2 _AKA ${member.courtesyName}`)
|
|
}
|
|
|
|
if (member.gender === 'MALE') {
|
|
lines.push("1 SEX M")
|
|
} else if (member.gender === 'FEMALE') {
|
|
lines.push("1 SEX F")
|
|
}
|
|
|
|
// 出生信息
|
|
if (member.birthDate) {
|
|
lines.push("1 BIRT")
|
|
lines.push(`2 DATE ${formatGedcomDate(member.birthDate)}`)
|
|
if (member.birthPlace) {
|
|
lines.push(`2 PLAC ${member.birthPlace}`)
|
|
}
|
|
}
|
|
|
|
// 逝世信息
|
|
if (member.deathDate) {
|
|
lines.push("1 DEAT")
|
|
lines.push(`2 DATE ${formatGedcomDate(member.deathDate)}`)
|
|
if (member.burialPlace) {
|
|
lines.push(`2 PLAC ${member.burialPlace}`)
|
|
}
|
|
}
|
|
|
|
// 籍贯
|
|
if (member.ancestralHome) {
|
|
lines.push("1 RESI")
|
|
lines.push(`2 PLAC ${member.ancestralHome}`)
|
|
}
|
|
|
|
// 备注
|
|
if (member.bio) {
|
|
lines.push("1 NOTE " + member.bio.replace(/\n/g, "\n1 CONT "))
|
|
}
|
|
|
|
// 家庭关系 - 作为子女
|
|
if (member.fatherId || member.motherId) {
|
|
const famId = `F${member.fatherId || member.motherId}_${member.id}`
|
|
lines.push(`1 FAMC @${famId}@`)
|
|
}
|
|
|
|
// 家庭关系 - 作为配偶
|
|
if (member.spouseIds.length > 0) {
|
|
member.spouseIds.forEach(spouseId => {
|
|
const famId = `F${member.id}_${spouseId}`
|
|
lines.push(`1 FAMS @${famId}@`)
|
|
})
|
|
}
|
|
})
|
|
|
|
// 家庭记录 (FAM)
|
|
const processedFamilies = new Set<string>()
|
|
|
|
members.forEach(member => {
|
|
// 创建父母家庭
|
|
if (member.fatherId || member.motherId) {
|
|
const famId = `F${member.fatherId || member.motherId}_${member.id}`
|
|
if (!processedFamilies.has(famId)) {
|
|
lines.push(`0 @${famId}@ FAM`)
|
|
if (member.fatherId) {
|
|
lines.push(`1 HUSB @I${member.fatherId}@`)
|
|
}
|
|
if (member.motherId) {
|
|
lines.push(`1 WIFE @I${member.motherId}@`)
|
|
}
|
|
|
|
// 查找所有同父母的子女
|
|
const siblings = members.filter(m =>
|
|
m.fatherId === member.fatherId && m.motherId === member.motherId
|
|
)
|
|
siblings.forEach(child => {
|
|
lines.push(`1 CHIL @I${child.id}@`)
|
|
})
|
|
|
|
processedFamilies.add(famId)
|
|
}
|
|
}
|
|
|
|
// 创建配偶家庭
|
|
if (member.spouseIds.length > 0) {
|
|
member.spouseIds.forEach(spouseId => {
|
|
const famId = member.gender === 'MALE'
|
|
? `F${member.id}_${spouseId}`
|
|
: `F${spouseId}_${member.id}`
|
|
|
|
if (!processedFamilies.has(famId)) {
|
|
lines.push(`0 @${famId}@ FAM`)
|
|
|
|
if (member.gender === 'MALE') {
|
|
lines.push(`1 HUSB @I${member.id}@`)
|
|
lines.push(`1 WIFE @I${spouseId}@`)
|
|
} else {
|
|
lines.push(`1 HUSB @I${spouseId}@`)
|
|
lines.push(`1 WIFE @I${member.id}@`)
|
|
}
|
|
|
|
// 添加子女
|
|
member.childrenIds.forEach(childId => {
|
|
lines.push(`1 CHIL @I${childId}@`)
|
|
})
|
|
|
|
processedFamilies.add(famId)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
// GEDCOM Trailer
|
|
lines.push("0 TRLR")
|
|
|
|
return lines.join("\n")
|
|
}
|
|
|
|
/**
|
|
* 从 GEDCOM 格式导入数据
|
|
*/
|
|
export function importFromGedcom(gedcomText: string): FamilyTreeData {
|
|
const parsed = parseGedcom(gedcomText as any)
|
|
const members: Record<string, FamilyMember> = {}
|
|
let rootId: string | undefined
|
|
|
|
// 第一遍:创建所有个人
|
|
const records = Array.isArray(parsed) ? parsed : (parsed as any).tree || []
|
|
records.forEach((record: any) => {
|
|
if (record.tag === 'INDI') {
|
|
const id = record.pointer.replace('@I', '').replace('@', '')
|
|
|
|
let fullName = ''
|
|
let givenName = ''
|
|
let surname = ''
|
|
let courtesyName: string | undefined
|
|
let gender: 'MALE' | 'FEMALE' | 'OTHER' = 'OTHER'
|
|
let birthDate: string | undefined
|
|
let deathDate: string | undefined
|
|
let birthPlace: string | undefined
|
|
let burialPlace: string | undefined
|
|
let ancestralHome: string | undefined
|
|
let bio: string | undefined
|
|
|
|
record.tree.forEach((field: any) => {
|
|
if (field.tag === 'NAME') {
|
|
fullName = field.data.replace(/\//g, '').trim()
|
|
field.tree?.forEach((subField: any) => {
|
|
if (subField.tag === 'GIVN') givenName = subField.data
|
|
if (subField.tag === 'SURN') surname = subField.data
|
|
if (subField.tag === '_AKA') courtesyName = subField.data
|
|
})
|
|
}
|
|
if (field.tag === 'SEX') {
|
|
gender = field.data === 'M' ? 'MALE' : field.data === 'F' ? 'FEMALE' : 'OTHER'
|
|
}
|
|
if (field.tag === 'BIRT') {
|
|
field.tree?.forEach((subField: any) => {
|
|
if (subField.tag === 'DATE') birthDate = parseGedcomDate(subField.data)
|
|
if (subField.tag === 'PLAC') birthPlace = subField.data
|
|
})
|
|
}
|
|
if (field.tag === 'DEAT') {
|
|
field.tree?.forEach((subField: any) => {
|
|
if (subField.tag === 'DATE') deathDate = parseGedcomDate(subField.data)
|
|
if (subField.tag === 'PLAC') burialPlace = subField.data
|
|
})
|
|
}
|
|
if (field.tag === 'RESI') {
|
|
field.tree?.forEach((subField: any) => {
|
|
if (subField.tag === 'PLAC') ancestralHome = subField.data
|
|
})
|
|
}
|
|
if (field.tag === 'NOTE') {
|
|
bio = field.data
|
|
}
|
|
})
|
|
|
|
members[id] = {
|
|
id,
|
|
surname: surname || '未知',
|
|
givenName: givenName || fullName,
|
|
fullName: fullName || `${surname}${givenName}`,
|
|
gender,
|
|
generation: 1, // 将在第二遍中计算
|
|
courtesyName,
|
|
birthDate,
|
|
deathDate,
|
|
birthPlace,
|
|
burialPlace,
|
|
ancestralHome,
|
|
bio,
|
|
spouseIds: [],
|
|
childrenIds: [],
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
}
|
|
}
|
|
})
|
|
|
|
// 第二遍:建立关系
|
|
records.forEach((record: any) => {
|
|
if (record.tag === 'FAM') {
|
|
let husbId: string | undefined
|
|
let wifeId: string | undefined
|
|
const childIds: string[] = []
|
|
|
|
record.tree.forEach((field: any) => {
|
|
if (field.tag === 'HUSB') {
|
|
husbId = field.data.replace('@I', '').replace('@', '')
|
|
}
|
|
if (field.tag === 'WIFE') {
|
|
wifeId = field.data.replace('@I', '').replace('@', '')
|
|
}
|
|
if (field.tag === 'CHIL') {
|
|
childIds.push(field.data.replace('@I', '').replace('@', ''))
|
|
}
|
|
})
|
|
|
|
// 设置配偶关系
|
|
if (husbId && wifeId) {
|
|
if (members[husbId] && !members[husbId].spouseIds.includes(wifeId)) {
|
|
members[husbId].spouseIds.push(wifeId)
|
|
}
|
|
if (members[wifeId] && !members[wifeId].spouseIds.includes(husbId)) {
|
|
members[wifeId].spouseIds.push(husbId)
|
|
}
|
|
}
|
|
|
|
// 设置父母-子女关系
|
|
childIds.forEach(childId => {
|
|
if (members[childId]) {
|
|
if (husbId) {
|
|
members[childId].fatherId = husbId
|
|
if (members[husbId] && !members[husbId].childrenIds.includes(childId)) {
|
|
members[husbId].childrenIds.push(childId)
|
|
}
|
|
}
|
|
if (wifeId) {
|
|
members[childId].motherId = wifeId
|
|
if (members[wifeId] && !members[wifeId].childrenIds.includes(childId)) {
|
|
members[wifeId].childrenIds.push(childId)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
// 计算世代并找到根节点
|
|
const calculateGeneration = (id: string, visited = new Set<string>()): number => {
|
|
if (visited.has(id)) return 1 // 防止循环引用
|
|
visited.add(id)
|
|
|
|
const member = members[id]
|
|
if (!member) return 1
|
|
|
|
if (!member.fatherId && !member.motherId) {
|
|
return 1 // 根节点
|
|
}
|
|
|
|
const fatherGen = member.fatherId ? calculateGeneration(member.fatherId, visited) : 0
|
|
const motherGen = member.motherId ? calculateGeneration(member.motherId, visited) : 0
|
|
|
|
return Math.max(fatherGen, motherGen) + 1
|
|
}
|
|
|
|
Object.keys(members).forEach(id => {
|
|
members[id].generation = calculateGeneration(id)
|
|
})
|
|
|
|
// 找到第一代作为根节点
|
|
rootId = Object.keys(members).find(id => members[id].generation === 1)
|
|
|
|
return {
|
|
members,
|
|
rootId,
|
|
}
|
|
}
|
|
|
|
// 辅助函数:格式化日期为 GEDCOM 格式
|
|
function formatGedcomDate(isoDate: string): string {
|
|
const date = new Date(isoDate)
|
|
const months = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC']
|
|
return `${date.getDate()} ${months[date.getMonth()]} ${date.getFullYear()}`
|
|
}
|
|
|
|
// 辅助函数:解析 GEDCOM 日期为 ISO 格式
|
|
function parseGedcomDate(gedcomDate: string): string {
|
|
const months: Record<string, number> = {
|
|
JAN: 0, FEB: 1, MAR: 2, APR: 3, MAY: 4, JUN: 5,
|
|
JUL: 6, AUG: 7, SEP: 8, OCT: 9, NOV: 10, DEC: 11
|
|
}
|
|
|
|
const parts = gedcomDate.trim().split(' ')
|
|
if (parts.length === 3) {
|
|
const day = parseInt(parts[0])
|
|
const month = months[parts[1].toUpperCase()]
|
|
const year = parseInt(parts[2])
|
|
|
|
if (!isNaN(day) && month !== undefined && !isNaN(year)) {
|
|
return new Date(year, month, day).toISOString().split('T')[0]
|
|
}
|
|
}
|
|
|
|
// 如果只有年份
|
|
if (parts.length === 1 && !isNaN(parseInt(parts[0]))) {
|
|
return `${parts[0]}-01-01`
|
|
}
|
|
|
|
return new Date().toISOString().split('T')[0]
|
|
}
|