0.0.5.1
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
}}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user