docs: 添加环境变量模板与 superpowers 文档
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
3
.env.example
Normal file
3
.env.example
Normal file
@@ -0,0 +1,3 @@
|
||||
# 腾讯地图 API Key
|
||||
# 请到 https://lbs.qq.com/index.html 申请
|
||||
VITE_TENCENT_MAP_KEY=your_tencent_map_key_here
|
||||
@@ -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 project’s 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.
|
||||
Reference in New Issue
Block a user