- 拆分讲解展厅列表、业务单元列表、讲解点列表页面 - 明确标题、返回、路由参数与回归验证步骤 - 约束在现有 explain 数据边界内实施 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
21 KiB
讲解流独立页面拆分 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状态切换
- 首页讲解 Tab 点击展厅时调用
-
Step 1: 阅读并锁定当前讲解入口代码
阅读以下代码块并记录要删除或迁移的行为:
// 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 点击展厅跳独立页”的手工检查点
将以下检查点写进任务注释或临时核对清单,不写入生产代码:
入口回归预期:
1. 首页切到讲解 Tab,仍能看到展厅列表
2. 点击任一展厅,不再在首页内部切到业务单元 stage
3. 而是跳转到 /pages/explain/business-unit-list
- Step 3: 将首页 explain Tab 的点击行为改为页面跳转
把首页 explain 逻辑从“内部 stage 切换”改为“跳到独立页”。保留 explain Tab 首屏展厅列表展示,但去掉首页中业务单元和讲解点的状态推进逻辑。实现目标示意:
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()}`
})
}
同时移除首页中对以下逻辑的业务依赖:
explainStage.value = 'unit'
selectedExplainBusinessUnitId.value = ''
selectedExplainBusinessUnitName.value = ''
并把 handleExplainBusinessUnitClick / handleExplainBack 调整为不再驱动首页内部多级状态。
- Step 4: 收敛
pages/explain/list.vue为展厅列表页
删除 src/pages/explain/list.vue 中仅为单页多阶段服务的逻辑,目标代码形态示意:
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()}`
})
}
模板里只保留:
<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: 提交
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注册新页面
新增页面配置:
{
"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,先写最小骨架:
<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: 实现参数解析、标题同步与数据加载
在脚本中实现以下最小接口:
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 内实现:
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: 实现业务单元点击和页面级返回
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: 提交
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 }): ExplainDetailTargetExplainHallSelect.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注册讲解点列表页
新增页面配置:
{
"path": "pages/explain/guide-stop-list",
"style": {
"navigationBarTitleText": "讲解点",
"navigationBarBackgroundColor": "#FFFFFF",
"navigationStyle": "custom"
}
}
- Step 2: 创建讲解点页骨架
<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: 实现参数解析、标题同步与业务单元恢复
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 中:
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: 实现讲解点跳详情和业务单元页回退
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: 提交
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: 阅读组件中仅服务单页状态机的逻辑
检查以下内容是否仍有必要:
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: 删除不再需要的宿主导航分支和单页补丁依赖
如果页面层已经承接宿主标题与返回,则组件只保留纯展示逻辑。最小目标是让组件不再承担“宿主导航是否可用”的页面级责任。目标代码形态:
const headerTitle = computed(() => {
if (props.stage === 'unit') return props.selectedHallName || '业务单元'
if (props.stage === 'stop') return props.selectedBusinessUnitName || '讲解点'
return '讲解'
})
如确认嵌入小程序场景仍需隐藏自绘页头,可保留 shouldUseHostNavigation,但不要再让组件承担页面级 history/title 逻辑。
- Step 3: 确认三种模式在拆页后都能独立渲染
核对模板三块:
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: 提交
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 补丁
删除以下类型逻辑:
type ExplainHistoryState = { ... }
window.history.pushState(...)
window.history.replaceState(...)
window.addEventListener('popstate', ...)
因为页面拆分后它们不再是正确抽象。
- Step 2: 删除首页中旧的 explain 多阶段状态字段
从 src/pages/index/index.vue 删除或收敛以下字段:
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 当前逻辑:
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 手工回归
按以下顺序人工验证:
1. 首页进入讲解 Tab,展厅列表正常显示
2. 点击展厅,进入业务单元列表页,标题为当前展厅名
3. 点击业务单元,进入讲解点列表页,标题为当前业务单元名
4. 点击讲解点,进入讲解详情页
5. 页面返回:详情 -> 讲解点列表 -> 业务单元列表 -> 展厅列表 -> 讲解首页/入口页
6. 直接打开业务单元列表页缺少 hallId 时显示错误态并可返回
7. 直接打开讲解点列表页缺少 hallId/unitId 时显示错误态并可返回
记录结果到提交说明或任务备注中,不写入生产代码。
- Step 6: 提交
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"