@@ -26,7 +26,12 @@ import {
2626 ROUTES_STORAGE_KEY ,
2727} from '../constants/mapApp'
2828import { 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'
3035import { 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,
0 commit comments