This commit is contained in:
freedakgmail
2025-11-24 14:02:34 +08:00
parent b7a8c9ee6e
commit 3d075c6076
941 changed files with 25613 additions and 27641 deletions
+176 -27
View File
@@ -1,6 +1,6 @@
"use client"
import { useState, useEffect } from "react"
import { useState, useEffect, useCallback } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { ScrollArea } from "@/components/ui/scroll-area"
@@ -11,6 +11,8 @@ import { format } from "date-fns"
import { useFamily } from "@/context/family-context"
import { useSession } from "next-auth/react"
import { useDialog } from "@/components/ui/alert-dialog-custom"
import Link from "next/link"
import { useSearchParams } from "next/navigation"
// 定义日志类型
interface ActivityLog {
@@ -21,12 +23,14 @@ interface ActivityLog {
entityName?: string | null
changes?: any
timestamp: string
treeId: string
userId: string
user?: {
name?: string | null
email?: string | null
}
tree?: {
id: string
name: string
}
}
@@ -72,6 +76,7 @@ export function ActivityLogViewer() {
const { currentTree } = useFamily()
const { data: session } = useSession()
const { showAlert, showConfirm, showPrompt } = useDialog()
const searchParams = useSearchParams()
const [logs, setLogs] = useState<ActivityLog[]>([])
const [trees, setTrees] = useState<FamilyTree[]>([])
const [loading, setLoading] = useState(false)
@@ -81,24 +86,47 @@ export function ActivityLogViewer() {
// 加载家族树列表
useEffect(() => {
fetch('/api/trees')
const controller = new AbortController()
fetch('/api/trees', { signal: controller.signal })
.then(res => res.json())
.then(data => {
if (data.trees) {
setTrees(data.trees)
}
})
.catch(err => console.error("加载家族树列表失败:", err))
.catch(err => {
if (err.name !== 'AbortError') {
console.error("加载家族树列表失败:", err)
}
})
return () => controller.abort()
}, [])
// currentTree 改变时,更新筛选
// 从 URL 参数或 currentTree 获取初始 treeId
// 只有在 trees 加载完成后才设置 filterTreeId
useEffect(() => {
if (currentTree?.id) {
setFilterTreeId(currentTree.id)
if (trees.length === 0) return // 等待 trees 加载完成
const urlTreeId = searchParams.get('treeId')
if (urlTreeId) {
// 优先使用 URL 中的 treeId
// 确保这个 treeId 在 trees 列表中存在
const treeExists = trees.some(t => t.id === urlTreeId)
if (treeExists) {
setFilterTreeId(urlTreeId)
}
} else if (currentTree?.id) {
// 如果 URL 中没有,使用 currentTree
const treeExists = trees.some(t => t.id === currentTree.id)
if (treeExists) {
setFilterTreeId(currentTree.id)
}
}
}, [currentTree?.id])
}, [searchParams, currentTree?.id, trees])
const loadLogs = async () => {
const loadLogs = useCallback(async (signal?: AbortSignal) => {
setLoading(true)
setError(null)
try {
@@ -110,22 +138,26 @@ export function ActivityLogViewer() {
url = `/api/trees/${filterTreeId}/activity-logs?limit=100`
}
const res = await fetch(url)
const res = await fetch(url, { signal })
if (!res.ok) throw new Error("获取日志失败")
const data = await res.json()
setLogs(data.logs || [])
} catch (err) {
console.error("加载日志失败:", err)
setError(err instanceof Error ? err.message : "加载失败")
if (err instanceof Error && err.name !== 'AbortError') {
console.error("加载日志失败:", err)
setError(err.message)
}
} finally {
setLoading(false)
}
}
}, [filterTreeId])
useEffect(() => {
loadLogs()
}, [filterTreeId])
const controller = new AbortController()
loadLogs(controller.signal)
return () => controller.abort()
}, [loadLogs])
// 检查当前用户是否是选中家族树的所有者
const isOwner = () => {
@@ -137,6 +169,113 @@ export function ActivityLogViewer() {
return selectedTree.ownerId === session?.user?.id || selectedTree.currentUserRole === "OWNER"
}
// 格式化变更详情,使其更易读
const formatChanges = (changes: any, action: string) => {
if (!changes || typeof changes !== 'object') return null
const fieldLabels: Record<string, string> = {
fullName: '姓名',
surname: '姓氏',
givenName: '名字',
gender: '性别',
birthDate: '出生日期',
deathDate: '去世日期',
birthPlace: '出生地',
ancestralHome: '祖籍',
generation: '世代',
generationName: '字辈',
courtesyName: '字',
artName: '号',
posthumousName: '谥号',
rank: '排行',
bio: '简介',
phone: '手机',
telephone: '电话',
email: '邮箱',
address: '地址',
photoIds: '照片',
spouseIds: '配偶',
childrenIds: '子女',
motherId: '母亲',
fatherId: '父亲',
isFounder: '始祖',
isLunarDate: '农历日期',
burialPlace: '安葬地',
tags: '标签',
}
const formatValue = (key: string, value: any) => {
if (value === null || value === undefined) return '无'
if (key === 'gender') {
return value === 'MALE' ? '男' : value === 'FEMALE' ? '女' : '未知'
}
if (key === 'tags' && Array.isArray(value)) {
return value.length > 0 ? value.join(', ') : '无'
}
if (Array.isArray(value)) {
return `${value.length}`
}
if (typeof value === 'boolean') {
return value ? '是' : '否'
}
if (typeof value === 'string' && value.length > 50) {
return value.substring(0, 50) + '...'
}
return String(value)
}
const getChangeDescription = (key: string, value: any) => {
const label = fieldLabels[key] || key
// 如果是对象且包含 old 和 new,说明是修改
if (value && typeof value === 'object' && 'old' in value && 'new' in value) {
const oldVal = formatValue(key, value.old)
const newVal = formatValue(key, value.new)
// 特殊处理数组变化
if (key === 'photoIds') {
const oldCount = Array.isArray(value.old) ? value.old.length : 0
const newCount = Array.isArray(value.new) ? value.new.length : 0
if (newCount > oldCount) {
return `添加了${label}:新增 ${newCount - oldCount}`
} else if (newCount < oldCount) {
return `删除了${label}:减少 ${oldCount - newCount}`
}
}
if (key === 'childrenIds' || key === 'spouseIds') {
const oldCount = Array.isArray(value.old) ? value.old.length : 0
const newCount = Array.isArray(value.new) ? value.new.length : 0
if (newCount > oldCount) {
return `添加了${label}:新增 ${newCount - oldCount}`
} else if (newCount < oldCount) {
return `删除了${label}:减少 ${oldCount - newCount}`
}
}
return `修改了${label}:从 "${oldVal}" → "${newVal}"`
}
return null
}
const entries = Object.entries(changes)
.filter(([key]) => fieldLabels[key]) // 只显示有标签的字段
.map(([key, value]: [string, any]) => {
const description = getChangeDescription(key, value)
if (!description) return null
return (
<div key={key} className="text-xs py-1.5 text-muted-foreground">
{description}
</div>
)
})
.filter(Boolean) // 移除 null 值
return entries.length > 0 ? entries : null
}
const handleClearLogs = async () => {
if (filterTreeId === "all") {
await showAlert("请先选择一个具体的家族树再清空日志", "提示")
@@ -210,7 +349,7 @@ export function ActivityLogViewer() {
<Button
variant="outline"
size="icon"
onClick={loadLogs}
onClick={() => loadLogs()}
disabled={loading || clearing}
className="h-9 w-9"
title="刷新日志"
@@ -291,9 +430,18 @@ export function ActivityLogViewer() {
</Badge>
)}
{log.entityName && (
<span className="text-sm font-medium truncate">
{log.entityName}
</span>
log.entityId && log.entityType === 'MEMBER' ? (
<Link
href={`/members/${log.entityId}?treeId=${log.treeId}`}
className="text-sm font-medium truncate hover:text-primary hover:underline transition-colors"
>
{log.entityName}
</Link>
) : (
<span className="text-sm font-medium truncate">
{log.entityName}
</span>
)
)}
</div>
<div className="flex items-center justify-between">
@@ -304,15 +452,16 @@ export function ActivityLogViewer() {
{format(new Date(log.timestamp), "yyyy-MM-dd HH:mm:ss")}
</p>
</div>
{log.changes && Object.keys(log.changes).length > 0 && (
<details className="mt-2">
<summary className="text-xs text-muted-foreground cursor-pointer hover:text-foreground">
</summary>
<pre className="text-xs mt-1 p-2 bg-background rounded overflow-x-auto">
{JSON.stringify(log.changes, null, 2)}
</pre>
</details>
{log.action === 'UPDATE' && (
<div className="mt-2 pl-2 border-l-2 border-muted">
{log.changes && formatChanges(log.changes, log.action) ? (
formatChanges(log.changes, log.action)
) : (
<div className="text-xs py-1.5 text-muted-foreground italic">
</div>
)}
</div>
)}
</div>
</div>
+13 -2
View File
@@ -10,6 +10,7 @@ import { useDialog } from "@/components/ui/alert-dialog-custom"
import { requestNotificationPermission, checkUpcomingAnniversaries } from "@/lib/notifications"
import { useFamily } from "@/context/family-context"
import { Alert, AlertDescription } from "@/components/ui/alert"
import { MemberNameWithStatus } from "@/components/member-name-with-status"
export function NotificationSettings() {
const { showAlert } = useDialog()
@@ -162,7 +163,12 @@ export function NotificationSettings() {
className="flex items-center justify-between p-3 bg-muted/50 rounded-lg"
>
<div>
<p className="font-medium">{member.fullName}</p>
<p className="font-medium">
<MemberNameWithStatus
name={member.fullName}
isDead={!!member.deathDate}
/>
</p>
<p className="text-sm text-muted-foreground">
{new Date(member.birthDate).toLocaleDateString('zh-CN', {
month: 'long',
@@ -193,7 +199,12 @@ export function NotificationSettings() {
className="flex items-center justify-between p-3 bg-muted/50 rounded-lg"
>
<div>
<p className="font-medium">{member.fullName}</p>
<p className="font-medium">
<MemberNameWithStatus
name={member.fullName}
isDead={!!member.deathDate}
/>
</p>
<p className="text-sm text-muted-foreground">
{new Date(member.deathDate!).toLocaleDateString('zh-CN', {
month: 'long',