This commit is contained in:
freedakgmail
2025-11-23 00:49:16 +08:00
parent c033e46782
commit 626d7dddde
184 changed files with 321926 additions and 2720 deletions
+133
View File
@@ -0,0 +1,133 @@
import { db, type ActivityLog } from "./db"
import { v4 as uuidv4 } from "uuid"
/**
* 记录活动日志
*/
export async function logActivity(
action: ActivityLog["action"],
entityType: ActivityLog["entityType"],
entityId?: string,
entityName?: string,
changes?: any
): Promise<void> {
try {
const log: ActivityLog = {
id: uuidv4(),
action,
entityType,
entityId,
entityName,
changes,
timestamp: new Date().toISOString(),
userId: "local-user", // 本地版本暂时使用固定用户ID
userName: "本地用户",
}
await db.activityLogs.add(log)
} catch (error) {
console.error("记录活动日志失败:", error)
}
}
/**
* 获取活动日志列表
*/
export async function getActivityLogs(limit: number = 50): Promise<ActivityLog[]> {
try {
return await db.activityLogs
.orderBy("timestamp")
.reverse()
.limit(limit)
.toArray()
} catch (error) {
console.error("获取活动日志失败:", error)
return []
}
}
/**
* 获取特定成员的活动日志
*/
export async function getMemberActivityLogs(memberId: string): Promise<ActivityLog[]> {
try {
return await db.activityLogs
.where("entityId")
.equals(memberId)
.reverse()
.sortBy("timestamp")
} catch (error) {
console.error("获取成员活动日志失败:", error)
return []
}
}
/**
* 清除旧日志(保留最近N天)
*/
export async function cleanOldLogs(daysToKeep: number = 90): Promise<number> {
try {
const cutoffDate = new Date()
cutoffDate.setDate(cutoffDate.getDate() - daysToKeep)
const cutoffTimestamp = cutoffDate.toISOString()
const oldLogs = await db.activityLogs
.where("timestamp")
.below(cutoffTimestamp)
.toArray()
await db.activityLogs
.where("timestamp")
.below(cutoffTimestamp)
.delete()
return oldLogs.length
} catch (error) {
console.error("清除旧日志失败:", error)
return 0
}
}
/**
* 获取活动统计
*/
export async function getActivityStats(): Promise<{
total: number
byAction: Record<string, number>
byEntityType: Record<string, number>
recentCount: number
}> {
try {
const allLogs = await db.activityLogs.toArray()
const oneDayAgo = new Date()
oneDayAgo.setDate(oneDayAgo.getDate() - 1)
const byAction: Record<string, number> = {}
const byEntityType: Record<string, number> = {}
let recentCount = 0
allLogs.forEach((log) => {
byAction[log.action] = (byAction[log.action] || 0) + 1
byEntityType[log.entityType] = (byEntityType[log.entityType] || 0) + 1
if (new Date(log.timestamp) > oneDayAgo) {
recentCount++
}
})
return {
total: allLogs.length,
byAction,
byEntityType,
recentCount,
}
} catch (error) {
console.error("获取活动统计失败:", error)
return {
total: 0,
byAction: {},
byEntityType: {},
recentCount: 0,
}
}
}
+268
View File
@@ -0,0 +1,268 @@
import type { FamilyMember } from "@/types/family"
/**
* AI 辅助功能配置
*/
export interface AIConfig {
provider: "openai" | "custom" | "browser"
apiKey?: string
apiEndpoint?: string
model?: string
}
/**
* 获取 AI 配置
*/
export function getAIConfig(): AIConfig | null {
if (typeof window === "undefined") return null
const config = localStorage.getItem("ai-config")
return config ? JSON.parse(config) : null
}
/**
* 保存 AI 配置
*/
export function saveAIConfig(config: AIConfig): void {
if (typeof window === "undefined") return
localStorage.setItem("ai-config", JSON.stringify(config))
}
/**
* 生成成员简介
*/
export async function generateBio(member: Partial<FamilyMember>): Promise<string> {
const config = getAIConfig()
if (!config) {
throw new Error("请先配置 AI 服务")
}
const prompt = `请根据以下信息生成一段简洁的人物简介(100字以内):
姓名:${member.fullName || "未知"}
性别:${member.gender === "male" ? "男" : member.gender === "female" ? "女" : "未知"}
出生日期:${member.birthDate || "未知"}
${member.deathDate ? `逝世日期:${member.deathDate}` : ""}
籍贯:${member.ancestralHome || "未知"}
${member.courtesyName ? `字:${member.courtesyName}` : ""}
${member.artName ? `号:${member.artName}` : ""}
请用第三人称,简洁专业的语言描述。`
return await callAI(prompt, config)
}
/**
* 分析家族关系
*/
export async function analyzeRelationship(
member1: FamilyMember,
member2: FamilyMember,
allMembers: FamilyMember[]
): Promise<string> {
const config = getAIConfig()
if (!config) {
throw new Error("请先配置 AI 服务")
}
const prompt = `请分析以下两位家族成员的关系:
成员A${member1.fullName},第${member1.generation}
成员B${member2.fullName},第${member2.generation}
家族成员总数:${allMembers.length}
请分析他们可能的关系(如:兄弟、堂兄弟、叔侄等),并给出理由。`
return await callAI(prompt, config)
}
/**
* 智能推荐字辈
*/
export async function suggestGenerationName(
generation: number,
existingNames: string[]
): Promise<string[]> {
const config = getAIConfig()
if (!config) {
throw new Error("请先配置 AI 服务")
}
const prompt = `请为家族第${generation}世推荐5个合适的字辈用字。
已使用的字辈:${existingNames.join("、")}
要求:
1. 符合中国传统字辈命名习惯
2. 寓意吉祥、积极向上
3. 避免与已有字辈重复
4. 只返回5个汉字,用逗号分隔`
const response = await callAI(prompt, config)
return response.split(/[,,、]/).map(s => s.trim()).filter(Boolean).slice(0, 5)
}
/**
* 生成家族故事标题
*/
export async function generateStoryTitle(content: string): Promise<string> {
const config = getAIConfig()
if (!config) {
throw new Error("请先配置 AI 服务")
}
const prompt = `请为以下家族故事生成一个简洁有吸引力的标题(10字以内):
${content.substring(0, 200)}...
只返回标题,不要其他内容。`
return await callAI(prompt, config)
}
/**
* 智能数据校验
*/
export async function validateMemberData(member: Partial<FamilyMember>): Promise<{
valid: boolean
issues: string[]
suggestions: string[]
}> {
const config = getAIConfig()
if (!config) {
return {
valid: true,
issues: [],
suggestions: []
}
}
const prompt = `请检查以下家族成员信息是否合理:
姓名:${member.fullName || "未填写"}
性别:${member.gender || "未填写"}
出生日期:${member.birthDate || "未填写"}
逝世日期:${member.deathDate || "未填写"}
世代:${member.generation || "未填写"}
请指出可能存在的问题和改进建议。以JSON格式返回:
{
"valid": true/false,
"issues": ["问题1", "问题2"],
"suggestions": ["建议1", "建议2"]
}`
try {
const response = await callAI(prompt, config)
return JSON.parse(response)
} catch (error) {
return {
valid: true,
issues: [],
suggestions: []
}
}
}
/**
* 调用 AI API
*/
async function callAI(prompt: string, config: AIConfig): Promise<string> {
if (config.provider === "browser") {
// 使用浏览器内置 AI(如果支持)
if ("ai" in window && "languageModel" in (window as any).ai) {
try {
const session = await (window as any).ai.languageModel.create()
const result = await session.prompt(prompt)
return result
} catch (error) {
throw new Error("浏览器 AI 不可用")
}
} else {
throw new Error("浏览器不支持内置 AI")
}
}
if (config.provider === "openai") {
// 调用 OpenAI API
const response = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${config.apiKey}`,
},
body: JSON.stringify({
model: config.model || "gpt-3.5-turbo",
messages: [
{
role: "system",
content: "你是一个专业的家谱管理助手,帮助用户整理和分析家族信息。"
},
{
role: "user",
content: prompt
}
],
temperature: 0.7,
max_tokens: 500,
}),
})
if (!response.ok) {
throw new Error(`API 调用失败: ${response.statusText}`)
}
const data = await response.json()
return data.choices[0].message.content.trim()
}
if (config.provider === "custom") {
// 调用自定义 API
const response = await fetch(config.apiEndpoint!, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(config.apiKey ? { "Authorization": `Bearer ${config.apiKey}` } : {}),
},
body: JSON.stringify({ prompt }),
})
if (!response.ok) {
throw new Error(`API 调用失败: ${response.statusText}`)
}
const data = await response.json()
return data.response || data.result || data.text || ""
}
throw new Error("不支持的 AI 提供商")
}
/**
* 检查 AI 是否可用
*/
export function isAIAvailable(): boolean {
const config = getAIConfig()
if (!config) return false
if (config.provider === "browser") {
return typeof window !== "undefined" && "ai" in window
}
if (config.provider === "openai") {
return !!config.apiKey
}
if (config.provider === "custom") {
return !!config.apiEndpoint
}
return false
}
+71
View File
@@ -0,0 +1,71 @@
import Dexie from "dexie"
import { db } from "./db"
/**
* 检查数据库版本并升级
*/
export async function checkAndUpgradeDatabase(): Promise<boolean> {
try {
// 检查 activityLogs 表是否存在
const tables = db.tables.map(t => t.name)
if (!tables.includes("activityLogs")) {
console.log("检测到需要升级数据库...")
// 关闭当前连接
db.close()
// 删除旧数据库(注意:这会清除所有数据)
// 在生产环境中,应该做数据迁移而不是删除
await Dexie.delete("FamilyTreeDB")
// 重新打开数据库(会自动创建最新版本)
await db.open()
console.log("数据库升级完成")
return true
}
return false
} catch (error) {
console.error("数据库升级失败:", error)
return false
}
}
/**
* 手动升级数据库(保留数据)
*/
export async function upgradeDatabase(): Promise<void> {
try {
// 1. 备份现有数据
const members = await db.members.toArray()
const settings = await db.settings.toArray()
const images = await db.images.toArray()
console.log("备份数据:", { members: members.length, settings: settings.length, images: images.length })
// 2. 关闭并删除数据库
db.close()
await Dexie.delete("FamilyTreeDB")
// 3. 重新打开(会创建新版本)
await db.open()
// 4. 恢复数据
if (members.length > 0) {
await db.members.bulkAdd(members)
}
if (settings.length > 0) {
await db.settings.bulkAdd(settings)
}
if (images.length > 0) {
await db.images.bulkAdd(images)
}
console.log("数据库升级完成,数据已恢复")
} catch (error) {
console.error("数据库升级失败:", error)
throw error
}
}
+19
View File
@@ -13,10 +13,23 @@ export interface StoredImage {
createdAt: string
}
export interface ActivityLog {
id: string
action: "create" | "update" | "delete" | "import" | "export"
entityType: "member" | "photo" | "story" | "settings"
entityId?: string
entityName?: string
changes?: any
timestamp: string
userId?: string
userName?: string
}
export class FamilyTreeDB extends Dexie {
members!: Table<FamilyMember>
settings!: Table<GlobalSettings>
images!: Table<StoredImage>
activityLogs!: Table<ActivityLog>
constructor() {
super("FamilyTreeDB")
@@ -25,6 +38,12 @@ export class FamilyTreeDB extends Dexie {
settings: "key", // Key-value store for rootId etc.
images: "id", // Store images by ID
})
this.version(2).stores({
members: "id, fullName, fatherId, motherId, [fatherId+motherId]", // Index for searching
settings: "key", // Key-value store for rootId etc.
images: "id", // Store images by ID
activityLogs: "id, timestamp, action, entityType, entityId",
})
}
}