新增讲解多页面流实现计划
- 拆分讲解展厅列表、业务单元列表、讲解点列表页面 - 明确标题、返回、路由参数与回归验证步骤 - 约束在现有 explain 数据边界内实施 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
665
docs/superpowers/plans/2026-07-10-explain-multi-page-flow.md
Normal file
665
docs/superpowers/plans/2026-07-10-explain-multi-page-flow.md
Normal file
@@ -0,0 +1,665 @@
|
|||||||
|
# 讲解流独立页面拆分 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:** 将讲解流从单页 `stage` 状态机拆分为展厅列表页、业务单元列表页、讲解点列表页三个真实路由页面,使嵌入小程序时标题与返回都跟随页面层级工作。
|
||||||
|
|
||||||
|
**Architecture:** 保留现有 `explainUseCase` / repository 数据边界,页面层拆开但组件层短期继续复用 `ExplainHallSelect.vue`。`pages/explain/list.vue` 收敛为展厅列表页,新增 `business-unit-list.vue` 与 `guide-stop-list.vue` 分别承载业务单元和讲解点,详情页继续沿用 `pages/exhibit/detail.vue`。
|
||||||
|
|
||||||
|
**Tech Stack:** uni-app、Vue 3 `<script setup>`、TypeScript、SCSS、现有 explain use case/repository/view-model 层
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- 嵌入小程序时,宿主标题和返回行为与页面层级一致。
|
||||||
|
- 普通 H5 中现有讲解流视觉风格和交互尽量保持不变。
|
||||||
|
- 继续复用当前 explain use case / repository 数据边界,不引入新的页面直接数据耦合。
|
||||||
|
- 讲解点进入 `pages/exhibit/detail` 的既有详情能力保持可用。
|
||||||
|
- 不重做讲解详情页的数据模型或播放器架构。
|
||||||
|
- 不改造 explain 数据来源边界。
|
||||||
|
- 不新增小程序宿主桥接协议。
|
||||||
|
- 不重构 `ExplainHallSelect.vue` 为多个纯展示组件,除非实现时为降低复杂度必须做最小拆分。
|
||||||
|
- 标题规则固定为:展厅列表页 `讲解`,业务单元列表页 `当前展厅名`,讲解点列表页 `当前业务单元名`。
|
||||||
|
- 返回链路固定为:讲解点列表 → 业务单元列表 → 展厅列表 → 讲解首页/入口页。
|
||||||
|
- 当前项目没有独立 `test` 脚本,验证以 `pnpm type-check`、`pnpm lint`、`pnpm build:h5` 和 H5 手工回归为准。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: 收敛讲解首页入口,改为展厅列表独立页跳转
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/pages/index/index.vue`
|
||||||
|
- Modify: `src/pages/explain/list.vue`
|
||||||
|
- Test: `src/pages/index/index.vue`(手工回归入口)
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes:
|
||||||
|
- `explainUseCase.loadExplainHalls(): Promise<MuseumHall[]>`
|
||||||
|
- `explainUseCase.loadExplainHallSummaries(hallIds: string[]): Promise<Record<string, ExplainHallSummary>>`
|
||||||
|
- `navigateToGuideTopTab(tab: GuideTopTab): void`
|
||||||
|
- Produces:
|
||||||
|
- 首页讲解 Tab 点击展厅时调用 `uni.navigateTo({ url: '/pages/explain/list' })` 或直接在独立页内部继续流转
|
||||||
|
- `src/pages/explain/list.vue` 仅暴露展厅列表行为,不再承担 `unit` / `stop` 状态切换
|
||||||
|
|
||||||
|
- [ ] **Step 1: 阅读并锁定当前讲解入口代码**
|
||||||
|
|
||||||
|
阅读以下代码块并记录要删除或迁移的行为:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/pages/index/index.vue
|
||||||
|
const explainHallItems = ref<ExplainHallSelectItem[]>([])
|
||||||
|
const explainBusinessUnits = ref<ExplainBusinessUnit[]>([])
|
||||||
|
const explainStage = ref<ExplainSelectStage>('hall')
|
||||||
|
const selectedExplainHallId = ref('')
|
||||||
|
const selectedExplainBusinessUnitId = ref('')
|
||||||
|
```
|
||||||
|
|
||||||
|
重点确认:
|
||||||
|
- `ExplainHallSelect` 在首页 explain Tab 中被直接渲染
|
||||||
|
- `handleExplainHallClick / handleExplainBusinessUnitClick / handleExplainGuideStopClick / handleExplainBack` 仍由首页维护
|
||||||
|
|
||||||
|
- [ ] **Step 2: 先写实现后验证“首页 explain Tab 点击展厅跳独立页”的手工检查点**
|
||||||
|
|
||||||
|
将以下检查点写进任务注释或临时核对清单,不写入生产代码:
|
||||||
|
|
||||||
|
```text
|
||||||
|
入口回归预期:
|
||||||
|
1. 首页切到讲解 Tab,仍能看到展厅列表
|
||||||
|
2. 点击任一展厅,不再在首页内部切到业务单元 stage
|
||||||
|
3. 而是跳转到 /pages/explain/business-unit-list
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 将首页 explain Tab 的点击行为改为页面跳转**
|
||||||
|
|
||||||
|
把首页 explain 逻辑从“内部 stage 切换”改为“跳到独立页”。保留 explain Tab 首屏展厅列表展示,但去掉首页中业务单元和讲解点的状态推进逻辑。实现目标示意:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const handleExplainHallClick = async (hallId: string) => {
|
||||||
|
const hall = explainHallItems.value.find((item) => item.id === hallId)
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
hallId,
|
||||||
|
hallName: hall?.name || '讲解'
|
||||||
|
})
|
||||||
|
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/explain/business-unit-list?${params.toString()}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
同时移除首页中对以下逻辑的业务依赖:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
explainStage.value = 'unit'
|
||||||
|
selectedExplainBusinessUnitId.value = ''
|
||||||
|
selectedExplainBusinessUnitName.value = ''
|
||||||
|
```
|
||||||
|
|
||||||
|
并把 `handleExplainBusinessUnitClick` / `handleExplainBack` 调整为不再驱动首页内部多级状态。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 收敛 `pages/explain/list.vue` 为展厅列表页**
|
||||||
|
|
||||||
|
删除 `src/pages/explain/list.vue` 中仅为单页多阶段服务的逻辑,目标代码形态示意:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const explainHallItems = ref<ExplainHallSelectItem[]>([])
|
||||||
|
const explainLoading = ref(false)
|
||||||
|
const explainError = ref('')
|
||||||
|
|
||||||
|
const handleExplainHallClick = (hallId: string) => {
|
||||||
|
const hall = explainHallItems.value.find((item) => item.id === hallId)
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
hallId,
|
||||||
|
hallName: hall?.name || '讲解'
|
||||||
|
})
|
||||||
|
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/explain/business-unit-list?${params.toString()}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
模板里只保留:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<ExplainHallSelect
|
||||||
|
:halls="explainHallItems"
|
||||||
|
stage="hall"
|
||||||
|
:loading="explainLoading"
|
||||||
|
:error="explainError"
|
||||||
|
@hall-click="handleExplainHallClick"
|
||||||
|
@back="handleBack"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
移除 `business-units`、`guide-stops`、`selected-*`、`history.pushState/popstate` 相关逻辑。
|
||||||
|
|
||||||
|
- [ ] **Step 5: 运行类型检查验证页面收敛没有破坏现有引用**
|
||||||
|
|
||||||
|
Run: `pnpm type-check`
|
||||||
|
Expected: exit 0,`pages/index/index.vue` 与 `pages/explain/list.vue` 不再报 `ExplainSelectStage` / `selectedExplainBusinessUnit` 等已删除字段错误
|
||||||
|
|
||||||
|
- [ ] **Step 6: 提交**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/pages/index/index.vue src/pages/explain/list.vue
|
||||||
|
git commit -m "refactor: split explain hall entry from home tab"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 2: 新增业务单元列表页并接管展厅级标题与返回
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/pages/explain/business-unit-list.vue`
|
||||||
|
- Modify: `src/pages.json`
|
||||||
|
- Test: `src/pages/explain/business-unit-list.vue`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes:
|
||||||
|
- `explainUseCase.loadTemporaryBusinessUnitsByHall(hallId: string): Promise<ExplainBusinessUnit[]>`
|
||||||
|
- `ExplainHallSelect.vue` 的 `stage="unit"` 展示模式
|
||||||
|
- Produces:
|
||||||
|
- 新页面路由:`/pages/explain/business-unit-list?hallId=<id>&hallName=<name>`
|
||||||
|
- 页面级标题设置函数:`syncPageTitle(title: string): void`
|
||||||
|
- 页面级返回:`goBackToExplainHallList(): void`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 在 `pages.json` 注册新页面**
|
||||||
|
|
||||||
|
新增页面配置:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"path": "pages/explain/business-unit-list",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "业务单元",
|
||||||
|
"navigationBarBackgroundColor": "#FFFFFF",
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
插入位置紧跟 `pages/explain/list`,保持 explain 路由集中。
|
||||||
|
|
||||||
|
- [ ] **Step 2: 创建业务单元页骨架并先放失败态/空态兜底**
|
||||||
|
|
||||||
|
创建 `src/pages/explain/business-unit-list.vue`,先写最小骨架:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<GuidePageFrame
|
||||||
|
active-tab="explain"
|
||||||
|
variant="static"
|
||||||
|
:show-top-tabs="false"
|
||||||
|
show-back
|
||||||
|
@tab-change="handleTopTabChange"
|
||||||
|
@back="handleBack"
|
||||||
|
>
|
||||||
|
<view class="explain-business-unit-page">
|
||||||
|
<ExplainHallSelect
|
||||||
|
:business-units="explainBusinessUnitItems"
|
||||||
|
stage="unit"
|
||||||
|
:selected-hall-name="selectedExplainHallName"
|
||||||
|
:loading="explainLoading"
|
||||||
|
:error="explainError"
|
||||||
|
@business-unit-click="handleExplainBusinessUnitClick"
|
||||||
|
@back="handleBack"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</GuidePageFrame>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 实现参数解析、标题同步与数据加载**
|
||||||
|
|
||||||
|
在脚本中实现以下最小接口:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const selectedExplainHallId = ref('')
|
||||||
|
const selectedExplainHallName = ref('')
|
||||||
|
const explainBusinessUnits = ref<ExplainBusinessUnit[]>([])
|
||||||
|
const explainLoading = ref(false)
|
||||||
|
const explainError = ref('')
|
||||||
|
|
||||||
|
const explainBusinessUnitItems = computed<ExplainBusinessUnitSelectItem[]>(() => (
|
||||||
|
explainBusinessUnits.value.map((unit) => ({
|
||||||
|
id: unit.id,
|
||||||
|
name: unit.name,
|
||||||
|
hallId: unit.hallId,
|
||||||
|
previewImageUrl: unit.previewImageUrl,
|
||||||
|
guideStopCount: unit.guideStopCount
|
||||||
|
}))
|
||||||
|
))
|
||||||
|
|
||||||
|
const syncPageTitle = (title: string) => {
|
||||||
|
uni.setNavigationBarTitle({ title })
|
||||||
|
if (typeof document !== 'undefined') document.title = title
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`onLoad` 内实现:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
onLoad(async (options: any = {}) => {
|
||||||
|
const hallId = Array.isArray(options.hallId) ? options.hallId[0] : options.hallId
|
||||||
|
const hallName = Array.isArray(options.hallName) ? options.hallName[0] : options.hallName
|
||||||
|
|
||||||
|
selectedExplainHallId.value = hallId || ''
|
||||||
|
selectedExplainHallName.value = hallName || '业务单元'
|
||||||
|
syncPageTitle(selectedExplainHallName.value)
|
||||||
|
|
||||||
|
if (!selectedExplainHallId.value) {
|
||||||
|
explainError.value = '缺少展厅参数,请返回讲解列表后重试'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
explainLoading.value = true
|
||||||
|
try {
|
||||||
|
explainBusinessUnits.value = await explainUseCase.loadTemporaryBusinessUnitsByHall(selectedExplainHallId.value)
|
||||||
|
} catch (error) {
|
||||||
|
explainError.value = '展厅讲解点加载失败,请稍后重试'
|
||||||
|
} finally {
|
||||||
|
explainLoading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 实现业务单元点击和页面级返回**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const handleExplainBusinessUnitClick = (unitId: string) => {
|
||||||
|
const unit = explainBusinessUnits.value.find((item) => item.id === unitId)
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
hallId: selectedExplainHallId.value,
|
||||||
|
hallName: selectedExplainHallName.value,
|
||||||
|
unitId,
|
||||||
|
unitName: unit?.name || '讲解点'
|
||||||
|
})
|
||||||
|
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/explain/guide-stop-list?${params.toString()}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const goBackToExplainHallList = () => {
|
||||||
|
uni.redirectTo({
|
||||||
|
url: '/pages/explain/list',
|
||||||
|
fail: () => {
|
||||||
|
uni.reLaunch({ url: '/pages/explain/list' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
uni.navigateBack({
|
||||||
|
delta: 1,
|
||||||
|
fail: goBackToExplainHallList
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: 运行 lint 验证新页面与 pages.json 兼容**
|
||||||
|
|
||||||
|
Run: `pnpm lint`
|
||||||
|
Expected: exit 0,`business-unit-list.vue` 没有未使用导入、`pages.json` 对应引用未触发脚本错误
|
||||||
|
|
||||||
|
- [ ] **Step 6: 提交**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/pages.json src/pages/explain/business-unit-list.vue
|
||||||
|
git commit -m "feat: add explain business unit page"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 3: 新增讲解点列表页并接管业务单元级标题与返回
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/pages/explain/guide-stop-list.vue`
|
||||||
|
- Modify: `src/pages.json`
|
||||||
|
- Test: `src/pages/explain/guide-stop-list.vue`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes:
|
||||||
|
- `explainUseCase.selectBusinessUnit(hallId: string, unitId: string): Promise<ExplainBusinessUnit | null>`
|
||||||
|
- `normalizeExplainDetailTargetFromGuideStop({ id: string }): ExplainDetailTarget`
|
||||||
|
- `ExplainHallSelect.vue` 的 `stage="stop"` 展示模式
|
||||||
|
- Produces:
|
||||||
|
- 新页面路由:`/pages/explain/guide-stop-list?hallId=<id>&hallName=<name>&unitId=<id>&unitName=<name>`
|
||||||
|
- 点击讲解点后进入 `pages/exhibit/detail`
|
||||||
|
- 页面级返回:`goBackToBusinessUnitList(): void`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 在 `pages.json` 注册讲解点列表页**
|
||||||
|
|
||||||
|
新增页面配置:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"path": "pages/explain/guide-stop-list",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "讲解点",
|
||||||
|
"navigationBarBackgroundColor": "#FFFFFF",
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 创建讲解点页骨架**
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<GuidePageFrame
|
||||||
|
active-tab="explain"
|
||||||
|
variant="static"
|
||||||
|
:show-top-tabs="false"
|
||||||
|
show-back
|
||||||
|
@tab-change="handleTopTabChange"
|
||||||
|
@back="handleBack"
|
||||||
|
>
|
||||||
|
<view class="explain-guide-stop-page">
|
||||||
|
<ExplainHallSelect
|
||||||
|
:guide-stops="explainGuideStopItems"
|
||||||
|
stage="stop"
|
||||||
|
:selected-business-unit-name="selectedExplainBusinessUnitName"
|
||||||
|
:loading="explainLoading"
|
||||||
|
:error="explainError"
|
||||||
|
@guide-stop-click="handleExplainGuideStopClick"
|
||||||
|
@back="handleBack"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</GuidePageFrame>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 实现参数解析、标题同步与业务单元恢复**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const selectedExplainHallId = ref('')
|
||||||
|
const selectedExplainHallName = ref('')
|
||||||
|
const selectedExplainBusinessUnitId = ref('')
|
||||||
|
const selectedExplainBusinessUnitName = ref('')
|
||||||
|
const explainGuideStopItems = ref<ExplainGuideStopSelectItem[]>([])
|
||||||
|
const explainLoading = ref(false)
|
||||||
|
const explainError = ref('')
|
||||||
|
|
||||||
|
const toGuideStopItems = (unit: ExplainBusinessUnit): ExplainGuideStopSelectItem[] => (
|
||||||
|
unit.stops.map((stop) => ({
|
||||||
|
id: stop.id,
|
||||||
|
name: stop.name,
|
||||||
|
hallId: stop.hallId,
|
||||||
|
hallName: stop.hallName,
|
||||||
|
floorId: stop.floorId,
|
||||||
|
poiId: stop.poiId,
|
||||||
|
mapX: stop.mapX,
|
||||||
|
mapY: stop.mapY,
|
||||||
|
coverImageUrl: stop.coverImageUrl,
|
||||||
|
description: stop.description,
|
||||||
|
hasAudio: stop.hasAudio,
|
||||||
|
audioStatus: stop.audioStatus,
|
||||||
|
guideLevel: stop.guideLevel,
|
||||||
|
playTargetType: stop.playTargetType,
|
||||||
|
playTargetId: stop.playTargetId
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
`onLoad` 中:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
onLoad(async (options: any = {}) => {
|
||||||
|
selectedExplainHallId.value = Array.isArray(options.hallId) ? options.hallId[0] : options.hallId || ''
|
||||||
|
selectedExplainHallName.value = Array.isArray(options.hallName) ? options.hallName[0] : options.hallName || ''
|
||||||
|
selectedExplainBusinessUnitId.value = Array.isArray(options.unitId) ? options.unitId[0] : options.unitId || ''
|
||||||
|
selectedExplainBusinessUnitName.value = Array.isArray(options.unitName) ? options.unitName[0] : options.unitName || '讲解点'
|
||||||
|
|
||||||
|
syncPageTitle(selectedExplainBusinessUnitName.value)
|
||||||
|
|
||||||
|
if (!selectedExplainHallId.value || !selectedExplainBusinessUnitId.value) {
|
||||||
|
explainError.value = '缺少业务单元参数,请返回上一级后重试'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
explainLoading.value = true
|
||||||
|
try {
|
||||||
|
const unit = await explainUseCase.selectBusinessUnit(
|
||||||
|
selectedExplainHallId.value,
|
||||||
|
selectedExplainBusinessUnitId.value
|
||||||
|
)
|
||||||
|
if (!unit) {
|
||||||
|
explainError.value = '未找到对应业务单元,请返回上一级后重试'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
selectedExplainBusinessUnitName.value = unit.name || selectedExplainBusinessUnitName.value
|
||||||
|
syncPageTitle(selectedExplainBusinessUnitName.value)
|
||||||
|
explainGuideStopItems.value = toGuideStopItems(unit)
|
||||||
|
} catch (error) {
|
||||||
|
explainError.value = '讲解点加载失败,请稍后重试'
|
||||||
|
} finally {
|
||||||
|
explainLoading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 实现讲解点跳详情和业务单元页回退**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const handleExplainGuideStopClick = (stop: ExplainGuideStopSelectItem) => {
|
||||||
|
const target = stop.playTargetType && stop.playTargetId
|
||||||
|
? { targetType: stop.playTargetType, targetId: stop.playTargetId }
|
||||||
|
: normalizeExplainDetailTargetFromGuideStop({ id: stop.id })
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
id: stop.id,
|
||||||
|
tab: 'explain',
|
||||||
|
targetType: target.targetType,
|
||||||
|
targetId: target.targetId
|
||||||
|
})
|
||||||
|
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/exhibit/detail?${params.toString()}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const goBackToBusinessUnitList = () => {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
hallId: selectedExplainHallId.value,
|
||||||
|
hallName: selectedExplainHallName.value
|
||||||
|
})
|
||||||
|
|
||||||
|
uni.redirectTo({
|
||||||
|
url: `/pages/explain/business-unit-list?${params.toString()}`,
|
||||||
|
fail: () => {
|
||||||
|
uni.reLaunch({ url: `/pages/explain/business-unit-list?${params.toString()}` })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
uni.navigateBack({
|
||||||
|
delta: 1,
|
||||||
|
fail: goBackToBusinessUnitList
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: 运行类型检查确认 explain 详情入参链路未被破坏**
|
||||||
|
|
||||||
|
Run: `pnpm type-check`
|
||||||
|
Expected: exit 0,`guide-stop-list.vue` 到 `pages/exhibit/detail` 的 `targetType/targetId` 参数类型与详情页现有解析逻辑兼容
|
||||||
|
|
||||||
|
- [ ] **Step 6: 提交**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/pages.json src/pages/explain/guide-stop-list.vue
|
||||||
|
git commit -m "feat: add explain guide stop page"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 4: 收敛 ExplainHallSelect 组件为“页面驱动、单模式复用”
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/components/explain/ExplainHallSelect.vue`
|
||||||
|
- Test: `src/components/explain/ExplainHallSelect.vue`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes:
|
||||||
|
- 父页面传入的 `stage: 'hall' | 'unit' | 'stop'`
|
||||||
|
- 父页面传入的 `selectedHallName?: string`
|
||||||
|
- 父页面传入的 `selectedBusinessUnitName?: string`
|
||||||
|
- Produces:
|
||||||
|
- 保持 `hallClick(hallId: string)`
|
||||||
|
- 保持 `businessUnitClick(unitId: string)`
|
||||||
|
- 保持 `guideStopClick(stop: ExplainGuideStopSelectItem)`
|
||||||
|
- 保持 `back()`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 阅读组件中仅服务单页状态机的逻辑**
|
||||||
|
|
||||||
|
检查以下内容是否仍有必要:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const shouldUseHostNavigation = computed(() => isEmbeddedInWechatMiniProgram())
|
||||||
|
const headerTitle = computed(() => {
|
||||||
|
if (props.stage === 'unit') return props.selectedHallName || '选择业务单元'
|
||||||
|
if (props.stage === 'stop') return props.selectedBusinessUnitName || '选择讲解点'
|
||||||
|
return '讲解'
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
判断原则:页面已拆开后,组件内部标题不再是宿主标题来源,只是普通 H5 自绘页头文案。
|
||||||
|
|
||||||
|
- [ ] **Step 2: 删除不再需要的宿主导航分支和单页补丁依赖**
|
||||||
|
|
||||||
|
如果页面层已经承接宿主标题与返回,则组件只保留纯展示逻辑。最小目标是让组件不再承担“宿主导航是否可用”的页面级责任。目标代码形态:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const headerTitle = computed(() => {
|
||||||
|
if (props.stage === 'unit') return props.selectedHallName || '业务单元'
|
||||||
|
if (props.stage === 'stop') return props.selectedBusinessUnitName || '讲解点'
|
||||||
|
return '讲解'
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
如确认嵌入小程序场景仍需隐藏自绘页头,可保留 `shouldUseHostNavigation`,但不要再让组件承担页面级 history/title 逻辑。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 确认三种模式在拆页后都能独立渲染**
|
||||||
|
|
||||||
|
核对模板三块:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
v-if="stage === 'hall'"
|
||||||
|
v-else-if="stage === 'unit'"
|
||||||
|
v-else-if="stage === 'stop'"
|
||||||
|
```
|
||||||
|
|
||||||
|
确保:
|
||||||
|
- hall 模式不依赖 `businessUnits`
|
||||||
|
- unit 模式不依赖 `guideStops`
|
||||||
|
- stop 模式不依赖首页 explain 旧状态
|
||||||
|
|
||||||
|
- [ ] **Step 4: 运行 lint 保证组件收敛后无未使用代码**
|
||||||
|
|
||||||
|
Run: `pnpm lint`
|
||||||
|
Expected: exit 0,`ExplainHallSelect.vue` 无未使用 import / computed / props 分支
|
||||||
|
|
||||||
|
- [ ] **Step 5: 提交**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/components/explain/ExplainHallSelect.vue
|
||||||
|
git commit -m "refactor: narrow explain hall select responsibilities"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 5: 清理旧的单页讲解状态逻辑并完成全链路回归
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/pages/explain/list.vue`
|
||||||
|
- Modify: `src/pages/index/index.vue`
|
||||||
|
- Modify: `src/pages/exhibit/detail.vue`(仅当详情返回链路需要最小调整)
|
||||||
|
- Test: `src/pages/explain/list.vue`
|
||||||
|
- Test: `src/pages/explain/business-unit-list.vue`
|
||||||
|
- Test: `src/pages/explain/guide-stop-list.vue`
|
||||||
|
- Test: `src/pages/exhibit/detail.vue`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes:
|
||||||
|
- 新页面路由 `/pages/explain/business-unit-list`
|
||||||
|
- 新页面路由 `/pages/explain/guide-stop-list`
|
||||||
|
- 现有详情页 `handleBack()` / `buildDetailRoute()` 逻辑
|
||||||
|
- Produces:
|
||||||
|
- 不再存在依赖 `explainStage` 的宿主标题 / history patch
|
||||||
|
- 完整回归链路:展厅列表 → 业务单元 → 讲解点 → 详情 → 返回
|
||||||
|
|
||||||
|
- [ ] **Step 1: 删除 `pages/explain/list.vue` 中旧的单页 history 补丁**
|
||||||
|
|
||||||
|
删除以下类型逻辑:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type ExplainHistoryState = { ... }
|
||||||
|
window.history.pushState(...)
|
||||||
|
window.history.replaceState(...)
|
||||||
|
window.addEventListener('popstate', ...)
|
||||||
|
```
|
||||||
|
|
||||||
|
因为页面拆分后它们不再是正确抽象。
|
||||||
|
|
||||||
|
- [ ] **Step 2: 删除首页中旧的 explain 多阶段状态字段**
|
||||||
|
|
||||||
|
从 `src/pages/index/index.vue` 删除或收敛以下字段:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const explainBusinessUnits = ref<ExplainBusinessUnit[]>([])
|
||||||
|
const explainStage = ref<ExplainSelectStage>('hall')
|
||||||
|
const selectedExplainHallId = ref('')
|
||||||
|
const selectedExplainBusinessUnitId = ref('')
|
||||||
|
const selectedExplainBusinessUnitName = ref('')
|
||||||
|
```
|
||||||
|
|
||||||
|
并同步删除只为这些字段服务的 computed / handlers。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 最小检查详情返回是否还需要补充来源兜底**
|
||||||
|
|
||||||
|
检查 `src/pages/exhibit/detail.vue` 当前逻辑:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const handleBack = () => {
|
||||||
|
if (typeof window !== 'undefined' && window.history.length > 1) {
|
||||||
|
window.history.back()
|
||||||
|
...
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.navigateBack({
|
||||||
|
delta: 1,
|
||||||
|
fail: fallbackToExplainHome
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
如果从新讲解点页进入详情后,`navigateBack()` 已能自然回到讲解点列表,则不改代码;如果仍会误回首页,再补最小来源参数(例如 `from=guide-stop-list` 与返回兜底路由),但禁止顺手改动播放器或详情正文逻辑。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 运行完整静态验证**
|
||||||
|
|
||||||
|
Run: `pnpm type-check`
|
||||||
|
Expected: exit 0
|
||||||
|
|
||||||
|
Run: `pnpm lint`
|
||||||
|
Expected: exit 0
|
||||||
|
|
||||||
|
Run: `pnpm build:h5`
|
||||||
|
Expected: build complete,允许保留项目既有 Sass / 字体告警,但不得有新的 explain 路由构建错误
|
||||||
|
|
||||||
|
- [ ] **Step 5: 执行 H5 手工回归**
|
||||||
|
|
||||||
|
按以下顺序人工验证:
|
||||||
|
|
||||||
|
```text
|
||||||
|
1. 首页进入讲解 Tab,展厅列表正常显示
|
||||||
|
2. 点击展厅,进入业务单元列表页,标题为当前展厅名
|
||||||
|
3. 点击业务单元,进入讲解点列表页,标题为当前业务单元名
|
||||||
|
4. 点击讲解点,进入讲解详情页
|
||||||
|
5. 页面返回:详情 -> 讲解点列表 -> 业务单元列表 -> 展厅列表 -> 讲解首页/入口页
|
||||||
|
6. 直接打开业务单元列表页缺少 hallId 时显示错误态并可返回
|
||||||
|
7. 直接打开讲解点列表页缺少 hallId/unitId 时显示错误态并可返回
|
||||||
|
```
|
||||||
|
|
||||||
|
记录结果到提交说明或任务备注中,不写入生产代码。
|
||||||
|
|
||||||
|
- [ ] **Step 6: 提交**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/pages/index/index.vue src/pages/explain/list.vue src/pages/exhibit/detail.vue src/components/explain/ExplainHallSelect.vue src/pages/explain/business-unit-list.vue src/pages/explain/guide-stop-list.vue src/pages.json
|
||||||
|
git commit -m "feat: split explain flow into dedicated pages"
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user