Compare commits

...

4 Commits

Author SHA1 Message Date
8cc06d4e75 test(e2e): 手机编辑配方 overlay 宽度 + swipe 屏蔽
Some checks are pending
Test / e2e-test (push) Blocked by required conditions
PR Preview / teardown-preview (pull_request) Has been skipped
Test / unit-test (push) Successful in 8s
Test / build-check (push) Successful in 6s
PR Preview / test (pull_request) Successful in 9s
PR Preview / deploy-preview (pull_request) Successful in 18s
新增 responsive.cy.js 里的 Mobile edit recipe overlay 块,覆盖:
- overlay-panel 无横向溢出
- 成分/用量/单价/小计 4 列 header 都在 panel 内
- 精油搜索输入不会撑出 panel 右边
- editor 打开时模拟横向 swipe,当前 tab 不切换
2026-04-18 18:20:27 +00:00
2a2b1b8928 fix: 精油搜索框撑宽整表,限制 width 100% 让 4 列都能展示
All checks were successful
PR Preview / teardown-preview (pull_request) Has been skipped
Test / unit-test (push) Successful in 9s
Test / build-check (push) Successful in 5s
PR Preview / test (pull_request) Successful in 10s
PR Preview / deploy-preview (pull_request) Successful in 17s
Test / e2e-test (push) Successful in 4m52s
.form-select 之前没设 width,<input> 默认 size=20 宽约 180px,
把整个 editor-table 撑出面板,后面几列被挤出屏幕。加 width 100%
+ min-width 0 + box-sizing border-box,让精油列占剩余空间即可。
2026-04-18 18:12:52 +00:00
dacd4887aa fix: 手机编辑配方 overlay 合适宽度 + 屏蔽 tab 滑动
All checks were successful
Test / unit-test (push) Successful in 7s
Test / build-check (push) Successful in 4s
PR Preview / teardown-preview (pull_request) Has been skipped
Test / e2e-test (push) Successful in 3m1s
PR Preview / test (pull_request) Successful in 6s
PR Preview / deploy-preview (pull_request) Successful in 17s
移动端下 .overlay-panel 内边距从 24px 缩小到 16px,配合标题
字号、容器外边距和 ratio 提示换行调整,使编辑配方弹层在手机
上视觉更紧凑。

另外把 .overlay 加入 onSwipeEnd 的 modal 选择器,编辑弹层
打开时不再被左右滑动误触切换 tab。
2026-04-18 13:27:49 +00:00
bac5e0a26a fix: 手机左右滑动切换 tab 不生效
All checks were successful
PR Preview / test (pull_request) Has been skipped
Deploy Production / test (push) Successful in 7s
PR Preview / teardown-preview (pull_request) Successful in 14s
PR Preview / deploy-preview (pull_request) Has been skipped
Test / unit-test (push) Successful in 6s
Test / build-check (push) Successful in 4s
Deploy Production / deploy (push) Successful in 6s
Test / e2e-test (push) Successful in 3m1s
- 加 touch-action: pan-y 阻止浏览器抢占水平手势
- 用 touchmove 实时跟踪手指位置(比 touchend.changedTouches 更可靠)
- 用 touchstart 的 target 判断 no-swipe 区域(手指移动后 target 可能变)
- 弹窗/遮罩层打开时跳过滑动切换
- 阈值从 50px 调到 60px 减少误触

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:34:48 +00:00
3 changed files with 114 additions and 12 deletions

View File

