前五卷地图

This commit is contained in:
龙澳
2026-03-30 17:26:44 +08:00
parent faeeea44d9
commit 565c0184c0
13 changed files with 2530 additions and 443 deletions

2
.gitignore vendored
View File

@@ -2,6 +2,8 @@
# gitignore template for Vue.js projects
#
# Recommended template: Node.gitignore
node_modules/
.claude/
# TODO: where does this rule come from?
docs/_book

187
data/vol02.json Normal file
View File

@@ -0,0 +1,187 @@
{
"volume": "卷二",
"chapters": ["第01章 老奸巨猾", "第02章 尔虞我诈", "第03章 误打误撞", "第04章 发财大计", "第05章 东溟夫人", "第06章 利己利人", "第07章 网中之鱼", "第08章 红粉帮主", "第09章 初窥堂奥", "第10章 秘密账簿", "第11章 毒如蛇蝎", "第12章 诈死脱身"],
"locations": [
{ "id": "liyang", "name": "历阳", "type": "city", "lat": 31.7, "lng": 118.37, "description": "杜伏威占据的重镇,江淮军的根据地" },
{ "id": "xin_an", "name": "新安郡", "type": "city", "lat": 31.2, "lng": 118.5, "description": "沿长江南岸兴旺大城,寇徐与杜伏威同行处" },
{ "id": "danyang", "name": "丹阳", "type": "city", "lat": 32.0, "lng": 118.95, "description": "长江沿岸重镇,宋师道活动地" },
{ "id": "long_hai", "name": "陇海", "type": "city", "lat": 31.9, "lng": 118.7, "description": "沿海重镇" },
{ "id": "yu_hang", "name": "余杭", "type": "city", "lat": 30.3, "lng": 120.1, "description": "沿海郡县,海沙帮分舵所在地" },
{ "id": "sea_beach", "name": "海滩", "type": "landmark", "lat": 29.8, "lng": 121.5, "description": "东溟派船队停泊地,寇徐修炼两月处" },
{ "id": "hui_shui", "name": "淮水", "type": "waterway", "lat": 32.1, "lng": 117.0, "description": "贯通南北的重要水道" },
{ "id": "lei_gong_gorge", "name": "雷公峡", "type": "landmark", "lat": 32.5, "lng": 116.8, "description": "淮水沿岸险峻峡谷,杜伏威与巨鲲帮对战处" },
{ "id": "peng_cheng", "name": "彭城", "type": "city", "lat": 34.4, "lng": 117.1, "description": "东溟夫人单美仙与李渊会面之地" },
{ "id": "zhong_yang", "name": "钟阳", "type": "city", "lat": 31.0, "lng": 116.0, "description": "云玉真计划前往地" }
],
"factions": [
{
"id": "du_fuwei_army",
"name": "杜伏威军",
"type": "义军",
"color": "#FF8C00",
"leader": "杜伏威",
"territory": ["liyang"],
"key_figures": ["杜伏威", "辅公祏"],
"description": "江淮义军,占据历阳,攻占新安郡,与寇徐因缘际会相识"
},
{
"id": "hai_sha_bang",
"name": "海沙帮",
"type": "帮派",
"color": "#1E90FF",
"leader": "韩盖天",
"territory": ["yu_hang"],
"key_figures": ["韩盖天", "游秋雁", "尤贵", "凌志高"],
"description": "东南沿海三大帮派之一,私盐贸易,依附宇文门阀,龙王韩盖天之名颇著"
},
{
"id": "ju_kun_bang",
"name": "巨鲲帮",
"type": "帮派",
"color": "#FF1493",
"leader": "云玉真",
"territory": [],
"key_figures": ["云玉真", "卜天志", "云芝", "陈老谋"],
"description": "东南沿海三大帮派之一,独立自主,红粉帮主云玉真武艺精湛,实为独孤阀幕后势力"
},
{
"id": "shui_long_bang",
"name": "水龙帮",
"type": "帮派",
"color": "#4169E1",
"leader": "未提及",
"territory": [],
"key_figures": [],
"description": "东南沿海三大帮派之一,依附南方宋姓门阀"
},
{
"id": "ba_ling_bang",
"name": "巴陵帮",
"type": "帮派",
"color": "#8B0000",
"leader": "隋炀帝杨广",
"territory": [],
"key_figures": [],
"description": "以洞庭湖为根据地,专事贩卖妇女,供应妓院,获利最厚,背后乃皇帝"
},
{
"id": "dong_ming_pai",
"name": "东溟派",
"type": "外族势力",
"color": "#00CED1",
"leader": "单美仙",
"territory": [],
"key_figures": ["单美仙", "游秋雁", "东溟夫人"],
"description": "来自琉球大海岛,以女性为主,打造优质兵器,每年春分到中土选少男,运营飘香号"
},
{
"id": "du_gu_fa",
"name": "独孤阀",
"type": "门阀",
"color": "#9932CC",
"leader": "独孤峰",
"territory": [],
"key_figures": ["独孤策", "独孤峰", "尤楚红"],
"description": "四姓门阀之一,独孤策为阀主独孤峰之子,武功高强,与云玉真勾结"
},
{
"id": "li_fa",
"name": "李阀",
"type": "门阀",
"color": "#DC143C",
"leader": "李渊",
"territory": [],
"key_figures": ["李渊"],
"description": "四姓门阀之一,与东溟派有交易往来,购买兵器"
},
{
"id": "yu_wen_fa",
"name": "宇文阀",
"type": "门阀",
"color": "#4169E1",
"leader": "宇文伤",
"territory": [],
"key_figures": ["宇文化及", "宇文伤"],
"description": "四姓门阀之一,宇文化及为禁卫总管,命海沙帮追捕寇徐,图谋获取东溟派账簿"
}
],
"character_routes": [
{
"character": "寇仲 & 徐子陵",
"color": "#FF4500",
"route": [
{ "location": "liyang", "chapter": 1, "event": "逃出杜伏威追捕,被挟到新安郡" },
{ "location": "xin_an", "chapter": 2, "event": "到新安郡酒楼,遇沈乃堂和梁舜明,杜伏威动武" },
{ "location": "xin_an", "chapter": 3, "event": "逃入飘香院躲避杜伏威追捕" },
{ "lat": 30.5, "lng": 120.0, "chapter": 5, "event": "在海上从东溟派大船跳入水中,首次学会水底闭气" },
{ "location": "yu_hang", "chapter": 5, "event": "登陆余杭城,遇老刘和谭勇,得知东溟派信息" },
{ "lat": 30.2, "lng": 120.3, "chapter": 6, "event": "被海沙帮追捕后逃脱" },
{ "lat": 29.8, "lng": 121.5, "chapter": 7, "event": "与偷盐船被海沙帮追获,被困铁笼,成功逃脱" },
{ "location": "sea_beach", "chapter": 8, "event": "船只触礁沉没,损失盐包,遇云玉真" },
{ "location": "sea_beach", "chapter": 9, "event": "海滩修炼两月,观海鸥悟道,获得天人合一之境" },
{ "location": "sea_beach", "chapter": 10, "event": "尝试跳崖修炼轻功,成功突破" },
{ "location": "sea_beach", "chapter": 11, "event": "被纳入云玉真巨鲲帮,船上修为大进" },
{ "lat": 32.5, "lng": 116.8, "chapter": 12, "event": "淮水激战,杜伏威对独孤策,危崖绝境" }
]
},
{
"character": "杜伏威",
"color": "#FF8C00",
"route": [
{ "location": "liyang", "chapter": 1, "event": "在镇外路上擒拿寇徐二人,强行索取关帝庙秘密" },
{ "location": "xin_an", "chapter": 2, "event": "在新安郡酒楼与沈乃堂、梁舜明对战" },
{ "location": "xin_an", "chapter": 3, "event": "追捕逃离的寇徐" },
{ "lat": 32.5, "lng": 116.8, "chapter": 12, "event": "追击云玉真等人至雷公峡,与独孤策对战十招" }
]
},
{
"character": "云玉真",
"color": "#FF1493",
"route": [
{ "location": "sea_beach", "chapter": 8, "event": "沙滩出现,与寇徐交手,提议拜师" },
{ "location": "sea_beach", "chapter": 9, "event": "三次到沙滩传授鸟渡术轻功心法" },
{ "lat": 32.0, "lng": 119.5, "chapter": 10, "event": "约定接寇徐上船进行任务" },
{ "location": "yu_hang", "chapter": 11, "event": "接寇徐上巨鲲帮战船,传授偷盗技艺,与独孤策密会" },
{ "lat": 32.1, "lng": 117.0, "chapter": 12, "event": "淮水行进中遭遇杜伏威,逃离至雷公峡绝崖" }
]
},
{
"character": "韩盖天",
"color": "#1E90FF",
"route": [
{ "lat": 30.0, "lng": 120.5, "chapter": 7, "event": "海上追获偷盐船,俘获寇徐" },
{ "location": "yu_hang", "chapter": 7, "event": "船队审问寇徐,获悉杨公宝藏信息" }
]
},
{
"character": "东溟派",
"color": "#00CED1",
"route": [
{ "lat": 30.5, "lng": 120.0, "chapter": 5, "event": "飘香号船队在海上与海沙帮交战,寇徐从船上跳海逃脱" },
{ "location": "yu_hang", "chapter": 5, "event": "东溟派在余杭招募选婿" },
{ "location": "peng_cheng", "chapter": 11, "event": "东溟夫人单美仙七天后赴彭城会李渊,为偷账簿的机会窗口" }
]
}
],
"key_events": [
{ "chapter": 1, "location": "liyang", "event": "杜伏威在官道上追获逃离的寇徐,威胁要杀人灭口" },
{ "chapter": 1, "location": "xin_an", "event": "寇徐被杜伏威挟到新安郡,众人在酒楼进餐" },
{ "chapter": 2, "location": "xin_an", "event": "杜伏威与沈乃堂、梁舜明交手,展示袖里乾坤武功" },
{ "chapter": 3, "event": "寇徐逃入飘香院躲避杜伏威,遇沈无双踩踏" },
{ "chapter": 5, "event": "寇徐首次登上东溟派飘香号,遇单美仙但被逐下船", "lat": 30.5, "lng": 120.0 },
{ "chapter": 5, "event": "寇徐从飘香号跳海,在水底首次领悟内息呼吸之妙" },
{ "chapter": 6, "event": "寇徐进城遇海沙帮人员,得知盐仓位置和东溟派信息" },
{ "chapter": 7, "event": "海沙帮五船追获寇徐与偷盐船,韩盖天审问与威胁" },
{ "chapter": 7, "event": "寇徐在船底挖洞脱逃,偷盐船与海沙帮分开,触礁沉没", "lat": 30.0, "lng": 121.0 },
{ "chapter": 8, "event": "徐子陵遇海鸥受启发,与寇仲一击打散其剑,首次成功使用奇功" },
{ "chapter": 9, "event": "两人从高崖跳下,在生死关头突破轻功,掌握反劲之道" },
{ "chapter": 10, "event": "寇徐试图在海底修炼,后来通过观察鱼儿与螃蟹修行渐悟奥妙" },
{ "chapter": 11, "event": "云玉真与独孤策在船舱密会,寇徐无意中听闻被杀人灭口计划" },
{ "chapter": 11, "event": "寇徐得知云玉真真实身份与陷害,计划诈死脱身" },
{ "chapter": 12, "location": "lei_gong_gorge", "event": "杜伏威追至雷公峡,与独孤策约定十招对决,寇徐危崖绝处" }
]
}

