This commit is contained in:
freedakgmail
2025-12-09 18:52:00 +08:00
parent debcce71bf
commit 09809c0e49
156 changed files with 1150 additions and 1330 deletions
+106 -52
View File
@@ -10,7 +10,7 @@ import { permissions } from "@/lib/permissions"
import { MemberNameWithStatus } from "@/components/member-name-with-status"
import { MemberVersionHistory } from "@/components/members/member-version-history"
import { useSession } from "next-auth/react"
import { useEffect, useState } from "react"
import { useEffect, useState, useCallback } from "react"
import { useParams, useRouter } from "next/navigation"
import Link from "next/link"
import { useFamily } from "@/context/family-context"
@@ -19,6 +19,7 @@ import { SiteHeader } from "@/components/site-header"
import { MemberForm } from "@/components/members/member-form"
import { Button } from "@/components/ui/button"
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
import { AddRelationDialog, type RelationType } from "@/components/tree/add-relation-dialog"
export default function MemberProfilePage() {
const { id } = useParams()
@@ -32,6 +33,11 @@ export default function MemberProfilePage() {
const [isUploadingAvatar, setIsUploadingAvatar] = useState(false)
const [lastSaveTime, setLastSaveTime] = useState(Date.now())
// 添加成员对话框状态
const [relationDialog, setRelationDialog] = useState<{
type: RelationType
} | null>(null)
// 未登录时重定向到登录页
useEffect(() => {
if (status === 'unauthenticated') {
@@ -124,56 +130,100 @@ export default function MemberProfilePage() {
}
}
const handleQuickAddChild = () => {
// 跳转到新增成员页面,并预填父母信息
const params = new URLSearchParams(window.location.search)
if (member.gender === 'MALE') {
params.set('fatherId', member.id)
// 如果父亲有配偶,设置配偶为母亲
if (member.spouseIds && member.spouseIds.length > 0) {
params.set('motherId', member.spouseIds[0])
}
} else {
params.set('motherId', member.id)
// 如果母亲有配偶,设置配偶为父亲
if (member.spouseIds && member.spouseIds.length > 0) {
params.set('fatherId', member.spouseIds[0])
// 姓名分解函数
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) }
}
}
params.set('generation', (member.generation + 1).toString())
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])
const handleQuickAddChild = () => {
setRelationDialog({ type: 'child' })
}
const handleQuickAddSpouse = () => {
// 跳转到新增成员页面,并预填配偶信息
const params = new URLSearchParams(window.location.search)
params.set('spouseId', member.id)
params.set('generation', member.generation.toString())
// 配偶性别与当前成员相反
params.set('gender', member.gender === 'MALE' ? 'FEMALE' : 'MALE')
// 如果当前成员有子女,将他们也作为新配偶的子女
if (member.childrenIds && member.childrenIds.length > 0) {
// 传递所有子女ID
member.childrenIds.forEach(id => {
params.append('childrenIds', id)
})
}
router.push(`/members/new?${params.toString()}`)
setRelationDialog({ type: 'spouse' })
}
const handleQuickAddFather = () => {
// 跳转到新增成员页面,并预填父亲信息
const params = new URLSearchParams(window.location.search)
params.set('childId', member.id)
params.set('generation', (member.generation - 1).toString())
params.set('gender', 'MALE')
// 如果已有母亲,将母亲设置为配偶
if (member.motherId) {
params.set('spouseId', member.motherId)
}
router.push(`/members/new?${params.toString()}`)
setRelationDialog({ type: 'father' })
}
const handleAvatarUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -216,16 +266,7 @@ export default function MemberProfilePage() {
}
const handleQuickAddMother = () => {
// 跳转到新增成员页面,并预填母亲信息
const params = new URLSearchParams(window.location.search)
params.set('childId', member.id)
params.set('generation', (member.generation - 1).toString())
params.set('gender', 'FEMALE')
// 如果已有父亲,将父亲设置为配偶
if (member.fatherId) {
params.set('spouseId', member.fatherId)
}
router.push(`/members/new?${params.toString()}`)
setRelationDialog({ type: 'mother' })
}
if (isEditing) {
@@ -865,6 +906,19 @@ export default function MemberProfilePage() {
</div>
</Tabs>
</main>
{/* 添加成员对话框 */}
{relationDialog && member && (
<AddRelationDialog
open={!!relationDialog}
onOpenChange={(open) => {
if (!open) setRelationDialog(null)
}}
member={member}
relationType={relationDialog.type}
onCreateNew={handleCreateNewRelation}
/>
)}
</div>
)
}
+15 -1
View File
@@ -38,11 +38,25 @@ export default function NewMemberPage() {
const childId = searchParams.get('childId')
const generation = searchParams.get('generation')
const gender = searchParams.get('gender')
const surname = searchParams.get('surname')
const givenName = searchParams.get('givenName')
if (!fatherId && !motherId && !spouseId && !childId && !generation && !gender) return undefined
if (!fatherId && !motherId && !spouseId && !childId && !generation && !gender && !surname && !givenName) return undefined
const data: Partial<FamilyMember> = {}
// 处理姓名信息
if (surname) {
data.surname = surname
}
if (givenName) {
data.givenName = givenName
}
// 如果有姓和名,自动生成全名
if (surname || givenName) {
data.fullName = (surname || '') + (givenName || '')
}
// 处理父母信息
if (fatherId) {
data.fatherId = fatherId
+285 -3
View File
@@ -7,7 +7,7 @@ import { useRouter } from "next/navigation"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { ChineseCard, ChineseCardContent } from "@/components/ui/chinese-card"
import { Search, Plus, Filter, X, Users, User } from "lucide-react"
import { Search, Plus, Filter, X, Users, User, UserPlus, Baby } from "lucide-react"
import Link from "next/link"
import { AvatarDisplay } from "@/components/ui/avatar-display"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
@@ -20,6 +20,15 @@ import { permissions } from "@/lib/permissions"
import { MemberNameWithStatus } from "@/components/member-name-with-status"
import { useSession } from "next-auth/react"
import { EmptyStateBadge } from "@/components/empty-state-badge"
import { AddRelationDialog, type RelationType } from "@/components/tree/add-relation-dialog"
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuTrigger,
} from "@/components/ui/context-menu"
// 占位符类型
interface PlaceholderMember {
@@ -69,10 +78,16 @@ const MemberCard = memo(function MemberCard({
member,
allMembers,
onLocateInTree,
currentTreeId,
canEdit,
onOpenAddDialog,
}: {
member: FamilyMember
allMembers: FamilyMember[]
onLocateInTree: (id: string) => void
currentTreeId?: string
canEdit: boolean
onOpenAddDialog: (member: FamilyMember, type: RelationType) => void
}) {
// 判断是否是配偶
const isFounder = member.isFounder === true
@@ -108,7 +123,79 @@ const MemberCard = memo(function MemberCard({
? '!border-2 !border-gray-400 dark:!border-gray-600 !bg-gray-50/50 dark:!bg-gray-900/30' // 已故:灰色深边框
: '!border-2 !border-amber-300/70 !bg-card dark:!border-amber-600/50' // 在世:浅金色边框
return (
// 判断可以添加的关系
const canAddFather = !member.fatherId
const canAddMother = !member.motherId
const canAddSpouse = true // 配偶总是可以添加
const canAddChild = true // 子女总是可以添加
// 构建新增成员的 URL
const buildAddUrl = (type: 'father' | 'mother' | 'spouse' | 'child') => {
const params = new URLSearchParams()
if (currentTreeId) {
params.set('treeId', currentTreeId)
}
switch (type) {
case 'father':
params.set('childId', member.id)
params.set('gender', 'MALE')
params.set('generation', (member.generation - 1).toString())
// 如果已有母亲,设为配偶
if (member.motherId) {
params.set('spouseId', member.motherId)
}
break
case 'mother':
params.set('childId', member.id)
params.set('gender', 'FEMALE')
params.set('generation', (member.generation - 1).toString())
// 如果已有父亲,设为配偶
if (member.fatherId) {
params.set('spouseId', member.fatherId)
}
break
case 'spouse':
params.set('spouseId', member.id)
params.set('generation', member.generation.toString())
// 配偶性别与当前成员相反
params.set('gender', member.gender === 'MALE' ? 'FEMALE' : 'MALE')
// 如果当前成员有子女,将他们也作为新配偶的子女
if (member.childrenIds && member.childrenIds.length > 0) {
member.childrenIds.forEach(id => {
params.append('childrenIds', id)
})
}
break
case 'child':
// 根据当前成员性别设置父/母
if (member.gender === 'MALE') {
params.set('fatherId', member.id)
// 如果父亲有配偶,设置配偶为母亲
if (member.spouseIds && member.spouseIds.length > 0) {
params.set('motherId', member.spouseIds[0])
}
} else {
params.set('motherId', member.id)
// 如果母亲有配偶,设置配偶为父亲
if (member.spouseIds && member.spouseIds.length > 0) {
params.set('fatherId', member.spouseIds[0])
}
}
params.set('generation', (member.generation + 1).toString())
break
}
return `/members/new?${params.toString()}`
}
const handleAddMember = (type: RelationType) => {
onOpenAddDialog(member, type)
}
// 卡片内容
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}`}>
@@ -258,15 +345,187 @@ const MemberCard = memo(function MemberCard({
</Button>
</div>
)
// 如果没有编辑权限,直接返回卡片内容
if (!canEdit) {
return cardContent
}
// 有编辑权限时,包裹右键菜单
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) && (
<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>
)
})
export default function MembersPage() {
const { treeData, currentTree, searchMembers, setHighlightedMemberId } = useFamily()
const { treeData, currentTree, searchMembers, setHighlightedMemberId, refreshData } = useFamily()
const { data: session, status } = useSession()
const router = useRouter()
const [query, setQuery] = useState("")
const [showFilters, setShowFilters] = useState(false)
// 添加成员对话框状态
const [relationDialog, setRelationDialog] = useState<{
member: FamilyMember
type: RelationType
} | null>(null)
// 打开添加成员对话框
const handleOpenAddDialog = useCallback((member: FamilyMember, type: RelationType) => {
setRelationDialog({ member, type })
}, [])
// 姓名分解函数
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 (!relationDialog) 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(relationDialog.member.generation + 1))
if (relationDialog.member.gender === "MALE") {
params.set("fatherId", relationDialog.member.id)
if (relationDialog.member.spouseIds?.length) {
params.set("motherId", relationDialog.member.spouseIds[0])
}
} else if (relationDialog.member.gender === "FEMALE") {
params.set("motherId", relationDialog.member.id)
if (relationDialog.member.spouseIds?.length) {
params.set("fatherId", relationDialog.member.spouseIds[0])
}
}
} else if (relationType === "spouse") {
params.set("generation", String(relationDialog.member.generation))
params.set("spouseId", relationDialog.member.id)
params.set("gender", relationDialog.member.gender === "MALE" ? "FEMALE" : "MALE")
if (relationDialog.member.childrenIds && relationDialog.member.childrenIds.length > 0) {
relationDialog.member.childrenIds.forEach(id => {
params.append('childrenIds', id)
})
}
} else if (relationType === "father") {
params.set("generation", String(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))
params.set("childId", relationDialog.member.id)
params.set("gender", "FEMALE")
if (relationDialog.member.fatherId) {
params.set("spouseId", relationDialog.member.fatherId)
}
}
if (currentTree?.id) {
params.set("treeId", currentTree.id)
}
setRelationDialog(null)
router.push(`/members/new?${params.toString()}`)
}, [relationDialog, currentTree?.id, router])
// 未登录时重定向到登录页
useEffect(() => {
if (status === 'unauthenticated') {
@@ -274,6 +533,13 @@ export default function MembersPage() {
}
}, [status, router])
// 页面加载时刷新数据
useEffect(() => {
if (currentTree?.id) {
refreshData()
}
}, [currentTree?.id, refreshData])
// 从 URL 参数读取搜索关键词
useEffect(() => {
const params = new URLSearchParams(window.location.search)
@@ -608,6 +874,9 @@ export default function MembersPage() {
const params = new URLSearchParams(window.location.search)
router.push(`/tree?${params.toString()}`)
}}
currentTreeId={currentTree?.id}
canEdit={permissions.canCreate(currentTree?.currentUserRole)}
onOpenAddDialog={handleOpenAddDialog}
/>
)
})}
@@ -617,6 +886,19 @@ export default function MembersPage() {
</div>
)}
</main>
{/* 添加成员对话框 */}
{relationDialog && (
<AddRelationDialog
open={!!relationDialog}
onOpenChange={(open) => {
if (!open) setRelationDialog(null)
}}
member={relationDialog.member}
relationType={relationDialog.type}
onCreateNew={handleCreateNewRelation}
/>
)}
</div>
)
}
+9 -2
View File
@@ -16,7 +16,7 @@ import {
} from "@/components/ui/dropdown-menu"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Badge } from "@/components/ui/badge"
import { ZoomIn, ZoomOut, Move, Download, LayoutGrid, ChevronDown, Users, Network, User, Loader2 } from "lucide-react"
import { ZoomIn, ZoomOut, Move, Download, LayoutGrid, ChevronDown, Users, Network, Loader2 } from "lucide-react"
import { useState, useRef, useEffect, useCallback } from "react"
import { useRouter, useSearchParams } from "next/navigation"
import { MemberNameWithStatus } from "@/components/member-name-with-status"
@@ -44,7 +44,7 @@ import type { D3OrgChartRef } from "@/components/tree/d3-org-chart-flow"
type ViewMode = "traditional" | "d3-org-chart"
export default function TreePage() {
const { treeData } = useFamily()
const { treeData, currentTree, refreshData } = useFamily()
const { calculateRelationship } = useRelationship()
const { data: session, status } = useSession()
const router = useRouter()
@@ -73,6 +73,13 @@ export default function TreePage() {
}
}, [status, router])
// 页面加载时刷新数据
useEffect(() => {
if (currentTree?.id) {
refreshData()
}
}, [currentTree?.id, refreshData])
// 从 URL 参数读取视图模式
useEffect(() => {
const view = searchParams.get('view')