"use client" import { SiteHeader } from "@/components/site-header" import { useFamily } from "@/context/family-context" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { ScrollArea } from "@/components/ui/scroll-area" import { Clock, Map as MapIcon, ArrowRight, Calendar } from "lucide-react" import dynamic from 'next/dynamic' import { useMemo, useEffect, useState } from 'react' import * as echarts from 'echarts' const ReactECharts = dynamic(() => import('echarts-for-react'), { ssr: false }) // 中国地图 GeoJSON 数据 - 完整的轮廓 const chinaGeoJSON = { type: "FeatureCollection", features: [ { type: "Feature", properties: { name: "中国" }, geometry: { type: "MultiPolygon", coordinates: [ [ [ // 中国大陆完整轮廓 - 从东北开始逆时针 [134.5, 48.5], [135, 47], [134, 45], [133, 43], [131, 42], [130, 43], [129, 44], [128, 45], [127, 46], [126, 47], [125, 48], [124, 49], [123.5, 50], [123, 51], [122.5, 52], [122, 53], [121, 53.5], [120, 53.3], [119, 53], [118.5, 52], [119, 51], [119.5, 50], [120, 49], [121, 48], [122, 47], [123, 46], [124, 45], [125, 44], [126, 43], [127, 42.5], [128, 42], [129, 41.5], [130, 41], // 东部海岸线 [130, 40], [129, 39], [128, 38.5], [127, 38], [126, 37.5], [125, 37], [124, 36.5], [123, 36], [122.5, 35.5], [122, 35], [121.5, 34.5], [121, 34], [120.5, 33.5], [120, 33], [119.5, 32.5], [119.5, 32], [120, 31.5], [120.5, 31], [121, 30.5], [121.5, 30], [122, 29.5], [122, 29], [121.5, 28.5], [121, 28], [120.5, 27.5], [120, 27], [119.5, 26.5], [119, 26], [118.5, 25.5], [118, 25], [117.5, 24.5], [117, 24], [116.5, 23.5], [116, 23], [115.5, 22.5], [115, 22], [114, 21.8], [113, 21.5], [112, 21.3], [111, 21.2], [110.5, 21], // 南部边界 [110, 20.5], [109.5, 20], [109, 20.5], [108.5, 21], [108, 21.5], [107.5, 22], [107, 22.5], [106.5, 23], [106, 23.5], [105.5, 24], [105, 24.5], [104.5, 25], [104, 25.5], [103.5, 26], [103, 26.5], [102.5, 27], [102, 27.5], [101.5, 28], [101, 28.5], [100.5, 29], [100, 29.5], [99.5, 30], [99, 30.5], [98.5, 31], [98, 31.5], [97.5, 32], [97, 32.5], // 西南边界 [96.5, 33], [96, 33.5], [95.5, 34], [95, 34.5], [94.5, 35], [94, 35.5], [93.5, 36], [93, 36.5], [92.5, 37], [92, 37.5], [91.5, 38], [91, 38.5], [90.5, 39], [90, 39.5], [89.5, 40], [89, 40.5], [88.5, 41], [88, 41.5], [87.5, 42], [87, 42.5], [86.5, 43], [86, 43.5], [85.5, 44], [85, 44.5], [84.5, 45], [84, 45.5], [83.5, 46], [83, 46.5], [82.5, 47], [82, 47.5], [81.5, 48], [81, 48.5], [80.5, 49], [80, 49.5], // 西北边界 [79.5, 49.5], [79, 49.3], [78.5, 49], [78, 48.5], [77.5, 48], [77, 47.5], [76.5, 47], [76, 46.5], [75.5, 46], [75, 45.5], [74.5, 45], [74, 44.5], [73.5, 44], [73.5, 43.5], [74, 43], [74.5, 42.5], [75, 42], [75.5, 41.5], [76, 41], [76.5, 40.5], [77, 40], [77.5, 39.5], [78, 39], [78.5, 38.5], [79, 38.5], [79.5, 39], [80, 39.5], [80.5, 40], [81, 40.5], [81.5, 41], [82, 41.5], [82.5, 42], [83, 42.5], [83.5, 43], [84, 43.5], [84.5, 44], [85, 44.5], [85.5, 45], [86, 45.5], [86.5, 46], [87, 46.5], [87.5, 47], [88, 47.5], [88.5, 48], [89, 48.5], [89.5, 49], [90, 49.5], [90.5, 50], [91, 50.5], [91.5, 51], [92, 51.5], [92.5, 52], [93, 52.5], [93.5, 53], // 北部边界 [94, 53], [95, 52.8], [96, 52.5], [97, 52.2], [98, 52], [99, 51.8], [100, 51.5], [101, 51.2], [102, 51], [103, 50.8], [104, 50.5], [105, 50.2], [106, 50], [107, 49.8], [108, 49.5], [109, 49.3], [110, 49.2], [111, 49.1], [112, 49], [113, 48.9], [114, 48.8], [115, 48.7], [116, 48.6], [117, 48.5], [118, 48.5], [119, 48.5], [120, 48.6], [121, 48.7], [122, 48.8], [123, 48.9], [124, 49], [125, 49.1], [126, 49.2], [127, 49.3], [128, 49.4], [129, 49.5], [130, 49.5], [131, 49.4], [132, 49.2], [133, 49], [134, 48.8], [134.5, 48.5] ] ], // 海南岛 [[[108.6, 18.5], [109.5, 18.3], [110.5, 18.5], [111, 19], [111, 19.5], [110.5, 20], [109.5, 20.2], [108.6, 20], [108.2, 19.5], [108.6, 18.5]]], // 台湾岛 [[[120, 22], [120.5, 21.9], [121, 22], [121.5, 22.5], [122, 23], [122, 23.5], [122, 24], [121.8, 24.5], [121.5, 25], [121, 25.2], [120.5, 25.1], [120.2, 24.8], [120, 24.5], [120, 24], [120, 23.5], [120, 23], [120, 22.5], [120, 22]]] ] } } ] } // 主要城市坐标(包含1、2、3线城市) const geoCoordMap: Record = { // 一线城市 '北京': [116.46, 39.92], '上海': [121.48, 31.22], '广州': [113.23, 23.16], '深圳': [114.07, 22.62], // 新一线城市 '成都': [104.06, 30.67], '杭州': [120.19, 30.26], '重庆': [106.55, 29.56], '西安': [108.95, 34.27], '苏州': [120.62, 31.32], '武汉': [114.31, 30.52], '南京': [118.78, 32.04], '天津': [117.20, 39.13], '郑州': [113.65, 34.76], '长沙': [112.94, 28.23], '东莞': [113.75, 23.05], '沈阳': [123.43, 41.80], '青岛': [120.38, 36.07], '合肥': [117.27, 31.86], '佛山': [113.12, 23.02], // 二线城市 '昆明': [102.73, 25.04], '福州': [119.30, 26.08], '无锡': [120.29, 31.57], '厦门': [118.10, 24.46], '哈尔滨': [126.63, 45.75], '长春': [125.35, 43.88], '南昌': [115.89, 28.68], '济南': [117.12, 36.65], '宁波': [121.55, 29.87], '大连': [121.62, 38.91], '贵阳': [106.71, 26.57], '温州': [120.67, 28.00], '石家庄': [114.52, 38.05], '泉州': [118.58, 24.93], '南宁': [108.33, 22.84], '金华': [119.65, 29.08], '常州': [119.95, 31.79], '珠海': [113.52, 22.30], '惠州': [114.42, 23.09], '嘉兴': [120.76, 30.77], '南通': [120.86, 32.01], '中山': [113.38, 22.52], '保定': [115.48, 38.85], '太原': [112.55, 37.87], '兰州': [103.79, 36.06], '台州': [121.43, 28.66], // 三线城市 '乌鲁木齐': [87.68, 43.77], '绍兴': [120.58, 30.03], '廊坊': [116.70, 39.54], '洛阳': [112.45, 34.62], '威海': [122.12, 37.52], '盐城': [120.13, 33.38], '临沂': [118.35, 35.05], '江门': [113.08, 22.61], '汕头': [116.69, 23.39], '泰州': [119.90, 32.49], '漳州': [117.65, 24.52], '邯郸': [114.47, 36.60], '济宁': [116.59, 35.38], '芜湖': [118.38, 31.33], '淄博': [118.05, 36.78], '银川': [106.27, 38.47], '柳州': [109.40, 24.33], '绵阳': [104.73, 31.48], '湖州': [120.10, 30.86], '衡阳': [112.61, 26.89], '莆田': [119.01, 25.43], '宜昌': [111.29, 30.70], '桂林': [110.28, 25.29], '三亚': [109.51, 18.25], '遵义': [106.93, 27.71], '咸阳': [108.71, 34.33], '上饶': [117.97, 28.45], '莱芜': [117.68, 36.21], '赣州': [114.94, 25.85], '揭阳': [116.35, 23.55], '扬州': [119.42, 32.39], '菏泽': [115.47, 35.23], '徐州': [117.20, 34.26], '肇庆': [112.47, 23.05], '海口': [110.33, 20.02], '拉萨': [91.13, 29.66], '呼和浩特': [111.75, 40.84], '包头': [109.84, 40.66], '南阳': [112.53, 33.00], '鞍山': [122.99, 41.12], '九江': [115.99, 29.71], '大庆': [125.11, 46.59], '平顶山': [113.31, 33.74], '抚顺': [123.92, 41.88], '秦皇岛': [119.57, 39.95], '株洲': [113.15, 27.83], '齐齐哈尔': [123.95, 47.33] } export default function TimelinePage() { const { treeData, getMember, isLoading } = useFamily() const members = Object.values(treeData.members) const [mapLoaded, setMapLoaded] = useState(false) // 注册中国地图 - 从外部文件加载完整数据(必须在条件渲染之前) useEffect(() => { fetch('/china.json') .then(response => response.json()) .then(geoJson => { echarts.registerMap('china', geoJson) setMapLoaded(true) }) .catch(error => { console.error('Failed to load China map:', error) // 如果加载失败,使用简化版本 echarts.registerMap('china', chinaGeoJSON as any) setMapLoaded(true) }) }, []) // -- Timeline Logic -- (必须在条件渲染之前) // Flatten events: Births and Deaths const events = useMemo(() => members .flatMap((m) => { const evs = [] if (m.birthDate) { evs.push({ date: m.birthDate, year: Number.parseInt(m.birthDate.substring(0, 4)), type: "birth", member: m, }) } if (m.deathDate) { evs.push({ date: m.deathDate, year: Number.parseInt(m.deathDate.substring(0, 4)), type: "death", member: m, }) } return evs }) .sort((a, b) => b.date.localeCompare(a.date)), [members]) // 倒序排列:最新的在上面 // -- Migration Logic -- // Find moves between generations (Father -> Child location change) const migrations = useMemo(() => members .flatMap((m) => { if (!m.fatherId) return [] const father = getMember(m.fatherId) if (!father) return [] // Check if location exists and is different // 优先使用 birthPlace(出生地)来判断迁徙,因为 ancestralHome(祖籍)通常不变 const loc1 = father.birthPlace || father.ancestralHome const loc2 = m.birthPlace || m.ancestralHome if (loc1 && loc2 && loc1 !== loc2) { return [ { year: m.birthDate ? Number.parseInt(m.birthDate.substring(0, 4)) : null, from: loc1, to: loc2, person: m, father: father, }, ] } return [] }) .sort((a, b) => (a.year || 0) - (b.year || 0)), [members, getMember]) // ECharts 地图配置 const mapOption = useMemo(() => { // 城市坐标映射(简化城市名) // 城市名称映射(支持省+市格式) const cityMap: Record = {} // 自动生成映射:将"XX省XX市"或"XX市"映射到城市名 Object.keys(geoCoordMap).forEach(city => { cityMap[city] = city // 直接城市名 cityMap[`${city}市`] = city // 城市+市 // 根据城市所在省份添加映射 const provinceMap: Record = { '广东省': ['广州', '深圳', '东莞', '佛山', '珠海', '惠州', '中山', '江门', '汕头', '肇庆'], '浙江省': ['杭州', '宁波', '温州', '金华', '嘉兴', '台州', '绍兴', '湖州'], '江苏省': ['苏州', '南京', '无锡', '常州', '南通', '盐城', '泰州', '扬州', '徐州'], '山东省': ['济南', '青岛', '威海', '临沂', '济宁', '淄博', '莱芜', '菏泽'], '四川省': ['成都', '绵阳'], '河北省': ['石家庄', '保定', '廊坊', '邯郸', '秦皇岛'], '福建省': ['福州', '厦门', '泉州', '莆田', '漳州'], '湖北省': ['武汉', '宜昌'], '湖南省': ['长沙', '衡阳', '株洲'], '陕西省': ['西安', '咸阳'], '河南省': ['郑州', '洛阳', '南阳', '平顶山'], '辽宁省': ['沈阳', '大连', '鞍山', '抚顺'], '安徽省': ['合肥', '芜湖'], '云南省': ['昆明'], '贵州省': ['贵阳', '遵义'], '广西壮族自治区': ['南宁', '柳州', '桂林'], '江西省': ['南昌', '九江', '赣州', '上饶'], '黑龙江省': ['哈尔滨', '大庆', '齐齐哈尔'], '吉林省': ['长春'], '山西省': ['太原'], '甘肃省': ['兰州'], '新疆维吾尔自治区': ['乌鲁木齐'], '宁夏回族自治区': ['银川'], '内蒙古自治区': ['呼和浩特', '包头'], '海南省': ['海口', '三亚'], '西藏自治区': ['拉萨'], '天津市': ['天津'], '重庆市': ['重庆'], '北京市': ['北京'], '上海市': ['上海'] } // 为每个城市添加"省+市"格式的映射 Object.entries(provinceMap).forEach(([province, cities]) => { if (cities.includes(city)) { cityMap[`${province}${city}市`] = city cityMap[`${province}${city}`] = city } }) }) // 提取城市名称的辅助函数(去掉区的部分) const extractCity = (location: string): string => { // 尝试从 cityMap 中查找 let city = cityMap[location] if (city && geoCoordMap[city]) return city // 如果没找到,尝试提取省市部分(去掉区) // 例如:"山东省济南市历下区" -> "济南" const match = location.match(/(.+?省)?(.+?市)/) if (match) { const province = match[1] || '' const cityName = match[2] || '' // 尝试多种组合 const attempts = [ location, province + cityName, cityName.replace('市', ''), cityName ] for (const attempt of attempts) { const mapped = cityMap[attempt] if (mapped && geoCoordMap[mapped]) return mapped } } return location } // 转换迁徙数据为 ECharts 格式(使用经纬度坐标) const lines = migrations.map(mig => { const fromCity = extractCity(mig.from) const toCity = extractCity(mig.to) const fromCoord = geoCoordMap[fromCity] const toCoord = geoCoordMap[toCity] if (!fromCoord || !toCoord) { console.log('坐标未找到:', { from: mig.from, to: mig.to, fromCity, toCity, fromCoord, toCoord }) return null } return { fromName: fromCity, toName: toCity, coords: [fromCoord, toCoord] } }).filter(Boolean) console.log('迁徙数据:', { migrations, lines }) // 统计每个城市的迁入迁出 const cityData: Record = {} migrations.forEach(mig => { const from = extractCity(mig.from) const to = extractCity(mig.to) cityData[from] = (cityData[from] || 0) + 1 cityData[to] = (cityData[to] || 0) + 1 }) const scatterData = Object.entries(cityData).map(([name, value]) => { const coord = geoCoordMap[name] if (!coord) return null return { name, value: [...coord, value * 10] } }).filter(Boolean) return { backgroundColor: 'transparent', geo: { map: 'china', roam: true, label: { show: true, color: '#666', fontSize: 12 }, itemStyle: { areaColor: 'rgba(128, 128, 128, 0.15)', borderColor: 'rgba(128, 128, 128, 0.5)', borderWidth: 2 }, emphasis: { itemStyle: { areaColor: 'rgba(128, 128, 128, 0.25)' }, label: { show: true, color: '#333', fontSize: 14 } } }, series: [ { type: 'lines', coordinateSystem: 'geo', data: lines, lineStyle: { color: 'transparent', width: 0, opacity: 0 }, effect: { show: true, period: 3, trailLength: 0.2, symbol: 'arrow', symbolSize: 12, color: '#3b82f6', loop: true }, zlevel: 1 }, { type: 'scatter', coordinateSystem: 'geo', data: scatterData, symbolSize: 10, label: { show: true, formatter: '{b}', position: 'right', fontSize: 12, fontWeight: 'normal', color: '#333' }, itemStyle: { color: '#000', shadowBlur: 0, borderWidth: 0 }, emphasis: { scale: true, itemStyle: { color: '#000' }, label: { show: true, fontSize: 14, fontWeight: 'bold' } } } ] } }, [migrations]) // 显示加载状态 if (isLoading) { return (