166
data/vol03.json Normal file
View File

@@ -0,0 +1,166 @@
{
"volume": "卷三",
"chapters": ["第01章 生灵涂炭", "第02章 阴谋诡计", "第03章 美女赌约", "第04章 中计被擒", "第05章 一单交易", "第06章 绝地逃生", "第07章 嫖赌合一", "第08章 赌场风云", "第09章 东溟公主", "第10章 微湖战火"],
"locations": [
{ "id": "pengcheng", "name": "彭城", "aliases": ["徐州"], "type": "city", "lat": 34.28, "lng": 117.57, "description": "卷三主要活动地点,战略要地,隋朝重镇" },
{ "id": "shuishui", "name": "泗水", "aliases": ["泗河"], "type": "waterway", "lat": 34.5, "lng": 117.0, "description": "贯通中原的重要水道,寇徐二人逃生路线" },
{ "id": "lvliang_mountain", "name": "吕梁山", "type": "landmark", "lat": 34.8, "lng": 117.3, "description": "彭城附近的山脉,秦叔宝念想所在" },
{ "id": "piaoxi_number", "name": "飘香号", "aliases": ["飘香船"], "type": "landmark", "lat": 32.5, "lng": 119.0, "description": "东溟夫人的商船,海沙帮欲攻打之地,李世民船队所在" },
{ "id": "luo_yang", "name": "洛阳", "aliases": ["东都"], "type": "city", "lat": 34.62, "lng": 112.45, "description": "李世民船队出发地" },
{ "id": "xingyang", "name": "荥阳", "type": "city", "lat": 34.79, "lng": 113.38, "description": "瓦岗军翟让根据地,寇徐欲寻素素处" },
{ "id": "taiyang", "name": "太原", "type": "city", "lat": 37.87, "lng": 112.55, "description": "李渊驻地,李世民活动范围" },
{ "id": "fuchun", "name": "扶春", "type": "town", "lat": 34.5, "lng": 115.0, "description": "沈落雁防守之地,秦叔宝与隋将张须陀的战场" },
{ "id": "ghost_stone_gorge", "name": "鬼石峡", "type": "landmark", "lat": 34.2, "lng": 117.3, "description": "泗水险滩,寇徐与渔夫失船处" },
{ "id": "qinglong_beach", "name": "青龙滩", "type": "landmark", "lat": 34.15, "lng": 117.25, "description": "泗水上打鱼地点" },
{ "id": "hunan", "name": "湖南", "type": "region", "lat": 27.0, "lng": 112.0, "description": "飘香号来往路线区域" }
],
"factions": [
{
"id": "sui",
"name": "隋朝",
"type": "朝廷",
"color": "#FFD700",
"leader": "杨广(隋炀帝)",
"territory": ["pengcheng", "luo_yang"],
"key_figures": ["杨广", "秦叔宝", "张须陀"],
"description": "杨广统治下衰亡中的隋朝,诸将各有心思"
},
{
"id": "wagang_army",
"name": "瓦岗军",
"type": "义军",
"color": "#FF6347",
"leader": "翟让/李密",
"territory": ["xingyang"],
"key_figures": ["翟让", "李密", "沈落雁", "祖君彦"],
"description": "天下最大义军势力,李密智计过人,沈落雁为俏军师"
},
{
"id": "du_fuwei",
"name": "杜伏威军",
"type": "义军",
"color": "#FF8C00",
"leader": "杜伏威",
"territory": ["pengcheng"],
"key_figures": ["杜伏威"],
"description": "江淮义军首领,内功深厚,掌控彭城地区,与寇徐二人有师徒渊源"
},
{
"id": "li_clan",
"name": "李阀",
"type": "门阀",
"color": "#DC143C",
"leader": "李渊",
"territory": ["taiyang"],
"key_figures": ["李渊", "李世民", "李秀宁"],
"description": "四姓门阀之一,暗中购买兵器,李世民智计过人,与寇徐二人有交易"
},
{
"id": "dongming_pai",
"name": "东溟派",
"type": "江湖势力",
"color": "#20B2AA",
"leader": "东溟夫人(单美仙)",
"territory": ["piaoxi_number"],
"key_figures": ["东溟夫人", "单秀", "单玉蝶", "云玉真"],
"description": "琉球兵器贸易大户,掌控天下兵器供应,护法仙子武力超群"
},
{
"id": "pengliang_gang",
"name": "彭梁会",
"type": "黑道帮会",
"color": "#8B008B",
"leader": "任媚媚/香贵",
"territory": ["pengcheng"],
"key_figures": ["任媚媚", "香贵", "香玉山"],
"description": "彭城重要黑帮,专事人口贩卖,与皇帝勾结"
},
{
"id": "turks",
"name": "突厥",
"type": "外族",
"color": "#2F4F4F",
"leader": "始毕可汗",
"territory": [],
"key_figures": ["颜里回", "慕铁雄"],
"description": "北方外患,与宇文阀勾结暗害翟让"
}
],
"character_routes": [
{
"character": "寇仲 & 徐子陵",
"color": "#FF4500",
"route": [
{ "lat": 32.8, "lng": 116.5, "chapter": 12, "event": "离开翠山镇,北上赶往彭城" },
{ "lat": 33.5, "lng": 117.0, "chapter": 12, "event": "遇见战场生灵涂炭之象,斩杀隋兵" },
{ "lat": 34.0, "lng": 117.2, "chapter": 12, "event": "参与大战,与沈落雁初识" },
{ "location": "pengcheng", "chapter": 13, "event": "到达彭城,遇秦叔宝" },
{ "location": "shuishui", "chapter": 13, "event": "河边约定与沈落雁赌赛" },
{ "lat": 34.2, "lng": 117.3, "chapter": 13, "event": "乘船逆流,遇莫成老渔夫,潜水脱险" },
{ "lat": 34.3, "lng": 117.4, "chapter": 14, "event": "乘船逃脱被发现,躲入水底" },
{ "location": "pengcheng", "chapter": 14, "event": "返回彭城,入住旅馆" },
{ "location": "pengcheng", "chapter": 14, "event": "与沈落雁交手,冲破包围逃脱" },
{ "location": "pengcheng", "chapter": 15, "event": "入翠碧楼,遭沈落雁暗杀队伏击" },
{ "location": "pengcheng", "chapter": 16, "event": "赌场与杜伏威对峙,东溟派援救" }
]
},
{
"character": "沈落雁",
"color": "#FF1493",
"route": [
{ "location": "shuishui", "chapter": 13, "event": "船上生擒秦叔宝,与寇徐定赌约" },
{ "lat": 34.2, "lng": 117.3, "chapter": 13, "event": "追捕过程中,投放追踪粉末" },
{ "location": "pengcheng", "chapter": 14, "event": "追至彭城,多次围捕寇徐" },
{ "lat": 34.5, "lng": 117.5, "chapter": 14, "event": "房间内与寇徐交手,闯关失败" },
{ "location": "pengcheng", "chapter": 15, "event": "警告寇徐不要去青楼" },
{ "location": "pengcheng", "chapter": 16, "event": "赌场制止任媚媚,对阵杜伏威与东溟派" }
]
},
{
"character": "秦叔宝",
"color": "#FFB6C1",
"route": [
{ "location": "shuishui", "chapter": 13, "event": "河边遇寇仲徐子陵,被沈落雁生擒" },
{ "location": "pengcheng", "chapter": 13, "event": "到达彭城,参与赌赛" },
{ "location": "pengcheng", "chapter": 14, "event": "与寇徐分头逃走,最终被沈落雁所擒" },
{ "location": "pengcheng", "chapter": 16, "event": "已归降瓦岗军,与沈落雁配合" }
]
},
{
"character": "李世民",
"color": "#4169E1",
"route": [
{ "location": "luo_yang", "chapter": 14, "event": "与李渊船队从洛阳出发" },
{ "location": "shuishui", "chapter": 14, "event": "船上与寇徐相识,达成合作协议" },
{ "location": "pengcheng", "chapter": 14, "event": "船队到达彭城,继续谈判" }
]
},
{
"character": "杜伏威",
"color": "#FF8C00",
"route": [
{ "location": "pengcheng", "chapter": 16, "event": "在赌场突然出现,力压群雄" }
]
}
],
"key_events": [
{ "chapter": 12, "event": "寇徐二人目睹隋军暴行,开始习武对敌", "lat": 33.5, "lng": 117.0 },
{ "chapter": 12, "event": "寇徐参与战场,遇见沈落雁率领的青衣武士", "lat": 33.8, "lng": 117.2 },
{ "chapter": 13, "location": "pengcheng", "event": "秦叔宝、寇徐遇沈落雁于泗水,定下赌约" },
{ "chapter": 13, "event": "寇徐二人身上被沾追踪粉末,识破鸟儿追踪之法", "lat": 34.0, "lng": 117.2 },
{ "chapter": 13, "event": "渔夫莫成布局,寇徐潜水脱险,乘大船逃脱", "lat": 34.15, "lng": 117.25 },
{ "chapter": 14, "event": "莫成等人为李阀来客所阻,意识到形势变化", "lat": 34.15, "lng": 117.2 },
{ "chapter": 14, "event": "李世民在船上与寇徐相识,得知东溟夫人账簿秘密", "location": "shuishui" },
{ "chapter": 14, "event": "寇徐进城赴约,提防沈落雁陷阱", "location": "pengcheng" },
{ "chapter": 14, "event": "寇徐在旅馆遭沈落雁暴力追杀,破壁逃脱", "location": "pengcheng" },
{ "chapter": 15, "event": "寇徐计划攀城逃脱,购取钩子与绳索", "location": "pengcheng" },
{ "chapter": 15, "event": "入翠碧楼,遇香玉山与任媚媚,被施展媚术", "location": "pengcheng" },
{ "chapter": 16, "event": "赌场与任媚媚对赌,沈落雁突然现身", "location": "pengcheng" },
{ "chapter": 16, "event": "杜伏威突现赌场,压倒众人,欲带寇徐回家", "location": "pengcheng" },
{ "chapter": 16, "event": "东溟派护法单秀、单玉蝶现身,与杜伏威对峙", "location": "pengcheng" }
]
}

