This commit is contained in:
@@ -12,10 +12,11 @@
|
||||
class="explain-scroll"
|
||||
:class="{
|
||||
'hall-stage': stage === 'hall',
|
||||
'unit-stage': stage === 'unit',
|
||||
'stop-stage': stage === 'stop'
|
||||
}"
|
||||
scroll-y
|
||||
@scroll="handleScroll"
|
||||
@scrolltolower="handleScrollToLower"
|
||||
:show-scrollbar="false"
|
||||
>
|
||||
<view v-if="loading" class="state-block">
|
||||
@@ -24,8 +25,9 @@
|
||||
</view>
|
||||
|
||||
<view v-else-if="error" class="state-block error">
|
||||
<text class="state-title">讲解内容加载失败</text>
|
||||
<text class="state-title">讲解对象加载失败</text>
|
||||
<text class="state-desc">{{ error }}</text>
|
||||
<button class="state-retry" @tap="emit('retry')">重新加载</button>
|
||||
</view>
|
||||
|
||||
<template v-else>
|
||||
@@ -75,31 +77,6 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else-if="stage === 'unit' && businessUnits.length" class="hall-list">
|
||||
<view
|
||||
v-for="unit in businessUnits"
|
||||
:key="unit.id"
|
||||
class="hall-card unit-card"
|
||||
@tap="handleBusinessUnitClick(unit.id)"
|
||||
>
|
||||
<image
|
||||
class="unit-thumb"
|
||||
:src="unitPreviewImageUrl(unit)"
|
||||
mode="aspectFill"
|
||||
@error="handleUnitPreviewError(unit.id)"
|
||||
/>
|
||||
|
||||
<view class="hall-main">
|
||||
<text class="hall-name">{{ unit.name }}</text>
|
||||
<view class="hall-meta-row">
|
||||
<text class="hall-meta-text">{{ unit.guideStopCount }} 个讲解</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<text class="hall-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else-if="stage === 'stop' && guideStops.length" class="hall-list">
|
||||
<view
|
||||
v-for="stop in guideStops"
|
||||
@@ -140,6 +117,14 @@
|
||||
<text class="state-title">{{ emptyTitle }}</text>
|
||||
<text class="state-desc">{{ emptyDescription }}</text>
|
||||
</view>
|
||||
<view v-if="stage === 'stop' && guideStops.length" class="load-more-state">
|
||||
<text v-if="loadingMore" class="state-desc">正在加载更多讲解对象</text>
|
||||
<template v-else-if="loadMoreError">
|
||||
<text class="state-desc">{{ loadMoreError }}</text>
|
||||
<button class="state-retry" @tap="emit('retryMore')">重试加载更多</button>
|
||||
</template>
|
||||
<text v-else-if="!hasMore" class="state-desc">已加载全部讲解对象</text>
|
||||
</view>
|
||||
</template>
|
||||
</scroll-view>
|
||||
</view>
|
||||
@@ -147,9 +132,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import {
|
||||
EXPLAIN_UNIT_PREVIEW_FALLBACK
|
||||
} from '@/utils/explainUnitPreviews'
|
||||
import { isEmbeddedInWechatMiniProgram } from '@/utils/hostEnvironment'
|
||||
|
||||
export interface ExplainHallSelectItem {
|
||||
@@ -158,19 +140,10 @@ export interface ExplainHallSelectItem {
|
||||
floorLabel?: string
|
||||
image?: string
|
||||
explainCount: number
|
||||
businessUnitCount?: number
|
||||
guideStopCount?: number
|
||||
searchText: string
|
||||
}
|
||||
|
||||
export interface ExplainBusinessUnitSelectItem {
|
||||
id: string
|
||||
name: string
|
||||
hallId: string
|
||||
previewImageUrl?: string
|
||||
guideStopCount: number
|
||||
}
|
||||
|
||||
export interface ExplainGuideStopSelectItem {
|
||||
id: string
|
||||
name: string
|
||||
@@ -189,40 +162,42 @@ export interface ExplainGuideStopSelectItem {
|
||||
playTargetId?: string
|
||||
}
|
||||
|
||||
export type ExplainSelectStage = 'hall' | 'unit' | 'stop'
|
||||
export type ExplainSelectStage = 'hall' | 'stop'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
halls?: ExplainHallSelectItem[]
|
||||
businessUnits?: ExplainBusinessUnitSelectItem[]
|
||||
guideStops?: ExplainGuideStopSelectItem[]
|
||||
stage?: ExplainSelectStage
|
||||
selectedHallName?: string
|
||||
selectedBusinessUnitName?: string
|
||||
loading?: boolean
|
||||
loadingMore?: boolean
|
||||
hasMore?: boolean
|
||||
error?: string
|
||||
loadMoreError?: string
|
||||
}>(), {
|
||||
halls: () => [],
|
||||
businessUnits: () => [],
|
||||
guideStops: () => [],
|
||||
stage: 'hall',
|
||||
selectedHallName: '',
|
||||
selectedBusinessUnitName: '',
|
||||
loading: false,
|
||||
error: ''
|
||||
loadingMore: false,
|
||||
hasMore: false,
|
||||
error: '',
|
||||
loadMoreError: ''
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
hallClick: [hallId: string]
|
||||
businessUnitClick: [unitId: string]
|
||||
guideStopClick: [stop: ExplainGuideStopSelectItem]
|
||||
back: []
|
||||
retry: []
|
||||
retryMore: []
|
||||
}>()
|
||||
|
||||
const HALL_PREVIEW_BASE = '/static/explain/hall-previews'
|
||||
const HALL_PREVIEW_EAGER_COUNT = 4
|
||||
const loadedHallPreviewIds = ref(new Set<string>())
|
||||
const failedHallPreviewIds = ref(new Set<string>())
|
||||
const failedUnitPreviewIds = ref(new Set<string>())
|
||||
|
||||
const hallPreviewMap: Record<string, string> = {
|
||||
宇宙厅: `${HALL_PREVIEW_BASE}/universe.webp`,
|
||||
@@ -240,28 +215,23 @@ const filteredHalls = computed(() => props.halls)
|
||||
const shouldUseHostNavigation = computed(() => isEmbeddedInWechatMiniProgram())
|
||||
const showInternalHeader = computed(() => !shouldUseHostNavigation.value)
|
||||
const headerTitle = computed(() => {
|
||||
if (props.stage === 'unit') return props.selectedHallName || '选择业务单元'
|
||||
if (props.stage === 'stop') return props.selectedBusinessUnitName || '选择讲解点'
|
||||
if (props.stage === 'stop') return props.selectedHallName || '讲解对象'
|
||||
return '讲解'
|
||||
})
|
||||
const loadingTitle = computed(() => {
|
||||
if (props.stage === 'unit') return '正在加载业务单元'
|
||||
if (props.stage === 'stop') return '正在加载讲解点'
|
||||
if (props.stage === 'stop') return '正在加载讲解对象'
|
||||
return '正在加载展厅讲解'
|
||||
})
|
||||
const loadingDescription = computed(() => {
|
||||
if (props.stage === 'unit') return '正在按讲解点所属 outline 分组生成临时业务单元。'
|
||||
if (props.stage === 'stop') return '稍后将展示该业务单元下的讲解点。'
|
||||
if (props.stage === 'stop') return '稍后将展示该展厅的讲解对象。'
|
||||
return '稍后将展示可选择的展厅。'
|
||||
})
|
||||
const emptyTitle = computed(() => {
|
||||
if (props.stage === 'unit') return '该展厅暂无已标定讲解点'
|
||||
if (props.stage === 'stop') return '该业务单元暂无讲解点'
|
||||
if (props.stage === 'stop') return '该展厅暂无讲解对象'
|
||||
return '未找到相关展厅'
|
||||
})
|
||||
const emptyDescription = computed(() => {
|
||||
if (props.stage === 'unit') return '方案 B 只展示已在地图上标定的讲解点。'
|
||||
if (props.stage === 'stop') return '可返回选择其他业务单元。'
|
||||
if (props.stage === 'stop') return '可返回选择其他展厅。'
|
||||
return '暂无可选择的展厅讲解。'
|
||||
})
|
||||
|
||||
@@ -295,19 +265,9 @@ const handleHallPreviewError = (hallId: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
const unitPreviewImageUrl = (unit: ExplainBusinessUnitSelectItem) => (
|
||||
failedUnitPreviewIds.value.has(unit.id)
|
||||
? EXPLAIN_UNIT_PREVIEW_FALLBACK
|
||||
: unit.previewImageUrl || EXPLAIN_UNIT_PREVIEW_FALLBACK
|
||||
)
|
||||
|
||||
const handleUnitPreviewError = (unitId: string) => {
|
||||
failedUnitPreviewIds.value = new Set([...failedUnitPreviewIds.value, unitId])
|
||||
}
|
||||
|
||||
const hallMetaText = (hall: ExplainHallSelectItem) => (
|
||||
typeof hall.businessUnitCount === 'number' || typeof hall.guideStopCount === 'number'
|
||||
? `${hall.businessUnitCount || 0} 个业务单元 · ${hall.guideStopCount || 0} 个讲解点`
|
||||
typeof hall.guideStopCount === 'number'
|
||||
? `${hall.guideStopCount || 0} 个讲解对象`
|
||||
: hall.explainCount > 0 ? `${hall.explainCount} 个展项` : '展厅'
|
||||
)
|
||||
|
||||
@@ -315,10 +275,6 @@ const handleHallClick = (hallId: string) => {
|
||||
emit('hallClick', hallId)
|
||||
}
|
||||
|
||||
const handleBusinessUnitClick = (unitId: string) => {
|
||||
emit('businessUnitClick', unitId)
|
||||
}
|
||||
|
||||
const handleGuideStopClick = (stopId: string) => {
|
||||
const stop = props.guideStops.find((item) => item.id === stopId)
|
||||
if (stop) {
|
||||
@@ -326,6 +282,21 @@ const handleGuideStopClick = (stopId: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleScrollToLower = () => {
|
||||
requestMore()
|
||||
}
|
||||
|
||||
const requestMore = () => {
|
||||
if (props.stage === 'stop' && props.hasMore && !props.loading && !props.loadingMore && !props.loadMoreError) {
|
||||
emit('retryMore')
|
||||
}
|
||||
}
|
||||
|
||||
const handleScroll = (event: Event) => {
|
||||
const target = event.target as HTMLElement | null
|
||||
if (target && target.scrollTop + target.clientHeight >= target.scrollHeight - 8) requestMore()
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
emit('back')
|
||||
}
|
||||
@@ -351,7 +322,6 @@ const handleBack = () => {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.explain-scroll.unit-stage,
|
||||
.explain-scroll.stop-stage {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
@@ -553,17 +523,6 @@ const handleBack = () => {
|
||||
color: #83927a;
|
||||
}
|
||||
|
||||
.unit-thumb {
|
||||
flex-shrink: 0;
|
||||
width: 84px;
|
||||
height: 58px;
|
||||
border-radius: 8px;
|
||||
background: #eef0e4;
|
||||
border: 1px solid rgba(36, 49, 42, 0.06);
|
||||
box-sizing: border-box;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.hall-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
@@ -734,4 +693,21 @@ const handleBack = () => {
|
||||
border-color: #e5c2c2;
|
||||
background: #fff7f7;
|
||||
}
|
||||
|
||||
.load-more-state {
|
||||
min-height: 52px;
|
||||
padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.state-retry {
|
||||
margin-top: 12px;
|
||||
padding: 0 14px;
|
||||
font-size: 13px;
|
||||
line-height: 32px;
|
||||
color: #151713;
|
||||
background: #e0df00;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type {
|
||||
ExplainBusinessUnit,
|
||||
ExplainGuideStop,
|
||||
ExplainGuideStopPage,
|
||||
MuseumExhibit,
|
||||
MuseumHall
|
||||
} from '@/domain/museum'
|
||||
@@ -84,6 +85,11 @@ interface TimedCacheEntry<T> {
|
||||
expiresAt: number
|
||||
}
|
||||
|
||||
interface BackendCatalogStopPage {
|
||||
list?: BackendCatalogStopItem[]
|
||||
total?: number
|
||||
}
|
||||
|
||||
const requireArrayData = <T>(response: CommonResult<T[]>, message: string) => {
|
||||
if (response.code !== 0) {
|
||||
throw new Error(response.msg || message)
|
||||
@@ -92,6 +98,14 @@ const requireArrayData = <T>(response: CommonResult<T[]>, message: string) => {
|
||||
return Array.isArray(response.data) ? response.data : []
|
||||
}
|
||||
|
||||
const requirePageData = (response: CommonResult<BackendCatalogStopPage>, message: string) => {
|
||||
if (response.code !== 0) throw new Error(response.msg || message)
|
||||
return {
|
||||
list: Array.isArray(response.data?.list) ? response.data.list : [],
|
||||
total: Math.max(0, Number(response.data?.total || 0))
|
||||
}
|
||||
}
|
||||
|
||||
export class BackendExplainContentProvider implements ExplainContentProvider {
|
||||
private readonly explainExhibitCache = new Map<string, TimedCacheEntry<MuseumExhibit[]>>()
|
||||
private readonly explainExhibitInflight = new Map<string, Promise<MuseumExhibit[]>>()
|
||||
@@ -100,6 +114,8 @@ export class BackendExplainContentProvider implements ExplainContentProvider {
|
||||
private readonly hallListInflight = new Map<string, Promise<MuseumHall[]>>()
|
||||
private readonly guideStopCache = new Map<string, TimedCacheEntry<ExplainGuideStop[]>>()
|
||||
private readonly guideStopInflight = new Map<string, Promise<ExplainGuideStop[]>>()
|
||||
private readonly guideStopPageCache = new Map<string, TimedCacheEntry<ExplainGuideStopPage>>()
|
||||
private readonly guideStopPageInflight = new Map<string, Promise<ExplainGuideStopPage>>()
|
||||
private readonly outlineCache = new Map<string, TimedCacheEntry<BackendCatalogOutlineItem[]>>()
|
||||
private readonly outlineInflight = new Map<string, Promise<BackendCatalogOutlineItem[]>>()
|
||||
private readonly businessUnitCache = new Map<string, TimedCacheEntry<ExplainBusinessUnit[]>>()
|
||||
@@ -222,11 +238,51 @@ export class BackendExplainContentProvider implements ExplainContentProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private async requestCatalogStopsPageByHall(hallId: string, pageNo: number, pageSize: number): Promise<ExplainGuideStopPage> {
|
||||
const normalizedHallId = hallId.trim()
|
||||
const normalizedPageNo = Math.max(1, Math.floor(pageNo) || 1)
|
||||
const normalizedPageSize = Math.min(100, Math.max(1, Math.floor(pageSize) || 20))
|
||||
if (!normalizedHallId) return { items: [], total: 0, pageNo: normalizedPageNo, pageSize: normalizedPageSize, hasMore: false }
|
||||
|
||||
const key = cacheKey('hall', normalizedHallId, 'stops', 'page', String(normalizedPageNo), String(normalizedPageSize))
|
||||
const cached = this.getCached(this.guideStopPageCache, key)
|
||||
if (cached) return cached
|
||||
|
||||
const inflight = this.guideStopPageInflight.get(key)
|
||||
if (inflight) return inflight
|
||||
|
||||
const promise = (async () => {
|
||||
const halls = await this.requestCatalogHalls()
|
||||
const hall = halls.find((item) => item.id === normalizedHallId) || null
|
||||
const params = new URLSearchParams({ lang: catalogLang(), pageNo: String(normalizedPageNo), pageSize: String(normalizedPageSize) })
|
||||
const url = `${resolveAppApiBaseUrl()}/gis/guide/catalog/halls/${encodeURIComponent(normalizedHallId)}/stops/page?${params.toString()}`
|
||||
const data = requirePageData(await requestJson<CommonResult<BackendCatalogStopPage>>(url), '讲解对象目录加载失败')
|
||||
const items = data.list
|
||||
.map((item) => toCatalogGuideStop(item, hall, null))
|
||||
.filter(Boolean) as ExplainGuideStop[]
|
||||
const page = {
|
||||
items,
|
||||
total: data.total,
|
||||
pageNo: normalizedPageNo,
|
||||
pageSize: normalizedPageSize,
|
||||
hasMore: normalizedPageNo * normalizedPageSize < data.total
|
||||
}
|
||||
this.setCached(this.guideStopPageCache, key, page)
|
||||
return page
|
||||
})()
|
||||
|
||||
this.guideStopPageInflight.set(key, promise)
|
||||
try {
|
||||
return await promise
|
||||
} finally {
|
||||
this.guideStopPageInflight.delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
private async requestCatalogStopsByHall(hallId: string) {
|
||||
const normalizedHallId = hallId.trim()
|
||||
if (!normalizedHallId) return []
|
||||
|
||||
const key = cacheKey('hall', normalizedHallId, 'stops')
|
||||
const key = cacheKey('hall', normalizedHallId, 'stops', 'all')
|
||||
const cached = this.getCached(this.guideStopCache, key)
|
||||
if (cached) return cached
|
||||
|
||||
@@ -234,28 +290,18 @@ export class BackendExplainContentProvider implements ExplainContentProvider {
|
||||
if (inflight) return inflight
|
||||
|
||||
const promise = (async () => {
|
||||
const [halls, outlines] = await Promise.all([
|
||||
this.requestCatalogHalls(),
|
||||
this.requestCatalogOutlinesByHall(normalizedHallId).catch(() => [])
|
||||
])
|
||||
const hall = halls.find((item) => item.id === normalizedHallId) || null
|
||||
const outlineById = new Map(flattenCatalogOutlines(outlines).map((outline) => [String(outline.id || ''), outline]))
|
||||
|
||||
const params = new URLSearchParams({ lang: catalogLang() })
|
||||
const url = `${resolveAppApiBaseUrl()}/gis/guide/catalog/halls/${encodeURIComponent(normalizedHallId)}/stops?${params.toString()}`
|
||||
const response = await requestJson<CommonResult<BackendCatalogStopItem[]>>(url)
|
||||
const stops = requireArrayData(response, '讲解点目录加载失败')
|
||||
.map((item) => {
|
||||
const outline = outlineById.get(String(item.outlineId || ''))
|
||||
return toCatalogGuideStop(item, hall, outline
|
||||
? {
|
||||
id: String(outline.id || ''),
|
||||
name: String(outline.name || '')
|
||||
}
|
||||
: null)
|
||||
const stops: ExplainGuideStop[] = []
|
||||
const seen = new Set<string>()
|
||||
for (let pageNo = 1; ; pageNo += 1) {
|
||||
const page = await this.requestCatalogStopsPageByHall(normalizedHallId, pageNo, 100)
|
||||
page.items.forEach((stop) => {
|
||||
if (!seen.has(stop.id)) {
|
||||
seen.add(stop.id)
|
||||
stops.push(stop)
|
||||
}
|
||||
})
|
||||
.filter(Boolean) as ExplainGuideStop[]
|
||||
|
||||
if (!page.hasMore || stops.length >= page.total) break
|
||||
}
|
||||
this.setCached(this.guideStopCache, key, stops)
|
||||
return stops
|
||||
})()
|
||||
@@ -416,6 +462,17 @@ export class BackendExplainContentProvider implements ExplainContentProvider {
|
||||
}
|
||||
}
|
||||
|
||||
async listGuideStopsPageByHall(hallId: string, pageNo: number, pageSize: number) {
|
||||
try {
|
||||
return await this.requestCatalogStopsPageByHall(hallId, pageNo, pageSize)
|
||||
} catch (error) {
|
||||
return this.fallbackOrThrow(error, '讲解对象目录加载失败', async () => (
|
||||
this.fallbackProvider?.listGuideStopsPageByHall?.(hallId, pageNo, pageSize)
|
||||
|| { items: [], total: 0, pageNo, pageSize, hasMore: false }
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
async listExplainExhibitsByHall(hallId: string) {
|
||||
const [halls, stops] = await Promise.all([
|
||||
this.listHalls(),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type {
|
||||
ExplainBusinessUnit,
|
||||
ExplainGuideStop,
|
||||
ExplainGuideStopPage,
|
||||
ExplainTrack,
|
||||
MediaAsset,
|
||||
MuseumExhibit,
|
||||
@@ -44,6 +45,7 @@ export interface ExplainContentProvider extends MuseumContentProvider {
|
||||
listExplainExhibitsByHall?(hallId: string): Promise<MuseumExhibit[]>
|
||||
searchExplainExhibits?(keyword?: string): Promise<MuseumExhibit[]>
|
||||
listGuideStopsByHall?(hallId: string): Promise<ExplainGuideStop[]>
|
||||
listGuideStopsPageByHall?(hallId: string, pageNo: number, pageSize: number): Promise<ExplainGuideStopPage>
|
||||
listGuideStopsByBusinessUnit?(hallId: string, unitId: string): Promise<ExplainGuideStop[]>
|
||||
listTemporaryBusinessUnitsByHall?(hallId: string): Promise<ExplainBusinessUnit[]>
|
||||
listTracks(): Promise<ExplainTrack[]>
|
||||
@@ -161,6 +163,20 @@ export class StaticGuideContentProvider implements ExplainContentProvider {
|
||||
return adapter.guideStops.filter((stop) => stop.hallId === hallId)
|
||||
}
|
||||
|
||||
async listGuideStopsPageByHall(hallId: string, pageNo: number, pageSize: number) {
|
||||
const stops = await this.listGuideStopsByHall(hallId)
|
||||
const normalizedPageNo = Math.max(1, Math.floor(pageNo) || 1)
|
||||
const normalizedPageSize = Math.max(1, Math.floor(pageSize) || 20)
|
||||
const offset = (normalizedPageNo - 1) * normalizedPageSize
|
||||
return {
|
||||
items: stops.slice(offset, offset + normalizedPageSize),
|
||||
total: stops.length,
|
||||
pageNo: normalizedPageNo,
|
||||
pageSize: normalizedPageSize,
|
||||
hasMore: offset + normalizedPageSize < stops.length
|
||||
}
|
||||
}
|
||||
|
||||
async listTemporaryBusinessUnitsByHall(hallId: string) {
|
||||
const adapter = await this.loadExplainNavigationAdapter()
|
||||
return adapter.businessUnitsByHallId[hallId] || []
|
||||
|
||||
@@ -244,6 +244,14 @@ export interface ExplainGuideStop {
|
||||
playTargetId?: string
|
||||
}
|
||||
|
||||
export interface ExplainGuideStopPage {
|
||||
items: ExplainGuideStop[]
|
||||
total: number
|
||||
pageNo: number
|
||||
pageSize: number
|
||||
hasMore: boolean
|
||||
}
|
||||
|
||||
export interface ExplainBusinessUnit {
|
||||
id: string
|
||||
name: string
|
||||
|
||||
@@ -30,18 +30,10 @@
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/explain/business-unit-list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "业务单元",
|
||||
"navigationBarBackgroundColor": "#FFFFFF",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/explain/guide-stop-list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "讲解点",
|
||||
"navigationBarTitleText": "讲解对象",
|
||||
"navigationBarBackgroundColor": "#FFFFFF",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
<template>
|
||||
<GuidePageFrame
|
||||
active-tab="explain"
|
||||
variant="static"
|
||||
:show-top-tabs="false"
|
||||
@tab-change="handleTopTabChange"
|
||||
@back="handleBack"
|
||||
>
|
||||
<view class="explain-business-unit-page">
|
||||
<ExplainHallSelect
|
||||
:business-units="explainBusinessUnitItems"
|
||||
stage="unit"
|
||||
:selected-hall-name="selectedExplainHallName"
|
||||
:loading="explainLoading"
|
||||
:error="explainError"
|
||||
@business-unit-click="handleExplainBusinessUnitClick"
|
||||
@back="handleBack"
|
||||
/>
|
||||
</view>
|
||||
</GuidePageFrame>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import GuidePageFrame from '@/components/navigation/GuidePageFrame.vue'
|
||||
import ExplainHallSelect, {
|
||||
type ExplainBusinessUnitSelectItem
|
||||
} from '@/components/explain/ExplainHallSelect.vue'
|
||||
import {
|
||||
explainUseCase
|
||||
} from '@/usecases/explainUseCase'
|
||||
import type {
|
||||
ExplainBusinessUnit
|
||||
} from '@/domain/museum'
|
||||
import {
|
||||
navigateToGuideTopTab,
|
||||
type GuideTopTab
|
||||
} from '@/utils/guideTopTabs'
|
||||
|
||||
const selectedExplainHallId = ref('')
|
||||
const selectedExplainHallName = ref('')
|
||||
const explainBusinessUnits = ref<ExplainBusinessUnit[]>([])
|
||||
const explainLoading = ref(false)
|
||||
const explainError = ref('')
|
||||
|
||||
const explainBusinessUnitItems = computed<ExplainBusinessUnitSelectItem[]>(() => (
|
||||
explainBusinessUnits.value.map((unit) => ({
|
||||
id: unit.id,
|
||||
name: unit.name,
|
||||
hallId: unit.hallId,
|
||||
previewImageUrl: unit.previewImageUrl,
|
||||
guideStopCount: unit.guideStopCount
|
||||
}))
|
||||
))
|
||||
|
||||
const syncPageTitle = (title: string) => {
|
||||
uni.setNavigationBarTitle({ title })
|
||||
if (typeof document !== 'undefined') document.title = title
|
||||
}
|
||||
|
||||
onLoad(async (options: any = {}) => {
|
||||
const hallId = Array.isArray(options.hallId) ? options.hallId[0] : options.hallId
|
||||
const hallName = Array.isArray(options.hallName) ? options.hallName[0] : options.hallName
|
||||
|
||||
selectedExplainHallId.value = hallId || ''
|
||||
selectedExplainHallName.value = hallName || '业务单元'
|
||||
syncPageTitle(selectedExplainHallName.value)
|
||||
|
||||
if (!selectedExplainHallId.value) {
|
||||
explainError.value = '缺少展厅参数,请返回讲解列表后重试'
|
||||
return
|
||||
}
|
||||
|
||||
explainLoading.value = true
|
||||
try {
|
||||
explainBusinessUnits.value = await explainUseCase.loadTemporaryBusinessUnitsByHall(selectedExplainHallId.value)
|
||||
} catch (error) {
|
||||
explainError.value = '展厅讲解点加载失败,请稍后重试'
|
||||
} finally {
|
||||
explainLoading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
const handleExplainBusinessUnitClick = (unitId: string) => {
|
||||
const unit = explainBusinessUnits.value.find((item) => item.id === unitId)
|
||||
const params = new URLSearchParams({
|
||||
hallId: selectedExplainHallId.value,
|
||||
hallName: selectedExplainHallName.value,
|
||||
unitId,
|
||||
unitName: unit?.name || '讲解点'
|
||||
})
|
||||
|
||||
uni.navigateTo({
|
||||
url: `/pages/explain/guide-stop-list?${params.toString()}`
|
||||
})
|
||||
}
|
||||
|
||||
const goBackToExplainHallList = () => {
|
||||
uni.redirectTo({
|
||||
url: '/pages/explain/list',
|
||||
fail: () => {
|
||||
uni.reLaunch({ url: '/pages/explain/list' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
const pages = getCurrentPages()
|
||||
const previousPage = pages.length >= 2 ? pages[pages.length - 2] : null
|
||||
|
||||
if (
|
||||
previousPage?.route === 'pages/explain/list'
|
||||
|| previousPage?.route === 'pages/index/index'
|
||||
) {
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
fail: goBackToExplainHallList
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
goBackToExplainHallList()
|
||||
}
|
||||
|
||||
const handleTopTabChange = (tab: GuideTopTab) => {
|
||||
navigateToGuideTopTab(tab)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.explain-business-unit-page {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #edf0ea;
|
||||
}
|
||||
</style>
|
||||
@@ -1,19 +1,18 @@
|
||||
<template>
|
||||
<GuidePageFrame
|
||||
active-tab="explain"
|
||||
variant="static"
|
||||
:show-top-tabs="false"
|
||||
@tab-change="handleTopTabChange"
|
||||
@back="handleBack"
|
||||
>
|
||||
<GuidePageFrame active-tab="explain" variant="static" :show-top-tabs="false" @tab-change="handleTopTabChange" @back="handleBack">
|
||||
<view class="explain-guide-stop-page">
|
||||
<ExplainHallSelect
|
||||
:guide-stops="explainGuideStopItems"
|
||||
stage="stop"
|
||||
:selected-business-unit-name="selectedExplainBusinessUnitName"
|
||||
:selected-hall-name="selectedExplainHallName"
|
||||
:loading="explainLoading"
|
||||
:loading-more="loadingMore"
|
||||
:has-more="hasMore"
|
||||
:error="explainError"
|
||||
:load-more-error="loadMoreError"
|
||||
@guide-stop-click="handleExplainGuideStopClick"
|
||||
@retry="reloadFirstPage"
|
||||
@retry-more="loadNextPage"
|
||||
@back="handleBack"
|
||||
/>
|
||||
</view>
|
||||
@@ -21,147 +20,158 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { onUnmounted, ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import GuidePageFrame from '@/components/navigation/GuidePageFrame.vue'
|
||||
import ExplainHallSelect, {
|
||||
type ExplainGuideStopSelectItem
|
||||
} from '@/components/explain/ExplainHallSelect.vue'
|
||||
import {
|
||||
explainUseCase
|
||||
} from '@/usecases/explainUseCase'
|
||||
import type {
|
||||
ExplainGuideStop
|
||||
} from '@/domain/museum'
|
||||
import {
|
||||
normalizeExplainDetailTargetFromGuideStop
|
||||
} from '@/domain/explainDetailTarget'
|
||||
import {
|
||||
navigateToGuideTopTab,
|
||||
type GuideTopTab
|
||||
} from '@/utils/guideTopTabs'
|
||||
import ExplainHallSelect, { type ExplainGuideStopSelectItem } from '@/components/explain/ExplainHallSelect.vue'
|
||||
import { explainUseCase } from '@/usecases/explainUseCase'
|
||||
import type { ExplainGuideStop } from '@/domain/museum'
|
||||
import { normalizeExplainDetailTargetFromGuideStop } from '@/domain/explainDetailTarget'
|
||||
import { navigateToGuideTopTab, type GuideTopTab } from '@/utils/guideTopTabs'
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
const selectedExplainHallId = ref('')
|
||||
const selectedExplainHallName = ref('')
|
||||
const selectedExplainBusinessUnitId = ref('')
|
||||
const selectedExplainBusinessUnitName = ref('')
|
||||
const explainGuideStopItems = ref<ExplainGuideStopSelectItem[]>([])
|
||||
const explainLoading = ref(false)
|
||||
const loadingMore = ref(false)
|
||||
const explainError = ref('')
|
||||
const loadMoreError = ref('')
|
||||
const hasMore = ref(false)
|
||||
const nextPageNo = ref(1)
|
||||
let requestVersion = 0
|
||||
let disposed = false
|
||||
|
||||
const toGuideStopItems = (stops: ExplainGuideStop[]): ExplainGuideStopSelectItem[] => (
|
||||
stops.map((stop) => ({
|
||||
id: stop.id,
|
||||
name: stop.name,
|
||||
hallId: stop.hallId,
|
||||
hallName: stop.hallName,
|
||||
floorId: stop.floorId,
|
||||
poiId: stop.poiId,
|
||||
mapX: stop.mapX,
|
||||
mapY: stop.mapY,
|
||||
coverImageUrl: stop.coverImageUrl,
|
||||
description: stop.description,
|
||||
hasAudio: stop.hasAudio,
|
||||
audioStatus: stop.audioStatus,
|
||||
guideLevel: stop.guideLevel,
|
||||
playTargetType: stop.playTargetType,
|
||||
playTargetId: stop.playTargetId
|
||||
}))
|
||||
)
|
||||
const toGuideStopItems = (stops: ExplainGuideStop[]): ExplainGuideStopSelectItem[] => stops.map((stop) => ({
|
||||
id: stop.id,
|
||||
name: stop.name,
|
||||
hallId: stop.hallId,
|
||||
hallName: stop.hallName,
|
||||
floorId: stop.floorId,
|
||||
poiId: stop.poiId,
|
||||
mapX: stop.mapX,
|
||||
mapY: stop.mapY,
|
||||
coverImageUrl: stop.imageStatus === 'MISSING' ? undefined : stop.coverImageUrl,
|
||||
description: stop.description,
|
||||
hasAudio: stop.hasAudio,
|
||||
audioStatus: stop.audioStatus,
|
||||
guideLevel: stop.guideLevel,
|
||||
playTargetType: stop.playTargetType,
|
||||
playTargetId: stop.playTargetId
|
||||
}))
|
||||
|
||||
const syncPageTitle = (title: string) => {
|
||||
uni.setNavigationBarTitle({ title })
|
||||
if (typeof document !== 'undefined') document.title = title
|
||||
}
|
||||
|
||||
onLoad(async (options: any = {}) => {
|
||||
selectedExplainHallId.value = Array.isArray(options.hallId) ? options.hallId[0] : options.hallId || ''
|
||||
selectedExplainHallName.value = Array.isArray(options.hallName) ? options.hallName[0] : options.hallName || ''
|
||||
selectedExplainBusinessUnitId.value = Array.isArray(options.unitId) ? options.unitId[0] : options.unitId || ''
|
||||
selectedExplainBusinessUnitName.value = Array.isArray(options.unitName) ? options.unitName[0] : options.unitName || '讲解点'
|
||||
const loadPage = async (pageNo: number, replace: boolean) => {
|
||||
const hallId = selectedExplainHallId.value
|
||||
if (!hallId || disposed || (!replace && (loadingMore.value || !hasMore.value))) return
|
||||
const version = ++requestVersion
|
||||
if (replace) {
|
||||
explainLoading.value = true
|
||||
explainError.value = ''
|
||||
loadMoreError.value = ''
|
||||
} else {
|
||||
loadingMore.value = true
|
||||
loadMoreError.value = ''
|
||||
}
|
||||
try {
|
||||
const page = await explainUseCase.listGuideStopsPageByHall(hallId, pageNo, PAGE_SIZE)
|
||||
if (disposed || version !== requestVersion || hallId !== selectedExplainHallId.value) return
|
||||
const incoming = toGuideStopItems(page.items)
|
||||
if (replace) {
|
||||
explainGuideStopItems.value = incoming
|
||||
} else {
|
||||
const existingIds = new Set(explainGuideStopItems.value.map((item) => item.id))
|
||||
explainGuideStopItems.value = [...explainGuideStopItems.value, ...incoming.filter((item) => !existingIds.has(item.id))]
|
||||
}
|
||||
nextPageNo.value = page.pageNo + 1
|
||||
hasMore.value = page.hasMore && explainGuideStopItems.value.length < page.total
|
||||
} catch (error) {
|
||||
if (disposed || version !== requestVersion) return
|
||||
if (replace) explainError.value = '讲解对象加载失败,请稍后重试'
|
||||
else loadMoreError.value = '加载更多讲解对象失败,请重试'
|
||||
} finally {
|
||||
if (!disposed && version === requestVersion) {
|
||||
explainLoading.value = false
|
||||
loadingMore.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
syncPageTitle(selectedExplainBusinessUnitName.value)
|
||||
const reloadFirstPage = () => {
|
||||
if (!selectedExplainHallId.value) return
|
||||
nextPageNo.value = 1
|
||||
hasMore.value = false
|
||||
void loadPage(1, true)
|
||||
}
|
||||
|
||||
if (!selectedExplainHallId.value || !selectedExplainBusinessUnitId.value) {
|
||||
explainError.value = '缺少业务单元参数,请返回上一级后重试'
|
||||
const loadNextPage = () => void loadPage(nextPageNo.value, false)
|
||||
|
||||
onLoad((options: Record<string, string | string[] | undefined> = {}) => {
|
||||
const first = (value: string | string[] | undefined) => Array.isArray(value) ? value[0] : value
|
||||
selectedExplainHallId.value = first(options.hallId) || ''
|
||||
selectedExplainHallName.value = first(options.hallName) || '讲解对象'
|
||||
syncPageTitle('讲解对象')
|
||||
if (!selectedExplainHallId.value) {
|
||||
explainError.value = '缺少展厅参数,请返回展厅列表后重试'
|
||||
return
|
||||
}
|
||||
reloadFirstPage()
|
||||
})
|
||||
|
||||
explainLoading.value = true
|
||||
try {
|
||||
const unit = await explainUseCase.selectBusinessUnit(
|
||||
selectedExplainHallId.value,
|
||||
selectedExplainBusinessUnitId.value
|
||||
)
|
||||
if (!unit) {
|
||||
explainError.value = '未找到对应业务单元,请返回上一级后重试'
|
||||
return
|
||||
}
|
||||
selectedExplainBusinessUnitName.value = unit.name || selectedExplainBusinessUnitName.value
|
||||
syncPageTitle(selectedExplainBusinessUnitName.value)
|
||||
const stops = await explainUseCase.listGuideStopsByBusinessUnit(
|
||||
selectedExplainHallId.value,
|
||||
selectedExplainBusinessUnitId.value
|
||||
)
|
||||
explainGuideStopItems.value = toGuideStopItems(stops)
|
||||
} catch (error) {
|
||||
explainError.value = '讲解点加载失败,请稍后重试'
|
||||
} finally {
|
||||
explainLoading.value = false
|
||||
}
|
||||
onUnmounted(() => {
|
||||
disposed = true
|
||||
requestVersion += 1
|
||||
})
|
||||
|
||||
const handleExplainGuideStopClick = (stop: ExplainGuideStopSelectItem) => {
|
||||
const target = stop.playTargetType && stop.playTargetId
|
||||
? { targetType: stop.playTargetType, targetId: stop.playTargetId }
|
||||
: normalizeExplainDetailTargetFromGuideStop({ id: stop.id })
|
||||
|
||||
const params = new URLSearchParams({
|
||||
id: stop.id,
|
||||
tab: 'explain',
|
||||
targetType: target.targetType,
|
||||
targetId: target.targetId
|
||||
})
|
||||
const params = new URLSearchParams({ id: stop.id, tab: 'explain', targetType: target.targetType, targetId: target.targetId })
|
||||
if (stop.hallId) params.set('hallId', stop.hallId)
|
||||
if (stop.hallName) params.set('hallName', stop.hallName)
|
||||
if (stop.floorId) params.set('floorId', stop.floorId)
|
||||
if (stop.poiId) params.set('poiId', stop.poiId)
|
||||
|
||||
uni.navigateTo({
|
||||
url: `/pages/exhibit/detail?${params.toString()}`
|
||||
})
|
||||
uni.navigateTo({ url: `/pages/exhibit/detail?${params.toString()}` })
|
||||
}
|
||||
|
||||
const goBackToBusinessUnitList = () => {
|
||||
const params = new URLSearchParams({
|
||||
hallId: selectedExplainHallId.value,
|
||||
hallName: selectedExplainHallName.value
|
||||
})
|
||||
const fallbackToHallList = () => uni.reLaunch({ url: '/pages/explain/list' })
|
||||
|
||||
uni.redirectTo({
|
||||
url: `/pages/explain/business-unit-list?${params.toString()}`,
|
||||
fail: () => {
|
||||
uni.reLaunch({ url: `/pages/explain/business-unit-list?${params.toString()}` })
|
||||
}
|
||||
})
|
||||
type ExplainPageStackEntry = {
|
||||
route?: string
|
||||
options?: {
|
||||
tab?: string | string[]
|
||||
}
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
fail: goBackToBusinessUnitList
|
||||
})
|
||||
const returnToHallList = () => {
|
||||
const pages = getCurrentPages()
|
||||
const previousPage = pages[pages.length - 2] as ExplainPageStackEntry | undefined
|
||||
const previousRoute = previousPage?.route
|
||||
const previousTab = Array.isArray(previousPage?.options?.tab)
|
||||
? previousPage.options.tab[0]
|
||||
: previousPage?.options?.tab
|
||||
const canNavigateBackToHallList = (
|
||||
previousRoute === 'pages/explain/list'
|
||||
|| (previousRoute === 'pages/index/index' && previousTab === 'explain')
|
||||
)
|
||||
|
||||
if (canNavigateBackToHallList) {
|
||||
uni.navigateBack({ delta: 1, fail: fallbackToHallList })
|
||||
return
|
||||
}
|
||||
|
||||
fallbackToHallList()
|
||||
}
|
||||
|
||||
const handleTopTabChange = (tab: GuideTopTab) => {
|
||||
navigateToGuideTopTab(tab)
|
||||
}
|
||||
const handleBack = returnToHallList
|
||||
const handleTopTabChange = (tab: GuideTopTab) => navigateToGuideTopTab(tab)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.explain-guide-stop-page {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #edf0ea;
|
||||
}
|
||||
.explain-guide-stop-page { position: absolute; inset: 0; background: #edf0ea; }
|
||||
</style>
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
type GuideTopTab
|
||||
} from '@/utils/guideTopTabs'
|
||||
import { isEmbeddedInWechatMiniProgram } from '@/utils/hostEnvironment'
|
||||
import { explainGuideStopListUrl } from '@/utils/explainNavigation'
|
||||
|
||||
const explainHallItems = ref<ExplainHallSelectItem[]>([])
|
||||
const explainLoading = ref(false)
|
||||
@@ -56,7 +57,6 @@ const buildExplainHallItems = (
|
||||
floorLabel: hall.floorLabel,
|
||||
image: hall.image,
|
||||
explainCount: hall.exhibitCount || 0,
|
||||
businessUnitCount: summaries[hall.id]?.businessUnitCount,
|
||||
guideStopCount: summaries[hall.id]?.guideStopCount,
|
||||
searchText: normalizeExplainKeyword([
|
||||
hall.name,
|
||||
@@ -225,13 +225,8 @@ const handleTopTabChange = (tab: GuideTopTab) => {
|
||||
|
||||
const handleExplainHallClick = (hallId: string) => {
|
||||
const hall = explainHallItems.value.find((item) => item.id === hallId)
|
||||
const params = new URLSearchParams({
|
||||
hallId,
|
||||
hallName: hall?.name || '讲解'
|
||||
})
|
||||
|
||||
uni.navigateTo({
|
||||
url: `/pages/explain/business-unit-list?${params.toString()}`
|
||||
url: explainGuideStopListUrl(hallId, hall?.name || '讲解')
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -302,6 +302,7 @@ import type {
|
||||
import ExplainHallSelect, {
|
||||
type ExplainHallSelectItem
|
||||
} from '@/components/explain/ExplainHallSelect.vue'
|
||||
import { explainGuideStopListUrl } from '@/utils/explainNavigation'
|
||||
import {
|
||||
getLastGuideLocationPreviewUrl,
|
||||
isGuideTopTab,
|
||||
@@ -990,7 +991,6 @@ const buildExplainHallItems = (
|
||||
floorLabel: hall.floorLabel,
|
||||
image: hall.image,
|
||||
explainCount: hall.exhibitCount || 0,
|
||||
businessUnitCount: summaries[hall.id]?.businessUnitCount,
|
||||
guideStopCount: summaries[hall.id]?.guideStopCount,
|
||||
searchText: normalizeExplainKeyword(keywordParts.join(' '))
|
||||
}
|
||||
@@ -1725,13 +1725,8 @@ const guideSearchText = computed(() => {
|
||||
// 讲解页面处理
|
||||
const handleExplainHallClick = async (hallId: string) => {
|
||||
const hall = explainHallItems.value.find((item) => item.id === hallId)
|
||||
const params = new URLSearchParams({
|
||||
hallId,
|
||||
hallName: hall?.name || '讲解'
|
||||
})
|
||||
|
||||
uni.navigateTo({
|
||||
url: `/pages/explain/business-unit-list?${params.toString()}`
|
||||
url: explainGuideStopListUrl(hallId, hall?.name || '讲解')
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type {
|
||||
ExplainBusinessUnit,
|
||||
ExplainGuideStop,
|
||||
ExplainGuideStopPage,
|
||||
ExplainTrack,
|
||||
MuseumExhibit,
|
||||
MuseumHall,
|
||||
@@ -27,6 +28,7 @@ export interface ExplainRepository {
|
||||
listHalls(): Promise<MuseumHall[]>
|
||||
getHallById(id: string): Promise<MuseumHall | null>
|
||||
listGuideStopsByHall(hallId: string): Promise<ExplainGuideStop[]>
|
||||
listGuideStopsPageByHall(hallId: string, pageNo: number, pageSize: number): Promise<ExplainGuideStopPage>
|
||||
listGuideStopsByBusinessUnit(hallId: string, unitId: string): Promise<ExplainGuideStop[]>
|
||||
listTemporaryBusinessUnitsByHall(hallId: string): Promise<ExplainBusinessUnit[]>
|
||||
listTracks(): Promise<ExplainTrack[]>
|
||||
@@ -73,7 +75,36 @@ export class DefaultExplainRepository implements ExplainRepository {
|
||||
}
|
||||
|
||||
async listGuideStopsByHall(hallId: string) {
|
||||
return this.explainContent.listGuideStopsByHall?.(hallId) || []
|
||||
const pageSize = 100
|
||||
const items: ExplainGuideStop[] = []
|
||||
const seen = new Set<string>()
|
||||
for (let pageNo = 1; ; pageNo += 1) {
|
||||
const page = await this.listGuideStopsPageByHall(hallId, pageNo, pageSize)
|
||||
page.items.forEach((item) => {
|
||||
if (!seen.has(item.id)) {
|
||||
seen.add(item.id)
|
||||
items.push(item)
|
||||
}
|
||||
})
|
||||
if (!page.hasMore || items.length >= page.total) return items
|
||||
}
|
||||
}
|
||||
|
||||
async listGuideStopsPageByHall(hallId: string, pageNo: number, pageSize: number) {
|
||||
if (this.explainContent.listGuideStopsPageByHall) {
|
||||
return this.explainContent.listGuideStopsPageByHall(hallId, pageNo, pageSize)
|
||||
}
|
||||
const items = await this.explainContent.listGuideStopsByHall?.(hallId) || []
|
||||
const normalizedPageNo = Math.max(1, Math.floor(pageNo) || 1)
|
||||
const normalizedPageSize = Math.max(1, Math.floor(pageSize) || 20)
|
||||
const offset = (normalizedPageNo - 1) * normalizedPageSize
|
||||
return {
|
||||
items: items.slice(offset, offset + normalizedPageSize),
|
||||
total: items.length,
|
||||
pageNo: normalizedPageNo,
|
||||
pageSize: normalizedPageSize,
|
||||
hasMore: offset + normalizedPageSize < items.length
|
||||
}
|
||||
}
|
||||
|
||||
async listGuideStopsByBusinessUnit(hallId: string, unitId: string) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type {
|
||||
AudioPlayTargetType,
|
||||
ExplainBusinessUnit,
|
||||
ExplainGuideStop,
|
||||
ExplainGuideStopPage,
|
||||
ExplainTrack,
|
||||
MediaAsset,
|
||||
MuseumExhibit,
|
||||
@@ -67,7 +68,6 @@ export interface ExhibitDetailAudioOptions {
|
||||
|
||||
export interface ExplainHallSummary {
|
||||
hallId: string
|
||||
businessUnitCount: number
|
||||
guideStopCount: number
|
||||
}
|
||||
|
||||
@@ -487,24 +487,20 @@ export class ExplainUseCase {
|
||||
if (hall && (typeof hall.outlineCount === 'number' || typeof hall.stopCount === 'number')) {
|
||||
return [hallId, {
|
||||
hallId,
|
||||
businessUnitCount: hall.outlineCount || 0,
|
||||
guideStopCount: hall.stopCount || 0
|
||||
}] as const
|
||||
}
|
||||
|
||||
try {
|
||||
const units = await this.loadTemporaryBusinessUnitsByHall(hallId)
|
||||
const guideStopCount = units.reduce((total, unit) => total + unit.guideStopCount, 0)
|
||||
const page = await this.listGuideStopsPageByHall(hallId, 1, 1)
|
||||
return [hallId, {
|
||||
hallId,
|
||||
businessUnitCount: units.length,
|
||||
guideStopCount
|
||||
guideStopCount: page.total
|
||||
}] as const
|
||||
} catch (error) {
|
||||
console.warn('讲解展厅统计加载失败:', hallId, error)
|
||||
return [hallId, {
|
||||
hallId,
|
||||
businessUnitCount: 0,
|
||||
guideStopCount: 0
|
||||
}] as const
|
||||
}
|
||||
@@ -513,6 +509,10 @@ export class ExplainUseCase {
|
||||
return Object.fromEntries(entries)
|
||||
}
|
||||
|
||||
listGuideStopsPageByHall(hallId: string, pageNo: number, pageSize: number): Promise<ExplainGuideStopPage> {
|
||||
return this.explain.listGuideStopsPageByHall(hallId, pageNo, pageSize)
|
||||
}
|
||||
|
||||
getHallById(id: string) {
|
||||
return this.explain.getHallById(id)
|
||||
}
|
||||
|
||||
4
src/utils/explainNavigation.ts
Normal file
4
src/utils/explainNavigation.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const explainGuideStopListUrl = (hallId: string, hallName = '讲解') => {
|
||||
const params = new URLSearchParams({ hallId, hallName })
|
||||
return `/pages/explain/guide-stop-list?${params.toString()}`
|
||||
}
|
||||
Reference in New Issue
Block a user