Files
chinese-family-tree-2/app/timeline/page.tsx
T
freedakgmail e7167f4340 0.6.2.0
2025-12-10 13:03:10 +08:00

1331 lines
50 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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>
)
}