186
data/vol04.json Normal file
View File

@@ -0,0 +1,186 @@
{
"volume": "卷四",
"chapters": [
"第01章 志比天高",
"第02章 井边悟道",
"第03章 彗星北来",
"第04章 奇女青璇",
"第05章 宇文无敌",
"第06章 重会素素",
"第07章 避难学艺",
"第08章 笼中之鸟",
"第09章 衷诚合作",
"第10章 以怨报德",
"第11章 夜访青楼",
"第12章 大祸忽至"
],
"locations": [
{ "id": "baiyezeregion", "name": "巨野泽地区", "aliases": ["巨野", "大湖"], "type": "waterway", "lat": 35.0, "lng": 115.3, "description": "鲁西南运河沿线湖泽,寇徐与李世民分手处" },
{ "id": "dongpingg郡", "name": "东平郡", "aliases": ["东平"], "type": "city", "lat": 35.4, "lng": 115.8, "description": "运河沿线重镇,石青璇初现、跋锋寒约战欧阳希夷处,寇徐短暂隐居处" },
{ "id": "xingyang", "name": "荥阳", "aliases": ["荥阳城"], "type": "city", "lat": 34.79, "lng": 113.38, "description": "瓦岗军大龙头翟让根据地,大运河与黄河交汇处,兵家必争之地" },
{ "id": "luokou", "name": "洛口", "aliases": ["兴洛仓", "洛口仓"], "type": "landmark", "lat": 34.5, "lng": 113.1, "description": "大运河与黄河交汇处,隋朝最大粮仓,李密攻占处" },
{ "id": "luoyang", "name": "洛阳", "aliases": ["东都"], "type": "city", "lat": 34.62, "lng": 112.45, "description": "隋朝东都,王世充镇守处" },
{ "id": "hujie", "name": "虎牢", "aliases": ["虎牢关"], "type": "landmark", "lat": 34.78, "lng": 112.95, "description": "荥阳西要塞,裴仁基守兵处" },
{ "id": "xiangyang_region", "name": "香玉山来处", "type": "landmark", "lat": 31.0, "lng": 116.0, "description": "巴陵帮二当家萧铣活动地区" },
{ "id": "leshoucheng", "name": "乐寿", "aliases": ["乐寿城"], "type": "city", "lat": 38.5, "lng": 116.8, "description": "窦建德根据地,河北地区" }
],
"factions": [
{
"id": "wagang_army",
"name": "瓦岗军",
"type": "义军",
"color": "#FF6347",
"leader": "翟让(后由李密取而代之)",
"territory": ["xingyang", "luokou"],
"key_figures": ["翟让", "李密", "祖君彦", "沈落雁", "王伯当", "徐世绩"],
"description": "天下义军之首,翟让为名义大龙头,但李密实权逐渐上升,最终导致内争"
},
{
"id": "li_clan",
"name": "李阀",
"type": "门阀",
"color": "#DC143C",
"leader": "李渊",
"territory": [],
"key_figures": ["李渊", "李世民", "李秀宁"],
"description": "四姓门阀之一,李世民招募寇徐,图谋割据"
},
{
"id": "yuwen",
"name": "宇文阀",
"type": "门阀",
"color": "#4169E1",
"leader": "宇文伤",
"territory": ["daxing"],
"key_figures": ["宇文伤", "宇文化及", "宇文无敌", "宇文成都"],
"description": "四姓门阀之一,宇文无敌为高手,追捕寇徐账簿"
},
{
"id": "balingang",
"name": "巴陵帮",
"type": "江湖势力",
"color": "#9370DB",
"leader": "萧铣(二当家)",
"territory": [],
"key_figures": ["萧铣", "香玉山", "陆抗"],
"description": "八帮次席,经营青楼赌馆庞大网络,与宇文阿针对"
},
{
"id": "dou_jiande",
"name": "窦建德军",
"type": "义军",
"color": "#8B4513",
"leader": "窦建德",
"territory": ["leshoucheng"],
"key_figures": ["窦建德"],
"description": "河北黑道霸主,翟让往日命交"
},
{
"id": "dong_ming_pai",
"name": "东溟派",
"type": "江湖势力",
"color": "#FF8C00",
"leader": "东溟夫人",
"territory": [],
"key_figures": ["单婉晶", "尚明", "尚邦", "尚奎义"],
"description": "海上势力,贩运兵器,与李阀、宇文阀有贸易往来"
},
{
"id": "sui",
"name": "隋朝",
"type": "朝廷",
"color": "#FFD700",
"leader": "杨广(隋炀帝)",
"territory": ["luoyang", "yangzhou"],
"key_figures": ["杨广", "王世充", "刘长恭", "裴仁基"],
"description": "当朝政权,各地义军起义,气数将尽"
}
],
"character_routes": [
{
"character": "寇仲 & 徐子陵",
"color": "#FF4500",
"route": [
{ "location": "baiyezeregion", "chapter": 1, "event": "与李世民分手,寇仲立志争天下" },
{ "lat": 35.2, "lng": 115.5, "chapter": 1, "event": "两人在野外露宿,讨论人生理想" },
{ "location": "dongpingg郡", "chapter": 2, "event": "抵达东平郡,在酒楼与沈乃堂等遇见" },
{ "lat": 35.4, "lng": 115.8, "chapter": 3, "event": "在王府见证跋锋寒与欧阳希夷激战,听石青璇箫音" },
{ "location": "dongpingg郡", "chapter": 4, "event": "在王府柴房潜修七天,领悟武道真谛" },
{ "lat": 34.9, "lng": 114.5, "chapter": 5, "event": "途中遇宇文无敌,首次恶战获胜,伤敌不伤己" },
{ "location": "xingyang", "chapter": 6, "event": "抵达荥阳翟府,重逢素素" },
{ "location": "xingyang", "chapter": 7, "event": "在翟府学艺于屠叔方,被翟娇奴役于膳房" },
{ "location": "xingyang", "chapter": 8, "event": "与屠叔方交心,获得保护与指导" },
{ "location": "xingyang", "chapter": 9, "event": "在街上遇香玉山,洽谈合作协议" },
{ "location": "xingyang", "chapter": 10, "event": "见翟让,遭其掌击,进入合作双修阶段" },
{ "lat": 34.8, "lng": 113.2, "chapter": 11, "event": "赴黛青院见沈落雁,获悉翟让伪装让位计划" },
{ "location": "xingyang", "chapter": 12, "event": "李密发动突变,与素素逃离荥阳" }
]
},
{
"character": "翟让",
"color": "#FF1493",
"route": [
{ "location": "xingyang", "chapter": 6, "event": "大胜张须陀,声威如日中天" },
{ "location": "xingyang", "chapter": 10, "event": "被李密暗算受重伤,与寇徐二人激战" },
{ "location": "xingyang", "chapter": 11, "event": "伪装让位,设计延缓李密动作" },
{ "location": "xingyang", "chapter": 12, "event": "与李密决战,为寇徐逃脱争取时间" }
]
},
{
"character": "李密",
"color": "#8B0000",
"route": [
{ "location": "luokou", "chapter": 6, "event": "攻占兴洛仓,声望日增,威胁翟让地位" },
{ "location": "xingyang", "chapter": 10, "event": "暗算翟让,伤其经脉" },
{ "location": "xingyang", "chapter": 12, "event": "率军进攻翟府,发动最终决战" }
]
},
{
"character": "沈落雁",
"color": "#228B22",
"route": [
{ "location": "luokou", "chapter": 8, "event": "在洛口战场统军" },
{ "location": "xingyang", "chapter": 8, "event": "返回荥阳,对寇徐用刑逼供" },
{ "location": "xingyang", "chapter": 11, "event": "在黛青院与寇徐谈心,试探真实意图" },
{ "location": "xingyang", "chapter": 12, "event": "配合李密行动,发动突变" }
]
},
{
"character": "素素",
"color": "#FFB6C1",
"route": [
{ "location": "xingyang", "chapter": 6, "event": "在翟府与寇徐重逢,充当桥梁人物" },
{ "location": "xingyang", "chapter": 7, "event": "被王伯当所辱,心灵创伤" },
{ "location": "xingyang", "chapter": 10, "event": "陪伴寇徐,成为三人唯一纽带" },
{ "location": "xingyang", "chapter": 12, "event": "与寇徐逃离荥阳,踏上新征途" }
]
},
{
"character": "跋锋寒",
"color": "#DC143C",
"route": [
{ "location": "dongpingg郡", "chapter": 4, "event": "初临中原,与欧阳希夷激战,震撼众人" }
]
}
],
"key_events": [
{ "chapter": 1, "location": "baiyezeregion", "event": "寇仲立志争天下,与李世民和平分手" },
{ "chapter": 2, "location": "dongpingg郡", "event": "寇徐在井边悟道,领悟'不波井水'心法精髓" },
{ "chapter": 3, "location": "dongpingg郡", "event": "跋锋寒现身,与欧阳希夷激战,石青璇箫音化解恶斗" },
{ "chapter": 4, "location": "dongpingg郡", "event": "寇徐躲藏柴房七天,深化武学理解" },
{ "chapter": 5, "lat": 34.9, "lng": 114.5, "event": "寇徐与宇文无敌首次对战,双方受伤,分出高下" },
{ "chapter": 6, "location": "xingyang", "event": "寇徐抵达荥阳,与素素重逢,获翟让接纳" },
{ "chapter": 6, "location": "luokou", "event": "瓦岗军攻占兴洛仓,李密声望上升,翟让地位动摇" },
{ "chapter": 7, "location": "xingyang", "event": "寇徐在翟府学艺,屠叔方成为恩师" },
{ "chapter": 8, "location": "xingyang", "event": "沈落雁对寇徐进行盘问与试探,双方交集增加" },
{ "chapter": 9, "location": "xingyang", "event": "香玉山出现,代表巴陵帮提议合作对付宇文阀" },
{ "chapter": 10, "lat": 34.75, "lng": 113.4, "event": "翟让因内伤复发与寇徐激战,双方力量均衡" },
{ "chapter": 10, "location": "xingyang", "event": "翟让与李密之间的矛盾公开化,翟让拟将大权下交" },
{ "chapter": 11, "lat": 35.4, "lng": 115.8, "event": "寇徐赴黛青院见沈落雁,获悉翟让伪装让位的计谋" },
{ "chapter": 12, "location": "xingyang", "event": "李密发动突变进攻翟府,翟让与其决战,寇徐素素乘乱逃脱" }
]
}

