230 lines
8.1 KiB
TypeScript
230 lines
8.1 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect } from "react"
|
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { User } from "lucide-react"
|
|
import { format } from "date-fns"
|
|
import { zhCN } from "date-fns/locale"
|
|
|
|
interface MemberHistory {
|
|
id: string
|
|
memberId: string
|
|
version: number
|
|
snapshot: any
|
|
changedBy: string
|
|
changedAt: string
|
|
changeType: "CREATE" | "UPDATE" | "DELETE"
|
|
changes: Record<string, { old: any, new: any }> | null
|
|
user?: {
|
|
name?: string | null
|
|
email?: string | null
|
|
}
|
|
}
|
|
|
|
interface MemberVersionHistoryProps {
|
|
memberId: string
|
|
treeId: string
|
|
onVersionSelect?: (version: MemberHistory) => void
|
|
lastUpdated?: number // 用于触发刷新的时间戳
|
|
}
|
|
|
|
export function MemberVersionHistory({ memberId, treeId, onVersionSelect, lastUpdated }: MemberVersionHistoryProps) {
|
|
const [history, setHistory] = useState<MemberHistory[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
useEffect(() => {
|
|
loadHistory()
|
|
}, [memberId, treeId, lastUpdated])
|
|
|
|
const loadHistory = async () => {
|
|
try {
|
|
setLoading(true)
|
|
const res = await fetch(`/api/trees/${treeId}/members/${memberId}/history`)
|
|
if (!res.ok) throw new Error("获取历史失败")
|
|
|
|
const data = await res.json()
|
|
setHistory(data.history || [])
|
|
} catch (error) {
|
|
console.error("加载历史失败:", error)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
|
|
const getChangeTypeLabel = (type: string) => {
|
|
const labels: Record<string, string> = {
|
|
CREATE: "创建",
|
|
UPDATE: "更新",
|
|
DELETE: "删除",
|
|
}
|
|
return labels[type] || type
|
|
}
|
|
|
|
const getChangeTypeColor = (type: string) => {
|
|
const colors: Record<string, string> = {
|
|
CREATE: "bg-green-100 text-green-700 border-green-200",
|
|
UPDATE: "bg-blue-100 text-blue-700 border-blue-200",
|
|
DELETE: "bg-red-100 text-red-700 border-red-200",
|
|
}
|
|
return colors[type] || "bg-gray-100 text-gray-700 border-gray-200"
|
|
}
|
|
|
|
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: '标签',
|
|
spouseFatherName: '岳父/公公', spouseMotherName: '岳母/婆婆',
|
|
avatarUrl: '头像', stories: '家族故事',
|
|
}
|
|
|
|
const formatValue = (value: any, field?: string) => {
|
|
if (value === null || value === undefined) return '-'
|
|
if (typeof value === 'boolean') return value ? '是' : '否'
|
|
if (Array.isArray(value)) {
|
|
if (field === 'photoIds') return `${value.length} 张照片`
|
|
if (field === 'stories') return `${value.length} 个故事`
|
|
return value.length > 0 ? value.join(', ') : '-'
|
|
}
|
|
if (field === 'avatarUrl') return value ? '已上传' : '无'
|
|
return String(value)
|
|
}
|
|
|
|
const renderVersionChanges = (changes: Record<string, { old: any, new: any }> | null, changeType: string) => {
|
|
// 如果是创建操作,不显示变更详情
|
|
if (changeType === 'CREATE') {
|
|
return (
|
|
<div className="mt-3 text-xs text-muted-foreground">
|
|
创建了新成员
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// 如果没有变更信息
|
|
if (!changes || Object.keys(changes).length === 0) {
|
|
return (
|
|
<div className="mt-3 text-xs text-muted-foreground">
|
|
无变更记录
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// 分组显示变更
|
|
const relationFields = ['fatherId', 'motherId', 'spouseIds', 'childrenIds', 'spouseFatherName', 'spouseMotherName']
|
|
const changedRelations = Object.entries(changes).filter(([key]) => relationFields.includes(key))
|
|
const changedOthers = Object.entries(changes).filter(([key]) => !relationFields.includes(key))
|
|
|
|
return (
|
|
<div className="mt-3 space-y-4">
|
|
{/* 家族关系变更 */}
|
|
{changedRelations.length > 0 && (
|
|
<div>
|
|
<h4 className="text-xs font-semibold text-foreground mb-2">家族关系变更</h4>
|
|
<div className="space-y-2 text-xs">
|
|
{changedRelations.map(([field, change]) => (
|
|
<div key={field} className="flex items-start gap-2">
|
|
<span className="text-muted-foreground min-w-[60px]">{fieldLabels[field]}:</span>
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-red-600 line-through">{formatValue(change.old, field)}</span>
|
|
<span className="text-muted-foreground">→</span>
|
|
<span className="text-green-600 font-medium">{formatValue(change.new, field)}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 其他信息变更 */}
|
|
{changedOthers.length > 0 && (
|
|
<div>
|
|
<h4 className="text-xs font-semibold text-foreground mb-2">基本信息变更</h4>
|
|
<div className="space-y-2 text-xs">
|
|
{changedOthers.map(([field, change]) => (
|
|
<div key={field} className="flex items-start gap-2">
|
|
<span className="text-muted-foreground min-w-[60px]">{fieldLabels[field]}:</span>
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
<span className="text-red-600 line-through">{formatValue(change.old, field)}</span>
|
|
<span className="text-muted-foreground">→</span>
|
|
<span className="text-green-600 font-medium">{formatValue(change.new, field)}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="text-center py-8 text-muted-foreground">
|
|
加载中...
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (history.length === 0) {
|
|
return (
|
|
<div className="text-center py-8 text-muted-foreground">
|
|
暂无历史记录
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="w-full overflow-hidden">
|
|
<p className="text-sm text-muted-foreground mb-4">
|
|
共 {history.length} 个版本
|
|
</p>
|
|
<ScrollArea className="h-[400px] w-full">
|
|
<div className="space-y-6 pr-4">
|
|
{history.map((record) => (
|
|
<div
|
|
key={record.id}
|
|
className="p-4 rounded-lg border border-border bg-card shadow-sm overflow-hidden"
|
|
>
|
|
{/* 版本头部 */}
|
|
<div className="flex items-center justify-between mb-3 pb-3 border-b border-border">
|
|
<div className="flex items-center gap-2">
|
|
<Badge variant="outline" className={getChangeTypeColor(record.changeType)}>
|
|
{getChangeTypeLabel(record.changeType)}
|
|
</Badge>
|
|
<span className="text-sm font-semibold">
|
|
版本 {record.version}
|
|
</span>
|
|
</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
{format(new Date(record.changedAt), 'yyyy-MM-dd HH:mm', { locale: zhCN })}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 操作人 */}
|
|
{record.user && (
|
|
<div className="flex items-center gap-2 text-xs text-muted-foreground mb-3">
|
|
<User className="h-3 w-3" />
|
|
<span>{record.user.name || record.user.email || '未知用户'}</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* 变更详细内容 */}
|
|
{renderVersionChanges(record.changes, record.changeType)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</ScrollArea>
|
|
</div>
|
|
)
|
|
}
|