0.0.0.4
This commit is contained in:
+341
@@ -0,0 +1,341 @@
|
||||
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]
|
||||
}
|
||||
Reference in New Issue
Block a user