183
data/vol05.json Normal file
View File

@@ -0,0 +1,183 @@
{
"volume": "卷五",
"chapters": ["第01章 仅以身免", "第02章 大隐于市", "第03章 影子刺客", "第04章 偷龙转凤", "第05章 情孽纠缠"],
"locations": [
{ "id": "yangzhou", "name": "扬州", "aliases": ["江都"], "type": "city", "lat": 32.39, "lng": 119.43, "description": "隋朝南都,长江与运河交汇的重镇,寇仲徐子陵的出生地" },
{ "id": "xingyang", "name": "荥阳", "type": "city", "lat": 34.79, "lng": 113.38, "description": "瓦岗军翟让的根据地,本卷主要舞台" },
{ "id": "wagang", "name": "瓦岗寨", "type": "town", "lat": 35.25, "lng": 114.7, "description": "翟让起义之地,大龙头府所在地" },
{ "id": "grand_canal", "name": "大运河", "aliases": ["运河","通济渠"], "type": "waterway", "lat": 33.5, "lng": 118.0, "description": "贯通南北的水路交通线" },
{ "id": "yangtze", "name": "长江", "aliases": ["大江"], "type": "waterway", "lat": 32.2, "lng": 119.0, "description": "中国第一大河" },
{ "id": "liyang", "name": "历阳", "type": "city", "lat": 31.7, "lng": 118.37, "description": "长江沿岸重镇,被杜伏威攻占,截断长江水道" },
{ "id": "luoyang", "name": "洛阳", "aliases": ["东都"], "type": "city", "lat": 34.62, "lng": 112.45, "description": "隋朝东都" },
{ "id": "daxing", "name": "大兴", "aliases": ["长安","京师"], "type": "city", "lat": 34.26, "lng": 108.94, "description": "隋朝西京,帝国首都" },
{ "id": "heiyang", "name": "黎阳仓", "type": "town", "lat": 35.8, "lng": 114.5, "description": "粮仓重镇,李密攻打的目标" }
],
"factions": [
{
"id": "wagang_army",
"name": "瓦岗军",
"type": "义军",
"color": "#FF6347",
"leader": "翟让(已死)、李密",
"territory": ["xingyang", "wagang"],
"key_figures": ["翟让(死于本卷开头)", "李密", "徐世绩", "沈落雁", "祖君彦"],
"description": "天下义军之首,翟让已遭李密属下击杀,内部出现严重分裂。李密接管势力,但仍有翟让旧部不服。现驻荥阳"
},
{
"id": "li_clan",
"name": "李阀",
"type": "门阀",
"color": "#DC143C",
"leader": "李渊",
"territory": [],
"key_figures": ["李渋"],
"description": "四姓门阀之一"
},
{
"id": "song_clan",
"name": "宋阀",
"type": "门阀",
"color": "#228B22",
"leader": "宋缺",
"territory": ["chengdu"],
"key_figures": ["宋缺", "宋师道", "宋玉致"],
"description": "四姓门阀之一,派宋玉致往荥阳联结瓦岗军,图谋联手对付杜伏威"
},
{
"id": "du_fuwei",
"name": "杜伏威军",
"type": "义军",
"color": "#FF8C00",
"leader": "杜伏威",
"territory": ["liyang"],
"key_figures": ["杜伏威", "辅公祏"],
"description": "江淮义军,占据历阳,截断长江盐船交通,成为宋阀和瓦岗军的共同威胁"
},
{
"id": "sui",
"name": "隋朝",
"type": "朝廷",
"color": "#FFD700",
"leader": "杨广(隋炀帝)",
"territory": [],
"key_figures": ["杨广"],
"description": "当朝政权,大军源源进驻江东,图谋收复江南"
},
{
"id": "baijing_bang",
"name": "巴陵帮",
"type": "江湖势力",
"color": "#DAA520",
"leader": "佩佩(香玉山)",
"territory": [],
"key_figures": ["佩佩", "香玉山", "云娘"],
"description": "荥阳黛青楼所属势力,老板娘佩佩为首领,受瓦岗军压制"
},
{
"id": "iron_serfs",
"name": "铁勒",
"type": "外族",
"color": "#696969",
"leader": "铁勒王",
"territory": [],
"key_figures": ["曲傲"],
"description": "西疆势力,与突厥为敌,派曲傲来中原联结势力,密谋刺杀李密"
}
],
"character_routes": [
{
"character": "寇仲 & 徐子陵",
"color": "#FF4500",
"route": [
{ "location": "wagang", "chapter": 1, "event": "大龙头府火烧翟让死讯传出,两人在府内逃脱" },
{ "lat": 34.79, "lng": 113.35, "chapter": 1, "event": "躲在水池假石山干井内,听李密与沈落雁商议" },
{ "location": "xingyang", "chapter": 2, "event": "潜入沈落雁香居落雁庄躲藏" },
{ "lat": 34.79, "lng": 113.38, "chapter": 2, "event": "在衣铺购买绸缎计划,通过素素寻找佩佩" },
{ "location": "xingyang", "chapter": 3, "event": "徐子陵在沈宅闺房遭杨虚彦重创,险些丧命" },
{ "lat": 34.79, "lng": 113.35, "chapter": 3, "event": "徐子陵伤重后潜逃,后向沈落雁诈降救素素" },
{ "location": "xingyang", "chapter": 4, "event": "两人互换名册,以论语骗沈落雁,救出素素" },
{ "location": "xingyang", "chapter": 5, "event": "躲入黛青楼,寻求佩佩帮助无果,改道潜入徐世绩府第探图" }
]
},
{
"character": "素素",
"color": "#FFB6C1",
"route": [
{ "location": "wagang", "chapter": 1, "event": "翟府大火中被寇仲背着逃出,躲在水池假石山干井" },
{ "location": "xingyang", "chapter": 2, "event": "进入沈落雁香居躲藏,后被沈府婢仆发现行迹异常" },
{ "location": "xingyang", "chapter": 2, "event": "以小婢身份赴衣铺购买绸缎,获取佩佩的联系方式" },
{ "location": "xingyang", "chapter": 3, "event": "在沈宅遭徐世绩逮捕,但寇仲以名册诈降救回" },
{ "location": "xingyang", "chapter": 5, "event": "躲入徐世绩府中大柜,被徐子陵以真气保护呼吸" }
]
},
{
"character": "李密",
"color": "#FF6347",
"route": [
{ "location": "wagang", "chapter": 1, "event": "率众火烧翟让大龙头府,杀翟让并接管瓦岗军" },
{ "lat": 34.79, "lng": 113.35, "chapter": 1, "event": "亲临现场搜捕漏网之人,对寇仲徐子陵下杀令" },
{ "location": "xingyang", "chapter": 2, "event": "率兵出城北上,攻打黎阳仓" }
]
},
{
"character": "沈落雁",
"color": "#228B22",
"route": [
{ "location": "wagang", "chapter": 1, "event": "与沈落雁在翟府外商议李密建议" },
{ "location": "xingyang", "chapter": 2, "event": "在落雁庄接待宋玉致,商议对付杜伏威的计划" },
{ "location": "xingyang", "chapter": 3, "event": "闺房遭杨虚彦暗袭,险被刺杀" },
{ "location": "xingyang", "chapter": 4, "event": "与寇仲在门前交易,以素素换名册,却被骗以论语替代" },
{ "location": "xingyang", "chapter": 5, "event": "与徐世绩商议搜捕计划,怀疑徐子陵未死" }
]
},
{
"character": "徐世绩",
"color": "#FF6347",
"route": [
{ "location": "wagang", "chapter": 1, "event": "参与火烧翟府,主持全城搜捕寇徐二人" },
{ "location": "xingyang", "chapter": 2, "event": "在沈宅设伏搜索,全城逐户逐街查缉" },
{ "location": "xingyang", "chapter": 5, "event": "与沈落雁商议扩大搜索范围,对感情生变而不悦" }
]
},
{
"character": "宋玉致",
"color": "#228B22",
"route": [
{ "location": "xingyang", "chapter": 2, "event": "自成都来荥阳谒见李密,与沈落雁商议联手对付杜伏威及曲傲" }
]
},
{
"character": "杨虚彦",
"color": "#000000",
"route": [
{ "location": "xingyang", "chapter": 3, "event": "躲在沈落雁闺房内暗袭,误将徐子陵作为沈落雁的人而重创他" }
]
},
{
"character": "佩佩",
"color": "#DAA520",
"route": [
{ "location": "xingyang", "chapter": 5, "event": "返回黛青楼闺房,遭沈落雁威胁,决定向其报告寇徐消息" }
]
}
],
"key_events": [
{ "chapter": 1, "location": "wagang", "event": "翟让大龙头府大火,翟让遭李密属下击杀,瓦岗军内部分裂" },
{ "chapter": 1, "lat": 34.79, "lng": 113.35, "event": "寇仲徐子陵救素素躲入水池假石山干井,听李密决议杀死两人" },
{ "chapter": 1, "event": "沈落雁对徐世绩表示寇徐两人功力日增,可能成为祸患", "lat": 34.79, "lng": 113.35 },
{ "chapter": 2, "location": "xingyang", "event": "三人潜入沈落雁的落雁庄,利用荥阳城的迷宫般小巷躲避搜查" },
{ "chapter": 2, "event": "宋玉致抵荥阳,向沈落雁透露曲傲密谋刺杀李密的计划", "lat": 34.79, "lng": 113.38 },
{ "chapter": 2, "event": "素素为获取佩佩联系方式,赴衣铺指定购买绸缎,险被沈落雁撞见", "lat": 34.79, "lng": 113.38 },
{ "chapter": 3, "location": "xingyang", "event": "沈宅内徐子陵遭影子刺客杨虚彦重创,险些丧命,寇仲救出" },
{ "chapter": 3, "event": "三人夜间逃离沈宅,躲入附近民居储物房,徐子陵通过真气疗伤恢复", "lat": 34.79, "lng": 113.35 },
{ "chapter": 4, "location": "xingyang", "event": "寇仲赴沈宅门前诈降,声称徐子陵已死,骗得沈落雁释放素素" },
{ "chapter": 4, "event": "寇仲以论语替代名册,骗过沈落雁的验证,成功救出素素", "lat": 34.79, "lng": 113.38 },
{ "chapter": 5, "location": "xingyang", "event": "三人躲入黛青楼,计划通过佩佩(香玉山)获得逃生帮助" },
{ "chapter": 5, "event": "佩佩被沈落雁威胁,决定向其通报寇徐行踪,望破灭", "lat": 34.79, "lng": 113.38 },
{ "chapter": 5, "event": "寇徐潜入徐世绩府中,在书室搜得荥阳城地下水沟图,寻得逃脱之路", "lat": 34.79, "lng": 113.38 }
]
}

