diff --git a/mobile-uni/src/pages/user/sop/index.vue b/mobile-uni/src/pages/user/sop/index.vue index 23002727..636bac6e 100644 --- a/mobile-uni/src/pages/user/sop/index.vue +++ b/mobile-uni/src/pages/user/sop/index.vue @@ -237,7 +237,7 @@ import { submitAnswerFeedback } from '@/services/knowledge'; import { getSelectedPosition, getSelectedPositionProfile, isLoggedIn, rememberLoginRedirect } from '@/services/auth'; -import { resetPageScroll } from '@/services/navigation'; +import { scrollPageToSelector } from '@/services/navigation'; import { ensureEmployeePosition } from '@/services/position'; const positionProfile = ref(getSelectedPositionProfile()); @@ -315,7 +315,7 @@ const search = async () => { resetResultState(); try { result.value = await searchKnowledge(value); - resetPageScroll(); + scrollPageToSelector('.answer-panel'); if (!result.value.answer && !result.value.snippets?.length) { message.value = '未检索到可引用内容'; } diff --git a/mobile-uni/src/services/navigation.ts b/mobile-uni/src/services/navigation.ts index 7b9460b1..795185e0 100644 --- a/mobile-uni/src/services/navigation.ts +++ b/mobile-uni/src/services/navigation.ts @@ -14,6 +14,37 @@ export const resetPageScroll = () => { setTimeout(scrollTop, 240); }; +export const scrollPageToSelector = (selector: string, offset = 16) => { + const scrollToTarget = () => { + if (typeof window !== 'undefined' && typeof document !== 'undefined') { + const element = document.querySelector(selector); + if (element) { + const scrollTop = Math.max(0, window.scrollY + element.getBoundingClientRect().top - offset); + uni.pageScrollTo({ scrollTop, duration: 0 }); + window.scrollTo({ top: scrollTop, left: 0, behavior: 'auto' }); + document.documentElement.scrollTop = scrollTop; + document.body.scrollTop = scrollTop; + return; + } + } + + uni.createSelectorQuery() + .select(selector) + .boundingClientRect((rect) => { + const target = Array.isArray(rect) ? rect[0] : rect; + if (target && typeof target.top === 'number') { + uni.pageScrollTo({ scrollTop: Math.max(0, target.top - offset), duration: 0 }); + } + }) + .exec(); + }; + + scrollToTarget(); + setTimeout(scrollToTarget, 0); + setTimeout(scrollToTarget, 80); + setTimeout(scrollToTarget, 240); +}; + const switchToToday = () => { uni.switchTab({ url: '/pages/user/today/index' }); }; diff --git a/mobile-uni/tests/mobile-user-pages.test.mjs b/mobile-uni/tests/mobile-user-pages.test.mjs index 503d8ac6..b1e5b040 100644 --- a/mobile-uni/tests/mobile-user-pages.test.mjs +++ b/mobile-uni/tests/mobile-user-pages.test.mjs @@ -37,14 +37,16 @@ test('训练页提交失败会回滚本次乐观追加的对话', async () => { assert.match(source, /turns\.value = previousTurns/); }); -test('训练结果和 SOP 答案生成后重置到内容首屏', async () => { - const [practiceSource, sopSource] = await Promise.all([ +test('训练结果回到顶部,SOP 答案生成后定位到回答区', async () => { + const [practiceSource, sopSource, navigationSource] = await Promise.all([ pageSource('../src/pages/user/practice/index.vue'), - pageSource('../src/pages/user/sop/index.vue') + pageSource('../src/pages/user/sop/index.vue'), + pageSource('../src/services/navigation.ts') ]); assert.match(practiceSource, /result\.value = await finishPractice\([\s\S]*?resetPageScroll\(\)/); - assert.match(sopSource, /result\.value = await searchKnowledge\(value\);\s*resetPageScroll\(\)/); + assert.match(sopSource, /result\.value = await searchKnowledge\(value\);\s*scrollPageToSelector\('\.answer-panel'/); + assert.match(navigationSource, /export const scrollPageToSelector = \(selector: string/); }); test('训练历史按摘要、逐句复盘、SOP 依据和行动建议分层', async () => {