前40卷地图
This commit is contained in:
267
frontend/App.vue
267
frontend/App.vue
@@ -20,7 +20,7 @@
|
||||
<div class="legend-section">
|
||||
<h3>人物路线</h3>
|
||||
<div
|
||||
v-for="route in mergedRoutes"
|
||||
v-for="route in visibleRouteDefs"
|
||||
:key="route.character"
|
||||
class="legend-item"
|
||||
@click="toggleRoute(route)"
|
||||
@@ -37,7 +37,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 卷进度面板 -->
|
||||
<!-- 卷册进度面板 -->
|
||||
<div class="chapter-panel">
|
||||
<h3>卷册进度</h3>
|
||||
<div class="vol-slider-wrap">
|
||||
@@ -46,7 +46,7 @@
|
||||
class="chapter-slider"
|
||||
v-model.number="currentVol"
|
||||
:min="1"
|
||||
:max="20"
|
||||
:max="40"
|
||||
@input="onVolSliderChange"
|
||||
>
|
||||
<div class="vol-ticks">
|
||||
@@ -59,7 +59,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="vol-info">
|
||||
当前:{{ currentVolLabel }} | 已加载 {{ loadedCount }} / 20 卷
|
||||
当前:{{ currentVolLabel }} | 已加载 {{ loadedCount }} / 40 卷
|
||||
</div>
|
||||
|
||||
<!-- 关键事件列表 -->
|
||||
@@ -91,10 +91,10 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 加载进度 -->
|
||||
<!-- 加载进度条 -->
|
||||
<div class="loading-bar" v-if="isLoading">
|
||||
<div class="loading-inner" :style="{ width: (loadedCount / 20 * 100) + '%' }"></div>
|
||||
<span class="loading-text">加载数据中… {{ loadedCount }}/20 卷</span>
|
||||
<div class="loading-inner" :style="{ width: (loadedCount / 40 * 100) + '%' }"></div>
|
||||
<span class="loading-text">加载数据中… {{ loadedCount }}/40 卷</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -104,30 +104,46 @@ import { ref, computed, onMounted, watch } from 'vue'
|
||||
import L from 'leaflet'
|
||||
import 'leaflet/dist/leaflet.css'
|
||||
|
||||
// ── 常量 ────────────────────────────────────────────────────
|
||||
const TOTAL_VOLS = 40
|
||||
const SPLIT_VOL = 21 // 从该卷起寇仲/徐子陵分离
|
||||
|
||||
// 合并路线名称(卷 1-20 使用)
|
||||
const MERGED_NAME = '寇仲 & 徐子陵'
|
||||
const MERGED_COLOR = '#FF6347'
|
||||
|
||||
// 分离路线定义(卷 21+ 使用)
|
||||
const KOUZHONG_NAME = '寇仲'
|
||||
const KOUZHONG_COLOR = '#FF4500'
|
||||
const XUZILING_NAME = '徐子陵'
|
||||
const XUZILING_COLOR = '#FFA07A'
|
||||
|
||||
// ── 响应式状态 ──────────────────────────────────────────────
|
||||
const allData = ref([]) // 原始卷数据,index 0 = vol01
|
||||
const currentVol = ref(1) // 1-20
|
||||
const allData = ref([]) // 原始卷数据,index 0 = vol01
|
||||
const currentVol = ref(1) // 1-40
|
||||
const selectedRoutes = ref([])
|
||||
const selectedFaction = ref(null)
|
||||
const isLoading = ref(true)
|
||||
const loadedCount = ref(0)
|
||||
const isLoading = ref(true)
|
||||
const loadedCount = ref(0)
|
||||
|
||||
// ── Leaflet 图层管理 ────────────────────────────────────────
|
||||
let map = null
|
||||
let layerGroups = {
|
||||
territories: null,
|
||||
locations: null,
|
||||
routes: null,
|
||||
events: null
|
||||
locations: null,
|
||||
routes: null,
|
||||
events: null
|
||||
}
|
||||
|
||||
// ── 滑块刻度 ────────────────────────────────────────────────
|
||||
// ── 滑块刻度(每10卷一个)──────────────────────────────────
|
||||
// 滑块范围 1-40,共 39 步
|
||||
// pct = (vol - 1) / (40 - 1) * 100
|
||||
const volTicks = [
|
||||
{ vol: 1, label: '卷一', pct: 0 },
|
||||
{ vol: 5, label: '卷五', pct: 21.05 },
|
||||
{ vol: 10, label: '卷十', pct: 47.37 },
|
||||
{ vol: 15, label: '卷十五', pct: 73.68 },
|
||||
{ vol: 20, label: '卷二十', pct: 100 }
|
||||
{ vol: 1, label: '卷一', pct: 0 },
|
||||
{ vol: 10, label: '卷十', pct: (9 / 39 * 100).toFixed(3) * 1 },
|
||||
{ vol: 20, label: '卷二十', pct: (19 / 39 * 100).toFixed(3) * 1 },
|
||||
{ vol: 30, label: '卷三十', pct: (29 / 39 * 100).toFixed(3) * 1 },
|
||||
{ vol: 40, label: '卷四十', pct: 100 }
|
||||
]
|
||||
|
||||
// ── 计算属性:当前卷标签 ────────────────────────────────────
|
||||
@@ -138,55 +154,174 @@ const currentVolLabel = computed(() => {
|
||||
|
||||
// ── 跨卷合并数据(截至 currentVol)──────────────────────────
|
||||
|
||||
// 地点:按 id 去重,取最新(最大卷号)的定义
|
||||
// 地点:按 id 去重,取最新卷定义
|
||||
const mergedLocations = computed(() => {
|
||||
const map = new Map()
|
||||
const locMap = new Map()
|
||||
for (let i = 0; i < currentVol.value; i++) {
|
||||
const d = allData.value[i]
|
||||
if (!d) continue
|
||||
for (const loc of (d.locations || [])) {
|
||||
map.set(loc.id, loc)
|
||||
locMap.set(loc.id, loc)
|
||||
}
|
||||
}
|
||||
return Array.from(map.values())
|
||||
return Array.from(locMap.values())
|
||||
})
|
||||
|
||||
// 势力:按 id 去重,取最新卷定义
|
||||
const mergedFactions = computed(() => {
|
||||
const map = new Map()
|
||||
const facMap = new Map()
|
||||
for (let i = 0; i < currentVol.value; i++) {
|
||||
const d = allData.value[i]
|
||||
if (!d) continue
|
||||
for (const f of (d.factions || [])) {
|
||||
map.set(f.id, f)
|
||||
facMap.set(f.id, f)
|
||||
}
|
||||
}
|
||||
return Array.from(map.values())
|
||||
return Array.from(facMap.values())
|
||||
})
|
||||
|
||||
// 人物路线:同名人物路点合并,每个点附带 vol 标记
|
||||
/**
|
||||
* 合并人物路线,处理寇仲/徐子陵在 vol21 前后的分合逻辑:
|
||||
*
|
||||
* 卷 1-20:数据中是 "寇仲 & 徐子陵",直接按名合并。
|
||||
*
|
||||
* 卷 21+(当 currentVol >= SPLIT_VOL):
|
||||
* - 数据中是独立的 "寇仲" 和 "徐子陵"。
|
||||
* - 前20卷的 "寇仲 & 徐子陵" 路线同时挂到这两条线上,
|
||||
* 这样路线就连续(两人共同行走的历史 → 各自后续独立路线)。
|
||||
*
|
||||
* 当 currentVol < SPLIT_VOL:只展示合并路线。
|
||||
* 当 currentVol >= SPLIT_VOL:只展示分离路线,前20卷合并段分别并入两人。
|
||||
*/
|
||||
const mergedRoutes = computed(() => {
|
||||
const charMap = new Map()
|
||||
for (let i = 0; i < currentVol.value; i++) {
|
||||
const d = allData.value[i]
|
||||
if (!d) continue
|
||||
const vol = i + 1
|
||||
for (const r of (d.character_routes || [])) {
|
||||
const name = r.character
|
||||
if (!charMap.has(name)) {
|
||||
charMap.set(name, { character: name, color: r.color, route: [] })
|
||||
}
|
||||
const existing = charMap.get(name)
|
||||
// 合并颜色(取最新)
|
||||
existing.color = r.color
|
||||
for (const pt of (r.route || [])) {
|
||||
existing.route.push({ ...pt, vol })
|
||||
const isSplit = currentVol.value >= SPLIT_VOL
|
||||
|
||||
if (!isSplit) {
|
||||
// ── 模式一:合并路线(卷 1-20)──────────────────────────
|
||||
const charMap = new Map()
|
||||
for (let i = 0; i < currentVol.value; i++) {
|
||||
const d = allData.value[i]
|
||||
if (!d) continue
|
||||
const vol = i + 1
|
||||
for (const r of (d.character_routes || [])) {
|
||||
const name = r.character
|
||||
if (!charMap.has(name)) {
|
||||
charMap.set(name, { character: name, color: r.color, route: [] })
|
||||
}
|
||||
const entry = charMap.get(name)
|
||||
entry.color = r.color
|
||||
for (const pt of (r.route || [])) {
|
||||
entry.route.push({ ...pt, vol })
|
||||
}
|
||||
}
|
||||
}
|
||||
return Array.from(charMap.values())
|
||||
} else {
|
||||
// ── 模式二:分离路线(卷 21+)───────────────────────────
|
||||
// 先收集前20卷的合并路线路点
|
||||
const earlyMergedPts = []
|
||||
for (let i = 0; i < SPLIT_VOL - 1; i++) {
|
||||
const d = allData.value[i]
|
||||
if (!d) continue
|
||||
const vol = i + 1
|
||||
for (const r of (d.character_routes || [])) {
|
||||
if (r.character === MERGED_NAME) {
|
||||
for (const pt of (r.route || [])) {
|
||||
earlyMergedPts.push({ ...pt, vol })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 再按角色名合并 vol21+ 的独立路线
|
||||
const splitMap = new Map()
|
||||
|
||||
// 初始化两条分离路线,并把早期合并路点作为前缀
|
||||
const initSplitRoute = (name, color) => {
|
||||
splitMap.set(name, {
|
||||
character: name,
|
||||
color,
|
||||
route: [...earlyMergedPts] // 共享早期合并路点
|
||||
})
|
||||
}
|
||||
initSplitRoute(KOUZHONG_NAME, KOUZHONG_COLOR)
|
||||
initSplitRoute(XUZILING_NAME, XUZILING_COLOR)
|
||||
|
||||
// 收集其他角色(非寇仲/徐子陵)在 vol21+ 中的路线
|
||||
const otherMap = new Map()
|
||||
|
||||
for (let i = SPLIT_VOL - 1; i < currentVol.value; i++) {
|
||||
const d = allData.value[i]
|
||||
if (!d) continue
|
||||
const vol = i + 1
|
||||
for (const r of (d.character_routes || [])) {
|
||||
const name = r.character
|
||||
if (name === KOUZHONG_NAME || name === XUZILING_NAME) {
|
||||
const entry = splitMap.get(name)
|
||||
entry.color = r.color
|
||||
for (const pt of (r.route || [])) {
|
||||
entry.route.push({ ...pt, vol })
|
||||
}
|
||||
} else {
|
||||
// 其他角色正常合并
|
||||
if (!otherMap.has(name)) {
|
||||
otherMap.set(name, { character: name, color: r.color, route: [] })
|
||||
}
|
||||
const entry = otherMap.get(name)
|
||||
entry.color = r.color
|
||||
for (const pt of (r.route || [])) {
|
||||
entry.route.push({ ...pt, vol })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 同时也收集前20卷中其他角色(非寇仲&徐子陵合并)的路线
|
||||
const otherEarlyMap = new Map()
|
||||
for (let i = 0; i < SPLIT_VOL - 1; i++) {
|
||||
const d = allData.value[i]
|
||||
if (!d) continue
|
||||
const vol = i + 1
|
||||
for (const r of (d.character_routes || [])) {
|
||||
if (r.character === MERGED_NAME) continue
|
||||
const name = r.character
|
||||
if (!otherEarlyMap.has(name)) {
|
||||
otherEarlyMap.set(name, { character: name, color: r.color, route: [] })
|
||||
}
|
||||
const entry = otherEarlyMap.get(name)
|
||||
entry.color = r.color
|
||||
for (const pt of (r.route || [])) {
|
||||
entry.route.push({ ...pt, vol })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 合并早期其他角色和后期其他角色
|
||||
const allOther = new Map([...otherEarlyMap])
|
||||
for (const [name, entry] of otherMap) {
|
||||
if (allOther.has(name)) {
|
||||
allOther.get(name).color = entry.color
|
||||
allOther.get(name).route.push(...entry.route)
|
||||
} else {
|
||||
allOther.set(name, entry)
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
...Array.from(splitMap.values()),
|
||||
...Array.from(allOther.values())
|
||||
]
|
||||
}
|
||||
return Array.from(charMap.values())
|
||||
})
|
||||
|
||||
/**
|
||||
* 图例中展示的路线定义(用于点击切换显隐)
|
||||
* 与 mergedRoutes 同步,但只保留 character/color
|
||||
*/
|
||||
const visibleRouteDefs = computed(() =>
|
||||
mergedRoutes.value.map(r => ({ character: r.character, color: r.color }))
|
||||
)
|
||||
|
||||
// 当前卷关键事件
|
||||
const currentKeyEvents = computed(() => {
|
||||
const d = allData.value[currentVol.value - 1]
|
||||
@@ -202,20 +337,52 @@ onMounted(async () => {
|
||||
|
||||
// 监听 currentVol 变化重绘
|
||||
watch(currentVol, () => {
|
||||
syncSelectedRoutesOnModeChange()
|
||||
drawAll()
|
||||
})
|
||||
|
||||
/**
|
||||
* 切换到卷21时,将已选中的 "寇仲 & 徐子陵" 替换为两个分离路线;
|
||||
* 切换回20以前时,将分离路线替换为合并路线。
|
||||
*/
|
||||
function syncSelectedRoutesOnModeChange() {
|
||||
const isSplit = currentVol.value >= SPLIT_VOL
|
||||
const hasMerged = selectedRoutes.value.includes(MERGED_NAME)
|
||||
const hasKouzhong = selectedRoutes.value.includes(KOUZHONG_NAME)
|
||||
const hasXuziling = selectedRoutes.value.includes(XUZILING_NAME)
|
||||
|
||||
if (isSplit && hasMerged) {
|
||||
// 合并 → 分离:把合并路线换成两条分离路线
|
||||
const idx = selectedRoutes.value.indexOf(MERGED_NAME)
|
||||
selectedRoutes.value.splice(idx, 1, KOUZHONG_NAME, XUZILING_NAME)
|
||||
} else if (!isSplit && (hasKouzhong || hasXuziling)) {
|
||||
// 分离 → 合并:把两条分离路线换成合并路线
|
||||
selectedRoutes.value = selectedRoutes.value.filter(
|
||||
n => n !== KOUZHONG_NAME && n !== XUZILING_NAME
|
||||
)
|
||||
if (!selectedRoutes.value.includes(MERGED_NAME)) {
|
||||
selectedRoutes.value.push(MERGED_NAME)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 数据加载 ────────────────────────────────────────────────
|
||||
async function loadAllData() {
|
||||
isLoading.value = true
|
||||
loadedCount.value = 0
|
||||
// 初始化数组长度,确保响应性
|
||||
allData.value = new Array(TOTAL_VOLS).fill(null)
|
||||
|
||||
const promises = []
|
||||
for (let i = 1; i <= 20; i++) {
|
||||
for (let i = 1; i <= TOTAL_VOLS; i++) {
|
||||
const volStr = String(i).padStart(2, '0')
|
||||
promises.push(
|
||||
fetch(`/data/vol${volStr}.json`)
|
||||
.then(r => r.json())
|
||||
.then(r => {
|
||||
if (!r.ok) throw new Error(`HTTP ${r.status}`)
|
||||
return r.json()
|
||||
})
|
||||
.then(data => {
|
||||
// 按索引顺序写入(保证位置正确)
|
||||
allData.value[i - 1] = data
|
||||
loadedCount.value++
|
||||
})
|
||||
@@ -227,7 +394,7 @@ async function loadAllData() {
|
||||
}
|
||||
await Promise.all(promises)
|
||||
isLoading.value = false
|
||||
// 触发响应式更新
|
||||
// 触发响应式更新(替换整个数组引用)
|
||||
allData.value = [...allData.value]
|
||||
}
|
||||
|
||||
@@ -374,9 +541,9 @@ function drawRoutes() {
|
||||
}
|
||||
|
||||
resolved.forEach((p, idx) => {
|
||||
const isEnd = idx === resolved.length - 1
|
||||
const isEnd = idx === resolved.length - 1
|
||||
const isStart = idx === 0
|
||||
const size = (isStart || isEnd) ? 7 : 5
|
||||
const size = (isStart || isEnd) ? 7 : 5
|
||||
|
||||
const dot = L.circleMarker([p.lat, p.lng], {
|
||||
radius: size,
|
||||
|
||||
Reference in New Issue
Block a user