加载中...

) } // 显示无数据状态 if (members.length === 0) { return (

时光未启

) } return (

家族历史 (History)

纵览家族发展的时间脉络与迁徙足迹。

时间轴 (Chronology) 迁徙图 (Migration)
{events.map((event, idx) => (
{/* Dot */}
{/* Year Label (Mobile vs Desktop) */}
{event.year}
{/* Content Card */}
{event.type === "birth" ? "诞生" : "逝世"} {event.member.fullName} {event.member.gender === "MALE" ? "男" : "女"} 第{event.member.generation}世 {/* 父母信息 */} {event.member.fatherId && ( 父:{getMember(event.member.fatherId)?.fullName || "未知"} )} {event.member.motherId && ( 母:{getMember(event.member.motherId)?.fullName || "未知"} )} {/* 配偶信息 */} {event.member.spouseIds && event.member.spouseIds.length > 0 && ( 配偶:{getMember(event.member.spouseIds[0])?.fullName || "未知"} )} {/* 子女信息 */} {event.member.childrenIds && event.member.childrenIds.length > 0 && ( 子女:{event.member.childrenIds.length}人 )} {/* 出生地信息 */} {event.type === "birth" && (event.member.birthPlace || event.member.ancestralHome) && ( 📍{event.member.birthPlace || event.member.ancestralHome} )} {/* 享年 */} {event.type === "death" && event.member.birthDate && ( 享年{event.year - Number.parseInt(event.member.birthDate.substring(0, 4))}岁 )} {/* 葬地 */} {event.type === "death" && event.member.burialPlace && ( ⚰️{event.member.burialPlace} )} {event.date}
))} {events.length === 0 && (

时光留白

)}
迁徙记录 (Records)
{migrations.map((mig, idx) => (
{mig.year ? `${mig.year}年` : "未知年代"} 第 {mig.person.generation} 世
迁出地
{mig.from}
{mig.father.fullName}
迁入地
{mig.to}
{mig.person.fullName}
))} {migrations.length === 0 && (

足迹未显

)}
迁徙路径图 (Migration Flow)
{!mapLoaded ? (

加载地图中...

) : migrations.length === 0 ? (

山河未绘

) : ( )}
) }