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