This commit is contained in:
freedakgmail
2025-11-23 15:56:24 +08:00
parent 8b32cda811
commit 3375a79ed4
74 changed files with 99918 additions and 1227 deletions
+3 -3
View File
@@ -21,9 +21,9 @@ export function StatisticsCharts({ members }: StatisticsChartsProps) {
// 性别分布饼图
const genderChart = echarts.init(genderChartRef.current)
const genderData = {
male: members.filter(m => m.gender === 'male').length,
female: members.filter(m => m.gender === 'female').length,
other: members.filter(m => m.gender === 'other').length,
male: members.filter(m => m.gender === 'MALE').length,
female: members.filter(m => m.gender === 'FEMALE').length,
other: members.filter(m => m.gender === 'OTHER').length,
}
genderChart.setOption({
+183 -24
View File
@@ -28,20 +28,35 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
? Math.max(...existingMembers.map(m => m.generation || 1))
: 1
// 获取始祖的姓氏(第1世成员)
const ancestorSurname = existingMembers.find(m => m.generation === 1)?.surname || ""
// Initialize state with default values or initialData
const [formData, setFormData] = useState<Partial<FamilyMember>>({
id: initialData?.id || crypto.randomUUID(),
surname: initialData?.surname || "李",
givenName: initialData?.givenName || "",
fullName: initialData?.fullName || "",
gender: (initialData?.gender || "MALE").toUpperCase() as "MALE" | "FEMALE",
generation: initialData?.generation || maxGeneration,
spouseIds: initialData?.spouseIds || [],
childrenIds: initialData?.childrenIds || [],
tags: initialData?.tags || [],
fatherId: initialData?.fatherId,
motherId: initialData?.motherId,
...initialData,
const [formData, setFormData] = useState<Partial<FamilyMember>>(() => {
const defaults = {
id: crypto.randomUUID(),
surname: ancestorSurname,
givenName: "",
fullName: "",
gender: "MALE" as "MALE" | "FEMALE",
generation: maxGeneration,
spouseIds: [],
childrenIds: [],
tags: [],
}
if (!initialData) return defaults
return {
...defaults,
...initialData,
gender: (initialData.gender || "MALE").toUpperCase() as "MALE" | "FEMALE",
spouseIds: initialData.spouseIds || [],
childrenIds: initialData.childrenIds || [],
tags: initialData.tags || [],
fatherId: initialData.fatherId || undefined,
motherId: initialData.motherId || undefined,
}
})
const [errors, setErrors] = useState<string[]>([])
@@ -81,14 +96,48 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
if (errors.length > 0) setErrors([])
}
const handleSpouseChange = (spouseId: string) => {
// If selecting "none" (empty string), clear spouses
if (!spouseId) {
setFormData((prev) => ({ ...prev, spouseIds: [] }))
return
}
// Otherwise set as single spouse (logic can be expanded for multiple later)
setFormData((prev) => ({ ...prev, spouseIds: [spouseId] }))
const handleSpouseAdd = (spouseId: string) => {
if (!spouseId || spouseId === "none") return
setFormData((prev) => {
const currentSpouses = prev.spouseIds || []
// 避免重复添加
if (currentSpouses.includes(spouseId)) return prev
return {
...prev,
spouseIds: [...currentSpouses, spouseId]
}
})
}
const handleSpouseRemove = (spouseId: string) => {
setFormData((prev) => ({
...prev,
spouseIds: (prev.spouseIds || []).filter(id => id !== spouseId)
}))
}
const handleChildAdd = (childId: string) => {
if (!childId || childId === "none") return
setFormData((prev) => {
const currentChildren = prev.childrenIds || []
// 避免重复添加
if (currentChildren.includes(childId)) return prev
return {
...prev,
childrenIds: [...currentChildren, childId]
}
})
}
const handleChildRemove = (childId: string) => {
setFormData((prev) => ({
...prev,
childrenIds: (prev.childrenIds || []).filter(id => id !== childId)
}))
}
const validate = (): boolean => {
@@ -341,17 +390,30 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
<div className="space-y-2">
<Label htmlFor="spouseId"> (Spouse)</Label>
<Select value={formData.spouseIds?.[0] || "none"} onValueChange={handleSpouseChange}>
<Select value="none" onValueChange={handleSpouseAdd}>
<SelectTrigger>
<SelectValue placeholder="选择配偶 Select Spouse" />
<SelectValue placeholder="添加配偶..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"> (None)</SelectItem>
<SelectItem value="none"></SelectItem>
{potentialRelatives
.filter((m) => {
// 必须是同世系
if (m.generation !== formData.generation) return false
// 通常配偶应该是异性
if (m.gender === formData.gender) return false
// 排除已选的配偶
if (formData.spouseIds?.includes(m.id)) return false
// 一夫多妻:女性可以选择已有配偶的男性,男性只能选择未婚女性
if (formData.gender === 'MALE') {
// 男性:排除已有配偶的女性
if (m.spouseIds && m.spouseIds.length > 0) return false
}
// 女性:可以选择已有配偶的男性(一夫多妻)
// 排除直系亲属
// 1. 排除父母
if (m.id === formData.fatherId || m.id === formData.motherId) return false
@@ -372,6 +434,103 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
))}
</SelectContent>
</Select>
{/* 已选配偶列表 */}
{formData.spouseIds && formData.spouseIds.length > 0 && (
<div className="mt-2 space-y-2">
{formData.spouseIds.map((spouseId, index) => {
const spouse = existingMembers.find(m => m.id === spouseId)
if (!spouse) return null
return (
<div key={spouseId} className="flex items-center justify-between p-2 bg-muted rounded">
<span className="text-sm">
{index + 1}. {spouse.fullName} ({spouse.generation})
</span>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => handleSpouseRemove(spouseId)}
className="h-6 w-6 p-0 hover:bg-destructive hover:text-destructive-foreground"
>
×
</Button>
</div>
)
})}
</div>
)}
</div>
<div className="space-y-2">
<Label htmlFor="childId"> (Children)</Label>
<Select value="none" onValueChange={handleChildAdd}>
<SelectTrigger>
<SelectValue placeholder="添加子女..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"></SelectItem>
{potentialRelatives
.filter((m) => {
// 必须是下一世系
if (m.generation !== (formData.generation || 1) + 1) return false
// 排除已选的子女
if (formData.childrenIds?.includes(m.id)) return false
// 排除配偶
if (formData.spouseIds?.includes(m.id)) return false
// 排除配偶身份的成员(没有父母但有配偶的)
const isSpouse = !m.fatherId && !m.motherId && m.spouseIds && m.spouseIds.length > 0
if (isSpouse) return false
// 根据当前成员性别,排除已有对应父母的子女
if (formData.gender === 'MALE') {
// 男性:排除已有父亲的子女
if (m.fatherId) return false
} else if (formData.gender === 'FEMALE') {
// 女性:排除已有母亲的子女
if (m.motherId) return false
}
return true
})
.map((m) => (
<SelectItem key={m.id} value={m.id}>
{m.fullName} ({m.generation})
</SelectItem>
))}
</SelectContent>
</Select>
{/* 已选子女列表 */}
{formData.childrenIds && formData.childrenIds.length > 0 && (
<div className="mt-2 space-y-2">
{formData.childrenIds.map((childId, index) => {
const child = existingMembers.find(m => m.id === childId)
if (!child) return null
return (
<div key={childId} className="flex items-center justify-between p-2 bg-muted rounded">
<span className="text-sm">
{index + 1}. {child.fullName} ({child.generation})
</span>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => handleChildRemove(childId)}
className="h-6 w-6 p-0 hover:bg-destructive hover:text-destructive-foreground"
>
×
</Button>
</div>
)
})}
</div>
)}
</div>
</CardContent>
</Card>
+6 -7
View File
@@ -88,11 +88,10 @@ const D3OrgChartFlowComponent = forwardRef<D3OrgChartRef, D3OrgChartFlowProps>(
const member = members[currentId]
if (!member) continue
// 获取配偶信息,确保配偶存在于members中
let spouse = null
if (member.spouseIds?.[0] && members[member.spouseIds[0]]) {
spouse = members[member.spouseIds[0]]
}
// 获取所有配偶信息,确保配偶存在于members中
const spouses = member.spouseIds
?.map(id => members[id])
.filter(Boolean) || []
// 确定父节点ID - 只有根节点的parentId为null
let parentId: string | null = null
@@ -135,8 +134,8 @@ const D3OrgChartFlowComponent = forwardRef<D3OrgChartRef, D3OrgChartFlowProps>(
birthPlace: member.birthPlace || '',
gender: member.gender === 'MALE' ? '男' : '女',
avatarImageId: member.avatarImageId,
spouseName: spouse?.fullName || '',
spouseId: spouse?.id || null,
spouseName: spouses.length > 0 ? spouses.map(s => s.fullName).join(', ') : '',
spouseId: spouses.length > 0 ? spouses[0].id : null,
hasChildren: Object.values(members).some(m => m.fatherId === member.id || m.motherId === member.id),
ageText: ageText
})
+14 -6
View File
@@ -8,6 +8,7 @@ import { CircleUser, CircleUserRound, Heart } from "lucide-react"
interface FamilyNodeProps {
member: FamilyMember
spouse?: FamilyMember
spouses?: FamilyMember[]
isRoot?: boolean
onSelect?: (member: FamilyMember) => void
isHighlighted?: boolean
@@ -88,7 +89,7 @@ function PersonCard({ member, isRoot, isSpouse, onSelect, isHighlighted, relatio
className="h-full w-full object-cover"
/>
) : (
member.gender === 'female' ? (
member.gender === 'FEMALE' ? (
<CircleUserRound className="h-8 w-8 text-pink-400" />
) : (
<CircleUser className="h-8 w-8 text-blue-400" />
@@ -125,17 +126,24 @@ function PersonCard({ member, isRoot, isSpouse, onSelect, isHighlighted, relatio
)
}
export function FamilyNode({ member, spouse, isRoot, onSelect, isHighlighted, relationMode, isSelected }: FamilyNodeProps) {
if (!spouse) {
export function FamilyNode({ member, spouse, spouses, isRoot, onSelect, isHighlighted, relationMode, isSelected }: FamilyNodeProps) {
// 优先使用 spouses 数组,如果没有则使用单个 spouse
const spouseList = spouses || (spouse ? [spouse] : [])
if (spouseList.length === 0) {
return <PersonCard member={member} isRoot={isRoot} onSelect={onSelect} isHighlighted={isHighlighted} relationMode={relationMode} isSelected={isSelected} />
}
// 有配偶时,并排显示
// 有配偶时,并排显示所有配偶
return (
<div className="flex items-center gap-2">
<PersonCard member={member} isRoot={isRoot} onSelect={onSelect} isHighlighted={isHighlighted} relationMode={relationMode} isSelected={isSelected} />
<Heart className="h-4 w-4 text-red-400 flex-shrink-0" />
<PersonCard member={spouse} isSpouse={true} onSelect={onSelect} relationMode={relationMode} />
{spouseList.map((spouseMember, index) => (
<div key={spouseMember.id} className="flex items-center gap-2">
<Heart className="h-4 w-4 text-red-400 flex-shrink-0" />
<PersonCard member={spouseMember} isSpouse={true} onSelect={onSelect} relationMode={relationMode} />
</div>
))}
</div>
)
}
+28 -10
View File
@@ -139,10 +139,10 @@ export function TreeLayout({
const hasChildren = member.childrenIds && member.childrenIds.length > 0
// 获取配偶(取第一个配偶
const spouse = member.spouseIds && member.spouseIds.length > 0
? getMember(member.spouseIds[0])
: undefined
// 获取所有配偶
const spouses = member.spouseIds && member.spouseIds.length > 0
? member.spouseIds.map(id => getMember(id)).filter(Boolean) as FamilyMember[]
: []
return (
<div className="flex flex-col items-center">
@@ -159,7 +159,7 @@ export function TreeLayout({
>
<FamilyNode
member={member}
spouse={spouse}
spouses={spouses}
onSelect={handleNodeClick}
isRoot={isRoot}
isHighlighted={member.id === highlightedMemberId}
@@ -177,9 +177,27 @@ export function TreeLayout({
gap: `${HORIZONTAL_GAP}px`
}}
>
{member.childrenIds.map((childId) => (
<TreeNode key={childId} memberId={childId} />
))}
{(() => {
// 对子节点进行排序:始祖排最左边,其他按出生日期排序
const sortedChildren = [...member.childrenIds].sort((aId, bId) => {
const a = getMember(aId)
const b = getMember(bId)
if (!a || !b) return 0
// 始祖永远排最左边
const aIsFounder = a.tags?.includes('始祖') || a.generation === 1
const bIsFounder = b.tags?.includes('始祖') || b.generation === 1
if (aIsFounder && !bIsFounder) return -1
if (!aIsFounder && bIsFounder) return 1
// 其他按出生日期排序
return (a.birthDate || "").localeCompare(b.birthDate || "")
})
return sortedChildren.map((childId) => (
<TreeNode key={childId} memberId={childId} />
))
})()}
</div>
)}
</div>
@@ -205,13 +223,13 @@ export function TreeLayout({
return (
<div
key={index}
className="absolute bg-foreground/25"
className="absolute bg-black"
style={{
left: `${left}px`,
top: `${top}px`,
width: `${width}px`,
height: `${height}px`,
transform: isHorizontal ? 'scaleY(0.3)' : 'scaleX(0.3)',
transform: isHorizontal ? 'scaleY(0.5)' : 'scaleX(0.5)',
transformOrigin: 'top left',
}}
/>