Files
frontend-miniapp/src/components/explain/ExplainHallSelect.vue
lyf 9efcef5190 feat: 临时改造讲解页方案 B 链路
接入展厅列表、展厅讲解点分组生成临时业务单元,并迁移讲解详情播放正文到新接口链路。
2026-07-02 00:51:27 +08:00

490 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="explain-hall-select">
<view class="explain-page-header">
<view class="header-back" @tap="handleBack">
<text class="header-back-icon"></text>
<text class="header-back-text">返回</text>
</view>
<text class="header-title">{{ headerTitle }}</text>
</view>
<scroll-view
class="explain-scroll"
scroll-y
:show-scrollbar="false"
>
<view v-if="loading" class="state-block">
<text class="state-title">{{ loadingTitle }}</text>
<text class="state-desc">{{ loadingDescription }}</text>
</view>
<view v-else-if="error" class="state-block error">
<text class="state-title">讲解内容加载失败</text>
<text class="state-desc">{{ error }}</text>
</view>
<template v-else>
<view v-if="stage === 'hall' && filteredHalls.length" class="hall-list">
<view
v-for="hall in filteredHalls"
:key="hall.id"
class="hall-card"
@tap="handleHallClick(hall.id)"
>
<image
v-if="hallIconUrl(hall)"
class="hall-thumb"
:src="hallIconUrl(hall)"
mode="aspectFit"
/>
<view v-else class="hall-thumb placeholder">
<text class="hall-thumb-text">{{ hallIconText(hall.name) }}</text>
</view>
<view class="hall-main">
<text class="hall-name">{{ hall.name }}</text>
<view class="hall-meta-row">
<view class="floor-badge">
<text class="floor-badge-text">{{ hall.floorLabel || '楼层待补充' }}</text>
</view>
<text class="hall-meta-text">{{ hall.explainCount > 0 ? `${hall.explainCount} 条讲解` : '暂无讲解' }}</text>
</view>
</view>
<text class="hall-arrow"></text>
</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)"
>
<view class="unit-mark">
<text class="unit-mark-text">{{ unit.name.slice(0, 1) || '讲' }}</text>
</view>
<view class="hall-main">
<text class="hall-name">{{ unit.name }}</text>
<view class="hall-meta-row">
<view class="floor-badge">
<text class="floor-badge-text">业务单元</text>
</view>
<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"
:key="stop.id"
class="hall-card stop-card"
@tap="handleGuideStopClick(stop.id)"
>
<image
v-if="stop.coverImageUrl"
class="hall-thumb"
:src="stop.coverImageUrl"
mode="aspectFill"
/>
<view v-else class="hall-thumb placeholder">
<text class="hall-thumb-text">{{ hallIconText(stop.name) }}</text>
</view>
<view class="hall-main">
<text class="hall-name">{{ stop.name }}</text>
<text v-if="stop.description" class="stop-desc">{{ stop.description }}</text>
<view class="hall-meta-row">
<view class="floor-badge">
<text class="floor-badge-text">{{ stop.hasAudio ? '可讲解' : '图文' }}</text>
</view>
<text class="hall-meta-text">{{ stop.floorId || selectedHallName || '楼层待补充' }}</text>
</view>
</view>
<text class="hall-arrow"></text>
</view>
</view>
<view v-else class="state-block empty">
<text class="state-title">{{ emptyTitle }}</text>
<text class="state-desc">{{ emptyDescription }}</text>
</view>
</template>
</scroll-view>
</view>
</template>
<script setup lang="ts">
import { computed } from 'vue'
export interface ExplainHallSelectItem {
id: string
name: string
floorLabel?: string
image?: string
explainCount: number
searchText: string
}
export interface ExplainBusinessUnitSelectItem {
id: string
name: string
hallId: string
guideStopCount: number
}
export interface ExplainGuideStopSelectItem {
id: string
name: string
hallId?: string
hallName?: string
floorId?: string
coverImageUrl?: string
description?: string
hasAudio?: boolean
}
export type ExplainSelectStage = 'hall' | 'unit' | 'stop'
const props = withDefaults(defineProps<{
halls?: ExplainHallSelectItem[]
businessUnits?: ExplainBusinessUnitSelectItem[]
guideStops?: ExplainGuideStopSelectItem[]
stage?: ExplainSelectStage
selectedHallName?: string
selectedBusinessUnitName?: string
loading?: boolean
error?: string
}>(), {
halls: () => [],
businessUnits: () => [],
guideStops: () => [],
stage: 'hall',
selectedHallName: '',
selectedBusinessUnitName: '',
loading: false,
error: ''
})
const emit = defineEmits<{
hallClick: [hallId: string]
businessUnitClick: [unitId: string]
guideStopClick: [stopId: string]
back: []
}>()
const HALL_ICON_BASE = '/static/icons/halls'
const hallIconMap: Record<string, string> = {
宇宙厅: `${HALL_ICON_BASE}/universe.jpg`,
地球厅: `${HALL_ICON_BASE}/earth.jpg`,
演化厅: `${HALL_ICON_BASE}/evolution.jpg`,
恐龙厅: `${HALL_ICON_BASE}/dinosaur.jpg`,
人类厅: `${HALL_ICON_BASE}/human.jpg`,
动物厅: `${HALL_ICON_BASE}/animal.jpg`,
生物厅: `${HALL_ICON_BASE}/biology.jpg`,
生态厅: `${HALL_ICON_BASE}/ecology.jpg`,
家园厅: `${HALL_ICON_BASE}/homeland.jpg`
}
const filteredHalls = computed(() => props.halls)
const headerTitle = computed(() => {
if (props.stage === 'unit') return props.selectedHallName || '选择业务单元'
if (props.stage === 'stop') return props.selectedBusinessUnitName || '选择讲解点'
return '讲解'
})
const loadingTitle = computed(() => {
if (props.stage === 'unit') return '正在加载业务单元'
if (props.stage === 'stop') return '正在加载讲解点'
return '正在加载展厅讲解'
})
const loadingDescription = computed(() => {
if (props.stage === 'unit') return '正在按讲解点所属 outline 分组生成临时业务单元。'
if (props.stage === 'stop') return '稍后将展示该业务单元下的讲解点。'
return '稍后将展示可选择的展厅。'
})
const emptyTitle = computed(() => {
if (props.stage === 'unit') return '该展厅暂无已标定讲解点'
if (props.stage === 'stop') return '该业务单元暂无讲解点'
return '未找到相关展厅'
})
const emptyDescription = computed(() => {
if (props.stage === 'unit') return '方案 B 只展示已在地图上标定的讲解点。'
if (props.stage === 'stop') return '可返回选择其他业务单元。'
return '暂无可选择的展厅讲解。'
})
const hallIconUrl = (hall: ExplainHallSelectItem) => {
return hallIconMap[hall.name.trim()] || hall.image || ''
}
const hallIconText = (name: string) => name.trim().slice(0, 1) || '讲'
const handleHallClick = (hallId: string) => {
emit('hallClick', hallId)
}
const handleBusinessUnitClick = (unitId: string) => {
emit('businessUnitClick', unitId)
}
const handleGuideStopClick = (stopId: string) => {
emit('guideStopClick', stopId)
}
const handleBack = () => {
emit('back')
}
</script>
<style scoped lang="scss">
.explain-hall-select {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
background: #f9fafb;
}
.explain-scroll {
height: 100%;
padding: 76px 20px calc(112px + env(safe-area-inset-bottom));
box-sizing: border-box;
}
.state-title,
.state-desc,
.hall-name,
.hall-meta-text {
display: block;
}
.explain-page-header {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 56px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 20px;
box-sizing: border-box;
background: #ffffff;
border-bottom: 1px solid #eceee8;
z-index: 2001;
}
.header-back {
position: absolute;
left: 12px;
top: 0;
height: 56px;
width: 82px;
display: flex;
align-items: center;
gap: 2px;
z-index: 1;
}
.header-back-icon {
font-size: 27px;
line-height: 27px;
color: #151713;
}
.header-back-text {
font-size: 15px;
line-height: 21px;
font-weight: 500;
color: #151713;
}
.header-title {
max-width: calc(100% - 188px);
padding: 0 8px;
box-sizing: border-box;
font-size: 18px;
line-height: 25px;
font-weight: 800;
color: #151713;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.hall-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.hall-card {
min-height: 112px;
padding: 14px 13px 14px 16px;
display: flex;
align-items: center;
gap: 14px;
box-sizing: border-box;
background: #ffffff;
border: 1px solid #e4e6df;
border-radius: 8px;
box-shadow: 0 8px 18px rgba(36, 49, 42, 0.05);
}
.hall-card:active {
background: #f7f8f3;
}
.hall-thumb {
flex-shrink: 0;
width: 68px;
height: 68px;
border-radius: 8px;
background: #ffffff;
border: 1px solid rgba(36, 49, 42, 0.06);
box-sizing: border-box;
object-fit: contain;
}
.hall-thumb.placeholder {
display: flex;
align-items: center;
justify-content: center;
}
.hall-thumb-text {
font-size: 24px;
line-height: 32px;
font-weight: 800;
color: #83927a;
}
.unit-mark {
flex-shrink: 0;
width: 68px;
height: 68px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
background: #151713;
}
.unit-mark-text {
font-size: 22px;
line-height: 30px;
font-weight: 800;
color: var(--museum-accent);
}
.hall-main {
flex: 1;
min-width: 0;
}
.stop-desc {
display: block;
margin-top: 5px;
font-size: 12px;
line-height: 18px;
color: #68725d;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.hall-name {
font-size: 17px;
line-height: 24px;
font-weight: 800;
color: #151713;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.hall-meta-row {
margin-top: 10px;
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.floor-badge {
flex-shrink: 0;
min-width: 28px;
height: 24px;
padding: 0 6px;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
background: #ffffff;
border: 1px solid #e0df00;
border-radius: 6px;
}
.floor-badge-text {
font-size: 13px;
line-height: 17px;
font-weight: 600;
color: #aaa900;
}
.hall-meta-text {
min-width: 0;
font-size: 15px;
line-height: 21px;
color: #555c51;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.hall-arrow {
flex-shrink: 0;
font-size: 30px;
line-height: 30px;
color: #151713;
}
.state-block {
margin: 34px 0;
padding: 22px 16px;
box-sizing: border-box;
text-align: center;
background: #ffffff;
border: 1px solid #e4e6df;
border-radius: 8px;
}
.state-title {
font-size: 15px;
line-height: 21px;
font-weight: 700;
color: #151713;
}
.state-desc {
margin-top: 6px;
font-size: 12px;
line-height: 18px;
color: #68725d;
}
.state-block.error {
border-color: #e5c2c2;
background: #fff7f7;
}
</style>