fix: 手机编辑配方弹层合适宽度 + 打开时屏蔽 tab 滑动 #44

Merged
fam merged 7 commits from fix/mobile-edit-recipe-width into main 2026-04-18 19:13:05 +00:00
4 changed files with 122 additions and 19 deletions

View File

@@ -62,35 +62,48 @@ jobs:
exit 1 exit 1
fi fi
# Run all specs in 3 batches to avoid Electron memory crashes # Run specs in smaller batches (≤6 per run) to avoid Electron memory
# hangs seen when a single cypress run handles 10+ specs on the CI runner.
cd frontend cd frontend
CYPRESS_CFG="video=false,defaultCommandTimeout=5000,pageLoadTimeout=10000,requestTimeout=5000,responseTimeout=10000,baseUrl=http://localhost:$FE_PORT,experimentalMemoryManagement=true,numTestsKeptInMemory=0" CYPRESS_CFG="video=false,defaultCommandTimeout=5000,pageLoadTimeout=10000,requestTimeout=5000,responseTimeout=10000,baseUrl=http://localhost:$FE_PORT,experimentalMemoryManagement=true,numTestsKeptInMemory=0"
FAIL=0
echo "=== Batch 1: API & data tests ===" run_batch() {
local name="$1"; shift
echo "=== $name ==="
timeout 300 npx cypress run \ timeout 300 npx cypress run \
--spec "cypress/e2e/api-crud.cy.js,cypress/e2e/api-health.cy.js,cypress/e2e/oil-data-integrity.cy.js,cypress/e2e/recipe-cost-parity.cy.js,cypress/e2e/endpoint-parity.cy.js,cypress/e2e/registration-flow.cy.js,cypress/e2e/pr27-features.cy.js,cypress/e2e/kit-export.cy.js" \ --spec "$1" \
--config "$CYPRESS_CFG" --env "ADMIN_TOKEN=$ADMIN_TOKEN" --config "$CYPRESS_CFG" --env "ADMIN_TOKEN=$ADMIN_TOKEN"
B1=$? local rc=$?
if [ $rc -ne 0 ]; then
echo "!!! $name failed (rc=$rc)"
FAIL=1
fi
}
echo "=== Batch 2: UI flow tests ===" run_batch "Batch 1a: API parity" \
timeout 300 npx cypress run \ "cypress/e2e/api-crud.cy.js,cypress/e2e/api-health.cy.js,cypress/e2e/oil-data-integrity.cy.js,cypress/e2e/recipe-cost-parity.cy.js,cypress/e2e/endpoint-parity.cy.js"
--spec "cypress/e2e/auth-flow.cy.js,cypress/e2e/admin-flow.cy.js,cypress/e2e/navigation.cy.js,cypress/e2e/recipe-detail.cy.js,cypress/e2e/recipe-search.cy.js,cypress/e2e/manage-recipes.cy.js,cypress/e2e/diary-flow.cy.js,cypress/e2e/favorites.cy.js,cypress/e2e/inventory-flow.cy.js,cypress/e2e/demo-walkthrough.cy.js" \ run_batch "Batch 1b: features" \
--config "$CYPRESS_CFG" --env "ADMIN_TOKEN=$ADMIN_TOKEN" "cypress/e2e/registration-flow.cy.js,cypress/e2e/pr27-features.cy.js,cypress/e2e/kit-export.cy.js"
B2=$?
echo "=== Batch 3: Remaining tests ===" run_batch "Batch 2a: auth & nav" \
timeout 300 npx cypress run \ "cypress/e2e/auth-flow.cy.js,cypress/e2e/admin-flow.cy.js,cypress/e2e/navigation.cy.js,cypress/e2e/recipe-detail.cy.js,cypress/e2e/recipe-search.cy.js"
--spec "cypress/e2e/app-load.cy.js,cypress/e2e/account-settings.cy.js,cypress/e2e/audit-log-advanced.cy.js,cypress/e2e/batch-operations.cy.js,cypress/e2e/bug-tracker-flow.cy.js,cypress/e2e/category-modules.cy.js,cypress/e2e/notification-flow.cy.js,cypress/e2e/oil-reference.cy.js,cypress/e2e/oil-smart-paste.cy.js,cypress/e2e/performance.cy.js,cypress/e2e/price-display.cy.js,cypress/e2e/projects-flow.cy.js,cypress/e2e/responsive.cy.js,cypress/e2e/search-advanced.cy.js,cypress/e2e/user-management-flow.cy.js,cypress/e2e/visual-check.cy.js" \ run_batch "Batch 2b: user flows" \
--config "$CYPRESS_CFG" --env "ADMIN_TOKEN=$ADMIN_TOKEN" "cypress/e2e/manage-recipes.cy.js,cypress/e2e/diary-flow.cy.js,cypress/e2e/favorites.cy.js,cypress/e2e/inventory-flow.cy.js,cypress/e2e/demo-walkthrough.cy.js"
B3=$?
run_batch "Batch 3a: misc flows" \
"cypress/e2e/app-load.cy.js,cypress/e2e/account-settings.cy.js,cypress/e2e/audit-log-advanced.cy.js,cypress/e2e/batch-operations.cy.js,cypress/e2e/bug-tracker-flow.cy.js,cypress/e2e/category-modules.cy.js"
run_batch "Batch 3b: pages & perf" \
"cypress/e2e/notification-flow.cy.js,cypress/e2e/oil-reference.cy.js,cypress/e2e/oil-smart-paste.cy.js,cypress/e2e/performance.cy.js,cypress/e2e/price-display.cy.js,cypress/e2e/projects-flow.cy.js"
run_batch "Batch 3c: responsive & admin" \
"cypress/e2e/responsive.cy.js,cypress/e2e/search-advanced.cy.js,cypress/e2e/user-management-flow.cy.js,cypress/e2e/visual-check.cy.js"
# Cleanup # Cleanup
kill $BE_PID $FE_PID 2>/dev/null kill $BE_PID $FE_PID 2>/dev/null
pkill -f "Cypress" 2>/dev/null || true pkill -f "Cypress" 2>/dev/null || true
rm -f "$DB_FILE" rm -f "$DB_FILE"
echo "Results: Batch1=$B1 Batch2=$B2 Batch3=$B3" if [ $FAIL -ne 0 ]; then
if [ $B1 -ne 0 ] || [ $B2 -ne 0 ] || [ $B3 -ne 0 ]; then
exit 1 exit 1
fi fi

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

@@ -196,7 +196,7 @@ function onSwipeEnd() {
// Carousel area excluded // Carousel area excluded
if (swipeStartTarget.value?.closest?.('[data-no-tab-swipe]')) return if (swipeStartTarget.value?.closest?.('[data-no-tab-swipe]')) return
// Skip when modal/overlay is open // Skip when modal/overlay is open
if (document.querySelector('.modal-overlay, .detail-overlay, .dialog-overlay')) return 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)

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>