This commit is contained in:
freedakgmail
2025-11-22 23:49:24 +08:00
parent b83382b604
commit c033e46782
3580 changed files with 1745279 additions and 2428 deletions
+341
View File
@@ -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
View File
@@ -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",
+174
View File
@@ -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) // 每小时检查一次
}
+46
View File
@@ -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)
})
}