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
+18 -62
View File
@@ -96,46 +96,7 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
const [errors, setErrors] = useState<string[]>([])
// 监听 initialData 变化,更新 formData
useEffect(() => {
if (!initialData) return
setFormData((prev) => {
const normalizedPhotos: FamilyPhoto[] = initialData.photos && initialData.photos.length > 0
? initialData.photos
: (initialData.photoIds || []).map((url) => ({
url,
uploadedAt: initialData.updatedAt || new Date().toISOString(),
}))
// 优先使用 initialData 中的 motherId,如果没有才使用之前的值
const newMotherId = initialData.motherId !== undefined ? initialData.motherId : prev.motherId
console.log('Calculating new motherId:', {
initial: initialData.motherId,
prev: prev.motherId,
result: newMotherId,
hasInitialMother: initialData.motherId !== undefined
})
return {
...prev,
...initialData,
// 确保特定字段被正确更新
gender: initialData.gender ? (initialData.gender.toUpperCase() as "MALE" | "FEMALE") : prev.gender,
spouseIds: initialData.spouseIds || prev.spouseIds || [],
childrenIds: initialData.childrenIds || prev.childrenIds || [],
tags: initialData.tags || prev.tags || [],
fatherId: initialData.fatherId || prev.fatherId,
motherId: newMotherId,
photos: initialData.photos ? normalizedPhotos : prev.photos,
photoIds: initialData.photoIds || (initialData.photos ? normalizedPhotos.map((p) => p.url) : prev.photoIds),
}
})
}, [initialData])
const handleChange = (field: keyof FamilyMember, value: any) => {
console.log('handleChange:', field, value)
setFormData((prev) => {
const newData = { ...prev, [field]: value }
@@ -149,9 +110,8 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
const father = existingMembers.find(m => m.id === value)
if (father && father.spouseIds && father.spouseIds.length > 0) {
const motherId = father.spouseIds[0]
if (!newData.motherId) { // Only auto-set if motherId is not already present
if (!newData.motherId) {
newData.motherId = motherId
console.log('Auto-set mother from father:', { fatherId: value, motherId })
}
}
}
@@ -161,9 +121,8 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
const mother = existingMembers.find(m => m.id === value)
if (mother && mother.spouseIds && mother.spouseIds.length > 0) {
const fatherId = mother.spouseIds[0]
if (!newData.fatherId) { // Only auto-set if fatherId is not already present
if (!newData.fatherId) {
newData.fatherId = fatherId
console.log('Auto-set father from mother:', { motherId: value, fatherId })
}
}
}
@@ -289,13 +248,6 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
}
const potentialRelatives = existingMembers.filter((m) => m.id !== formData.id)
// 调试日志
console.log('MemberForm render:', {
motherId: formData.motherId,
existingMembersCount: existingMembers.length,
motherInList: existingMembers.find(m => m.id === formData.motherId)
})
return (
<form onSubmit={handleSubmit} className="w-full">
@@ -430,18 +382,20 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
<div className="space-y-2">
<Label htmlFor="fatherId"></Label>
{isFounder ? (
<Input
placeholder="填写父亲姓名"
value={formData.spouseFatherName || ""}
onChange={(e) => handleChange("spouseFatherName", e.target.value)}
/>
<div className="space-y-2">
<Input
placeholder="填写父亲姓名"
value={formData.spouseFatherName || ""}
onChange={(e) => handleChange("spouseFatherName", e.target.value)}
/>
<p className="text-xs text-muted-foreground"></p>
</div>
) : (
<div className="flex gap-2">
<Select
value={formData.fatherId || "none"}
onValueChange={(val) => {
const newValue = val === "none" ? undefined : val
console.log('Father select change:', { val, newValue })
handleChange("fatherId", newValue)
}}
>
@@ -480,18 +434,20 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
<div className="space-y-2">
<Label htmlFor="motherId"></Label>
{isFounder ? (
<Input
placeholder="填写母亲姓名"
value={formData.spouseMotherName || ""}
onChange={(e) => handleChange("spouseMotherName", e.target.value)}
/>
<div className="space-y-2">
<Input
placeholder="填写母亲姓名"
value={formData.spouseMotherName || ""}
onChange={(e) => handleChange("spouseMotherName", e.target.value)}
/>
<p className="text-xs text-muted-foreground"></p>
</div>
) : (
<div className="flex gap-2">
<Select
value={formData.motherId || "none"}
onValueChange={(val) => {
const newValue = val === "none" ? undefined : val
console.log('Mother select change:', { val, newValue })
handleChange("motherId", newValue)
}}
>
+118 -71
View File
@@ -10,6 +10,8 @@ import { MemberNameWithStatus } from "@/components/member-name-with-status"
import { useFamily } from "@/context/family-context"
import { useToast } from "@/components/ui/use-toast"
import type { FamilyMember } from "@/types/family"
import { GenerationWarningDialog } from "@/components/tree/generation-warning-dialog"
import { shouldShowGenerationWarning } from "@/lib/generation-utils"
export type RelationType = "child" | "spouse" | "father" | "mother"
@@ -38,11 +40,17 @@ export function AddRelationDialog({
const { toast } = useToast()
const [query, setQuery] = useState("")
const [linkingId, setLinkingId] = useState<string | null>(null)
// 代数重新计算警告对话框状态
const [showWarningDialog, setShowWarningDialog] = useState(false)
const [pendingAction, setPendingAction] = useState<{ type: 'link' | 'create', target?: FamilyMember } | null>(null)
useEffect(() => {
if (!open) {
setQuery("")
setLinkingId(null)
setShowWarningDialog(false)
setPendingAction(null)
}
}, [open])
@@ -72,7 +80,6 @@ export function AddRelationDialog({
if (member.gender === "MALE" && !fatherMatches) return false
if (member.gender === "FEMALE" && !motherMatches) return false
if (member.gender !== "MALE" && member.gender !== "FEMALE") {
// 非男女成员,允许填补任一缺失的父母
if (candidate.fatherId && candidate.motherId) return false
}
} else if (relationType === "spouse") {
@@ -81,12 +88,10 @@ export function AddRelationDialog({
if (candidate.spouseIds?.includes(member.id)) return false
if (candidate.gender === member.gender) return false
} else if (relationType === "father") {
// 父亲必须是男性,世代比当前成员小1
if (candidate.gender !== "MALE") return false
if (candidate.generation !== (member.generation - 1)) return false
if (member.fatherId === candidate.id) return false
} else if (relationType === "mother") {
// 母亲必须是女性,世代比当前成员小1
if (candidate.gender !== "FEMALE") return false
if (candidate.generation !== (member.generation - 1)) return false
if (member.motherId === candidate.id) return false
@@ -103,6 +108,17 @@ export function AddRelationDialog({
}, [member, relationType, treeData.members, normalizedQuery])
const handleLink = async (target: FamilyMember) => {
// 检查是否需要显示代数重新计算警告
if ((relationType === 'father' || relationType === 'mother') && shouldShowGenerationWarning(member, relationType)) {
setPendingAction({ type: 'link', target })
setShowWarningDialog(true)
return
}
await performLink(target)
}
const performLink = async (target: FamilyMember) => {
try {
setLinkingId(target.id)
if (relationType === "child") {
@@ -161,80 +177,111 @@ export function AddRelationDialog({
const handleCreateNew = () => {
if (!normalizedQuery) return
// 检查是否需要显示代数重新计算警告
if ((relationType === 'father' || relationType === 'mother') && shouldShowGenerationWarning(member, relationType)) {
setPendingAction({ type: 'create' })
setShowWarningDialog(true)
return
}
onCreateNew(query.trim(), relationType)
}
// 确认添加父母后的处理
const handleConfirmAddParent = () => {
if (pendingAction?.type === 'link' && pendingAction.target) {
performLink(pendingAction.target)
} else if (pendingAction?.type === 'create') {
onCreateNew(query.trim(), relationType)
}
setPendingAction(null)
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>{relationType === "child" ? "子女" : relationType === "spouse" ? "配偶" : relationType === "father" ? "父亲" : "母亲"}</DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>{relationType === "child" ? "子女" : relationType === "spouse" ? "配偶" : relationType === "father" ? "父亲" : "母亲"}</DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="relation-name"></Label>
<Input
id="relation-name"
placeholder="请输入姓名或字号"
value={query}
autoFocus
onChange={(e) => setQuery(e.target.value)}
/>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="relation-name"></Label>
<Input
id="relation-name"
placeholder="请输入姓名或字号"
value={query}
autoFocus
onChange={(e) => setQuery(e.target.value)}
/>
</div>
{normalizedQuery ? (
candidates.length > 0 ? (
<div>
<p className="text-sm text-muted-foreground mb-2"></p>
<ScrollArea className="max-h-64 rounded-md border">
<div className="divide-y">
{candidates.map(({ member: candidate, fatherName, motherName }) => (
<div key={candidate.id} className="p-3 flex flex-col gap-2">
<div className="flex items-center justify-between gap-3">
<div>
<MemberNameWithStatus name={candidate.fullName} isDead={!!candidate.deathDate} />
<p className="text-xs text-muted-foreground">
{candidate.generation} · {candidate.gender === "FEMALE" ? "女" : "男"}
</p>
</div>
<Button
size="sm"
onClick={() => handleLink(candidate)}
disabled={linkingId === candidate.id}
>
{linkingId === candidate.id ? "关联中..." : "关联"}
</Button>
</div>
<div className="text-xs text-muted-foreground">
<div>{fatherName || "未登记"}</div>
<div>{motherName || "未登记"}</div>
</div>
</div>
))}
</div>
</ScrollArea>
</div>
) : (
<p className="text-sm text-muted-foreground"></p>
)
) : (
<p className="text-sm text-muted-foreground"></p>
)}
</div>
{normalizedQuery ? (
candidates.length > 0 ? (
<div>
<p className="text-sm text-muted-foreground mb-2"></p>
<ScrollArea className="max-h-64 rounded-md border">
<div className="divide-y">
{candidates.map(({ member: candidate, fatherName, motherName }) => (
<div key={candidate.id} className="p-3 flex flex-col gap-2">
<div className="flex items-center justify-between gap-3">
<div>
<MemberNameWithStatus name={candidate.fullName} isDead={!!candidate.deathDate} />
<p className="text-xs text-muted-foreground">
{candidate.generation} · {candidate.gender === "FEMALE" ? "女" : "男"}
</p>
</div>
<Button
size="sm"
onClick={() => handleLink(candidate)}
disabled={linkingId === candidate.id}
>
{linkingId === candidate.id ? "关联中..." : "关联"}
</Button>
</div>
<div className="text-xs text-muted-foreground">
<div>{fatherName || "未登记"}</div>
<div>{motherName || "未登记"}</div>
</div>
</div>
))}
</div>
</ScrollArea>
</div>
) : (
<p className="text-sm text-muted-foreground"></p>
)
) : (
<p className="text-sm text-muted-foreground"></p>
)}
</div>
<DialogFooter className="flex flex-col gap-2 sm:flex-row sm:justify-between">
<Button variant="outline" onClick={() => onOpenChange(false)} className="sm:flex-1">
</Button>
<Button onClick={handleCreateNew} disabled={!normalizedQuery} className="sm:flex-1">
{query || "成员"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<DialogFooter className="flex flex-col gap-2 sm:flex-row sm:justify-between">
<Button variant="outline" onClick={() => onOpenChange(false)} className="sm:flex-1">
</Button>
<Button onClick={handleCreateNew} disabled={!normalizedQuery} className="sm:flex-1">
"{query || ""}"
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 代数重新计算警告对话框 */}
{pendingAction && (relationType === 'father' || relationType === 'mother') && (
<GenerationWarningDialog
open={showWarningDialog}
onOpenChange={setShowWarningDialog}
memberName={member.fullName}
relationType={relationType}
onConfirm={handleConfirmAddParent}
/>
)}
</>
)
}
+41 -3
View File
@@ -9,6 +9,8 @@ import type { FamilyMember } from "@/types/family"
import jsPDF from "jspdf"
import { useRouter } from "next/navigation"
import { useFamily } from "@/context/family-context"
import { GenerationWarningDialog } from "@/components/tree/generation-warning-dialog"
import { shouldShowGenerationWarning } from "@/lib/generation-utils"
// 全局类型声明
declare global {
@@ -99,6 +101,10 @@ const D3OrgChartFlowComponent = forwardRef<D3OrgChartRef, D3OrgChartFlowProps>(
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 })
const router = useRouter()
const { currentTree } = useFamily()
// 代数重新计算警告对话框状态
const [showWarningDialog, setShowWarningDialog] = useState(false)
const [pendingAddMember, setPendingAddMember] = useState<{ type: 'father' | 'mother', member: FamilyMember } | null>(null)
// 构建新增成员的 URL
const buildAddUrl = (type: 'father' | 'mother' | 'spouse' | 'child', member: FamilyMember) => {
@@ -111,7 +117,8 @@ const D3OrgChartFlowComponent = forwardRef<D3OrgChartRef, D3OrgChartFlowProps>(
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)
@@ -120,7 +127,8 @@ const D3OrgChartFlowComponent = forwardRef<D3OrgChartRef, D3OrgChartFlowProps>(
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)
@@ -163,6 +171,13 @@ const D3OrgChartFlowComponent = forwardRef<D3OrgChartRef, D3OrgChartFlowProps>(
}
const handleAddMember = (type: 'father' | 'mother' | 'spouse' | 'child', member: FamilyMember) => {
// 检查是否需要显示代数重新计算警告
if ((type === 'father' || type === 'mother') && shouldShowGenerationWarning(member, type)) {
setPendingAddMember({ type, member })
setShowWarningDialog(true)
return
}
const url = buildAddUrl(type, member)
console.log('D3 handleAddMember called:', type, url)
// 使用 setTimeout 延迟跳转,确保菜单关闭后再执行
@@ -171,6 +186,18 @@ const D3OrgChartFlowComponent = forwardRef<D3OrgChartRef, D3OrgChartFlowProps>(
window.location.href = url
}, 100)
}
// 确认添加父母后的处理
const handleConfirmAddParent = () => {
if (pendingAddMember) {
const url = buildAddUrl(pendingAddMember.type, pendingAddMember.member)
console.log('Confirmed add parent, navigating to:', url)
setTimeout(() => {
window.location.href = url
}, 100)
setPendingAddMember(null)
}
}
// 判断可以添加的关系
const canAddFather = (member: FamilyMember) => !member.fatherId
@@ -277,7 +304,7 @@ const D3OrgChartFlowComponent = forwardRef<D3OrgChartRef, D3OrgChartFlowProps>(
spouseCount: spouses.length,
hasChildren: Object.values(members).some(m => m.fatherId === member.id || m.motherId === member.id),
ageText: ageText,
isFounder: member.isFounder === true
isFounder: member.id === rootId // 使用 rootId 判断始祖
})
// 添加子节点到队列
@@ -1022,6 +1049,17 @@ const D3OrgChartFlowComponent = forwardRef<D3OrgChartRef, D3OrgChartFlowProps>(
onClick={() => setContextMenuNode(null)}
/>
)}
{/* 代数重新计算警告对话框 */}
{pendingAddMember && (
<GenerationWarningDialog
open={showWarningDialog}
onOpenChange={setShowWarningDialog}
memberName={pendingAddMember.member.fullName}
relationType={pendingAddMember.type}
onConfirm={handleConfirmAddParent}
/>
)}
</div>
)
})
+121 -78
View File
@@ -1,5 +1,6 @@
"use client"
import { useState } from "react"
import type { FamilyMember } from "@/types/family"
import { cn } from "@/lib/utils"
import { useAvatarCache } from "@/hooks/use-avatar-cache"
@@ -15,6 +16,8 @@ import {
ContextMenuLabel,
ContextMenuTrigger,
} from "@/components/ui/context-menu"
import { GenerationWarningDialog } from "@/components/tree/generation-warning-dialog"
import { shouldShowGenerationWarning } from "@/lib/generation-utils"
interface FamilyNodeProps {
member: FamilyMember
@@ -90,6 +93,10 @@ function PersonCard({
const router = useRouter()
const { currentTree } = useFamily()
// 代数重新计算警告对话框状态
const [showWarningDialog, setShowWarningDialog] = useState(false)
const [pendingRelationType, setPendingRelationType] = useState<'father' | 'mother' | null>(null)
const birthYear = formatYear(member.birthDate)
const deathYear = formatYear(member.deathDate)
const age = calculateAge(member.birthDate, member.deathDate)
@@ -123,7 +130,8 @@ function PersonCard({
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)
@@ -132,7 +140,8 @@ function PersonCard({
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)
@@ -175,6 +184,13 @@ function PersonCard({
}
const handleAddMember = (type: 'father' | 'mother' | 'spouse' | 'child') => {
// 检查是否需要显示代数重新计算警告
if ((type === 'father' || type === 'mother') && shouldShowGenerationWarning(member, type)) {
setPendingRelationType(type)
setShowWarningDialog(true)
return
}
const url = buildAddUrl(type)
console.log('handleAddMember called:', type, url)
// 使用 setTimeout 延迟跳转,确保菜单关闭后再执行
@@ -183,6 +199,18 @@ function PersonCard({
window.location.href = url
}, 100)
}
// 确认添加父母后的处理
const handleConfirmAddParent = () => {
if (pendingRelationType) {
const url = buildAddUrl(pendingRelationType)
console.log('Confirmed add parent, navigating to:', url)
setTimeout(() => {
window.location.href = url
}, 100)
setPendingRelationType(null)
}
}
// 是否显示右键菜单(有可添加的选项时才显示)
const hasAddOptions = canEdit && (canAddFather || canAddMother || canAddSpouse || canAddChild)
@@ -193,7 +221,8 @@ function PersonCard({
// 庄严正式的中国风配色
const isDead = !!member.deathDate
const isMale = member.gender === 'MALE'
const isFounder = member.isFounder === true
// 使用 isRoot prop 而不是 isFounder 字段
const isFounder = isRoot === true
const cardContent = (
<div
@@ -378,81 +407,94 @@ function PersonCard({
// 有可添加选项时,包裹右键菜单
return (
<ContextMenu>
<ContextMenuTrigger>
{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>
{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()
if (onAddRelation) {
onAddRelation('spouse')
} else {
handleAddMember('spouse')
}
}}
>
<Users className="h-4 w-4 text-red-500" />
</ContextMenuItem>
)}
{canAddChild && (
<ContextMenuItem
className="gap-2 cursor-pointer"
onPointerDown={(e) => {
e.preventDefault()
if (onAddRelation) {
onAddRelation('child')
} else {
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()
if (onAddRelation) {
onAddRelation('spouse')
} else {
handleAddMember('spouse')
}
}}
>
<Users className="h-4 w-4 text-red-500" />
</ContextMenuItem>
)}
{canAddChild && (
<ContextMenuItem
className="gap-2 cursor-pointer"
onPointerDown={(e) => {
e.preventDefault()
if (onAddRelation) {
onAddRelation('child')
} else {
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}
/>
)}
</>
)
}
@@ -496,7 +538,8 @@ export function FamilyNode({
// 有配偶时,合并到一个卡片中显示(类似D3布局)
const isMale = member.gender === 'MALE'
const isDead = !!member.deathDate
const isFounder = member.isFounder === true
// 使用 isRoot prop 而不是 isFounder 字段
const isFounder = isRoot === true
const avatarUrl = member.avatarUrl
// 计算生卒年份
@@ -0,0 +1,86 @@
"use client"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog"
import { AlertTriangle } from "lucide-react"
interface GenerationWarningDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
memberName: string
relationType: 'father' | 'mother'
onConfirm: () => void
}
/**
* 代数重新计算警告对话框
* 当用户尝试给第1代成员(始祖)添加父母时显示此对话框
*/
export function GenerationWarningDialog({
open,
onOpenChange,
memberName,
relationType,
onConfirm,
}: GenerationWarningDialogProps) {
const relationText = relationType === 'father' ? '父亲' : '母亲'
const handleConfirm = () => {
onOpenChange(false)
onConfirm()
}
return (
<AlertDialog open={open} onOpenChange={onOpenChange}>
<AlertDialogContent className="max-w-md">
<AlertDialogHeader>
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-amber-100 dark:bg-amber-900/30">
<AlertTriangle className="h-5 w-5 text-amber-600 dark:text-amber-500" />
</div>
<AlertDialogTitle className="text-lg">
</AlertDialogTitle>
</div>
<AlertDialogDescription asChild>
<div className="space-y-3 pt-2">
<p>
<span className="font-semibold text-foreground">{memberName}</span> {relationText}
<span className="text-amber-600 dark:text-amber-500 font-medium"></span>
</p>
<div className="rounded-lg bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 p-3">
<p className="text-sm text-amber-800 dark:text-amber-200">
<span className="font-medium"></span>
<br />
12{relationText}1
</p>
</div>
<p className="text-sm text-muted-foreground">
</p>
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction
onClick={handleConfirm}
className="bg-amber-600 hover:bg-amber-700 text-white"
>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}
+4 -2
View File
@@ -333,7 +333,8 @@ export function TreeLayout({
})
}
} 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")
// 如果已有母亲,设为配偶
@@ -341,7 +342,8 @@ export function TreeLayout({
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")
// 如果已有父亲,设为配偶
+10 -5
View File
@@ -50,7 +50,8 @@ export function DialogProvider({ children }: { children: ReactNode }) {
confirmText: "确定",
onConfirm: () => {
setOpen(false)
resolve()
// 等待模态框动画完成后再 resolve
setTimeout(() => resolve(), 150)
},
})
setOpen(true)
@@ -72,11 +73,13 @@ export function DialogProvider({ children }: { children: ReactNode }) {
variant,
onConfirm: () => {
setOpen(false)
resolve(true)
// 等待模态框动画完成后再 resolve
setTimeout(() => resolve(true), 150)
},
onCancel: () => {
setOpen(false)
resolve(false)
// 等待模态框动画完成后再 resolve
setTimeout(() => resolve(false), 150)
},
})
setOpen(true)
@@ -99,11 +102,13 @@ export function DialogProvider({ children }: { children: ReactNode }) {
cancelText: "取消",
onConfirm: (value) => {
setOpen(false)
resolve(value || null)
// 等待模态框动画完成后再 resolve
setTimeout(() => resolve(value || null), 150)
},
onCancel: () => {
setOpen(false)
resolve(null)
// 等待模态框动画完成后再 resolve
setTimeout(() => resolve(null), 150)
},
})
setOpen(true)
+6 -4
View File
@@ -8,6 +8,7 @@ import { Upload, X, Loader2, Crop } from "lucide-react"
import ReactCrop, { type Crop as CropType } from "react-image-crop"
import "react-image-crop/dist/ReactCrop.css"
import imageCompression from "browser-image-compression"
import { useDialog } from "@/components/ui/alert-dialog-custom"
interface ImageUploadProps {
value?: string // The image URL
@@ -24,6 +25,7 @@ export function ImageUpload({ value, onChange, className }: ImageUploadProps) {
const [completedCrop, setCompletedCrop] = useState<CropType>()
const imgRef = useRef<HTMLImageElement>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
const { showAlert } = useDialog()
// Load preview if value (URL) exists
useEffect(() => {
@@ -41,7 +43,7 @@ export function ImageUpload({ value, onChange, className }: ImageUploadProps) {
try {
// Basic validation
if (!file.type.startsWith("image/")) {
alert("请选择图片文件")
await showAlert("请选择图片文件", "提示")
return
}
@@ -63,7 +65,7 @@ export function ImageUpload({ value, onChange, className }: ImageUploadProps) {
reader.readAsDataURL(compressedFile)
} catch (error) {
console.error("Failed to process image:", error)
alert("图片处理失败")
await showAlert("图片处理失败", "错误")
} finally {
// Reset input
if (fileInputRef.current) fileInputRef.current.value = ""
@@ -108,7 +110,7 @@ export function ImageUpload({ value, onChange, className }: ImageUploadProps) {
}, 'image/jpeg', 0.9)
} catch (error) {
console.error("Failed to crop image:", error)
alert("图片裁剪失败")
await showAlert("图片裁剪失败", "错误")
} finally {
setIsLoading(false)
setShowCropDialog(false)
@@ -146,7 +148,7 @@ export function ImageUpload({ value, onChange, className }: ImageUploadProps) {
setImageToCrop(null)
} catch (error) {
console.error("Failed to save image:", error)
alert("图片保存失败: " + (error instanceof Error ? error.message : '未知错误'))
await showAlert("图片保存失败: " + (error instanceof Error ? error.message : '未知错误'), "错误")
} finally {
setIsLoading(false)
}