1331 lines
50 KiB
TypeScript
1331 lines
50 KiB
TypeScript
"use client"
|
||
import { SiteHeader } from "@/components/site-header"
|
||
import { useFamily } from "@/context/family-context"
|
||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||
import { ChineseCard, ChineseCardContent, ChineseCardHeader, ChineseCardTitle } from "@/components/ui/chinese-card"
|
||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||
import { Clock, Map as MapIcon, ArrowRight, Calendar, User, Loader2 } from 'lucide-react'
|
||
import { useMemo, useEffect, useState, useRef, useCallback } from 'react'
|
||
import { MemberNameWithStatus } from "@/components/member-name-with-status"
|
||
import { useSession } from 'next-auth/react'
|
||
import { EmptyStateBadge } from '@/components/empty-state-badge'
|
||
import { useRouter } from 'next/navigation'
|
||
import type { ECharts } from 'echarts'
|
||
|
||
// 中国地图 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<string, [number, number]> = {
|
||
// 一线城市
|
||
'北京': [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],
|
||
|
||
// 国际城市 - 澳大利亚
|
||
'悉尼': [151.21, -33.87],
|
||
'墨尔本': [144.96, -37.81],
|
||
'布里斯班': [153.03, -27.47],
|
||
'珀斯': [115.86, -31.95],
|
||
'阿德莱德': [138.60, -34.93],
|
||
'堪培拉': [149.13, -35.28],
|
||
'黄金海岸': [153.43, -28.00],
|
||
'Sydney': [151.21, -33.87],
|
||
'Melbourne': [144.96, -37.81],
|
||
'Brisbane': [153.03, -27.47],
|
||
'Perth': [115.86, -31.95],
|
||
'Adelaide': [138.60, -34.93],
|
||
'Canberra': [149.13, -35.28],
|
||
|
||
// 国际城市 - 美国主要城市
|
||
'纽约': [-74.01, 40.71],
|
||
'洛杉矶': [-118.24, 34.05],
|
||
'旧金山': [-122.42, 37.77],
|
||
'西雅图': [-122.33, 47.61],
|
||
'芝加哥': [-87.63, 41.88],
|
||
'波士顿': [-71.06, 42.36],
|
||
'华盛顿': [-77.04, 38.91],
|
||
'迈阿密': [-80.19, 25.76],
|
||
'休斯顿': [-95.37, 29.76],
|
||
'达拉斯': [-96.80, 32.78],
|
||
'费城': [-75.17, 39.95],
|
||
'凤凰城': [-112.07, 33.45],
|
||
'圣安东尼奥': [-98.49, 29.42],
|
||
'圣地亚哥': [-117.16, 32.72],
|
||
'丹佛': [-104.99, 39.74],
|
||
'拉斯维加斯': [-115.14, 36.17],
|
||
'波特兰': [-122.68, 45.52],
|
||
'亚特兰大': [-84.39, 33.75],
|
||
'底特律': [-83.05, 42.33],
|
||
'明尼阿波利斯': [-93.27, 44.98],
|
||
'奥斯汀': [-97.74, 30.27],
|
||
'夏洛特': [-80.84, 35.23],
|
||
'印第安纳波利斯': [-86.16, 39.77],
|
||
'哥伦布': [-82.99, 39.96],
|
||
'盐湖城': [-111.89, 40.76],
|
||
'堪萨斯城': [-94.58, 39.10],
|
||
'圣路易斯': [-90.20, 38.63],
|
||
'匹兹堡': [-79.99, 40.44],
|
||
'辛辛那提': [-84.51, 39.10],
|
||
'克利夫兰': [-81.69, 41.50],
|
||
'奥兰多': [-81.38, 28.54],
|
||
'坦帕': [-82.46, 27.95],
|
||
'新奥尔良': [-90.07, 29.95],
|
||
'纳什维尔': [-86.78, 36.16],
|
||
'孟菲斯': [-90.05, 35.15],
|
||
'巴尔的摩': [-76.61, 39.29],
|
||
'密尔沃基': [-87.91, 43.04],
|
||
'阿尔伯克基': [-106.65, 35.08],
|
||
'图森': [-110.93, 32.22],
|
||
'檀香山': [-157.86, 21.31],
|
||
'安克雷奇': [-149.90, 61.22],
|
||
|
||
// 美国各州首府
|
||
'杰克逊': [-90.18, 32.30], // 密西西比州首府
|
||
'蒙哥马利': [-86.30, 32.38], // 阿拉巴马州首府
|
||
'小石城': [-92.29, 34.75], // 阿肯色州首府
|
||
'萨克拉门托': [-121.49, 38.58], // 加州首府
|
||
'哈特福德': [-72.68, 41.76], // 康涅狄格州首府
|
||
'多佛': [-75.52, 39.16], // 特拉华州首府
|
||
'塔拉哈西': [-84.28, 30.44], // 佛罗里达州首府
|
||
'博伊西': [-116.20, 43.62], // 爱达荷州首府
|
||
'斯普林菲尔德': [-89.64, 39.80], // 伊利诺伊州首府
|
||
'得梅因': [-93.62, 41.59], // 艾奥瓦州首府
|
||
'托皮卡': [-95.68, 39.05], // 堪萨斯州首府
|
||
'法兰克福肯塔基': [-84.87, 38.20], // 肯塔基州首府
|
||
'巴吞鲁日': [-91.15, 30.45], // 路易斯安那州首府
|
||
'奥古斯塔': [-69.77, 44.31], // 缅因州首府
|
||
'安纳波利斯': [-76.49, 38.98], // 马里兰州首府
|
||
'兰辛': [-84.55, 42.73], // 密歇根州首府
|
||
'圣保罗明尼苏达': [-93.09, 44.94], // 明尼苏达州首府
|
||
'杰斐逊城': [-92.17, 38.58], // 密苏里州首府
|
||
'海伦娜': [-112.03, 46.59], // 蒙大拿州首府
|
||
'林肯': [-96.70, 40.81], // 内布拉斯加州首府
|
||
'卡森城': [-119.77, 39.16], // 内华达州首府
|
||
'康科德': [-71.54, 43.21], // 新罕布什尔州首府
|
||
'特伦顿': [-74.76, 40.22], // 新泽西州首府
|
||
'圣菲': [-105.94, 35.69], // 新墨西哥州首府
|
||
'奥尔巴尼': [-73.76, 42.65], // 纽约州首府
|
||
'罗利': [-78.64, 35.78], // 北卡罗来纳州首府
|
||
'俾斯麦': [-100.79, 46.81], // 北达科他州首府
|
||
'俄克拉荷马城': [-97.52, 35.47], // 俄克拉荷马州首府
|
||
'塞勒姆': [-123.03, 44.94], // 俄勒冈州首府
|
||
'哈里斯堡': [-76.88, 40.27], // 宾夕法尼亚州首府
|
||
'普罗维登斯': [-71.41, 41.82], // 罗德岛州首府
|
||
'哥伦比亚': [-81.03, 34.00], // 南卡罗来纳州首府
|
||
'皮尔': [-100.35, 44.37], // 南达科他州首府
|
||
'里士满': [-77.43, 37.54], // 弗吉尼亚州首府
|
||
'奥林匹亚': [-122.90, 47.04], // 华盛顿州首府
|
||
'查尔斯顿': [-81.63, 38.35], // 西弗吉尼亚州首府
|
||
'麦迪逊': [-89.40, 43.07], // 威斯康星州首府
|
||
'夏延': [-104.82, 41.14], // 怀俄明州首府
|
||
|
||
// 美国州名映射(使用州内最大城市)
|
||
'密西西比': [-90.18, 32.30], // 杰克逊
|
||
'密西西比州': [-90.18, 32.30],
|
||
'Mississippi': [-90.18, 32.30],
|
||
'加利福尼亚': [-118.24, 34.05], // 洛杉矶
|
||
'加利福尼亚州': [-118.24, 34.05],
|
||
'California': [-118.24, 34.05],
|
||
'德克萨斯': [-95.37, 29.76], // 休斯顿
|
||
'德克萨斯州': [-95.37, 29.76],
|
||
'Texas': [-95.37, 29.76],
|
||
'佛罗里达': [-80.19, 25.76], // 迈阿密
|
||
'佛罗里达州': [-80.19, 25.76],
|
||
'Florida': [-80.19, 25.76],
|
||
'伊利诺伊': [-87.63, 41.88], // 芝加哥
|
||
'伊利诺伊州': [-87.63, 41.88],
|
||
'Illinois': [-87.63, 41.88],
|
||
'宾夕法尼亚': [-75.17, 39.95], // 费城
|
||
'宾夕法尼亚州': [-75.17, 39.95],
|
||
'Pennsylvania': [-75.17, 39.95],
|
||
'俄亥俄': [-82.99, 39.96], // 哥伦布
|
||
'俄亥俄州': [-82.99, 39.96],
|
||
'Ohio': [-82.99, 39.96],
|
||
'乔治亚': [-84.39, 33.75], // 亚特兰大
|
||
'乔治亚州': [-84.39, 33.75],
|
||
'Georgia': [-84.39, 33.75],
|
||
'北卡罗来纳': [-80.84, 35.23], // 夏洛特
|
||
'北卡罗来纳州': [-80.84, 35.23],
|
||
'North Carolina': [-80.84, 35.23],
|
||
'密歇根': [-83.05, 42.33], // 底特律
|
||
'密歇根州': [-83.05, 42.33],
|
||
'Michigan': [-83.05, 42.33],
|
||
'新泽西': [-74.01, 40.71], // 纽约附近
|
||
'新泽西州': [-74.01, 40.71],
|
||
'New Jersey': [-74.01, 40.71],
|
||
'弗吉尼亚': [-77.43, 37.54], // 里士满
|
||
'弗吉尼亚州': [-77.43, 37.54],
|
||
'Virginia': [-77.43, 37.54],
|
||
'华盛顿州': [-122.33, 47.61], // 西雅图
|
||
'Washington': [-122.33, 47.61],
|
||
'亚利桑那': [-112.07, 33.45], // 凤凰城
|
||
'亚利桑那州': [-112.07, 33.45],
|
||
'Arizona': [-112.07, 33.45],
|
||
'马萨诸塞': [-71.06, 42.36], // 波士顿
|
||
'马萨诸塞州': [-71.06, 42.36],
|
||
'Massachusetts': [-71.06, 42.36],
|
||
'田纳西': [-86.78, 36.16], // 纳什维尔
|
||
'田纳西州': [-86.78, 36.16],
|
||
'Tennessee': [-86.78, 36.16],
|
||
'印第安纳': [-86.16, 39.77], // 印第安纳波利斯
|
||
'印第安纳州': [-86.16, 39.77],
|
||
'Indiana': [-86.16, 39.77],
|
||
'马里兰': [-76.61, 39.29], // 巴尔的摩
|
||
'马里兰州': [-76.61, 39.29],
|
||
'Maryland': [-76.61, 39.29],
|
||
'明尼苏达': [-93.27, 44.98], // 明尼阿波利斯
|
||
'明尼苏达州': [-93.27, 44.98],
|
||
'Minnesota': [-93.27, 44.98],
|
||
'科罗拉多': [-104.99, 39.74], // 丹佛
|
||
'科罗拉多州': [-104.99, 39.74],
|
||
'Colorado': [-104.99, 39.74],
|
||
'威斯康星': [-87.91, 43.04], // 密尔沃基
|
||
'威斯康星州': [-87.91, 43.04],
|
||
'Wisconsin': [-87.91, 43.04],
|
||
'密苏里': [-90.20, 38.63], // 圣路易斯
|
||
'密苏里州': [-90.20, 38.63],
|
||
'Missouri': [-90.20, 38.63],
|
||
'阿拉巴马': [-86.30, 32.38], // 蒙哥马利
|
||
'阿拉巴马州': [-86.30, 32.38],
|
||
'Alabama': [-86.30, 32.38],
|
||
'路易斯安那': [-90.07, 29.95], // 新奥尔良
|
||
'路易斯安那州': [-90.07, 29.95],
|
||
'Louisiana': [-90.07, 29.95],
|
||
'肯塔基': [-84.87, 38.20], // 法兰克福
|
||
'肯塔基州': [-84.87, 38.20],
|
||
'Kentucky': [-84.87, 38.20],
|
||
'俄勒冈': [-122.68, 45.52], // 波特兰
|
||
'俄勒冈州': [-122.68, 45.52],
|
||
'Oregon': [-122.68, 45.52],
|
||
'俄克拉荷马': [-97.52, 35.47], // 俄克拉荷马城
|
||
'俄克拉荷马州': [-97.52, 35.47],
|
||
'Oklahoma': [-97.52, 35.47],
|
||
'康涅狄格': [-72.68, 41.76], // 哈特福德
|
||
'康涅狄格州': [-72.68, 41.76],
|
||
'Connecticut': [-72.68, 41.76],
|
||
'犹他': [-111.89, 40.76], // 盐湖城
|
||
'犹他州': [-111.89, 40.76],
|
||
'Utah': [-111.89, 40.76],
|
||
'内华达': [-115.14, 36.17], // 拉斯维加斯
|
||
'内华达州': [-115.14, 36.17],
|
||
'Nevada': [-115.14, 36.17],
|
||
'阿肯色': [-92.29, 34.75], // 小石城
|
||
'阿肯色州': [-92.29, 34.75],
|
||
'Arkansas': [-92.29, 34.75],
|
||
'爱荷华': [-93.62, 41.59], // 得梅因
|
||
'爱荷华州': [-93.62, 41.59],
|
||
'Iowa': [-93.62, 41.59],
|
||
'堪萨斯': [-94.58, 39.10], // 堪萨斯城
|
||
'堪萨斯州': [-94.58, 39.10],
|
||
'Kansas': [-94.58, 39.10],
|
||
'新墨西哥': [-106.65, 35.08], // 阿尔伯克基
|
||
'新墨西哥州': [-106.65, 35.08],
|
||
'New Mexico': [-106.65, 35.08],
|
||
'内布拉斯加': [-96.70, 40.81], // 林肯
|
||
'内布拉斯加州': [-96.70, 40.81],
|
||
'Nebraska': [-96.70, 40.81],
|
||
'夏威夷': [-157.86, 21.31], // 檀香山
|
||
'夏威夷州': [-157.86, 21.31],
|
||
'Hawaii': [-157.86, 21.31],
|
||
'阿拉斯加': [-149.90, 61.22], // 安克雷奇
|
||
'阿拉斯加州': [-149.90, 61.22],
|
||
'Alaska': [-149.90, 61.22],
|
||
|
||
// 英文城市名
|
||
'New York': [-74.01, 40.71],
|
||
'Los Angeles': [-118.24, 34.05],
|
||
'San Francisco': [-122.42, 37.77],
|
||
'Seattle': [-122.33, 47.61],
|
||
'Chicago': [-87.63, 41.88],
|
||
'Boston': [-71.06, 42.36],
|
||
'Washington DC': [-77.04, 38.91],
|
||
'Miami': [-80.19, 25.76],
|
||
'Houston': [-95.37, 29.76],
|
||
'Dallas': [-96.80, 32.78],
|
||
'Philadelphia': [-75.17, 39.95],
|
||
'Phoenix': [-112.07, 33.45],
|
||
'San Diego': [-117.16, 32.72],
|
||
'Denver': [-104.99, 39.74],
|
||
'Las Vegas': [-115.14, 36.17],
|
||
'Portland': [-122.68, 45.52],
|
||
'Atlanta': [-84.39, 33.75],
|
||
'Detroit': [-83.05, 42.33],
|
||
'Jackson': [-90.18, 32.30],
|
||
|
||
// 国际城市 - 加拿大
|
||
'多伦多': [-79.38, 43.65],
|
||
'温哥华': [-123.12, 49.28],
|
||
'蒙特利尔': [-73.57, 45.50],
|
||
'卡尔加里': [-114.07, 51.05],
|
||
'渥太华': [-75.70, 45.42],
|
||
'埃德蒙顿': [-113.49, 53.55],
|
||
'Toronto': [-79.38, 43.65],
|
||
'Vancouver': [-123.12, 49.28],
|
||
'Montreal': [-73.57, 45.50],
|
||
'Calgary': [-114.07, 51.05],
|
||
'Ottawa': [-75.70, 45.42],
|
||
|
||
// 国际城市 - 欧洲
|
||
'伦敦': [-0.13, 51.51],
|
||
'巴黎': [2.35, 48.86],
|
||
'柏林': [13.40, 52.52],
|
||
'阿姆斯特丹': [4.90, 52.37],
|
||
'罗马': [12.50, 41.90],
|
||
'马德里': [-3.70, 40.42],
|
||
'巴塞罗那': [2.17, 41.39],
|
||
'维也纳': [16.37, 48.21],
|
||
'布鲁塞尔': [4.35, 50.85],
|
||
'苏黎世': [8.54, 47.38],
|
||
'慕尼黑': [11.58, 48.14],
|
||
'法兰克福德国': [8.68, 50.11],
|
||
'米兰': [9.19, 45.46],
|
||
'都柏林': [-6.26, 53.35],
|
||
'斯德哥尔摩': [18.07, 59.33],
|
||
'哥本哈根': [12.57, 55.68],
|
||
'奥斯陆': [10.75, 59.91],
|
||
'赫尔辛基': [24.94, 60.17],
|
||
'华沙': [21.01, 52.23],
|
||
'布拉格': [14.42, 50.08],
|
||
'布达佩斯': [19.04, 47.50],
|
||
'雅典': [23.73, 37.98],
|
||
'里斯本': [-9.14, 38.72],
|
||
'莫斯科': [37.62, 55.76],
|
||
'圣彼得堡': [30.31, 59.94],
|
||
'London': [-0.13, 51.51],
|
||
'Paris': [2.35, 48.86],
|
||
'Berlin': [13.40, 52.52],
|
||
'Rome': [12.50, 41.90],
|
||
'Madrid': [-3.70, 40.42],
|
||
'Amsterdam': [4.90, 52.37],
|
||
'Munich': [11.58, 48.14],
|
||
'Frankfurt': [8.68, 50.11],
|
||
|
||
// 国际城市 - 亚洲其他
|
||
'东京': [139.69, 35.69],
|
||
'大阪': [135.50, 34.69],
|
||
'京都': [135.77, 35.01],
|
||
'新加坡': [103.82, 1.35],
|
||
'首尔': [126.98, 37.57],
|
||
'釜山': [129.04, 35.18],
|
||
'曼谷': [100.50, 13.76],
|
||
'吉隆坡': [101.69, 3.14],
|
||
'雅加达': [106.85, -6.21],
|
||
'马尼拉': [120.98, 14.60],
|
||
'河内': [105.85, 21.03],
|
||
'胡志明市': [106.63, 10.82],
|
||
'新德里': [77.21, 28.61],
|
||
'孟买': [72.88, 19.08],
|
||
'班加罗尔': [77.59, 12.97],
|
||
'迪拜': [55.27, 25.20],
|
||
'特拉维夫': [34.78, 32.09],
|
||
'香港': [114.17, 22.32],
|
||
'澳门': [113.55, 22.20],
|
||
'台北': [121.56, 25.04],
|
||
'高雄': [120.31, 22.62],
|
||
'Tokyo': [139.69, 35.69],
|
||
'Osaka': [135.50, 34.69],
|
||
'Singapore': [103.82, 1.35],
|
||
'Seoul': [126.98, 37.57],
|
||
'Bangkok': [100.50, 13.76],
|
||
'Hong Kong': [114.17, 22.32],
|
||
'Dubai': [55.27, 25.20],
|
||
|
||
// 国际城市 - 南美洲
|
||
'圣保罗巴西': [-46.63, -23.55],
|
||
'里约热内卢': [-43.17, -22.91],
|
||
'布宜诺斯艾利斯': [-58.38, -34.60],
|
||
'圣地亚哥智利': [-70.65, -33.45],
|
||
'利马': [-77.03, -12.05],
|
||
'波哥大': [-74.07, 4.71],
|
||
'São Paulo': [-46.63, -23.55],
|
||
'Rio de Janeiro': [-43.17, -22.91],
|
||
'Buenos Aires': [-58.38, -34.60],
|
||
|
||
// 国际城市 - 非洲
|
||
'开罗': [31.24, 30.04],
|
||
'约翰内斯堡': [28.05, -26.20],
|
||
'开普敦': [18.42, -33.93],
|
||
'拉各斯': [3.39, 6.45],
|
||
'内罗毕': [36.82, -1.29],
|
||
'Cairo': [31.24, 30.04],
|
||
'Johannesburg': [28.05, -26.20],
|
||
'Cape Town': [18.42, -33.93],
|
||
|
||
// 国际城市 - 大洋洲
|
||
'奥克兰': [174.76, -36.85],
|
||
'惠灵顿': [174.78, -41.29],
|
||
'Auckland': [174.76, -36.85],
|
||
'Wellington': [174.78, -41.29]
|
||
}
|
||
|
||
export default function TimelinePage() {
|
||
const { treeData, currentTree, getMember, isLoading } = useFamily()
|
||
const { data: session, status } = useSession()
|
||
const members = Object.values(treeData.members)
|
||
const [mapLoaded, setMapLoaded] = useState(false)
|
||
const treeId = currentTree?.id
|
||
const router = useRouter()
|
||
const chartInstanceRef = useRef<ECharts | null>(null)
|
||
|
||
// 未登录时重定向到登录页
|
||
useEffect(() => {
|
||
if (status === 'unauthenticated') {
|
||
router.push('/auth/signin')
|
||
}
|
||
}, [status, router])
|
||
|
||
// 注册中国地图 - 从外部文件加载完整数据(必须在条件渲染之前)
|
||
useEffect(() => {
|
||
let isMounted = true
|
||
|
||
// 动态导入 echarts 并注册地图
|
||
const loadMap = async () => {
|
||
try {
|
||
const echarts = await import('echarts')
|
||
|
||
try {
|
||
const response = await fetch('/china.json')
|
||
const geoJson = await response.json()
|
||
if (isMounted) {
|
||
echarts.registerMap('china', geoJson)
|
||
setMapLoaded(true)
|
||
}
|
||
} catch (fetchError) {
|
||
console.error('Failed to load China map JSON, using fallback:', fetchError)
|
||
// 如果加载失败,使用简化版本
|
||
if (isMounted) {
|
||
echarts.registerMap('china', chinaGeoJSON as any)
|
||
setMapLoaded(true)
|
||
}
|
||
}
|
||
} catch (importError) {
|
||
console.error('Failed to import echarts:', importError)
|
||
}
|
||
}
|
||
|
||
loadMap()
|
||
|
||
return () => {
|
||
isMounted = false
|
||
}
|
||
}, [])
|
||
|
||
// -- 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(() => {
|
||
const result: Array<{
|
||
year: number | null
|
||
from: string
|
||
to: string
|
||
person: typeof members[0]
|
||
father: typeof members[0]
|
||
}> = []
|
||
|
||
// 1. 父子之间的迁徙(出生地变化)
|
||
members.forEach((m) => {
|
||
if (m.fatherId) {
|
||
const father = getMember(m.fatherId)
|
||
if (father) {
|
||
// 优先使用 birthPlace(出生地),其次 ancestralHome(祖籍)
|
||
const loc1 = father.birthPlace || father.ancestralHome
|
||
const loc2 = m.birthPlace || m.ancestralHome
|
||
|
||
if (loc1 && loc2 && loc1 !== loc2) {
|
||
result.push({
|
||
year: m.birthDate ? Number.parseInt(m.birthDate.substring(0, 4)) : null,
|
||
from: loc1,
|
||
to: loc2,
|
||
person: m,
|
||
father: father,
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
// 2. 同一个人的出生地到现居地的迁徙
|
||
const birthLoc = m.birthPlace || m.ancestralHome
|
||
const currentLoc = m.address
|
||
if (birthLoc && currentLoc && birthLoc !== currentLoc) {
|
||
// 避免重复:检查是否已经有相同的迁徙记录
|
||
const exists = result.some(r =>
|
||
r.person.id === m.id && r.from === birthLoc && r.to === currentLoc
|
||
)
|
||
if (!exists) {
|
||
result.push({
|
||
year: m.birthDate ? Number.parseInt(m.birthDate.substring(0, 4)) + 20 : null, // 假设20岁左右迁徙
|
||
from: birthLoc,
|
||
to: currentLoc,
|
||
person: m,
|
||
father: m, // 自己迁徙,father 指向自己
|
||
})
|
||
}
|
||
}
|
||
})
|
||
|
||
return result.sort((a, b) => (a.year || 0) - (b.year || 0))
|
||
}, [members, getMember])
|
||
|
||
// ECharts 地图配置
|
||
const mapOption = useMemo(() => {
|
||
// 城市坐标映射(简化城市名)
|
||
// 城市名称映射(支持省+市格式)
|
||
const cityMap: Record<string, string> = {}
|
||
|
||
// 自动生成映射:将"XX省XX市"或"XX市"映射到城市名
|
||
Object.keys(geoCoordMap).forEach(city => {
|
||
cityMap[city] = city // 直接城市名
|
||
cityMap[`${city}市`] = city // 城市+市
|
||
|
||
// 根据城市所在省份添加映射
|
||
const provinceMap: Record<string, string[]> = {
|
||
'广东省': ['广州', '深圳', '东莞', '佛山', '珠海', '惠州', '中山', '江门', '汕头', '肇庆'],
|
||
'浙江省': ['杭州', '宁波', '温州', '金华', '嘉兴', '台州', '绍兴', '湖州'],
|
||
'江苏省': ['苏州', '南京', '无锡', '常州', '南通', '盐城', '泰州', '扬州', '徐州'],
|
||
'山东省': ['济南', '青岛', '威海', '临沂', '济宁', '淄博', '莱芜', '菏泽'],
|
||
'四川省': ['成都', '绵阳'],
|
||
'河北省': ['石家庄', '保定', '廊坊', '邯郸', '秦皇岛'],
|
||
'福建省': ['福州', '厦门', '泉州', '莆田', '漳州'],
|
||
'湖北省': ['武汉', '宜昌'],
|
||
'湖南省': ['长沙', '衡阳', '株洲'],
|
||
'陕西省': ['西安', '咸阳'],
|
||
'河南省': ['郑州', '洛阳', '南阳', '平顶山'],
|
||
'辽宁省': ['沈阳', '大连', '鞍山', '抚顺'],
|
||
'安徽省': ['合肥', '芜湖'],
|
||
'云南省': ['昆明'],
|
||
'贵州省': ['贵阳', '遵义'],
|
||
'广西壮族自治区': ['南宁', '柳州', '桂林'],
|
||
'江西省': ['南昌', '九江', '赣州', '上饶'],
|
||
'黑龙江省': ['哈尔滨', '大庆', '齐齐哈尔'],
|
||
'吉林省': ['长春'],
|
||
'山西省': ['太原'],
|
||
'甘肃省': ['兰州'],
|
||
'新疆维吾尔自治区': ['乌鲁木齐'],
|
||
'宁夏回族自治区': ['银川'],
|
||
'内蒙古自治区': ['呼和浩特', '包头'],
|
||
'海南省': ['海口', '三亚'],
|
||
'西藏自治区': ['拉萨'],
|
||
'天津市': ['天津'],
|
||
'重庆市': ['重庆'],
|
||
'北京市': ['北京'],
|
||
'上海市': ['上海']
|
||
}
|
||
|
||
// 为每个城市添加"省+市"格式的映射
|
||
Object.entries(provinceMap).forEach(([province, cities]) => {
|
||
if (cities.includes(city)) {
|
||
cityMap[`${province}${city}市`] = city
|
||
cityMap[`${province}${city}`] = city
|
||
}
|
||
})
|
||
})
|
||
|
||
// 国家/地区到默认城市的映射
|
||
const countryToCityMap: Record<string, string> = {
|
||
'澳大利亚': '悉尼',
|
||
'澳洲': '悉尼',
|
||
'Australia': '悉尼',
|
||
'美国': '纽约',
|
||
'USA': '纽约',
|
||
'加拿大': '多伦多',
|
||
'Canada': '多伦多',
|
||
'英国': '伦敦',
|
||
'UK': '伦敦',
|
||
'法国': '巴黎',
|
||
'France': '巴黎',
|
||
'德国': '柏林',
|
||
'Germany': '柏林',
|
||
'日本': '东京',
|
||
'Japan': '东京',
|
||
'韩国': '首尔',
|
||
'Korea': '首尔',
|
||
'新加坡': '新加坡',
|
||
'Singapore': '新加坡',
|
||
'马来西亚': '吉隆坡',
|
||
'Malaysia': '吉隆坡',
|
||
'泰国': '曼谷',
|
||
'Thailand': '曼谷',
|
||
}
|
||
|
||
// 提取城市名称的辅助函数(去掉区的部分)
|
||
const extractCity = (location: string): string => {
|
||
if (!location) return location
|
||
|
||
// 1. 直接匹配 geoCoordMap
|
||
if (geoCoordMap[location]) return location
|
||
|
||
// 2. 尝试从 cityMap 中查找
|
||
let city = cityMap[location]
|
||
if (city && geoCoordMap[city]) return city
|
||
|
||
// 3. 国家/地区映射到默认城市
|
||
const countryCity = countryToCityMap[location]
|
||
if (countryCity && geoCoordMap[countryCity]) return countryCity
|
||
|
||
// 4. 尝试提取省市部分(去掉区)
|
||
// 例如:"山东省济南市历下区" -> "济南"
|
||
// 例如:"陕西西安" -> "西安"
|
||
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
|
||
}
|
||
}
|
||
|
||
// 5. 尝试直接在地址中查找已知城市名
|
||
for (const knownCity of Object.keys(geoCoordMap)) {
|
||
if (location.includes(knownCity)) {
|
||
return knownCity
|
||
}
|
||
}
|
||
|
||
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<string, number> = {}
|
||
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 hasInternational = lines.some((line: any) => {
|
||
if (!line) return false
|
||
const [from, to] = line.coords
|
||
return from[0] < 70 || from[0] > 140 || from[1] < 0 ||
|
||
to[0] < 70 || to[0] > 140 || to[1] < 0
|
||
})
|
||
|
||
const scatterData = Object.entries(cityData).map(([name, value]) => {
|
||
const coord = geoCoordMap[name]
|
||
if (!coord) return null
|
||
return {
|
||
name,
|
||
value: [...coord, value * 10]
|
||
}
|
||
}).filter(Boolean)
|
||
|
||
// 如果有国际城市,使用世界地图模式(不依赖 geo map)
|
||
if (hasInternational) {
|
||
return {
|
||
backgroundColor: 'transparent',
|
||
title: {
|
||
text: '家族迁徙路径',
|
||
subtext: '包含国际迁徙 (滚轮缩放,拖拽移动)',
|
||
left: 'center',
|
||
textStyle: { color: '#333', fontSize: 16 }
|
||
},
|
||
tooltip: {
|
||
trigger: 'item',
|
||
formatter: (params: any) => {
|
||
if (params.seriesType === 'lines') {
|
||
return `${params.data.fromName} → ${params.data.toName}`
|
||
}
|
||
return params.name
|
||
}
|
||
},
|
||
// 添加缩放和拖拽功能
|
||
dataZoom: [
|
||
{
|
||
type: 'inside',
|
||
xAxisIndex: 0,
|
||
filterMode: 'none'
|
||
},
|
||
{
|
||
type: 'inside',
|
||
yAxisIndex: 0,
|
||
filterMode: 'none'
|
||
}
|
||
],
|
||
xAxis: {
|
||
type: 'value',
|
||
min: -180,
|
||
max: 180,
|
||
show: false
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
min: -60,
|
||
max: 70,
|
||
show: false
|
||
},
|
||
series: [
|
||
{
|
||
type: 'lines',
|
||
coordinateSystem: 'cartesian2d',
|
||
data: lines.map((line: any) => ({
|
||
...line,
|
||
coords: line.coords
|
||
})),
|
||
lineStyle: {
|
||
color: '#3b82f6',
|
||
width: 2,
|
||
opacity: 0.6,
|
||
curveness: 0.3
|
||
},
|
||
effect: {
|
||
show: true,
|
||
period: 4,
|
||
trailLength: 0.3,
|
||
symbol: 'arrow',
|
||
symbolSize: 10,
|
||
color: '#3b82f6'
|
||
},
|
||
zlevel: 1
|
||
},
|
||
{
|
||
type: 'scatter',
|
||
coordinateSystem: 'cartesian2d',
|
||
data: scatterData.map((item: any) => ({
|
||
name: item.name,
|
||
value: [item.value[0], item.value[1]]
|
||
})),
|
||
symbolSize: 12,
|
||
label: {
|
||
show: true,
|
||
formatter: '{b}',
|
||
position: 'right',
|
||
fontSize: 11,
|
||
color: '#333'
|
||
},
|
||
itemStyle: {
|
||
color: '#ef4444',
|
||
shadowBlur: 5,
|
||
shadowColor: 'rgba(0,0,0,0.3)'
|
||
}
|
||
}
|
||
]
|
||
}
|
||
}
|
||
|
||
// 仅中国城市,使用中国地图
|
||
return {
|
||
backgroundColor: 'transparent',
|
||
title: {
|
||
text: '家族迁徙路径',
|
||
subtext: '滚轮缩放,拖拽移动',
|
||
left: 'center',
|
||
textStyle: { color: '#333', fontSize: 16 }
|
||
},
|
||
tooltip: {
|
||
trigger: 'item'
|
||
},
|
||
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])
|
||
|
||
// 使用 callback ref 来确保 DOM 元素挂载后初始化图表
|
||
const initChartRef = useCallback((node: HTMLDivElement | null) => {
|
||
if (!node || !mapLoaded) {
|
||
console.log('initChartRef: node或mapLoaded不存在', { node: !!node, mapLoaded })
|
||
return
|
||
}
|
||
|
||
console.log('initChartRef: 开始初始化图表')
|
||
|
||
// 动态导入并初始化图表
|
||
import('echarts').then((echarts) => {
|
||
// 再次检查节点是否存在
|
||
if (!node) return
|
||
|
||
// 检查是否已有实例
|
||
let chart = echarts.getInstanceByDom(node)
|
||
|
||
if (!chart) {
|
||
console.log('创建新的图表实例')
|
||
// 创建新实例
|
||
chart = echarts.init(node)
|
||
chartInstanceRef.current = chart
|
||
|
||
// 监听窗口大小变化
|
||
const handleResize = () => {
|
||
chart?.resize()
|
||
}
|
||
window.addEventListener('resize', handleResize)
|
||
|
||
// 保存清理函数到节点
|
||
;(node as any).__resizeHandler = handleResize
|
||
} else {
|
||
console.log('使用已有的图表实例')
|
||
}
|
||
|
||
// 设置选项
|
||
console.log('设置图表选项')
|
||
chart.setOption(mapOption, { notMerge: true })
|
||
console.log('图表选项设置完成')
|
||
}).catch(error => {
|
||
console.error('Failed to initialize chart:', error)
|
||
})
|
||
}, [mapLoaded, mapOption])
|
||
|
||
// 未登录时不渲染内容(已在 useEffect 中重定向)
|
||
if (status === 'loading' || !session) {
|
||
return null
|
||
}
|
||
|
||
// 显示加载状态
|
||
if (isLoading) {
|
||
return (
|
||
<div className="min-h-screen bg-background flex flex-col">
|
||
<SiteHeader />
|
||
<div className="flex-1 flex items-center justify-center">
|
||
<div className="text-center">
|
||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"></div>
|
||
<p className="text-muted-foreground">加载中...</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// 显示无数据状态
|
||
if (members.length === 0) {
|
||
return (
|
||
<div className="min-h-screen bg-background flex flex-col">
|
||
<SiteHeader />
|
||
<div className="flex-1 flex items-center justify-center">
|
||
<div className="text-center space-y-4">
|
||
<div className="inline-flex items-center justify-center w-16 h-16 text-primary/80">
|
||
<Calendar className="h-16 w-16" strokeWidth={1} />
|
||
</div>
|
||
<p className="text-base text-foreground font-light tracking-wide">时光未启</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className="h-screen bg-background flex flex-col font-sans overflow-hidden">
|
||
<SiteHeader />
|
||
<main className="container mx-auto py-8 px-4 md:px-6 flex-1 flex flex-col overflow-hidden">
|
||
<div className="mb-8 flex-shrink-0">
|
||
<h1 className="text-3xl font-serif font-bold mb-2">家族历史</h1>
|
||
<p className="text-muted-foreground">纵览家族发展的时间脉络与迁徙足迹。</p>
|
||
</div>
|
||
|
||
<Tabs defaultValue="timeline" className="w-full flex-1 flex flex-col overflow-hidden">
|
||
<TabsList className="mb-6 w-full md:w-auto flex-shrink-0">
|
||
<TabsTrigger value="timeline" className="flex items-center gap-2">
|
||
<Clock className="h-4 w-4" /> 时间轴
|
||
</TabsTrigger>
|
||
<TabsTrigger value="migration" className="flex items-center gap-2">
|
||
<MapIcon className="h-4 w-4" /> 迁徙图
|
||
</TabsTrigger>
|
||
</TabsList>
|
||
|
||
<TabsContent value="timeline" className="flex-1 overflow-y-auto">
|
||
<ChineseCard variant="scroll" className="border-none shadow-none bg-transparent">
|
||
<ChineseCardContent className="p-0">
|
||
<div className="relative border-l-2 border-primary/20 ml-6 md:ml-12 space-y-8 py-4">
|
||
{events.map((event, idx) => (
|
||
<div key={idx} className="relative pl-8 md:pl-12 group">
|
||
{/* Dot */}
|
||
<div
|
||
className={`absolute -left-[9px] top-1 h-4 w-4 rounded-full border-2 border-background transition-colors group-hover:scale-110 ${event.type === "birth" ? "bg-primary" : "bg-neutral-400"
|
||
}`}
|
||
/>
|
||
|
||
{/* Year Label (Mobile vs Desktop) */}
|
||
<div className="absolute -left-16 md:-left-24 top-0 w-12 md:w-20 text-right text-sm font-mono text-muted-foreground font-bold">
|
||
{event.year}
|
||
</div>
|
||
|
||
{/* Content Card */}
|
||
<div className="bg-card rounded-lg px-4 py-2 transition-all hover:bg-accent/5 border border-border/50">
|
||
<div className="flex items-center gap-2 flex-wrap text-sm">
|
||
<span
|
||
className={`text-xs px-2 py-0.5 rounded-full flex-shrink-0 ${event.type === "birth" ? "bg-primary/10 text-primary" : "bg-neutral-100 text-neutral-500"
|
||
}`}
|
||
>
|
||
{event.type === "birth" ? "诞生" : "逝世"}
|
||
</span>
|
||
<span className="font-serif font-bold">
|
||
<MemberNameWithStatus
|
||
name={event.member.fullName}
|
||
isDead={!!event.member.deathDate}
|
||
memberId={event.member.id}
|
||
treeId={treeId}
|
||
/>
|
||
</span>
|
||
<span className={`text-xs px-1.5 py-0.5 rounded ${event.member.gender === "MALE" ? "bg-blue-100 text-blue-700" : "bg-pink-100 text-pink-700"}`}>
|
||
{event.member.gender === "MALE" ? "男" : "女"}
|
||
</span>
|
||
<span className="text-xs text-muted-foreground">第{event.member.generation}世</span>
|
||
|
||
{/* 父母信息 */}
|
||
{event.member.fatherId && (
|
||
<span className="text-xs px-1.5 py-0.5 rounded bg-orange-50 text-orange-700 border border-orange-200">
|
||
父:<MemberNameWithStatus
|
||
name={getMember(event.member.fatherId)?.fullName || "未知"}
|
||
isDead={!!getMember(event.member.fatherId)?.deathDate}
|
||
memberId={event.member.fatherId}
|
||
treeId={treeId}
|
||
/>
|
||
</span>
|
||
)}
|
||
{event.member.motherId && (
|
||
<span className="text-xs px-1.5 py-0.5 rounded bg-orange-50 text-orange-700 border border-orange-200">
|
||
母:<MemberNameWithStatus
|
||
name={getMember(event.member.motherId)?.fullName || "未知"}
|
||
isDead={!!getMember(event.member.motherId)?.deathDate}
|
||
memberId={event.member.motherId}
|
||
treeId={treeId}
|
||
/>
|
||
</span>
|
||
)}
|
||
|
||
{/* 配偶信息 */}
|
||
{event.member.spouseIds && event.member.spouseIds.length > 0 && (
|
||
<span className="text-xs px-1.5 py-0.5 rounded bg-purple-50 text-purple-700 border border-purple-200">
|
||
配偶:<MemberNameWithStatus
|
||
name={getMember(event.member.spouseIds[0])?.fullName || "未知"}
|
||
isDead={!!getMember(event.member.spouseIds[0])?.deathDate}
|
||
memberId={event.member.spouseIds[0]}
|
||
treeId={treeId}
|
||
/>
|
||
</span>
|
||
)}
|
||
|
||
{/* 子女信息 */}
|
||
{event.member.childrenIds && event.member.childrenIds.length > 0 && (
|
||
<span className="text-xs px-1.5 py-0.5 rounded bg-green-50 text-green-700 border border-green-200">
|
||
子女:{event.member.childrenIds.length}人
|
||
</span>
|
||
)}
|
||
|
||
{/* 出生地信息 */}
|
||
{event.type === "birth" && (event.member.birthPlace || event.member.ancestralHome) && (
|
||
<span className="text-xs text-muted-foreground">
|
||
📍{event.member.birthPlace || event.member.ancestralHome}
|
||
</span>
|
||
)}
|
||
|
||
{/* 享年 */}
|
||
{event.type === "death" && event.member.birthDate && (
|
||
<span className="text-xs text-muted-foreground">
|
||
享年{event.year - Number.parseInt(event.member.birthDate.substring(0, 4))}岁
|
||
</span>
|
||
)}
|
||
|
||
{/* 葬地 */}
|
||
{event.type === "death" && event.member.burialPlace && (
|
||
<span className="text-xs text-muted-foreground">
|
||
⚰️{event.member.burialPlace}
|
||
</span>
|
||
)}
|
||
|
||
<span className="text-xs text-muted-foreground font-mono ml-auto">{event.date}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
|
||
{events.length === 0 && (
|
||
<div className="pl-8 py-8">
|
||
<p className="text-sm text-foreground font-light tracking-wide">时光留白</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</ChineseCardContent>
|
||
</ChineseCard>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="migration" className="flex-1 overflow-y-auto">
|
||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||
<ChineseCard variant="bamboo" className="shadow-sm">
|
||
<ChineseCardHeader>
|
||
<ChineseCardTitle className="font-serif">迁徙记录</ChineseCardTitle>
|
||
</ChineseCardHeader>
|
||
<ChineseCardContent>
|
||
<ScrollArea className="h-[600px] pr-4">
|
||
<div className="space-y-6">
|
||
{migrations.map((mig, idx) => {
|
||
const isSelfMigration = mig.father.id === mig.person.id
|
||
return (
|
||
<div key={idx} className="flex flex-col gap-2 p-4 rounded-lg bg-muted/30 border border-border">
|
||
<div className="flex items-center gap-2 text-sm text-muted-foreground mb-1">
|
||
<span className="font-mono bg-background px-1 rounded">
|
||
{mig.year ? `${mig.year}年` : "未知年代"}
|
||
</span>
|
||
<span>第 {mig.person.generation} 世</span>
|
||
{isSelfMigration && (
|
||
<span className="text-xs bg-blue-100 text-blue-700 px-1.5 py-0.5 rounded">个人迁徙</span>
|
||
)}
|
||
</div>
|
||
<div className="flex items-center justify-between gap-4">
|
||
<div className="flex-1 text-center p-2 bg-background rounded border border-border/50">
|
||
<div className="text-xs text-muted-foreground mb-1">{isSelfMigration ? '出生地' : '迁出地'}</div>
|
||
<div className="font-bold">{mig.from}</div>
|
||
{!isSelfMigration && (
|
||
<div className="text-xs text-muted-foreground mt-1">
|
||
<MemberNameWithStatus
|
||
name={mig.father.fullName}
|
||
isDead={!!mig.father.deathDate}
|
||
memberId={mig.father.id}
|
||
treeId={treeId}
|
||
/>
|
||
</div>
|
||
)}
|
||
</div>
|
||
<ArrowRight className="text-muted-foreground" />
|
||
<div className="flex-1 text-center p-2 bg-background rounded border border-border/50">
|
||
<div className="text-xs text-muted-foreground mb-1">{isSelfMigration ? '现居地' : '迁入地'}</div>
|
||
<div className="font-bold text-primary">{mig.to}</div>
|
||
<div className="text-xs text-muted-foreground mt-1">
|
||
<MemberNameWithStatus
|
||
name={mig.person.fullName}
|
||
isDead={!!mig.person.deathDate}
|
||
memberId={mig.person.id}
|
||
treeId={treeId}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)})}
|
||
{migrations.length === 0 && (
|
||
<div className="text-center py-16">
|
||
<p className="text-sm text-foreground font-light tracking-wide">足迹未显</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</ScrollArea>
|
||
</ChineseCardContent>
|
||
</ChineseCard>
|
||
|
||
<ChineseCard variant="default" cornerOrnament className="bg-gradient-to-br from-primary/5 via-background to-secondary/5 border-primary/20 shadow-sm">
|
||
<ChineseCardHeader>
|
||
<ChineseCardTitle className="font-serif">迁徙路径图</ChineseCardTitle>
|
||
</ChineseCardHeader>
|
||
<ChineseCardContent>
|
||
<div className="relative h-[600px] overflow-hidden rounded-lg bg-background/50 backdrop-blur border border-border/50">
|
||
{!mapLoaded ? (
|
||
<div className="flex items-center justify-center h-full text-muted-foreground">
|
||
<div className="text-center">
|
||
<MapIcon className="h-16 w-16 mx-auto mb-4 opacity-20 animate-pulse" />
|
||
<p>加载地图中...</p>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div
|
||
ref={initChartRef}
|
||
style={{ height: '100%', width: '100%' }}
|
||
/>
|
||
)}
|
||
</div>
|
||
</ChineseCardContent>
|
||
</ChineseCard>
|
||
</div>
|
||
</TabsContent>
|
||
</Tabs>
|
||
</main>
|
||
</div>
|
||
)
|
||
}
|