From 72d1f30c8945effe803210a5237cce8b8087cec0 Mon Sep 17 00:00:00 2001
From: lyf <2514544224@qq.com>
Date: Fri, 17 Jul 2026 23:02:09 +0800
Subject: [PATCH] =?UTF-8?q?=E8=AE=B2=E8=A7=A3=E5=AF=B9=E8=B1=A1=E9=A1=B5?=
=?UTF-8?q?=E6=94=B9=E4=B8=BA=E5=8D=95=E5=88=97=E5=88=97=E8=A1=A8=E5=B9=B6?=
=?UTF-8?q?=E8=A1=A5=E5=85=85=E7=A7=BB=E5=8A=A8=E7=AB=AF=E9=AA=8C=E8=AF=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../explain/ExplainGuideStopCatalog.vue | 120 ++++++++++++++++++
tests/e2e/explain-hall-page.spec.ts | 34 +++++
tests/unit/ExplainGuideStopCatalog.spec.ts | 66 ++++++++++
3 files changed, 220 insertions(+)
create mode 100644 src/components/explain/ExplainGuideStopCatalog.vue
create mode 100644 tests/unit/ExplainGuideStopCatalog.spec.ts
diff --git a/src/components/explain/ExplainGuideStopCatalog.vue b/src/components/explain/ExplainGuideStopCatalog.vue
new file mode 100644
index 0000000..16a7998
--- /dev/null
+++ b/src/components/explain/ExplainGuideStopCatalog.vue
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+ {{ filter.label }}
+
+
+
+
+
+
+ 正在加载讲解对象稍后将展示该展厅的讲解对象。
+ 讲解对象加载失败{{ error }}
+
+
+
+
+
+
+ {{ stop.name }}
+ {{ primaryMeta(stop) }}
+ {{ secondaryMeta(stop) }}
+
+
+
+ {{ keyword || activeFilter !== 'all' ? '未找到匹配的讲解对象' : '该展厅暂无讲解对象' }}{{ keyword || activeFilter !== 'all' ? '可尝试调整搜索词或筛选条件。' : '可返回选择其他展厅。' }}
+
+ 正在加载更多讲解对象
+ {{ loadMoreError }}
+ 已加载全部讲解对象
+
+
+
+
+
+
+
+
+
diff --git a/tests/e2e/explain-hall-page.spec.ts b/tests/e2e/explain-hall-page.spec.ts
index 5b1d822..b2e9700 100644
--- a/tests/e2e/explain-hall-page.spec.ts
+++ b/tests/e2e/explain-hall-page.spec.ts
@@ -5,6 +5,9 @@ const stops = Array.from({ length: 21 }, (_, index) => ({
name: `讲解对象${index + 1}`,
imageStatus: index === 0 ? 'READY' : 'MISSING',
coverImageUrl: index === 0 ? '/one.jpg' : undefined,
+ audioStatus: index === 0 ? 'READY' : 'MISSING',
+ hasAudio: index === 0,
+ hasTextRecord: index !== 0,
playTargetType: 'STOP' as const,
playTargetId: `target-${index + 1}`
}))
@@ -30,6 +33,37 @@ test('hall goes directly to paged guide objects without outline requests', async
await page.getByText('恐龙厅', { exact: true }).click()
await expect(page).toHaveURL(/guide-stop-list\?hallId=hall-1/)
await expect(page.getByText('讲解对象1', { exact: true })).toBeVisible()
+ const cards = page.locator('.stop-card')
+ await expect(cards).toHaveCount(20)
+ for (const width of [375, 390, 430]) {
+ await page.setViewportSize({ width, height: 844 })
+ const layout = await page.locator('.stop-list').evaluate((list) => {
+ const cards = Array.from(list.querySelectorAll('.stop-card'))
+ const listRect = list.getBoundingClientRect()
+ const firstRect = cards[0].getBoundingClientRect()
+ const secondRect = cards[1].getBoundingClientRect()
+ return {
+ clientWidth: list.clientWidth,
+ scrollWidth: list.scrollWidth,
+ listWidth: listRect.width,
+ first: { x: firstRect.x, width: firstRect.width, bottom: firstRect.bottom },
+ second: { x: secondRect.x, y: secondRect.y }
+ }
+ })
+ expect(layout.scrollWidth).toBeLessThanOrEqual(layout.clientWidth)
+ expect(layout.first.x).toBeCloseTo(layout.second.x, 0)
+ expect(layout.first.width).toBeCloseTo(layout.listWidth, 0)
+ expect(layout.second.y).toBeGreaterThan(layout.first.bottom)
+ }
+ const search = page.getByRole('textbox')
+ await search.fill('讲解对象21')
+ await expect(page.getByText('讲解对象21', { exact: true })).toBeVisible()
+ await search.fill('')
+ const audioFilter = page.getByText('有音频', { exact: true })
+ await audioFilter.click()
+ await expect(page.getByText('讲解对象1', { exact: true })).toBeVisible()
+ await expect(page.getByText('讲解对象2', { exact: true })).not.toBeVisible()
+ await page.getByText('全部', { exact: true }).click()
await page.locator('.explain-scroll').hover()
await page.mouse.wheel(0, 10000)
await expect(page.getByText('讲解对象21', { exact: true })).toBeVisible()
diff --git a/tests/unit/ExplainGuideStopCatalog.spec.ts b/tests/unit/ExplainGuideStopCatalog.spec.ts
new file mode 100644
index 0000000..79e1e20
--- /dev/null
+++ b/tests/unit/ExplainGuideStopCatalog.spec.ts
@@ -0,0 +1,66 @@
+// @vitest-environment happy-dom
+
+import { mount } from '@vue/test-utils'
+import { describe, expect, it } from 'vitest'
+import ExplainGuideStopCatalog from '@/components/explain/ExplainGuideStopCatalog.vue'
+
+const stops = [
+ { id: 'ready', name: '一个很长很长很长很长很长的讲解对象名称', coverImageUrl: '/cover.jpg', audioStatus: 'READY', hasAudio: true, hasTextRecord: true },
+ { id: 'text', name: '图文对象', hasTextRecord: true },
+ { id: 'none', name: '无内容对象' }
+]
+
+const mountCatalog = () => mount(ExplainGuideStopCatalog, { props: { guideStops: stops, selectedHallName: '宇宙厅', hasMore: true } })
+
+describe('ExplainGuideStopCatalog', () => {
+ it('renders a stable single-column list with real availability copy and no arrows', () => {
+ const wrapper = mountCatalog()
+ expect(wrapper.find('.stop-list').exists()).toBe(true)
+ expect(wrapper.find('.stop-grid').exists()).toBe(false)
+ expect(wrapper.findAll('.stop-card')).toHaveLength(3)
+ expect(wrapper.findAll('.stop-cover.placeholder')).toHaveLength(2)
+ expect(wrapper.text()).toContain('音频可播放')
+ expect(wrapper.text()).toContain('图文可查看')
+ expect(wrapper.text()).toContain('暂无可用内容')
+ expect(wrapper.find('.hall-arrow').exists()).toBe(false)
+ expect(wrapper.find('.stop-name').classes()).toContain('stop-name')
+ })
+
+ it('searches and uses only data-driven fallback filters when guide levels are absent', async () => {
+ const wrapper = mountCatalog()
+ expect(wrapper.text()).not.toContain('核心展品')
+ expect(wrapper.get('input').attributes('placeholder')).toBe('搜索讲解对象')
+ await wrapper.get('input').setValue('图文')
+ expect(wrapper.findAll('.stop-card')).toHaveLength(1)
+ expect(wrapper.emitted('requestAll')).toHaveLength(1)
+ await wrapper.get('input').setValue('')
+ await wrapper.get('.filter-chip:nth-child(2)').trigger('tap')
+ expect(wrapper.findAll('.stop-card')).toHaveLength(1)
+ })
+
+ it('keeps loading and empty states available for the single-column list', async () => {
+ const wrapper = mount(ExplainGuideStopCatalog, { props: { loading: true } })
+ expect(wrapper.text()).toContain('正在加载讲解对象')
+
+ await wrapper.setProps({ loading: false, guideStops: [] })
+ expect(wrapper.text()).toContain('该展厅暂无讲解对象')
+ expect(wrapper.find('.stop-list').exists()).toBe(false)
+ })
+
+ it('emits card, back, retry, retry-more, and request-all actions', async () => {
+ const wrapper = mountCatalog()
+ await wrapper.get('.stop-card').trigger('tap')
+ await wrapper.get('.header-back').trigger('tap')
+ await wrapper.get('.filter-chip:nth-child(2)').trigger('tap')
+ expect(wrapper.emitted('guideStopClick')?.[0][0]).toMatchObject({ id: 'ready' })
+ expect(wrapper.emitted('back')).toHaveLength(1)
+ expect(wrapper.emitted('requestAll')).toHaveLength(1)
+
+ await wrapper.setProps({ error: '失败' })
+ await wrapper.get('.state-retry').trigger('tap')
+ expect(wrapper.emitted('retry')).toHaveLength(1)
+ await wrapper.setProps({ error: '', loadMoreError: '更多失败' })
+ await wrapper.get('.state-retry').trigger('tap')
+ expect(wrapper.emitted('retryMore')).toHaveLength(1)
+ })
+})