This commit is contained in:
freedakgmail
2025-12-21 17:32:33 +08:00
parent cffdd75e96
commit 5c8c70097c
190 changed files with 2746 additions and 893 deletions
+114 -89
View File
@@ -6,6 +6,7 @@ import type { FamilyMember } from "@/types/family"
import { PhotoGallery } from "@/components/members/photo-gallery"
import { StoryManager } from "@/components/members/story-manager"
import { useDialog } from "@/components/ui/alert-dialog-custom"
// useDialog 已导入,用于 showAlert 和 showConfirm
import { permissions } from "@/lib/permissions"
import { MemberNameWithStatus } from "@/components/member-name-with-status"
import { MemberVersionHistory } from "@/components/members/member-version-history"
@@ -24,7 +25,7 @@ import { AddRelationDialog, type RelationType } from "@/components/tree/add-rela
export default function MemberProfilePage() {
const { id } = useParams()
const { treeData, currentTree, getMember, updateMember, deleteMember, isLoading } = useFamily()
const { showConfirm } = useDialog()
const { showConfirm, showAlert } = useDialog()
const router = useRouter()
const { data: session, status } = useSession()
const [isEditing, setIsEditing] = useState(false)
@@ -90,6 +91,90 @@ export default function MemberProfilePage() {
}
}, [member?.avatarUrl])
// 姓名分解函数
const parseName = (fullName: string): { surname: string; givenName: string } => {
const name = fullName.trim()
if (!name) return { surname: '', givenName: '' }
const compoundSurnames = [
'欧阳', '太史', '端木', '上官', '司马', '东方', '独孤', '南宫', '万俟', '闻人',
'夏侯', '诸葛', '尉迟', '公羊', '赫连', '澹台', '皇甫', '宗政', '濮阳', '公冶',
'太叔', '申屠', '公孙', '慕容', '仲孙', '钟离', '长孙', '宇文', '司徒', '鲜于',
'司空', '闾丘', '子车', '亓官', '司寇', '巫马', '公西', '颛孙', '壤驷', '公良',
'漆雕', '乐正', '宰父', '谷梁', '拓跋', '夹谷', '轩辕', '令狐', '段干', '百里',
'呼延', '东郭', '南门', '羊舌', '微生', '公户', '公玉', '公仪', '梁丘', '公仲',
'公上', '公门', '公山', '公坚', '左丘', '公伯', '西门', '公祖', '第五', '公乘',
'贯丘', '公皙', '南荣', '东里', '东宫', '仲长', '子书', '子桑', '即墨', '达奚', '褚师'
]
for (const compound of compoundSurnames) {
if (name.startsWith(compound) && name.length > compound.length) {
return { surname: compound, givenName: name.slice(compound.length) }
}
}
if (name.length >= 2) {
return { surname: name.charAt(0), givenName: name.slice(1) }
}
return { surname: name, givenName: '' }
}
// 创建新成员的处理函数 - 必须在条件return之前定义
const handleCreateNewRelation = useCallback((prefillName: string, relationType: RelationType) => {
if (!member) return
const params = new URLSearchParams()
const { surname, givenName } = parseName(prefillName)
if (surname) params.set("surname", surname)
if (givenName) params.set("givenName", givenName)
if (relationType === "child") {
params.set("generation", String(member.generation + 1))
if (member.gender === "MALE") {
params.set("fatherId", member.id)
if (member.spouseIds?.length) {
params.set("motherId", member.spouseIds[0])
}
} else if (member.gender === "FEMALE") {
params.set("motherId", member.id)
if (member.spouseIds?.length) {
params.set("fatherId", member.spouseIds[0])
}
}
} else if (relationType === "spouse") {
params.set("generation", String(member.generation))
params.set("spouseId", member.id)
params.set("gender", member.gender === "MALE" ? "FEMALE" : "MALE")
if (member.childrenIds && member.childrenIds.length > 0) {
member.childrenIds.forEach(id => {
params.append('childrenIds', id)
})
}
} else if (relationType === "father") {
params.set("generation", String(member.generation === 1 ? 1 : member.generation - 1))
params.set("childId", member.id)
params.set("gender", "MALE")
if (member.motherId) {
params.set("spouseId", member.motherId)
}
} else if (relationType === "mother") {
params.set("generation", String(member.generation === 1 ? 1 : member.generation - 1))
params.set("childId", member.id)
params.set("gender", "FEMALE")
if (member.fatherId) {
params.set("spouseId", member.fatherId)
}
}
if (currentTree?.id) {
params.set("treeId", currentTree.id)
}
setRelationDialog(null)
router.push(`/members/new?${params.toString()}`)
}, [member, currentTree?.id, router])
// 显示加载状态
if (isLoading) {
return (
@@ -122,97 +207,37 @@ export default function MemberProfilePage() {
}
const handleDelete = async () => {
const confirmed = await showConfirm("确定要删除这位成员吗?这将同时删除其关联关系。", "删除成员", "destructive")
// 检查是否正在删除始祖
let confirmMessage = "确定要删除这位成员吗?这将同时删除其关联关系。"
if (member.generation === 1 && (!member.fatherId && !member.motherId)) {
confirmMessage = "您正在删除始祖,这将导致全族代数重新计算。\n原第2代将变为第1代,以此类推。\n\n确定要删除这位成员吗?"
}
const confirmed = await showConfirm(confirmMessage, "删除成员", "destructive")
if (confirmed) {
deleteMember(member.id)
const params = new URLSearchParams(window.location.search)
router.push(`/members?${params.toString()}`)
try {
const result = await deleteMember(member.id)
// 构建跳转参数,只保留 treeId
const treeId = new URLSearchParams(window.location.search).get('treeId')
const params = new URLSearchParams()
if (treeId) params.set('treeId', treeId)
// 如果有提示信息(代数调整),显示给用户,等待确认后再跳转
if (result.message) {
await showAlert(result.message, "提示")
}
// 删除后跳转到成员列表页
router.push(`/members?${params.toString()}`)
} catch (error) {
console.error('删除成员失败:', error)
await showAlert('删除成员失败: ' + (error as Error).message, "错误")
}
}
}
// 姓名分解函数
const parseName = (fullName: string): { surname: string; givenName: string } => {
const name = fullName.trim()
if (!name) return { surname: '', givenName: '' }
const compoundSurnames = [
'欧阳', '太史', '端木', '上官', '司马', '东方', '独孤', '南宫', '万俟', '闻人',
'夏侯', '诸葛', '尉迟', '公羊', '赫连', '澹台', '皇甫', '宗政', '濮阳', '公冶',
'太叔', '申屠', '公孙', '慕容', '仲孙', '钟离', '长孙', '宇文', '司徒', '鲜于',
'司空', '闾丘', '子车', '亓官', '司寇', '巫马', '公西', '颛孙', '壤驷', '公良',
'漆雕', '乐正', '宰父', '谷梁', '拓跋', '夹谷', '轩辕', '令狐', '段干', '百里',
'呼延', '东郭', '南门', '羊舌', '微生', '公户', '公玉', '公仪', '梁丘', '公仲',
'公上', '公门', '公山', '公坚', '左丘', '公伯', '西门', '公祖', '第五', '公乘',
'贯丘', '公皙', '南荣', '东里', '东宫', '仲长', '子书', '子桑', '即墨', '达奚', '褚师'
]
for (const compound of compoundSurnames) {
if (name.startsWith(compound) && name.length > compound.length) {
return { surname: compound, givenName: name.slice(compound.length) }
}
}
if (name.length >= 2) {
return { surname: name.charAt(0), givenName: name.slice(1) }
}
return { surname: name, givenName: '' }
}
// 创建新成员的处理函数
const handleCreateNewRelation = useCallback((prefillName: string, relationType: RelationType) => {
if (!member) return
const params = new URLSearchParams()
const { surname, givenName } = parseName(prefillName)
if (surname) params.set("surname", surname)
if (givenName) params.set("givenName", givenName)
if (relationType === "child") {
params.set("generation", String(member.generation + 1))
if (member.gender === "MALE") {
params.set("fatherId", member.id)
if (member.spouseIds?.length) {
params.set("motherId", member.spouseIds[0])
}
} else if (member.gender === "FEMALE") {
params.set("motherId", member.id)
if (member.spouseIds?.length) {
params.set("fatherId", member.spouseIds[0])
}
}
} else if (relationType === "spouse") {
params.set("generation", String(member.generation))
params.set("spouseId", member.id)
params.set("gender", member.gender === "MALE" ? "FEMALE" : "MALE")
if (member.childrenIds && member.childrenIds.length > 0) {
member.childrenIds.forEach(id => {
params.append('childrenIds', id)
})
}
} else if (relationType === "father") {
params.set("generation", String(member.generation - 1))
params.set("childId", member.id)
params.set("gender", "MALE")
if (member.motherId) {
params.set("spouseId", member.motherId)
}
} else if (relationType === "mother") {
params.set("generation", String(member.generation - 1))
params.set("childId", member.id)
params.set("gender", "FEMALE")
if (member.fatherId) {
params.set("spouseId", member.fatherId)
}
}
if (currentTree?.id) {
params.set("treeId", currentTree.id)
}
setRelationDialog(null)
router.push(`/members/new?${params.toString()}`)
}, [member, currentTree?.id, router])
// 创建新成员的处理函数 - 已移到条件return之前
const handleQuickAddChild = () => {
setRelationDialog({ type: 'child' })
@@ -259,7 +284,7 @@ export default function MemberProfilePage() {
setLastSaveTime(Date.now())
} catch (error) {
console.error('上传头像失败:', error)
alert('上传头像失败: ' + (error instanceof Error ? error.message : '未知错误'))
showAlert('上传头像失败: ' + (error instanceof Error ? error.message : '未知错误'), "错误")
} finally {
setIsUploadingAvatar(false)
}
+62 -13
View File
@@ -12,23 +12,25 @@ import type { FamilyMember } from "@/types/family"
import { useMemo, useEffect, useState } from "react"
import { useSession } from "next-auth/react"
import { BackButton } from "@/components/ui/back-button"
import { useDialog } from "@/components/ui/alert-dialog-custom"
export default function NewMemberPage() {
const router = useRouter()
const searchParams = useSearchParams()
const { addMember, treeData, getMember } = useFamily()
const { showAlert, showConfirm } = useDialog()
const { data: session, status } = useSession()
const [activeTab, setActiveTab] = useState("info")
const existingMembers = Object.values(treeData.members)
const isFirstMember = existingMembers.length === 0
// 未登录时重定向到登录页
useEffect(() => {
if (status === 'unauthenticated') {
router.push('/auth/signin')
}
}, [status, router])
const existingMembers = Object.values(treeData.members)
const isFirstMember = existingMembers.length === 0
// 从URL参数获取预填信息
const initialData = useMemo(() => {
@@ -128,9 +130,46 @@ export default function NewMemberPage() {
return '成员'
}, [searchParams, isFirstMember])
// 判断是否是给始祖添加父母(新成员将成为新始祖)
const isAddingParentToFounder = useMemo(() => {
const childId = searchParams.get('childId')
if (!childId) return false
const child = getMember(childId)
return child?.generation === 1
}, [searchParams, getMember])
// 是否应该显示始祖样式
const showFounderStyle = isFirstMember || isAddingParentToFounder
const handleSave = async (newMember: FamilyMember) => {
try {
// 检查是否正在给始祖添加父母
const childId = searchParams.get('childId')
if (childId && memberType === '父母') {
const child = getMember(childId)
// 如果子女不存在(已被删除),提示用户
if (!child) {
await showAlert('目标成员已不存在,无法添加父母。请返回成员列表重新操作。', "提示")
const params = new URLSearchParams(window.location.search)
router.push(`/members?${params.toString()}`)
return
}
// 如果子女是第1代(始祖),需要确认
if (child.generation === 1) {
const confirmed = await showConfirm(
'您正在给始祖添加父母,这将导致全族代数重新计算。\n原第1代将变为第2代,以此类推。\n\n是否继续?',
"确认操作"
)
if (!confirmed) {
return
}
}
}
// 如果是第一个成员,自动设置为始祖
const memberData = isFirstMember ? {
...newMember,
@@ -144,13 +183,23 @@ export default function NewMemberPage() {
updatedAt: new Date().toISOString(),
}
await addMember(memberData)
// 跳转到成员列表页,保留treeId参数
const params = new URLSearchParams(window.location.search)
router.push(`/members?${params.toString()}`)
const result = await addMember(memberData)
// 构建跳转参数
const params = new URLSearchParams()
const treeId = searchParams.get('treeId')
if (treeId) params.set('treeId', treeId)
// 如果有提示信息(代数调整),显示给用户,等待用户确认后再跳转
if (result.message) {
await showAlert(result.message, "提示")
}
// 跳转到新成员的详情页
router.push(`/members/${result.member.id}?${params.toString()}`)
} catch (error) {
console.error('添加成员失败:', error)
alert('添加成员失败: ' + (error as Error).message)
await showAlert('添加成员失败: ' + (error as Error).message, "错误")
}
}
@@ -167,8 +216,8 @@ export default function NewMemberPage() {
{/* Header Section */}
<div className="flex flex-col md:flex-row justify-between items-start gap-3 md:gap-6 pb-3 md:pb-4">
<div className="flex items-start gap-3 md:gap-6">
<div className={`h-14 w-14 md:h-24 md:w-24 rounded-lg bg-muted flex items-center justify-center border-2 ${isFirstMember ? 'border-amber-400 bg-amber-50/30' : 'border-primary/20'} overflow-hidden flex-shrink-0`}>
{isFirstMember ? (
<div className={`h-14 w-14 md:h-24 md:w-24 rounded-lg bg-muted flex items-center justify-center border-2 ${showFounderStyle ? 'border-amber-400 bg-amber-50/30' : 'border-primary/20'} overflow-hidden flex-shrink-0`}>
{showFounderStyle ? (
<span className="text-2xl md:text-4xl">👑</span>
) : (
<UserPlus className="h-8 w-8 md:h-16 md:w-16 text-muted-foreground" />
@@ -177,10 +226,10 @@ export default function NewMemberPage() {
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2 md:gap-3 mb-1 md:mb-2 flex-wrap">
<h1 className="text-lg md:text-3xl font-bold font-serif text-foreground flex items-center gap-1 md:gap-2">
{isFirstMember && <span className="text-amber-600 text-lg md:text-2xl">👑</span>}
{isFirstMember ? '添加家族始祖' : `添加${memberType}`}
{showFounderStyle && <span className="text-amber-600 text-lg md:text-2xl">👑</span>}
{isFirstMember ? '添加家族始祖' : isAddingParentToFounder ? '添加新始祖' : `添加${memberType}`}
</h1>
{isFirstMember && (
{showFounderStyle && (
<span className="px-2 md:px-3 py-0.5 md:py-1 bg-gradient-to-r from-amber-100 to-orange-100 text-amber-800 text-xs md:text-sm font-semibold rounded-full border-2 border-amber-300 shadow-sm">
· 1
</span>
+118 -77
View File
@@ -29,6 +29,8 @@ import {
ContextMenuSeparator,
ContextMenuTrigger,
} from "@/components/ui/context-menu"
import { GenerationWarningDialog } from "@/components/tree/generation-warning-dialog"
import { isFirstGenerationMember, shouldShowGenerationWarning } from "@/lib/generation-utils"
// 占位符类型
interface PlaceholderMember {
@@ -81,8 +83,14 @@ const MemberCard = memo(function MemberCard({
canEdit: boolean
onOpenAddDialog: (member: FamilyMember, type: RelationType) => void
}) {
// 代数重新计算警告对话框状态
const [showWarningDialog, setShowWarningDialog] = useState(false)
const [pendingRelationType, setPendingRelationType] = useState<'father' | 'mother' | null>(null)
// 判断是否是始祖(使用 rootMemberId 而不是 isFounder 字段)
const isFounder = member.id === currentTreeId
// 判断是否是配偶
const isFounder = member.isFounder === true
const isSpouse = !member.fatherId && !member.motherId &&
member.spouseIds && member.spouseIds.length > 0 &&
!isFounder
@@ -132,7 +140,8 @@ const MemberCard = memo(function MemberCard({
case 'father':
params.set('childId', member.id)
params.set('gender', 'MALE')
params.set('generation', (member.generation - 1).toString())
// 如果是第1代成员,新父母的世代也设为1(后端会重新计算所有成员世代)
params.set('generation', member.generation === 1 ? '1' : (member.generation - 1).toString())
// 如果已有母亲,设为配偶
if (member.motherId) {
params.set('spouseId', member.motherId)
@@ -141,7 +150,8 @@ const MemberCard = memo(function MemberCard({
case 'mother':
params.set('childId', member.id)
params.set('gender', 'FEMALE')
params.set('generation', (member.generation - 1).toString())
// 如果是第1代成员,新父母的世代也设为1(后端会重新计算所有成员世代)
params.set('generation', member.generation === 1 ? '1' : (member.generation - 1).toString())
// 如果已有父亲,设为配偶
if (member.fatherId) {
params.set('spouseId', member.fatherId)
@@ -183,14 +193,28 @@ const MemberCard = memo(function MemberCard({
}
const handleAddMember = (type: RelationType) => {
// 检查是否需要显示代数重新计算警告
if ((type === 'father' || type === 'mother') && shouldShowGenerationWarning(member, type)) {
setPendingRelationType(type)
setShowWarningDialog(true)
return
}
onOpenAddDialog(member, type)
}
// 确认添加父母后的处理
const handleConfirmAddParent = () => {
if (pendingRelationType) {
onOpenAddDialog(member, pendingRelationType)
setPendingRelationType(null)
}
}
// 卡片内容
const cardContent = (
<div className="relative group">
<Link href={`/members/${member.id}${typeof window !== 'undefined' ? window.location.search : ''}`}>
<ChineseCard variant="default" className={`hover:shadow-lg transition-all cursor-pointer h-full ${cardStyle}`}>
<ChineseCard variant="default" className={`hover:shadow-lg transition-all cursor-pointer h-full rounded-xl ${cardStyle}`}>
<ChineseCardContent className="px-4 py-3 flex items-center gap-5">
<div className="relative flex-shrink-0">
<AvatarDisplay
@@ -345,73 +369,86 @@ const MemberCard = memo(function MemberCard({
// 有编辑权限时,包裹右键菜单
return (
<ContextMenu>
<ContextMenuTrigger asChild>
{cardContent}
</ContextMenuTrigger>
<ContextMenuContent className="w-48">
<ContextMenuLabel className="text-xs text-muted-foreground">
{member.fullName}
</ContextMenuLabel>
<ContextMenuSeparator />
{canAddFather && (
<ContextMenuItem
className="gap-2 cursor-pointer"
onPointerDown={(e) => {
e.preventDefault()
handleAddMember('father')
}}
>
<UserPlus className="h-4 w-4 text-blue-500" />
</ContextMenuItem>
)}
{canAddMother && (
<ContextMenuItem
className="gap-2 cursor-pointer"
onPointerDown={(e) => {
e.preventDefault()
handleAddMember('mother')
}}
>
<UserPlus className="h-4 w-4 text-pink-500" />
</ContextMenuItem>
)}
{(canAddFather || canAddMother) && (canAddSpouse || canAddChild) && (
<>
<ContextMenu>
<ContextMenuTrigger asChild>
{cardContent}
</ContextMenuTrigger>
<ContextMenuContent className="w-48">
<ContextMenuLabel className="text-xs text-muted-foreground">
{member.fullName}
</ContextMenuLabel>
<ContextMenuSeparator />
)}
{canAddSpouse && (
<ContextMenuItem
className="gap-2 cursor-pointer"
onPointerDown={(e) => {
e.preventDefault()
handleAddMember('spouse')
}}
>
<Users className="h-4 w-4 text-red-500" />
</ContextMenuItem>
)}
{canAddChild && (
<ContextMenuItem
className="gap-2 cursor-pointer"
onPointerDown={(e) => {
e.preventDefault()
handleAddMember('child')
}}
>
<Baby className="h-4 w-4 text-green-500" />
</ContextMenuItem>
)}
</ContextMenuContent>
</ContextMenu>
{canAddFather && (
<ContextMenuItem
className="gap-2 cursor-pointer"
onPointerDown={(e) => {
e.preventDefault()
handleAddMember('father')
}}
>
<UserPlus className="h-4 w-4 text-blue-500" />
</ContextMenuItem>
)}
{canAddMother && (
<ContextMenuItem
className="gap-2 cursor-pointer"
onPointerDown={(e) => {
e.preventDefault()
handleAddMember('mother')
}}
>
<UserPlus className="h-4 w-4 text-pink-500" />
</ContextMenuItem>
)}
{(canAddFather || canAddMother) && (canAddSpouse || canAddChild) && (
<ContextMenuSeparator />
)}
{canAddSpouse && (
<ContextMenuItem
className="gap-2 cursor-pointer"
onPointerDown={(e) => {
e.preventDefault()
handleAddMember('spouse')
}}
>
<Users className="h-4 w-4 text-red-500" />
</ContextMenuItem>
)}
{canAddChild && (
<ContextMenuItem
className="gap-2 cursor-pointer"
onPointerDown={(e) => {
e.preventDefault()
handleAddMember('child')
}}
>
<Baby className="h-4 w-4 text-green-500" />
</ContextMenuItem>
)}
</ContextMenuContent>
</ContextMenu>
{/* 代数重新计算警告对话框 */}
{pendingRelationType && (
<GenerationWarningDialog
open={showWarningDialog}
onOpenChange={setShowWarningDialog}
memberName={member.fullName}
relationType={pendingRelationType}
onConfirm={handleConfirmAddParent}
/>
)}
</>
)
})
@@ -495,14 +532,16 @@ export default function MembersPage() {
})
}
} else if (relationType === "father") {
params.set("generation", String(relationDialog.member.generation - 1))
// 如果是第1代成员,新父母的世代也设为1(后端会重新计算所有成员世代)
params.set("generation", String(relationDialog.member.generation === 1 ? 1 : relationDialog.member.generation - 1))
params.set("childId", relationDialog.member.id)
params.set("gender", "MALE")
if (relationDialog.member.motherId) {
params.set("spouseId", relationDialog.member.motherId)
}
} else if (relationType === "mother") {
params.set("generation", String(relationDialog.member.generation - 1))
// 如果是第1代成员,新父母的世代也设为1(后端会重新计算所有成员世代)
params.set("generation", String(relationDialog.member.generation === 1 ? 1 : relationDialog.member.generation - 1))
params.set("childId", relationDialog.member.id)
params.set("gender", "FEMALE")
if (relationDialog.member.fatherId) {
@@ -642,7 +681,8 @@ export default function MembersPage() {
// 配偶定义:没有父母但有配偶的成员(嫁入/娶入的成员)
// 其他所有成员都应该显示,包括孤立成员
const familyMembers = members.filter(m => {
const isFounder = m.isFounder === true
// 使用 treeData.rootId 判断始祖,而不是 isFounder 字段
const isFounder = m.id === treeData.rootId
const hasFamilyParents = m.fatherId || m.motherId
const hasSpouse = m.spouseIds && m.spouseIds.length > 0
@@ -661,8 +701,9 @@ export default function MembersPage() {
// 对家族成员按年龄排序
const sortedFamilyMembers = familyMembers.sort((a, b) => {
const aIsFounder = a.isFounder === true
const bIsFounder = b.isFounder === true
// 使用 treeData.rootId 判断始祖
const aIsFounder = a.id === treeData.rootId
const bIsFounder = b.id === treeData.rootId
if (aIsFounder && !bIsFounder) return -1
if (!aIsFounder && bIsFounder) return 1
@@ -689,7 +730,7 @@ export default function MembersPage() {
actualCount: actualMemberCount
}
})
}, [filteredMembers])
}, [filteredMembers, treeData.rootId])
const clearFilters = () => {
setSelectedSurname("all")
@@ -866,7 +907,7 @@ export default function MembersPage() {
const params = new URLSearchParams(window.location.search)
router.push(`/tree?${params.toString()}`)
}}
currentTreeId={currentTree?.id}
currentTreeId={treeData.rootId || undefined}
canEdit={permissions.canCreate(currentTree?.currentUserRole)}
onOpenAddDialog={handleOpenAddDialog}
/>