507
frontend/App.vue Normal file
View File

@@ -0,0 +1,507 @@
<template>
<div class="map-container">
<div id="map"></div>
<div class="map-title">大唐双龙传 - 势力分布地图</div>
<!-- 势力图例 -->
<div class="legend">
<h3>势力图例</h3>
<div
v-for="faction in factions"
:key="faction.id"
class="legend-item"
@click="showFactionInfo(faction)"
>
<div class="legend-dot" :style="{ background: faction.color }"></div>
<span>{{ faction.name }}{{ faction.leader }}</span>
</div>
<div class="legend-section">
<h3>人物路线</h3>
<div
v-for="route in routes"
:key="route.character"
class="legend-item"
@click="toggleRoute(route)"
>
<div
class="legend-line"
:style="{
background: route.color,
opacity: selectedRoutes.includes(route.character) ? 1 : 0.4
}"
></div>
<span :style="{ opacity: selectedRoutes.includes(route.character) ? 1 : 0.5 }">{{ route.character }}</span>
</div>
</div>
</div>
<!-- 章节面板 -->
<div class="chapter-panel">
<h3>章节进度</h3>
<select v-model="selectedVolume" class="volume-select" @change="onVolumeChange">
<option v-for="(data, idx) in allData" :key="idx" :value="idx">
{{ data.volume }}
</option>
</select>
<input
type="range"
class="chapter-slider"
v-model.number="currentChapter"
:min="1"
:max="maxChapter"
@input="updateMap"
>
<div class="chapter-label"> {{ currentChapter }} / {{ maxChapter }} </div>
<div class="chapter-name">{{ currentChapterName }}</div>
</div>
<!-- 势力详情面板 -->
<div class="info-panel" v-if="selectedFaction">
<span class="close-btn" @click="selectedFaction = null">&times;</span>
<h3 :style="{ color: selectedFaction.color }">{{ selectedFaction.name }}</h3>
<p><b>首领</b>{{ selectedFaction.leader }}</p>
<p>{{ selectedFaction.description }}</p>
<p v-if="selectedFaction.territory && selectedFaction.territory.length">
<b>据点</b>{{ getTerritoryNames(selectedFaction.territory).join('、') }}
</p>
<p v-if="selectedFaction.key_figures && selectedFaction.key_figures.length">
<b>关键人物</b>{{ selectedFaction.key_figures.join('、') }}
</p>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import L from 'leaflet'
import 'leaflet/dist/leaflet.css'
const allData = ref([])
const factions = ref([])
const locations = ref([])
const routes = ref([])
const currentChapter = ref(1)
const selectedVolume = ref(0)
const selectedRoutes = ref([])
const selectedFaction = ref(null)
let map = null
let routeLayers = []
let locationMarkers = []
let territoryLayers = []
const currentData = computed(() => allData.value[selectedVolume.value] || {})
const chapters = computed(() => currentData.value.chapters || [])
const maxChapter = computed(() => chapters.value.length || 1)
const currentChapterName = computed(() => chapters.value[currentChapter.value - 1] || '')
onMounted(async () => {
await loadAllData()
initMap()
updateMap()
})
async function loadAllData() {
for (let i = 1; i <= 5; i++) {
const response = await fetch(`/data/vol${String(i).padStart(2, '0')}.json`)
const data = await response.json()
allData.value.push(data)
}
// 初始化第一卷数据
factions.value = allData.value[0]?.factions || []
locations.value = allData.value[0]?.locations || []
routes.value = allData.value[0]?.character_routes || []
currentChapter.value = maxChapter.value
}
function initMap() {
map = L.map('map', {
center: [32.0, 116.0],
zoom: 6,
zoomControl: true,
attributionControl: false
})
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
maxZoom: 18,
subdomains: 'abcd'
}).addTo(map)
L.control.attribution({ prefix: false, position: 'bottomright' })
.addAttribution('大唐双龙传 - 黄易')
.addTo(map)
}
function onVolumeChange() {
factions.value = currentData.value.factions || []
locations.value = currentData.value.locations || []
routes.value = currentData.value.character_routes || []
currentChapter.value = maxChapter.value
selectedRoutes.value = []
updateMap()
}
function updateMap() {
if (!map) return
// 确保当前章节在有效范围内
if (currentChapter.value > maxChapter.value) {
currentChapter.value = maxChapter.value
}
if (currentChapter.value < 1) {
currentChapter.value = 1
}
clearLayers()
drawTerritories()
drawLocations()
drawRoutes()
}
function clearLayers() {
routeLayers.forEach(layer => map.removeLayer(layer))
locationMarkers.forEach(layer => map.removeLayer(layer))
territoryLayers.forEach(layer => map.removeLayer(layer))
routeLayers = []
locationMarkers = []
territoryLayers = []
}
function drawTerritories() {
factions.value.forEach(f => {
if (!f.territory || !f.territory.length) return
f.territory.forEach(tid => {
const loc = locations.value.find(l => l.id === tid)
if (!loc) return
const circle = L.circle([loc.lat, loc.lng], {
radius: 50000,
color: f.color,
fillColor: f.color,
fillOpacity: 0.12,
weight: 2,
opacity: 0.5,
dashArray: '6 4'
}).addTo(map)
const label = L.marker([loc.lat + 0.35, loc.lng], {
icon: L.divIcon({
className: '',
html: `<div style="color:${f.color};font-size:11px;font-weight:bold;text-shadow:0 0 4px rgba(0,0,0,0.8);white-space:nowrap;text-align:center">${f.name}</div>`,
iconAnchor: [30, 8]
})
}).addTo(map)
territoryLayers.push(circle, label)
})
})
}
function drawLocations() {
locations.value.forEach(loc => {
let markerColor = '#aaa'
for (const f of factions.value) {
if (f.territory && f.territory.includes(loc.id)) {
markerColor = f.color
break
}
}
const size = loc.type === 'city' ? 10 : loc.type === 'landmark' ? 8 : 6
const icon = L.divIcon({
className: '',
html: `<div style="width:${size}px;height:${size}px;background:${markerColor};border:2px solid #fff;border-radius:50%;box-shadow:0 0 6px ${markerColor}"></div>`,
iconSize: [size, size],
iconAnchor: [size / 2, size / 2]
})
const marker = L.marker([loc.lat, loc.lng], { icon })
.bindPopup(`<b>${loc.name}</b><br>${loc.description || ''}`)
.addTo(map)
const nameLabel = L.marker([loc.lat - 0.15, loc.lng], {
icon: L.divIcon({
className: '',
html: `<div style="color:#ccc;font-size:11px;text-shadow:0 0 3px #000,0 0 6px #000;white-space:nowrap;text-align:center">${loc.name}</div>`,
iconAnchor: [30, 0]
})
}).addTo(map)
locationMarkers.push(marker, nameLabel)
})
}
function drawRoutes() {
routes.value.forEach(route => {
if (!selectedRoutes.value.includes(route.character)) return
const points = route.route || []
for (let i = 0; i < points.length - 1; i++) {
const p1 = getPointCoords(points[i])
const p2 = getPointCoords(points[i + 1])
if (!p1 || !p2) continue
const line = L.polyline([[p1.lat, p1.lng], [p2.lat, p2.lng]], {
color: route.color,
weight: 3,
opacity: 0.8
}).addTo(map)
routeLayers.push(line)
}
points.forEach((p, idx) => {
const coords = getPointCoords(p)
if (!coords) return
const isEnd = idx === points.length - 1
const isStart = idx === 0
const size = (isStart || isEnd) ? 7 : 5
const dot = L.circleMarker([coords.lat, coords.lng], {
radius: size,
color: route.color,
fillColor: isEnd ? '#fff' : route.color,
fillOpacity: 0.9,
weight: 2
})
.bindPopup(`<b>${route.character}</b><br>第${p.chapter}章:${p.event || ''}`)
.addTo(map)
routeLayers.push(dot)
})
})
}
function getPointCoords(point) {
if (point.lat && point.lng) {
return { lat: point.lat, lng: point.lng }
}
if (point.location) {
const loc = locations.value.find(l => l.id === point.location)
if (loc) {
return { lat: loc.lat, lng: loc.lng }
}
}
return null
}
function toggleRoute(route) {
const idx = selectedRoutes.value.indexOf(route.character)
if (idx > -1) {
selectedRoutes.value.splice(idx, 1)
} else {
selectedRoutes.value.push(route.character)
}
clearLayers()
drawTerritories()
drawLocations()
drawRoutes()
}
function showFactionInfo(faction) {
selectedFaction.value = faction
}
function getTerritoryNames(territoryIds) {
return territoryIds.map(tid => {
const loc = locations.value.find(l => l.id === tid)
return loc ? loc.name : tid
})
}
</script>
<style scoped>
/* 根容器必须 position:relative使绝对定位子元素相对它定位 */
.map-container {
position: relative;
width: 100%;
height: 100vh;
overflow: hidden;
}
#map {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
.map-title {
position: absolute;
top: 10px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
background: rgba(20, 20, 40, 0.9);
padding: 8px 24px;
border-radius: 8px;
border: 1px solid #c9a96e;
font-size: 20px;
font-weight: bold;
color: #c9a96e;
letter-spacing: 4px;
text-shadow: 0 0 10px rgba(201, 169, 110, 0.5);
pointer-events: none;
white-space: nowrap;
}
/* ====== 势力图例 ====== */
.legend {
position: absolute;
bottom: 20px;
left: 10px;
z-index: 1000;
background: rgba(20, 20, 40, 0.92);
padding: 12px 16px;
border-radius: 8px;
border: 1px solid #555;
max-width: 260px;
max-height: calc(100vh - 60px);
overflow-y: auto;
font-size: 13px;
line-height: 1.6;
color: #e0e0e0;
}
.legend h3 {
color: #c9a96e;
margin-bottom: 8px;
font-size: 14px;
border-bottom: 1px solid #444;
padding-bottom: 4px;
}
.legend-item {
display: flex;
align-items: center;
margin: 3px 0;
cursor: pointer;
}
.legend-item:hover {
opacity: 0.7;
}
.legend-dot {
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 8px;
flex-shrink: 0;
border: 1px solid rgba(255, 255, 255, 0.3);
}
.legend-line {
width: 20px;
height: 3px;
margin-right: 8px;
flex-shrink: 0;
border-radius: 2px;
}
.legend-section {
margin-top: 8px;
}
/* ====== 章节面板 ====== */
.chapter-panel {
position: absolute;
bottom: 20px;
right: 10px;
z-index: 1000;
background: rgba(20, 20, 40, 0.92);
padding: 12px 16px;
border-radius: 8px;
border: 1px solid #555;
width: 320px;
color: #e0e0e0;
}
.chapter-panel h3 {
color: #c9a96e;
margin-bottom: 8px;
font-size: 14px;
border-bottom: 1px solid #444;
padding-bottom: 4px;
}
.volume-select {
width: 100%;
margin-bottom: 8px;
padding: 4px 8px;
background: #333;
color: #e0e0e0;
border: 1px solid #555;
border-radius: 4px;
}
.chapter-slider {
width: 100%;
margin: 8px 0;
accent-color: #c9a96e;
}
.chapter-label {
font-size: 12px;
color: #aaa;
text-align: center;
}
.chapter-name {
font-size: 13px;
color: #e0e0e0;
text-align: center;
margin-top: 4px;
}
/* ====== 势力详情面板 ====== */
.info-panel {
position: absolute;
top: 60px;
right: 10px;
z-index: 1000;
background: rgba(20, 20, 40, 0.92);
padding: 12px 16px;
border-radius: 8px;
border: 1px solid #555;
width: 320px;
max-height: 50vh;
overflow-y: auto;
color: #e0e0e0;
}
.info-panel h3 {
color: #c9a96e;
margin-bottom: 6px;
font-size: 15px;
}
.info-panel .close-btn {
position: absolute;
top: 8px;
right: 12px;
cursor: pointer;
color: #aaa;
font-size: 16px;
}
.info-panel .close-btn:hover {
color: #fff;
}
.info-panel p {
font-size: 13px;
line-height: 1.6;
margin: 4px 0;
}
</style>

