diff --git a/.env.development b/.env.development
index 31263c9..09d51de 100644
--- a/.env.development
+++ b/.env.development
@@ -25,7 +25,7 @@ VITE_PUBLIC_LEGACY_AUDIO_HOST=
# Vite 开发服务器代理目标,只由 vite.config.ts 读取。
# /engine 由 public/engine 本地静态资源提供,保持和大屏端一致,不再代理到地图管理端。
-DEV_PROXY_APP_API_TARGET=http://localhost:48080
+DEV_PROXY_APP_API_TARGET=https://guide.whaoyue.com:4433
DEV_PROXY_ENGINE_TARGET=
DEV_PROXY_SDK_TARGET=
DEV_PROXY_MUSEUM_ASSETS_TARGET=http://1.92.206.90:9000
diff --git a/src/components/explain/ExplainHallSelect.vue b/src/components/explain/ExplainHallSelect.vue
index 971b117..6b892d2 100644
--- a/src/components/explain/ExplainHallSelect.vue
+++ b/src/components/explain/ExplainHallSelect.vue
@@ -33,6 +33,24 @@
+
+
+
+
+
+ 按展厅选择
+
+
+
+
+
+
+ 输入讲解编号
+ 请输入展品旁标注的 4 位编号
+
+
+ {{ codeEntryValue[index - 1] || '' }}
+
+
+
+ {{ codeEntryError }}
+ 请输入展品标签上的四位编号
+
+
+
+
@@ -234,6 +291,7 @@ const props = withDefaults(defineProps<{
error?: string
loadMoreError?: string
forceInternalHeader?: boolean
+ codeSubmitting?: boolean
}>(), {
halls: () => [],
businessUnits: () => [],
@@ -245,11 +303,13 @@ const props = withDefaults(defineProps<{
hasMore: false,
error: '',
loadMoreError: '',
- forceInternalHeader: false
+ forceInternalHeader: false,
+ codeSubmitting: false
})
const emit = defineEmits<{
hallClick: [hallId: string]
+ codeSubmit: [code: string]
businessUnitClick: [unitId: string]
guideStopClick: [stop: ExplainGuideStopSelectItem]
back: []
@@ -261,6 +321,9 @@ const HALL_PREVIEW_BASE = '/static/icons/halls/portal-icons'
const HALL_PREVIEW_EAGER_COUNT = 4
const loadedHallPreviewIds = ref(new Set())
const failedHallPreviewIds = ref(new Set())
+const codeEntryVisible = ref(false)
+const codeEntryValue = ref('')
+const codeEntryError = ref('')
const hallPreviewMap: Record = {
宇宙厅: `${HALL_PREVIEW_BASE}/universe.png`,
@@ -292,6 +355,7 @@ const showInternalHeader = computed(() => props.forceInternalHeader || !shouldUs
const headerTitle = computed(() => {
if (props.stage === 'stop') return props.selectedHallName || '讲解对象'
if (props.stage === 'unit') return props.selectedHallName || '讲解单元'
+ if (codeEntryVisible.value) return '编号讲解'
return '免费讲解'
})
const loadingTitle = computed(() => {
@@ -355,6 +419,45 @@ const handleHallClick = (hallId: string) => {
emit('hallClick', hallId)
}
+const openCodeEntry = () => {
+ codeEntryError.value = ''
+ codeEntryVisible.value = true
+}
+
+const closeCodeEntry = () => {
+ codeEntryVisible.value = false
+ codeEntryValue.value = ''
+ codeEntryError.value = ''
+}
+
+const handleCodeEntryInput = (event: Event) => {
+ const inputEvent = event as Event & { detail?: { value?: string } }
+ codeEntryValue.value = (inputEvent.detail?.value || '')
+ .replace(/\D/g, '')
+ .slice(0, 4)
+ codeEntryError.value = ''
+}
+
+const submitCodeEntry = () => {
+ if (props.codeSubmitting) return
+ const code = codeEntryValue.value
+ if (code.length !== 4) {
+ codeEntryError.value = '请输入完整的 4 位讲解编号'
+ return
+ }
+ emit('codeSubmit', code)
+}
+
+const showCodeEntryError = (message: string) => {
+ codeEntryError.value = message
+}
+
+const completeCodeEntry = () => {
+ closeCodeEntry()
+}
+
+defineExpose({ showCodeEntryError, completeCodeEntry })
+
const handleBusinessUnitClick = (unitId: string) => {
emit('businessUnitClick', unitId)
}
@@ -382,6 +485,10 @@ const handleScroll = (event: Event) => {
}
const handleBack = () => {
+ if (codeEntryVisible.value) {
+ closeCodeEntry()
+ return
+ }
emit('back')
}
@@ -405,6 +512,64 @@ const handleBack = () => {
padding: 76px 16px 16px;
}
+.code-entry-section {
+ padding-bottom: 10px;
+}
+
+.code-entry-button {
+ position: relative;
+ width: 100%;
+ min-height: 82px;
+ margin: 0;
+ padding: 13px 72px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ color: #171713;
+ border: 1px solid #cfd4c7;
+ border-radius: 8px;
+ background: #ffffff;
+ box-shadow: 0 3px 10px rgba(36, 49, 42, 0.06);
+}
+
+.code-entry-button::after { border: 0; }
+.code-entry-button:active { background: #f0f3e8; }
+.code-entry-icon { position: absolute; left: 28px; top: 50%; width: 44px; height: 44px; border-radius: 8px; background: #e0e100; transform: translateY(-50%); }
+.headphone-band { position: absolute; left: 10px; top: 9px; width: 24px; height: 23px; box-sizing: border-box; border: 3px solid #171713; border-bottom: 0; border-radius: 14px 14px 0 0; }
+.headphone-ear { position: absolute; top: 25px; width: 7px; height: 11px; border-radius: 3px; background: #171713; }
+.headphone-ear.left { left: 8px; }
+.headphone-ear.right { right: 8px; }
+.code-entry-copy { width: 100%; min-width: 0; text-align: center; }
+.code-entry-title, .code-entry-desc { display: block; }
+.code-entry-title { font-size: 17px; line-height: 23px; font-weight: 700; color: #171713; }
+.code-entry-desc { margin-top: 3px; font-size: 12px; line-height: 18px; color: #68725d; }
+
+.hall-section-heading { height: 30px; display: flex; align-items: center; justify-content: center; }
+.hall-section-title { font-size: 13px; line-height: 19px; font-weight: 600; color: #68725d; }
+
+.code-entry-overlay { position: absolute; z-index: 3000; top: 64px; right: 0; bottom: 0; left: 0; background: #f7f9f2; }
+.code-entry-sheet { position: absolute; inset: 0; padding: 24px 24px calc(24px + env(safe-area-inset-bottom)); box-sizing: border-box; background: #f7f9f2; }
+.code-entry-content { width: min(100%, 330px); min-height: 100%; margin: 0 auto; display: flex; flex-direction: column; align-items: center; justify-content: center; box-sizing: border-box; transform: translateY(-28px); }
+.code-entry-sheet-title, .code-entry-sheet-desc, .code-entry-error, .code-entry-demo { display: block; text-align: center; }
+.code-entry-sheet-title { font-size: 22px; line-height: 30px; font-weight: 700; color: #171713; }
+.code-entry-sheet-desc { margin-top: 8px; font-size: 14px; line-height: 20px; color: #68725d; }
+.code-entry-action::after { border: 0; }
+.code-digit-fields { position: relative; width: 100%; margin-top: 32px; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; }
+.code-digit-cell { height: 68px; display: flex; align-items: center; justify-content: center; box-sizing: border-box; border: 1.5px solid #aeb5a5; border-radius: 6px; background: #ffffff; }
+.code-digit-cell.active { border-color: #777d00; box-shadow: 0 0 0 1px #e0e100 inset; }
+.code-digit-fields.error .code-digit-cell { border-color: #b63f3f; }
+.code-digit-value { font-size: 30px; line-height: 36px; font-weight: 700; color: #171713; }
+.code-entry-native-input { position: absolute; inset: 0; width: 100%; height: 68px; opacity: 0.01; color: transparent; caret-color: transparent; }
+.code-entry-error, .code-entry-hint { min-height: 19px; margin-top: 10px; font-size: 12px; line-height: 19px; }
+.code-entry-error { color: #a62f2f; }
+.code-entry-hint { color: #7a8271; }
+.code-entry-action { width: 100%; height: 48px; margin: 28px 0 0; padding: 0 12px; font-size: 15px; line-height: 48px; font-weight: 700; border: 0; border-radius: 6px; }
+.code-entry-action.primary { color: #171713; background: #e0e100; }
+.code-entry-action.primary[disabled] { color: #909489; background: #e4e6df; opacity: 1; }
+
+.explain-hall-select.host-navigation .code-entry-overlay { top: 0; }
+
.explain-scroll.stop-stage {
padding-bottom: 0;
}
@@ -906,13 +1071,23 @@ const handleBack = () => {
}
.explain-scroll.hall-stage {
- padding: 88px 16px 24px;
+ padding: 88px 16px 12px;
+ overflow: hidden;
}
.hall-overview-list {
- // 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));
+ grid-template-rows: repeat(4, minmax(0, 1fr));
+ grid-auto-rows: minmax(0, 1fr);
+ height: calc(100vh - 222px);
+ gap: 10px 12px;
+}
+
+.hall-overview-card {
+ min-height: 0;
+}
+
+.explain-hall-select.host-navigation .hall-overview-list {
+ height: calc(100vh - 146px);
}
.hall-overview-card {
diff --git a/src/pages/exhibit/detail.vue b/src/pages/exhibit/detail.vue
index 19a4370..98af532 100644
--- a/src/pages/exhibit/detail.vue
+++ b/src/pages/exhibit/detail.vue
@@ -246,6 +246,7 @@ const detailTextLoadedByLanguage = ref>({
'en-US': false
})
const shouldUseHostNavigation = computed(() => isEmbeddedInWechatMiniProgram())
+const shouldAutoplayOnEntry = ref(false)
const isAudioLanguage = (value: unknown): value is AudioLanguage => (
value === 'zh-CN' || value === 'yue-HK' || value === 'en-US'
@@ -531,6 +532,8 @@ onLoad(async (options: any = {}) => {
return
}
const rawLang = Array.isArray(options.lang) ? options.lang[0] : options.lang
+ const rawAutoplay = Array.isArray(options.autoplay) ? options.autoplay[0] : options.autoplay
+ shouldAutoplayOnEntry.value = rawAutoplay === '1' || rawAutoplay === 'true'
const lang = typeof rawLang === 'string'
? normalizeGuideAudioLanguage(rawLang)
: 'zh-CN'
@@ -545,6 +548,18 @@ onLoad(async (options: any = {}) => {
poiId: Array.isArray(options.poiId) ? options.poiId[0] : options.poiId
}
await loadExplainDetail(detailEntryRequest.value, lang)
+ if (shouldAutoplayOnEntry.value) {
+ shouldAutoplayOnEntry.value = false
+ try {
+ await handlePlayAudio()
+ } catch (error) {
+ console.warn('自动播放讲解失败,将等待用户手动播放:', error)
+ uni.showToast({
+ title: '点击底部播放键开始讲解',
+ icon: 'none'
+ })
+ }
+ }
} catch (error) {
console.error('讲解详情加载失败:', error)
detailState.value = 'error'
diff --git a/src/pages/explain/list.vue b/src/pages/explain/list.vue
index e1d2bdb..a051328 100644
--- a/src/pages/explain/list.vue
+++ b/src/pages/explain/list.vue
@@ -8,11 +8,14 @@
>
@@ -41,12 +44,15 @@ import {
} from '@/utils/guideTopTabs'
import { isEmbeddedInWechatMiniProgram } from '@/utils/hostEnvironment'
import { explainGuideStopListUrl } from '@/utils/explainNavigation'
+import { explainCodeDetailUrl, explainCodeErrorMessage } from '@/utils/explainCodeEntry'
import { returnToWechatMiniProgram } from '@/services/WechatMiniProgramBridgeService'
import { syncHostNavigationTitle } from '@/utils/hostNavigationTitle'
const explainHallItems = ref([])
const explainLoading = ref(false)
const explainError = ref('')
+const hallSelectRef = ref | null>(null)
+const codeSubmitting = ref(false)
let explainLoadPromise: Promise | null = null
const normalizeExplainKeyword = (value?: string) => (value || '').trim().toLowerCase()
@@ -270,6 +276,20 @@ const handleExplainHallClick = (hallId: string) => {
})
}
+const handleExplainCodeSubmit = async (code: string) => {
+ if (codeSubmitting.value) return
+ codeSubmitting.value = true
+ try {
+ const target = await explainUseCase.resolveExplainCode(code)
+ hallSelectRef.value?.completeCodeEntry()
+ uni.navigateTo({ url: explainCodeDetailUrl(target) })
+ } catch (error) {
+ hallSelectRef.value?.showCodeEntryError(explainCodeErrorMessage(error))
+ } finally {
+ codeSubmitting.value = false
+ }
+}
+