Browse Source

fix: make route editor map resize and fit after layout

Wait for editor host size, switch 2D mercator, and refit after
resize so short routes are visible when editing.
main
xiaosi 2 weeks ago
parent
commit
72d3bed3a3
  1. 11
      src/core/mapHelper.js
  2. 9
      src/styles/prototype.css
  3. 88
      src/views/TasksView/TasksView.vue

11
src/core/mapHelper.js

@ -19,6 +19,11 @@ class MapHelper {
if (!this._map) return if (!this._map) return
if (is3D) { if (is3D) {
try {
this._map.setProjection('globe')
} catch (_) {
/* ignore */
}
this._map.easeTo({ pitch: 45, bearing: 0, duration: 0 }) this._map.easeTo({ pitch: 45, bearing: 0, duration: 0 })
this._map.dragRotate.enable() this._map.dragRotate.enable()
this._map.touchZoomRotate.enableRotation() this._map.touchZoomRotate.enableRotation()
@ -27,6 +32,12 @@ class MapHelper {
} }
if (is2D) { if (is2D) {
// 航线/回放编辑需要平面投影;globe 下短航线 fitBounds 易失效
try {
this._map.setProjection('mercator')
} catch (_) {
/* ignore */
}
this._map.easeTo({ pitch: 0, bearing: 0, duration: 0 }) this._map.easeTo({ pitch: 0, bearing: 0, duration: 0 })
this._map.dragRotate.disable() this._map.dragRotate.disable()
this._map.touchZoomRotate.disableRotation() this._map.touchZoomRotate.disableRotation()

9
src/styles/prototype.css

@ -309,6 +309,11 @@ svg:not(.t-icon){ width: 18px; height: 18px; fill: none; stroke: currentColor; s
.business-workspace{ flex: 1; min-height: 440px; display: flex; flex-direction: column; overflow: hidden; border: 1px solid var(--line); border-radius: 6px; background: #fff; } .business-workspace{ flex: 1; min-height: 440px; display: flex; flex-direction: column; overflow: hidden; border: 1px solid var(--line); border-radius: 6px; background: #fff; }
.business-panel{ display: none; flex: 1; min-height: 0; overflow: auto; } .business-panel{ display: none; flex: 1; min-height: 0; overflow: auto; }
.business-panel.active{ display: block; } .business-panel.active{ display: block; }
.business-panel.route-library.active{
display: flex;
flex-direction: column;
overflow: hidden;
}
.business-toolbar{ min-height: 58px; padding: 11px 16px; display: flex; align-items: center; justify-content: space-between; gap: 12px; border-bottom: 1px solid var(--line); } .business-toolbar{ min-height: 58px; padding: 11px 16px; display: flex; align-items: center; justify-content: space-between; gap: 12px; border-bottom: 1px solid var(--line); }
.business-toolbar > div{ display: flex; align-items: center; gap: 8px; } .business-toolbar > div{ display: flex; align-items: center; gap: 8px; }
.business-toolbar select, .task-dialog select{ height: 34px; padding: 0 28px 0 9px; border: 1px solid var(--line); border-radius: 4px; color: #536270; background: #fff; font-size: 11px; outline: 0; } .business-toolbar select, .task-dialog select{ height: 34px; padding: 0 28px 0 9px; border: 1px solid var(--line); border-radius: 4px; color: #536270; background: #fff; font-size: 11px; outline: 0; }
@ -352,7 +357,7 @@ svg:not(.t-icon){ width: 18px; height: 18px; fill: none; stroke: currentColor; s
.route-card-actions{ margin-top: 8px; display: flex; align-items: center; gap: 2px; } .route-card-actions{ margin-top: 8px; display: flex; align-items: center; gap: 2px; }
.route-card-actions .t-button{ height: 28px; } .route-card-actions .t-button{ height: 28px; }
.route-library-view[hidden], .route-editor[hidden]{ display: none !important; } .route-library-view[hidden], .route-editor[hidden]{ display: none !important; }
.route-editor{ height: 100%; min-height: 520px; display: flex; flex-direction: column; background: #fff; }
.route-editor{ flex: 1; min-height: 0; height: 100%; display: flex; flex-direction: column; background: #fff; }
.route-editor-header{ height: 58px; min-height: 58px; padding: 0 14px; display: grid; grid-template-columns: 150px 1fr auto; align-items: center; gap: 12px; border-bottom: 1px solid var(--line); } .route-editor-header{ height: 58px; min-height: 58px; padding: 0 14px; display: grid; grid-template-columns: 150px 1fr auto; align-items: center; gap: 12px; border-bottom: 1px solid var(--line); }
.route-editor-header .back-command{ grid-column: auto; } .route-editor-header .back-command{ grid-column: auto; }
.route-editor-header > div:nth-child(2){ min-width: 0; display: flex; align-items: baseline; gap: 9px; } .route-editor-header > div:nth-child(2){ min-width: 0; display: flex; align-items: baseline; gap: 9px; }
@ -482,7 +487,7 @@ svg:not(.t-icon){ width: 18px; height: 18px; fill: none; stroke: currentColor; s
.waypoint-item-actions button:last-child:hover{ color: var(--red); background: #fff0f0; } .waypoint-item-actions button:last-child:hover{ color: var(--red); background: #fff0f0; }
.waypoint-item-actions button svg{ width: 11px; } .waypoint-item-actions button svg{ width: 11px; }
.route-form-error{ min-height: 25px; padding-top: 8px; color: var(--red); font-size: 10px; } .route-form-error{ min-height: 25px; padding-top: 8px; color: var(--red); font-size: 10px; }
.route-planner-map{ position: relative; min-width: 0; min-height: 430px; overflow: hidden; background: #75887a; cursor: crosshair; isolation: isolate; }
.route-planner-map{ position: relative; min-width: 0; min-height: 430px; height: 100%; overflow: hidden; background: #75887a; cursor: crosshair; isolation: isolate; }
.route-planner-map > #map, .route-planner-map > .mapboxgl-map{ .route-planner-map > #map, .route-planner-map > .mapboxgl-map{
position: absolute !important; position: absolute !important;
inset: 0; inset: 0;

88
src/views/TasksView/TasksView.vue

@ -674,22 +674,64 @@ function syncRouteLayer({ fit = false } = {}) {
if (fit && coords.length) fitRouteInView(coords) if (fit && coords.length) fitRouteInView(coords)
} }
function fitRouteInView(coords) { function fitRouteInView(coords) {
if (!routeMapInstance || !coords?.length) return if (!routeMapInstance || !coords?.length) return
// fit wait + resize
const canvas = routeMapInstance.getCanvas?.()
if (canvas && (canvas.clientWidth < 2 || canvas.clientHeight < 2)) return false
if (coords.length === 1) { if (coords.length === 1) {
routeMapInstance.easeTo({
routeMapInstance.jumpTo({
center: coords[0], center: coords[0],
zoom: Math.max(routeMapInstance.getZoom(), 16), zoom: Math.max(routeMapInstance.getZoom(), 16),
duration: 400,
pitch: 0,
bearing: 0,
}) })
return
return true
} }
const bounds = coords.reduce( const bounds = coords.reduce(
(b, c) => b.extend(c), (b, c) => b.extend(c),
new mapboxgl.LngLatBounds(coords[0], coords[0]), new mapboxgl.LngLatBounds(coords[0], coords[0]),
) )
routeMapInstance.fitBounds(bounds, { padding: 80, duration: 400, maxZoom: 17 })
routeMapInstance.fitBounds(bounds, {
padding: 80,
duration: 0,
maxZoom: 17,
pitch: 0,
bearing: 0,
})
return true
}
async function waitForMapHostSize(el, tries = 40) {
if (!el) return false
for (let i = 0; i < tries; i++) {
if (el.clientWidth > 10 && el.clientHeight > 10) return true
await new Promise((r) => requestAnimationFrame(r))
}
return el.clientWidth > 0 && el.clientHeight > 0
}
function scheduleRouteSync({ fit = false } = {}) {
const map = routeMapInstance
if (!map) return
const run = (shouldFit) => {
if (!routeMapAttached || routeMapInstance !== map) return
try {
map.resize()
} catch (_) {
/* ignore */
}
syncRouteLayer({ fit: shouldFit })
}
// rAF hidden / resize + fit
requestAnimationFrame(() => {
requestAnimationFrame(() => {
run(fit)
if (!fit) return
// ResizeObserver/ fit 0
requestAnimationFrame(() => run(true))
})
})
} }
function onRouteMapClick(e) { function onRouteMapClick(e) {
@ -732,6 +774,9 @@ async function attachRouteMap() {
return return
} }
// display:none 宿
await waitForMapHostSize(host)
routeMapInstance = map routeMapInstance = map
mapHelper.replaceMapContainer(host) mapHelper.replaceMapContainer(host)
mapHelper.toggleMapMode({ is2D: true }) mapHelper.toggleMapMode({ is2D: true })
@ -748,12 +793,9 @@ async function attachRouteMap() {
const c = map.getCenter() const c = map.getCenter()
routeCursor.value = formatCoord(c.lng, c.lat) routeCursor.value = formatCoord(c.lng, c.lat)
routeMapReady.value = true routeMapReady.value = true
requestAnimationFrame(() => {
map.resize()
const shouldFit = pendingFitRoute
pendingFitRoute = false
syncRouteLayer({ fit: shouldFit })
})
const shouldFit = pendingFitRoute
pendingFitRoute = false
scheduleRouteSync({ fit: shouldFit })
} }
if (map.isStyleLoaded()) { if (map.isStyleLoaded()) {
@ -776,6 +818,13 @@ function teardownRouteMap() {
} }
if (routeMapAttached) { if (routeMapAttached) {
mapHelper.restoreMapContainer() mapHelper.restoreMapContainer()
// globe
mapHelper.toggleMapMode({ is3D: true })
try {
routeMapInstance?.easeTo?.({ pitch: 0, bearing: 0, duration: 0 })
} catch (_) {
/* ignore */
}
routeMapAttached = false routeMapAttached = false
} }
routeMapInstance = null routeMapInstance = null
@ -802,11 +851,17 @@ async function openRouteEditorForEdit(route) {
const detail = await request.get(urls.ROUTE(route.id)) const detail = await request.get(urls.ROUTE(route.id))
resetRouteEditor() resetRouteEditor()
editingRouteId.value = String(detail?.id ?? route.id) editingRouteId.value = String(detail?.id ?? route.id)
const list = Array.isArray(detail?.waypoints) ? detail.waypoints : []
routePoints.value = list.map(mapApiWaypointToPoint)
const list = Array.isArray(detail?.waypoints)
? detail.waypoints
: Array.isArray(detail?.route?.waypoints)
? detail.route.waypoints
: []
routePoints.value = list.map(mapApiWaypointToPoint).filter(
(p) => Number.isFinite(p.longitude) && Number.isFinite(p.latitude),
)
const first = routePoints.value[0] const first = routePoints.value[0]
Object.assign(routeForm, { Object.assign(routeForm, {
name: detail?.name || route.name || '',
name: detail?.name || detail?.route?.name || route.name || '',
altitude: first ? first.altitude : 50, altitude: first ? first.altitude : 50,
speed: first ? first.speed : 10, speed: first ? first.speed : 10,
turnMode: first ? first.turnMode : 'auto', turnMode: first ? first.turnMode : 'auto',
@ -814,8 +869,9 @@ async function openRouteEditorForEdit(route) {
pendingFitRoute = routePoints.value.length > 0 pendingFitRoute = routePoints.value.length > 0
routeEditorOpen.value = true routeEditorOpen.value = true
await attachRouteMap() await attachRouteMap()
// boot already syncs+fits when pendingFitRoute; ready fit
if (routeMapReady.value) syncRouteLayer({ fit: routePoints.value.length > 0 })
if (routeMapReady.value && routePoints.value.length) {
scheduleRouteSync({ fit: true })
}
} catch (e) { } catch (e) {
ui.toast(e.message || '加载航线详情失败') ui.toast(e.message || '加载航线详情失败')
} }

Loading…
Cancel
Save