From 1984f3ed97fc15cc4b055b6e9a980eedd4c0ec58 Mon Sep 17 00:00:00 2001 From: zoujing Date: Wed, 12 Aug 2026 10:32:33 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BF=AE=E5=A4=8D=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E6=BB=9A=E5=8A=A8=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tickets/components/TicketHome.vue | 33 ++++++++++++++++-- .../tickets/view-contract.test.mjs | 34 +++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/pages-service/tickets/components/TicketHome.vue b/src/pages-service/tickets/components/TicketHome.vue index fafd54c..6bb7390 100644 --- a/src/pages-service/tickets/components/TicketHome.vue +++ b/src/pages-service/tickets/components/TicketHome.vue @@ -90,6 +90,7 @@ :scroll-into-view="scrollIntoViewId" scroll-with-animation @scroll="handleProductScroll" + @touchstart="enableProductScrollSync" > @@ -142,6 +143,23 @@ import { computed, getCurrentInstance, nextTick, onMounted, ref, watch } from 'vue' import TopNavBar from "@/components/TopNavBar/index.vue" +const createCategoryScrollSelection = () => { + let shouldSyncFromScroll = true + + return { + select(groupId) { + shouldSyncFromScroll = false + return groupId + }, + resumeScrollSync() { + shouldSyncFromScroll = true + }, + resolve(currentGroupId, scrollGroupId) { + return shouldSyncFromScroll ? scrollGroupId : currentGroupId + }, + } +} + const props = defineProps({ homeData: { type: Object, required: true }, }) @@ -164,6 +182,7 @@ const scrollIntoViewId = ref('') const leftScrollIntoViewId = computed(() => `ticket-nav-${activeGroupId.value}`) const groupOffsets = ref([]) const rightScrollTop = ref(0) +const categoryScrollSelection = createCategoryScrollSelection() const instance = getCurrentInstance() const scenicMetaText = computed(() => [props.homeData.levelText, props.homeData.subtitle].filter(Boolean).join('|') @@ -178,8 +197,13 @@ const isActiveGroup = (groupId) => const productsForGroup = (group) => Array.isArray(group.products) ? group.products : [] +const enableProductScrollSync = () => { + categoryScrollSelection.resumeScrollSync() + scrollIntoViewId.value = '' +} + const selectGroup = (groupId) => { - activeGroupId.value = groupId + activeGroupId.value = categoryScrollSelection.select(groupId) scrollIntoViewId.value = '' nextTick(() => { scrollIntoViewId.value = `ticket-group-${groupId}` @@ -225,7 +249,12 @@ const handleProductScroll = (event) => { (current, group) => (group.top <= activationLine ? group : current), groupOffsets.value[0] ) - if (activeGroup?.id) activeGroupId.value = activeGroup.id + if (activeGroup?.id) { + activeGroupId.value = categoryScrollSelection.resolve( + activeGroupId.value, + activeGroup.id + ) + } } onMounted(measureGroupOffsets) diff --git a/src/pages-service/tickets/view-contract.test.mjs b/src/pages-service/tickets/view-contract.test.mjs index 984ffe6..84881ad 100644 --- a/src/pages-service/tickets/view-contract.test.mjs +++ b/src/pages-service/tickets/view-contract.test.mjs @@ -63,6 +63,40 @@ test("storefront synchronizes the left category from right-column scrolling", as assert.match(source, /TopNavBar/); }); +test("left category selection wins over programmatic product scrolling", async () => { + const source = await readTicketFile("components/TicketHome.vue"); + const factorySource = source.match( + /const createCategoryScrollSelection = \(\) => \{[\s\S]*?^\}\n/m + )?.[0]; + + assert.match(source, /@touchstart="enableProductScrollSync"/); + assert.doesNotMatch(source, /category-scroll-selection\.mjs/); + assert.ok(factorySource, "inline category scroll selection factory missing"); + assert.match(source, /const categoryScrollSelection =\s*createCategoryScrollSelection\(\)/); + assert.match( + source, + /const selectGroup = \(groupId\) => \{\s*activeGroupId\.value = categoryScrollSelection\.select\(groupId\)/ + ); + assert.match( + source, + /const enableProductScrollSync = \(\) => \{\s*categoryScrollSelection\.resumeScrollSync\(\)\s*scrollIntoViewId\.value = ''\s*\}/ + ); + assert.match( + source, + /const handleProductScroll = \(event\) => \{[\s\S]*rightScrollTop\.value = Number[\s\S]*measureGroupOffsets\(\)[\s\S]*activeGroupId\.value = categoryScrollSelection\.resolve\(\s*activeGroupId\.value,\s*activeGroup\.id\s*\)/ + ); + + const createCategoryScrollSelection = Function( + `${factorySource}\nreturn createCategoryScrollSelection` + )(); + const selection = createCategoryScrollSelection(); + const clickedGroupId = selection.select("second"); + + assert.equal(selection.resolve(clickedGroupId, "first"), "second"); + selection.resumeScrollSync(); + assert.equal(selection.resolve(clickedGroupId, "third"), "third"); +}); + test("ticket storefront loads API home data without a static adapter", async () => { const source = await readTicketFile("index.vue");