refactor: 重构数据层支持 SGS SDK 集成

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
lyf
2026-06-26 04:21:12 +08:00
parent a7413ee037
commit 15fbbec12d
16 changed files with 762 additions and 54 deletions

View File

@@ -14,6 +14,8 @@
:floors="guideFloors"
:normalize-floor-id="normalizeGuideFloorId"
:target-focus-request="targetFocusRequest"
:route-preview="activeRoutePreview"
:show-route="Boolean(activeRoutePreview)"
@search-tap="handleSearchTap"
@mode-change="handleModeChange"
@floor-change="handleFloorChange"
@@ -108,6 +110,34 @@
</view>
</view>
</view>
<view
v-else-if="routeViewState === 'preview'"
class="indoor-guide-card"
>
<text class="indoor-guide-title">{{ indoorGuideTitle }}</text>
<text class="indoor-guide-desc">{{ indoorGuideDesc }}</text>
<view v-if="routePlanError" class="indoor-guide-error">
<text class="indoor-guide-error-text">{{ routePlanError }}</text>
</view>
<view class="indoor-guide-actions">
<view
class="indoor-guide-btn secondary"
@tap="handleReturnToPreview"
>
<text class="indoor-guide-btn-text">查看位置</text>
</view>
<view
class="indoor-guide-btn primary"
:class="{ disabled: !routeReady || routePlanning }"
@tap="handleStartIndoorGuide"
>
<text class="indoor-guide-btn-text">
{{ routePlanning ? '规划中' : routeReady ? '开始导览' : '导航不可用' }}
</text>
</view>
</view>
</view>
</GuideMapShell>
</GuidePageFrame>
</template>
@@ -127,9 +157,13 @@ import {
import type {
GuideLocationPreview,
GuideLocationPreviewPlan,
GuideRouteResult,
GuideStartLocation,
MuseumFloor
} from '@/domain/museum'
import {
guideRouteUseCase
} from '@/usecases/guideRouteUseCase'
import {
navigateToGuideTopTab,
saveLastGuideLocationPreviewUrl,
@@ -166,6 +200,11 @@ const routeViewState = ref<RouteViewState>('preview')
const activeFloor = ref('1F')
const targetFocusRequest = ref<TargetPoiFocusRequest | null>(null)
const targetFocusRequestId = ref(0)
const routeReady = ref(false)
const routeReadinessMessage = ref('')
const routePlanning = ref(false)
const routePlanError = ref('')
const activeRoutePreview = ref<GuideRouteResult | null>(null)
const manualFloors = computed(() => guideFloors.value.map((floor) => floor.label))
const guideFloorLabel = (floorId: string) => (
guideFloors.value.find((floor) => floor.id === floorId)?.label || floorId
@@ -263,7 +302,7 @@ const shellConfig = computed(() => {
}
return {
searchText: `位置预览:${route.value.target}`,
searchText: routeReady.value ? `室内导览:${route.value.target}` : `位置预览:${route.value.target}`,
searchTop: '60px',
activeMode: '3d' as GuideMode,
activeFloor: activeFloor.value,
@@ -281,6 +320,20 @@ const shellConfig = computed(() => {
}
})
const indoorGuideTitle = computed(() => (
routeReady.value ? `室内导览:${route.value.target}` : `位置预览:${route.value.target}`
))
const indoorGuideDesc = computed(() => {
if (activeRoutePreview.value) {
return `已规划 ${activeRoutePreview.value.start.name}${activeRoutePreview.value.end.name},约 ${Math.round(activeRoutePreview.value.distanceMeters)} 米。`
}
return routeReady.value
? 'SGS 路线数据已就绪,可从已选起点开始规划馆内路线。'
: routeReadinessMessage.value || '当前暂不支持正式导航,可查看位置预览。'
})
const safeDecode = (value: string) => {
try {
return decodeURIComponent(value)
@@ -328,6 +381,19 @@ const saveCurrentLocationPreviewUrl = () => {
saveLastGuideLocationPreviewUrl(`/pages/route/detail?${params.toString()}`)
}
const refreshRouteReadinessState = async () => {
try {
const readiness = await guideRouteUseCase.getRouteReadiness()
routeReady.value = readiness.ready
routeReadinessMessage.value = readiness.message
routePlanError.value = readiness.ready ? '' : readiness.message
} catch (error) {
routeReady.value = false
routeReadinessMessage.value = error instanceof Error ? error.message : '路线能力状态读取失败'
routePlanError.value = routeReadinessMessage.value
}
}
const createStartLocationFromOptions = (options: Record<string, unknown>) => {
const label = normalizeStringOption(options.startLabel)
@@ -373,6 +439,7 @@ const requestTargetFocus = (target: Partial<RouteTargetLocation> | null, fallbac
const applyRouteOptions = async (options: Record<string, unknown> = {}) => {
await loadGuideFloors()
await refreshRouteReadinessState()
const facilityId = normalizeStringOption(options.facilityId)
const target = normalizeStringOption(options.target)
@@ -385,6 +452,7 @@ const applyRouteOptions = async (options: Record<string, unknown> = {}) => {
routeViewState.value = 'preview'
activeFloor.value = defaultGuideFloorLabel()
targetFocusRequest.value = null
activeRoutePreview.value = null
isManualLocationPanelOpen.value = false
return false
}
@@ -397,6 +465,7 @@ const applyRouteOptions = async (options: Record<string, unknown> = {}) => {
routeViewState.value = 'preview'
activeFloor.value = startLocation?.floor || defaultGuideFloorLabel()
targetFocusRequest.value = null
activeRoutePreview.value = null
isManualLocationPanelOpen.value = false
return false
}
@@ -485,6 +554,7 @@ const handleTargetFocus = (result: TargetPoiFocusResult) => {
const handleReturnToPreview = () => {
isManualLocationPanelOpen.value = false
activeRoutePreview.value = null
routeViewState.value = 'preview'
requestTargetFocus(route.value.targetLocation)
saveCurrentLocationPreviewUrl()
@@ -527,11 +597,67 @@ const handleConfirmLocation = async () => {
)
activeFloor.value = startLocation.floor || activeFloor.value
isManualLocationPanelOpen.value = false
activeRoutePreview.value = null
routeViewState.value = 'preview'
requestTargetFocus(route.value.targetLocation)
saveCurrentLocationPreviewUrl()
}
const handleStartIndoorGuide = async () => {
if (routePlanning.value || !route.value.facilityId) return
await refreshRouteReadinessState()
if (!routeReady.value) return
routePlanning.value = true
routePlanError.value = ''
try {
const targets = await guideRouteUseCase.listTargets()
const startName = route.value.startLocation?.label || ''
const startFloor = route.value.startLocation?.floor || ''
const targetLocation = route.value.targetLocation
const fallbackStart = targets.find((target) => (
startName
? target.name.includes(startName) || startName.includes(target.name)
: false
)) || targets.find((target) => (
startFloor
? target.floorLabel === startFloor || target.floorId === normalizeGuideFloorId(startFloor)
: false
)) || targets.find((target) => target.poiId !== route.value.facilityId)
const endTarget = targets.find((target) => target.poiId === route.value.facilityId)
|| (targetLocation
? targets.find((target) => target.floorId === targetLocation.floorId && target.name === targetLocation.name)
: null)
if (!fallbackStart || !endTarget) {
routePlanError.value = '未找到可用于路线规划的起点或终点'
return
}
const result = await guideRouteUseCase.planRoute({
startPoiId: fallbackStart.poiId,
endPoiId: endTarget.poiId
})
if (!result.route) {
routePlanError.value = result.error || '室内导览路线规划失败'
activeRoutePreview.value = null
return
}
activeRoutePreview.value = result.route
activeFloor.value = result.route.start.floorLabel
} catch (error) {
routePlanError.value = error instanceof Error ? error.message : '室内导览路线规划失败'
activeRoutePreview.value = null
} finally {
routePlanning.value = false
}
}
const handleModeChange = (mode: GuideMode) => {
if (routeViewState.value === 'outdoor-preview' && mode === '3d') {
routeViewState.value = 'preview'
@@ -606,6 +732,94 @@ const handlePageBack = () => {
z-index: 45;
}
.indoor-guide-card {
position: absolute;
left: 16px;
right: 16px;
bottom: calc(env(safe-area-inset-bottom) + 34px);
min-height: 156px;
padding: 18px 18px 16px;
display: flex;
flex-direction: column;
gap: 10px;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.96);
border: 1px solid #e5e6de;
border-radius: 8px;
box-shadow: 0 10px 24px rgba(36, 49, 42, 0.14);
z-index: 45;
}
.indoor-guide-title {
font-size: 17px;
line-height: 24px;
font-weight: 700;
color: #151713;
}
.indoor-guide-desc {
font-size: 13px;
line-height: 19px;
color: #545861;
}
.indoor-guide-error {
padding: 7px 9px;
background: #fff4f2;
border: 1px solid #ffd2c9;
border-radius: 8px;
}
.indoor-guide-error-text {
font-size: 12px;
line-height: 17px;
color: #b44b42;
}
.indoor-guide-actions {
display: flex;
gap: 10px;
margin-top: auto;
}
.indoor-guide-btn {
flex: 1;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
border-radius: 8px;
}
.indoor-guide-btn.secondary {
background: #f5f7f2;
border: 1px solid #e4e5df;
}
.indoor-guide-btn.primary {
background: #151713;
}
.indoor-guide-btn.primary.disabled {
background: #d8dbd2;
}
.indoor-guide-btn-text {
font-size: 14px;
line-height: 19px;
font-weight: 500;
color: #151713;
}
.indoor-guide-btn.primary .indoor-guide-btn-text {
color: var(--museum-accent);
}
.indoor-guide-btn.primary.disabled .indoor-guide-btn-text {
color: #8b8b84;
}
.target-required-title {
font-size: 18px;
line-height: 24px;