Skip to content

Commit c198fa2

Browse files
fix: 优化点位编辑功能
1 parent 606d27e commit c198fa2

6 files changed

Lines changed: 286 additions & 45 deletions

File tree

src/App.vue

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ const {
7171
navigationWebSocketUrl,
7272
openEditLocation,
7373
pendingLocationChangeCount,
74+
pendingLocationFilterCount,
7475
previewImage,
7576
progress,
7677
publicAssetUrl,
@@ -88,6 +89,7 @@ const {
8889
segmentPoints,
8990
showFavoritesOnly,
9091
showIncompleteOnly,
92+
showPendingLocationChangesOnly,
9193
sidebarCollapsed,
9294
startSegment,
9395
statusMessage,
@@ -131,6 +133,15 @@ const {
131133
<button v-if="editorMode" type="button" :disabled="!pendingLocationChangeCount" @click="exportPendingLocationChanges">
132134
导出点位修改<span v-if="pendingLocationChangeCount">({{ pendingLocationChangeCount }})</span>
133135
</button>
136+
<button
137+
v-if="editorMode"
138+
type="button"
139+
:class="{ 'toolbar-button--active': showPendingLocationChangesOnly }"
140+
:disabled="!pendingLocationFilterCount"
141+
@click="showPendingLocationChangesOnly = !showPendingLocationChangesOnly"
142+
>
143+
当前修改<span v-if="pendingLocationFilterCount">({{ pendingLocationFilterCount }})</span>
144+
</button>
134145
<input ref="locationChangesImportInput" class="toolbar-file-input" type="file" accept="application/json,.json" @change="importLocationChanges" />
135146
<button :class="{ 'toolbar-button--active': routePanelOpen }" type="button" @click="routePanelOpen = !routePanelOpen">
136147
路线
@@ -384,8 +395,11 @@ const {
384395
<div v-if="editorFormOpen" class="modal-backdrop" @click.self="editorFormOpen = false">
385396
<form class="editor-form glass-panel" @submit.prevent="saveLocation">
386397
<div class="sidebar-heading"><h2>{{ editingLocationId ? '编辑点位' : '新建点位' }}</h2><button type="button" class="close-button" @click="editorFormOpen = false">×</button></div>
398+
<label>点位 ID<input v-model.trim="locationForm.locationId" :disabled="!!editingLocationId" placeholder="留空自动生成 local ID" /></label>
387399
<label>名称<input v-model="locationForm.name" required /></label>
388-
<label>区域<input v-model="locationForm.district" placeholder="全地图" /></label>
400+
<label>区域<select v-model="locationForm.district">
401+
<option v-for="district in districtOptions" :key="district" :value="district">{{ district }}</option>
402+
</select></label>
389403
<div class="form-grid"><label>LAT<input v-model.number="locationForm.lat" type="number" step="any" /></label><label>LNG<input v-model.number="locationForm.lng" type="number" step="any" /></label></div>
390404
<label>描述<textarea v-model="locationForm.description" rows="3" /></label>
391405
<label>搜索关键词(可选)<input v-model="locationForm.tagsText" placeholder="使用英文逗号分隔,用于辅助搜索" /></label>

src/composables/useMapApp.js

Lines changed: 112 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,12 @@ import {
2626
ROUTES_STORAGE_KEY,
2727
} from '../constants/mapApp'
2828
import { clone, publicAssetUrl } from '../utils/assets'
29-
import { normalizeNavigationHost, normalizeNavigationPort, parseNavigationWebSocketUrl } from '../utils/navigationEndpoint'
29+
import {
30+
normalizeNavigationHost,
31+
normalizeNavigationPort,
32+
normalizeNavigationProtocol,
33+
parseNavigationWebSocketUrl,
34+
} from '../utils/navigationEndpoint'
3035
import { readStoredIds, readStoredMapView, readStoredMarkerFilters } from '../utils/storage'
3136

3237
// 地图应用的主组合函数。App.vue 只关心模板,具体行为在这里按功能区维护。
@@ -41,12 +46,6 @@ export function useMapApp() {
4146
const bounds = L.latLngBounds([-MAP_HEIGHT, 0], [0, MAP_WIDTH])
4247
const isLocalEditor = import.meta.env.DEV
4348

44-
45-
46-
47-
48-
49-
5049
function getInitialCategories() {
5150
return new Set(visibleCategories.value.map((category) => category.id))
5251
}
@@ -86,6 +85,7 @@ export function useMapApp() {
8685
}
8786
return nextCategories
8887
})()
88+
const initialCategoryGroupLabels = new Set(categories.value.map((category) => category.group).filter(Boolean))
8989
const initialActiveDistricts = new Set(
9090
Array.isArray(storedMarkerFilters?.activeDistricts)
9191
? storedMarkerFilters.activeDistricts.map((district) => normalizeDistrictLabel(district)).filter(Boolean)
@@ -116,6 +116,7 @@ export function useMapApp() {
116116
? storedMarkerFilters.centerNavigationEnabled
117117
: true)
118118
const defaultNavigationEndpoint = parseNavigationWebSocketUrl(DEFAULT_NAVIGATION_WEBSOCKET_URL)
119+
const navigationProtocol = ref(normalizeNavigationProtocol(storedMarkerFilters?.navigationProtocol || defaultNavigationEndpoint.protocol))
119120
const navigationHost = ref(normalizeNavigationHost(storedMarkerFilters?.navigationHost || defaultNavigationEndpoint.host))
120121
const navigationPort = ref(normalizeNavigationPort(storedMarkerFilters?.navigationPort || defaultNavigationEndpoint.port))
121122
const coordinates = ref({ lat: 0, lng: 0 })
@@ -125,6 +126,7 @@ export function useMapApp() {
125126
const editorMode = ref(false)
126127
const editorFormOpen = ref(false)
127128
const editingLocationId = ref(null)
129+
const showPendingLocationChangesOnly = ref(false)
128130
const previewImage = ref('')
129131
const statusMessage = ref('')
130132
const routePanelOpen = ref(false)
@@ -150,12 +152,13 @@ export function useMapApp() {
150152
connecting: 'CONNECTING',
151153
disconnected: 'OFFLINE',
152154
})[navigationConnectionStatus.value])
153-
const navigationWebSocketUrl = computed(() => `ws://${normalizeNavigationHost(navigationHost.value)}:${normalizeNavigationPort(navigationPort.value)}`)
155+
const navigationWebSocketUrl = computed(() => `${normalizeNavigationProtocol(navigationProtocol.value)}://${normalizeNavigationHost(navigationHost.value)}:${normalizeNavigationPort(navigationPort.value)}`)
154156

155157
const emptyLocationForm = () => ({
158+
locationId: '',
156159
name: '',
157160
types: [],
158-
district: '',
161+
district: '全地图',
159162
lat: 0,
160163
lng: 0,
161164
description: '',
@@ -199,8 +202,14 @@ export function useMapApp() {
199202
const favoriteCount = computed(() => [...favoriteIds.value].filter((id) => visibleLocationIds.value.has(id)).length)
200203
const progress = computed(() => Math.round((completedCount.value / Math.max(visibleLocationIds.value.size, 1)) * 100))
201204
const pendingLocationChangeCount = computed(() => (
202-
pendingLocationChanges.value.upsertLocations.length + pendingLocationChanges.value.deletedLocationIds.length
205+
pendingLocationChanges.value.categories.length
206+
+ pendingLocationChanges.value.upsertLocations.length
207+
+ pendingLocationChanges.value.deletedLocationIds.length
208+
))
209+
const pendingLocationChangeIds = computed(() => new Set(
210+
pendingLocationChanges.value.upsertLocations.map((location) => location.id),
203211
))
212+
const pendingLocationFilterCount = computed(() => pendingLocationChangeIds.value.size)
204213
const districtOptions = computed(() => {
205214
const districts = [...new Set(locations.value.map((location) => normalizeDistrictLabel(location.district)).filter(Boolean))]
206215
return districts.sort((left, right) => {
@@ -235,9 +244,10 @@ export function useMapApp() {
235244
|| (districtLabel === '全地图' && isTeleportLocation(location))
236245
const incompleteVisible = !showIncompleteOnly.value || !completedIds.value.has(location.id)
237246
const favoriteVisible = !showFavoritesOnly.value || favoriteIds.value.has(location.id)
247+
const pendingVisible = !showPendingLocationChangesOnly.value || pendingLocationChangeIds.value.has(location.id)
238248
const typeLabels = location.types.map((type) => categoryLookup.value[type]?.label || type)
239249
const text = `${location.name} ${districtLabel} ${location.tags.join(' ')} ${typeLabels.join(' ')}`.toLowerCase()
240-
return categoryVisible && districtVisible && incompleteVisible && favoriteVisible && (!keyword || text.includes(keyword))
250+
return categoryVisible && districtVisible && incompleteVisible && favoriteVisible && pendingVisible && (!keyword || text.includes(keyword))
241251
})
242252
})
243253

@@ -327,6 +337,7 @@ export function useMapApp() {
327337
showFavoritesOnly: showFavoritesOnly.value,
328338
realtimeNavigationEnabled: realtimeNavigationEnabled.value,
329339
centerNavigationEnabled: centerNavigationEnabled.value,
340+
navigationProtocol: normalizeNavigationProtocol(navigationProtocol.value),
330341
navigationHost: normalizeNavigationHost(navigationHost.value),
331342
navigationPort: normalizeNavigationPort(navigationPort.value),
332343
districtFilterOpen: districtFilterOpen.value,
@@ -395,14 +406,46 @@ export function useMapApp() {
395406
}
396407
}
397408

409+
function normalizeCategoryGroup(category) {
410+
return String(category?.group || category?.groupLabel || '自定义')
411+
}
412+
413+
function minimalCategoryForExport(category, forceLabel = false) {
414+
const group = normalizeCategoryGroup(category)
415+
const exported = {
416+
id: category.id,
417+
group,
418+
}
419+
if (forceLabel && category.label) exported.label = category.label
420+
if (!initialCategoryGroupLabels.has(group)) exported.isNewGroup = true
421+
return exported
422+
}
423+
424+
function collectCategoriesForChanges(changes) {
425+
const exportedCategories = new Map()
426+
changes.categories?.forEach((category) => {
427+
if (category?.id) exportedCategories.set(category.id, minimalCategoryForExport(category, true))
428+
})
429+
changes.upsertLocations?.forEach((location) => {
430+
if (!Array.isArray(location.types)) return
431+
location.types.forEach((type) => {
432+
if (exportedCategories.has(type)) return
433+
const category = categoryLookup.value[type]
434+
if (category) exportedCategories.set(type, minimalCategoryForExport(category, sessionCreatedCategoryIds.has(type)))
435+
})
436+
})
437+
return [...exportedCategories.values()]
438+
}
439+
398440
function exportLocationChanges(changes) {
399441
const payload = {
400442
version: 1,
401443
type: 'location-changes',
402444
}
403-
if (changes.categories?.length) payload.categories = changes.categories
404-
if (changes.upsertLocations?.length) payload.upsertLocations = changes.upsertLocations
405-
if (changes.deletedLocationIds?.length) payload.deletedLocationIds = changes.deletedLocationIds
445+
const exportCategories = collectCategoriesForChanges(changes)
446+
if (exportCategories.length) payload.categories = exportCategories
447+
if (changes.upsertLocations?.length) payload.upsertLocations = clone(changes.upsertLocations)
448+
if (changes.deletedLocationIds?.length) payload.deletedLocationIds = [...changes.deletedLocationIds]
406449
downloadJson(payload, `MaaNTE-location-changes-${new Date().toISOString().slice(0, 10)}.json`)
407450
showStatus('点位修改 JSON 已导出')
408451
}
@@ -850,6 +893,7 @@ export function useMapApp() {
850893
}
851894

852895
function applyNavigationEndpoint() {
896+
navigationProtocol.value = normalizeNavigationProtocol(navigationProtocol.value)
853897
navigationHost.value = normalizeNavigationHost(navigationHost.value)
854898
navigationPort.value = normalizeNavigationPort(navigationPort.value)
855899
persistMarkerFilters()
@@ -991,7 +1035,12 @@ export function useMapApp() {
9911035
function normalizeLocationChanges(payload) {
9921036
if (!payload || payload.type !== 'location-changes') throw new Error('invalid location changes')
9931037
const categories = Array.isArray(payload.categories)
994-
? payload.categories.filter((category) => category && typeof category === 'object' && typeof category.id === 'string')
1038+
? payload.categories
1039+
.filter((category) => category && typeof category === 'object' && typeof category.id === 'string')
1040+
.map((category) => ({
1041+
...category,
1042+
group: normalizeCategoryGroup(category),
1043+
}))
9951044
: []
9961045
const upsertLocations = Array.isArray(payload.upsertLocations)
9971046
? payload.upsertLocations.filter((location) => location && typeof location === 'object' && typeof location.id === 'string')
@@ -1010,8 +1059,26 @@ export function useMapApp() {
10101059
const changes = normalizeLocationChanges(JSON.parse(await file.text()))
10111060
changes.categories.forEach((category) => {
10121061
const index = categories.value.findIndex((item) => item.id === category.id)
1013-
if (index >= 0) mapData.value.categories.splice(index, 1, category)
1014-
else mapData.value.categories.push(category)
1062+
const { id, group, label, icon, iconUrl, color, isDefault, isHidden } = category
1063+
if (index >= 0) {
1064+
const current = categories.value[index]
1065+
mapData.value.categories.splice(index, 1, {
1066+
...current,
1067+
group,
1068+
...(label ? { label } : {}),
1069+
})
1070+
} else {
1071+
mapData.value.categories.push({
1072+
id,
1073+
group,
1074+
label: label || id,
1075+
icon: icon || '·',
1076+
...(iconUrl ? { iconUrl } : {}),
1077+
color: color || '#87a9ff',
1078+
isDefault: Boolean(isDefault),
1079+
...(typeof isHidden === 'boolean' ? { isHidden } : {}),
1080+
})
1081+
}
10151082
})
10161083
changes.upsertLocations.forEach((location) => {
10171084
const index = locations.value.findIndex((item) => item.id === location.id)
@@ -1109,7 +1176,12 @@ export function useMapApp() {
11091176
// 点位编辑器:新增、编辑、删除和图片上传。
11101177
function openCreateLocation(point) {
11111178
editingLocationId.value = null
1112-
locationForm.value = { ...emptyLocationForm(), ...point, types: visibleCategories.value.length ? [visibleCategories.value[0].id] : [] }
1179+
locationForm.value = {
1180+
...emptyLocationForm(),
1181+
...point,
1182+
district: districtOptions.value.includes(point?.district) ? point.district : '全地图',
1183+
types: visibleCategories.value.length ? [visibleCategories.value[0].id] : [],
1184+
}
11131185
editorFormOpen.value = true
11141186
}
11151187

@@ -1118,6 +1190,10 @@ export function useMapApp() {
11181190
locationForm.value = {
11191191
...emptyLocationForm(),
11201192
...clone(location),
1193+
locationId: location.id,
1194+
district: districtOptions.value.includes(normalizeDistrictLabel(location.district))
1195+
? normalizeDistrictLabel(location.district)
1196+
: '全地图',
11211197
tagsText: location.tags.join(', '),
11221198
images: [...location.images],
11231199
}
@@ -1157,15 +1233,20 @@ export function useMapApp() {
11571233
showStatus('请填写名称并选择至少一个类型')
11581234
return
11591235
}
1236+
const isNewLocation = !editingLocationId.value
1237+
const locationId = editingLocationId.value || form.locationId.trim() || `local-${Date.now()}`
1238+
if (isNewLocation && locations.value.some((location) => location.id === locationId)) {
1239+
showStatus('点位 ID 已存在')
1240+
return
1241+
}
11601242
const addedCategories = clone(form.pendingCustomTypes)
11611243
mapData.value.categories.push(...addedCategories)
11621244
addedCategories.forEach((category) => sessionCreatedCategoryIds.add(category.id))
1163-
const isNewLocation = !editingLocationId.value
11641245
const saved = {
1165-
id: editingLocationId.value || `local-${Date.now()}`,
1246+
id: locationId,
11661247
name: form.name.trim(),
11671248
types: [...form.types],
1168-
district: form.district.trim() || '全地图',
1249+
district: districtOptions.value.includes(form.district) ? form.district : '全地图',
11691250
lat: Number(form.lat),
11701251
lng: Number(form.lng),
11711252
description: form.description.trim(),
@@ -1218,10 +1299,11 @@ export function useMapApp() {
12181299
const files = [...event.target.files]
12191300
for (const file of files) {
12201301
try {
1302+
const dataUrl = await readFileAsDataUrl(file)
12211303
const response = await fetch('/api/upload-image', {
12221304
method: 'POST',
12231305
headers: { 'Content-Type': 'application/json' },
1224-
body: JSON.stringify({ dataUrl: await readFileAsDataUrl(file), name: file.name }),
1306+
body: JSON.stringify({ dataUrl, name: file.name }),
12251307
})
12261308
const data = await response.json()
12271309
if (!data.ok) throw new Error(data.error)
@@ -1320,6 +1402,12 @@ export function useMapApp() {
13201402
watch(activeDistricts, persistMarkerFilters, { deep: true })
13211403
watch(activeRouteId, () => nextTick(renderRouteArrows))
13221404
watch([() => [...activeCategories.value], keepTeleportEnabled, showIncompleteOnly, showFavoritesOnly], persistMarkerFilters)
1405+
watch(editorMode, () => {
1406+
if (!editorMode.value) showPendingLocationChangesOnly.value = false
1407+
})
1408+
watch(pendingLocationFilterCount, () => {
1409+
if (!pendingLocationFilterCount.value) showPendingLocationChangesOnly.value = false
1410+
})
13231411
watch(mergeAdjacentLocationsEnabled, () => {
13241412
persistMarkerFilters()
13251413
rebuildMarkerLayer()
@@ -1462,6 +1550,7 @@ export function useMapApp() {
14621550
navigationWebSocketUrl,
14631551
openEditLocation,
14641552
pendingLocationChangeCount,
1553+
pendingLocationFilterCount,
14651554
previewImage,
14661555
progress,
14671556
publicAssetUrl,
@@ -1479,6 +1568,7 @@ export function useMapApp() {
14791568
segmentPoints,
14801569
showFavoritesOnly,
14811570
showIncompleteOnly,
1571+
showPendingLocationChangesOnly,
14821572
sidebarCollapsed,
14831573
startSegment,
14841574
statusMessage,

src/data/map-data.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14520,7 +14520,7 @@
1452014520
},
1452114521
{
1452214522
"id": "tower-004",
14523-
"name": "维特海��塔 #004",
14523+
"name": "维特海����塔 #004",
1452414524
"types": [
1452514525
"tower"
1452614526
],

0 commit comments

Comments
 (0)