docs: 添加环境变量模板与 superpowers 文档

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
lyf
2026-06-26 04:25:08 +08:00
parent 3d6dea2872
commit 029c7a6696
2 changed files with 873 additions and 0 deletions

3
.env.example Normal file
View File

@@ -0,0 +1,3 @@
# 腾讯地图 API Key
# 请到 https://lbs.qq.com/index.html 申请
VITE_TENCENT_MAP_KEY=your_tencent_map_key_here

View File

@@ -0,0 +1,870 @@
# SGS SDK Data Switch ViewModel Convergence Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Complete the SGS SDK data-source switch by keeping `static/api/sdk` mode behavior intact while moving remaining page-level mapping into the existing adapter/view-model middle layers.
**Architecture:** Keep the existing split: providers/adapters map source payloads to domain models, repositories/use cases expose business operations, and `src/view-models/*` maps domain models into presentation props. The implementation does not rewrite the SGS provider, repository, renderer, or pages; it only closes remaining mapping leaks and adds observable mode diagnostics.
**Tech Stack:** uni-app H5, Vue 3 `<script setup>`, TypeScript, `vue-tsc`, ESLint, existing `GuideUseCase`, `GuideRepository`, `GuideMapShell`, `ThreeMap`, and `SgsMapRenderer`.
---
## Current State Summary
Already present and should be preserved:
```text
VITE_DATA_SOURCE_MODE=static -> StaticGuideRepository + ThreeMap
VITE_DATA_SOURCE_MODE=api -> SgsSdkGuideRepository + ThreeMap
VITE_DATA_SOURCE_MODE=sdk -> SgsSdkGuideRepository + SgsMapRenderer
```
Evidence from source audit:
- `src/config/dataSource.ts` supports `static | api | sdk` and centralizes SGS API/SDK config.
- `src/repositories/createGuideRepository.ts` selects `SgsSdkGuideRepository` for `api` and `sdk`.
- `src/components/navigation/GuideMapShell.vue` selects `SgsMapRenderer` only when `isSgsSdkMode()` is true.
- `src/data/adapters/sgsSdkGuideAdapter.ts` maps SGS payloads to `MuseumFloor`, `MuseumPoi`, diagnostics, and route readiness.
- `src/view-models/guideViewModels.ts` and `src/view-models/explainViewModels.ts` already exist, but `src/pages/index/index.vue` still owns some route/explain/card mapping.
## File Structure
Modify these files only:
- `src/view-models/guideViewModels.ts`
- Add guide presentation view models for selected POI cards and route-point picker options.
- Add conversion helpers so pages do not build route picker props from domain models inline.
- `src/view-models/explainViewModels.ts`
- Move explain hall selector item mapping from `src/pages/index/index.vue` into the view-model layer.
- `src/components/navigation/RoutePointPicker.vue`
- Consume `RoutePointOptionViewModel` from `src/view-models/guideViewModels.ts` and re-export it as `RoutePointOption` for existing imports.
- `src/components/navigation/RoutePlannerPanel.vue`
- Consume the same `RoutePointOption` alias exported by `RoutePointPicker.vue`; no behavior change.
- `src/components/explain/ExplainHallSelect.vue`
- Consume `ExplainHallSelectItem` from `src/view-models/explainViewModels.ts` instead of defining presentation item shape locally.
- `src/pages/index/index.vue`
- Replace inline mapping helpers with imports from view-models.
- Keep page state and user interactions in the page.
- Keep the existing `GuideUseCase`/`guideRouteUseCase` calls.
- `src/config/dataSource.ts`
- Add a small diagnostics helper that reports selected data source and renderer mode.
- `src/components/navigation/GuideMapShell.vue`
- Log the mode diagnostic once in dev/H5 so QA can confirm `static/api/sdk` mode without inspecting code.
Do not modify:
- `src/data/providers/sgsSdkApiProvider.ts`
- `src/data/adapters/sgsSdkGuideAdapter.ts`
- `src/repositories/GuideRepository.ts`
- `src/repositories/createGuideRepository.ts`
- `src/components/map/SgsMapRenderer.vue`
- `src/services/sgs/SgsMapService.ts`
- `src/services/sgs/SgsMapScriptLoader.ts`
---
### Task 1: Add Guide ViewModel Mappers
**Files:**
- Modify: `src/view-models/guideViewModels.ts`
- [ ] **Step 1: Add imports for route target and renderer POI types**
At the top of `src/view-models/guideViewModels.ts`, replace the existing imports with:
```ts
import type {
GuideLocationPreview,
GuideLocationPreviewPlan,
GuideRouteTarget,
MuseumFloor,
MuseumPoi
} from '@/domain/museum'
import type {
GuideRenderPoi
} from '@/domain/guideModel'
```
Expected: the existing `GuideLocationPreview`, `GuideLocationPreviewPlan`, and `MuseumPoi` usages still compile, and the file can now define route picker and selected POI card mappers.
- [ ] **Step 2: Add route point and selected POI card view-model interfaces**
After `TargetPoiFocusRequestViewModel`, add:
```ts
export interface RoutePointOptionViewModel {
poiId: string
name: string
floorId: string
floorLabel: string
categoryLabel?: string
}
export interface SelectedGuidePoiCardViewModel {
id: string
name: string
subtitle: string
collapsedSubtitle: string
}
```
Expected: these types become the presentation contract for `RoutePointPicker`, `RoutePlannerPanel`, and the selected POI card in `src/pages/index/index.vue`.
- [ ] **Step 3: Add mappers for route targets and selected POI cards**
After `toTargetFocusRequestViewModel`, add:
```ts
export const toRoutePointOptionViewModel = (target: GuideRouteTarget): RoutePointOptionViewModel => ({
poiId: target.poiId,
name: target.name,
floorId: target.floorId,
floorLabel: target.floorLabel,
categoryLabel: target.categoryLabel
})
export const toSelectedGuidePoiCardViewModel = (
poi: GuideRenderPoi,
floors: MuseumFloor[]
): SelectedGuidePoiCardViewModel => {
const floor = floors.find((item) => item.id === poi.floorId)
const floorLabel = floor?.label || poi.floorId
const categoryLabel = poi.primaryCategoryZh || '位置预览'
return {
id: poi.id,
name: poi.name,
subtitle: `${floorLabel} · ${categoryLabel}`,
collapsedSubtitle: '位置预览 · 点按展开操作'
}
}
```
Expected: `src/pages/index/index.vue` no longer needs to know how to format selected POI subtitles or route-point picker items.
- [ ] **Step 4: Run type-check for this isolated change**
Run:
```powershell
pnpm type-check
```
Expected: PASS. If it fails because imports are unused, continue to Task 2 and Task 5 before re-running.
- [ ] **Step 5: Commit if authorized**
Do not commit unless the user explicitly authorizes commits in this session. If authorized, run:
```powershell
git add src/view-models/guideViewModels.ts
git commit -m @'
refactor: add guide presentation view models
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
'@
```
Expected: one commit containing only `src/view-models/guideViewModels.ts`.
---
### Task 2: Move Route Picker Presentation Type To ViewModel Layer
**Files:**
- Modify: `src/components/navigation/RoutePointPicker.vue`
- Modify: `src/components/navigation/RoutePlannerPanel.vue`
- [ ] **Step 1: Update RoutePointPicker type source**
In `src/components/navigation/RoutePointPicker.vue`, replace the local `RoutePointOption` interface with this import/export block immediately after `import { computed, ref, watch } from 'vue'`:
```ts
import type {
RoutePointOptionViewModel
} from '@/view-models/guideViewModels'
export type RoutePointOption = RoutePointOptionViewModel
```
Remove this old block from the same file:
```ts
export interface RoutePointOption {
poiId: string
name: string
floorId: string
floorLabel: string
categoryLabel?: string
}
```
Expected: `RoutePointPicker` still accepts the same prop shape, but the contract is owned by `src/view-models/guideViewModels.ts`.
- [ ] **Step 2: Confirm RoutePlannerPanel remains compatible**
Leave this import in `src/components/navigation/RoutePlannerPanel.vue` unchanged:
```ts
import RoutePointPicker, {
type RoutePointOption
} from '@/components/navigation/RoutePointPicker.vue'
```
Expected: because `RoutePointPicker.vue` re-exports the view-model alias, no RoutePlannerPanel behavior changes.
- [ ] **Step 3: Run type-check for picker type migration**
Run:
```powershell
pnpm type-check
```
Expected: PASS or only failures from later not-yet-updated page imports if Task 5 has not been applied.
- [ ] **Step 4: Commit if authorized**
Do not commit unless the user explicitly authorizes commits in this session. If authorized, run:
```powershell
git add src/components/navigation/RoutePointPicker.vue src/components/navigation/RoutePlannerPanel.vue
git commit -m @'
refactor: route picker uses guide view model type
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
'@
```
Expected: one commit containing the route picker type ownership change.
---
### Task 3: Add Explain Hall Selector ViewModel Mapper
**Files:**
- Modify: `src/view-models/explainViewModels.ts`
- Modify: `src/components/explain/ExplainHallSelect.vue`
- [ ] **Step 1: Add ExplainHallSelectItem interface to explain view models**
In `src/view-models/explainViewModels.ts`, after `HallDetailViewModel`, add:
```ts
export interface ExplainHallSelectItem {
id: string
name: string
floorLabel?: string
image?: string
explainCount: number
searchText: string
}
```
Expected: the hall selector item presentation contract is no longer owned by the component file.
- [ ] **Step 2: Add hall-selector mapping helpers**
In `src/view-models/explainViewModels.ts`, after `toExplainSearchResultViewModel`, add:
```ts
const normalizeExplainKeyword = (value?: string) => (value || '').trim().toLowerCase()
export const toExplainHallSelectItems = (
halls: MuseumHall[],
exhibits: MuseumExhibit[]
): ExplainHallSelectItem[] => {
const countsByHallId = new Map<string, number>()
const countsByHallName = new Map<string, number>()
const keywordsByHallId = new Map<string, Set<string>>()
const keywordsByHallName = new Map<string, Set<string>>()
exhibits.forEach((exhibit) => {
const keywordParts = [
exhibit.name,
exhibit.hallName,
exhibit.floorLabel,
exhibit.description,
exhibit.guideTitle,
exhibit.guideText,
...(exhibit.tags || [])
].filter(Boolean) as string[]
if (exhibit.hallId) {
countsByHallId.set(exhibit.hallId, (countsByHallId.get(exhibit.hallId) || 0) + 1)
const keywords = keywordsByHallId.get(exhibit.hallId) || new Set<string>()
keywordParts.forEach((part) => keywords.add(part))
keywordsByHallId.set(exhibit.hallId, keywords)
}
if (exhibit.hallName) {
countsByHallName.set(exhibit.hallName, (countsByHallName.get(exhibit.hallName) || 0) + 1)
const keywords = keywordsByHallName.get(exhibit.hallName) || new Set<string>()
keywordParts.forEach((part) => keywords.add(part))
keywordsByHallName.set(exhibit.hallName, keywords)
}
})
return halls.map((hall) => {
const explainCount = countsByHallId.get(hall.id)
|| countsByHallName.get(hall.name)
|| hall.exhibitCount
|| 0
const keywordParts = [
hall.name,
hall.floorLabel,
hall.description,
hall.area,
...Array.from(keywordsByHallId.get(hall.id) || []),
...Array.from(keywordsByHallName.get(hall.name) || [])
].filter(Boolean) as string[]
return {
id: hall.id,
name: hall.name,
floorLabel: hall.floorLabel,
image: hall.image,
explainCount,
searchText: normalizeExplainKeyword(keywordParts.join(' '))
}
})
}
```
Expected: the exact logic currently in `src/pages/index/index.vue` is moved without behavior changes.
- [ ] **Step 3: Update ExplainHallSelect component type ownership**
In `src/components/explain/ExplainHallSelect.vue`, replace the local `ExplainHallSelectItem` interface with this import/export block after `import { computed } from 'vue'`:
```ts
import type {
ExplainHallSelectItem as ExplainHallSelectItemViewModel
} from '@/view-models/explainViewModels'
export type ExplainHallSelectItem = ExplainHallSelectItemViewModel
```
Remove this old interface from the same file:
```ts
export interface ExplainHallSelectItem {
id: string
name: string
floorLabel?: string
image?: string
explainCount: number
searchText: string
}
```
Expected: component props retain the same shape, but the presentation contract is defined in `src/view-models/explainViewModels.ts`.
- [ ] **Step 4: Run type-check for explain view-model migration**
Run:
```powershell
pnpm type-check
```
Expected: PASS or only failures from `src/pages/index/index.vue` until Task 5 is applied.
- [ ] **Step 5: Commit if authorized**
Do not commit unless the user explicitly authorizes commits in this session. If authorized, run:
```powershell
git add src/view-models/explainViewModels.ts src/components/explain/ExplainHallSelect.vue
git commit -m @'
refactor: move explain hall selector mapping to view models
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
'@
```
Expected: one commit containing explain hall selector view-model ownership.
---
### Task 4: Add Data-Source Mode Diagnostics
**Files:**
- Modify: `src/config/dataSource.ts`
- Modify: `src/components/navigation/GuideMapShell.vue`
- [ ] **Step 1: Add diagnostics helper to dataSource config**
At the end of `src/config/dataSource.ts`, after `getSgsMapTargetOrigin`, add:
```ts
export const getGuideDataSourceDiagnostics = () => {
const usesSgsRepository = dataSourceConfig.mode === 'api' || dataSourceConfig.mode === 'sdk'
const usesSgsRenderer = dataSourceConfig.mode === 'sdk'
return {
mode: dataSourceConfig.mode,
guideRepository: usesSgsRepository ? 'SgsSdkGuideRepository' : 'StaticGuideRepository',
mapRenderer: usesSgsRenderer ? 'SgsMapRenderer' : 'ThreeMap',
apiBaseUrl: usesSgsRepository ? dataSourceConfig.sgsApiBaseUrl : '',
mapId: usesSgsRepository ? dataSourceConfig.sgsMapId : '',
sdkScriptUrl: usesSgsRenderer ? dataSourceConfig.sgsSdkScriptUrl : '',
sdkEngineUrl: usesSgsRenderer ? dataSourceConfig.sgsMapEngineUrl : ''
}
}
```
Expected: diagnostics are centralized with mode parsing and do not read raw env vars in page/component code.
- [ ] **Step 2: Import diagnostics helper in GuideMapShell**
In `src/components/navigation/GuideMapShell.vue`, change the H5 config import from:
```ts
import {
isSgsSdkMode
} from '@/config/dataSource'
```
to:
```ts
import {
getGuideDataSourceDiagnostics,
isSgsSdkMode
} from '@/config/dataSource'
```
Also change the Vue import from:
```ts
import { computed, ref } from 'vue'
```
to:
```ts
import { computed, onMounted, ref } from 'vue'
```
Expected: `GuideMapShell` can log diagnostics in H5 without changing mode-selection behavior.
- [ ] **Step 3: Log selected mode once in dev H5**
In `src/components/navigation/GuideMapShell.vue`, after `const useSgsMapRenderer = computed(() => isSgsSdkMode())`, add:
```ts
onMounted(() => {
if (!import.meta.env.DEV) return
const diagnostics = getGuideDataSourceDiagnostics()
console.info('[guide-data-source]', diagnostics)
})
```
Expected in dev console:
```text
[guide-data-source] { mode: 'sdk', guideRepository: 'SgsSdkGuideRepository', mapRenderer: 'SgsMapRenderer', ... }
```
Expected in production build: no diagnostic log.
- [ ] **Step 4: Run type-check for diagnostics change**
Run:
```powershell
pnpm type-check
```
Expected: PASS or only failures from `src/pages/index/index.vue` until Task 5 is applied.
- [ ] **Step 5: Commit if authorized**
Do not commit unless the user explicitly authorizes commits in this session. If authorized, run:
```powershell
git add src/config/dataSource.ts src/components/navigation/GuideMapShell.vue
git commit -m @'
feat: expose guide data source diagnostics
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
'@
```
Expected: one commit containing diagnostics only.
---
### Task 5: Replace Remaining Page-Level Mapping In Index Page
**Files:**
- Modify: `src/pages/index/index.vue`
- [ ] **Step 1: Update imports**
In `src/pages/index/index.vue`, remove this import block:
```ts
import type {
RoutePointOption
} from '@/components/navigation/RoutePointPicker.vue'
import ExplainHallSelect, {
type ExplainHallSelectItem
} from '@/components/explain/ExplainHallSelect.vue'
```
Replace it with:
```ts
import type {
RoutePointOption
} from '@/components/navigation/RoutePointPicker.vue'
import ExplainHallSelect from '@/components/explain/ExplainHallSelect.vue'
```
Then replace the existing domain import:
```ts
import type {
GuideRouteResult,
GuideRouteTarget,
MuseumExhibit,
MuseumFloor
} from '@/domain/museum'
```
with:
```ts
import type {
GuideRouteResult,
MuseumFloor
} from '@/domain/museum'
```
Finally add this view-model import near the other app imports:
```ts
import {
toExplainHallSelectItems,
type ExplainHallSelectItem
} from '@/view-models/explainViewModels'
import {
toRoutePointOptionViewModel,
toSelectedGuidePoiCardViewModel
} from '@/view-models/guideViewModels'
```
Expected: page no longer imports `GuideRouteTarget` or `ExplainHallSelectItem` from presentation components.
- [ ] **Step 2: Replace selected POI subtitle computed with card view model**
In `src/pages/index/index.vue`, replace this computed block:
```ts
const selectedGuidePoiSubtitle = computed(() => {
const poi = selectedGuidePoi.value
if (!poi) return ''
const floor = guideFloors.value.find((item) => item.id === poi.floorId)
const floorLabel = floor?.label || poi.floorId
return `${floorLabel} · ${poi.primaryCategoryZh || '位置预览'}`
})
```
with:
```ts
const selectedGuidePoiCard = computed(() => (
selectedGuidePoi.value
? toSelectedGuidePoiCardViewModel(selectedGuidePoi.value, guideFloors.value)
: null
))
```
Expected: page still owns selected state, but no longer formats selected POI display text inline.
- [ ] **Step 3: Update selected POI card template bindings**
In the selected POI card template near the top of `src/pages/index/index.vue`, replace:
```vue
<text class="poi-card-title">{{ selectedGuidePoi.name }}</text>
<text class="poi-card-subtitle">位置预览 · 点按展开操作</text>
```
with:
```vue
<text class="poi-card-title">{{ selectedGuidePoiCard?.name }}</text>
<text class="poi-card-subtitle">{{ selectedGuidePoiCard?.collapsedSubtitle }}</text>
```
Also replace:
```vue
<text class="poi-card-title">{{ selectedGuidePoi.name }}</text>
<text class="poi-card-subtitle">{{ selectedGuidePoiSubtitle }}</text>
```
with:
```vue
<text class="poi-card-title">{{ selectedGuidePoiCard?.name }}</text>
<text class="poi-card-subtitle">{{ selectedGuidePoiCard?.subtitle }}</text>
```
Expected: user-facing text remains identical.
- [ ] **Step 4: Remove inline route point mapper**
Delete this function from `src/pages/index/index.vue`:
```ts
const toRoutePointOption = (target: GuideRouteTarget): RoutePointOption => ({
poiId: target.poiId,
name: target.name,
floorId: target.floorId,
floorLabel: target.floorLabel,
categoryLabel: target.categoryLabel
})
```
Expected: route-point option mapping comes from `src/view-models/guideViewModels.ts`.
- [ ] **Step 5: Use guide view-model route mapper**
In `src/pages/index/index.vue`, replace:
```ts
const options = targets.map(toRoutePointOption)
```
with:
```ts
const options: RoutePointOption[] = targets.map(toRoutePointOptionViewModel)
```
Expected: route planner point list remains unchanged.
- [ ] **Step 6: Remove inline explain hall mapper helpers**
Delete these functions from `src/pages/index/index.vue`:
```ts
const normalizeExplainKeyword = (value?: string) => (value || '').trim().toLowerCase()
const buildExplainHallItems = (
halls: Awaited<ReturnType<typeof explainUseCase.listHalls>>,
exhibits: MuseumExhibit[]
): ExplainHallSelectItem[] => {
const countsByHallId = new Map<string, number>()
const countsByHallName = new Map<string, number>()
const keywordsByHallId = new Map<string, Set<string>>()
const keywordsByHallName = new Map<string, Set<string>>()
exhibits.forEach((exhibit) => {
const keywordParts = [
exhibit.name,
exhibit.hallName,
exhibit.floorLabel,
exhibit.description,
exhibit.guideTitle,
exhibit.guideText,
...(exhibit.tags || [])
].filter(Boolean) as string[]
if (exhibit.hallId) {
countsByHallId.set(exhibit.hallId, (countsByHallId.get(exhibit.hallId) || 0) + 1)
const keywords = keywordsByHallId.get(exhibit.hallId) || new Set<string>()
keywordParts.forEach((part) => keywords.add(part))
keywordsByHallId.set(exhibit.hallId, keywords)
}
if (exhibit.hallName) {
countsByHallName.set(exhibit.hallName, (countsByHallName.get(exhibit.hallName) || 0) + 1)
const keywords = keywordsByHallName.get(exhibit.hallName) || new Set<string>()
keywordParts.forEach((part) => keywords.add(part))
keywordsByHallName.set(exhibit.hallName, keywords)
}
})
return halls.map((hall) => {
const explainCount = countsByHallId.get(hall.id)
|| countsByHallName.get(hall.name)
|| hall.exhibitCount
|| 0
const keywordParts = [
hall.name,
hall.floorLabel,
hall.description,
hall.area,
...Array.from(keywordsByHallId.get(hall.id) || []),
...Array.from(keywordsByHallName.get(hall.name) || [])
].filter(Boolean) as string[]
return {
id: hall.id,
name: hall.name,
floorLabel: hall.floorLabel,
image: hall.image,
explainCount,
searchText: normalizeExplainKeyword(keywordParts.join(' '))
}
})
}
```
Expected: `src/pages/index/index.vue` no longer owns explain hall card mapping.
- [ ] **Step 7: Use explain view-model hall mapper**
In `loadExplainExhibits`, replace:
```ts
explainHallItems.value = buildExplainHallItems(halls, exhibits)
```
with:
```ts
explainHallItems.value = toExplainHallSelectItems(halls, exhibits)
```
Expected: explain hall selector still shows the same halls and counts.
- [ ] **Step 8: Run type-check after index page migration**
Run:
```powershell
pnpm type-check
```
Expected: PASS.
- [ ] **Step 9: Commit if authorized**
Do not commit unless the user explicitly authorizes commits in this session. If authorized, run:
```powershell
git add src/pages/index/index.vue
git commit -m @'
refactor: use view models on guide index page
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
'@
```
Expected: one commit containing only index page mapping removal.
---
### Task 6: Verify Data-Source Switch Modes
**Files:**
- No source edits expected.
- [ ] **Step 1: Verify no forbidden page imports exist**
Run these searches:
```powershell
Select-String -Path "src/pages/**/*.vue" -Pattern "SGSMapSDK|SgsMapService|sgsSdkApiProvider|/app-api/gis/sdk" -CaseSensitive
Select-String -Path "src/components/**/*.vue" -Pattern "sgsSdkApiProvider|/app-api/gis/sdk" -CaseSensitive
```
Expected: no matches in pages. Component matches should not include generic UI components; `SgsMapRenderer.vue` may import `SgsMapService`, which is allowed because it is the SDK renderer boundary.
- [ ] **Step 2: Verify mode switch source code**
Run:
```powershell
Select-String -Path "src/repositories/createGuideRepository.ts","src/components/navigation/GuideMapShell.vue","src/config/dataSource.ts" -Pattern "dataSourceConfig.mode|isSgsSdkMode|SgsSdkGuideRepository|StaticGuideRepository|SgsMapRenderer|ThreeMap" -CaseSensitive
```
Expected evidence:
```text
createGuideRepository.ts: api/sdk -> SgsSdkGuideRepository, static -> StaticGuideRepository
GuideMapShell.vue: sdk -> SgsMapRenderer, otherwise -> ThreeMap
config/dataSource.ts: mode supports static/api/sdk
```
- [ ] **Step 3: Run type-check**
Run:
```powershell
pnpm type-check
```
Expected: PASS.
- [ ] **Step 4: Run lint because shell/page/view-model files changed**
Run:
```powershell
pnpm lint
```
Expected: PASS. If warnings appear but exit code is 0, record them as debt in the final report.
- [ ] **Step 5: Run H5 build because shell/page UI files changed**
This command writes `dist`.
Run:
```powershell
pnpm build:h5
```
Expected: PASS and `dist/build/h5` or the projects configured H5 output is updated.
- [ ] **Step 6: Commit verification-related source edits if authorized**
If Task 6 made no source edits, do not commit. If a small verification fix was required and the user has authorized commits, run:
```powershell
git add src
git commit -m @'
chore: verify guide data source switching
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
'@
```
Expected: commit only source fixes made during verification.
---
## Acceptance Criteria
- `VITE_DATA_SOURCE_MODE=static` continues to use `StaticGuideRepository` and `ThreeMap`.
- `VITE_DATA_SOURCE_MODE=api` continues to use `SgsSdkGuideRepository` and `ThreeMap`.
- `VITE_DATA_SOURCE_MODE=sdk` continues to use `SgsSdkGuideRepository` and `SgsMapRenderer`.
- Pages and ordinary presentation components do not directly import `SGSMapSDK`, `SgsMapService`, `sgsSdkApiProvider`, or raw `/app-api/gis/sdk/*` URLs.
- Remaining route picker, selected POI card, and explain hall selector mapping is owned by `src/view-models/*` instead of `src/pages/index/index.vue`.
- Route readiness gate remains unchanged; no user-facing copy claims certified route navigation.
- `pnpm type-check` passes.
- `pnpm lint` passes.
- `pnpm build:h5` passes.
## Self-Review
- Spec coverage: plan covers data-source switching, renderer switching preservation, middle-layer/view-model convergence, forbidden direct imports, diagnostics, and quality gates.
- Placeholder scan: no TBD/TODO/fill-in-later instructions remain; all code snippets are concrete.
- Type consistency: `RoutePointOptionViewModel` is aliased as `RoutePointOption`, `ExplainHallSelectItem` is exported from the view-model layer and aliased by the component, and the page imports the mapper names defined in the tasks.