From 350491157abcaac4a0a57b614585317b0bf03284 Mon Sep 17 00:00:00 2001 From: lyf <2514544224@qq.com> Date: Mon, 13 Jul 2026 23:12:37 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=82=B9=E4=BD=8D=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E5=BF=AB=E6=8D=B7=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/search/PoiSearchPanel.vue | 318 +++++++---------------- src/domain/poiCategories.ts | 84 ++++-- static/icons/poi/shortcut-icons.svg | 55 ++++ tests/unit/PoiSearchPanel.spec.ts | 27 +- 4 files changed, 239 insertions(+), 245 deletions(-) create mode 100644 static/icons/poi/shortcut-icons.svg diff --git a/src/components/search/PoiSearchPanel.vue b/src/components/search/PoiSearchPanel.vue index 884b60c..774270e 100644 --- a/src/components/search/PoiSearchPanel.vue +++ b/src/components/search/PoiSearchPanel.vue @@ -47,16 +47,17 @@ - - - - + + {{ item.label }} @@ -130,7 +131,7 @@ - + - - - - + + {{ item.label }} @@ -230,6 +231,7 @@ import { HOME_POI_CATEGORIES, POI_CATEGORIES, findPoiCategoryByKeyword, + getPoiCategoryIconHref, getPoiCategoryById, getPoiDataIssues, inspectPoiCollection, @@ -273,8 +275,11 @@ type PreparedPoiResults = { } const homeCategories = HOME_POI_CATEGORIES +const collapsedHomeCategories = computed(() => homeCategories.slice(0, 5)) const searchCategories = POI_CATEGORIES -const searchCategoryColumns = searchCategories +const searchCategoryColumns = computed(() => ( + props.variant === 'home' ? homeCategories : searchCategories +)) const searchKeyword = ref('') const searchDraftKeyword = ref('') @@ -1083,89 +1088,76 @@ defineExpose({ .home-category-strip { margin-top: 10px; display: grid; - grid-template-columns: repeat(6, minmax(0, 1fr)); + grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 6px; } .variant-home.is-collapsed .home-category-strip { margin-top: 7px; - gap: 5px; + gap: 6px; } .home-category-chip { min-width: 0; - height: 60px; + min-height: 68px; display: flex; flex-direction: column; align-items: center; justify-content: center; - gap: 5px; - padding: 6px 2px; + gap: 4px; + padding: 6px 2px 5px; box-sizing: border-box; - background: #fffef2; - border: 1px solid #ecebd2; + background: #f5f5ed; + border: 1px solid #dfe2d7; border-radius: 8px; - box-shadow: none; + box-shadow: 0 2px 5px rgba(21, 23, 19, 0.1); + transition: background-color 0.18s ease, border-color 0.18s ease, transform 0.14s ease; } .variant-home.is-collapsed .home-category-chip { min-width: 0; - height: 56px; - gap: 4px; - padding: 5px 1px; - border-radius: 7px; + min-height: 68px; } .home-category-chip:active { transform: translateY(1px); - border-color: #d8d900; + background: #eef06d; + border-color: #c8ca00; +} + +.home-category-chip.active { + background: #f2f48d; + border-color: #bfc100; } .home-category-icon { - width: 24px; - height: 23px; - flex: 0 0 24px; + width: 28px; + height: 28px; + flex: 0 0 28px; +} + +.poi-category-icon { + display: block; + width: 100%; + height: 100%; color: #151713; + pointer-events: none; } -.home-category-icon.line-icon { - width: 24px; - height: 23px; -} - -.home-category-icon .icon-part { - transform: scale(0.8); - transform-origin: center; -} - -.variant-home.is-collapsed .home-category-icon, -.variant-home.is-collapsed .home-category-icon.line-icon { - width: 22px; - height: 21px; - flex-basis: 22px; -} - -.variant-home.is-collapsed .home-category-icon .icon-part { - transform: scale(0.74); +.home-category-chip.active .poi-category-icon, +.category-item.active .poi-category-icon { + color: #151713; } .home-category-label { min-width: 0; max-width: 100%; - font-size: 12px; - line-height: 16px; + font-size: 11px; + line-height: 15px; font-weight: 700; color: #151713; text-align: center; white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.variant-home.is-collapsed .home-category-label { - max-width: 100%; - font-size: 11px; - line-height: 14px; } .home-category-results { @@ -1436,6 +1428,14 @@ defineExpose({ touch-action: pan-x; } +.variant-home .category-scroll { + width: calc(100% - 8px); + margin: 0 4px; + overflow: visible; + padding-bottom: 0; + touch-action: pan-y; +} + .category-scroll::-webkit-scrollbar { display: none; } @@ -1455,22 +1455,24 @@ defineExpose({ min-width: 0; min-height: 70px; position: relative; - padding: 9px 6px 10px; + padding: 9px 4px 10px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 7px; box-sizing: border-box; - overflow: hidden; - background: #fffef2; - border: 1px solid #ecebd2; + overflow: visible; + background: #f5f5ed; + border: 1px solid #dfe2d7; border-radius: 8px; + box-shadow: 0 2px 5px rgba(21, 23, 19, 0.08); + transition: background-color 0.18s ease, border-color 0.18s ease, transform 0.14s ease; } .category-item.active { - background: #fffddc; - border-color: #d8d900; + background: #f2f48d; + border-color: #bfc100; } .home-category-chip.disabled, @@ -1482,170 +1484,52 @@ defineExpose({ .category-item.active::after { content: ''; position: absolute; - left: 14px; - right: 14px; - bottom: 0; - height: 4px; - background: var(--museum-accent); -} - -.line-icon { - width: 26px; - height: 24px; - position: relative; - color: #151713; -} - -.category-item.active .line-icon { - color: #151713; -} - -.icon-part { - position: absolute; - box-sizing: border-box; -} - -.icon-service .icon-part.a, -.icon-exhibition .icon-part.a, -.icon-cinema .icon-part.a, -.icon-ticket .icon-part.a, -.icon-drink .icon-part.a, -.icon-bag .icon-part.a { - left: 4px; - right: 4px; - top: 5px; - bottom: 5px; - border: 2px solid currentColor; - border-radius: 3px; -} - -.icon-service .icon-part.b, -.icon-exhibition .icon-part.b, -.icon-cinema .icon-part.b, -.icon-ticket .icon-part.b, -.icon-drink .icon-part.b, -.icon-bag .icon-part.b { - left: 9px; - right: 9px; - top: 1px; - height: 5px; - border: 2px solid currentColor; - border-bottom: 0; - border-radius: 6px 6px 0 0; -} - -.icon-elevator .icon-part.a, -.icon-escalator .icon-part.a, -.icon-restroom .icon-part.a, -.icon-nursing .icon-part.a, -.icon-restaurant .icon-part.a, -.icon-stairs .icon-part.a, -.icon-entrance .icon-part.a { - left: 4px; - right: 4px; - top: 3px; - bottom: 3px; - border: 2px solid currentColor; - border-radius: 4px; -} - -.icon-elevator .icon-part.b { - left: 9px; - top: 7px; - width: 8px; - height: 10px; - border-left: 2px solid currentColor; - border-right: 2px solid currentColor; -} - -.icon-escalator .icon-part.b, -.icon-entrance .icon-part.b { - left: 7px; - top: 12px; - width: 13px; - height: 2px; - background: currentColor; - transform: rotate(-22deg); -} - -.icon-restroom .icon-part.b, -.icon-nursing .icon-part.b { - left: 8px; - top: 8px; - width: 4px; - height: 4px; - background: currentColor; - border-radius: 50%; - box-shadow: 8px 0 0 currentColor; -} - -.icon-exhibition .icon-part.c { - left: 6px; - right: 6px; - bottom: 2px; - height: 2px; - background: currentColor; - border-radius: 2px; -} - -.icon-cinema .icon-part.b { - left: 2px; - right: 2px; - top: 4px; - height: 4px; - border: 0; - border-top: 2px solid currentColor; - border-bottom: 2px solid currentColor; - border-radius: 0; -} - -.icon-restaurant .icon-part.b { - left: 7px; - top: 6px; - width: 2px; - height: 12px; - background: currentColor; - box-shadow: 5px 0 0 currentColor, 10px 0 0 currentColor; -} - -.icon-restaurant .icon-part.c { - right: 3px; - top: 5px; - width: 7px; - height: 15px; - border-left: 2px solid currentColor; - border-bottom: 2px solid currentColor; - border-radius: 0 0 0 5px; -} - -.icon-stairs .icon-part.b { - left: 6px; - top: 7px; - width: 14px; - height: 12px; - border-left: 2px solid currentColor; - border-bottom: 2px solid currentColor; -} - -.icon-stairs .icon-part.c { left: 10px; - top: 11px; - width: 10px; - height: 8px; - border-left: 2px solid currentColor; - border-bottom: 2px solid currentColor; + right: 10px; + bottom: -1px; + height: 3px; + background: var(--museum-accent); + border-radius: 3px 3px 0 0; +} + +.category-icon-shell { + width: 28px; + height: 28px; + flex: 0 0 28px; +} + +.category-item:active { + transform: translateY(1px); + background: #eef06d; + border-color: #c8ca00; +} + +.variant-home .home-shortcut-grid { + display: grid; + grid-auto-flow: row; + grid-template-columns: repeat(5, minmax(0, 1fr)); + grid-template-rows: repeat(2, 72px); + grid-auto-columns: auto; + width: 100%; + min-width: 0; + gap: 6px; +} + +.variant-home .category-item { + min-height: 72px; + height: 72px; + padding: 6px 2px 5px; + gap: 4px; } .category-label { max-width: 100%; - font-size: 12px; - line-height: 16px; + font-size: 11px; + line-height: 15px; font-weight: 700; color: #151713; text-align: center; white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; } .category-item.active .category-label { diff --git a/src/domain/poiCategories.ts b/src/domain/poiCategories.ts index 063917b..9e915f8 100644 --- a/src/domain/poiCategories.ts +++ b/src/domain/poiCategories.ts @@ -4,6 +4,7 @@ import type { } from '@/domain/museum' export type PoiCategoryId = + | 'itinerary' | 'exhibition-hall' | 'cinema' | 'ticket-office' @@ -25,25 +26,37 @@ export interface PoiCategoryDefinition { keywords: readonly string[] categoryIds: readonly string[] iconTypes: readonly string[] + excludedCategoryIds?: readonly string[] } const definitions: PoiCategoryDefinition[] = [ { - id: 'exhibition-hall', - label: '展厅', - icon: 'exhibition', + id: 'itinerary', + label: '行程', + icon: 'itinerary', order: 1, homeVisible: true, - keywords: ['展厅', '展览', '展馆', 'exhibition_hall', 'touring_poi', 'hall'], - categoryIds: ['exhibition_hall', 'exhibition_hall_entrance', 'touring_poi'], - iconTypes: ['exhibition_hall', 'hall_entrance', 'exhibition'] + keywords: ['行程', '导览', '参观', 'touring_poi'], + categoryIds: ['touring_poi'], + iconTypes: ['touring', 'guide'] + }, + { + id: 'exhibition-hall', + label: '展览', + icon: 'exhibition', + order: 2, + homeVisible: true, + keywords: ['展厅', '展览', '展馆', 'exhibition_hall', 'hall'], + categoryIds: ['exhibition_hall', 'exhibition_hall_entrance'], + iconTypes: ['exhibition_hall', 'hall_entrance', 'exhibition'], + excludedCategoryIds: ['touring_poi'] }, { id: 'cinema', label: '影院', icon: 'cinema', - order: 2, - homeVisible: false, + order: 3, + homeVisible: true, keywords: ['影院', '影厅', '剧场', '报告厅', 'cinema', 'theater'], categoryIds: ['space_theater'], iconTypes: ['cinema', 'theater'] @@ -52,8 +65,8 @@ const definitions: PoiCategoryDefinition[] = [ id: 'ticket-office', label: '售票处', icon: 'ticket', - order: 3, - homeVisible: false, + order: 6, + homeVisible: true, keywords: ['售票处', '售票', '票务', 'ticket_office'], categoryIds: [], iconTypes: ['ticket_office'] @@ -62,18 +75,18 @@ const definitions: PoiCategoryDefinition[] = [ id: 'service-center', label: '服务中心', icon: 'service', - order: 4, - homeVisible: false, + order: 8, + homeVisible: true, keywords: ['服务中心', '服务台', '咨询台', 'service_desk'], categoryIds: [], iconTypes: ['service_desk'] }, { id: 'dining', - label: '餐饮', + label: '餐厅', icon: 'restaurant', order: 5, - homeVisible: false, + homeVisible: true, keywords: ['餐饮', '餐厅', '快餐', '咖啡', 'restaurant', 'dining', 'food', 'cafe'], categoryIds: [], iconTypes: ['restaurant', 'dining', 'food', 'cafe'] @@ -82,7 +95,7 @@ const definitions: PoiCategoryDefinition[] = [ id: 'souvenir', label: '文创', icon: 'bag', - order: 6, + order: 11, homeVisible: false, keywords: ['文创', '纪念品', '商店', 'souvenir', 'gift', 'shop'], categoryIds: [], @@ -100,9 +113,9 @@ const definitions: PoiCategoryDefinition[] = [ }, { id: 'restroom', - label: '卫生间', + label: '洗手间', icon: 'restroom', - order: 8, + order: 4, homeVisible: true, keywords: ['卫生间', '洗手间', '厕所', 'toilet', 'accessible_toilet', 'restroom'], categoryIds: [], @@ -122,8 +135,8 @@ const definitions: PoiCategoryDefinition[] = [ id: 'stairs', label: '楼梯', icon: 'stairs', - order: 10, - homeVisible: true, + order: 12, + homeVisible: false, keywords: ['楼梯', 'stairs', 'stair'], categoryIds: [], iconTypes: ['stairs', 'stair'] @@ -132,7 +145,7 @@ const definitions: PoiCategoryDefinition[] = [ id: 'escalator', label: '扶梯', icon: 'escalator', - order: 11, + order: 10, homeVisible: true, keywords: ['扶梯', 'escalator'], categoryIds: [], @@ -145,11 +158,15 @@ export const POI_CATEGORIES = Object.freeze( ) const homeCategoryOrder: readonly PoiCategoryId[] = [ + 'itinerary', 'exhibition-hall', + 'cinema', 'restroom', + 'dining', + 'ticket-office', 'nursing-room', + 'service-center', 'elevator', - 'stairs', 'escalator' ] @@ -159,6 +176,25 @@ export const HOME_POI_CATEGORIES = Object.freeze( )) ) +const POI_CATEGORY_ICON_SYMBOLS: Readonly> = { + itinerary: 'poi-itinerary', + 'exhibition-hall': 'poi-exhibition-hall', + cinema: 'poi-cinema', + 'ticket-office': 'poi-ticket-office', + 'service-center': 'poi-service-center', + dining: 'poi-dining', + souvenir: 'poi-souvenir', + 'nursing-room': 'poi-nursing-room', + restroom: 'poi-restroom', + elevator: 'poi-elevator', + stairs: 'poi-stairs', + escalator: 'poi-escalator' +} + +export const getPoiCategoryIconHref = (categoryId: PoiCategoryId) => ( + `/static/icons/poi/shortcut-icons.svg#${POI_CATEGORY_ICON_SYMBOLS[categoryId]}` +) + type PoiCategorySource = Pick const normalizeValue = (value?: string | null) => (value || '').trim().toLowerCase() @@ -177,6 +213,12 @@ export const matchesPoiCategory = ( const normalizedCategoryIds = new Set(categories.map((category) => normalizeValue(category.id))) const normalizedIconTypes = new Set(categories.map((category) => normalizeValue(category.iconType))) + if (definition.excludedCategoryIds?.some((categoryId) => ( + normalizedCategoryIds.has(normalizeValue(categoryId)) + ))) { + return false + } + if (definition.categoryIds.some((categoryId) => normalizedCategoryIds.has(normalizeValue(categoryId)))) { return true } diff --git a/static/icons/poi/shortcut-icons.svg b/static/icons/poi/shortcut-icons.svg new file mode 100644 index 0000000..6458e7d --- /dev/null +++ b/static/icons/poi/shortcut-icons.svg @@ -0,0 +1,55 @@ + diff --git a/tests/unit/PoiSearchPanel.spec.ts b/tests/unit/PoiSearchPanel.spec.ts index 7d7678d..a64630b 100644 --- a/tests/unit/PoiSearchPanel.spec.ts +++ b/tests/unit/PoiSearchPanel.spec.ts @@ -88,7 +88,7 @@ afterEach(() => { }) describe('POI 搜索面板实际渲染与状态闭环', () => { - it('首页显示六类统一入口,搜索页显示完整十一类入口', async () => { + it('首页收缩和展开复用同一份十项快捷入口,搜索页保留完整分类', async () => { const homeWrapper = mount(PoiSearchPanel, { props: { variant: 'home', @@ -99,19 +99,30 @@ describe('POI 搜索面板实际渲染与状态闭环', () => { await flushMountedSearch() const homeCategories = homeWrapper.findAll('.home-category-chip') - expect(homeCategories).toHaveLength(6) + expect(homeCategories).toHaveLength(5) expect(homeCategories.map((item) => item.text())).toEqual([ - '展厅', '卫生间', '母婴室', '电梯', '楼梯', '扶梯' + '行程', '展览', '影院', '洗手间', '餐厅' ]) homeCategories.forEach((item) => { - expect(item.find('.line-icon').exists()).toBe(true) + expect(item.find('.poi-category-icon').exists()).toBe(true) expect(item.find('.home-category-label').exists()).toBe(true) }) + + await homeWrapper.get('.search-box').trigger('tap') + await flushMountedSearch() + const expandedCategories = homeWrapper.findAll('.home-shortcut-grid .category-item') + expect(expandedCategories).toHaveLength(10) + expect(expandedCategories.map((item) => item.text())).toEqual([ + '行程', '展览', '影院', '洗手间', '餐厅', + '售票处', '母婴室', '服务中心', '电梯', '扶梯' + ]) + expect(expandedCategories.slice(0, 5).map((item) => item.text())) + .toEqual(homeCategories.map((item) => item.text())) homeWrapper.unmount() const pageWrapper = mount(PoiSearchPanel, { props: { variant: 'page' } }) await flushMountedSearch() - expect(pageWrapper.findAll('.category-item')).toHaveLength(11) + expect(pageWrapper.findAll('.category-item')).toHaveLength(12) expect(pageWrapper.text()).toContain('影院') expect(pageWrapper.text()).toContain('服务中心') expect(pageWrapper.text()).toContain('文创') @@ -189,6 +200,8 @@ describe('POI 搜索面板实际渲染与状态闭环', () => { await wrapper.get('[data-testid="poi-category-cancel"]').trigger('tap') await flushMountedSearch() + await wrapper.get('.search-box').trigger('tap') + await flushMountedSearch() await wrapper.get('[data-testid="poi-category-nursing-room"]').trigger('tap') await flushMountedSearch() expect(wrapper.find('[data-testid="poi-result-nursing-2"]').exists()).toBe(true) @@ -197,7 +210,7 @@ describe('POI 搜索面板实际渲染与状态闭环', () => { await flushMountedSearch() expect(wrapper.get('[data-testid="poi-empty"]').text()).toContain('当前楼层暂无匹配点位') expect((wrapper.emitted('results-change') || []).at(-1)?.[0]).toMatchObject({ - active: true, + active: false, visiblePoiIds: [] }) wrapper.unmount() @@ -221,7 +234,7 @@ describe('POI 搜索面板实际渲染与状态闭环', () => { const query = new URLSearchParams(url.split('?')[1]) expect(query.get('source')).toBe('search') expect(query.get('searchOrigin')).toBe('page') - expect(query.get('searchKeyword')).toBe('卫生间') + expect(query.get('searchKeyword')).toBe('洗手间') expect(query.get('searchCategoryId')).toBe('restroom') expect(query.get('searchFloorId')).toBe('floor-2') expect(query.get('resultCount')).toBe('3')