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]
|
||||
}
|
||||
+274
-40
@@ -25,61 +25,295 @@ const createMember = (
|
||||
}
|
||||
|
||||
const members: Record<string, FamilyMember> = {
|
||||
"1": createMember("1", "文德", "male", 1, {
|
||||
courtesyName: "博远",
|
||||
ancestralHome: "陇西",
|
||||
bio: "李氏入闽始祖,洪武年间迁居福建。",
|
||||
tags: ["始祖", "进士"],
|
||||
birthDate: "1350-01-01",
|
||||
deathDate: "1410-05-20",
|
||||
// ========== 第一代 (曾祖父母辈,1920年代出生) ==========
|
||||
"1": createMember("1", "国强", "male", 1, {
|
||||
ancestralHome: "广东省广州市",
|
||||
bio: "李氏家族第一代,抗战时期参加革命工作,新中国成立后在政府部门工作,为人正直,深受敬重。",
|
||||
tags: ["革命干部", "始祖"],
|
||||
birthDate: "1925-03-15",
|
||||
deathDate: "2008-08-20",
|
||||
spouseIds: ["2"],
|
||||
childrenIds: ["3", "4", "5"],
|
||||
}),
|
||||
"2": createMember("2", "氏(王)", "female", 1, {
|
||||
fullName: "王氏",
|
||||
"2": createMember("2", "秀芳", "female", 1, {
|
||||
fullName: "王秀芳",
|
||||
surname: "王",
|
||||
givenName: "氏",
|
||||
givenName: "秀芳",
|
||||
ancestralHome: "广东省广州市",
|
||||
bio: "知识分子家庭出身,建国后在学校任教,相夫教子,育有三子。",
|
||||
birthDate: "1928-07-08",
|
||||
deathDate: "2015-12-10",
|
||||
spouseIds: ["1"],
|
||||
childrenIds: ["3", "4", "5"],
|
||||
}),
|
||||
// Generation 2
|
||||
"3": createMember("3", "世昌", "male", 2, {
|
||||
|
||||
// ========== 第二代 (祖父母辈,1950年代出生) ==========
|
||||
"3": createMember("3", "建华", "male", 2, {
|
||||
fatherId: "1",
|
||||
motherId: "2",
|
||||
courtesyName: "继业",
|
||||
ancestralHome: "广东省深圳市",
|
||||
birthPlace: "广东省深圳市",
|
||||
bio: "国强长子,改革开放后下海经商,迁往深圳创办家族企业,事业有成。",
|
||||
tags: ["企业家"],
|
||||
birthDate: "1952-05-20",
|
||||
deathDate: "2020-03-15",
|
||||
spouseIds: ["6"],
|
||||
childrenIds: ["9", "10"],
|
||||
}),
|
||||
"4": createMember("4", "世盛", "male", 2, {
|
||||
"4": createMember("4", "建国", "male", 2, {
|
||||
fatherId: "1",
|
||||
motherId: "2",
|
||||
courtesyName: "继荣",
|
||||
}),
|
||||
// Generation 3
|
||||
"5": createMember("5", "宗仁", "male", 3, {
|
||||
fatherId: "3",
|
||||
generationName: "宗",
|
||||
}),
|
||||
"6": createMember("6", "宗义", "male", 3, {
|
||||
fatherId: "3",
|
||||
generationName: "宗",
|
||||
}),
|
||||
// Generation 24 (Modern)
|
||||
"100": createMember("100", "建国", "male", 23, {
|
||||
generationName: "建",
|
||||
rank: 2,
|
||||
bio: "次子,大学教授,追随教育事业迁往北京,从事教育工作四十余年,桃李满天下。",
|
||||
tags: ["教授", "次子"],
|
||||
birthDate: "1955-11-03",
|
||||
ancestralHome: "北京市",
|
||||
birthPlace: "北京市",
|
||||
spouseIds: ["7"],
|
||||
childrenIds: ["11", "12", "13"],
|
||||
}),
|
||||
"101": createMember("101", "明远", "male", 24, {
|
||||
fatherId: "100",
|
||||
generationName: "明",
|
||||
birthDate: "1985-06-15",
|
||||
tags: ["户主"],
|
||||
"5": createMember("5", "建军", "male", 2, {
|
||||
fatherId: "1",
|
||||
motherId: "2",
|
||||
generationName: "建",
|
||||
rank: 3,
|
||||
bio: "三子,医生,支援西部建设迁往上海,从医三十余年,救死扶伤。",
|
||||
tags: ["医生", "三子"],
|
||||
birthDate: "1958-11-03",
|
||||
ancestralHome: "上海市",
|
||||
birthPlace: "上海市",
|
||||
spouseIds: ["8"],
|
||||
childrenIds: ["14"],
|
||||
}),
|
||||
"102": createMember("102", "子豪", "male", 25, {
|
||||
fatherId: "101",
|
||||
birthDate: "2015-09-01",
|
||||
|
||||
// 第二代配偶
|
||||
"6": createMember("6", "丽华", "female", 2, {
|
||||
fullName: "陈丽华",
|
||||
surname: "陈",
|
||||
givenName: "丽华",
|
||||
ancestralHome: "广东省深圳市",
|
||||
bio: "建华之妻,企业管理者,协助丈夫打理家族企业。",
|
||||
birthDate: "1955-05-20",
|
||||
deathDate: "2021-11-05",
|
||||
spouseIds: ["3"],
|
||||
childrenIds: ["9", "10"],
|
||||
}),
|
||||
"7": createMember("7", "美玲", "female", 2, {
|
||||
fullName: "张美玲",
|
||||
surname: "张",
|
||||
givenName: "美玲",
|
||||
ancestralHome: "广东省广州市",
|
||||
bio: "建国之妻,中学教师,与丈夫志同道合,共同从事教育事业。",
|
||||
birthDate: "1957-01-15",
|
||||
spouseIds: ["4"],
|
||||
childrenIds: ["11", "12", "13"],
|
||||
}),
|
||||
"8": createMember("8", "雅琴", "female", 2, {
|
||||
fullName: "林雅琴",
|
||||
surname: "林",
|
||||
givenName: "雅琴",
|
||||
ancestralHome: "福建省厦门市",
|
||||
bio: "建军之妻,护士长,温柔贤惠,善于持家。",
|
||||
birthDate: "1960-08-10",
|
||||
spouseIds: ["5"],
|
||||
childrenIds: ["14"],
|
||||
}),
|
||||
|
||||
// ========== 第三代 (父母辈,1980年代出生) ==========
|
||||
"9": createMember("9", "志远", "male", 3, {
|
||||
fatherId: "3",
|
||||
motherId: "6",
|
||||
generationName: "志",
|
||||
rank: 1,
|
||||
ancestralHome: "广东省深圳市",
|
||||
birthPlace: "广东省深圳市",
|
||||
bio: "建华长子,留学归国,接管家族企业,推动企业转型升级。",
|
||||
tags: ["企业家", "海归"],
|
||||
birthDate: "1980-03-08",
|
||||
phone: "13800138001",
|
||||
email: "zhiyuan.li@company.com",
|
||||
address: "广东省广州市天河区珠江新城XX路XX号",
|
||||
spouseIds: ["15"],
|
||||
childrenIds: ["17", "18"],
|
||||
}),
|
||||
"10": createMember("10", "志强", "male", 3, {
|
||||
fatherId: "3",
|
||||
motherId: "6",
|
||||
generationName: "志",
|
||||
rank: 2,
|
||||
ancestralHome: "北京市",
|
||||
birthPlace: "北京市",
|
||||
bio: "建华次子,互联网创业者,北上创业,创办科技公司。",
|
||||
tags: ["创业者", "科技"],
|
||||
birthDate: "1983-07-22",
|
||||
phone: "13900139002",
|
||||
email: "zhiqiang.li@techco.com",
|
||||
address: "北京市朝阳区望京SOHO",
|
||||
spouseIds: ["16"],
|
||||
childrenIds: ["19"],
|
||||
}),
|
||||
"11": createMember("11", "志明", "male", 3, {
|
||||
fatherId: "4",
|
||||
motherId: "7",
|
||||
generationName: "志",
|
||||
rank: 1,
|
||||
ancestralHome: "广东省广州市",
|
||||
birthPlace: "广东省广州市",
|
||||
bio: "建国长子,大学副教授,从北京返回广州从事教育研究工作。",
|
||||
tags: ["副教授", "学者"],
|
||||
birthDate: "1982-01-18",
|
||||
phone: "13700137003",
|
||||
telephone: "020-8888-8888",
|
||||
email: "zhiming.li@university.edu.cn",
|
||||
address: "广东省广州市海珠区大学城",
|
||||
spouseIds: ["20"],
|
||||
childrenIds: ["21", "22"],
|
||||
}),
|
||||
"12": createMember("12", "志文", "male", 3, {
|
||||
fatherId: "4",
|
||||
motherId: "7",
|
||||
generationName: "志",
|
||||
rank: 2,
|
||||
bio: "建国次子,作家,追求文学梦想迁往成都,出版多部文学作品。",
|
||||
tags: ["作家", "文学"],
|
||||
birthDate: "1985-09-12",
|
||||
ancestralHome: "四川省成都市",
|
||||
birthPlace: "四川省成都市",
|
||||
phone: "13600136004",
|
||||
email: "zhiwen.li@writer.com",
|
||||
}),
|
||||
"13": createMember("13", "志勇", "male", 3, {
|
||||
fatherId: "4",
|
||||
motherId: "7",
|
||||
generationName: "志",
|
||||
rank: 3,
|
||||
ancestralHome: "上海市",
|
||||
birthPlace: "上海市",
|
||||
bio: "建国三子,律师,从北京迁往上海,执业于知名律师事务所。",
|
||||
tags: ["律师"],
|
||||
birthDate: "1988-11-05",
|
||||
phone: "13500135005",
|
||||
email: "zhiyong.li@lawfirm.com",
|
||||
address: "上海市浦东新区陆家嘴金融中心",
|
||||
}),
|
||||
"14": createMember("14", "志诚", "male", 3, {
|
||||
fatherId: "5",
|
||||
motherId: "8",
|
||||
generationName: "志",
|
||||
rank: 1,
|
||||
ancestralHome: "广东省深圳市",
|
||||
birthPlace: "广东省深圳市",
|
||||
bio: "建军独子,医生,从上海迁往深圳,继承父业,在医院工作。",
|
||||
tags: ["医生"],
|
||||
birthDate: "1986-04-28",
|
||||
phone: "13400134006",
|
||||
telephone: "0755-6666-6666",
|
||||
email: "zhicheng.li@hospital.com",
|
||||
address: "广东省深圳市福田区人民医院",
|
||||
spouseIds: ["23"],
|
||||
childrenIds: ["24"],
|
||||
}),
|
||||
|
||||
// 第三代配偶
|
||||
"15": createMember("15", "晓雯", "female", 3, {
|
||||
fullName: "赵晓雯",
|
||||
surname: "赵",
|
||||
givenName: "晓雯",
|
||||
ancestralHome: "上海市",
|
||||
bio: "志远之妻,金融行业从业者,MBA学位。",
|
||||
birthDate: "1982-06-15",
|
||||
spouseIds: ["9"],
|
||||
childrenIds: ["17", "18"],
|
||||
}),
|
||||
"16": createMember("16", "诗涵", "female", 3, {
|
||||
fullName: "刘诗涵",
|
||||
surname: "刘",
|
||||
givenName: "诗涵",
|
||||
ancestralHome: "北京市",
|
||||
bio: "志强之妻,设计师,从事品牌设计工作。",
|
||||
birthDate: "1985-10-08",
|
||||
spouseIds: ["10"],
|
||||
childrenIds: ["19"],
|
||||
}),
|
||||
"20": createMember("20", "雨欣", "female", 3, {
|
||||
fullName: "周雨欣",
|
||||
surname: "周",
|
||||
givenName: "雨欣",
|
||||
ancestralHome: "浙江省杭州市",
|
||||
bio: "志明之妻,中学教师,温柔贤淑。",
|
||||
birthDate: "1984-02-25",
|
||||
spouseIds: ["11"],
|
||||
childrenIds: ["21", "22"],
|
||||
}),
|
||||
"23": createMember("23", "婉婷", "female", 3, {
|
||||
fullName: "郑婉婷",
|
||||
surname: "郑",
|
||||
givenName: "婉婷",
|
||||
ancestralHome: "广东省广州市",
|
||||
bio: "志诚之妻,护士,在医院工作。",
|
||||
birthDate: "1988-12-03",
|
||||
spouseIds: ["14"],
|
||||
childrenIds: ["24"],
|
||||
}),
|
||||
|
||||
// ========== 第四代 (子女辈,2010年代出生) ==========
|
||||
"17": createMember("17", "浩然", "male", 4, {
|
||||
fatherId: "9",
|
||||
motherId: "15",
|
||||
generationName: "浩",
|
||||
rank: 1,
|
||||
bio: "志远长子,在读高中生,成绩优异。",
|
||||
tags: ["学生"],
|
||||
birthDate: "2010-05-10",
|
||||
}),
|
||||
"18": createMember("18", "思琪", "female", 4, {
|
||||
fatherId: "9",
|
||||
motherId: "15",
|
||||
generationName: "思",
|
||||
rank: 2,
|
||||
bio: "志远次女,在读初中,喜欢绘画。",
|
||||
tags: ["学生"],
|
||||
birthDate: "2013-08-22",
|
||||
}),
|
||||
"19": createMember("19", "宇轩", "male", 4, {
|
||||
fatherId: "10",
|
||||
motherId: "16",
|
||||
generationName: "宇",
|
||||
rank: 1,
|
||||
bio: "志强独子,在读小学,对编程感兴趣。",
|
||||
tags: ["学生"],
|
||||
birthDate: "2015-11-18",
|
||||
}),
|
||||
"21": createMember("21", "梓涵", "female", 4, {
|
||||
fatherId: "11",
|
||||
motherId: "20",
|
||||
generationName: "梓",
|
||||
rank: 1,
|
||||
bio: "志明长女,在读大学,主修经济学。",
|
||||
tags: ["大学生"],
|
||||
birthDate: "2007-03-05",
|
||||
}),
|
||||
"22": createMember("22", "子轩", "male", 4, {
|
||||
fatherId: "11",
|
||||
motherId: "20",
|
||||
generationName: "子",
|
||||
rank: 2,
|
||||
bio: "志明次子,在读高中,擅长数学。",
|
||||
tags: ["学生"],
|
||||
birthDate: "2010-07-14",
|
||||
}),
|
||||
"24": createMember("24", "欣怡", "female", 4, {
|
||||
fatherId: "14",
|
||||
motherId: "23",
|
||||
generationName: "欣",
|
||||
rank: 1,
|
||||
bio: "志诚独女,在读初中,活泼可爱。",
|
||||
tags: ["学生"],
|
||||
birthDate: "2012-09-20",
|
||||
}),
|
||||
}
|
||||
|
||||
// Link relationships bi-directionally for the mock data
|
||||
members["1"].spouseIds = ["2"]
|
||||
members["1"].childrenIds = ["3", "4"]
|
||||
members["3"].childrenIds = ["5", "6"]
|
||||
|
||||
export const mockFamilyData: FamilyTreeData = {
|
||||
members,
|
||||
rootId: "1",
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
import type { FamilyMember } from "@/types/family"
|
||||
|
||||
/**
|
||||
* 请求浏览器通知权限
|
||||
*/
|
||||
export async function requestNotificationPermission(): Promise<boolean> {
|
||||
if (typeof window === "undefined" || !("Notification" in window)) {
|
||||
console.warn("浏览器不支持通知")
|
||||
return false
|
||||
}
|
||||
|
||||
if (Notification.permission === "granted") {
|
||||
return true
|
||||
}
|
||||
|
||||
if (Notification.permission !== "denied") {
|
||||
const permission = await Notification.requestPermission()
|
||||
return permission === "granted"
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送浏览器通知
|
||||
*/
|
||||
export function sendNotification(title: string, options?: NotificationOptions) {
|
||||
if (typeof window === "undefined" || !("Notification" in window)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (Notification.permission === "granted") {
|
||||
new Notification(title, {
|
||||
icon: "/icon.svg",
|
||||
badge: "/icon.svg",
|
||||
...options,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查今天是否有纪念日
|
||||
*/
|
||||
export function checkTodayAnniversaries(members: FamilyMember[]): {
|
||||
birthdays: FamilyMember[]
|
||||
deathdays: FamilyMember[]
|
||||
} {
|
||||
const today = new Date()
|
||||
const currentMonth = today.getMonth() + 1
|
||||
const currentDay = today.getDate()
|
||||
|
||||
const birthdays: FamilyMember[] = []
|
||||
const deathdays: FamilyMember[] = []
|
||||
|
||||
members.forEach((member) => {
|
||||
// 检查生日
|
||||
if (member.birthDate) {
|
||||
const birthDate = new Date(member.birthDate)
|
||||
if (birthDate.getMonth() + 1 === currentMonth && birthDate.getDate() === currentDay) {
|
||||
birthdays.push(member)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查忌日
|
||||
if (member.deathDate) {
|
||||
const deathDate = new Date(member.deathDate)
|
||||
if (deathDate.getMonth() + 1 === currentMonth && deathDate.getDate() === currentDay) {
|
||||
deathdays.push(member)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return { birthdays, deathdays }
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查即将到来的纪念日(未来7天)
|
||||
*/
|
||||
export function checkUpcomingAnniversaries(members: FamilyMember[], days: number = 7): {
|
||||
birthdays: Array<{ member: FamilyMember; daysUntil: number }>
|
||||
deathdays: Array<{ member: FamilyMember; daysUntil: number }>
|
||||
} {
|
||||
const today = new Date()
|
||||
const birthdays: Array<{ member: FamilyMember; daysUntil: number }> = []
|
||||
const deathdays: Array<{ member: FamilyMember; daysUntil: number }> = []
|
||||
|
||||
members.forEach((member) => {
|
||||
// 检查生日
|
||||
if (member.birthDate) {
|
||||
const birthDate = new Date(member.birthDate)
|
||||
const thisYearBirthday = new Date(today.getFullYear(), birthDate.getMonth(), birthDate.getDate())
|
||||
|
||||
if (thisYearBirthday < today) {
|
||||
thisYearBirthday.setFullYear(today.getFullYear() + 1)
|
||||
}
|
||||
|
||||
const daysUntil = Math.ceil((thisYearBirthday.getTime() - today.getTime()) / (1000 * 60 * 60 * 24))
|
||||
|
||||
if (daysUntil > 0 && daysUntil <= days) {
|
||||
birthdays.push({ member, daysUntil })
|
||||
}
|
||||
}
|
||||
|
||||
// 检查忌日
|
||||
if (member.deathDate) {
|
||||
const deathDate = new Date(member.deathDate)
|
||||
const thisYearDeathday = new Date(today.getFullYear(), deathDate.getMonth(), deathDate.getDate())
|
||||
|
||||
if (thisYearDeathday < today) {
|
||||
thisYearDeathday.setFullYear(today.getFullYear() + 1)
|
||||
}
|
||||
|
||||
const daysUntil = Math.ceil((thisYearDeathday.getTime() - today.getTime()) / (1000 * 60 * 60 * 24))
|
||||
|
||||
if (daysUntil > 0 && daysUntil <= days) {
|
||||
deathdays.push({ member, daysUntil })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return { birthdays, deathdays }
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送今日纪念日通知
|
||||
*/
|
||||
export function notifyTodayAnniversaries(members: FamilyMember[]) {
|
||||
const { birthdays, deathdays } = checkTodayAnniversaries(members)
|
||||
|
||||
if (birthdays.length > 0) {
|
||||
const names = birthdays.map(m => m.fullName).join("、")
|
||||
sendNotification("🎂 今日生日提醒", {
|
||||
body: `今天是 ${names} 的生日!`,
|
||||
tag: "birthday-today",
|
||||
})
|
||||
}
|
||||
|
||||
if (deathdays.length > 0) {
|
||||
const names = deathdays.map(m => m.fullName).join("、")
|
||||
sendNotification("🕯️ 今日忌日提醒", {
|
||||
body: `今天是 ${names} 的忌日,缅怀先人。`,
|
||||
tag: "deathday-today",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置每日通知检查
|
||||
*/
|
||||
export function setupDailyNotificationCheck(members: FamilyMember[]) {
|
||||
if (typeof window === "undefined") {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否已经在今天检查过
|
||||
const lastCheckDate = localStorage.getItem("lastNotificationCheck")
|
||||
const today = new Date().toDateString()
|
||||
|
||||
if (lastCheckDate !== today) {
|
||||
notifyTodayAnniversaries(members)
|
||||
localStorage.setItem("lastNotificationCheck", today)
|
||||
}
|
||||
|
||||
// 设置每小时检查一次(防止用户长时间开着页面)
|
||||
setInterval(() => {
|
||||
const currentDate = new Date().toDateString()
|
||||
const lastCheck = localStorage.getItem("lastNotificationCheck")
|
||||
|
||||
if (lastCheck !== currentDate) {
|
||||
notifyTodayAnniversaries(members)
|
||||
localStorage.setItem("lastNotificationCheck", currentDate)
|
||||
}
|
||||
}, 60 * 60 * 1000) // 每小时检查一次
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
export function registerServiceWorker() {
|
||||
if (typeof window === "undefined" || !("serviceWorker" in navigator)) {
|
||||
console.log("Service Worker 不被支持")
|
||||
return
|
||||
}
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
navigator.serviceWorker
|
||||
.register("/sw.js")
|
||||
.then((registration) => {
|
||||
console.log("Service Worker 注册成功:", registration.scope)
|
||||
|
||||
// 检查更新
|
||||
registration.addEventListener("updatefound", () => {
|
||||
const newWorker = registration.installing
|
||||
if (newWorker) {
|
||||
newWorker.addEventListener("statechange", () => {
|
||||
if (newWorker.state === "installed" && navigator.serviceWorker.controller) {
|
||||
// 新的 Service Worker 已安装,提示用户刷新
|
||||
if (confirm("发现新版本,是否立即更新?")) {
|
||||
window.location.reload()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("Service Worker 注册失败:", error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function unregisterServiceWorker() {
|
||||
if (typeof window === "undefined" || !("serviceWorker" in navigator)) {
|
||||
return
|
||||
}
|
||||
|
||||
navigator.serviceWorker.ready
|
||||
.then((registration) => {
|
||||
registration.unregister()
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Service Worker 注销失败:", error)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user