同步 sgs-frontend-mobile 源码
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
lyf
2026-07-27 11:26:01 +08:00
parent 575dca430f
commit 9ea1bfab71
181 changed files with 24395 additions and 5212 deletions

View File

@@ -12,6 +12,7 @@
class="explain-scroll"
:class="{
'hall-stage': stage === 'hall',
'unit-stage': stage === 'unit',
'stop-stage': stage === 'stop'
}"
scroll-y
@@ -19,10 +20,11 @@
@scrolltolower="handleScrollToLower"
:show-scrollbar="false"
>
<view v-if="loading" class="state-block">
<text class="state-title">{{ loadingTitle }}</text>
<text class="state-desc">{{ loadingDescription }}</text>
</view>
<GuideLoadingState
v-if="loading"
:title="loadingTitle"
:description="loadingDescription"
/>
<view v-else-if="error" class="state-block error">
<text class="state-title">讲解对象加载失败</text>
@@ -39,35 +41,57 @@
:style="{ '--hall-card-color': hallCardTheme(hall).color }"
@tap="handleHallClick(hall.id)"
>
<view
v-if="hallPreviewUrl(hall)"
class="hall-card-art hall-thumb-shell"
:class="{
'is-image-loaded': isHallPreviewLoaded(hall.id),
'is-image-error': isHallPreviewError(hall.id)
}"
>
<view class="hall-thumb-fallback">
<text class="hall-thumb-text">{{ hallIconText(hall.name) }}</text>
<view
v-if="isCustomHallCard(hall)"
class="hall-full-card-art"
:class="{
'is-image-loaded': isHallPreviewLoaded(hall.id),
'is-image-error': isHallPreviewError(hall.id)
}"
>
<image
v-if="!isHallPreviewError(hall.id)"
class="hall-full-card-image"
:src="hallPreviewUrl(hall)"
mode="aspectFill"
:lazy-load="isHallPreviewLazy(index)"
@load="handleHallPreviewLoad(hall.id)"
@error="handleHallPreviewError(hall.id)"
/>
<view v-else class="hall-full-card-fallback">
<text class="hall-name">{{ hall.name }}</text>
</view>
</view>
<image
v-if="!isHallPreviewError(hall.id)"
class="hall-card-art-image"
:src="hallPreviewUrl(hall)"
mode="aspectFit"
:lazy-load="isHallPreviewLazy(index)"
@load="handleHallPreviewLoad(hall.id)"
@error="handleHallPreviewError(hall.id)"
/>
<template v-else>
<view
v-if="hallPreviewUrl(hall)"
class="hall-card-art hall-thumb-shell"
:class="{
'is-image-loaded': isHallPreviewLoaded(hall.id),
'is-image-error': isHallPreviewError(hall.id)
}"
>
<view class="hall-thumb-fallback">
<text class="hall-thumb-text">{{ hallIconText(hall.name) }}</text>
</view>
<image
v-if="!isHallPreviewError(hall.id)"
class="hall-card-art-image"
:src="hallPreviewUrl(hall)"
mode="aspectFit"
:lazy-load="isHallPreviewLazy(index)"
@load="handleHallPreviewLoad(hall.id)"
@error="handleHallPreviewError(hall.id)"
/>
</view>
<view v-else class="hall-card-art placeholder">
<text class="hall-thumb-text">{{ hallIconText(hall.name) }}</text>
</view>
<view class="hall-overview-copy">
<text class="hall-name">{{ hall.name }}</text>
</view>
</template>
</view>
<view v-else class="hall-card-art placeholder">
<text class="hall-thumb-text">{{ hallIconText(hall.name) }}</text>
</view>
<view class="hall-overview-copy">
<text class="hall-name">{{ hall.name }}</text>
</view>
</view>
</view>
<view v-else-if="stage === 'stop' && guideStops.length" class="hall-list">
@@ -106,6 +130,34 @@
</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 business-unit-card"
@tap="handleBusinessUnitClick(unit.id)"
>
<image
v-if="unit.previewImageUrl"
class="hall-thumb"
:src="unit.previewImageUrl"
mode="aspectFit"
/>
<view v-else class="hall-thumb placeholder">
<text class="hall-thumb-text">{{ hallIconText(unit.name) }}</text>
</view>
<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 class="state-block empty">
<text class="state-title">{{ emptyTitle }}</text>
<text class="state-desc">{{ emptyDescription }}</text>
@@ -126,6 +178,7 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { isEmbeddedInWechatMiniProgram } from '@/utils/hostEnvironment'
import GuideLoadingState from '@/components/navigation/GuideLoadingState.vue'
export interface ExplainHallSelectItem {
id: string
@@ -155,10 +208,19 @@ export interface ExplainGuideStopSelectItem {
playTargetId?: string
}
export type ExplainSelectStage = 'hall' | 'stop'
export interface ExplainBusinessUnitSelectItem {
id: string
name: string
hallId?: string
previewImageUrl?: string
guideStopCount: number
}
export type ExplainSelectStage = 'hall' | 'unit' | 'stop'
const props = withDefaults(defineProps<{
halls?: ExplainHallSelectItem[]
businessUnits?: ExplainBusinessUnitSelectItem[]
guideStops?: ExplainGuideStopSelectItem[]
stage?: ExplainSelectStage
selectedHallName?: string
@@ -170,6 +232,7 @@ const props = withDefaults(defineProps<{
forceInternalHeader?: boolean
}>(), {
halls: () => [],
businessUnits: () => [],
guideStops: () => [],
stage: 'hall',
selectedHallName: '',
@@ -183,27 +246,28 @@ const props = withDefaults(defineProps<{
const emit = defineEmits<{
hallClick: [hallId: string]
businessUnitClick: [unitId: string]
guideStopClick: [stop: ExplainGuideStopSelectItem]
back: []
retry: []
retryMore: []
}>()
const HALL_PREVIEW_BASE = '/static/icons/halls'
const HALL_PREVIEW_BASE = '/static/icons/halls/custom-cards'
const HALL_PREVIEW_EAGER_COUNT = 4
const loadedHallPreviewIds = ref(new Set<string>())
const failedHallPreviewIds = ref(new Set<string>())
const hallPreviewMap: Record<string, string> = {
宇宙厅: `${HALL_PREVIEW_BASE}/normalized/universe.png`,
地球厅: `${HALL_PREVIEW_BASE}/normalized/earth.png`,
演化厅: `${HALL_PREVIEW_BASE}/normalized/evolution.png`,
恐龙厅: `${HALL_PREVIEW_BASE}/normalized/dinosaur.png`,
人类厅: `${HALL_PREVIEW_BASE}/normalized/human.png`,
动物厅: `${HALL_PREVIEW_BASE}/normalized/biology.png`,
生物厅: `${HALL_PREVIEW_BASE}/normalized/biology.png`,
生态厅: `${HALL_PREVIEW_BASE}/normalized/ecology.png`,
家园厅: `${HALL_PREVIEW_BASE}/normalized/homeland.png`
宇宙厅: `${HALL_PREVIEW_BASE}/universe.png`,
地球厅: `${HALL_PREVIEW_BASE}/earth.png`,
演化厅: `${HALL_PREVIEW_BASE}/evolution.png`,
恐龙厅: `${HALL_PREVIEW_BASE}/dinosaur.png`,
人类厅: `${HALL_PREVIEW_BASE}/human.png`,
动物厅: `${HALL_PREVIEW_BASE}/biology.png`,
生物厅: `${HALL_PREVIEW_BASE}/biology.png`,
生态厅: `${HALL_PREVIEW_BASE}/ecology.png`,
家园厅: `${HALL_PREVIEW_BASE}/homeland.png`
}
const hallCardThemeMap: Record<string, { color: string; englishName: string }> = {
@@ -223,29 +287,36 @@ const shouldUseHostNavigation = computed(() => isEmbeddedInWechatMiniProgram())
const showInternalHeader = computed(() => props.forceInternalHeader || !shouldUseHostNavigation.value)
const headerTitle = computed(() => {
if (props.stage === 'stop') return props.selectedHallName || '讲解对象'
if (props.stage === 'unit') return props.selectedHallName || '讲解单元'
return '免费讲解'
})
const loadingTitle = computed(() => {
if (props.stage === 'stop') return '正在加载讲解对象'
return '正在加载展厅讲解'
if (props.stage === 'unit') return '正在加载讲解单元'
return '正在加载免费讲解'
})
const loadingDescription = computed(() => {
if (props.stage === 'stop') return '稍后将展示该展厅的讲解对象。'
if (props.stage === 'unit') return '稍后将展示该展厅的讲解单元。'
return '稍后将展示可选择的展厅。'
})
const emptyTitle = computed(() => {
if (props.stage === 'stop') return '该展厅暂无讲解对象'
if (props.stage === 'unit') return '该展厅暂无讲解单元'
return '未找到相关展厅'
})
const emptyDescription = computed(() => {
if (props.stage === 'stop') return '可返回选择其他展厅。'
return '暂无可选择的展厅讲解。'
if (props.stage === 'unit') return '可返回选择其他展厅。'
return '暂无可选择的免费讲解。'
})
const hallPreviewUrl = (hall: ExplainHallSelectItem) => {
return hallPreviewMap[hall.name.trim()] || hall.image || ''
}
const isCustomHallCard = (hall: ExplainHallSelectItem) => Boolean(hallPreviewMap[hall.name.trim()])
const hallIconText = (name: string) => name.trim().slice(0, 1) || '讲'
const isHallPreviewLoaded = (hallId: string) => loadedHallPreviewIds.value.has(hallId)
@@ -280,6 +351,10 @@ 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) {
@@ -831,11 +906,13 @@ const handleBack = () => {
}
.hall-overview-list {
grid-auto-rows: 138px;
// Keep the eight-hall overview balanced on tall phones while preserving a
// fixed minimum row height for compact screens.
grid-auto-rows: max(138px, calc((100vh - 148px) / 4));
}
.hall-overview-card {
min-height: 138px;
min-height: 0;
}
.hall-overview-card .hall-card-art {
@@ -850,6 +927,28 @@ const handleBack = () => {
filter: grayscale(1) brightness(0.72) contrast(1.65);
}
.hall-full-card-art {
position: absolute;
inset: 0;
overflow: hidden;
background: var(--hall-card-color);
}
.hall-full-card-image {
display: block;
width: 100%;
height: 100%;
}
.hall-full-card-fallback {
position: absolute;
inset: 0;
display: flex;
align-items: center;
padding: 24px;
box-sizing: border-box;
}
.hall-overview-copy {
top: 24px;
}