feat: refine guide explain UX and QA docs
This commit is contained in:
@@ -1,43 +1,92 @@
|
||||
---
|
||||
name: shenzhen-natural-museum-dev
|
||||
description: Shenzhen Natural Museum frontend-miniapp H5 development standards. Use when working in this project on navigation, guide pages, indoor 3D/Three.js/GLB, Tencent Map/outdoor map logic, static nav assets, Guide Data Access Layer/导览数据访问层, data provider/adapter architecture, route_graph/nav_data readiness, 3D guide data audits, browser-simulated user-flow/user-acceptance audits, legacy demo data cleanup, mobile H5 compatibility, browser-based user flow testing, dead-end/return/state-loss/cancel-reset audits, quality gates, or architecture reviews for the museum guide H5 app. Treat mini-program/mp-weixin as out of scope unless the user explicitly asks for it.
|
||||
description: Shenzhen Natural Museum mobile H5 spatial content guide architecture standards. Use when working in this frontend-miniapp project on guide and explain businesses, indoor 3D/Three.js/GLB, POI/location preview, exhibit/hall/facility content, audio explanation, transcript/media data, Tencent Map/outdoor reference logic, static nav assets, Museum Content/Guide/Explain Data Access Layers, provider/adapter/repository architecture, route_graph/nav_data readiness, browser-simulated H5 user-flow audits, business-logic audits, engineering architecture reviews, legacy demo data cleanup, mobile overlay/canvas compatibility, and quality gates. Treat mini-program/mp-weixin as out of scope unless the user explicitly asks for it.
|
||||
---
|
||||
|
||||
# Shenzhen Natural Museum Dev
|
||||
|
||||
Use this skill for this repository's product work. The project may live under historical folder names, but the target product is the Shenzhen Natural Museum guide miniapp.
|
||||
Use this skill for the Shenzhen Natural Museum `frontend-miniapp` project. Treat the app as a mobile H5 spatial content guide platform, not as only an indoor 3D map.
|
||||
|
||||
The product has two first-class H5 businesses:
|
||||
|
||||
- `guide`: outdoor reference map, indoor 3D display, full-building/single-floor switching, POI search/focus, and POI/location preview. Real route planning requires verified `route_graph` and `nav_data`.
|
||||
- `explain`: exhibit/hall content, audio explanation, transcript/chapters/media metadata, search/filtering, player state, and optional linkage from an explain item to a guide location preview.
|
||||
|
||||
## Project Ground Truth
|
||||
|
||||
- Main workspace: `E:\MyWork\深圳国际艺术馆\museum-guide\museum-guide-v4.0\frontend-miniapp`.
|
||||
- Default runtime target: H5. Do not consider mini-program/mp-weixin constraints unless the user explicitly asks for them.
|
||||
- Current clean nav asset package: `static/nav-assets/app_nav_assets_v2_clean_20260609_075339`.
|
||||
- Current guide data service: `src/services/navAssets.ts`.
|
||||
- Current clean nav asset package: `static/nav-assets/app_nav_assets_v2_clean_20260611_093623`.
|
||||
- Current guide data boundary: `src/data/providers/staticNavAssetsProvider.ts` + `src/data/adapters/navAssetsAdapter.ts` + `src/repositories/GuideRepository.ts` / `src/repositories/GuideModelRepository.ts`.
|
||||
- Current content/explain architecture is still transitional. Treat Provider / Adapter / Repository / UseCase as the current data boundary; do not reintroduce the retired legacy nav assets service.
|
||||
- Legacy demo data area: `src/assets/data`. Treat it as historical/demo-only unless the user explicitly asks to inspect or migrate it.
|
||||
- Current guide capability is indoor 3D display and POI/location preview. Do not present it as certified real indoor navigation until `route_graph` and `nav_data` are available and verified.
|
||||
- Current guide capability is indoor 3D display plus POI/location preview. Do not present it as certified real indoor navigation until `route_graph` and `nav_data` are available and verified.
|
||||
- Current explain capability must be described as content/audio explanation only when real media data is available. Do not invent working audio, transcript, or exhibit data from placeholders.
|
||||
|
||||
## Non-Negotiable Rules
|
||||
## Non-Negotiable Architecture Rules
|
||||
|
||||
- Do not reintroduce business dependencies from the guide module to `src/assets/data/*.json`.
|
||||
- Keep guide data loading centralized through `src/services/navAssets.ts` or a deliberate successor service with the same clear boundary.
|
||||
- Keep data and presentation decoupled. Pages and visual components must consume domain models or view models, not raw static package JSON, backend fields, GLB manifest details, or ad hoc demo arrays.
|
||||
- Use this target layer shape for new or refactored work:
|
||||
|
||||
```text
|
||||
Data Sources
|
||||
static nav-assets / CMS API / backend API / IMDF / BIM export / media manifest
|
||||
↓
|
||||
Providers
|
||||
StaticProvider / ApiProvider / CmsProvider / FutureImdfProvider
|
||||
↓
|
||||
Adapters
|
||||
source-specific fields -> unified museum domain models
|
||||
↓
|
||||
Repositories / Data Access Layers
|
||||
MuseumContentRepository / GuideRepository / ExplainRepository / MediaRepository
|
||||
↓
|
||||
Use Cases / Composables / Stores
|
||||
guide session, explain session, search, selected target, player, route readiness
|
||||
↓
|
||||
Presentation ViewModels
|
||||
lightweight props for pages, ThreeMap, ExplainList, cards, player, search
|
||||
↓
|
||||
Presentation Components
|
||||
GuideMapShell / ThreeMap / ExplainList / AudioPlayer / Search / Cards
|
||||
```
|
||||
|
||||
- Keep guide and explain coupled through stable IDs and domain relationships such as `poiId`, `exhibitId`, `hallId`, and `floorId`, not through component internals.
|
||||
- Keep Tencent/outdoor map logic separate from indoor Three.js/GLB rendering. Do not make Tencent Map branches depend on indoor model state, and do not let indoor 3D changes alter outdoor map behavior.
|
||||
- Treat POI coordinates from the clean package as display or preview candidates, not certified navigation anchors.
|
||||
- Keep `NAV_ROUTE_GRAPH_READY` false until real graph/nav data has been added, validated, and wired with tests or smoke verification.
|
||||
- Do not claim "开始馆内导航" or route planning is real unless the graph data exists. Prefer "位置预览", "查看位置", or an explicitly disabled state.
|
||||
- Keep `NAV_ROUTE_GRAPH_READY` false until real graph/nav data has been added, loaded, validated, and wired with smoke tests.
|
||||
- Do not claim "开始馆内导航", route planning, arrival guidance, turn-by-turn navigation, or positioning accuracy unless route graph/nav data and runtime behavior are verified.
|
||||
- Prefer "位置预览", "查看位置", "查看三维位置", or an explicitly disabled route state while route data is unavailable.
|
||||
- Do not use placeholder media such as `example.com/audio.mp3` as a working explain capability. Show an unavailable state or load a real media asset through the media/explain data layer.
|
||||
- Three.js/WebGL is H5-only for current project work. Do not add mini-program fallback or mp-weixin compatibility work unless specifically requested.
|
||||
- Preserve mobile overlays: top tabs/menu/search/cards/floor controls must remain visible and clickable above the 3D canvas.
|
||||
- Preserve mobile overlays: top tabs/menu/search/cards/floor controls/explain controls/audio player must remain visible and clickable above 3D canvas.
|
||||
- Avoid unrelated refactors, broad file deletion, or data cleanup without explicit confirmation. Contain and mark legacy first; delete only when asked.
|
||||
|
||||
## Domain Model Standards
|
||||
|
||||
Use stable museum spatial content domain models when designing data services or audits:
|
||||
|
||||
- `MuseumVenue`, `MuseumBuilding`, `MuseumFloor`, `MuseumSpace`
|
||||
- `MuseumPoi`, `MuseumHall`, `MuseumExhibit`, `MuseumFacility`, `MuseumCategory`
|
||||
- `GuideModelAsset`, `GuideLocationPreview`, `GuideRouteReadiness`, `GuideRouteGraph`
|
||||
- `ExplainTrack`, `ExplainMedia`, `ExplainTranscript`, `ExplainChapter`
|
||||
- `MediaAsset`, `SearchIndexItem`, `UserCollection`, `VisitHistory`
|
||||
|
||||
Guide data and explain data may share the same exhibit/hall/POI IDs, but their use cases differ:
|
||||
|
||||
- Guide answers: Where is it, on which floor, what should the 3D/map preview show, is real routing ready?
|
||||
- Explain answers: What is it, what should be played/read, what media/transcript is available, how can a visitor continue to a location preview?
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. Inspect before changing:
|
||||
- Use `rg`/`rg --files` to find guide, map, route, asset, and data references.
|
||||
- Check routing/page entry points before editing UI behavior.
|
||||
- Use `rg`/`rg --files` to find guide, explain, audio, map, route, asset, and data references.
|
||||
- Check page entry points and shared shell components before editing behavior.
|
||||
- Read the local implementation instead of assuming the uni-app/Vue structure.
|
||||
|
||||
2. Scope changes tightly:
|
||||
- For guide work, start around `src/pages/guide`, `src/components/guide`, `src/services/navAssets.ts`, and related route/search/facility pages.
|
||||
- Do not touch hall/exhibit/explain demo modules unless the task is explicitly about legacy cleanup or cross-module data consistency.
|
||||
- For guide work, start around `src/pages/index`, route/search/facility pages, `src/components/navigation`, `src/components/map`, `src/usecases/guideUseCase.ts`, and guide repositories/providers.
|
||||
- For explain work, start around `src/components/explain`, `src/components/audio`, exhibit/hall detail pages, top-tab routing, and the future content/explain repositories.
|
||||
- Do not touch legacy hall/exhibit/explain demo data unless the task is explicitly about migration, compatibility, or cleanup.
|
||||
|
||||
3. Protect the H5 boundary:
|
||||
- Default to H5 behavior, H5 styling, and H5 validation.
|
||||
@@ -46,53 +95,111 @@ Use this skill for this repository's product work. The project may live under hi
|
||||
|
||||
4. Verify after changes:
|
||||
- Run the smallest meaningful checks, then expand based on risk.
|
||||
- For frontend/UI guide changes, prefer at least `pnpm type-check`, `pnpm lint`, and `pnpm build:h5`.
|
||||
- For frontend/UI guide or explain changes, prefer at least `pnpm type-check`, `pnpm lint`, and `pnpm build:h5`.
|
||||
- Run `pnpm build:mp-weixin` only when the user explicitly requests mini-program validation.
|
||||
- Mention before running build commands if the task is read-only, because builds write `dist`.
|
||||
|
||||
## Data Source Standards
|
||||
|
||||
- Use clean manifests and future CMS/API manifests as source inputs, not as page/component contracts.
|
||||
- Keep static package fields and future API fields behind providers/adapters. Do not let pages depend on file paths, response field quirks, database IDs, or temporary migration aliases.
|
||||
- Keep all guide data loading centralized through providers/adapters/repositories/use cases. Do not let pages or components import static-package readers directly.
|
||||
- Introduce content/explain/media repositories before migrating explain pages out of hardcoded arrays.
|
||||
- Do not duplicate parsed POI, floor, category, model path, explain media, transcript, or route readiness logic in page components.
|
||||
- Use contract-first data evolution. Static packages, future APIs, and CMS data should expose compatible `schemaVersion`/dataset metadata and preserve stable domain models.
|
||||
- Mark old demo compatibility explicitly when a temporary bridge is unavoidable.
|
||||
- If cleaning old mock data, first inventory references with `rg`, then split work into:
|
||||
- guide-critical stale data that blocks current work;
|
||||
- explain/content demo data that needs repository migration;
|
||||
- unrelated historical demo modules;
|
||||
- deletion candidates that need user approval.
|
||||
|
||||
## Guide, Map, And 3D Standards
|
||||
|
||||
- Indoor 3D should load GLB/GLTF resources from the clean package under `static/nav-assets/...` while preserving texture/bin relationships.
|
||||
- Always provide loading and error fallback states for model loading.
|
||||
- Keep canvas sizing constrained by the guide layout. It must not cover fixed navigation, cards, buttons, or floor controls.
|
||||
- Avoid automatic page jumps when opening the indoor 3D view or clicking "start" actions. Explicit user intent should route, preview, or show a disabled explanation.
|
||||
- Watch GLB weight and runtime memory. The clean package previously contained multiple GLBs totaling about 24 MB; load only the primary model needed for the current view unless there is a product reason to load more.
|
||||
- Initial indoor 3D entry should load the full building model when that is the product requirement; switch to a single floor only after zoom/focus/explicit floor action.
|
||||
- Provide side controls or equivalent for full-building/multi-floor versus single-floor views.
|
||||
- Always provide loading, error, retry, and recovery paths for model loading.
|
||||
- Keep canvas sizing constrained by the guide layout. It must not cover fixed navigation, cards, buttons, floor controls, search, top tabs, or explain/player controls.
|
||||
- Avoid automatic page jumps when opening indoor 3D or clicking "start" actions. Explicit user intent should preview, route to search/detail, or show a disabled explanation.
|
||||
- Watch GLB weight and runtime memory. Load only the primary model needed for the current view unless there is a product reason to load more.
|
||||
- Prefer existing Three.js/GLTF patterns in the project. Add dependencies only when the repository lacks the needed loader/runtime.
|
||||
- Keep `ThreeMap` focused on rendering, camera, model lifecycle, POI visual highlighting, and emitted interactions. Move manifest parsing, floor/POI adapters, and route readiness outside of the renderer when refactoring.
|
||||
|
||||
## 数据源标准
|
||||
## Explain And Media Standards
|
||||
|
||||
- Use the clean manifest as the single guide data entry point.
|
||||
- If route/facility/search pages need POI data, consume normalized records from `src/services/navAssets.ts`.
|
||||
- Do not duplicate parsed nav data inside page components.
|
||||
- Use a Guide Data Access Layer/导览数据访问层 for all guide data. Pages and UI components must not bind directly to static JSON files or future backend API response shapes.
|
||||
- Treat `src/services/navAssets.ts` as the current 导览数据访问层 boundary. If it is replaced, the successor must keep the same boundary: pages consume normalized guide domain models, not raw static-package or API records.
|
||||
- Connect every data source through a Provider/Adapter architecture:
|
||||
- Static data packages use a Static Data Provider/静态数据源提供器.
|
||||
- Future backend APIs or database-backed services use an API Data Provider/接口数据源提供器.
|
||||
- Adapters/数据适配器 convert source-specific records into unified guide domain models such as floors, POIs, categories, model assets, connector endpoints, route previews, and future route graphs.
|
||||
- Keep current static-package fields and future API fields behind adapters. Do not let route/search/facility/detail pages depend on file paths, response field quirks, database IDs, or temporary migration aliases.
|
||||
- Use contract-first data evolution. Static packages and future APIs should expose compatible `schemaVersion`/dataset version metadata and preserve a stable domain model for `Floor`, `Poi`, `GuideAsset`, `ConnectorEndpoint`, `RoutePreview`, and future `RouteGraph`.
|
||||
- Keep phase boundaries explicit: phase 1 supports 3D display, POI search/filtering, and location preview; phase 2 route navigation requires `route_graph`, `nav_data`, verified anchors, and graph smoke tests.
|
||||
- Mark old demo compatibility explicitly when a temporary bridge is unavoidable.
|
||||
- If cleaning old mock data, first inventory references with `rg`, then split work into:
|
||||
- guide-critical stale data that blocks current work;
|
||||
- unrelated historical demo modules;
|
||||
- deletion candidates that need user approval.
|
||||
- Treat explain as a first-class content/media business, not as a decorative tab on the guide page.
|
||||
- Use explain/content repositories for exhibits, halls, tracks, transcripts, chapters, cover images, audio URLs, duration, language, and availability.
|
||||
- Keep `ExplainList` focused on presentation, filtering controls, and emitted user actions. Do not keep long-lived mock exhibit arrays or search indexes in the component once repository data is available.
|
||||
- Keep `AudioPlayer` focused on playback, progress, seek, pause/resume, error, and teardown. Do not fetch content, infer exhibit data, or decide guide location behavior inside the player.
|
||||
- Let explain items reference guide context through IDs such as `poiId`, `floorId`, `hallId`, or `exhibitId`. Clicking "查看位置" should invoke a guide location preview use case, not fake navigation.
|
||||
- Represent unavailable audio/transcripts explicitly. Do not silently replace missing media with placeholder URLs.
|
||||
- Preserve player and top-tab state across common H5 flows when product expectations require it: explain list -> exhibit detail -> back, guide -> explain -> guide, and audio mini-player open/close.
|
||||
- Audit exhibit/hall detail pages for historical demo content before treating them as current natural museum content.
|
||||
|
||||
## 三维导览数据审核模块
|
||||
## Business Logic Audit Module
|
||||
|
||||
Use this module when the user asks for business review, cross-business review, P1/P2/P3 audit, Shenzhen Science and Technology Museum interaction comparison, or guide/explain user-flow logic review.
|
||||
|
||||
Report with:
|
||||
|
||||
- Priority: P1/P2/P3.
|
||||
- Evidence: file path and line number.
|
||||
- Status: `已实现`, `部分实现`, `缺失`, or `数据不足导致无法判断`.
|
||||
- Evidence type: `源码审核`, `浏览器验证`, or both.
|
||||
- Suggested repair order.
|
||||
|
||||
Guide audit checklist:
|
||||
|
||||
- Initial outdoor/indoor state.
|
||||
- Indoor entry default full-building model when required.
|
||||
- Full-building/multi-floor versus single-floor switching.
|
||||
- Floor switching and floor state.
|
||||
- POI search, highlight, and location preview.
|
||||
- Route readiness and no false route/navigation claims.
|
||||
- Top "导览 / 讲解" switching.
|
||||
- Search, cards, floor controls, tool buttons, and top tabs visible/clickable above canvas.
|
||||
|
||||
Explain audit checklist:
|
||||
|
||||
- Explain tab entry, list loading, filters, search drawer, hot/history keyword behavior.
|
||||
- Exhibit/hall detail transitions, return paths, and top-tab preservation.
|
||||
- Audio play/pause/close, mini-player visibility, missing media fallback, playback state loss.
|
||||
- Transcript/chapter/language availability if data claims them.
|
||||
- "查看位置" or location actions must use location preview and must not promise real routing unless route data is verified.
|
||||
- No hardcoded historical art-demo content should appear as current natural museum data unless explicitly marked as legacy.
|
||||
|
||||
## Engineering Architecture Audit Module
|
||||
|
||||
Use this module when the user asks whether frontend architecture follows the spatial content guide strategy.
|
||||
|
||||
Check:
|
||||
|
||||
- Page layer: pages should compose use cases and view models, not parse raw data or own long-lived source arrays.
|
||||
- Shell layer: `GuidePageFrame`, top tabs, and shared navigation shell should handle layout and cross-business tab state without owning data conversion.
|
||||
- Guide shell: `GuideMapShell` should carry map/3D controls and mode switching without knowing static package internals beyond transitional props.
|
||||
- Renderer layer: `ThreeMap` should render model/POI/camera state, not own source-specific manifest/floor/POI adapters after migration.
|
||||
- Explain layer: `ExplainList` should render repository/view-model data and emit actions; `AudioPlayer` should own only playback runtime.
|
||||
- Data layer: static package and future API/CMS loading must stay behind providers/adapters/repositories/use cases; pages and presentation components must not own source-specific loading.
|
||||
- Repository layer: `MuseumContentRepository`, `GuideRepository`, `ExplainRepository`, and `MediaRepository` should hide providers/adapters from UI.
|
||||
- State layer: guide session, explain session, player state, search state, selected target, and route readiness should live in composables/stores/use cases when shared across pages.
|
||||
- H5/mp boundary: H5 guide/explain behavior should not be constrained by mp-weixin unless requested.
|
||||
- Overlay safety: canvas must not capture or cover top tabs, search, cards, floor/full-building controls, explain controls, or audio player.
|
||||
- Lifecycle: models, audio contexts, network requests, and event listeners must clean up on unmount and recover from failures.
|
||||
|
||||
## 3D Guide Data Audit Module
|
||||
|
||||
Use this module when the user asks for data review, nav data readiness, POI audit, route graph audit, GLB/3D guide data validation, or professional 3D guide industry review.
|
||||
|
||||
Audit the data as a navigation product, not just as JSON that parses:
|
||||
Audit the data as a spatial product, not just as JSON that parses:
|
||||
|
||||
1. Inventory the authoritative sources:
|
||||
- Identify clean package files under `static/nav-assets/...`, the manifest, GLB/GLTF assets, POI index, floor definitions, connector data, `route_graph`, `nav_data`, and code entry points that load them.
|
||||
- Confirm guide runtime pages consume data through `src/services/navAssets.ts` or an approved successor 导览数据访问层.
|
||||
- Treat 导览数据访问层 + 数据源提供器/数据适配器 as a required audit item. Flag any page, component, store, or utility that directly parses static-package files, directly depends on backend API response shapes, or duplicates source-specific transformation logic.
|
||||
- Check that static data packages and future API providers produce the same normalized guide domain models, or document the compatibility gap.
|
||||
- Mark `src/assets/data` as legacy/demo unless the task is explicitly about migrating it.
|
||||
1. Inventory authoritative sources:
|
||||
- Identify clean package files under `static/nav-assets/...`, manifest, GLB/GLTF assets, POI index, floor definitions, connector data, `route_graph`, `nav_data`, and code entry points.
|
||||
- Confirm runtime pages consume guide data through `GuideUseCase`, `GuideRepository`, `GuideModelRepository`, or approved data access successors rather than static-package files.
|
||||
- Flag any page, component, store, or utility that directly parses static-package files, directly depends on backend API response shapes, or duplicates source-specific transformation logic.
|
||||
- Mark `src/assets/data` as legacy/demo unless the task explicitly asks to migrate it.
|
||||
|
||||
2. Check professional 3D guide data readiness:
|
||||
2. Check professional guide readiness:
|
||||
- Coordinate system: every POI, floor, entrance, connector, and preview marker must declare or inherit the same coordinate space as the rendered GLB/GLTF.
|
||||
- Floor semantics: floor IDs, labels, order, elevation, and visible floor controls must match the model and UI.
|
||||
- POI quality: each POI needs stable `id`, display name, category, floor, source confidence, display coordinate, and enough context for search/detail/preview.
|
||||
@@ -105,16 +212,57 @@ Audit the data as a navigation product, not just as JSON that parses:
|
||||
- Keep `NAV_ROUTE_GRAPH_READY` false until `route_graph` and `nav_data` exist, are loaded, and pass smoke tests.
|
||||
- Do not approve route planning if graph nodes are unreachable, connectors are missing, floors are misaligned, or POI anchors are not mapped to walkable graph nodes.
|
||||
- If only preview data exists, report the capability as "位置预览" and list missing data needed for real navigation.
|
||||
- Verify that phase 2 route data can be added through the 导览数据访问层 without rewriting page-level route/search/facility logic.
|
||||
- Verify that phase 2 route data can be added through the data access layer without rewriting page-level route/search/facility/explain logic.
|
||||
|
||||
4. Report data findings with evidence:
|
||||
- Separate `Verified data issue`, `Runtime integration issue`, and `Source-only risk`.
|
||||
- Include priority, affected asset/file, object IDs, observed mismatch, expected 3D guide standard, data-access-layer/provider/adapter impact, user impact, and suggested verification.
|
||||
- Save requested data-audit reports under `docs/QA/` using names like `3d-guide-data-audit-YYYY-MM-DD.md`.
|
||||
## Explain Content Data Audit Module
|
||||
|
||||
Use this module when the user asks for explain/audio/content readiness.
|
||||
|
||||
Check:
|
||||
|
||||
- Content authority: exhibits, halls, facilities, themes, categories, and media must come from an approved content/explain repository or clearly marked legacy bridge.
|
||||
- Media integrity: audio URLs, cover images, transcripts, chapter timings, duration, language, and availability must be real and loadable on H5.
|
||||
- Cross-linking: explain tracks should link to exhibit/hall/POI/floor IDs that can resolve to content and optional location preview.
|
||||
- Search quality: explain search should use the same normalized content/search index strategy as guide search where sensible.
|
||||
- Truthfulness: UI must not claim audio, transcript, or guided explanation exists when the data is missing.
|
||||
- Player lifecycle: audio context cleanup, error handling, seek/progress, pause/resume, and back/top-tab transitions must be tested.
|
||||
|
||||
## Browser User Flow Closure Testing
|
||||
|
||||
Use this module when the user asks to test the H5 app through browser automation, find user logic loops that do not close, audit dead ends, verify return behavior, or test guide/explain/search/detail flows.
|
||||
|
||||
1. Prepare the target:
|
||||
- If the user provides a URL, use it directly.
|
||||
- If the user says to use the actual project or a temporary service, inspect `package.json` and start the smallest H5 dev server command that matches the repo; report the local preview URL.
|
||||
- Treat browser simulation as the evidence baseline for user testing. Use real clicks/taps, DOM snapshots, viewport changes, browser back, refresh, and deep-link checks when a runnable H5 URL is available.
|
||||
- Use screenshots when visual layering, mobile fit, 3D canvas coverage, or map/audio overlay behavior matters.
|
||||
- If browser verification is impossible, label findings as `source-only risk`; do not present them as user-tested or browser-verified.
|
||||
- Keep the test scoped to H5 unless the user explicitly asks for mp-weixin.
|
||||
|
||||
2. Simulate real user tasks:
|
||||
- Home guide: outdoor map, indoor 3D switch, full-building/single-floor switch, primary guide CTA, search entry, recommended entrance flow.
|
||||
- Search: keyword entry or existing query, filters, result card click, result action click, browser/back navigation.
|
||||
- Facility/detail/route preview: choose target, view position, unavailable route state, return/reset/cancel.
|
||||
- Explain: tab switch, search drawer, hot/history keyword, result click, exhibit detail, audio play/pause/close, missing audio fallback.
|
||||
- Hall/exhibit detail: direct-open and in-flow-open behavior, visible back path, top-tab preservation, bottom action behavior.
|
||||
|
||||
3. Check closure risks:
|
||||
- Dead end after click: page stays stuck, white screen, missing content, or action only logs/toasts without a useful next step.
|
||||
- Interrupted task: button text promises navigation/search/audio/selection but there is no continuation, completion, or recovery instruction.
|
||||
- State loss: selected tab, 2D/3D mode, full-building/floor mode, filter, search keyword, target, preview mode, audio item/progress, or route step disappears after navigation, refresh, share/deep link, or browser back.
|
||||
- Missing return control: custom-navigation pages must provide a visible in-page back/cancel/close path, not only rely on browser/system back.
|
||||
- Multi-step trap: start/target/location/route/explain flows must have cancel, reset, retry, and completion or disabled states.
|
||||
|
||||
4. Validate against current product truth:
|
||||
- Treat guide capability as indoor 3D display plus POI/location preview until verified `route_graph` and `nav_data` exist.
|
||||
- Treat explain capability as content/audio playback only when real media data exists.
|
||||
- Flag UI that claims route planning, start navigation, arrival, certified guidance, audio, transcript, or explain content while data/runtime support is missing.
|
||||
- Flag old mock data when it changes the user's selected object or shows unrelated historical demo detail.
|
||||
- Keep Tencent/outdoor map, indoor Three.js, and audio player risks separate.
|
||||
|
||||
## Quality Gates
|
||||
|
||||
Use these checks as the project baseline:
|
||||
Use these checks as the project baseline for code changes:
|
||||
|
||||
```powershell
|
||||
pnpm type-check
|
||||
@@ -123,89 +271,17 @@ pnpm build:h5
|
||||
```
|
||||
|
||||
- Treat lint warnings as debt even if the command exits successfully.
|
||||
- Add or run H5 smoke checks for: top menu visibility, indoor 3D scene, "开始馆内导航"/preview action, search result click, and route/detail page.
|
||||
- For mobile risk, inspect small viewport behavior: overlays above canvas, no text/button overlap, no horizontal overflow, and no accidental full-screen canvas capture.
|
||||
- Add or run H5 smoke checks for top tabs, guide/explain tab switching, indoor 3D scene, full-building/single-floor control, search result click, route/detail preview, explain search, and audio play/pause/close when affected.
|
||||
- For mobile risk, inspect small viewport behavior: overlays above canvas, no text/button overlap, no horizontal overflow, no accidental full-screen canvas capture, and no audio player overlap with critical controls.
|
||||
- If the user explicitly asks for mini-program/mp-weixin, add `pnpm build:mp-weixin` as an extra check for that task only.
|
||||
|
||||
## Browser User Flow Closure Testing
|
||||
|
||||
Use this module when the user asks to test the Shenzhen Natural Museum guide app through browser automation, find user logic loops that do not close, audit dead ends, verify return behavior, or test multi-step guide/search/detail/route flows.
|
||||
|
||||
1. Prepare the target:
|
||||
- If the user provides a URL, use it directly.
|
||||
- If the user says to use the actual project or a temporary service, inspect `package.json` and start the smallest H5 dev server command that matches the repo; report the local preview URL.
|
||||
- Treat browser simulation as the evidence baseline for user testing. If a runnable H5 URL or dev server is available, use the Browser in-app automation plugin with DOM snapshots, real clicks/taps, viewport changes, browser back, refresh, and deep-link checks.
|
||||
- Use screenshots when visual layering, mobile fit, 3D canvas coverage, or map overlay behavior matters.
|
||||
- If browser verification is impossible, label findings as `source-only risk`; do not present them as user-tested or browser-verified.
|
||||
- Keep the test scoped to H5 unless the user explicitly asks for mp-weixin.
|
||||
|
||||
2. Simulate real user tasks, not just route URLs:
|
||||
- Home guide: outdoor map, indoor 3D switch, primary guide CTA, search entry, recommended entrance flow.
|
||||
- Search: keyword entry or existing query, filters, result card click, result action click, browser/back navigation.
|
||||
- Facility detail: choose start, view position, search from detail, return from route.
|
||||
- Route detail: location preview, outdoor reference switch, target-location action, mode/floor/tool controls, browser reload.
|
||||
- Explain: tab switch, search drawer, hot/history keyword, result click, exhibit detail, audio/play/navigation actions.
|
||||
- Hall/exhibit detail: direct-open and in-flow-open behavior, visible back path, bottom action behavior.
|
||||
|
||||
3. Check these closure risks on every flow:
|
||||
- Dead end after click: page stays stuck, white screen, missing content, or action only logs/toasts without a useful next step.
|
||||
- Interrupted task: button text promises navigation/search/audio/selection but there is no continuation, completion, or recovery instruction.
|
||||
- State loss: route query, selected tab, 2D/3D mode, filter, search keyword, target, preview mode, or route step disappears after navigation, refresh, share/deep link, or browser back.
|
||||
- Missing return control: custom-navigation pages must provide a visible in-page back/cancel/close path, not only rely on browser/system back.
|
||||
- Multi-step trap: start/target/location/route flows must have cancel, reset, retry, and completion paths.
|
||||
|
||||
4. Validate against current product truth:
|
||||
- Treat current capability as indoor 3D display plus POI/location preview until verified `route_graph` and `nav_data` exist.
|
||||
- Flag any UI that claims real route planning, start navigation, arrival, or certified guidance while `NAV_ROUTE_GRAPH_READY` is false.
|
||||
- Flag old mock data when it changes the user's selected object, e.g. a natural museum search result opens an unrelated historical demo detail.
|
||||
- Keep Tencent/outdoor map issues separate from indoor Three.js/GLB issues.
|
||||
|
||||
5. Report with evidence:
|
||||
- Separate browser-verified findings from source-only risks.
|
||||
- Mark a finding as browser-verified only when it was reproduced through actual browser steps. Source inspection can support evidence but cannot replace the browser user test.
|
||||
- For each finding include: priority, user path, observed behavior, expected closure, impact, and file/line evidence when available.
|
||||
- Mention positive paths that work, such as back buttons, close buttons, or state preservation.
|
||||
- If saving a report is requested, place it under `docs/QA/` using a date-based name such as `user-flow-closure-audit-YYYY-MM-DD.md`.
|
||||
|
||||
## 3D Guide User Audit Module
|
||||
|
||||
Use this module when the user asks for user review, usability audit, user acceptance testing, guided-path testing, or professional 3D guide user-flow review.
|
||||
|
||||
Audit whether a real visitor can complete the museum task, not only whether clicks fire:
|
||||
|
||||
1. Define visitor tasks before testing:
|
||||
- Find a facility or exhibit from the home guide.
|
||||
- Understand indoor vs outdoor context.
|
||||
- Preview a target location in 3D.
|
||||
- Recover from route unavailable, model load failure, search miss, location denial, or wrong target.
|
||||
- Switch between "导览" and "讲解" without losing necessary context.
|
||||
|
||||
2. Apply 3D guide UX standards:
|
||||
- Orientation: the user always knows current floor, map mode, target, and whether the view is indoor 3D or outdoor map.
|
||||
- Control visibility: top tabs/menu/search/cards/floor controls remain above canvas, tappable, and readable on mobile.
|
||||
- Flow closure: every start/target/search/preview route has visible back, cancel, reset, retry, and completion or disabled states.
|
||||
- Trust and truthfulness: labels must not promise real route planning, arrival guidance, turn-by-turn navigation, or positioning accuracy unless data and runtime support it.
|
||||
- Spatial feedback: selecting a POI should highlight or frame the location, not only show a toast.
|
||||
- Failure recovery: WebGL/model/network/location failures need fallback text, retry, and a route back to search or home.
|
||||
- Accessibility and comfort: avoid tiny touch targets, hidden controls, excessive motion, unreadable overlays, and ambiguous color-only state.
|
||||
|
||||
3. Test with realistic H5 paths:
|
||||
- Mobile viewport first; include a small-screen pass for overlay collisions and horizontal overflow.
|
||||
- Use real clicks/taps through Browser automation for user-audit conclusions whenever the H5 app can run. Do not substitute route-only checks, source reading, or static screenshots for simulated visitor behavior.
|
||||
- Screenshots are required when checking 3D/map layering, canvas coverage, mobile fit, or visual feedback after a POI/action click.
|
||||
- Test refresh/deep link/browser back for search result, facility detail, route preview, top tab switching, and unavailable route states.
|
||||
|
||||
4. Report user findings with evidence:
|
||||
- Separate browser-verified findings from source-only risks.
|
||||
- If a report could not run browser simulation, state that limitation at the top and avoid calling it a user test report.
|
||||
- Include priority, task, path, observed behavior, expected visitor outcome, impact, and file/line evidence when available.
|
||||
- Mention positive paths that work, especially preserved state, visible back/close controls, and useful fallbacks.
|
||||
- Save requested user-audit reports under `docs/QA/` using names like `3d-guide-user-audit-YYYY-MM-DD.md` or `user-flow-closure-audit-YYYY-MM-DD.md`.
|
||||
|
||||
## Known Risk Radar
|
||||
|
||||
- `src/components/map/ThreeMap.vue` may still directly parse clean package manifest/floor/POI data. Treat this as transitional and flag it in architecture audits.
|
||||
- `src/components/explain/ExplainList.vue`, exhibit detail, and hall detail may still contain hardcoded or historical demo data. Treat these as migration targets for content/explain repositories.
|
||||
- Audio fallback URLs such as `example.com` are not valid explain capability.
|
||||
- `src/pages/route/detail.vue` may still contain historical navigation states such as planning/navigating/arrived/location-error. Keep them blocked unless real graph data exists.
|
||||
- Runtime static JSON loading through `uni.request` must be verified on H5 for current work; verify mp-weixin only when explicitly requested.
|
||||
- Historical demo data remains outside the guide module in areas such as hall, exhibit, and explain pages. Do not mix it back into the guide flow.
|
||||
- Tencent Map SDK behavior and indoor Three.js behavior are separate operational risks; test both after changes that touch shared guide shell UI.
|
||||
- Static asset size and GLB decode time can affect mobile performance. Prefer lazy loading, dispose Three.js resources on unmount, and avoid loading unused models.
|
||||
- Historical demo data remains outside the guide/explain production data boundary unless explicitly migrated.
|
||||
- Tencent Map SDK behavior, indoor Three.js behavior, and audio playback behavior are separate operational risks; test each after changes that touch shared shell UI.
|
||||
- Static asset size, GLB decode time, audio streaming, and image media size can affect mobile performance. Prefer lazy loading, caching, disposal, and unavailable states over loading unused assets.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
interface:
|
||||
display_name: "Shenzhen Natural Museum Dev"
|
||||
short_description: "H5 standards for guide, 导览数据访问层, 3D data audits, and user audits."
|
||||
default_prompt: "Use the Shenzhen Natural Museum frontend-miniapp H5 standards for guide, Three.js/GLB, Tencent Map, nav assets, 导览数据访问层 with data provider/adapter architecture, 三维导览数据审核, browser-simulated user-flow audits, legacy demo data, and H5 validation. Ignore mini-program/mp-weixin unless explicitly requested."
|
||||
short_description: "H5 spatial guide/explain architecture standards."
|
||||
default_prompt: "Use $shenzhen-natural-museum-dev to review or implement Shenzhen Natural Museum mobile H5 spatial content guide architecture, including guide, explain, data repositories, provider/adapter boundaries, browser user flows, and H5 quality gates."
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# H5 部署说明
|
||||
|
||||
最后更新:2026-06-10
|
||||
最后更新:2026-06-12
|
||||
|
||||
## 目标环境
|
||||
|
||||
@@ -29,7 +29,7 @@ pnpm build:h5
|
||||
构建后重点检查:
|
||||
|
||||
```powershell
|
||||
Get-ChildItem -LiteralPath 'dist\build\h5\static\nav-assets\app_nav_assets_v2_clean_20260609_075339' -Recurse -File |
|
||||
Get-ChildItem -LiteralPath 'dist\build\h5\static\nav-assets\app_nav_assets_v2_clean_20260611_093623' -Recurse -File |
|
||||
Measure-Object Length -Sum |
|
||||
Select-Object Count,Sum
|
||||
```
|
||||
@@ -111,9 +111,9 @@ ssh -o BatchMode=yes '自然博物馆-测试服务器' 'docker exec nginx-server
|
||||
公网资源响应:
|
||||
|
||||
```powershell
|
||||
curl.exe -I --max-time 30 https://guide.whaoyue.com/static/nav-assets/app_nav_assets_v2_clean_20260609_075339/app_nav_manifest.json
|
||||
curl.exe -I --max-time 30 https://guide.whaoyue.com/static/nav-assets/app_nav_assets_v2_clean_20260609_075339/models_by_floor/L1.glb
|
||||
curl.exe -I --max-time 30 https://guide.whaoyue.com/static/nav-assets/app_nav_assets_v2_clean_20260609_075339/models_by_floor/MISSING.glb
|
||||
curl.exe -I --max-time 30 https://guide.whaoyue.com/static/nav-assets/app_nav_assets_v2_clean_20260611_093623/app_nav_manifest.json
|
||||
curl.exe -I --max-time 30 https://guide.whaoyue.com/static/nav-assets/app_nav_assets_v2_clean_20260611_093623/models_by_floor/L1.glb
|
||||
curl.exe -I --max-time 30 https://guide.whaoyue.com/static/nav-assets/app_nav_assets_v2_clean_20260611_093623/models_by_floor/MISSING.glb
|
||||
```
|
||||
|
||||
预期结果:
|
||||
@@ -164,7 +164,7 @@ SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON
|
||||
处理:
|
||||
|
||||
1. 确认 `pnpm build:h5` 已执行资源复制脚本。
|
||||
2. 确认服务器存在 `/dmdata/nginx/html/guide/static/nav-assets/app_nav_assets_v2_clean_20260609_075339`。
|
||||
2. 确认服务器存在 `/dmdata/nginx/html/guide/static/nav-assets/app_nav_assets_v2_clean_20260611_093623`。
|
||||
3. 确认 Nginx `/static/nav-assets/` 配置生效。
|
||||
4. 强刷浏览器,避免继续使用旧 JS。
|
||||
|
||||
@@ -202,3 +202,4 @@ docker exec nginx-server nginx -s reload
|
||||
- 2026-06-10:补齐 H5 模型资源打包,新增 `/static/nav-assets/` Nginx 规则,修复模型资源请求返回 HTML 的问题。
|
||||
- 2026-06-10:室内 3D 默认加载当前楼层模型,减少首屏模型解析压力。线上备份:`/dmdata/nginx/html/_backups/guide-before-floor-first-20260610033959.tar.gz`。
|
||||
- 2026-06-10:上传域名根目录校验文件 `gpL0svkeao.txt`,公网验证地址为 `https://guide.whaoyue.com/gpL0svkeao.txt`。
|
||||
- 2026-06-12:按 H5 模式重新打包并部署功能更新,线上资源包为 `app_nav_assets_v2_clean_20260611_093623`。验证通过:`pnpm type-check`、`pnpm build:h5`、Nginx `nginx -t`、manifest 200 JSON、L1.glb 200 `model/gltf-binary`、缺失 GLB 404。线上备份:`/dmdata/nginx/html/_backups/guide-before-deploy-20260612032520.tar.gz`。
|
||||
|
||||
115
docs/QA/user-flow-closure-verification-2026-06-12.md
Normal file
115
docs/QA/user-flow-closure-verification-2026-06-12.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# H5 用户流程闭环验证报告
|
||||
|
||||
日期:2026-06-12
|
||||
项目:深圳自然博物馆智能导览应用 - frontend-miniapp
|
||||
测试地址:`http://192.168.0.112:5173/`
|
||||
运行目标:H5
|
||||
测试方式:人工浏览器测试
|
||||
验证范围:审计报告中P1级别闭环断点的修复状态
|
||||
|
||||
## 验证目标
|
||||
|
||||
根据 `docs/QA/user-flow-closure-audit-2026-06-10.md` 报告,验证以下P1问题的修复状态:
|
||||
|
||||
### P1-1:自定义导航页面缺少页面级返回/取消闭环
|
||||
**修复状态**:✅ 已在代码中实现
|
||||
- `GuidePageFrame.vue` 支持 `show-back` 和 `show-cancel` props
|
||||
- 搜索页、设施详情页、路线详情页均已启用返回按钮
|
||||
- 需人工验证:点击返回是否正确回到上一页
|
||||
|
||||
### P1-2:搜索结果页不能重新输入或重置搜索
|
||||
**修复状态**:✅ 已在代码中实现
|
||||
- `src/pages/search/index.vue:198-234` 实现完整搜索编辑流程
|
||||
- 点击搜索框进入编辑态,显示输入框、清空按钮、搜索按钮
|
||||
- 支持取消恢复原关键词
|
||||
- 需人工验证:编辑-清空-取消-确认搜索完整流程
|
||||
|
||||
### P1-3:路线页"查看目标位置"没有形成下一步
|
||||
**修复状态**:✅ 已在代码中实现
|
||||
- `src/pages/route/detail.vue:446-454` 实现 `handleShowTargetLocation`
|
||||
- 触发 `requestTargetFocus` 聚焦目标POI
|
||||
- 失败时显示 toast 提示
|
||||
- 需人工验证:点击"查看目标位置"是否高亮目标、切换到目标楼层
|
||||
|
||||
### P1-4:设施详情"选择起点"没有起点选择流程
|
||||
**修复状态**:✅ 已在代码中实现
|
||||
- `src/pages/facility/detail.vue:59-110` 实现完整起点选择面板
|
||||
- 支持楼层和区域选择
|
||||
- 支持取消和确认
|
||||
- 确认后写入路线页参数(第252-270行)
|
||||
- 需人工验证:选择起点-取消-重选-确认-查看位置完整流程
|
||||
|
||||
## 人工验证步骤
|
||||
|
||||
### 验证1:搜索页重新搜索流程
|
||||
1. 访问 `http://192.168.0.112:5173/`
|
||||
2. 首页点击"选择目标地点"进入搜索页(默认关键词"卫生间")
|
||||
3. 点击顶部搜索框,应进入编辑态(搜索框高亮、显示确认按钮)
|
||||
4. 清空关键词,输入"展厅"
|
||||
5. 点击"搜索"按钮,结果应更新为展厅相关
|
||||
6. 再次点击搜索框,点击取消,应保持"展厅"结果
|
||||
7. **预期**:搜索框可重新输入、清空、取消、确认搜索
|
||||
|
||||
### 验证2:页面返回保留状态
|
||||
1. 搜索页选择"无障碍"筛选
|
||||
2. 点击任一设施进入详情页
|
||||
3. 点击顶部"返回"按钮
|
||||
4. **预期**:回到搜索页,关键词和"无障碍"筛选保留
|
||||
|
||||
### 验证3:设施起点选择流程
|
||||
1. 设施详情页点击"选择起点"
|
||||
2. 应显示起点选择面板(楼层、区域)
|
||||
3. 选择"2F" + "停车场入口"
|
||||
4. 点击"取消",面板关闭
|
||||
5. 再次点击"选择起点",应恢复上次确认的起点(如果有)
|
||||
6. 重新选择"1F" + "主入口",点击"确认起点"
|
||||
7. 详情页应显示"预览起点:1F 主入口"
|
||||
8. 点击"查看位置"进入路线页
|
||||
9. **预期**:路线页 URL 包含 `startLabel=1F%20主入口&startFloor=1F`
|
||||
|
||||
### 验证4:路线页查看目标位置
|
||||
1. 搜索结果点击"查看"进入路线页(state=preview)
|
||||
2. 位置预览卡片显示目标名称和"查看目标位置"按钮
|
||||
3. 点击"查看目标位置"
|
||||
4. **预期**:
|
||||
- 如果目标有三维坐标,3D 地图应聚焦到目标点
|
||||
- 楼层切换到目标所在楼层
|
||||
- 失败时显示"目标暂无三维位置数据"
|
||||
|
||||
### 验证5:路线页室外预览返回
|
||||
1. 路线页点击"查看室外地图"
|
||||
2. 应切换到室外地图参考(2D模式、入口提示)
|
||||
3. 点击"返回室内3D"或"返回预览"
|
||||
4. **预期**:返回室内3D预览,目标对象不丢失
|
||||
|
||||
### 验证6:顶部Tab全局切换
|
||||
1. 路线页点击顶部"讲解"
|
||||
2. 应跳转到首页讲解Tab(会丢失路线上下文,符合全局导航定义)
|
||||
3. **预期**:用户理解顶部Tab是全局跳转,不是页面返回
|
||||
|
||||
## 浏览器控制台检查项
|
||||
|
||||
- 检查是否有 404 资源错误
|
||||
- 检查是否有 JavaScript 运行时错误
|
||||
- 检查网络请求是否正常(导览数据、楼层数据)
|
||||
- 检查 3D 模型加载状态
|
||||
|
||||
## 已知限制(不属于闭环问题)
|
||||
|
||||
1. **路线能力仍是预览态**:真实路线规划需等待 `route_graph/nav_data` 接入
|
||||
2. **POI 三维坐标覆盖率**:部分设施可能暂无三维位置数据
|
||||
3. **默认搜索词**:搜索页默认"卫生间",但现已支持重新搜索
|
||||
4. **首次加载可能白屏**:开发服务器首次编译延迟,生产构建应无此问题
|
||||
|
||||
## 验证结论
|
||||
|
||||
请在浏览器中按上述步骤执行人工验证,并记录:
|
||||
- ✅ 功能正常
|
||||
- ⚠️ 功能部分可用但体验需优化
|
||||
- ❌ 功能不可用或未按预期工作
|
||||
|
||||
## 下一步
|
||||
|
||||
- 如果所有验证通过,更新 `docs/pdca/tasks.md` 标记 T-008 为 done
|
||||
- 如果发现新问题,记录具体现象和复现步骤
|
||||
- 生产构建后复测首屏加载和资源完整性
|
||||
@@ -16,8 +16,8 @@
|
||||
- 导览模块页面、组件、交互或数据流调整
|
||||
- 室内三维导览、Three.js、GLB/GLTF 模型加载、WebGL 性能问题
|
||||
- 腾讯地图、室外导览、地图 SDK 或地图标记逻辑调整
|
||||
- `static/nav-assets/app_nav_assets_v2_clean_20260609_075339` 资源包相关工作
|
||||
- `src/services/navAssets.ts` 导览数据服务相关工作
|
||||
- `static/nav-assets/app_nav_assets_v2_clean_20260611_093623` 资源包相关工作
|
||||
- `src/data/providers/staticNavAssetsProvider.ts`、`src/repositories/GuideRepository.ts`、`src/repositories/GuideModelRepository.ts` 导览数据访问层相关工作
|
||||
- `src/assets/data` 历史 demo 数据清理、隔离或迁移
|
||||
- `route_graph`、`nav_data`、POI 坐标、路线规划能力判断
|
||||
- H5 适配、移动端遮挡、构建和质量门禁
|
||||
@@ -49,8 +49,8 @@
|
||||
|
||||
## Skill 固化的核心规则
|
||||
|
||||
- 导览业务数据必须优先走 `src/services/navAssets.ts`。
|
||||
- 当前干净导览资源包为 `static/nav-assets/app_nav_assets_v2_clean_20260609_075339`。
|
||||
- 导览业务数据必须优先走 Provider / Adapter / Repository / UseCase 边界,不再新增 `src/services/navAssets.ts` 兼容服务依赖。
|
||||
- 当前干净导览资源包为 `static/nav-assets/app_nav_assets_v2_clean_20260611_093623`。
|
||||
- `src/assets/data` 是历史 demo 数据区域,不应再作为当前导览模块的数据源。
|
||||
- 在 `route_graph` 和 `nav_data` 未准备并验证前,不应声明“真实馆内导航”或启用真实路线规划。
|
||||
- POI 坐标只能作为展示或位置预览候选,不能当作已认证导航锚点。
|
||||
|
||||
@@ -5,5 +5,8 @@
|
||||
| A-001 | 对 guide.whaoyue.com 执行 fresh browser/真实设备室内 3D 冒烟测试 | TBD | 2026-06-11 | todo | C-008 / R-002 |
|
||||
| A-002 | 将用户流程闭环审计拆成可执行修复任务并排期 | TBD | 2026-06-11 | todo | C-007 / R-001 |
|
||||
| A-003 | 确认下一轮发布验收标准和负责人 | TBD | 2026-06-11 | todo | M-005 |
|
||||
| A-004 | 监控模型资源缓存与 404 行为,确认无 `<!DOCTYPE` JSON 解析报错复发 | TBD | 2026-06-11 | todo | R-003 |
|
||||
| A-004 | 监控模型资源缓存与 404 行为,确认无 `<!DOCTYPE` JSON 解析报错复发 | Codex | 2026-06-12 | done | C-013 / R-003 |
|
||||
| A-005 | 梳理当前工作区未提交改动,明确本轮交付边界 | TBD | 2026-06-11 | todo | R-004 |
|
||||
| A-006 | 对 2026-06-12 新部署版本执行人工浏览器冒烟测试 | TBD | 2026-06-13 | todo | C-012 / C-013 |
|
||||
| A-007 | 对讲解列表、讲解详情、暂无音频状态和热搜展示做人工浏览器/真实设备验收 | TBD | 2026-06-13 | todo | C-016 / R-005 / T-012 |
|
||||
| A-008 | 将 16 项未提交/未跟踪改动拆分为本轮交付、文档更新、工具/技能更新和后续保留项 | TBD | 2026-06-13 | todo | R-004 / T-012 |
|
||||
|
||||
@@ -5,10 +5,16 @@
|
||||
| C-001 | H5 构建 | pass | 2026-06-10 | `pnpm build:h5` 通过,复制 `static/nav-assets` | Sass legacy API warning 不阻塞 |
|
||||
| C-002 | 类型检查 | pass | 2026-06-10 | `pnpm type-check` 通过 | |
|
||||
| C-003 | Nginx 配置检查 | pass | 2026-06-10 | `docker exec nginx-server nginx -t` 通过 | 存在 http2 deprecated warning,不影响加载 |
|
||||
| C-004 | manifest 公网响应 | pass | 2026-06-10 | `curl -I https://guide.whaoyue.com/static/nav-assets/app_nav_assets_v2_clean_20260609_075339/app_nav_manifest.json` 返回 200 JSON | |
|
||||
| C-004 | manifest 公网响应 | pass | 2026-06-10 | `curl -I https://guide.whaoyue.com/static/nav-assets/app_nav_assets_v2_clean_20260611_093623/app_nav_manifest.json` 返回 200 JSON | |
|
||||
| C-005 | 楼层 GLB 公网响应 | pass | 2026-06-10 | `curl -I .../models_by_floor/L1.glb` 返回 200 `model/gltf-binary` | |
|
||||
| C-006 | 缺失模型资源处理 | pass | 2026-06-10 | `curl -I .../models_by_floor/MISSING.glb` 返回 404 | 已避免 SPA fallback 返回 `index.html` |
|
||||
| C-007 | H5 用户流程闭环审计 | fail | 2026-06-10 | `docs/QA/user-flow-closure-audit-2026-06-10.md` | 搜索、路线、设施和详情页仍需闭环修复 |
|
||||
| C-008 | in-app browser fresh 3D 复测 | pending | 2026-06-10 | | 浏览器插件初始化异常,需后续复测 |
|
||||
| C-009 | PDCA 报告与面板生成 | pass | 2026-06-10 | `docs/pdca/reports/2026-06-10.md`;`docs/pdca/dashboard.html`;`docs/pdca/dashboard.png` | |
|
||||
| C-010 | 根目录 TXT 校验文件公网访问 | pass | 2026-06-10 | `curl -L https://guide.whaoyue.com/gpL0svkeao.txt` 返回 `b772216640a14171ba5655085c8523be` | 文件位于 `/dmdata/nginx/html/guide/gpL0svkeao.txt` |
|
||||
| C-011 | 2026-06-12 H5 构建与类型检查 | pass | 2026-06-12 | `pnpm type-check` 通过;`pnpm build:h5` 通过 | Sass legacy API warning 不阻塞 |
|
||||
| C-012 | 2026-06-12 线上首页更新 | pass | 2026-06-12 | `curl -L https://guide.whaoyue.com/` 引用 `assets/index-Da0rB3mi.js` | |
|
||||
| C-013 | 2026-06-12 新模型资源公网响应 | pass | 2026-06-12 | `app_nav_assets_v2_clean_20260611_093623/app_nav_manifest.json` 返回 200 JSON;`models_by_floor/L1.glb` 返回 200 `model/gltf-binary`;缺失 GLB 返回 404 | |
|
||||
| C-014 | 2026-06-12 当前工作区质量门复核 | pass | 2026-06-12 | `pnpm type-check`;`pnpm lint`;`pnpm build:h5`;`pnpm build:mp-weixin` | lint 18 个历史 warning;H5/mp-weixin 构建均有 Sass legacy API warning,不阻断 |
|
||||
| C-015 | 2026-06-12 线上 HTTP 资源复核 | pass | 2026-06-12 | `curl -L -I https://guide.whaoyue.com/` 返回 200;manifest 返回 200 `application/json`;`L1.glb` 返回 200 `model/gltf-binary`;缺失 GLB 返回 404 | 线上资源链路仍符合部署预期 |
|
||||
| C-016 | 讲解内容资源包 POI 派生改动验收 | pending | 2026-06-12 | `src/data/providers/staticMuseumContentProvider.ts`;`src/components/explain/ExplainList.vue`;`src/view-models/explainViewModels.ts` | 静态质量门已通过;仍需人工浏览器/真实设备确认讲解列表、详情、暂无音频和热搜展示体验 |
|
||||
|
||||
@@ -182,7 +182,7 @@
|
||||
border-radius: 50%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: conic-gradient(var(--green) 78%, #e4ebe6 0);
|
||||
background: conic-gradient(var(--green) 75%, #e4ebe6 0);
|
||||
}
|
||||
.ring span {
|
||||
width: 78px;
|
||||
@@ -640,9 +640,9 @@
|
||||
<p class="subtitle">从 docs/pdca 生成的专业项目状态、风险和行动闭环面板</p>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<div>生成日期: 2026-06-10</div>
|
||||
<div>生成日期: 2026-06-12</div>
|
||||
<div>项目: E:\MyWork\深圳国际艺术馆\museum-guide\museum-guide-v4.0\frontend-miniapp</div>
|
||||
<div>Git: master@a90f63c · 变更文件数:22</div>
|
||||
<div>Git: master@2055b13 · 变更文件数:19</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -692,7 +692,7 @@
|
||||
<button type="button" class="chip-button" data-quick-filter="missing-owner" aria-pressed="false">无负责人</button>
|
||||
<button type="button" class="chip-button" data-quick-filter="missing-evidence" aria-pressed="false">缺证据</button>
|
||||
<button type="button" class="chip-button" data-quick-filter="uncounted" aria-pressed="false">未计入完成率</button>
|
||||
<span class="task-counter"><strong id="visible-task-count">0</strong>/<span id="total-task-count">9</span> 当前可见</span>
|
||||
<span class="task-counter"><strong id="visible-task-count">0</strong>/<span id="total-task-count">12</span> 当前可见</span>
|
||||
<button type="button" id="reset-filters" class="tool-button">重置</button>
|
||||
<button type="button" id="export-visible" class="tool-button">导出 CSV</button>
|
||||
<button type="button" id="print-dashboard" class="tool-button">打印</button>
|
||||
@@ -703,16 +703,16 @@
|
||||
<section id="overview" class="panel health-card health-red span-4">
|
||||
<div class="health-label">健康状态</div>
|
||||
<strong class="health-value">红色</strong>
|
||||
<p class="health-reason">2 个任务有阻塞/依赖说明(不等同硬阻塞);2 个高影响风险仍开放;1 个决策待确认</p>
|
||||
<p class="health-reason">4 个任务有阻塞/依赖说明(不等同硬阻塞);1 个任务逾期;3 个高影响风险仍开放;2 个决策待确认</p>
|
||||
</section>
|
||||
|
||||
<section class="panel span-4">
|
||||
<h2>任务完成率</h2>
|
||||
<div class="ring-wrap">
|
||||
<div class="ring" aria-label="78%"><span>78%</span></div>
|
||||
<div class="ring" aria-label="75%"><span>75%</span></div>
|
||||
<div>
|
||||
<p>已完成: 7/9</p>
|
||||
<p class="subtitle">进行中/待复核: 0 · 硬阻塞: 0</p>
|
||||
<p>已完成: 9/12</p>
|
||||
<p class="subtitle">进行中/待复核: 1 · 硬阻塞: 0</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -723,13 +723,13 @@
|
||||
|
||||
<div class="metric metric-neutral">
|
||||
<span>任务总览</span>
|
||||
<strong>9</strong>
|
||||
<strong>12</strong>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="metric metric-good">
|
||||
<span>已完成(有证据)</span>
|
||||
<strong>7</strong>
|
||||
<strong>9</strong>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -741,7 +741,7 @@
|
||||
|
||||
<div class="metric metric-info">
|
||||
<span>进行中/待复核</span>
|
||||
<strong>0</strong>
|
||||
<strong>1</strong>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -753,37 +753,37 @@
|
||||
|
||||
<div class="metric metric-warn">
|
||||
<span>阻塞/依赖说明</span>
|
||||
<strong>2</strong>
|
||||
<strong>4</strong>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="metric metric-warn">
|
||||
<span>风险</span>
|
||||
<strong>4</strong>
|
||||
<strong>5</strong>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="metric metric-neutral">
|
||||
<span>检查</span>
|
||||
<strong>通过 8 / 未通过 1</strong>
|
||||
<strong>通过 13 / 未通过 1</strong>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="metric metric-info">
|
||||
<span>行动</span>
|
||||
<strong>5</strong>
|
||||
<strong>7</strong>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="metric metric-info">
|
||||
<span>里程碑</span>
|
||||
<strong>5</strong>
|
||||
<strong>6</strong>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="metric metric-warn">
|
||||
<span>决策待确认</span>
|
||||
<strong>1</strong>
|
||||
<strong>2</strong>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -793,10 +793,10 @@
|
||||
<h2>专业状态维度</h2>
|
||||
<div class="dimension-grid">
|
||||
|
||||
<article class="dimension-card status-yellow">
|
||||
<article class="dimension-card status-red">
|
||||
<span>进度</span>
|
||||
<strong>黄色</strong>
|
||||
<p>开放工作仍有截止日期待确认</p>
|
||||
<strong>红色</strong>
|
||||
<p>存在逾期任务或里程碑</p>
|
||||
</article>
|
||||
|
||||
|
||||
@@ -834,22 +834,38 @@
|
||||
<h2>核心工作包</h2>
|
||||
<div class="workstream-grid">
|
||||
|
||||
<article class="workstream-card workstream-frontend tone-good" role="button" tabindex="0" data-workstream="frontend" aria-label="小程序前端">
|
||||
<article class="workstream-card workstream-frontend tone-warn" role="button" tabindex="0" data-workstream="frontend" aria-label="小程序前端">
|
||||
<div class="workstream-head">
|
||||
<span>小程序前端</span>
|
||||
<strong>100%</strong>
|
||||
</div>
|
||||
<div class="mini-progress"><span style="width:100%;"></span></div>
|
||||
<div class="workstream-meta">
|
||||
<span>已验证完成 6/6</span>
|
||||
<span>已验证完成 8/8</span>
|
||||
<span>阻塞 0</span>
|
||||
<span>关注项 0</span>
|
||||
<span>关注项 1</span>
|
||||
<span>下一到期 待确认</span>
|
||||
<span>分类:关键词推断</span>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
|
||||
<article class="workstream-card workstream-navigation tone-warn" role="button" tabindex="0" data-workstream="navigation" aria-label="三维导览路网">
|
||||
<div class="workstream-head">
|
||||
<span>三维导览路网</span>
|
||||
<strong>0%</strong>
|
||||
</div>
|
||||
<div class="mini-progress"><span style="width:0%;"></span></div>
|
||||
<div class="workstream-meta">
|
||||
<span>已验证完成 0/1</span>
|
||||
<span>阻塞 0</span>
|
||||
<span>关注项 1</span>
|
||||
<span>下一到期 2026-06-13</span>
|
||||
<span>分类:关键词推断</span>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
|
||||
<article class="workstream-card workstream-governance tone-warn" role="button" tabindex="0" data-workstream="governance" aria-label="项目治理">
|
||||
<div class="workstream-head">
|
||||
<span>项目治理</span>
|
||||
@@ -934,6 +950,22 @@
|
||||
</article>
|
||||
|
||||
|
||||
<article class="timeline-row status-done">
|
||||
<div class="timeline-date">2026-06-12</div>
|
||||
<div class="timeline-dot"></div>
|
||||
<div class="timeline-body">
|
||||
<strong>2026-06-12 H5 功能更新部署到测试域名</strong>
|
||||
<div>
|
||||
<span>M-006</span>
|
||||
<span>已完成</span>
|
||||
<span>负责人: Codex</span>
|
||||
<span>截止: 2026-06-12</span>
|
||||
<span>证据: https://guide.whaoyue.com/ 引用 `index-Da0rB3mi.js`;备份 `/dmdata/nginx/html/_backups/guide-before-deploy-20260612032520.tar.gz`</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
|
||||
<article class="timeline-row status-todo">
|
||||
<div class="timeline-date">待确认</div>
|
||||
<div class="timeline-dot"></div>
|
||||
@@ -970,11 +1002,11 @@
|
||||
|
||||
<section class="panel span-7">
|
||||
<h2>任务状态分布</h2>
|
||||
<div class="status-bar"><span class="segment segment-done" style="width:78%;" title="已完成: 7"></span><span class="segment segment-todo" style="width:22%;" title="待办: 2"></span></div>
|
||||
<div class="status-bar"><span class="segment segment-done" style="width:75%;" title="已完成: 9"></span><span class="segment segment-review" style="width:8%;" title="待复核: 1"></span><span class="segment segment-todo" style="width:17%;" title="待办: 2"></span></div>
|
||||
<div class="legend">
|
||||
<span class="done">已完成 7</span>
|
||||
<span class="done">已完成 9</span>
|
||||
<span class="doing">进行中 0</span>
|
||||
<span class="review">待复核 0</span>
|
||||
<span class="review">待复核 1</span>
|
||||
<span class="blocked">阻塞 0</span>
|
||||
<span class="todo">待办 2</span>
|
||||
</div>
|
||||
@@ -990,7 +1022,7 @@
|
||||
</div>
|
||||
<div class="matrix-grid">
|
||||
<div></div><div>低</div><div>中</div><div>高</div>
|
||||
<div class="matrix-axis">高</div><div class="matrix-cell matrix-high-low"></div><div class="matrix-cell matrix-high-medium"><button type="button" class="risk-chip" data-risk-id="R-003" title="模型静态资源缓存 30 天,错误资源上线后恢复慢">R-003</button></div><div class="matrix-cell matrix-high-high"><button type="button" class="risk-chip" data-risk-id="R-001" title="H5 用户流程闭环仍有断点,影响真实到馆任务完成">R-001</button></div><div class="matrix-axis">中</div><div class="matrix-cell matrix-medium-low"></div><div class="matrix-cell matrix-medium-medium"><button type="button" class="risk-chip" data-risk-id="R-002" title="fresh browser 复测未完成,可能漏掉真实设备上的 3D 渲染问题">R-002</button><button type="button" class="risk-chip" data-risk-id="R-004" title="项目仍有多处未提交/未归属改动,发布边界可能不清">R-004</button></div><div class="matrix-cell matrix-medium-high"></div><div class="matrix-axis">低</div><div class="matrix-cell matrix-low-low"></div><div class="matrix-cell matrix-low-medium"></div><div class="matrix-cell matrix-low-high"></div>
|
||||
<div class="matrix-axis">高</div><div class="matrix-cell matrix-high-low"></div><div class="matrix-cell matrix-high-medium"><button type="button" class="risk-chip" data-risk-id="R-003" title="模型静态资源缓存 30 天,错误资源上线后恢复慢">R-003</button><button type="button" class="risk-chip" data-risk-id="R-005" title="讲解内容改为资源包 POI 派生后,若未补正式文案/音频和人工验收,可能造成讲解体验与验收口径不一致">R-005</button></div><div class="matrix-cell matrix-high-high"><button type="button" class="risk-chip" data-risk-id="R-001" title="H5 用户流程闭环仍有断点,影响真实到馆任务完成">R-001</button></div><div class="matrix-axis">中</div><div class="matrix-cell matrix-medium-low"></div><div class="matrix-cell matrix-medium-medium"><button type="button" class="risk-chip" data-risk-id="R-002" title="fresh browser 复测未完成,可能漏掉真实设备上的 3D 渲染问题">R-002</button><button type="button" class="risk-chip" data-risk-id="R-004" title="项目仍有多处未提交/未归属改动,发布边界可能不清">R-004</button></div><div class="matrix-cell matrix-medium-high"></div><div class="matrix-axis">低</div><div class="matrix-cell matrix-low-low"></div><div class="matrix-cell matrix-low-medium"></div><div class="matrix-cell matrix-low-high"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1024,15 +1056,15 @@
|
||||
|
||||
<div class="phase phase-do">
|
||||
<span>执行</span>
|
||||
<strong>6</strong>
|
||||
<small>已完成 5/6</small>
|
||||
<strong>7</strong>
|
||||
<small>已完成 6/7</small>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="phase phase-check">
|
||||
<span>检查</span>
|
||||
<strong>1</strong>
|
||||
<small>已完成 0/1</small>
|
||||
<strong>3</strong>
|
||||
<small>已完成 1/3</small>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1058,46 +1090,46 @@
|
||||
<div>
|
||||
<small>判定依据</small>
|
||||
<ul>
|
||||
<li>1 个质量门禁未通过</li><li>1 个检查待完成</li><li>2 个高影响风险仍开放</li><li>1 个决策待确认</li>
|
||||
<li>1 个质量门禁未通过</li><li>2 个检查待完成</li><li>3 个高影响风险仍开放</li><li>2 个决策待确认</li>
|
||||
</ul>
|
||||
</div>
|
||||
</article>
|
||||
<h3>最近检查</h3>
|
||||
|
||||
<article class="quality-row result-pass">
|
||||
<span class="quality-result">通过</span>
|
||||
<strong>根目录 TXT 校验文件公网访问</strong>
|
||||
<small>2026-06-10 · C-010</small>
|
||||
</article>
|
||||
|
||||
<article class="quality-row result-pass">
|
||||
<span class="quality-result">通过</span>
|
||||
<strong>PDCA 报告与面板生成</strong>
|
||||
<small>2026-06-10 · C-009</small>
|
||||
</article>
|
||||
|
||||
<article class="quality-row result-pending">
|
||||
<span class="quality-result">待检查</span>
|
||||
<strong>in-app browser fresh 3D 复测</strong>
|
||||
<small>2026-06-10 · C-008</small>
|
||||
</article>
|
||||
|
||||
<article class="quality-row result-fail">
|
||||
<span class="quality-result">未通过</span>
|
||||
<strong>H5 用户流程闭环审计</strong>
|
||||
<small>2026-06-10 · C-007</small>
|
||||
<strong>讲解内容资源包 POI 派生改动验收</strong>
|
||||
<small>2026-06-12 · C-016</small>
|
||||
</article>
|
||||
|
||||
<article class="quality-row result-pass">
|
||||
<span class="quality-result">通过</span>
|
||||
<strong>缺失模型资源处理</strong>
|
||||
<small>2026-06-10 · C-006</small>
|
||||
<strong>2026-06-12 线上 HTTP 资源复核</strong>
|
||||
<small>2026-06-12 · C-015</small>
|
||||
</article>
|
||||
|
||||
<article class="quality-row result-pass">
|
||||
<span class="quality-result">通过</span>
|
||||
<strong>楼层 GLB 公网响应</strong>
|
||||
<small>2026-06-10 · C-005</small>
|
||||
<strong>2026-06-12 当前工作区质量门复核</strong>
|
||||
<small>2026-06-12 · C-014</small>
|
||||
</article>
|
||||
|
||||
<article class="quality-row result-pass">
|
||||
<span class="quality-result">通过</span>
|
||||
<strong>2026-06-12 新模型资源公网响应</strong>
|
||||
<small>2026-06-12 · C-013</small>
|
||||
</article>
|
||||
|
||||
<article class="quality-row result-pass">
|
||||
<span class="quality-result">通过</span>
|
||||
<strong>2026-06-12 线上首页更新</strong>
|
||||
<small>2026-06-12 · C-012</small>
|
||||
</article>
|
||||
|
||||
<article class="quality-row result-pass">
|
||||
<span class="quality-result">通过</span>
|
||||
<strong>2026-06-12 H5 构建与类型检查</strong>
|
||||
<small>2026-06-12 · C-011</small>
|
||||
</article>
|
||||
|
||||
</div>
|
||||
@@ -1107,7 +1139,12 @@
|
||||
<section class="panel span-6">
|
||||
<h2>阻塞与逾期</h2>
|
||||
<div class="item-list">
|
||||
<p class="empty">暂无阻塞或逾期任务。</p>
|
||||
|
||||
<article class="item-row">
|
||||
<strong>浏览器 fresh load 复测室内 3D</strong>
|
||||
<div><span>编号: T-007</span><span>状态: 待办</span><span>负责人: 待确认</span><span>截止日期: 2026-06-11</span><span>阻塞项: 当前 in-app browser 调试通道初始化异常,需人工或恢复工具后复测</span></div>
|
||||
</article>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1267,12 +1304,12 @@
|
||||
<td><span class="counted-yes">是</span></td>
|
||||
</tr>
|
||||
|
||||
<tr class="task-row"
|
||||
<tr class="task-row row-due-overdue"
|
||||
data-status="todo"
|
||||
data-owner="TBD"
|
||||
data-owner-missing="true"
|
||||
data-due="2026-06-11"
|
||||
data-due-state="future"
|
||||
data-due-state="overdue"
|
||||
data-phase="check"
|
||||
data-workstream="governance"
|
||||
data-hard-blocked="false"
|
||||
@@ -1339,6 +1376,78 @@
|
||||
<td><span class="counted-yes">是</span></td>
|
||||
</tr>
|
||||
|
||||
<tr class="task-row"
|
||||
data-status="done"
|
||||
data-owner="Codex"
|
||||
data-owner-missing="false"
|
||||
data-due="2026-06-12"
|
||||
data-due-state="future"
|
||||
data-phase="do"
|
||||
data-workstream="frontend"
|
||||
data-hard-blocked="false"
|
||||
data-missing-evidence="false"
|
||||
data-counted="true"
|
||||
data-search="t-010 部署 2026-06-12 h5 功能更新 do done codex 2026-06-12 `pnpm type-check`;`pnpm build:h5`;线上备份 `/dmdata/nginx/html/_backups/guide-before-deploy-20260612032520.tar.gz`;首页引用 `index-da0rb3mi.js`">
|
||||
<td class="task-id">T-010</td>
|
||||
<td class="task-title">部署 2026-06-12 H5 功能更新</td>
|
||||
<td>执行</td>
|
||||
<td><span class="status-pill status-done">已完成</span></td>
|
||||
<td>Codex</td>
|
||||
<td>2026-06-12</td>
|
||||
<td><button type="button" class="link-button workstream-filter" data-workstream-filter="frontend">小程序前端</button></td>
|
||||
<td class="task-evidence">`pnpm type-check`;`pnpm build:h5`;线上备份 `/dmdata/nginx/html/_backups/guide-before-deploy-20260612032520.tar.gz`;首页引用 `index-Da0rB3mi.js`</td>
|
||||
<td>待确认</td>
|
||||
<td><span class="counted-yes">是</span></td>
|
||||
</tr>
|
||||
|
||||
<tr class="task-row"
|
||||
data-status="done"
|
||||
data-owner="Codex"
|
||||
data-owner-missing="false"
|
||||
data-due="2026-06-12"
|
||||
data-due-state="future"
|
||||
data-phase="check"
|
||||
data-workstream="frontend"
|
||||
data-hard-blocked="false"
|
||||
data-missing-evidence="false"
|
||||
data-counted="true"
|
||||
data-search="t-011 复核 2026-06-12 当前工作区质量门 check done codex 2026-06-12 `pnpm type-check` 通过;`pnpm lint` 通过且仅有 18 个 warning;`pnpm build:h5` 通过并复制 `static/nav-assets`;`pnpm build:mp-weixin` 通过 sass legacy api warning 不阻塞;小程序发布仍不在本轮 h5 部署范围">
|
||||
<td class="task-id">T-011</td>
|
||||
<td class="task-title">复核 2026-06-12 当前工作区质量门</td>
|
||||
<td>检查</td>
|
||||
<td><span class="status-pill status-done">已完成</span></td>
|
||||
<td>Codex</td>
|
||||
<td>2026-06-12</td>
|
||||
<td><button type="button" class="link-button workstream-filter" data-workstream-filter="frontend">小程序前端</button></td>
|
||||
<td class="task-evidence">`pnpm type-check` 通过;`pnpm lint` 通过且仅有 18 个 warning;`pnpm build:h5` 通过并复制 `static/nav-assets`;`pnpm build:mp-weixin` 通过</td>
|
||||
<td>Sass legacy API warning 不阻塞;小程序发布仍不在本轮 H5 部署范围</td>
|
||||
<td><span class="counted-yes">是</span></td>
|
||||
</tr>
|
||||
|
||||
<tr class="task-row"
|
||||
data-status="review"
|
||||
data-owner="TBD"
|
||||
data-owner-missing="true"
|
||||
data-due="2026-06-13"
|
||||
data-due-state="future"
|
||||
data-phase="check"
|
||||
data-workstream="navigation"
|
||||
data-hard-blocked="false"
|
||||
data-missing-evidence="false"
|
||||
data-counted="false"
|
||||
data-search="t-012 梳理讲解内容数据源改动和交付边界 check review tbd 2026-06-13 `src/data/providers/staticmuseumcontentprovider.ts` 从导览资源包 poi 派生展厅/展项;`src/components/explain/explainlist.vue` 显示暂无音频;`src/view-models/explainviewmodels.ts` 透出 tags 需人工浏览器/真实设备确认讲解列表、详情和搜索展示是否符合下一轮交付口径">
|
||||
<td class="task-id">T-012</td>
|
||||
<td class="task-title">梳理讲解内容数据源改动和交付边界</td>
|
||||
<td>检查</td>
|
||||
<td><span class="status-pill status-review">待复核</span></td>
|
||||
<td>待确认</td>
|
||||
<td>2026-06-13</td>
|
||||
<td><button type="button" class="link-button workstream-filter" data-workstream-filter="navigation">三维导览路网</button></td>
|
||||
<td class="task-evidence">`src/data/providers/staticMuseumContentProvider.ts` 从导览资源包 POI 派生展厅/展项;`src/components/explain/ExplainList.vue` 显示暂无音频;`src/view-models/explainViewModels.ts` 透出 tags</td>
|
||||
<td>需人工浏览器/真实设备确认讲解列表、详情和搜索展示是否符合下一轮交付口径</td>
|
||||
<td><span class="counted-no">未完成</span></td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -1360,6 +1469,12 @@
|
||||
<div><span>编号: R-003</span><span>状态: 缓解中</span><span>缓解措施: 模型包路径带版本号;缺失资源返回 404;部署时使用 URL 参数强刷前端包</span><span>负责人: Codex</span></div>
|
||||
</article>
|
||||
|
||||
|
||||
<article class="item-row">
|
||||
<strong>讲解内容改为资源包 POI 派生后,若未补正式文案/音频和人工验收,可能造成讲解体验与验收口径不一致</strong>
|
||||
<div><span>编号: R-005</span><span>状态: 开放</span><span>缓解措施: 保留“暂无音频”显性状态;将资源包 POI 派生内容作为过渡方案复核;进入发布前补人工浏览器/真实设备验收和正式内容接入计划</span><span>负责人: 待确认</span></div>
|
||||
</article>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1386,14 +1501,14 @@
|
||||
|
||||
|
||||
<article class="item-row">
|
||||
<strong>监控模型资源缓存与 404 行为,确认无 `<!DOCTYPE` JSON 解析报错复发</strong>
|
||||
<div><span>编号: A-004</span><span>负责人: 待确认</span><span>截止日期: 2026-06-11</span><span>来源: R-003</span></div>
|
||||
<strong>梳理当前工作区未提交改动,明确本轮交付边界</strong>
|
||||
<div><span>编号: A-005</span><span>负责人: 待确认</span><span>截止日期: 2026-06-11</span><span>来源: R-004</span></div>
|
||||
</article>
|
||||
|
||||
|
||||
<article class="item-row">
|
||||
<strong>梳理当前工作区未提交改动,明确本轮交付边界</strong>
|
||||
<div><span>编号: A-005</span><span>负责人: 待确认</span><span>截止日期: 2026-06-11</span><span>来源: R-004</span></div>
|
||||
<strong>对 2026-06-12 新部署版本执行人工浏览器冒烟测试</strong>
|
||||
<div><span>编号: A-006</span><span>负责人: 待确认</span><span>截止日期: 2026-06-13</span><span>来源: C-012 / C-013</span></div>
|
||||
</article>
|
||||
|
||||
</div>
|
||||
@@ -1408,6 +1523,12 @@
|
||||
<div><span>编号: D-004</span><span>负责人: 待确认</span><span>需要日期: 2026-06-11</span><span>建议: 建议纳入准入</span></div>
|
||||
</article>
|
||||
|
||||
|
||||
<article class="item-row">
|
||||
<strong>是否接受“从导览资源包 POI 派生讲解展厅/展项”作为正式内容接入前的过渡方案</strong>
|
||||
<div><span>编号: D-005</span><span>负责人: 待确认</span><span>需要日期: 2026-06-13</span><span>建议: 建议仅作为过渡展示方案纳入,发布前必须明确“暂无音频”和正式内容接入计划</span></div>
|
||||
</article>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1421,7 +1542,7 @@
|
||||
|
||||
<footer>由项目内 PDCA 进度工具生成 · 来源:项目管理目录</footer>
|
||||
</div>
|
||||
<script id="dashboard-data" type="application/json">{"language": "zh", "date": "2026-06-10", "risks": [{"id": "R-001", "risk": "H5 用户流程闭环仍有断点,影响真实到馆任务完成", "probability": "高", "impact": "高", "status": "开放", "mitigation": "按 `docs/QA/user-flow-closure-audit-2026-06-10.md` 拆分搜索、详情、路线、设施选择、返回/取消/重试任务", "owner": "待确认"}, {"id": "R-002", "risk": "fresh browser 复测未完成,可能漏掉真实设备上的 3D 渲染问题", "probability": "中", "impact": "中", "status": "观察中", "mitigation": "使用浏览器强刷、移动端视口和真实设备补测室内 3D;保留 HTTP 资源验证作为最低证据", "owner": "待确认"}, {"id": "R-003", "risk": "模型静态资源缓存 30 天,错误资源上线后恢复慢", "probability": "中", "impact": "高", "status": "缓解中", "mitigation": "模型包路径带版本号;缺失资源返回 404;部署时使用 URL 参数强刷前端包", "owner": "Codex"}, {"id": "R-004", "risk": "项目仍有多处未提交/未归属改动,发布边界可能不清", "probability": "中", "impact": "中", "status": "观察中", "mitigation": "发布前整理变更清单,确认哪些进入本轮交付,哪些留作后续", "owner": "待确认"}]}</script>
|
||||
<script id="dashboard-data" type="application/json">{"language": "zh", "date": "2026-06-12", "risks": [{"id": "R-001", "risk": "H5 用户流程闭环仍有断点,影响真实到馆任务完成", "probability": "高", "impact": "高", "status": "开放", "mitigation": "按 `docs/QA/user-flow-closure-audit-2026-06-10.md` 拆分搜索、详情、路线、设施选择、返回/取消/重试任务", "owner": "待确认"}, {"id": "R-002", "risk": "fresh browser 复测未完成,可能漏掉真实设备上的 3D 渲染问题", "probability": "中", "impact": "中", "status": "观察中", "mitigation": "使用浏览器强刷、移动端视口和真实设备补测室内 3D;保留 HTTP 资源验证作为最低证据", "owner": "待确认"}, {"id": "R-003", "risk": "模型静态资源缓存 30 天,错误资源上线后恢复慢", "probability": "中", "impact": "高", "status": "缓解中", "mitigation": "模型包路径带版本号;缺失资源返回 404;部署时使用 URL 参数强刷前端包", "owner": "Codex"}, {"id": "R-004", "risk": "项目仍有多处未提交/未归属改动,发布边界可能不清", "probability": "中", "impact": "中", "status": "观察中", "mitigation": "当前 `git status --short` 有 16 项变更/未跟踪记录;发布前整理变更清单,确认哪些进入本轮交付,哪些留作后续", "owner": "待确认"}, {"id": "R-005", "risk": "讲解内容改为资源包 POI 派生后,若未补正式文案/音频和人工验收,可能造成讲解体验与验收口径不一致", "probability": "中", "impact": "高", "status": "开放", "mitigation": "保留“暂无音频”显性状态;将资源包 POI 派生内容作为过渡方案复核;进入发布前补人工浏览器/真实设备验收和正式内容接入计划", "owner": "待确认"}]}</script>
|
||||
<script>
|
||||
|
||||
(() => {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 200 KiB After Width: | Height: | Size: 203 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
@@ -6,3 +6,4 @@
|
||||
| D-002 | 2026-06-10 | 导览模型资源通过 `/static/nav-assets/` 提供,并由 Nginx 对缺失资源返回 404 | accepted | Codex | | 保持专用 location | 避免资源缺失时返回 `index.html` 导致 JSON/GLB 解析错误 |
|
||||
| D-003 | 2026-06-10 | 室内 3D 首屏默认加载当前楼层模型 | accepted | Codex | | 保持 `initial-view="floor"` | 降低首屏模型解析压力,导览页已有楼层切换上下文 |
|
||||
| D-004 | 2026-06-10 | 是否以用户流程闭环作为下一轮发布准入 | needed | TBD | 2026-06-11 | 建议纳入准入 | 当前 QA 审计显示关键任务闭环仍未完成 |
|
||||
| D-005 | 2026-06-12 | 是否接受“从导览资源包 POI 派生讲解展厅/展项”作为正式内容接入前的过渡方案 | needed | TBD | 2026-06-13 | 建议仅作为过渡展示方案纳入,发布前必须明确“暂无音频”和正式内容接入计划 | 当前改动已把讲解内容源从固定过渡数组调整为资源包 POI 派生,但正式展陈文案、图片和音频仍未接入 |
|
||||
|
||||
@@ -7,3 +7,4 @@
|
||||
| M-003 | 部署说明与回滚流程沉淀 | done | Codex | 2026-06-10 | `docs/H5_DEPLOYMENT_GUIDE.md` | Now |
|
||||
| M-004 | H5 关键用户流程闭环 | todo | TBD | TBD | `docs/QA/user-flow-closure-audit-2026-06-10.md` | Next |
|
||||
| M-005 | 发布验收与交付确认 | todo | TBD | TBD | | Later |
|
||||
| M-006 | 2026-06-12 H5 功能更新部署到测试域名 | done | Codex | 2026-06-12 | https://guide.whaoyue.com/ 引用 `index-Da0rB3mi.js`;备份 `/dmdata/nginx/html/_backups/guide-before-deploy-20260612032520.tar.gz` | Now |
|
||||
|
||||
@@ -31,4 +31,7 @@
|
||||
## 当前判断
|
||||
|
||||
- 部署与模型请求修复已完成。
|
||||
- 2026-06-12 当前工作区复核:`pnpm type-check`、`pnpm lint`、`pnpm build:h5`、`pnpm build:mp-weixin` 均通过;lint 仍有 18 个历史 warning,构建仍有 Sass legacy API warning,均不阻断。
|
||||
- 2026-06-12 线上 HTTP 复核:`https://guide.whaoyue.com/` 返回 200,manifest 返回 200 JSON,`L1.glb` 返回 200 `model/gltf-binary`,缺失 GLB 返回 404。
|
||||
- 当前未提交改动包含讲解内容数据源改为从导览资源包 POI 派生展厅/展项,以及讲解列表显示“暂无音频”;该方向已通过静态质量门,但仍需人工浏览器/真实设备验收和交付边界确认。
|
||||
- 项目整体仍有用户流程闭环风险,需在下一轮集中处理搜索、详情、路线、设施选择、返回/取消/重试等关键链路。
|
||||
|
||||
89
docs/pdca/reports/2026-06-12.md
Normal file
89
docs/pdca/reports/2026-06-12.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# PDCA 项目进度报告 - 2026-06-12
|
||||
|
||||
## 健康状态
|
||||
|
||||
- 状态:红色
|
||||
- 原因:1 个任务逾期;3 个高影响风险仍开放;2 个决策待确认
|
||||
- 项目:`E:\MyWork\深圳国际艺术馆\museum-guide\museum-guide-v4.0\frontend-miniapp`
|
||||
- Git:master@2055b13,变更文件数:19
|
||||
|
||||
## 专业状态维度
|
||||
|
||||
| 维度 | 状态 | 原因 |
|
||||
| --- | --- | --- |
|
||||
| 进度 | 红色 | 存在逾期任务或里程碑 |
|
||||
| 范围 | 黄色 | 目标或里程碑仍待确认 |
|
||||
| 风险 | 红色 | 高影响风险仍开放 |
|
||||
| 证据 | 红色 | 存在未通过的验收检查 |
|
||||
| 决策 | 黄色 | 需要干系人确认决策 |
|
||||
|
||||
## 计划
|
||||
|
||||
- 查看 `docs/pdca/plan.md` 获取目标、里程碑、范围与成功指标。
|
||||
- 如果目标、里程碑、负责人或截止日期仍待确认,项目基线尚未完整。
|
||||
|
||||
### 里程碑
|
||||
|
||||
- M-001 | H5 测试服务器可访问 | 已完成 | Codex | 2026-06-10
|
||||
- M-002 | 室内 3D 模型资源请求修复 | 已完成 | Codex | 2026-06-10
|
||||
- M-003 | 部署说明与回滚流程沉淀 | 已完成 | Codex | 2026-06-10
|
||||
- M-004 | H5 关键用户流程闭环 | 待办 | 待确认 | 待确认
|
||||
- M-005 | 发布验收与交付确认 | 待办 | 待确认 | 待确认
|
||||
- M-006 | 2026-06-12 H5 功能更新部署到测试域名 | 已完成 | Codex | 2026-06-12
|
||||
|
||||
## 执行
|
||||
|
||||
- 任务统计:已完成: 9, 待复核: 1, 待办: 2
|
||||
- 完成率口径:仅统计 `status=done` 且 `evidence` 非空的任务。
|
||||
- 已完成并有证据:9/12
|
||||
- 标记完成但缺少证据:0
|
||||
- T-001 | 配置 H5 模型资源打包 | 已完成 | Codex | 2026-06-10
|
||||
- T-002 | 修复 Nginx 模型静态资源规则 | 已完成 | Codex | 2026-06-10
|
||||
- T-003 | 部署 H5 到 guide.whaoyue.com | 已完成 | Codex | 2026-06-10
|
||||
- T-004 | 优化室内 3D 首屏加载策略 | 已完成 | Codex | 2026-06-10
|
||||
- T-005 | 更新 H5 部署文档 | 已完成 | Codex | 2026-06-10
|
||||
- T-006 | 生成并维护 PDCA 项目进度 | 已完成 | Codex | 2026-06-10
|
||||
- T-007 | 浏览器 fresh load 复测室内 3D | 待办 | 待确认 | 2026-06-11
|
||||
- T-008 | 修复 H5 用户流程闭环 P0/P1 问题 | 待办 | 待确认 | 待确认
|
||||
- T-009 | 上传域名根目录校验文件 | 已完成 | Codex | 2026-06-10
|
||||
- T-010 | 部署 2026-06-12 H5 功能更新 | 已完成 | Codex | 2026-06-12
|
||||
|
||||
## 检查
|
||||
|
||||
### 阻塞
|
||||
|
||||
- 暂无阻塞记录。
|
||||
|
||||
### 逾期任务
|
||||
|
||||
- T-007 | 浏览器 fresh load 复测室内 3D | 待确认 | 2026-06-11
|
||||
|
||||
### 逾期里程碑
|
||||
|
||||
- 未发现逾期里程碑。
|
||||
|
||||
### 缺少证据
|
||||
|
||||
- 完成/复核状态任务均有证据或暂无此类任务。
|
||||
|
||||
### 高影响开放风险
|
||||
|
||||
- R-001 | H5 用户流程闭环仍有断点,影响真实到馆任务完成 | 开放 | 按 `docs/QA/user-flow-closure-audit-2026-06-10.md` 拆分搜索、详情、路线、设施选择、返回/取消/重试任务 | 待确认
|
||||
- R-003 | 模型静态资源缓存 30 天,错误资源上线后恢复慢 | 缓解中 | 模型包路径带版本号;缺失资源返回 404;部署时使用 URL 参数强刷前端包 | Codex
|
||||
- R-005 | 讲解内容改为资源包 POI 派生后,若未补正式文案/音频和人工验收,可能造成讲解体验与验收口径不一致 | 开放 | 保留“暂无音频”显性状态;将资源包 POI 派生内容作为过渡方案复核;进入发布前补人工浏览器/真实设备验收和正式内容接入计划 | 待确认
|
||||
|
||||
## 行动
|
||||
|
||||
- A-001 | 对 guide.whaoyue.com 执行 fresh browser/真实设备室内 3D 冒烟测试 | 待确认 | 2026-06-11 | C-008 / R-002
|
||||
- A-002 | 将用户流程闭环审计拆成可执行修复任务并排期 | 待确认 | 2026-06-11 | C-007 / R-001
|
||||
- A-003 | 确认下一轮发布验收标准和负责人 | 待确认 | 2026-06-11 | M-005
|
||||
- A-005 | 梳理当前工作区未提交改动,明确本轮交付边界 | 待确认 | 2026-06-11 | R-004
|
||||
- A-006 | 对 2026-06-12 新部署版本执行人工浏览器冒烟测试 | 待确认 | 2026-06-13 | C-012 / C-013
|
||||
- A-007 | 对讲解列表、讲解详情、暂无音频状态和热搜展示做人工浏览器/真实设备验收 | 待确认 | 2026-06-13 | C-016 / R-005 / T-012
|
||||
- A-008 | 将 16 项未提交/未跟踪改动拆分为本轮交付、文档更新、工具/技能更新和后续保留项 | 待确认 | 2026-06-13 | R-004 / T-012
|
||||
|
||||
### 决策待确认
|
||||
|
||||
- D-004 | 是否以用户流程闭环作为下一轮发布准入 | 待确认 | 2026-06-11 | 建议纳入准入
|
||||
- D-005 | 是否接受“从导览资源包 POI 派生讲解展厅/展项”作为正式内容接入前的过渡方案 | 待确认 | 2026-06-13 | 建议仅作为过渡展示方案纳入,发布前必须明确“暂无音频”和正式内容接入计划
|
||||
|
||||
@@ -5,4 +5,5 @@
|
||||
| R-001 | H5 用户流程闭环仍有断点,影响真实到馆任务完成 | high | high | open | 按 `docs/QA/user-flow-closure-audit-2026-06-10.md` 拆分搜索、详情、路线、设施选择、返回/取消/重试任务 | TBD |
|
||||
| R-002 | fresh browser 复测未完成,可能漏掉真实设备上的 3D 渲染问题 | medium | medium | watch | 使用浏览器强刷、移动端视口和真实设备补测室内 3D;保留 HTTP 资源验证作为最低证据 | TBD |
|
||||
| R-003 | 模型静态资源缓存 30 天,错误资源上线后恢复慢 | medium | high | mitigating | 模型包路径带版本号;缺失资源返回 404;部署时使用 URL 参数强刷前端包 | Codex |
|
||||
| R-004 | 项目仍有多处未提交/未归属改动,发布边界可能不清 | medium | medium | watch | 发布前整理变更清单,确认哪些进入本轮交付,哪些留作后续 | TBD |
|
||||
| R-004 | 项目仍有多处未提交/未归属改动,发布边界可能不清 | medium | medium | watch | 当前 `git status --short` 有 16 项变更/未跟踪记录;发布前整理变更清单,确认哪些进入本轮交付,哪些留作后续 | TBD |
|
||||
| R-005 | 讲解内容改为资源包 POI 派生后,若未补正式文案/音频和人工验收,可能造成讲解体验与验收口径不一致 | medium | high | open | 保留“暂无音频”显性状态;将资源包 POI 派生内容作为过渡方案复核;进入发布前补人工浏览器/真实设备验收和正式内容接入计划 | TBD |
|
||||
|
||||
@@ -11,3 +11,6 @@
|
||||
| T-007 | 浏览器 fresh load 复测室内 3D | check | todo | TBD | 2026-06-11 | | 当前 in-app browser 调试通道初始化异常,需人工或恢复工具后复测 |
|
||||
| T-008 | 修复 H5 用户流程闭环 P0/P1 问题 | do | todo | TBD | TBD | `docs/QA/user-flow-closure-audit-2026-06-10.md` | 需确认优先级、负责人和截止日期 |
|
||||
| T-009 | 上传域名根目录校验文件 | do | done | Codex | 2026-06-10 | `https://guide.whaoyue.com/gpL0svkeao.txt` 返回 `b772216640a14171ba5655085c8523be` | |
|
||||
| T-010 | 部署 2026-06-12 H5 功能更新 | do | done | Codex | 2026-06-12 | `pnpm type-check`;`pnpm build:h5`;线上备份 `/dmdata/nginx/html/_backups/guide-before-deploy-20260612032520.tar.gz`;首页引用 `index-Da0rB3mi.js` | |
|
||||
| T-011 | 复核 2026-06-12 当前工作区质量门 | check | done | Codex | 2026-06-12 | `pnpm type-check` 通过;`pnpm lint` 通过且仅有 18 个 warning;`pnpm build:h5` 通过并复制 `static/nav-assets`;`pnpm build:mp-weixin` 通过 | Sass legacy API warning 不阻塞;小程序发布仍不在本轮 H5 部署范围 |
|
||||
| T-012 | 梳理讲解内容数据源改动和交付边界 | check | review | TBD | 2026-06-13 | `src/data/providers/staticMuseumContentProvider.ts` 从导览资源包 POI 派生展厅/展项;`src/components/explain/ExplainList.vue` 显示暂无音频;`src/view-models/explainViewModels.ts` 透出 tags | 需人工浏览器/真实设备确认讲解列表、详情和搜索展示是否符合下一轮交付口径 |
|
||||
|
||||
544
docs/室内导览交互优化改进计划.md
Normal file
544
docs/室内导览交互优化改进计划.md
Normal file
@@ -0,0 +1,544 @@
|
||||
# 室内导览交互优化改进计划
|
||||
|
||||
## 文档信息
|
||||
|
||||
- **项目**:深圳自然博物馆智能导览应用
|
||||
- **模块**:室内 3D 导览交互
|
||||
- **创建时间**:2026-06-12
|
||||
- **状态**:待评审
|
||||
|
||||
---
|
||||
|
||||
## 背景
|
||||
|
||||
当前室内导览采用**显式"全馆/楼层"手动切换**模式,用户需要点击按钮在"建筑外观"和"楼层内部"之间切换。这个模式功能完整,但与基于缩放距离的自动切换模式相比,交互直觉性较弱。
|
||||
|
||||
**现有模式**:
|
||||
- 进入室内 3D → 加载建筑外观
|
||||
- 用户手动点击"楼层"按钮 → 楼层控件出现 → 选择楼层 → 加载楼层内部
|
||||
- 用户缩放只改变视角距离,不触发模型切换
|
||||
|
||||
**目标模式**:
|
||||
- 进入室内 3D → 加载建筑外观
|
||||
- 用户放大到一定距离 → **自动切换**到楼层内部
|
||||
- 用户缩小到一定距离 → **自动切换**回建筑外观
|
||||
- 保留手动控制入口作为兜底
|
||||
|
||||
---
|
||||
|
||||
## 改进目标
|
||||
|
||||
1. **增加基于缩放距离的自动视角切换**,提升交互直觉性。
|
||||
2. **保留手动切换入口**,避免误触和性能抖动。
|
||||
3. **优化楼层控件可见性**,缩短操作路径。
|
||||
4. **为未来多层展示预留架构空间**(不在本次实施)。
|
||||
|
||||
---
|
||||
|
||||
## 改进任务分解
|
||||
|
||||
### Phase 1:基础自动切换能力(核心)
|
||||
|
||||
**目标**:实现基于缩放距离的建筑外观 ↔ 楼层内部自动切换。
|
||||
|
||||
#### Task 1.1:在 ThreeMap 组件实现距离监听与自动切换逻辑
|
||||
|
||||
**优先级**:P0
|
||||
**预估工作量**:2-3 天
|
||||
**风险**:中等(需要调优阈值和滞回参数)
|
||||
|
||||
**实现要点**:
|
||||
|
||||
1. 监听 `OrbitControls` 的 `change` 事件,而不是每帧检查:
|
||||
```typescript
|
||||
controls.addEventListener('change', () => {
|
||||
throttledCheckAutoSwitch()
|
||||
})
|
||||
```
|
||||
|
||||
2. 实现双阈值滞回机制,避免反复切换:
|
||||
```typescript
|
||||
const autoSwitchConfig = {
|
||||
thresholdLowFactor: 1.0, // 放大到建筑尺寸 1.0 倍时切换到楼层
|
||||
thresholdHighFactor: 1.3, // 缩小到建筑尺寸 1.3 倍时切换回全馆
|
||||
cooldownMs: 2000, // 切换后冷却 2 秒
|
||||
enableAuto: true // 可配置开关
|
||||
}
|
||||
```
|
||||
|
||||
3. 增加加载锁和冷却时间:
|
||||
```typescript
|
||||
let isAutoSwitchLocked = false
|
||||
let lastAutoSwitchTime = 0
|
||||
|
||||
const checkAutoSwitch = () => {
|
||||
if (!autoSwitchConfig.enableAuto) return
|
||||
if (isAutoSwitchLocked) return
|
||||
|
||||
const now = Date.now()
|
||||
if (now - lastAutoSwitchTime < autoSwitchConfig.cooldownMs) return
|
||||
|
||||
const distance = controls.getDistance()
|
||||
// ... 判断阈值并切换
|
||||
}
|
||||
```
|
||||
|
||||
4. 切换时触发事件通知上层:
|
||||
```typescript
|
||||
emit('auto-switch', {
|
||||
from: 'overview',
|
||||
to: 'floor',
|
||||
trigger: 'zoom-in',
|
||||
distance: number
|
||||
})
|
||||
```
|
||||
|
||||
**验证标准**:
|
||||
- 在建筑外观缩放到近距离,自动切换到默认楼层(L1)。
|
||||
- 在楼层内部缩放到远距离,自动切换回建筑外观。
|
||||
- 快速缩放不会触发多次加载。
|
||||
- 切换过程有 loading 状态提示。
|
||||
|
||||
**文件修改**:
|
||||
- `src/components/map/ThreeMap.vue`:增加自动切换逻辑
|
||||
- `src/domain/guideModel.ts`:增加自动切换配置类型定义
|
||||
|
||||
---
|
||||
|
||||
#### Task 1.2:调整首页和 GuideMapShell 配合自动切换
|
||||
|
||||
**优先级**:P0
|
||||
**预估工作量**:1 天
|
||||
**依赖**:Task 1.1
|
||||
|
||||
**实现要点**:
|
||||
|
||||
1. 监听 `ThreeMap` 的 `auto-switch` 事件,同步 `indoorView` 状态:
|
||||
```typescript
|
||||
const handleAutoSwitch = (event: { from: string; to: string }) => {
|
||||
if (event.to === 'floor') {
|
||||
indoorView.value = 'floor'
|
||||
} else {
|
||||
indoorView.value = 'overview'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. 更新状态标签文案:
|
||||
- 自动切换时显示"室内楼层(自动)"或"室内全馆(自动)"
|
||||
- 手动切换时显示"室内楼层"或"室内全馆"
|
||||
|
||||
3. 楼层控件可见性调整:
|
||||
- 改为 `:show-floor="is3DMode"`,在全馆模式也显示楼层控件
|
||||
- 全馆模式下点击楼层,先切换到楼层视图,再加载对应楼层
|
||||
|
||||
**验证标准**:
|
||||
- 自动切换后,顶部状态标签正确更新。
|
||||
- 全馆模式下可以直接点击楼层控件选择楼层。
|
||||
|
||||
**文件修改**:
|
||||
- `src/pages/index/index.vue`:监听自动切换事件
|
||||
- `src/components/navigation/GuideMapShell.vue`:调整楼层控件可见性
|
||||
|
||||
---
|
||||
|
||||
#### Task 1.3:保留"全馆/楼层"手动切换作为兜底
|
||||
|
||||
**优先级**:P1
|
||||
**预估工作量**:0.5 天
|
||||
**依赖**:Task 1.1, 1.2
|
||||
|
||||
**实现要点**:
|
||||
|
||||
1. 保留现有"全馆/楼层"切换按钮,但调整定位:
|
||||
- 不再是主交互入口,而是手动控制和兜底入口。
|
||||
- 样式可以调整为更轻量(例如图标按钮,而不是双按钮切换组)。
|
||||
|
||||
2. 手动切换时临时禁用自动切换(例如 10 秒):
|
||||
```typescript
|
||||
const handleManualSwitch = (view: 'overview' | 'floor') => {
|
||||
// 临时禁用自动切换
|
||||
threeMapRef.value?.disableAutoSwitchTemporarily?.(10000)
|
||||
|
||||
// 执行手动切换
|
||||
if (view === 'overview') {
|
||||
void threeMapRef.value?.showOverview?.()
|
||||
} else {
|
||||
void threeMapRef.value?.switchFloor?.(activeFloorId.value)
|
||||
}
|
||||
|
||||
indoorView.value = view
|
||||
}
|
||||
```
|
||||
|
||||
3. 增加设置入口,允许用户永久禁用自动切换(可选)。
|
||||
|
||||
**验证标准**:
|
||||
- 手动切换后,10 秒内不会因缩放触发自动切换。
|
||||
- 手动切换按钮始终可用。
|
||||
|
||||
**文件修改**:
|
||||
- `src/components/map/ThreeMap.vue`:增加临时禁用自动切换方法
|
||||
- `src/components/navigation/GuideMapShell.vue`:调整切换按钮样式和行为
|
||||
|
||||
---
|
||||
|
||||
### Phase 2:交互细节优化(增强)
|
||||
|
||||
**目标**:优化自动切换的用户体验细节。
|
||||
|
||||
#### Task 2.1:增加视觉过渡和状态提示
|
||||
|
||||
**优先级**:P1
|
||||
**预估工作量**:1 天
|
||||
**依赖**:Task 1.1
|
||||
|
||||
**实现要点**:
|
||||
|
||||
1. 自动切换时增加淡入淡出过渡:
|
||||
```typescript
|
||||
const transitionSwitch = async (loadFn: () => Promise<void>) => {
|
||||
// 淡出当前模型
|
||||
if (activeModel) {
|
||||
await fadeOut(activeModel, 300)
|
||||
}
|
||||
|
||||
// 加载新模型
|
||||
await loadFn()
|
||||
|
||||
// 淡入新模型
|
||||
if (activeModel) {
|
||||
await fadeIn(activeModel, 300)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. 切换时显示提示文案:
|
||||
- "正在切换到楼层视图..."
|
||||
- "正在切换到完整建筑..."
|
||||
|
||||
3. 首次自动切换时显示引导提示(Toast):
|
||||
- "已自动切换到楼层内部,您也可以手动控制视图"
|
||||
- 提示只显示一次,存储到本地缓存
|
||||
|
||||
**验证标准**:
|
||||
- 切换过程视觉流畅,无突兀感。
|
||||
- 首次自动切换有引导提示。
|
||||
|
||||
**文件修改**:
|
||||
- `src/components/map/ThreeMap.vue`:增加过渡动画
|
||||
- `src/pages/index/index.vue`:增加首次引导提示
|
||||
|
||||
---
|
||||
|
||||
#### Task 2.2:优化阈值和参数
|
||||
|
||||
**优先级**:P2
|
||||
**预估工作量**:1-2 天(需要实测调优)
|
||||
**依赖**:Task 1.1
|
||||
|
||||
**实现要点**:
|
||||
|
||||
1. 在真实设备(手机、平板)上测试不同阈值的效果:
|
||||
- 建筑尺寸的 0.8x / 1.0x / 1.2x
|
||||
- 冷却时间 1s / 2s / 3s
|
||||
|
||||
2. 针对不同建筑尺寸动态调整阈值:
|
||||
```typescript
|
||||
const calculateThresholds = (modelSize: THREE.Vector3) => {
|
||||
const maxDim = Math.max(modelSize.x, modelSize.y, modelSize.z, 1)
|
||||
|
||||
// 大建筑用较大倍数,小建筑用较小倍数
|
||||
const sizeFactor = maxDim > 100 ? 1.2 : 1.0
|
||||
|
||||
return {
|
||||
low: maxDim * sizeFactor * 0.9,
|
||||
high: maxDim * sizeFactor * 1.1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. 记录切换频率和用户行为,优化参数。
|
||||
|
||||
**验证标准**:
|
||||
- 移动端测试无误触。
|
||||
- 切换时机符合用户预期。
|
||||
- 不会频繁切换导致加载抖动。
|
||||
|
||||
**文件修改**:
|
||||
- `src/components/map/ThreeMap.vue`:优化阈值计算
|
||||
|
||||
---
|
||||
|
||||
### Phase 3:多层展示支持(未来)
|
||||
|
||||
**目标**:支持单层/多层显示模式切换,适配跨层路线展示。
|
||||
|
||||
**前置条件**:
|
||||
- `route_graph` 和 `nav_data` 已接入
|
||||
- 跨层路线规划能力已实现
|
||||
|
||||
#### Task 3.1:扩展 ViewMode 类型和多层加载逻辑
|
||||
|
||||
**优先级**:P3(依赖路线能力)
|
||||
**预估工作量**:3-4 天
|
||||
|
||||
**实现要点**:
|
||||
|
||||
1. 扩展 `ViewMode` 类型:
|
||||
```typescript
|
||||
type ViewMode = 'overview' | 'single-floor' | 'multi-floor'
|
||||
```
|
||||
|
||||
2. 实现 `loadMultiFloor` 方法:
|
||||
```typescript
|
||||
const loadMultiFloor = async (floorIds: string[]) => {
|
||||
activeView.value = 'multi-floor'
|
||||
clearSceneData()
|
||||
|
||||
const verticalSpacing = 10 // 楼层间距
|
||||
|
||||
for (const [index, floorId] of floorIds.entries()) {
|
||||
const floor = floorIndex.value.find(f => f.floorId === floorId)
|
||||
if (!floor) continue
|
||||
|
||||
const gltf = await loadModel(floorModelUrl(floor), `加载 ${floor.label}`)
|
||||
const model = gltf.scene
|
||||
model.position.y = index * verticalSpacing
|
||||
prepareModel(model)
|
||||
scene.add(model)
|
||||
}
|
||||
|
||||
fitCameraToMultiFloor(floorIds)
|
||||
}
|
||||
```
|
||||
|
||||
3. 注意性能优化:
|
||||
- 多层模型总面数可能很大,需要监控帧率
|
||||
- 考虑 LOD(细节层次)或简化模型
|
||||
|
||||
**验证标准**:
|
||||
- 可以同时加载并显示多个楼层。
|
||||
- 楼层保持合理的垂直间距。
|
||||
- 帧率在可接受范围(>30fps)。
|
||||
|
||||
**文件修改**:
|
||||
- `src/components/map/ThreeMap.vue`:增加多层加载逻辑
|
||||
- `src/domain/guideModel.ts`:扩展 ViewMode 类型
|
||||
|
||||
---
|
||||
|
||||
#### Task 3.2:增加"单层/多层"切换控件
|
||||
|
||||
**优先级**:P3
|
||||
**预估工作量**:1 天
|
||||
**依赖**:Task 3.1
|
||||
|
||||
**实现要点**:
|
||||
|
||||
1. 在 `GuideMapShell` 增加切换控件:
|
||||
```vue
|
||||
<view v-if="mapType === 'indoor' && indoorView === 'floor'" class="floor-display-mode">
|
||||
<view class="mode-item" :class="{ active: floorDisplayMode === 'single' }">
|
||||
<text>单层</text>
|
||||
</view>
|
||||
<view class="mode-item" :class="{ active: floorDisplayMode === 'multi' }">
|
||||
<text>多层</text>
|
||||
</view>
|
||||
</view>
|
||||
```
|
||||
|
||||
2. 多层模式下显示楼层选择器,支持多选:
|
||||
- 用户可以勾选要同时显示的楼层(例如 B2、B1、1F)
|
||||
- 默认显示当前楼层的上下各一层
|
||||
|
||||
**验证标准**:
|
||||
- 切换到多层模式,可以选择要显示的楼层。
|
||||
- 多层模式下可以看到跨层路线完整路径。
|
||||
|
||||
**文件修改**:
|
||||
- `src/components/navigation/GuideMapShell.vue`:增加多层切换控件
|
||||
- `src/pages/index/index.vue`:增加多层模式状态管理
|
||||
|
||||
---
|
||||
|
||||
## 实施优先级
|
||||
|
||||
### 必须实施(Phase 1)
|
||||
|
||||
这些任务实现核心的自动切换能力,显著提升交互直觉性。
|
||||
|
||||
- Task 1.1:实现距离监听与自动切换逻辑
|
||||
- Task 1.2:调整首页和 GuideMapShell 配合自动切换
|
||||
- Task 1.3:保留手动切换作为兜底
|
||||
|
||||
**预估总工作量**:3.5-4.5 天
|
||||
|
||||
---
|
||||
|
||||
### 建议实施(Phase 2)
|
||||
|
||||
这些任务优化用户体验细节,提升产品完成度。
|
||||
|
||||
- Task 2.1:增加视觉过渡和状态提示
|
||||
- Task 2.2:优化阈值和参数
|
||||
|
||||
**预估总工作量**:2-3 天
|
||||
|
||||
---
|
||||
|
||||
### 未来实施(Phase 3)
|
||||
|
||||
这些任务依赖跨层路线能力,优先级较低。
|
||||
|
||||
- Task 3.1:扩展 ViewMode 和多层加载逻辑
|
||||
- Task 3.2:增加"单层/多层"切换控件
|
||||
|
||||
**预估总工作量**:4-5 天
|
||||
**前置条件**:`route_graph` 和 `nav_data` 接入完成
|
||||
|
||||
---
|
||||
|
||||
## 技术风险与应对
|
||||
|
||||
### 风险 1:自动切换触发频繁,导致加载抖动
|
||||
|
||||
**影响**:用户体验差,可能导致卡顿。
|
||||
|
||||
**应对措施**:
|
||||
- 使用双阈值滞回机制(低阈值 1.0x,高阈值 1.3x)
|
||||
- 增加冷却时间(2 秒内不重复触发)
|
||||
- 增加加载锁(切换期间锁定,不响应新的切换请求)
|
||||
- 监听 `controls.change` 事件而不是每帧检查,减少计算频率
|
||||
|
||||
---
|
||||
|
||||
### 风险 2:移动端误触缩放,导致意外切换
|
||||
|
||||
**影响**:用户可能在查看建筑外观时误触放大,意外切换到楼层。
|
||||
|
||||
**应对措施**:
|
||||
- 手动切换后临时禁用自动切换(10 秒)
|
||||
- 提供设置项,允许用户永久禁用自动切换
|
||||
- 优化阈值,避免轻微缩放就触发
|
||||
- 首次自动切换时显示引导提示,告知用户可以手动控制
|
||||
|
||||
---
|
||||
|
||||
### 风险 3:多层模式性能问题
|
||||
|
||||
**影响**:同时加载多个楼层可能导致内存占用过高、帧率下降。
|
||||
|
||||
**应对措施**:
|
||||
- 限制最多同时显示 3-4 层
|
||||
- 考虑使用 LOD(细节层次),远处楼层使用简化模型
|
||||
- 监控帧率,低于阈值时提示用户减少显示楼层
|
||||
- 多层模式下禁用部分高级渲染特性(例如阴影)
|
||||
|
||||
---
|
||||
|
||||
## 质量保证
|
||||
|
||||
### 单元测试
|
||||
|
||||
- `ThreeMap.vue` 的距离计算和阈值判断逻辑
|
||||
- 自动切换状态机测试(overview → floor → overview)
|
||||
- 加载锁和冷却时间逻辑测试
|
||||
|
||||
### 集成测试
|
||||
|
||||
- 完整的用户流程:进入室内 → 缩放 → 自动切换 → 手动切换 → 楼层切换
|
||||
- 自动切换与手动切换的交互测试
|
||||
- 多层模式与单层模式切换测试
|
||||
|
||||
### 移动端测试
|
||||
|
||||
- 在真实移动设备(iOS/Android)测试缩放手势
|
||||
- 测试误触场景
|
||||
- 测试弱网环境下的加载体验
|
||||
|
||||
### 性能测试
|
||||
|
||||
- 监控切换过程的内存占用
|
||||
- 监控多层模式的帧率
|
||||
- 测试大建筑和小建筑的阈值适配性
|
||||
|
||||
---
|
||||
|
||||
## 验收标准
|
||||
|
||||
### Phase 1 验收标准
|
||||
|
||||
- [ ] 用户在建筑外观放大到一定距离,自动切换到默认楼层(L1)
|
||||
- [ ] 用户在楼层内部缩小到一定距离,自动切换回建筑外观
|
||||
- [ ] 快速缩放不会触发多次加载
|
||||
- [ ] 手动切换后 10 秒内不会自动切换
|
||||
- [ ] 全馆模式下可以直接点击楼层控件选择楼层
|
||||
- [ ] 移动端测试无明显误触
|
||||
|
||||
### Phase 2 验收标准
|
||||
|
||||
- [ ] 切换过程有视觉过渡,无突兀感
|
||||
- [ ] 首次自动切换有引导提示
|
||||
- [ ] 切换时有 loading 状态提示
|
||||
- [ ] 阈值在真实设备上测试符合预期
|
||||
|
||||
### Phase 3 验收标准
|
||||
|
||||
- [ ] 可以切换到多层模式,同时显示多个楼层
|
||||
- [ ] 多层模式帧率 >30fps
|
||||
- [ ] 多层模式下可以看到跨层路线完整路径
|
||||
|
||||
---
|
||||
|
||||
## 附录:配置项设计
|
||||
|
||||
### ThreeMap 组件新增 Props
|
||||
|
||||
```typescript
|
||||
interface ThreeMapProps {
|
||||
// ... 现有 props
|
||||
|
||||
// 自动切换配置
|
||||
autoSwitch?: boolean // 是否启用自动切换,默认 true
|
||||
autoSwitchThresholdLow?: number // 下阈值倍数,默认 1.0
|
||||
autoSwitchThresholdHigh?: number // 上阈值倍数,默认 1.3
|
||||
autoSwitchCooldown?: number // 冷却时间(毫秒),默认 2000
|
||||
|
||||
// 多层模式配置(Phase 3)
|
||||
enableMultiFloor?: boolean // 是否启用多层模式,默认 false
|
||||
multiFloorVerticalSpacing?: number // 楼层间距(米),默认 10
|
||||
}
|
||||
```
|
||||
|
||||
### 新增 Events
|
||||
|
||||
```typescript
|
||||
// ThreeMap emit 的新事件
|
||||
emit('auto-switch', {
|
||||
from: 'overview' | 'floor',
|
||||
to: 'overview' | 'floor',
|
||||
trigger: 'zoom-in' | 'zoom-out',
|
||||
distance: number
|
||||
})
|
||||
|
||||
emit('auto-switch-blocked', {
|
||||
reason: 'loading-locked' | 'cooldown' | 'disabled'
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 参考资料
|
||||
|
||||
- OrbitControls 文档:https://threejs.org/docs/#examples/en/controls/OrbitControls
|
||||
- Three.js 性能优化最佳实践:https://threejs.org/manual/#en/optimize-lots-of-objects
|
||||
- 室内导览产品参考:机场导览 App、商场导览 App、博物馆 3D 导览
|
||||
|
||||
---
|
||||
|
||||
## 变更记录
|
||||
|
||||
| 日期 | 版本 | 变更内容 | 作者 |
|
||||
|------|------|---------|------|
|
||||
| 2026-06-12 | v1.0 | 初始版本 | Claude |
|
||||
@@ -1,26 +1,50 @@
|
||||
<template>
|
||||
<view class="exhibit-card" @tap="handleClick">
|
||||
<image v-if="exhibit.image" class="exhibit-image" :src="exhibit.image" mode="aspectFill" />
|
||||
<image
|
||||
v-if="coverImage"
|
||||
class="exhibit-image"
|
||||
:src="coverImage"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view v-else class="exhibit-image placeholder">
|
||||
<text class="placeholder-text">讲</text>
|
||||
</view>
|
||||
<view class="exhibit-content">
|
||||
<text class="exhibit-name">{{ exhibit.name }}</text>
|
||||
<text v-if="exhibit.artist" class="exhibit-artist">{{ exhibit.artist }}</text>
|
||||
<view v-if="exhibit.hall" class="exhibit-meta">
|
||||
<text class="meta-text">📍 {{ exhibit.hall }}</text>
|
||||
<text class="exhibit-name">{{ title }}</text>
|
||||
<text v-if="subtitle" class="exhibit-subtitle">{{ subtitle }}</text>
|
||||
<view class="exhibit-meta">
|
||||
<svg class="meta-icon" width="14" height="14" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 21C12 21 18 14.9 18 9.8C18 6.5 15.3 4 12 4C8.7 4 6 6.5 6 9.8C6 14.9 12 21 12 21Z" stroke="currentColor" stroke-width="1.8"/>
|
||||
<circle cx="12" cy="10" r="2.2" stroke="currentColor" stroke-width="1.8"/>
|
||||
</svg>
|
||||
<text class="meta-text">{{ metaText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="exhibit.hasAudio" class="audio-badge">
|
||||
<text class="audio-icon">🎧</text>
|
||||
<view class="audio-badge" :class="`status-${audioStatus}`">
|
||||
<text class="audio-text">{{ audioStatusText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type {
|
||||
ExplainAudioStatus
|
||||
} from '@/view-models/explainViewModels'
|
||||
|
||||
interface Exhibit {
|
||||
id: string
|
||||
name: string
|
||||
artist?: string
|
||||
title?: string
|
||||
name?: string
|
||||
subtitle?: string
|
||||
hall?: string
|
||||
hallName?: string
|
||||
floor?: string
|
||||
floorLabel?: string
|
||||
coverImage?: string
|
||||
image?: string
|
||||
audioStatus?: ExplainAudioStatus
|
||||
audioStatusText?: string
|
||||
hasAudio?: boolean
|
||||
}
|
||||
|
||||
@@ -32,6 +56,18 @@ const emit = defineEmits<{
|
||||
click: [exhibit: Exhibit]
|
||||
}>()
|
||||
|
||||
const title = computed(() => props.exhibit.title || props.exhibit.name || '讲解内容')
|
||||
const subtitle = computed(() => props.exhibit.subtitle || props.exhibit.hall || props.exhibit.hallName)
|
||||
const coverImage = computed(() => props.exhibit.coverImage || props.exhibit.image)
|
||||
const audioStatus = computed<ExplainAudioStatus>(() => (
|
||||
props.exhibit.audioStatus || (props.exhibit.hasAudio ? 'playable' : 'unavailable')
|
||||
))
|
||||
const audioStatusText = computed(() => props.exhibit.audioStatusText || (audioStatus.value === 'playable' ? '音频' : '图文'))
|
||||
const metaText = computed(() => [
|
||||
props.exhibit.floorLabel || props.exhibit.floor,
|
||||
audioStatusText.value
|
||||
].filter(Boolean).join(' · '))
|
||||
|
||||
const handleClick = () => {
|
||||
emit('click', props.exhibit)
|
||||
}
|
||||
@@ -40,12 +76,16 @@ const handleClick = () => {
|
||||
<style scoped lang="scss">
|
||||
.exhibit-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 104px;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--museum-bg-surface);
|
||||
border-radius: var(--radius-card);
|
||||
border: 1px solid #ebece5;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-sm);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.exhibit-card:active {
|
||||
@@ -53,33 +93,59 @@ const handleClick = () => {
|
||||
}
|
||||
|
||||
.exhibit-image {
|
||||
width: 100%;
|
||||
height: 160px;
|
||||
flex-shrink: 0;
|
||||
width: 76px;
|
||||
height: 84px;
|
||||
border-radius: 8px;
|
||||
background-color: var(--museum-bg-light);
|
||||
}
|
||||
|
||||
.exhibit-image.placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #eef1e8;
|
||||
}
|
||||
|
||||
.placeholder-text {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #68725d;
|
||||
}
|
||||
|
||||
.exhibit-content {
|
||||
padding: var(--space-md);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.exhibit-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
line-height: 21px;
|
||||
font-weight: 700;
|
||||
color: var(--museum-text-primary);
|
||||
margin-bottom: var(--space-xs);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.exhibit-artist {
|
||||
font-size: 13px;
|
||||
.exhibit-subtitle {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 17px;
|
||||
color: var(--museum-text-secondary);
|
||||
margin-bottom: var(--space-sm);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.exhibit-meta {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.meta-icon {
|
||||
flex-shrink: 0;
|
||||
color: #68725d;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.meta-text {
|
||||
@@ -89,19 +155,30 @@ const handleClick = () => {
|
||||
|
||||
.audio-badge {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-color: var(--museum-accent);
|
||||
border-radius: 50%;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
min-width: 44px;
|
||||
height: 24px;
|
||||
padding: 0 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: var(--shadow-sm);
|
||||
box-sizing: border-box;
|
||||
background-color: #eef1e8;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.audio-icon {
|
||||
font-size: 16px;
|
||||
.audio-badge.status-playable {
|
||||
background-color: var(--museum-accent);
|
||||
}
|
||||
|
||||
.audio-text {
|
||||
font-size: 11px;
|
||||
color: #4f574b;
|
||||
}
|
||||
|
||||
.audio-badge.status-playable .audio-text {
|
||||
color: #151713;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -117,7 +117,7 @@ const emit = defineEmits<{
|
||||
// POI ID: 7043042949572197975
|
||||
const mapCenter = ref<MapCenter>({
|
||||
latitude: 22.692763, // 深圳自然博物馆纬度
|
||||
longitude: 114.363987 // 深圳自然博物馆经度(向右偏移,让建筑显示在左侧)
|
||||
longitude: 114.363572 // 深圳自然博物馆经度(调整到室外 2D 视觉中心)
|
||||
})
|
||||
|
||||
// 地图缩放级别(调整为适中比例,既能看到建筑全貌又能看清细节)
|
||||
|
||||
@@ -2,121 +2,173 @@ import type {
|
||||
MuseumExhibit,
|
||||
MuseumHall
|
||||
} from '@/domain/museum'
|
||||
import {
|
||||
formatNavFloorLabel
|
||||
} from '@/data/adapters/navAssetsAdapter'
|
||||
import {
|
||||
defaultStaticNavAssetsProvider,
|
||||
type StaticNavAssetsProvider,
|
||||
type StaticNavPoiPayload
|
||||
} from '@/data/providers/staticNavAssetsProvider'
|
||||
|
||||
export interface MuseumContentProvider {
|
||||
listExhibits(): Promise<MuseumExhibit[]>
|
||||
listHalls(): Promise<MuseumHall[]>
|
||||
}
|
||||
|
||||
const transitionalHalls: MuseumHall[] = [
|
||||
{
|
||||
id: 'hall-evolution',
|
||||
name: '演化厅',
|
||||
floorId: 'L-1',
|
||||
floorLabel: 'B1',
|
||||
description: '自然演化主题内容。当前为内容仓储过渡数据,待 CMS/API 接入后替换。',
|
||||
image: '/static/hall-placeholder.jpg',
|
||||
exhibitCount: 3,
|
||||
area: '待补充'
|
||||
},
|
||||
{
|
||||
id: 'hall-dinosaur',
|
||||
name: '恐龙厅',
|
||||
floorId: 'L2',
|
||||
floorLabel: '2F',
|
||||
description: '恐龙与古生物主题内容。当前为内容仓储过渡数据,待 CMS/API 接入后替换。',
|
||||
image: '/static/hall-placeholder.jpg',
|
||||
exhibitCount: 2,
|
||||
area: '待补充'
|
||||
},
|
||||
{
|
||||
id: 'hall-biology',
|
||||
name: '生物厅',
|
||||
floorId: 'L3',
|
||||
floorLabel: '3F',
|
||||
description: '生物多样性主题内容。当前为内容仓储过渡数据,待 CMS/API 接入后替换。',
|
||||
image: '/static/hall-placeholder.jpg',
|
||||
exhibitCount: 2,
|
||||
area: '待补充'
|
||||
}
|
||||
]
|
||||
const contentImage = '/static/exhibit-placeholder.jpg'
|
||||
const hallImage = '/static/hall-placeholder.jpg'
|
||||
|
||||
const transitionalExhibits: MuseumExhibit[] = [
|
||||
{
|
||||
id: 'exhibit-ginkgo-fossil',
|
||||
name: '银杏化石',
|
||||
hallId: 'hall-evolution',
|
||||
hallName: '演化厅',
|
||||
floorId: 'L-1',
|
||||
floorLabel: 'B1',
|
||||
image: '/static/exhibit-placeholder.jpg',
|
||||
description: '银杏化石内容待由正式内容库补充。当前仅用于讲解业务架构迁移。',
|
||||
tags: ['化石', '古植物']
|
||||
},
|
||||
{
|
||||
id: 'exhibit-trilobite-fossil',
|
||||
name: '三叶虫化石',
|
||||
hallId: 'hall-evolution',
|
||||
hallName: '演化厅',
|
||||
floorId: 'L-1',
|
||||
floorLabel: 'B1',
|
||||
image: '/static/exhibit-placeholder.jpg',
|
||||
description: '三叶虫化石内容待由正式内容库补充。当前仅用于讲解业务架构迁移。',
|
||||
tags: ['化石', '古生物']
|
||||
},
|
||||
{
|
||||
id: 'exhibit-brachiosaurus',
|
||||
name: '腕龙骨骼',
|
||||
hallId: 'hall-dinosaur',
|
||||
hallName: '恐龙厅',
|
||||
floorId: 'L2',
|
||||
floorLabel: '2F',
|
||||
image: '/static/exhibit-placeholder.jpg',
|
||||
description: '腕龙骨骼内容待由正式内容库补充。当前仅用于讲解业务架构迁移。',
|
||||
tags: ['恐龙', '骨骼']
|
||||
},
|
||||
{
|
||||
id: 'exhibit-archaeopteryx',
|
||||
name: '始祖鸟标本',
|
||||
hallId: 'hall-evolution',
|
||||
hallName: '演化厅',
|
||||
floorId: 'L-1',
|
||||
floorLabel: 'B1',
|
||||
image: '/static/exhibit-placeholder.jpg',
|
||||
description: '始祖鸟标本内容待由正式内容库补充。当前仅用于讲解业务架构迁移。',
|
||||
tags: ['演化', '标本']
|
||||
},
|
||||
{
|
||||
id: 'exhibit-mammoth-tusk',
|
||||
name: '猛犸象象牙',
|
||||
hallId: 'hall-biology',
|
||||
hallName: '生物厅',
|
||||
floorId: 'L3',
|
||||
floorLabel: '3F',
|
||||
image: '/static/exhibit-placeholder.jpg',
|
||||
description: '猛犸象象牙内容待由正式内容库补充。当前仅用于讲解业务架构迁移。',
|
||||
tags: ['古生物', '标本']
|
||||
},
|
||||
{
|
||||
id: 'exhibit-coral-reef',
|
||||
name: '珊瑚礁标本',
|
||||
hallId: 'hall-biology',
|
||||
hallName: '生物厅',
|
||||
floorId: 'L3',
|
||||
floorLabel: '3F',
|
||||
image: '/static/exhibit-placeholder.jpg',
|
||||
description: '珊瑚礁标本内容待由正式内容库补充。当前仅用于讲解业务架构迁移。',
|
||||
tags: ['海洋生物', '标本']
|
||||
}
|
||||
]
|
||||
const cleanPoiName = (poi: StaticNavPoiPayload) => poi.name
|
||||
.replace(new RegExp(`^${poi.floorId}\\s*`), '')
|
||||
.replace(/^L\d+(?:\.\d+)?\s+/, '')
|
||||
.replace(/^L-\d+(?:\.\d+)?\s+/, '')
|
||||
.replace(/^L\d+(?:\.\d+)?\s+/, '')
|
||||
.trim()
|
||||
|
||||
const normalizeContentId = (prefix: string, value: string) => `${prefix}-${value}`
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9一-龥]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
|
||||
const isTouringPoi = (poi: StaticNavPoiPayload) => (
|
||||
poi.primaryCategory === 'touring_poi'
|
||||
|| poi.categories?.some((category) => category.topCategory === 'touring_poi') === true
|
||||
)
|
||||
|
||||
const isHallPoi = (poi: StaticNavPoiPayload) => {
|
||||
const name = cleanPoiName(poi)
|
||||
return /展厅|影院|剧场|报告厅|活动室/.test(name)
|
||||
&& !/展柜|装饰/.test(name)
|
||||
}
|
||||
|
||||
const isExhibitCandidate = (poi: StaticNavPoiPayload) => /展柜|装饰/.test(cleanPoiName(poi))
|
||||
|
||||
const hallKeyFromName = (name: string) => {
|
||||
const match = name.match(/(展厅\s*\d+\s*[^\s展柜装饰]+|临展厅\d+|[^\s]+厅|[^\s]+影院|自然剧场|学术报告厅|博物馆之友活动室)/)
|
||||
return (match?.[1] || name)
|
||||
.replace(/\s+/g, '')
|
||||
.replace(/L\d+\s*/g, '')
|
||||
}
|
||||
|
||||
const exhibitNameFromPoi = (poi: StaticNavPoiPayload) => cleanPoiName(poi)
|
||||
.replace(/\s+/g, ' ')
|
||||
.replace(/^(展厅\s*\d+\s*)/, '')
|
||||
.trim()
|
||||
|
||||
const hallDescription = (hallName: string, floorLabel: string) => (
|
||||
`${hallName}位于${floorLabel},来源于当前导览资源包中的游览 POI。当前内容用于讲解业务结构占位,正式展陈文案、音频和图文媒体待 CMS/API 接入后替换。`
|
||||
)
|
||||
|
||||
const exhibitDescription = (exhibitName: string, hallName: string, floorLabel: string) => (
|
||||
`${exhibitName}是${hallName}的讲解点,位置关联到${floorLabel}的导览 POI。当前仅展示内容结构和位置关联,不代表正式展陈文案或可播放音频。`
|
||||
)
|
||||
|
||||
export class StaticMuseumContentProvider implements MuseumContentProvider {
|
||||
private hallCache: MuseumHall[] | null = null
|
||||
private exhibitCache: MuseumExhibit[] | null = null
|
||||
private loading: Promise<void> | null = null
|
||||
|
||||
constructor(
|
||||
private readonly navAssets: StaticNavAssetsProvider = defaultStaticNavAssetsProvider
|
||||
) {}
|
||||
|
||||
async listExhibits() {
|
||||
return transitionalExhibits
|
||||
await this.ensureLoaded()
|
||||
return this.exhibitCache || []
|
||||
}
|
||||
|
||||
async listHalls() {
|
||||
return transitionalHalls
|
||||
await this.ensureLoaded()
|
||||
return this.hallCache || []
|
||||
}
|
||||
|
||||
private async ensureLoaded() {
|
||||
if (this.hallCache && this.exhibitCache) return
|
||||
if (this.loading) return this.loading
|
||||
|
||||
this.loading = this.navAssets.loadPoiIndex()
|
||||
.then((pois) => {
|
||||
const touringPois = pois.filter(isTouringPoi)
|
||||
const hallPois = touringPois.filter(isHallPoi)
|
||||
const hallByKey = new Map<string, MuseumHall>()
|
||||
|
||||
hallPois.forEach((poi) => {
|
||||
const hallName = cleanPoiName(poi)
|
||||
const key = `${poi.floorId}:${hallKeyFromName(hallName)}`
|
||||
const floorLabel = formatNavFloorLabel(poi.floorId)
|
||||
hallByKey.set(key, {
|
||||
id: normalizeContentId('hall', poi.id),
|
||||
name: hallName,
|
||||
floorId: poi.floorId,
|
||||
floorLabel,
|
||||
description: hallDescription(hallName, floorLabel),
|
||||
image: hallImage,
|
||||
exhibitCount: 0,
|
||||
area: '以导览资源包 POI 为准',
|
||||
poiId: poi.id
|
||||
})
|
||||
})
|
||||
|
||||
const fallbackHallFor = (poi: StaticNavPoiPayload) => {
|
||||
const poiName = cleanPoiName(poi)
|
||||
const key = `${poi.floorId}:${hallKeyFromName(poiName)}`
|
||||
const floorLabel = formatNavFloorLabel(poi.floorId)
|
||||
const hallName = hallKeyFromName(poiName)
|
||||
const existing = hallByKey.get(key)
|
||||
if (existing) return existing
|
||||
|
||||
const hall: MuseumHall = {
|
||||
id: normalizeContentId('hall', `${poi.floorId}-${hallName}`),
|
||||
name: hallName,
|
||||
floorId: poi.floorId,
|
||||
floorLabel,
|
||||
description: hallDescription(hallName, floorLabel),
|
||||
image: hallImage,
|
||||
exhibitCount: 0,
|
||||
area: '以导览资源包 POI 为准'
|
||||
}
|
||||
hallByKey.set(key, hall)
|
||||
return hall
|
||||
}
|
||||
|
||||
const exhibits = touringPois
|
||||
.filter(isExhibitCandidate)
|
||||
.map((poi): MuseumExhibit => {
|
||||
const hall = fallbackHallFor(poi)
|
||||
const exhibitName = exhibitNameFromPoi(poi)
|
||||
return {
|
||||
id: normalizeContentId('exhibit', poi.id),
|
||||
name: exhibitName,
|
||||
hallId: hall.id,
|
||||
hallName: hall.name,
|
||||
floorId: poi.floorId,
|
||||
floorLabel: formatNavFloorLabel(poi.floorId),
|
||||
image: contentImage,
|
||||
description: exhibitDescription(exhibitName, hall.name, formatNavFloorLabel(poi.floorId)),
|
||||
poiId: poi.id,
|
||||
tags: ['资源包POI', poi.primaryCategoryZh, poi.iconType || '展项'].filter(Boolean)
|
||||
}
|
||||
})
|
||||
|
||||
const exhibitCountByHallId = exhibits.reduce((result, exhibit) => {
|
||||
if (exhibit.hallId) {
|
||||
result.set(exhibit.hallId, (result.get(exhibit.hallId) || 0) + 1)
|
||||
}
|
||||
return result
|
||||
}, new Map<string, number>())
|
||||
|
||||
this.hallCache = Array.from(hallByKey.values()).map((hall) => ({
|
||||
...hall,
|
||||
exhibitCount: exhibitCountByHallId.get(hall.id) || hall.exhibitCount || 0
|
||||
}))
|
||||
this.exhibitCache = exhibits
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = null
|
||||
})
|
||||
|
||||
return this.loading
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,76 +6,158 @@
|
||||
>
|
||||
<view class="detail-page">
|
||||
<scroll-view class="content" scroll-y>
|
||||
<!-- 展品图片 -->
|
||||
<view class="exhibit-hero">
|
||||
<image class="hero-image" :src="exhibit.image" mode="aspectFill" />
|
||||
<view class="audio-control" @tap="handlePlayAudio">
|
||||
<text class="audio-icon">{{ isPlaying ? '⏸' : '▶' }}</text>
|
||||
<view class="detail-hero">
|
||||
<image
|
||||
v-if="heroImage"
|
||||
class="hero-image"
|
||||
:src="heroImage"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view v-else class="hero-placeholder">
|
||||
<text class="hero-placeholder-text">讲解</text>
|
||||
</view>
|
||||
<view
|
||||
v-if="exhibit.audio.status === 'playable'"
|
||||
class="audio-control"
|
||||
@tap="handlePlayAudio"
|
||||
>
|
||||
<svg v-if="!isPlaying" width="28" height="28" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M8 5V19L19 12L8 5Z" fill="currentColor"/>
|
||||
</svg>
|
||||
<svg v-else width="28" height="28" viewBox="0 0 24 24" fill="none">
|
||||
<rect x="6" y="5" width="4" height="14" rx="1" fill="currentColor"/>
|
||||
<rect x="14" y="5" width="4" height="14" rx="1" fill="currentColor"/>
|
||||
</svg>
|
||||
</view>
|
||||
<view v-else class="audio-status">
|
||||
<text class="audio-status-text">{{ audioStatusText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 展品信息 -->
|
||||
<view class="exhibit-info">
|
||||
<text class="exhibit-title">{{ exhibit.name }}</text>
|
||||
<text v-if="exhibit.artist" class="exhibit-artist">{{ exhibit.artist }}</text>
|
||||
<view class="detail-info">
|
||||
<text class="content-type">{{ contentTypeText }}</text>
|
||||
<text class="detail-title">{{ exhibit.title }}</text>
|
||||
<text v-if="exhibit.subtitle" class="detail-subtitle">{{ exhibit.subtitle }}</text>
|
||||
|
||||
<view class="info-grid">
|
||||
<view v-if="exhibit.year" class="info-item">
|
||||
<text class="info-label">创作年代</text>
|
||||
<text class="info-value">{{ exhibit.year }}</text>
|
||||
<view class="meta-grid">
|
||||
<view class="meta-item">
|
||||
<text class="meta-label">内容类型</text>
|
||||
<text class="meta-value">{{ contentTypeText }}</text>
|
||||
</view>
|
||||
<view v-if="exhibit.material" class="info-item">
|
||||
<text class="info-label">材质</text>
|
||||
<text class="info-value">{{ exhibit.material }}</text>
|
||||
<view v-if="exhibit.hallName" class="meta-item">
|
||||
<text class="meta-label">所属展厅</text>
|
||||
<text class="meta-value">{{ exhibit.hallName }}</text>
|
||||
</view>
|
||||
<view v-if="exhibit.size" class="info-item">
|
||||
<text class="info-label">尺寸</text>
|
||||
<text class="info-value">{{ exhibit.size }}</text>
|
||||
<view v-if="exhibit.floorLabel" class="meta-item">
|
||||
<text class="meta-label">所在楼层</text>
|
||||
<text class="meta-value">{{ exhibit.floorLabel }}</text>
|
||||
</view>
|
||||
<view v-if="exhibit.hall" class="info-item">
|
||||
<text class="info-label">展厅位置</text>
|
||||
<text class="info-value">{{ exhibit.hall }}</text>
|
||||
<view class="meta-item">
|
||||
<text class="meta-label">讲解状态</text>
|
||||
<text class="meta-value">{{ audioStatusText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="description-section">
|
||||
<text class="section-title">作品介绍</text>
|
||||
<text class="description-text">{{ exhibit.description }}</text>
|
||||
<view v-if="exhibit.audio.status !== 'playable'" class="audio-note">
|
||||
<text class="audio-note-title">音频待开放</text>
|
||||
<text class="audio-note-desc">
|
||||
{{ exhibit.audio.unavailableReason || '当前仅提供图文讲解,正式音频接入后将显示播放入口。' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="content-section">
|
||||
<text class="section-title">讲解摘要</text>
|
||||
<text class="section-text">{{ exhibit.summary }}</text>
|
||||
</view>
|
||||
|
||||
<view class="content-section">
|
||||
<text class="section-title">讲解介绍</text>
|
||||
<text class="section-text">{{ exhibit.body }}</text>
|
||||
</view>
|
||||
|
||||
<view class="content-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">章节</text>
|
||||
<text class="section-state">{{ exhibit.chapters.length ? `${exhibit.chapters.length} 段` : '待开放' }}</text>
|
||||
</view>
|
||||
<view v-if="exhibit.chapters.length" class="chapter-list">
|
||||
<view
|
||||
v-for="chapter in exhibit.chapters"
|
||||
:key="chapter.id"
|
||||
class="chapter-item"
|
||||
>
|
||||
<text class="chapter-title">{{ chapter.title }}</text>
|
||||
<text v-if="typeof chapter.startTime === 'number'" class="chapter-time">
|
||||
{{ formatChapterTime(chapter.startTime) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<text v-else class="section-text muted">正式章节数据接入后将在这里展示。</text>
|
||||
</view>
|
||||
|
||||
<view class="content-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">讲解文稿</text>
|
||||
<text class="section-state">{{ exhibit.transcript ? '已接入' : '待开放' }}</text>
|
||||
</view>
|
||||
<text class="section-text muted">
|
||||
{{ exhibit.transcript || '正式文稿数据接入后将在这里展示。' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view v-if="exhibit.location" class="location-note">
|
||||
<text class="location-title">位置说明</text>
|
||||
<text class="location-desc">当前支持三维位置预览,馆内路线规划待开放。</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="action-bar">
|
||||
<view class="action-btn-group">
|
||||
<view class="action-btn" @tap="handleNavigate">
|
||||
<text class="btn-icon">🗺️</text>
|
||||
<text class="btn-text">查看位置</text>
|
||||
<view
|
||||
class="action-btn primary"
|
||||
:class="{ disabled: !exhibit.location }"
|
||||
@tap="handleNavigate"
|
||||
>
|
||||
<svg class="btn-icon" width="18" height="18" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 21C12 21 18 14.9 18 9.8C18 6.5 15.3 4 12 4C8.7 4 6 6.5 6 9.8C6 14.9 12 21 12 21Z" stroke="currentColor" stroke-width="1.7"/>
|
||||
<circle cx="12" cy="10" r="2.2" stroke="currentColor" stroke-width="1.7"/>
|
||||
</svg>
|
||||
<text class="btn-text">{{ exhibit.location?.actionText || '暂无位置' }}</text>
|
||||
</view>
|
||||
<view class="action-btn" @tap="handleCollect">
|
||||
<text class="btn-icon">{{ isCollected ? '❤️' : '🤍' }}</text>
|
||||
<svg class="btn-icon" width="18" height="18" viewBox="0 0 24 24" fill="none">
|
||||
<path
|
||||
:fill="isCollected ? 'currentColor' : 'none'"
|
||||
d="M12 20.2L10.8 19.1C6.4 15.1 3.5 12.5 3.5 9.2C3.5 6.6 5.6 4.5 8.2 4.5C9.7 4.5 11.1 5.2 12 6.3C12.9 5.2 14.3 4.5 15.8 4.5C18.4 4.5 20.5 6.6 20.5 9.2C20.5 12.5 17.6 15.1 13.2 19.1L12 20.2Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.7"
|
||||
/>
|
||||
</svg>
|
||||
<text class="btn-text">收藏</text>
|
||||
</view>
|
||||
<view class="action-btn" @tap="handleShare">
|
||||
<text class="btn-icon">📤</text>
|
||||
<svg class="btn-icon" width="18" height="18" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M8 12H6.8C5.8 12 5 12.8 5 13.8V18.2C5 19.2 5.8 20 6.8 20H17.2C18.2 20 19 19.2 19 18.2V13.8C19 12.8 18.2 12 17.2 12H16" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/>
|
||||
<path d="M12 15V4M12 4L8.5 7.5M12 4L15.5 7.5" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<text class="btn-text">分享</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</GuidePageFrame>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import GuidePageFrame from '@/components/navigation/GuidePageFrame.vue'
|
||||
import {
|
||||
explainUseCase
|
||||
} from '@/usecases/explainUseCase'
|
||||
import {
|
||||
toExplainDetailViewModel,
|
||||
type ExplainDetailViewModel
|
||||
toExplainDetailPageViewModel,
|
||||
type ExplainContentType,
|
||||
type ExplainDetailPageViewModel
|
||||
} from '@/view-models/explainViewModels'
|
||||
import {
|
||||
isGuideTopTab,
|
||||
@@ -83,25 +165,53 @@ import {
|
||||
type GuideTopTab
|
||||
} from '@/utils/guideTopTabs'
|
||||
|
||||
const exhibit = ref<ExplainDetailViewModel>({
|
||||
const defaultDetail: ExplainDetailPageViewModel = {
|
||||
id: '',
|
||||
name: '展项内容',
|
||||
hall: '展厅位置待补充',
|
||||
image: '/static/exhibit-placeholder.jpg',
|
||||
description: '该展项介绍待正式内容库补充。',
|
||||
hasAudio: false,
|
||||
audioUnavailableReason: '正式讲解音频尚未接入媒体仓储'
|
||||
})
|
||||
title: '讲解内容',
|
||||
subtitle: '位置待补充',
|
||||
contentType: 'exhibit',
|
||||
coverImages: ['/static/exhibit-placeholder.jpg'],
|
||||
summary: '该讲解内容待正式内容库补充。',
|
||||
body: '该讲解内容待正式内容库补充。',
|
||||
audio: {
|
||||
status: 'unavailable',
|
||||
unavailableReason: '正式讲解音频尚未接入媒体仓储'
|
||||
},
|
||||
chapters: [],
|
||||
relatedItems: []
|
||||
}
|
||||
|
||||
const exhibit = ref<ExplainDetailPageViewModel>(defaultDetail)
|
||||
const isPlaying = ref(false)
|
||||
const isCollected = ref(false)
|
||||
const activeTopTab = ref<GuideTopTab>('guide')
|
||||
|
||||
const heroImage = computed(() => exhibit.value.coverImages[0])
|
||||
const audioStatusText = computed(() => {
|
||||
if (exhibit.value.audio.status === 'playable') {
|
||||
return exhibit.value.audio.durationLabel || '可播放'
|
||||
}
|
||||
if (exhibit.value.audio.status === 'none') {
|
||||
return '图文讲解'
|
||||
}
|
||||
return '音频待开放'
|
||||
})
|
||||
|
||||
const contentTypeText = computed(() => {
|
||||
const map: Record<ExplainContentType, string> = {
|
||||
hall: '展厅讲解',
|
||||
zone: '展区讲解',
|
||||
exhibit: '讲解点',
|
||||
theme: '主题讲解'
|
||||
}
|
||||
return map[exhibit.value.contentType]
|
||||
})
|
||||
|
||||
onLoad(async (options: any) => {
|
||||
if (options.id) {
|
||||
const exhibitData = await explainUseCase.getExhibitById(options.id)
|
||||
if (exhibitData) {
|
||||
exhibit.value = toExplainDetailViewModel(exhibitData)
|
||||
exhibit.value = toExplainDetailPageViewModel(exhibitData)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,35 +221,46 @@ onLoad(async (options: any) => {
|
||||
}
|
||||
})
|
||||
|
||||
const formatChapterTime = (seconds: number) => {
|
||||
const minute = Math.floor(seconds / 60)
|
||||
const second = Math.floor(seconds % 60)
|
||||
return `${minute}:${String(second).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
const handlePlayAudio = async () => {
|
||||
const selection = exhibit.value.id
|
||||
? await explainUseCase.selectAudioForExhibit(exhibit.value.id)
|
||||
: null
|
||||
|
||||
if (!selection?.playable || !selection.media?.url) {
|
||||
uni.showToast({
|
||||
title: selection?.unavailableMessage || exhibit.value.audioUnavailableReason || '该展项暂无讲解音频',
|
||||
title: selection?.unavailableMessage || exhibit.value.audio.unavailableReason || '该讲解暂无可播放音频',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
isPlaying.value = !isPlaying.value
|
||||
}
|
||||
|
||||
const handleNavigate = () => {
|
||||
if (!exhibit.value.poiId) {
|
||||
if (!exhibit.value.location?.poiId) {
|
||||
uni.showToast({
|
||||
title: '该展项暂无三维位置数据',
|
||||
title: '该讲解暂无三维位置数据',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
uni.navigateTo({
|
||||
url: `/pages/route/detail?facilityId=${encodeURIComponent(exhibit.value.poiId)}&target=${encodeURIComponent(exhibit.value.name)}&state=preview`
|
||||
url: `/pages/route/detail?facilityId=${encodeURIComponent(exhibit.value.location.poiId)}&target=${encodeURIComponent(exhibit.value.title)}&state=preview`
|
||||
})
|
||||
}
|
||||
|
||||
const handleCollect = () => {
|
||||
isCollected.value = !isCollected.value
|
||||
uni.showToast({
|
||||
title: isCollected.value ? '已收藏' : '已取消收藏',
|
||||
title: isCollected.value ? '已加入本地收藏' : '已取消本地收藏',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
@@ -151,16 +272,17 @@ const handleShare = () => {
|
||||
const handleTopTabChange = (tab: GuideTopTab) => {
|
||||
navigateToGuideTopTab(tab)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.detail-page {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--museum-bg-light);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content {
|
||||
@@ -168,147 +290,266 @@ const handleTopTabChange = (tab: GuideTopTab) => {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.exhibit-hero {
|
||||
.detail-hero {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 450px;
|
||||
background-color: var(--museum-bg-map);
|
||||
height: 250px;
|
||||
background: #edf0ea;
|
||||
}
|
||||
|
||||
.hero-image {
|
||||
.hero-image,
|
||||
.hero-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.audio-control {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background-color: var(--museum-accent);
|
||||
border-radius: 50%;
|
||||
.hero-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
background: #e6eadf;
|
||||
}
|
||||
|
||||
.audio-control:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.audio-icon {
|
||||
font-size: 28px;
|
||||
color: var(--museum-text-primary);
|
||||
}
|
||||
|
||||
.exhibit-info {
|
||||
padding: 20px 16px 32px;
|
||||
background-color: var(--museum-bg-surface);
|
||||
}
|
||||
|
||||
.exhibit-title {
|
||||
font-size: 26px;
|
||||
.hero-placeholder-text {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--museum-text-primary);
|
||||
margin-bottom: 6px;
|
||||
display: block;
|
||||
line-height: 1.3;
|
||||
color: #67705f;
|
||||
}
|
||||
|
||||
.exhibit-artist {
|
||||
font-size: 15px;
|
||||
color: var(--museum-text-secondary);
|
||||
margin-bottom: 20px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px 16px;
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
background-color: var(--museum-bg-light);
|
||||
border-radius: var(--radius-card);
|
||||
}
|
||||
|
||||
.info-item {
|
||||
.audio-control {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
bottom: 18px;
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--museum-accent);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
|
||||
color: #151713;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
.audio-status {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
background: rgba(21, 23, 19, 0.86);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.audio-status-text {
|
||||
font-size: 12px;
|
||||
color: var(--museum-text-disabled);
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
.detail-info {
|
||||
padding: 22px 16px 116px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.content-type,
|
||||
.detail-title,
|
||||
.detail-subtitle,
|
||||
.section-title,
|
||||
.section-text,
|
||||
.audio-note-title,
|
||||
.audio-note-desc,
|
||||
.location-title,
|
||||
.location-desc {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.content-type {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color: #68725d;
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
margin-top: 4px;
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
font-weight: 700;
|
||||
color: #151713;
|
||||
}
|
||||
|
||||
.detail-subtitle {
|
||||
margin-top: 6px;
|
||||
font-size: 14px;
|
||||
color: var(--museum-text-primary);
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
color: #68725d;
|
||||
}
|
||||
|
||||
.description-section {
|
||||
margin-top: 20px;
|
||||
.meta-grid {
|
||||
margin-top: 18px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
min-height: 62px;
|
||||
padding: 10px 12px;
|
||||
box-sizing: border-box;
|
||||
background: #f7f8f3;
|
||||
border: 1px solid #ebece5;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.meta-label,
|
||||
.meta-value {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color: #818a7a;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
margin-top: 4px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #151713;
|
||||
}
|
||||
|
||||
.audio-note,
|
||||
.location-note {
|
||||
margin-top: 14px;
|
||||
padding: 12px;
|
||||
box-sizing: border-box;
|
||||
background: #f1f4ea;
|
||||
border: 1px solid #e2e7d7;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.audio-note-title,
|
||||
.location-title {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
font-weight: 700;
|
||||
color: #151713;
|
||||
}
|
||||
|
||||
.audio-note-desc,
|
||||
.location-desc {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: #5d6656;
|
||||
}
|
||||
|
||||
.content-section {
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
color: var(--museum-text-primary);
|
||||
margin-bottom: 10px;
|
||||
display: block;
|
||||
line-height: 24px;
|
||||
font-weight: 700;
|
||||
color: #151713;
|
||||
}
|
||||
|
||||
.description-text {
|
||||
.section-state {
|
||||
font-size: 12px;
|
||||
color: #818a7a;
|
||||
}
|
||||
|
||||
.section-text {
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
color: var(--museum-text-secondary);
|
||||
line-height: 24px;
|
||||
color: #333a2f;
|
||||
}
|
||||
|
||||
.section-text.muted {
|
||||
color: #818a7a;
|
||||
}
|
||||
|
||||
.chapter-list {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.chapter-item {
|
||||
min-height: 44px;
|
||||
padding: 0 12px;
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
background: #f7f8f3;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.chapter-title {
|
||||
font-size: 14px;
|
||||
color: #151713;
|
||||
}
|
||||
|
||||
.chapter-time {
|
||||
font-size: 12px;
|
||||
color: #818a7a;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
background-color: var(--museum-bg-surface);
|
||||
border-top: 1px solid var(--museum-border-light);
|
||||
padding: 8px 16px;
|
||||
padding-bottom: calc(8px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.action-btn-group {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
|
||||
display: grid;
|
||||
grid-template-columns: 1.4fr 1fr 1fr;
|
||||
gap: 8px;
|
||||
box-sizing: border-box;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
border-top: 1px solid #e7e9e0;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
height: 46px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 10px 8px;
|
||||
background-color: var(--museum-bg-light);
|
||||
border-radius: var(--radius-button);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
box-sizing: border-box;
|
||||
background: #f7f8f3;
|
||||
border: 1px solid #e5e8df;
|
||||
border-radius: 8px;
|
||||
color: #151713;
|
||||
}
|
||||
|
||||
.action-btn:active {
|
||||
background-color: var(--museum-border-light);
|
||||
transform: scale(0.98);
|
||||
.action-btn.primary {
|
||||
background: #151713;
|
||||
border-color: #151713;
|
||||
color: var(--museum-accent);
|
||||
}
|
||||
|
||||
.action-btn.disabled {
|
||||
background: #f1f3ed;
|
||||
border-color: #e5e8df;
|
||||
color: #8b9385;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 22px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 11px;
|
||||
color: var(--museum-text-secondary);
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: currentColor;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -22,17 +22,17 @@
|
||||
<view class="stats-row">
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{ hall.exhibitCount }}</text>
|
||||
<text class="stat-label">展品数量</text>
|
||||
<text class="stat-label">讲解点</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{ hall.area }}</text>
|
||||
<text class="stat-label">展厅面积</text>
|
||||
<text class="stat-value">{{ hall.floorLabel }}</text>
|
||||
<text class="stat-label">所在楼层</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 展品列表 -->
|
||||
<!-- 讲解列表 -->
|
||||
<view class="exhibits-section">
|
||||
<text class="section-title">展厅展品</text>
|
||||
<text class="section-title">展厅讲解内容</text>
|
||||
<view class="exhibits-grid">
|
||||
<ExhibitCard
|
||||
v-for="exhibit in exhibits"
|
||||
@@ -48,7 +48,7 @@
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="action-bar">
|
||||
<view class="action-btn primary" @tap="handleNavigate">
|
||||
<text class="btn-text">查看展厅位置</text>
|
||||
<text class="btn-text">查看三维位置</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -83,10 +83,13 @@
|
||||
|
||||
<view v-else-if="currentTab === 'explain'" class="explain-page">
|
||||
<ExplainList
|
||||
:exhibits="explainExhibits"
|
||||
:cards="explainExhibits"
|
||||
:loading="explainLoading"
|
||||
:error="explainError"
|
||||
@exhibit-click="handleExplainExhibitClick"
|
||||
@featured-click="handleExplainFeaturedClick"
|
||||
@audio-click="handleAudioClick"
|
||||
@location-click="handleExplainLocationClick"
|
||||
/>
|
||||
|
||||
<FloatingAudioButton
|
||||
@@ -130,7 +133,7 @@ import {
|
||||
explainUseCase
|
||||
} from '@/usecases/explainUseCase'
|
||||
import {
|
||||
toExplainExhibitViewModel
|
||||
toExplainCardViewModel
|
||||
} from '@/view-models/explainViewModels'
|
||||
import type {
|
||||
MuseumFloor
|
||||
@@ -168,6 +171,8 @@ const showAudioPlayer = ref(false)
|
||||
const isAudioPlaying = ref(false)
|
||||
const currentAudio = ref<AudioItem | null>(null)
|
||||
const explainExhibits = ref<Exhibit[]>([])
|
||||
const explainLoading = ref(false)
|
||||
const explainError = ref('')
|
||||
|
||||
// 选中的标记详情
|
||||
interface MarkerDetail {
|
||||
@@ -227,12 +232,17 @@ const loadGuideFloors = async () => {
|
||||
}
|
||||
|
||||
const loadExplainExhibits = async () => {
|
||||
explainLoading.value = true
|
||||
explainError.value = ''
|
||||
try {
|
||||
const exhibits = await explainUseCase.listExhibits()
|
||||
explainExhibits.value = exhibits.map(toExplainExhibitViewModel)
|
||||
explainExhibits.value = exhibits.map(toExplainCardViewModel)
|
||||
} catch (error) {
|
||||
console.error('加载讲解内容失败:', error)
|
||||
explainError.value = '讲解内容加载失败,请稍后重试'
|
||||
explainExhibits.value = []
|
||||
} finally {
|
||||
explainLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -442,7 +452,7 @@ const handleAudioClick = async (exhibit: Exhibit) => {
|
||||
|
||||
if (!selection?.playable || !selection.media?.url) {
|
||||
uni.showToast({
|
||||
title: selection?.unavailableMessage || exhibit.audioUnavailableReason || '该展项暂无讲解音频',
|
||||
title: selection?.unavailableMessage || exhibit.audioStatusText || '该讲解暂无可播放音频',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
@@ -465,7 +475,7 @@ const handleFloatingButtonClick = () => {
|
||||
showAudioPlayer.value = true
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '请先选择一个展品',
|
||||
title: '请先选择一个讲解内容',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
@@ -496,6 +506,20 @@ const handleExplainExhibitClick = (exhibit: Exhibit) => {
|
||||
const handleExplainFeaturedClick = async (exhibit: Exhibit) => {
|
||||
await handleAudioClick(exhibit)
|
||||
}
|
||||
|
||||
const handleExplainLocationClick = (exhibit: Exhibit) => {
|
||||
if (!exhibit.poiId) {
|
||||
uni.showToast({
|
||||
title: '该讲解暂无三维位置数据',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
uni.navigateTo({
|
||||
url: `/pages/route/detail?facilityId=${encodeURIComponent(exhibit.poiId)}&target=${encodeURIComponent(exhibit.title)}&state=preview`
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -89,7 +89,7 @@ export class DefaultExplainRepository implements ExplainRepository {
|
||||
.map<SearchIndexItem>((exhibit) => ({
|
||||
id: exhibit.id,
|
||||
name: exhibit.name,
|
||||
desc: `${exhibit.hallName || '展项'} · ${exhibit.floorLabel || ''}`.trim(),
|
||||
desc: `${exhibit.hallName || '讲解点'} · ${exhibit.floorLabel || ''}`.trim(),
|
||||
type: 'exhibit',
|
||||
exhibitId: exhibit.id,
|
||||
hallId: exhibit.hallId,
|
||||
@@ -106,7 +106,7 @@ export class DefaultExplainRepository implements ExplainRepository {
|
||||
.map<SearchIndexItem>((hall) => ({
|
||||
id: hall.id,
|
||||
name: hall.name,
|
||||
desc: `${hall.floorLabel || ''} · ${hall.exhibitCount || 0}件展项`.trim(),
|
||||
desc: `${hall.floorLabel || ''} · ${hall.exhibitCount || 0}个讲解点`.trim(),
|
||||
type: 'hall',
|
||||
hallId: hall.id,
|
||||
poiId: hall.poiId,
|
||||
|
||||
@@ -81,7 +81,7 @@ export class DefaultMuseumContentRepository implements MuseumContentRepository {
|
||||
.map<SearchIndexItem>((exhibit) => ({
|
||||
id: exhibit.id,
|
||||
name: exhibit.name,
|
||||
desc: `${exhibit.hallName || '展项'} · ${exhibit.floorLabel || ''}`.trim(),
|
||||
desc: `${exhibit.hallName || '讲解点'} · ${exhibit.floorLabel || ''}`.trim(),
|
||||
type: 'exhibit',
|
||||
exhibitId: exhibit.id,
|
||||
poiId: exhibit.poiId,
|
||||
@@ -97,7 +97,7 @@ export class DefaultMuseumContentRepository implements MuseumContentRepository {
|
||||
.map<SearchIndexItem>((hall) => ({
|
||||
id: hall.id,
|
||||
name: hall.name,
|
||||
desc: `${hall.floorLabel || ''} · ${hall.exhibitCount || 0}件展项`.trim(),
|
||||
desc: `${hall.floorLabel || ''} · ${hall.exhibitCount || 0}个讲解点`.trim(),
|
||||
type: 'hall',
|
||||
hallId: hall.id,
|
||||
poiId: hall.poiId,
|
||||
|
||||
@@ -4,32 +4,62 @@ import type {
|
||||
SearchIndexItem
|
||||
} from '@/domain/museum'
|
||||
|
||||
export interface ExplainExhibitViewModel {
|
||||
export type ExplainContentType = 'hall' | 'zone' | 'exhibit' | 'theme'
|
||||
export type ExplainAudioStatus = 'playable' | 'unavailable' | 'none'
|
||||
|
||||
export interface ExplainCardViewModel {
|
||||
id: string
|
||||
name: string
|
||||
hall?: string
|
||||
floor?: string
|
||||
image?: string
|
||||
hasAudio: boolean
|
||||
audioUnavailableReason?: string
|
||||
title: string
|
||||
subtitle?: string
|
||||
summary?: string
|
||||
coverImage?: string
|
||||
contentType: ExplainContentType
|
||||
hallName?: string
|
||||
floorLabel?: string
|
||||
durationLabel?: string
|
||||
languageLabel?: string
|
||||
audioStatus: ExplainAudioStatus
|
||||
audioStatusText: string
|
||||
badges: string[]
|
||||
progress?: number
|
||||
poiId?: string
|
||||
locationActionText?: string
|
||||
}
|
||||
|
||||
export interface ExplainDetailViewModel {
|
||||
export interface ExplainDetailPageViewModel {
|
||||
id: string
|
||||
name: string
|
||||
artist?: string
|
||||
year?: string
|
||||
material?: string
|
||||
size?: string
|
||||
hall?: string
|
||||
image: string
|
||||
description: string
|
||||
hasAudio: boolean
|
||||
audioUnavailableReason?: string
|
||||
poiId?: string
|
||||
title: string
|
||||
subtitle?: string
|
||||
contentType: ExplainContentType
|
||||
coverImages: string[]
|
||||
hallName?: string
|
||||
floorLabel?: string
|
||||
summary: string
|
||||
body: string
|
||||
audio: {
|
||||
status: ExplainAudioStatus
|
||||
url?: string
|
||||
durationLabel?: string
|
||||
language?: string
|
||||
unavailableReason?: string
|
||||
}
|
||||
chapters: Array<{
|
||||
id: string
|
||||
title: string
|
||||
startTime?: number
|
||||
}>
|
||||
transcript?: string
|
||||
location?: {
|
||||
poiId: string
|
||||
actionText: string
|
||||
previewOnly: boolean
|
||||
}
|
||||
relatedItems: ExplainCardViewModel[]
|
||||
}
|
||||
|
||||
export type ExplainExhibitViewModel = ExplainCardViewModel
|
||||
export type ExplainDetailViewModel = ExplainDetailPageViewModel
|
||||
|
||||
export interface HallDetailViewModel {
|
||||
id: string
|
||||
name: string
|
||||
@@ -41,36 +71,76 @@ export interface HallDetailViewModel {
|
||||
poiId?: string
|
||||
}
|
||||
|
||||
export const toExplainExhibitViewModel = (exhibit: MuseumExhibit): ExplainExhibitViewModel => ({
|
||||
const contentTypeFor = (exhibit: MuseumExhibit): ExplainContentType => {
|
||||
if (exhibit.tags?.some((tag) => /主题/.test(tag))) return 'theme'
|
||||
if (exhibit.hallName && exhibit.tags?.some((tag) => /展厅/.test(tag))) return 'hall'
|
||||
return 'exhibit'
|
||||
}
|
||||
|
||||
const buildSubtitle = (exhibit: MuseumExhibit) => [
|
||||
exhibit.hallName,
|
||||
exhibit.floorLabel
|
||||
].filter(Boolean).join(' · ')
|
||||
|
||||
const buildBadges = (exhibit: MuseumExhibit) => {
|
||||
const badges = [
|
||||
exhibit.hallName ? '展厅讲解' : '讲解点',
|
||||
exhibit.floorLabel || '',
|
||||
...(exhibit.tags || [])
|
||||
].filter(Boolean)
|
||||
|
||||
return Array.from(new Set(badges)).slice(0, 3)
|
||||
}
|
||||
|
||||
export const toExplainCardViewModel = (exhibit: MuseumExhibit): ExplainCardViewModel => ({
|
||||
id: exhibit.id,
|
||||
name: exhibit.name,
|
||||
hall: exhibit.hallName && exhibit.floorLabel
|
||||
? `${exhibit.hallName} ${exhibit.floorLabel}`
|
||||
: exhibit.hallName || exhibit.floorLabel,
|
||||
floor: exhibit.floorLabel,
|
||||
image: exhibit.image,
|
||||
hasAudio: false,
|
||||
audioUnavailableReason: '正式讲解音频尚未接入媒体仓储',
|
||||
poiId: exhibit.poiId
|
||||
title: exhibit.name,
|
||||
subtitle: buildSubtitle(exhibit),
|
||||
summary: exhibit.description || '讲解内容待正式内容库补充。',
|
||||
coverImage: exhibit.image,
|
||||
contentType: contentTypeFor(exhibit),
|
||||
hallName: exhibit.hallName,
|
||||
floorLabel: exhibit.floorLabel,
|
||||
durationLabel: undefined,
|
||||
languageLabel: undefined,
|
||||
audioStatus: 'unavailable',
|
||||
audioStatusText: '图文讲解',
|
||||
badges: buildBadges(exhibit),
|
||||
progress: undefined,
|
||||
poiId: exhibit.poiId,
|
||||
locationActionText: '查看位置'
|
||||
})
|
||||
|
||||
export const toExplainDetailViewModel = (exhibit: MuseumExhibit): ExplainDetailViewModel => ({
|
||||
export const toExplainExhibitViewModel = (exhibit: MuseumExhibit): ExplainExhibitViewModel => toExplainCardViewModel(exhibit)
|
||||
|
||||
export const toExplainDetailPageViewModel = (exhibit: MuseumExhibit): ExplainDetailPageViewModel => ({
|
||||
id: exhibit.id,
|
||||
name: exhibit.name,
|
||||
artist: exhibit.artist,
|
||||
year: exhibit.year,
|
||||
material: exhibit.material,
|
||||
size: exhibit.size,
|
||||
hall: exhibit.hallName && exhibit.floorLabel
|
||||
? `${exhibit.hallName} ${exhibit.floorLabel}`
|
||||
: exhibit.hallName || exhibit.floorLabel,
|
||||
image: exhibit.image || '/static/exhibit-placeholder.jpg',
|
||||
description: exhibit.description || '该展项介绍待正式内容库补充。',
|
||||
hasAudio: false,
|
||||
audioUnavailableReason: '正式讲解音频尚未接入媒体仓储',
|
||||
poiId: exhibit.poiId
|
||||
title: exhibit.name,
|
||||
subtitle: buildSubtitle(exhibit),
|
||||
contentType: contentTypeFor(exhibit),
|
||||
coverImages: [exhibit.image || '/static/exhibit-placeholder.jpg'].filter(Boolean),
|
||||
hallName: exhibit.hallName,
|
||||
floorLabel: exhibit.floorLabel,
|
||||
summary: exhibit.description || '该讲解内容待正式内容库补充。',
|
||||
body: exhibit.description || '该讲解内容待正式内容库补充。',
|
||||
audio: {
|
||||
status: 'unavailable',
|
||||
unavailableReason: '正式讲解音频尚未接入媒体仓储'
|
||||
},
|
||||
chapters: [],
|
||||
transcript: undefined,
|
||||
location: exhibit.poiId
|
||||
? {
|
||||
poiId: exhibit.poiId,
|
||||
actionText: '查看三维位置',
|
||||
previewOnly: true
|
||||
}
|
||||
: undefined,
|
||||
relatedItems: []
|
||||
})
|
||||
|
||||
export const toExplainDetailViewModel = (exhibit: MuseumExhibit): ExplainDetailViewModel => toExplainDetailPageViewModel(exhibit)
|
||||
|
||||
export const toHallDetailViewModel = (hall: MuseumHall): HallDetailViewModel => ({
|
||||
id: hall.id,
|
||||
name: hall.name,
|
||||
|
||||
Reference in New Issue
Block a user