471 lines
12 KiB
TypeScript
471 lines
12 KiB
TypeScript
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 // 关系描述(如:父亲的哥哥的儿子)
|
||
}
|