0.8.0.0
This commit is contained in:
+3
-1
@@ -18,6 +18,7 @@ import {
|
||||
} from "@/components/ui/table"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { useDialog } from "@/components/ui/alert-dialog-custom"
|
||||
|
||||
interface Collaborator {
|
||||
id: string
|
||||
@@ -82,6 +83,7 @@ interface LoginLog {
|
||||
export default function AdminPage() {
|
||||
const { data: session, status } = useSession()
|
||||
const router = useRouter()
|
||||
const { showAlert } = useDialog()
|
||||
const [users, setUsers] = useState<UserInfo[]>([])
|
||||
const [stats, setStats] = useState<Stats | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
@@ -176,7 +178,7 @@ export default function AdminPage() {
|
||||
)
|
||||
)
|
||||
} catch (err) {
|
||||
alert(err instanceof Error ? err.message : "操作失败")
|
||||
await showAlert(err instanceof Error ? err.message : "操作失败", "错误")
|
||||
} finally {
|
||||
setUpdating(null)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { prisma } from "@/lib/prisma"
|
||||
import { checkPermission } from "@/lib/permissions"
|
||||
import { Role } from "@prisma/client"
|
||||
import { recordMemberHistory } from "@/lib/member-history"
|
||||
import { recalculateGenerations, isRootMember, hasChildren, findNewRootMember } from "@/lib/generation-utils"
|
||||
|
||||
// 获取单个成员
|
||||
export async function GET(
|
||||
@@ -323,10 +324,183 @@ export async function DELETE(
|
||||
return NextResponse.json({ error: "成员不存在" }, { status: 404 })
|
||||
}
|
||||
|
||||
// 获取家族树信息
|
||||
const tree = await prisma.familyTree.findUnique({
|
||||
where: { id: treeId },
|
||||
select: { rootMemberId: true }
|
||||
})
|
||||
|
||||
// 检查是否删除的是根成员(始祖)
|
||||
const isDeletingRoot = await isRootMember(memberId)
|
||||
const memberHasChildren = await hasChildren(memberId)
|
||||
let needRecalculate = false
|
||||
let newRootId: string | null = null
|
||||
|
||||
// 如果删除的是根成员且有子女,需要调整全族代数
|
||||
if (isDeletingRoot && memberHasChildren) {
|
||||
needRecalculate = true
|
||||
}
|
||||
|
||||
// 清理关系引用
|
||||
// 1. 清理配偶关系
|
||||
if (existingMember.spouseIds && existingMember.spouseIds.length > 0) {
|
||||
for (const spouseId of existingMember.spouseIds) {
|
||||
const spouse = await prisma.familyMember.findUnique({
|
||||
where: { id: spouseId }
|
||||
})
|
||||
if (spouse && spouse.spouseIds.includes(memberId)) {
|
||||
await prisma.familyMember.update({
|
||||
where: { id: spouseId },
|
||||
data: {
|
||||
spouseIds: spouse.spouseIds.filter(id => id !== memberId)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 清理父母的子女引用
|
||||
if (existingMember.fatherId) {
|
||||
const father = await prisma.familyMember.findUnique({
|
||||
where: { id: existingMember.fatherId }
|
||||
})
|
||||
if (father && father.childrenIds.includes(memberId)) {
|
||||
await prisma.familyMember.update({
|
||||
where: { id: existingMember.fatherId },
|
||||
data: {
|
||||
childrenIds: father.childrenIds.filter(id => id !== memberId)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (existingMember.motherId) {
|
||||
const mother = await prisma.familyMember.findUnique({
|
||||
where: { id: existingMember.motherId }
|
||||
})
|
||||
if (mother && mother.childrenIds.includes(memberId)) {
|
||||
await prisma.familyMember.update({
|
||||
where: { id: existingMember.motherId },
|
||||
data: {
|
||||
childrenIds: mother.childrenIds.filter(id => id !== memberId)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 清理子女的父母引用
|
||||
if (existingMember.childrenIds && existingMember.childrenIds.length > 0) {
|
||||
for (const childId of existingMember.childrenIds) {
|
||||
const child = await prisma.familyMember.findUnique({
|
||||
where: { id: childId }
|
||||
})
|
||||
if (child) {
|
||||
const updates: any = {}
|
||||
if (child.fatherId === memberId) {
|
||||
updates.fatherId = null
|
||||
}
|
||||
if (child.motherId === memberId) {
|
||||
updates.motherId = null
|
||||
}
|
||||
if (Object.keys(updates).length > 0) {
|
||||
await prisma.familyMember.update({
|
||||
where: { id: childId },
|
||||
data: updates
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除成员
|
||||
await prisma.familyMember.delete({
|
||||
where: { id: memberId }
|
||||
})
|
||||
|
||||
// 如果需要调整全族代数
|
||||
if (needRecalculate) {
|
||||
try {
|
||||
// 将所有成员的代数 -1
|
||||
await recalculateGenerations(treeId, -1)
|
||||
|
||||
// 查找新的根成员
|
||||
newRootId = await findNewRootMember(treeId)
|
||||
|
||||
if (newRootId) {
|
||||
// 更新家族树的 rootMemberId
|
||||
await prisma.familyTree.update({
|
||||
where: { id: treeId },
|
||||
data: { rootMemberId: newRootId }
|
||||
})
|
||||
|
||||
// 将新根成员设为始祖
|
||||
await prisma.familyMember.update({
|
||||
where: { id: newRootId },
|
||||
data: { isFounder: true }
|
||||
})
|
||||
} else {
|
||||
// 如果没有找到新的根成员,清空 rootMemberId
|
||||
await prisma.familyTree.update({
|
||||
where: { id: treeId },
|
||||
data: { rootMemberId: null }
|
||||
})
|
||||
}
|
||||
|
||||
// 记录全族代数调整的日志
|
||||
await prisma.activityLog.create({
|
||||
data: {
|
||||
treeId,
|
||||
userId: session.user.id,
|
||||
action: 'UPDATE',
|
||||
entityType: 'TREE',
|
||||
entityId: treeId,
|
||||
entityName: `全族代数调整(-1代)`,
|
||||
changes: {
|
||||
reason: '删除始祖',
|
||||
offset: -1,
|
||||
newRootMemberId: newRootId,
|
||||
deletedMemberId: memberId,
|
||||
deletedMemberName: existingMember.fullName
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('调整全族代数失败:', error)
|
||||
// 注意:此时成员已被删除,无法回滚
|
||||
// 记录错误日志
|
||||
await prisma.activityLog.create({
|
||||
data: {
|
||||
treeId,
|
||||
userId: session.user.id,
|
||||
action: 'ERROR',
|
||||
entityType: 'TREE',
|
||||
entityId: treeId,
|
||||
entityName: '代数调整失败',
|
||||
changes: {
|
||||
error: '删除始祖后调整代数失败',
|
||||
deletedMemberId: memberId
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
} else if (tree?.rootMemberId === memberId) {
|
||||
// 如果删除的是根成员但没有子女,需要更新 rootMemberId
|
||||
// 情况1: 有配偶,配偶成为新根
|
||||
if (existingMember.spouseIds && existingMember.spouseIds.length > 0) {
|
||||
const newRoot = existingMember.spouseIds[0]
|
||||
await prisma.familyTree.update({
|
||||
where: { id: treeId },
|
||||
data: { rootMemberId: newRoot }
|
||||
})
|
||||
} else {
|
||||
// 情况2: 无配偶无子女,清空 rootMemberId
|
||||
await prisma.familyTree.update({
|
||||
where: { id: treeId },
|
||||
data: { rootMemberId: null }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 记录操作日志
|
||||
await prisma.activityLog.create({
|
||||
data: {
|
||||
@@ -339,7 +513,11 @@ export async function DELETE(
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
generationRecalculated: needRecalculate,
|
||||
newRootMemberId: newRootId
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("删除成员失败:", error)
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -6,6 +6,7 @@ import { checkPermission } from "@/lib/permissions"
|
||||
import { Role, Gender } from "@prisma/client"
|
||||
import { z } from "zod"
|
||||
import { recordMemberHistory } from "@/lib/member-history"
|
||||
import { recalculateGenerations, getMinGeneration, findNewRootMember } from "@/lib/generation-utils"
|
||||
|
||||
const createMemberSchema = z.object({
|
||||
surname: z.string().min(1),
|
||||
@@ -122,6 +123,35 @@ export async function POST(
|
||||
const body = await req.json()
|
||||
const data = createMemberSchema.parse(body)
|
||||
|
||||
// 检查是否需要调整全族代数
|
||||
const minGeneration = await getMinGeneration(treeId)
|
||||
let needRecalculate = false
|
||||
let generationOffset = 0
|
||||
|
||||
// 检查是否给第1代成员添加父母
|
||||
let isAddingParentToFounder = false
|
||||
if (data.childrenIds && data.childrenIds.length > 0) {
|
||||
// 检查子女中是否有第1代成员
|
||||
const children = await prisma.familyMember.findMany({
|
||||
where: {
|
||||
id: { in: data.childrenIds },
|
||||
generation: 1
|
||||
},
|
||||
select: { id: true }
|
||||
})
|
||||
isAddingParentToFounder = children.length > 0
|
||||
}
|
||||
|
||||
if (minGeneration !== null && data.generation < minGeneration) {
|
||||
// 新成员的代数小于当前最小代数,需要调整全族代数
|
||||
needRecalculate = true
|
||||
generationOffset = minGeneration - data.generation
|
||||
} else if (isAddingParentToFounder && minGeneration === 1) {
|
||||
// 给第1代成员添加父母,需要将全族代数+1
|
||||
needRecalculate = true
|
||||
generationOffset = 1
|
||||
}
|
||||
|
||||
const member = await prisma.familyMember.create({
|
||||
data: {
|
||||
...data,
|
||||
@@ -204,6 +234,82 @@ export async function POST(
|
||||
}
|
||||
}
|
||||
|
||||
// 如果需要调整全族代数
|
||||
if (needRecalculate) {
|
||||
try {
|
||||
if (isAddingParentToFounder) {
|
||||
// 给第1代成员添加父母的情况
|
||||
// 1. 先将所有现有成员的代数+1(包括原来的第1代)
|
||||
await recalculateGenerations(treeId, generationOffset, member.id)
|
||||
|
||||
// 2. 将原始祖的 isFounder 设为 false
|
||||
const oldFounders = await prisma.familyMember.findMany({
|
||||
where: {
|
||||
treeId,
|
||||
isFounder: true,
|
||||
id: { not: member.id }
|
||||
}
|
||||
})
|
||||
for (const oldFounder of oldFounders) {
|
||||
await prisma.familyMember.update({
|
||||
where: { id: oldFounder.id },
|
||||
data: { isFounder: false }
|
||||
})
|
||||
}
|
||||
|
||||
// 3. 新父母成员保持代数为1,并设为始祖
|
||||
await prisma.familyMember.update({
|
||||
where: { id: member.id },
|
||||
data: { generation: 1, isFounder: true }
|
||||
})
|
||||
|
||||
// 4. 更新家族树的 rootMemberId 为新父母
|
||||
await prisma.familyTree.update({
|
||||
where: { id: treeId },
|
||||
data: { rootMemberId: member.id }
|
||||
})
|
||||
} else {
|
||||
// 其他情况:新成员代数小于最小代数
|
||||
// 调整其他成员的代数(排除新创建的成员)
|
||||
await recalculateGenerations(treeId, generationOffset, member.id)
|
||||
|
||||
// 将新成员的代数设为1(成为新的始祖)
|
||||
await prisma.familyMember.update({
|
||||
where: { id: member.id },
|
||||
data: { generation: 1 }
|
||||
})
|
||||
|
||||
// 更新家族树的 rootMemberId
|
||||
await prisma.familyTree.update({
|
||||
where: { id: treeId },
|
||||
data: { rootMemberId: member.id }
|
||||
})
|
||||
}
|
||||
|
||||
// 记录全族代数调整的日志
|
||||
await prisma.activityLog.create({
|
||||
data: {
|
||||
treeId,
|
||||
userId: session.user.id,
|
||||
action: 'UPDATE',
|
||||
entityType: 'TREE',
|
||||
entityId: treeId,
|
||||
entityName: `全族代数调整(+${generationOffset}代)`,
|
||||
changes: {
|
||||
reason: isAddingParentToFounder ? '给始祖添加父母' : '添加更早代数成员',
|
||||
offset: generationOffset,
|
||||
newRootMemberId: member.id
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('调整全族代数失败:', error)
|
||||
// 如果调整失败,删除新创建的成员
|
||||
await prisma.familyMember.delete({ where: { id: member.id } })
|
||||
throw new Error('调整全族代数失败,操作已回滚')
|
||||
}
|
||||
}
|
||||
|
||||
// 记录成员历史版本
|
||||
await recordMemberHistory({
|
||||
memberId: member.id,
|
||||
@@ -225,7 +331,11 @@ export async function POST(
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json({ member }, { status: 201 })
|
||||
return NextResponse.json({
|
||||
member,
|
||||
generationRecalculated: needRecalculate,
|
||||
generationOffset: needRecalculate ? generationOffset : 0
|
||||
}, { status: 201 })
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
|
||||
+8
-7
@@ -1,5 +1,5 @@
|
||||
import type React from "react"
|
||||
import type { Metadata } from "next"
|
||||
import type { Metadata, Viewport } from "next"
|
||||
import { Geist, Geist_Mono } from "next/font/google"
|
||||
// import { Analytics } from "@vercel/analytics/next"
|
||||
import "./globals.css"
|
||||
@@ -16,12 +16,6 @@ export const metadata: Metadata = {
|
||||
description: "现代化的中国家族树管理系统,支持多用户协作、权限管理、数据导入导出等功能",
|
||||
generator: "v0.app",
|
||||
manifest: "/manifest.json",
|
||||
viewport: {
|
||||
width: "device-width",
|
||||
initialScale: 1,
|
||||
maximumScale: 1,
|
||||
userScalable: false,
|
||||
},
|
||||
appleWebApp: {
|
||||
capable: true,
|
||||
statusBarStyle: "default",
|
||||
@@ -38,6 +32,13 @@ export const metadata: Metadata = {
|
||||
},
|
||||
}
|
||||
|
||||
export const viewport: Viewport = {
|
||||
width: "device-width",
|
||||
initialScale: 1,
|
||||
maximumScale: 1,
|
||||
userScalable: false,
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
|
||||
+114
-89
@@ -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
@@ -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
@@ -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}
|
||||
/>
|
||||
|
||||
+110
-4
@@ -7,11 +7,11 @@ import dynamic from "next/dynamic"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { ChineseCard, ChineseCardContent, ChineseCardHeader, ChineseCardTitle, ChineseCardDescription, ChineseDivider } from "@/components/ui/chinese-card"
|
||||
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"
|
||||
import { Dialog, DialogContent, DialogTitle, DialogHeader, DialogDescription, DialogFooter } from "@/components/ui/dialog"
|
||||
import { SiteHeader } from "@/components/site-header"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { UserPlus, Network, Map, Calendar, Users, ArrowRight, Clock, Search, BookOpen, BarChart3, Calculator, Loader2, Images, ChevronDown, ChevronUp, EyeOff, Play, Video } from "lucide-react"
|
||||
import { UserPlus, Network, Map, Calendar, Users, ArrowRight, Clock, Search, BookOpen, BarChart3, Calculator, Loader2, Images, ChevronDown, ChevronUp, EyeOff, Play, Video, Edit } from "lucide-react"
|
||||
import { useFamily } from "@/context/family-context"
|
||||
import { useMemo, useState, useEffect, useRef, useCallback } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
@@ -24,7 +24,11 @@ import { isInCurrentMonth, solar2lunar, lunar2solar } from "@/lib/lunar-calendar
|
||||
import { MemberNameWithStatus } from "@/components/member-name-with-status"
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import type { FamilyPhoto } from "@/types/family"
|
||||
import { useDialog } from "@/components/ui/alert-dialog-custom"
|
||||
|
||||
// 动态导入统计图表组件(减少初始加载体积)
|
||||
const StatisticsCharts = dynamic(
|
||||
@@ -63,10 +67,14 @@ const isVideoFile = (url: string) => {
|
||||
export default function DashboardPage() {
|
||||
const { treeData, isLoading, currentTree, updateMember } = useFamily()
|
||||
const { data: session } = useSession()
|
||||
const { showAlert } = useDialog()
|
||||
const [recentActivities, setRecentActivities] = useState<ActivityLog[]>([])
|
||||
const [selectedPhoto, setSelectedPhoto] = useState<string | null>(null)
|
||||
const [statsExpanded, setStatsExpanded] = useState(true)
|
||||
const [adminToggleLoading, setAdminToggleLoading] = useState<string | null>(null)
|
||||
const [editDialogOpen, setEditDialogOpen] = useState(false)
|
||||
const [editForm, setEditForm] = useState({ name: '', description: '' })
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const router = useRouter()
|
||||
|
||||
// 首次登录跳转到使用帮助
|
||||
@@ -223,6 +231,38 @@ export default function DashboardPage() {
|
||||
}
|
||||
}, [isOwner, treeData.members, updateMember])
|
||||
|
||||
// 保存编辑
|
||||
const handleSaveEdit = async () => {
|
||||
if (!currentTree?.id || !editForm.name.trim()) return
|
||||
|
||||
setIsSaving(true)
|
||||
try {
|
||||
const response = await fetch(`/api/trees/${currentTree.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: editForm.name.trim(),
|
||||
description: editForm.description.trim() || null,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('更新失败')
|
||||
}
|
||||
|
||||
setEditDialogOpen(false)
|
||||
// 刷新页面以显示更新后的数据
|
||||
window.location.reload()
|
||||
} catch (error) {
|
||||
console.error('更新家族信息失败:', error)
|
||||
await showAlert('更新失败,请重试', "错误")
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取本月纪念日(生日和忌日)- 支持农历
|
||||
const monthlyAnniversaries = useMemo(() => {
|
||||
const members = Object.values(treeData.members)
|
||||
@@ -452,12 +492,28 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
{currentTree?.description || '记录家族历史,传承家族文化'}
|
||||
{stats.maxGeneration > 0 && ` · 第 ${stats.maxGeneration} 代传人`}
|
||||
{stats.maxGeneration > 0 && ` · 共 ${stats.maxGeneration} 代传人`}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{session && currentTree?.ownerId === session.user?.id && (
|
||||
<CollaboratorDialog />
|
||||
<>
|
||||
<CollaboratorDialog />
|
||||
<Button
|
||||
variant="outline"
|
||||
className="gap-2"
|
||||
onClick={() => {
|
||||
setEditForm({
|
||||
name: currentTree?.name || '',
|
||||
description: currentTree?.description || ''
|
||||
})
|
||||
setEditDialogOpen(true)
|
||||
}}
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
编辑
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{permissions.canCreate(currentTree?.currentUserRole) && (
|
||||
<Link href={currentTree?.id ? `/members/new?treeId=${currentTree.id}` : '/members/new'}>
|
||||
@@ -1181,6 +1237,56 @@ export default function DashboardPage() {
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 编辑家族信息对话框 */}
|
||||
<Dialog open={editDialogOpen} onOpenChange={setEditDialogOpen}>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>编辑家族信息</DialogTitle>
|
||||
<DialogDescription>
|
||||
修改家族名称和说明
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="tree-name">家族名称</Label>
|
||||
<Input
|
||||
id="tree-name"
|
||||
value={editForm.name}
|
||||
onChange={(e) => setEditForm({ ...editForm, name: e.target.value })}
|
||||
placeholder="请输入家族名称"
|
||||
maxLength={50}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="tree-description">家族说明</Label>
|
||||
<Textarea
|
||||
id="tree-description"
|
||||
value={editForm.description}
|
||||
onChange={(e) => setEditForm({ ...editForm, description: e.target.value })}
|
||||
placeholder="请输入家族说明"
|
||||
rows={4}
|
||||
maxLength={200}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setEditDialogOpen(false)}
|
||||
disabled={isSaving}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSaveEdit}
|
||||
disabled={isSaving || !editForm.name.trim()}
|
||||
>
|
||||
{isSaving ? '保存中...' : '保存'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
|
||||
+5
-3
@@ -24,6 +24,7 @@ import { RelationshipPathDisplay } from "@/components/relationship-path-display"
|
||||
import { useSession } from "next-auth/react"
|
||||
import { EmptyStateBadge } from "@/components/empty-state-badge"
|
||||
import { exportFamilyHTML } from "@/lib/export-pdf"
|
||||
import { useDialog } from "@/components/ui/alert-dialog-custom"
|
||||
|
||||
// 动态导入 D3 图表组件(减少初始加载体积)
|
||||
const D3OrgChartFlow = dynamic(
|
||||
@@ -48,6 +49,7 @@ export default function TreePage() {
|
||||
const { treeData, currentTree, refreshData } = useFamily()
|
||||
const { calculateRelationship } = useRelationship()
|
||||
const { data: session, status } = useSession()
|
||||
const { showAlert } = useDialog()
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const [viewMode, setViewMode] = useState<ViewMode>("traditional")
|
||||
@@ -641,7 +643,7 @@ export default function TreePage() {
|
||||
// 导出 PDF(按世代导出成员信息)
|
||||
const handleExportPDF = useCallback(async () => {
|
||||
if (!treeData.members || Object.keys(treeData.members).length === 0) {
|
||||
alert('没有成员数据可导出')
|
||||
await showAlert('没有成员数据可导出', "提示")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -655,9 +657,9 @@ export default function TreePage() {
|
||||
await exportFamilyHTML(members, getMemberName, treeName, baseUrl)
|
||||
} catch (error) {
|
||||
console.error('导出PDF失败:', error)
|
||||
alert('导出失败,请重试')
|
||||
await showAlert('导出失败,请重试', "错误")
|
||||
}
|
||||
}, [treeData.members, currentTree?.name])
|
||||
}, [treeData.members, currentTree?.name, showAlert])
|
||||
|
||||
// 未登录时不渲染内容(已在 useEffect 中重定向)
|
||||
if (status === 'loading' || !session) {
|
||||
|
||||
Reference in New Issue
Block a user