跨平台家族谱系管理应用 - 支持桌面端 (Tauri 2) 和移动端 (Capacitor 6)
项目概述
整体架构
技术栈详解
核心库架构
桌面端架构
移动端架构
数据结构设计
快速开始
构建部署
API 参考
KindredTree 是一个现代化的家族谱系管理应用,采用 Monorepo 架构,支持多平台部署:
平台
技术方案
输出格式
Windows
Tauri 2
.exe / .msi
macOS
Tauri 2
.dmg / .app
Linux
Tauri 2
.deb / .AppImage
Android
Capacitor 6
.apk
iOS
Capacitor 6
.ipa
🌳 交互式家族树 - D3.js 驱动的可视化
👨👩👧👦 完整关系支持 - 父母、配偶、子女、兄弟姐妹
💾 本地数据存储 - 数据完全存储在本地
📱 跨平台一致 - 共享核心代码,平台适配
🎨 现代化 UI - Tailwind CSS 样式
🔄 实时更新 - Zustand 响应式状态管理
┌─────────────────────────────────────────────────────────────────────────┐
│ KindredTree Monorepo │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ @kindred-tree/core (共享核心) │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │ │
│ │ │ types │ │ utils │ │ layout/kinship │ │ │
│ │ │ 类型定义 │ │ 工具函数 │ │ 布局计算/亲属关系 │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────┴───────────────┐ │
│ ▼ ▼ │
│ ┌─────────────────────────────┐ ┌─────────────────────────────────┐ │
│ │ @kindred-tree/desktop │ │ @kindred-tree/mobile │ │
│ │ (Tauri 桌面端) │ │ (Capacitor 移动端) │ │
│ │ │ │ │ │
│ │ ┌───────────────────────┐ │ │ ┌───────────────────────────┐ │ │
│ │ │ React 18 前端 │ │ │ │ React 18 前端 │ │ │
│ │ │ - Zustand 状态管理 │ │ │ │ - Zustand 状态管理 │ │ │
│ │ │ - D3.js 可视化 │ │ │ │ - D3.js 可视化 │ │ │
│ │ │ - Tailwind CSS │ │ │ │ - Tailwind CSS │ │ │
│ │ └───────────────────────┘ │ │ └───────────────────────────┘ │ │
│ │ │ │ │ │ │ │
│ │ ▼ │ │ ▼ │ │
│ │ ┌───────────────────────┐ │ │ ┌───────────────────────────┐ │ │
│ │ │ Tauri 2 后端 │ │ │ │ Capacitor 插件 │ │ │
│ │ │ - Rust 文件系统 │ │ │ │ - Filesystem │ │ │
│ │ │ - 本地存储 API │ │ │ │ - Preferences │ │ │
│ │ └───────────────────────┘ │ │ └───────────────────────────┘ │ │
│ │ │ │ │ │ │ │
│ │ ▼ │ │ ▼ │ │
│ │ ┌───────────────────────┐ │ │ ┌───────────────────────────┐ │ │
│ │ │ 操作系统原生 API │ │ │ │ 平台原生 API │ │ │
│ │ │ - Windows/macOS/Linux│ │ │ - Android/iOS │ │ │
│ │ └───────────────────────┘ │ │ └───────────────────────────┘ │ │
│ └─────────────────────────────┘ └─────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
技术
版本
用途
TypeScript
^5.3.0
类型安全开发
D3.js
^7.9.0
树形布局计算
UUID
^9.0.0
唯一标识生成
tsup
^8.0.0
库打包工具
桌面端 (@kindred-tree/desktop)
技术
版本
用途
Tauri
2.x
桌面应用框架
React
^18.2.0
UI 框架
Zustand
^4.5.0
状态管理
D3.js
^7.9.0
SVG 可视化
Tailwind CSS
^3.4.0
样式框架
Vite
^5.0.0
构建工具
Rust
1.70+
后端逻辑
移动端 (@kindred-tree/mobile)
技术
版本
用途
Capacitor
6.x
跨平台移动框架
React
^18.2.0
UI 框架
Zustand
^4.5.0
状态管理
D3.js
^7.9.0
SVG 可视化
Tailwind CSS
^3.4.0
样式框架
@capacitor/filesystem
^6.0.0
文件存储
@capacitor/preferences
^6.0.0
键值存储
packages/core/src/
├── types.ts # 类型定义
├── utils.ts # 工具函数
├── layout.ts # 树形布局计算
├── kinship.ts # 亲属关系计算
└── index.ts # 导出入口
// 核心类型定义
export type Gender = 'male' | 'female' ;
export type RelationType = 'biological' | 'adopted' | 'step' | 'married-in' ;
export type SpouseType = 'primary' | 'secondary' | 'concubine' ;
// 人员关系结构
export interface PersonRels {
fathers : { parentId : string ; relationType : RelationType } [ ] ;
mothers : { parentId : string ; relationType : RelationType } [ ] ;
spouses : { spouseId : string ; spouseType : SpouseType } [ ] ;
children : string [ ] ;
}
// 人员信息
export interface Person {
id : string ;
name : string ;
gender ?: Gender ;
birthDate ?: string ;
deathDate ?: string ;
rels : PersonRels ;
// ... 其他字段
}
// 树节点
export interface TreeNode {
id : string ;
x : number ;
y : number ;
depth : number ;
person : Person ;
spouse ?: Person ;
children : TreeNode [ ] ;
}
// 使用 D3.js 层次布局
export const calculateTreeLayout = (
persons : Person [ ] ,
mainPersonId : string ,
options : TreeLayoutOptions
) : TreeNode | null => {
// 1. 构建后代树
const root = buildNode ( mainPerson , 0 ) ;
// 2. 构建祖先树
const ancestors = buildAncestors ( mainPerson , persons ) ;
// 3. 合并树结构
if ( ancestors ) {
return mergeAncestorsWithTree ( ancestors , root ) ;
}
// 4. 应用 D3.js 布局
applyD3Layout ( root , options ) ;
return root ;
} ;
// D3.js 布局配置
const treeLayout = d3 . tree < any > ( )
. nodeSize ( [ nodeSeparation , levelSeparation ] )
. separation ( ( a , b ) => {
const aWidth = a . data . spouse ? 1.5 : 1 ;
const bWidth = b . data . spouse ? 1.5 : 1 ;
return ( aWidth + bWidth ) / 2 ;
} ) ;
// BFS 搜索最短路径
export const calculateKinship = (
personId : string ,
targetId : string ,
allPersons : Person [ ]
) : KinshipResult | null => {
// 1. BFS 查找最短路径
const path = findShortestPath ( personId , targetId , allPersons ) ;
// 2. 根据路径确定关系
const relation = determineKinship ( path , allPersons ) ;
return { relation, distance, path } ;
} ;
// 关系判断逻辑
const determineKinship = ( path : string [ ] , persons : Person [ ] ) : string => {
// 直系亲属判断
if ( path . length === 2 ) {
// 配偶、父母、子女、兄弟姐妹
}
// 旁系亲属判断
if ( path . length === 3 ) {
// 祖父母、孙辈
}
return '亲属' ;
} ;
┌─────────────────────────────────────────────────────────────┐
│ React 前端层 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ App.tsx │ │ FamilyTree │ │ PersonPanel │ │
│ │ 主应用 │ │ 树形组件 │ │ 人员面板 │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ 状态管理层 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Zustand Store │ │
│ │ families[] | persons[] | selectedPersonId | ... │ │
│ └─────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ Tauri 桥接层 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ @tauri-apps/api │ │
│ │ invoke() | fs | dialog | notification │ │
│ └─────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ Rust 后端层 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ lib.rs │ │
│ │ save_family | load_family | list_families | ... │ │
│ └─────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ 文件系统层 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ {DATA_DIR}/KindredTree/{family_id}.json │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
// 数据结构定义
#[ derive( Serialize , Deserialize ) ]
pub struct Person {
pub id : String ,
pub name : String ,
pub gender : Option < String > ,
pub rels : PersonRels ,
// ...
}
// Tauri 命令
#[ tauri:: command]
fn save_family (
state : State < Mutex < AppState > > ,
family : Family ,
persons : Vec < Person > ,
) -> Result < ( ) , String > {
let path = state. get_family_path ( & family. id ) ;
let data = FamilyData { family, persons } ;
let json = serde_json:: to_string_pretty ( & data) ?;
fs:: write ( path, json) ?;
Ok ( ( ) )
}
// 应用入口
pub fn run ( ) {
tauri:: Builder :: default ( )
. plugin ( tauri_plugin_fs:: init ( ) )
. plugin ( tauri_plugin_dialog:: init ( ) )
. manage ( Mutex :: new ( AppState :: new ( ) ) )
. invoke_handler ( tauri:: generate_handler![
save_family,
load_family,
list_families,
delete_family
] )
. run ( tauri:: generate_context!( ) )
. expect ( "error while running tauri application" ) ;
}
// Zustand Store
export const useFamilyStore = create < FamilyState > ( ( set , get ) => ( {
families : [ ] ,
persons : [ ] ,
selectedPersonId : null ,
// 创建族谱
createNewFamily : async ( name ) => {
const family = createFamily ( { name } ) ;
const mainPerson = createPerson ( { name : '本人' , gender : 'male' } ) ;
await invoke ( 'save_family' , { family, persons : [ mainPerson ] } ) ;
set ( { families : [ ...get ( ) . families , family ] , persons : [ mainPerson ] } ) ;
} ,
// 添加亲属
addSpouse : ( personId , spouseData ) => {
const spouse = createPerson ( spouseData ) ;
// 更新双方关系...
get ( ) . saveFamily ( ) ;
} ,
} ) ) ;
// D3.js 可视化
const FamilyTree = ( ) => {
const { persons, currentFamily } = useFamilyStore ( ) ;
useEffect ( ( ) => {
const root = calculateTreeLayout ( persons , currentFamily . mainPersonId , options ) ;
// D3.js 渲染
const svg = d3 . select ( svgRef . current ) ;
const container = svg . append ( 'g' ) ;
// 绘制连接线
container . selectAll ( '.link' )
. data ( getLinks ( root ) )
. enter ( )
. append ( 'path' ) ;
// 绘制节点
container . selectAll ( '.node' )
. data ( getAllNodes ( root ) )
. enter ( )
. append ( 'g' )
. each ( renderCard ) ;
} , [ persons ] ) ;
} ;
┌─────────────────────────────────────────────────────────────┐
│ React 前端层 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ App.tsx │ │ FamilyTree │ │ PersonSheet │ │
│ │ 主应用 │ │ 树形组件 │ │ 底部弹出面板 │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ 状态管理层 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Zustand Store │ │
│ └─────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ 存储适配层 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ storage.ts │ │
│ │ saveFamily | loadFamily | listFamilies | ... │ │
│ └─────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ Capacitor 插件层 │
│ ┌──────────────────┐ ┌───────────────────────────────┐ │
│ │ Filesystem │ │ Preferences │ │
│ │ 文件存储 │ │ 键值存储 │ │
│ └──────────────────┘ └───────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ 平台原生层 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Android: Internal Storage │ │
│ │ iOS: App Documents Directory │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
import { Filesystem , Directory , Encoding } from '@capacitor/filesystem' ;
import { Preferences } from '@capacitor/preferences' ;
export const storage = {
// 保存族谱
async saveFamily ( family : Family , persons : Person [ ] ) : Promise < void > {
const fileName = `family_${ family . id } .json` ;
const data = JSON . stringify ( { family, persons } , null , 2 ) ;
// 写入文件
await Filesystem . writeFile ( {
path : fileName ,
data : data ,
directory : Directory . Data ,
encoding : Encoding . UTF8 ,
} ) ;
// 更新索引
const { value } = await Preferences . get ( { key : FAMILY_LIST_KEY } ) ;
const familyList : string [ ] = value ? JSON . parse ( value ) : [ ] ;
if ( ! familyList . includes ( family . id ) ) {
familyList . push ( family . id ) ;
await Preferences . set ( {
key : FAMILY_LIST_KEY ,
value : JSON . stringify ( familyList ) ,
} ) ;
}
} ,
// 加载族谱
async loadFamily ( id : string ) : Promise < FamilyData | null > {
const result = await Filesystem . readFile ( {
path : `family_${ id } .json` ,
directory : Directory . Data ,
encoding : Encoding . UTF8 ,
} ) ;
return JSON . parse ( result . data as string ) ;
} ,
} ;
// 底部弹出面板
const PersonSheet : React . FC < PersonSheetProps > = ( { person, onClose } ) => {
return (
< div className = "fixed inset-0 bg-black/50 z-50" onClick = { onClose } >
< div
className = "absolute bottom-0 left-0 right-0 bg-white rounded-t-2xl p-4 safe-area-bottom"
onClick = { e => e . stopPropagation ( ) }
>
{ /* 表单内容 */ }
< / div >
< / div >
) ;
} ;
// 安全区域适配
. safe - area - bottom {
padding - bottom : env ( safe - area - inset - bottom ) ;
}
┌─────────────────────────────────────────────────────────────┐
│ Person │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ id: string // 唯一标识 │ │
│ │ name: string // 姓名 │ │
│ │ gender: 'male' | 'female' // 性别 │ │
│ │ birthDate?: string // 出生日期 │ │
│ │ deathDate?: string // 逝世日期 │ │
│ │ ... │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ rels │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ fathers: [{ parentId, relationType }] │ │ │
│ │ │ mothers: [{ parentId, relationType }] │ │ │
│ │ │ spouses: [{ spouseId, spouseType }] │ │ │
│ │ │ children: [id1, id2, ...] │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
// 父母关系类型
type RelationType =
| 'biological' // 亲生
| 'adopted' // 收养
| 'step' // 继父母
| 'married-in' ; // 入赘/嫁入
// 配偶类型
type SpouseType =
| 'primary' // 元配
| 'secondary' // 继室
| 'concubine' ; // 侧室
{
"family" : {
"id" : " family-001" ,
"name" : " 张氏家族" ,
"mainPersonId" : " person-001" ,
"settings" : {
"orientation" : " vertical" ,
"showImages" : true ,
"cardWidth" : 200 ,
"cardHeight" : 85
}
},
"persons" : [
{
"id" : " person-001" ,
"name" : " 张三" ,
"gender" : " male" ,
"birthDate" : " 1980-01-01" ,
"rels" : {
"fathers" : [],
"mothers" : [],
"spouses" : [{ "spouseId" : " person-002" , "spouseType" : " primary" }],
"children" : [" person-003" ]
}
}
]
}
Node.js >= 18.0.0
pnpm >= 8.0.0
Rust >= 1.70 (桌面端)
Android Studio (Android 开发)
Xcode >= 15 (iOS 开发,需要 Mac)
# 安装 pnpm
npm install -g pnpm
# 进入项目目录
cd KindredTree
# 安装依赖
pnpm install
# 开发桌面端
pnpm tauri:dev
# 开发移动端
pnpm dev:mobile
# 构建所有平台
pnpm tauri:build
# 仅构建当前平台
cd apps/desktop
pnpm tauri:build
# 输出位置
# Windows: src-tauri/target/release/kindred-tree-desktop.exe
# macOS: src-tauri/target/release/bundle/dmg/
# Linux: src-tauri/target/release/bundle/deb/
# Android
cd apps/mobile
pnpm build
npx cap sync android
npx cap open android
# 在 Android Studio 中构建 APK
# iOS
pnpm build
npx cap sync ios
npx cap open ios
# 在 Xcode 中构建 IPA
import {
// 类型
Person , Family , TreeNode , KinshipResult ,
// 工厂函数
createPerson , createFamily ,
// 布局计算
calculateTreeLayout , getLinks , getAllNodes ,
// 亲属关系
calculateKinship , getAllKinships ,
// 工具函数
getParents , getChildren , getSpouses , getSiblings ,
getGenerationTitle , exportToJson , importFromJson ,
} from '@kindred-tree/core' ;
import { invoke } from '@tauri-apps/api/core' ;
// 保存族谱
await invoke ( 'save_family' , { family, persons } ) ;
// 加载族谱
const data = await invoke ( 'load_family' , { id } ) ;
// 列出所有族谱
const families = await invoke ( 'list_families' ) ;
// 删除族谱
await invoke ( 'delete_family' , { id } ) ;
import { storage } from './utils/storage' ;
// 保存族谱
await storage . saveFamily ( family , persons ) ;
// 加载族谱
const data = await storage . loadFamily ( id ) ;
// 列出所有族谱
const families = await storage . listFamilies ( ) ;
// 删除族谱
await storage . deleteFamily ( id ) ;
// 导出/导入
const json = await storage . exportFamily ( id ) ;
const family = await storage . importFamily ( json ) ;
MIT License
初始版本发布
支持桌面端 (Windows/macOS/Linux)
支持移动端 (Android/iOS)
核心功能:家族树可视化、人员管理、关系计算