@@ -39,6 +39,75 @@ describe('Responsive Design', () => {
}) })
}) })
describe('Mobile edit recipe overlay (375x667)', () => {
let adminToken
before(() => {
cy.getAdminToken().then(token => { adminToken = token })
})
beforeEach(() => {
cy.viewport(375, 667)
cy.visit('/manage', {
onBeforeLoad(win) {
win.localStorage.setItem('oil_auth_token', adminToken)
}
})
cy.get('.recipe-manager', { timeout: 10000 }).should('exist')
cy.contains('公共配方库', { timeout: 10000 }).should('be.visible').click()
cy.get('.recipe-row', { timeout: 10000 }).should('have.length.gte', 1)
cy.get('.recipe-row .row-info').first().click()
cy.get('.overlay-panel', { timeout: 5000 }).should('be.visible')
})
it('editor table fits within panel without horizontal overflow', () => {
cy.get('.overlay-panel').then($panel => {
const panel = $panel[0]
expect(panel.scrollWidth, 'panel has no horizontal overflow')
.to.be.lte(panel.clientWidth + 1)
})
cy.get('.overlay-panel .editor-table').then($table => {
const tableRect = $table[0].getBoundingClientRect()
const panelRect = Cypress.$('.overlay-panel')[0].getBoundingClientRect()
expect(tableRect.right, 'table right edge').to.be.lte(panelRect.right + 1)
expect(tableRect.left, 'table left edge').to.be.gte(panelRect.left - 1)
})
})
it('all 4 data column headers (成分/用量/单价/小计) are visible in panel', () => {
const headers = ['成分', '用量', '单价', '小计']
headers.forEach(label => {
cy.get('.overlay-panel .editor-table thead th').contains(label).then($th => {
const thRect = $th[0].getBoundingClientRect()
const panelRect = Cypress.$('.overlay-panel')[0].getBoundingClientRect()
expect(thRect.right, `${label} header right`).to.be.lte(panelRect.right + 1)
expect(thRect.left, `${label} header left`).to.be.gte(panelRect.left - 1)
})
})
})
it('oil search input does not push the row past panel edge', () => {
cy.get('.overlay-panel .form-select').first().then($input => {
const inputRect = $input[0].getBoundingClientRect()
const panelRect = Cypress.$('.overlay-panel')[0].getBoundingClientRect()
expect(inputRect.right, 'oil input right').to.be.lte(panelRect.right + 1)
})
})
it('horizontal swipe does not switch tabs while editor overlay is open', () => {
cy.get('.nav-tab.active').invoke('text').then(activeBefore => {
// Overlay covers .main — touch events bubble from .overlay up to .main's handler
cy.get('.overlay')
.trigger('touchstart', { touches: [{ clientX: 320, clientY: 400 }], force: true })
.trigger('touchmove', { touches: [{ clientX: 60, clientY: 400 }], force: true })
.trigger('touchend', { force: true })
cy.wait(200)
cy.get('.overlay-panel').should('be.visible')
cy.get('.nav-tab.active').invoke('text').should('eq', activeBefore)
})
})
})
describe('Tablet viewport (768x1024)', () => { describe('Tablet viewport (768x1024)', () => {
beforeEach(() => { beforeEach(() => {
cy.viewport(768, 1024) cy.viewport(768, 1024)

View File

@@ -38,7 +38,7 @@
</div> </div>
<!-- Main content --> <!-- Main content -->
<div class="main" @touchstart="onSwipeStart" @touchend="onSwipeEnd"> <div class="main" @touchstart.passive="onSwipeStart" @touchmove.passive="onSwipeMove" @touchend="onSwipeEnd" style="touch-action: pan-y">
<router-view /> <router-view />
</div> </div>
@@ -167,23 +167,36 @@ function toggleUserMenu() {
} }
// ── 左右滑动切换 tab ── // ── 左右滑动切换 tab ──
// 滑动顺序 = visibleTabs 的顺序(根据用户角色动态决定) // touch-action: pan-y on .main tells the browser to only handle vertical scroll natively,
// 轮播区域data-no-tab-swipe内的滑动不触发 tab 切换 // leaving horizontal swipe gestures to our JS handler.
const swipeStartX = ref(0) const swipeStartX = ref(0)
const swipeStartY = ref(0) const swipeStartY = ref(0)
const swipeEndX = ref(0)
const swipeEndY = ref(0)
const swipeStartTarget = ref(null)
function onSwipeStart(e) { function onSwipeStart(e) {
swipeStartX.value = e.touches[0].clientX swipeStartX.value = e.touches[0].clientX
swipeStartY.value = e.touches[0].clientY swipeStartY.value = e.touches[0].clientY
swipeEndX.value = e.touches[0].clientX
swipeEndY.value = e.touches[0].clientY
swipeStartTarget.value = e.target
} }
function onSwipeEnd(e) { function onSwipeMove(e) {
const dx = e.changedTouches[0].clientX - swipeStartX.value swipeEndX.value = e.touches[0].clientX
const dy = e.changedTouches[0].clientY - swipeStartY.value swipeEndY.value = e.touches[0].clientY
// 必须是水平滑动 > 50px且水平距离大于垂直距离 }
if (Math.abs(dx) < 50 || Math.abs(dy) > Math.abs(dx)) return
// 轮播区域内不触发 tab 切换 function onSwipeEnd() {
if (e.target.closest && e.target.closest('[data-no-tab-swipe]')) return const dx = swipeEndX.value - swipeStartX.value
const dy = swipeEndY.value - swipeStartY.value
// Must be primarily horizontal (>60px) and more horizontal than vertical
if (Math.abs(dx) < 60 || Math.abs(dy) > Math.abs(dx)) return
// Carousel area excluded
if (swipeStartTarget.value?.closest?.('[data-no-tab-swipe]')) return
// Skip when modal/overlay is open
if (document.querySelector('.modal-overlay, .detail-overlay, .dialog-overlay, .overlay')) return
const tabs = visibleTabs.value.map(t => t.key) const tabs = visibleTabs.value.map(t => t.key)
const currentIdx = tabs.indexOf(ui.currentSection) const currentIdx = tabs.indexOf(ui.currentSection)
@@ -193,8 +206,7 @@ function onSwipeEnd(e) {
if (dx < 0 && currentIdx < tabs.length - 1) nextIdx = currentIdx + 1 if (dx < 0 && currentIdx < tabs.length - 1) nextIdx = currentIdx + 1
else if (dx > 0 && currentIdx > 0) nextIdx = currentIdx - 1 else if (dx > 0 && currentIdx > 0) nextIdx = currentIdx - 1
if (nextIdx >= 0) { if (nextIdx >= 0) {
const tab = visibleTabs.value[nextIdx] handleTabClick(visibleTabs.value[nextIdx])
handleTabClick(tab)
} }
} }

View File

@@ -2307,6 +2307,9 @@ watch(() => recipeStore.recipes, () => {
.form-select { .form-select {
flex: 1; flex: 1;
width: 100%;
min-width: 0;
box-sizing: border-box;
padding: 8px 10px; padding: 8px 10px;
border: 1.5px solid #d4cfc7; border: 1.5px solid #d4cfc7;
border-radius: 8px; border-radius: 8px;
@@ -2328,6 +2331,7 @@ watch(() => recipeStore.recipes, () => {
.oil-search-wrap { .oil-search-wrap {
flex: 1; flex: 1;
width: 100%;
position: relative; position: relative;
} }
@@ -2483,5 +2487,22 @@ watch(() => recipeStore.recipes, () => {
.manage-toolbar { .manage-toolbar {
flex-direction: column; flex-direction: column;
} }
.overlay-panel {
padding: 16px;
border-radius: 12px;
max-height: calc(100vh - 32px);
}
.overlay-header {
margin-bottom: 12px;
}
.overlay-header h3 {
font-size: 15px;
}
.ratio-hint {
white-space: normal;
}
.editor-section {
margin-bottom: 12px;
}
} }
</style> </style>