0.0.8.5
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user