12
frontend/index.html Normal file
View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>大唐双龙传 - 势力分布地图</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/main.js"></script>
</body>
</html>

5
frontend/main.js Normal file
View File

@@ -0,0 +1,5 @@
import { createApp } from 'vue'
import App from './App.vue'
import './style.css'
createApp(App).mount('#app')

1214
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

18
frontend/package.json Normal file
View File

@@ -0,0 +1,18 @@
{
"name": "dt-map",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.4.0",
"leaflet": "^1.9.4"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.0",
"vite": "^5.0.0"
}
}

14
frontend/style.css Normal file
View File

@@ -0,0 +1,14 @@
* { margin: 0; padding: 0; box-sizing: border-box }
body { font-family: 'Microsoft YaHei', 'SimSun', sans-serif; background: #1a1a2e; color: #e0e0e0 }
#app { width: 100%; height: 100vh; overflow: hidden }
#map { width: 100%; height: 100vh }
.leaflet-popup-content-wrapper {
background: rgba(30,30,50,0.95) !important;
color: #e0e0e0 !important;
border: 1px solid #c9a96e !important;
border-radius: 6px !important
}
.leaflet-popup-tip { background: rgba(30,30,50,0.95) !important }
.leaflet-popup-content { font-size: 13px !important; line-height: 1.5 !important }
.leaflet-popup-content b { color: #c9a96e }

36
frontend/vite.config.js Normal file
View File

@@ -0,0 +1,36 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
export default defineConfig({
plugins: [
vue(),
// 开发时将 ../data 目录挂载到 /data 路径
{
name: 'serve-data-dir',
configureServer(server) {
server.middlewares.use('/data', (req, res, next) => {
const filePath = path.resolve(__dirname, '../data', req.url.replace(/^\//, '').split('?')[0])
try {
const content = fs.readFileSync(filePath, 'utf-8')
res.setHeader('Content-Type', 'application/json; charset=utf-8')
res.end(content)
} catch {
next()
}
})
}
}
],
server: {
port: 5173
},
build: {
outDir: resolve(__dirname, '../dist')
}
})

View File

@@ -1,443 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>大唐双龙传 - 势力分布地图(卷一)</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Microsoft YaHei', 'SimSun', sans-serif; background: #1a1a2e; color: #e0e0e0; }
#map { width: 100%; height: 100vh; }
/* Title overlay */
.map-title {
position: absolute; top: 10px; left: 50%; transform: translateX(-50%);
z-index: 1000; background: rgba(20,20,40,0.9); padding: 8px 24px;
border-radius: 8px; border: 1px solid #c9a96e;
font-size: 20px; font-weight: bold; color: #c9a96e;
letter-spacing: 4px; text-shadow: 0 0 10px rgba(201,169,110,0.5);
pointer-events: none;
}
/* Legend panel */
.legend {
position: absolute; bottom: 20px; left: 10px; z-index: 1000;
background: rgba(20,20,40,0.92); padding: 12px 16px;
border-radius: 8px; border: 1px solid #555; max-width: 260px;
font-size: 13px; line-height: 1.6;
}
.legend h3 { color: #c9a96e; margin-bottom: 8px; font-size: 14px; border-bottom: 1px solid #444; padding-bottom: 4px; }
.legend-item { display: flex; align-items: center; margin: 3px 0; cursor: pointer; }
.legend-item:hover { opacity: 0.7; }
.legend-dot { width: 12px; height: 12px; border-radius: 50%; margin-right: 8px; flex-shrink: 0; border: 1px solid rgba(255,255,255,0.3); }
.legend-line { width: 20px; height: 3px; margin-right: 8px; flex-shrink: 0; border-radius: 2px; }
.legend-section { margin-top: 8px; }
/* Chapter slider */
.chapter-panel {
position: absolute; bottom: 20px; right: 10px; z-index: 1000;
background: rgba(20,20,40,0.92); padding: 12px 16px;
border-radius: 8px; border: 1px solid #555; width: 320px;
}
.chapter-panel h3 { color: #c9a96e; margin-bottom: 8px; font-size: 14px; border-bottom: 1px solid #444; padding-bottom: 4px; }
.chapter-slider { width: 100%; margin: 8px 0; accent-color: #c9a96e; }
.chapter-label { font-size: 12px; color: #aaa; text-align: center; }
.chapter-name { font-size: 13px; color: #e0e0e0; text-align: center; margin-top: 4px; }
/* Info panel */
.info-panel {
position: absolute; top: 60px; right: 10px; z-index: 1000;
background: rgba(20,20,40,0.92); padding: 12px 16px;
border-radius: 8px; border: 1px solid #555; width: 320px;
max-height: 50vh; overflow-y: auto; display: none;
}
.info-panel h3 { color: #c9a96e; margin-bottom: 6px; font-size: 15px; }
.info-panel .close-btn {
position: absolute; top: 8px; right: 12px; cursor: pointer;
color: #aaa; font-size: 16px;
}
.info-panel .close-btn:hover { color: #fff; }
.info-panel p { font-size: 13px; line-height: 1.6; margin: 4px 0; }
.info-panel .tag { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 11px; margin-right: 4px; }
/* Custom popup */
.leaflet-popup-content-wrapper {
background: rgba(30,30,50,0.95) !important;
color: #e0e0e0 !important;
border: 1px solid #c9a96e !important;
border-radius: 6px !important;
}
.leaflet-popup-tip { background: rgba(30,30,50,0.95) !important; }
.leaflet-popup-content { font-size: 13px !important; line-height: 1.5 !important; }
.leaflet-popup-content b { color: #c9a96e; }
/* Route animation */
@keyframes dash { to { stroke-dashoffset: 0; } }
</style>
</head>
<body>
<div id="map"></div>
<div class="map-title">大唐双龙传 - 卷一势力分布图</div>
<div class="legend" id="legend">
<h3>势力图例</h3>
<div id="faction-legend"></div>
<div class="legend-section">
<h3>人物路线</h3>
<div id="route-legend"></div>
</div>
</div>
<div class="chapter-panel">
<h3>章节进度</h3>
<input type="range" class="chapter-slider" id="chapterSlider" min="1" max="11" value="11" step="1">
<div class="chapter-label"><span id="chapterNum">11</span> / 11 章</div>
<div class="chapter-name" id="chapterName">第11章 追兵忽至</div>
</div>
<div class="info-panel" id="infoPanel">
<span class="close-btn" onclick="document.getElementById('infoPanel').style.display='none'">&times;</span>
<div id="infoPanelContent"></div>
</div>
<script>
// ==================== DATA ====================
const chapters = [
"第01章 相依为命", "第02章 大祸临头", "第03章 远离扬州",
"第04章 纠缠不清", "第05章 晴天霹雳", "第06章 九玄大法",
"第07章 和氏之璧", "第08章 痛不欲生", "第09章 再上征途",
"第10章 奋不顾身", "第11章 追兵忽至"
];
const locations = [
{ id:"yangzhou", name:"扬州(江都)", type:"city", lat:32.39, lng:119.43, desc:"隋朝南都,运河与长江交汇的重镇。寇仲徐子陵出生地,宇文化及夺《长生诀》之地。", chapter:1 },
{ id:"danyang", name:"丹阳", type:"city", lat:32.0, lng:118.95, desc:"扬州上游最大城市,河道纵横的水城。三人遇宋师道之处。", chapter:5 },
{ id:"beipo", name:"北坡县", type:"town", lat:32.5, lng:119.3, desc:"扬州附近大县,寇徐冒充宇文家公子骗吃骗住。", chapter:4 },
{ id:"liyang", name:"历阳", type:"city", lat:31.7, lng:118.37, desc:"长江沿岸重镇,杜伏威大破隋军后占领,截断长江水路。", chapter:10 },
{ id:"luoyang", name:"洛阳(东都)", type:"city", lat:34.62, lng:112.45, desc:"隋朝东都,和氏璧传闻出现之地,寇徐目标目的地。", chapter:7 },
{ id:"daxing", name:"大兴(长安)", type:"city", lat:34.26, lng:108.94, desc:"隋朝西京,帝国首都。杨公宝库藏于京都跃马桥。", chapter:1 },
{ id:"xingyang", name:"荥阳", type:"city", lat:34.79, lng:113.38, desc:"瓦岗军翟让根据地。李密在此大败隋军、击杀张须陀。", chapter:10 },
{ id:"cuishan", name:"翠山镇", type:"town", lat:29.5, lng:116.8, desc:"鄱阳湖东,新安郡南的大镇。寇徐在老张饭馆打工三月。", chapter:9 },
{ id:"gaoyou", name:"高邮", type:"city", lat:32.78, lng:119.44, desc:"运河沿线城市,李靖约定的北上会合点。", chapter:11 },
{ id:"chengdu", name:"成都", type:"city", lat:30.57, lng:104.07, desc:"独尊堡解晖的基地,宋阀私盐运往此处。", chapter:7 },
{ id:"gaoji", name:"高鸡泊", type:"town", lat:37.5, lng:115.7, desc:"窦建德据此为基地,势力贯黄河,拥兵十万。", chapter:10 },
{ id:"valley", name:"傅君婥墓(山谷)", type:"landmark", lat:31.3, lng:118.0, desc:"傅君婥战死安葬之地。寇徐在此隐居练功,突破气机。", chapter:8 }
];
const factions = [
{ id:"sui", name:"隋朝", color:"#FFD700", leader:"杨广(隋炀帝)", territories:["yangzhou","luoyang","daxing","danyang","gaoyou"], desc:"控制三大重镇,但内忧外患,叛乱四起" },
{ id:"yuwen", name:"宇文阀", color:"#4169E1", leader:"宇文伤 / 宇文化及", territories:["daxing"], desc:"四大门阀之首,暗图复辟北周。宇文化及为禁卫总管" },
{ id:"song_clan", name:"宋阀", color:"#228B22", leader:"天刀·宋缺", territories:["chengdu"], desc:"南方汉族正统,私盐贸易暴利,势力暗增" },
{ id:"wagang", name:"瓦岗军", color:"#FF6347", leader:"翟让 / 李密", territories:["xingyang"], desc:"天下义军之首。李密声势凌驾翟让,暗藏内讧之患" },
{ id:"du_fuwei", name:"杜伏威军", color:"#FF8C00", leader:"杜伏威", territories:["liyang"], desc:"江淮义军,新占历阳,截断长江交通" },
{ id:"dou_jiande", name:"窦建德军", color:"#8B4513", leader:"窦建德", territories:["gaoji"], desc:"河北霸主,十万之众,势力直贯黄河" },
{ id:"li_clan", name:"李阀", color:"#DC143C", leader:"李渊", territories:[], desc:"四姓门阀之一,受杨广猜忌,尚未起事" },
{ id:"goguryeo", name:"高丽", color:"#00CED1", leader:"傅采林", territories:[], desc:"东北邻国,派傅君婥刺杀杨广" },
{ id:"turks", name:"突厥", color:"#2F4F4F", leader:"武尊·毕玄", territories:[], desc:"北方最大外患" }
];
const routes = [
{
name: "寇仲 & 徐子陵", color: "#FF4500", dashArray: null,
points: [
{ lat:32.39, lng:119.43, ch:1, label:"扬州:相依为命" },
{ lat:32.3, lng:119.2, ch:3, label:"经暗渠出城" },
{ lat:32.25, lng:119.1, ch:3, label:"跳入长江" },
{ lat:32.5, lng:119.3, ch:4, label:"北坡县冒充公子" },
{ lat:32.0, lng:118.95, ch:5, label:"丹阳遇宋师道" },
{ lat:31.8, lng:118.5, ch:6, label:"宋船上学九玄大法" },
{ lat:31.5, lng:118.2, ch:8, label:"逃离宋船" },
{ lat:31.3, lng:118.0, ch:8, label:"山谷:傅君婥战死" },
{ lat:29.5, lng:116.8, ch:9, label:"翠山镇打工三月" },
{ lat:31.5, lng:117.8, ch:10, label:"救素素、遇李靖" },
{ lat:31.9, lng:118.5, ch:11, label:"丹阳近郊遇追兵" }
]
},
{
name: "宇文化及", color: "#4169E1", dashArray: "8 4",
points: [
{ lat:33.5, lng:118.0, ch:1, label:"率五牙大舰南下" },
{ lat:32.39, lng:119.43, ch:1, label:"扬州击杀石龙" },
{ lat:32.0, lng:119.0, ch:3, label:"长江追击" },
{ lat:31.5, lng:118.2, ch:8, label:"山上决战受重伤" }
]
},
{
name: "傅君婥", color: "#E0E0E0", dashArray: "4 4",
points: [
{ lat:32.5, lng:119.35, ch:1, label:"扬州北郊杀焦邪" },
{ lat:32.25, lng:119.1, ch:3, label:"长江上救起二人" },
{ lat:32.5, lng:119.3, ch:4, label:"北坡县救人" },
{ lat:32.0, lng:118.95, ch:5, label:"丹阳" },
{ lat:31.8, lng:118.5, ch:7, label:"船上传功" },
{ lat:31.3, lng:118.0, ch:8, label:"山谷:战死" }
]
},
{
name: "李靖", color: "#00BFFF", dashArray: "6 3",
points: [
{ lat:31.7, lng:118.37, ch:10, label:"随杜伏威军驻历阳" },
{ lat:31.5, lng:117.8, ch:10, label:"射杀祈老大救人" },
{ lat:31.9, lng:118.5, ch:11, label:"丹阳近郊被重伤" }
]
}
];
// ==================== MAP INIT ====================
const map = L.map('map', {
center: [32.0, 116.0],
zoom: 6,
zoomControl: true,
attributionControl: false
});
// Dark tile layer
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
maxZoom: 18,
subdomains: 'abcd'
}).addTo(map);
// Attribution
L.control.attribution({ prefix: false, position: 'bottomright' })
.addAttribution('大唐双龙传 - 黄易')
.addTo(map);
// ==================== LAYERS ====================
const layerGroups = {};
let currentChapter = 11;
// Faction territory circles
const territoryLayers = [];
factions.forEach(f => {
f.territories.forEach(tid => {
const loc = locations.find(l => l.id === tid);
if (!loc) return;
const circle = L.circle([loc.lat, loc.lng], {
radius: 50000,
color: f.color,
fillColor: f.color,
fillOpacity: 0.12,
weight: 2,
opacity: 0.5,
dashArray: '6 4'
}).addTo(map);
circle._factionId = f.id;
circle._chapterMin = loc.chapter;
territoryLayers.push(circle);
// Faction label
const label = L.marker([loc.lat + 0.35, loc.lng], {
icon: L.divIcon({
className: '',
html: `<div style="color:${f.color};font-size:11px;font-weight:bold;text-shadow:0 0 4px rgba(0,0,0,0.8);white-space:nowrap;text-align:center">${f.name}</div>`,
iconAnchor: [30, 8]
})
}).addTo(map);
label._chapterMin = loc.chapter;
territoryLayers.push(label);
});
});
// Location markers
const locationMarkers = [];
const cityIcon = (color) => L.divIcon({
className: '',
html: `<div style="width:10px;height:10px;background:${color};border:2px solid #fff;border-radius:50%;box-shadow:0 0 6px ${color}"></div>`,
iconSize: [10, 10],
iconAnchor: [5, 5]
});
const townIcon = (color) => L.divIcon({
className: '',
html: `<div style="width:8px;height:8px;background:${color};border:1.5px solid #ccc;border-radius:50%;box-shadow:0 0 4px ${color}"></div>`,
iconSize: [8, 8],
iconAnchor: [4, 4]
});
const landmarkIcon = L.divIcon({
className: '',
html: `<div style="width:8px;height:8px;background:#FF69B4;border:1.5px solid #fff;transform:rotate(45deg);box-shadow:0 0 4px #FF69B4"></div>`,
iconSize: [8, 8],
iconAnchor: [4, 4]
});
locations.forEach(loc => {
// Determine controlling faction color
let markerColor = '#aaa';
for (const f of factions) {
if (f.territories.includes(loc.id)) { markerColor = f.color; break; }
}
const icon = loc.type === 'city' ? cityIcon(markerColor) :
loc.type === 'landmark' ? landmarkIcon : townIcon(markerColor);
const marker = L.marker([loc.lat, loc.lng], { icon })
.bindPopup(`<b>${loc.name}</b><br>${loc.desc}`)
.addTo(map);
marker._chapterMin = loc.chapter;
locationMarkers.push(marker);
// City name label
const nameLabel = L.marker([loc.lat - 0.15, loc.lng], {
icon: L.divIcon({
className: '',
html: `<div style="color:#ccc;font-size:11px;text-shadow:0 0 3px #000,0 0 6px #000;white-space:nowrap;text-align:center">${loc.name}</div>`,
iconAnchor: [30, 0]
})
}).addTo(map);
nameLabel._chapterMin = loc.chapter;
locationMarkers.push(nameLabel);
});
// Character routes
const routeLayers = [];
routes.forEach(r => {
// Create polyline segments per chapter
for (let i = 0; i < r.points.length - 1; i++) {
const p1 = r.points[i], p2 = r.points[i+1];
const maxCh = p2.ch;
const line = L.polyline([[p1.lat, p1.lng], [p2.lat, p2.lng]], {
color: r.color,
weight: 3,
opacity: 0.8,
dashArray: r.dashArray || undefined
}).addTo(map);
line._chapterMax = maxCh;
line._routeName = r.name;
routeLayers.push(line);
}
// Route point markers
r.points.forEach((p, idx) => {
const isEnd = idx === r.points.length - 1;
const isStart = idx === 0;
const size = (isStart || isEnd) ? 7 : 5;
const dot = L.circleMarker([p.lat, p.lng], {
radius: size,
color: r.color,
fillColor: isEnd ? '#fff' : r.color,
fillOpacity: 0.9,
weight: 2
}).bindPopup(`<b>${r.name}</b><br>第${p.ch}章:${p.label}`)
.addTo(map);
dot._chapterMax = p.ch;
dot._routeName = r.name;
routeLayers.push(dot);
});
});
// Waterways (simplified)
const grandCanal = L.polyline([
[39.9, 116.4], [37.5, 116.8], [35.0, 117.0], [34.62, 114.0],
[33.5, 118.0], [32.78, 119.44], [32.39, 119.43]
], { color: '#2a6496', weight: 1.5, opacity: 0.4, dashArray: '4 4' }).addTo(map);
const yangtze = L.polyline([
[30.57, 104.07], [29.5, 106.5], [30.5, 111.3], [30.6, 114.3],
[29.5, 116.0], [31.3, 118.0], [31.7, 118.37], [32.0, 118.95], [32.2, 119.2], [32.39, 119.43]
], { color: '#2a6496', weight: 2, opacity: 0.4 }).addTo(map);
// ==================== LEGEND ====================
const factionLegend = document.getElementById('faction-legend');
factions.forEach(f => {
if (f.territories.length === 0 && f.id !== 'li_clan') return;
const div = document.createElement('div');
div.className = 'legend-item';
div.innerHTML = `<div class="legend-dot" style="background:${f.color}"></div><span>${f.name}${f.leader}</span>`;
div.onclick = () => showFactionInfo(f);
factionLegend.appendChild(div);
});
const routeLegend = document.getElementById('route-legend');
routes.forEach(r => {
const div = document.createElement('div');
div.className = 'legend-item';
div.innerHTML = `<div class="legend-line" style="background:${r.color}"></div><span>${r.name}</span>`;
routeLegend.appendChild(div);
});
function showFactionInfo(f) {
const panel = document.getElementById('infoPanel');
const content = document.getElementById('infoPanelContent');
content.innerHTML = `
<h3 style="color:${f.color}">${f.name}</h3>
<p><b>首领:</b>${f.leader}</p>
<p>${f.desc}</p>
${f.territories.length ? `<p><b>据点:</b>${f.territories.map(t => {
const loc = locations.find(l => l.id === t);
return loc ? loc.name : t;
}).join('、')}</p>` : ''}
`;
panel.style.display = 'block';
}
// ==================== CHAPTER SLIDER ====================
const slider = document.getElementById('chapterSlider');
const chapterNumEl = document.getElementById('chapterNum');
const chapterNameEl = document.getElementById('chapterName');
slider.addEventListener('input', (e) => {
currentChapter = parseInt(e.target.value);
chapterNumEl.textContent = currentChapter;
chapterNameEl.textContent = chapters[currentChapter - 1];
updateVisibility();
});
function updateVisibility() {
// Territory layers
territoryLayers.forEach(layer => {
const show = !layer._chapterMin || layer._chapterMin <= currentChapter;
if (show) {
if (!map.hasLayer(layer)) map.addLayer(layer);
} else {
if (map.hasLayer(layer)) map.removeLayer(layer);
}
});
// Location markers
locationMarkers.forEach(marker => {
const show = !marker._chapterMin || marker._chapterMin <= currentChapter;
if (show) {
if (!map.hasLayer(marker)) map.addLayer(marker);
} else {
if (map.hasLayer(marker)) map.removeLayer(marker);
}
});
// Route layers
routeLayers.forEach(layer => {
const show = !layer._chapterMax || layer._chapterMax <= currentChapter;
if (show) {
if (!map.hasLayer(layer)) map.addLayer(layer);
} else {
if (map.hasLayer(layer)) map.removeLayer(layer);
}
});
}
// ==================== WATERWAY LABELS ====================
L.marker([33.0, 117.5], {
icon: L.divIcon({
className: '',
html: '<div style="color:#4a90b8;font-size:10px;opacity:0.6;transform:rotate(-30deg);white-space:nowrap">大运河</div>',
iconAnchor: [15, 5]
})
}).addTo(map);
L.marker([30.8, 113.0], {
icon: L.divIcon({
className: '',
html: '<div style="color:#4a90b8;font-size:10px;opacity:0.6;white-space:nowrap">长江</div>',
iconAnchor: [10, 5]
})
}).addTo(map);
// Initial state
updateVisibility();
</script>
</body>
</html>