This commit is contained in:
freedakgmail
2025-11-23 14:46:53 +08:00
parent 2b4bc92f1b
commit d797f1abb9
98 changed files with 187824 additions and 161 deletions
+470
View File
@@ -0,0 +1,470 @@
import relationship from 'relationship.js'
import type { FamilyMember } from '@/types/family'
/**
* 家族关系计算器
* 使用 relationship.js 库计算中文亲属关系
*/
export class RelationshipCalculator {
private members: Map<string, FamilyMember>
constructor(members: FamilyMember[]) {
this.members = new Map(members.map(m => [m.id, m]))
}
/**
* 计算两个成员之间的关系
* @param fromId 起始成员ID(成员A
* @param toId 目标成员ID(成员B
* @returns 关系描述(toId 对 fromId 的称谓,即B是A的XXX
*/
getRelationship(fromId: string, toId: string): RelationshipResult {
if (fromId === toId) {
return {
term: '本人',
path: '',
success: true
}
}
const fromMember = this.members.get(fromId)
const toMember = this.members.get(toId)
if (!fromMember || !toMember) {
return {
term: '未知',
path: '',
success: false,
error: '成员不存在'
}
}
// 构建关系路径(从 fromId 到 toId
const path = this.buildRelationPath(fromId, toId)
if (!path) {
return {
term: '无血缘关系',
path: '',
success: false
}
}
// 将路径转换为中文
const chinesePath = this.pathToChineseText(path)
// 使用 relationship.js 计算关系
try {
// 获取fromId成员的性别(从谁的角度看关系)
const fromMemberGender = fromMember.gender === 'FEMALE' ? 0 : 1
// 计算关系(toId 对 fromId 的称谓)
const result = relationship({
text: chinesePath,
target: '',
sex: fromMemberGender, // 传入fromId的性别(从谁的角度看)
reverse: true // 使用反向,因为路径是从A到B,但我们要的是B对A的称谓
})
const term = Array.isArray(result) && result.length > 0 ? result[0] : '亲戚'
return {
term,
path: chinesePath,
success: true
}
} catch (error) {
return {
term: '亲戚',
path: chinesePath,
success: true
}
}
}
/**
* 构建关系路径字符串
* 例如:'f' = 父亲, 'm' = 母亲, 'h' = 丈夫, 'w' = 妻子
* 'ob' = 哥哥, 'lb' = 弟弟, 'os' = 姐姐, 'ls' = 妹妹
*/
private buildRelationPath(fromId: string, toId: string): string | null {
// 使用BFS查找最短路径
const queue: Array<{ id: string; path: string }> = [{ id: fromId, path: '' }]
const visited = new Set<string>()
while (queue.length > 0) {
const { id, path } = queue.shift()!
if (id === toId) {
return path
}
if (visited.has(id)) continue
visited.add(id)
const member = this.members.get(id)
if (!member) continue
// 向上:父亲
if (member.fatherId && !visited.has(member.fatherId)) {
queue.push({ id: member.fatherId, path: path + 'f' })
}
// 向上:母亲
if (member.motherId && !visited.has(member.motherId)) {
queue.push({ id: member.motherId, path: path + 'm' })
}
// 配偶关系
if (member.spouseIds) {
for (const spouseId of member.spouseIds) {
if (!visited.has(spouseId)) {
const spouse = this.members.get(spouseId)
if (spouse) {
const spousePath = member.gender === 'MALE' ? 'w' : 'h'
queue.push({ id: spouseId, path: path + spousePath })
}
}
}
}
// 向下:子女
if (member.childrenIds) {
for (const childId of member.childrenIds) {
if (!visited.has(childId)) {
const child = this.members.get(childId)
if (child) {
const childPath = child.gender === 'MALE' ? 's' : 'd'
queue.push({ id: childId, path: path + childPath })
}
}
}
}
// 兄弟姐妹(通过父母查找)
if (member.fatherId || member.motherId) {
const siblings = this.getSiblings(id)
for (const siblingId of siblings) {
if (!visited.has(siblingId)) {
const sibling = this.members.get(siblingId)
if (sibling) {
// 需要比较年龄确定是哥哥/弟弟/姐姐/妹妹
const siblingPath = this.getSiblingPath(member, sibling)
queue.push({ id: siblingId, path: path + siblingPath })
}
}
}
}
}
return null
}
/**
* 获取兄弟姐妹
*/
private getSiblings(memberId: string): string[] {
const member = this.members.get(memberId)
if (!member) return []
const siblings: string[] = []
// 通过父亲查找
if (member.fatherId) {
const father = this.members.get(member.fatherId)
if (father?.childrenIds) {
siblings.push(...father.childrenIds.filter(id => id !== memberId))
}
}
// 通过母亲查找
if (member.motherId) {
const mother = this.members.get(member.motherId)
if (mother?.childrenIds) {
for (const childId of mother.childrenIds) {
if (childId !== memberId && !siblings.includes(childId)) {
siblings.push(childId)
}
}
}
}
return siblings
}
/**
* 获取兄弟姐妹关系路径
*/
private getSiblingPath(from: FamilyMember, to: FamilyMember): string {
const fromAge = this.getAge(from)
const toAge = this.getAge(to)
if (to.gender === 'MALE') {
// 男性:哥哥(ob) 或 弟弟(lb)
return toAge > fromAge ? 'ob' : 'lb'
} else {
// 女性:姐姐(os) 或 妹妹(ls)
return toAge > fromAge ? 'os' : 'ls'
}
}
/**
* 获取年龄(用于比较)
*/
private getAge(member: FamilyMember): number {
if (!member.birthDate) return 0
const birthYear = new Date(member.birthDate).getFullYear()
return new Date().getFullYear() - birthYear
}
/**
* 获取关系的详细说明
*/
getRelationshipDetail(fromId: string, toId: string): RelationshipDetail {
const result = this.getRelationship(fromId, toId)
if (!result.success) {
return {
...result,
description: result.error || '无法计算关系'
}
}
// 构建带人名的路径(从 fromId 到 toId
const pathWithNames = this.buildPathWithNames(fromId, toId)
// 只返回关系称谓,不包含完整句子
return {
...result,
path: pathWithNames || result.path,
description: result.term
}
}
/**
* 构建带人名的路径描述
* @param fromId 起始ID(查询的起点)
* @param toId 目标ID(查询的终点)
*/
private buildPathWithNames(fromId: string, toId: string): string | null {
const queue: Array<{ id: string; path: Array<{name: string, relation: string}> }> = [
{ id: fromId, path: [] }
]
const visited = new Set<string>()
// 获取起始成员名字
const startMember = this.members.get(fromId)
const startName = startMember?.fullName || '未知'
while (queue.length > 0) {
const { id, path } = queue.shift()!
if (id === toId) {
// 构建路径字符串:从起点到终点
if (path.length === 0) return '本人'
// 格式:起点 → 关系1(人名1) → 关系2(人名2) → ...
const pathStr = path.map(p => `${p.relation}(${p.name})`).join(' → ')
return `${startName}${pathStr}`
}
if (visited.has(id)) continue
visited.add(id)
const member = this.members.get(id)
if (!member) continue
// 向上:父亲
if (member.fatherId && !visited.has(member.fatherId)) {
const father = this.members.get(member.fatherId)
if (father) {
queue.push({
id: member.fatherId,
path: [...path, { name: father.fullName, relation: '父亲' }]
})
}
}
// 向上:母亲
if (member.motherId && !visited.has(member.motherId)) {
const mother = this.members.get(member.motherId)
if (mother) {
queue.push({
id: member.motherId,
path: [...path, { name: mother.fullName, relation: '母亲' }]
})
}
}
// 配偶关系
if (member.spouseIds) {
for (const spouseId of member.spouseIds) {
if (!visited.has(spouseId)) {
const spouse = this.members.get(spouseId)
if (spouse) {
const relation = member.gender === 'MALE' ? '妻子' : '丈夫'
queue.push({
id: spouseId,
path: [...path, { name: spouse.fullName, relation }]
})
}
}
}
}
// 向下:子女
if (member.childrenIds) {
for (const childId of member.childrenIds) {
if (!visited.has(childId)) {
const child = this.members.get(childId)
if (child) {
const relation = child.gender === 'MALE' ? '儿子' : '女儿'
queue.push({
id: childId,
path: [...path, { name: child.fullName, relation }]
})
}
}
}
}
// 兄弟姐妹
if (member.fatherId || member.motherId) {
const siblings = this.getSiblings(id)
for (const siblingId of siblings) {
if (!visited.has(siblingId)) {
const sibling = this.members.get(siblingId)
if (sibling) {
const siblingPath = this.getSiblingRelationName(member, sibling)
queue.push({
id: siblingId,
path: [...path, { name: sibling.fullName, relation: siblingPath }]
})
}
}
}
}
}
return null
}
/**
* 获取兄弟姐妹关系名称
*/
private getSiblingRelationName(from: FamilyMember, to: FamilyMember): string {
const fromAge = this.getAge(from)
const toAge = this.getAge(to)
if (to.gender === 'MALE') {
return toAge > fromAge ? '哥哥' : '弟弟'
} else {
return toAge > fromAge ? '姐姐' : '妹妹'
}
}
/**
* 计算路径长度(关系代数)
*/
private calculatePathLength(path: string): number {
let length = 0
let i = 0
while (i < path.length) {
// 两字符的关系
if (i + 1 < path.length) {
const twoChar = path.substring(i, i + 2)
if (['ob', 'lb', 'os', 'ls'].includes(twoChar)) {
length++
i += 2
continue
}
}
// 单字符的关系
length++
i++
}
return length
}
/**
* 将符号路径转换为中文文本
* 例如:'fm' -> '父亲的母亲'
*/
private pathToChineseText(path: string): string {
const pathMap: Record<string, string> = {
'f': '父亲',
'm': '母亲',
'h': '丈夫',
'w': '妻子',
's': '儿子',
'd': '女儿',
'ob': '哥哥',
'lb': '弟弟',
'os': '姐姐',
'ls': '妹妹'
}
const parts: string[] = []
let i = 0
while (i < path.length) {
// 处理两字符的关系(如 ob, lb, os, ls
if (i + 1 < path.length) {
const twoChar = path.substring(i, i + 2)
if (pathMap[twoChar]) {
parts.push(pathMap[twoChar])
i += 2
continue
}
}
// 处理单字符的关系
const oneChar = path[i]
if (pathMap[oneChar]) {
parts.push(pathMap[oneChar])
}
i++
}
return parts.join('的')
}
/**
* 将路径转换为中文描述(带"的"字)
*/
private getPathDescription(path: string): string {
const pathMap: Record<string, string> = {
'f': '的父亲',
'm': '的母亲',
'h': '的丈夫',
'w': '的妻子',
's': '的儿子',
'd': '的女儿',
'ob': '的哥哥',
'lb': '的弟弟',
'os': '的姐姐',
'ls': '的妹妹'
}
let description = ''
for (const char of path) {
description += pathMap[char] || ''
}
return description || '本人'
}
}
export interface RelationshipResult {
term: string // 关系称谓(如:父亲、堂兄弟)
path: string // 关系路径(如:f, fbs
success: boolean // 是否成功计算
error?: string // 错误信息
}
export interface RelationshipDetail extends RelationshipResult {
description: string // 关系描述(如:父亲的哥哥的儿子)
}