0.0.0.5
This commit is contained in:
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user