diff --git a/frontend/e2e/mobile-ui.spec.ts b/frontend/e2e/mobile-ui.spec.ts index e3cd6b8..32c889a 100644 --- a/frontend/e2e/mobile-ui.spec.ts +++ b/frontend/e2e/mobile-ui.spec.ts @@ -271,8 +271,8 @@ test('settings match the approved paper-ledger geometry and action hierarchy', a expect(layout.bodyOverflow).toBe(0) const isMobileContract = (await page.viewportSize())!.width <= 720 if (isMobileContract) { - expect(layout.leftPadding).toBeCloseTo(29, 0) - expect(layout.rightPadding).toBeCloseTo(29, 0) + expect(layout.leftPadding).toBeCloseTo(28, 0) + expect(layout.rightPadding).toBeCloseTo(28, 0) expect(layout.headingSize).toBe('24px') } else { const pageWidth = await page.locator('.settings-sections').evaluate(element => element.getBoundingClientRect().width) diff --git a/frontend/e2e/today-plain-list.spec.ts b/frontend/e2e/today-plain-list.spec.ts index e80305c..eb85791 100644 --- a/frontend/e2e/today-plain-list.spec.ts +++ b/frontend/e2e/today-plain-list.spec.ts @@ -60,7 +60,7 @@ test('Today plain-list layout matches the approved responsive geometry', async ( } })).toEqual({ media720: width <= 720, - contentWidth: width >= 1440 ? 1080 : width > 930 ? Math.min(1080, width - 324) : width === 721 ? 630 : width - 58, + contentWidth: width >= 1440 ? 1080 : width > 930 ? Math.min(1080, width - 324) : width === 721 ? 630 : width - 56, environmentHeight: width >= 721 ? 55 : width === 375 ? 79 : 81, }) await expect(page.locator('.today-heading')).toHaveCount(1) @@ -270,9 +270,9 @@ test('Today plain-list layout matches the approved responsive geometry', async ( expect(metrics.directions.weather).toBe('column') expect(metrics.directions.gold).toBe('column') if (width <= 720) { - expect(metrics.content.width).toBeCloseTo(width - 58, 0) - expect(metrics.content.left).toBeCloseTo(29, 0) - expect(metrics.styles.mainPaddingLeft).toBe('29px') + expect(metrics.content.width).toBeCloseTo(width - 56, 0) + expect(metrics.content.left).toBeCloseTo(28, 0) + expect(metrics.styles.mainPaddingLeft).toBe('28px') expect(metrics.styles.titleFontSize).toBe('24px') expect(metrics.environment.height).toBeCloseTo(width === 375 ? 79 : 81, 0) } diff --git a/frontend/e2e/upcoming-list-parity.spec.ts b/frontend/e2e/upcoming-list-parity.spec.ts index ddbc33e..b295840 100644 --- a/frontend/e2e/upcoming-list-parity.spec.ts +++ b/frontend/e2e/upcoming-list-parity.spec.ts @@ -78,8 +78,8 @@ test('Upcoming shares the task-list hierarchy while keeping its date scope and n expect(geometry.section.left).toBeCloseTo(geometry.list.left, 0) expect(geometry.header.width).toBeCloseTo(geometry.list.width, 0) if (geometry.viewportWidth <= 720) { - expect(geometry.header.left).toBeCloseTo(29, 0) - expect(geometry.list.right).toBeCloseTo(geometry.viewportWidth - 29, 0) + expect(geometry.header.left).toBeCloseTo(28, 0) + expect(geometry.list.right).toBeCloseTo(geometry.viewportWidth - 28, 0) } const fab = page.getByRole('button', { name: '添加任务' }) diff --git a/frontend/e2e/visual-regression.spec.ts-snapshots/inbox-mobile-390.png b/frontend/e2e/visual-regression.spec.ts-snapshots/inbox-mobile-390.png index 8c6a4de..2a4f31b 100644 Binary files a/frontend/e2e/visual-regression.spec.ts-snapshots/inbox-mobile-390.png and b/frontend/e2e/visual-regression.spec.ts-snapshots/inbox-mobile-390.png differ diff --git a/frontend/e2e/visual-regression.spec.ts-snapshots/task-composer-mobile-390.png b/frontend/e2e/visual-regression.spec.ts-snapshots/task-composer-mobile-390.png index a311b43..7c4dbb3 100644 Binary files a/frontend/e2e/visual-regression.spec.ts-snapshots/task-composer-mobile-390.png and b/frontend/e2e/visual-regression.spec.ts-snapshots/task-composer-mobile-390.png differ diff --git a/frontend/src/App.vue b/frontend/src/App.vue index c82d829..a37f38b 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -28,6 +28,7 @@ import AppDialog, { type AppDialogOptions } from './components/AppDialog.vue' import { shanghaiDateKey, useTaskDueClock, watchShanghaiDateRollover } from './lib/task-due-clock' import { readTodaySectionCollapse, writeTodaySectionCollapse, type TodaySectionCollapse } from './lib/today-section-collapse' import { loadTaskOpenTotal } from './lib/task-open-total' +import { failureHint } from './failureHint' type FolderItem = { id: string; name: string } type TaskList = { id: string; folder_id: string | null; name: string; is_inbox: boolean } @@ -470,7 +471,7 @@ async function api(path: string, options: RequestInit = {}) { ...options, }) if (!response.ok) { - let message = '请求失败' + let message = '请求失败,请检查网络后重试' try { const body = await response.json(); message = formatApiErrorDetail(body.detail) } catch { /* noop */ } const requestError = new Error(message) as Error & { status: number } requestError.status = response.status @@ -483,7 +484,7 @@ function toast(message: string) { notice.value = message window.setTimeout(() => { if (notice.value === message) notice.value = '' }, 2400) } -function fail(reason: unknown) { error.value = reason instanceof Error ? reason.message : '请求失败' } +function fail(reason: unknown) { error.value = failureHint('请求失败', reason) } async function syncBrowserTimezone(currentTimezone?: string) { const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone @@ -1138,7 +1139,7 @@ async function saveSelectedTaskChanges() { toast('已保存') } catch (reason) { if (recurrenceLoadToken !== selectionToken || selectedTask.value?.id !== taskId) return - selectedRepeatError.value = reason instanceof Error ? reason.message : '保存失败' + selectedRepeatError.value = failureHint('保存失败', reason) fail(reason) } finally { savingSelectedTask.value = false @@ -1443,7 +1444,7 @@ async function confirmPurgeList() { focusPurgeListTrigger() toast('清单已永久删除') } catch (reason) { - purgeListError.value = reason instanceof Error ? reason.message : '永久删除失败' + purgeListError.value = failureHint('永久删除失败', reason) } finally { purgeListSubmitting.value = false } @@ -1512,7 +1513,7 @@ async function saveListEditor() { listEditorFolderId.value = current.folder_id ?? '' } listEditorError.value = '名称已保存,但移动文件夹失败,请重试。' - } else listEditorError.value = reason instanceof Error ? reason.message : '保存失败' + } else listEditorError.value = reason instanceof Error ? reason.message : '保存失败,请检查网络后重试' } finally { listEditorBusy.value = false } } async function archiveListFromEditor() { @@ -1534,7 +1535,7 @@ async function archiveListFromEditor() { } toast('清单已归档') } catch (reason) { - listEditorError.value = reason instanceof Error ? reason.message : '归档失败' + listEditorError.value = failureHint('归档失败', reason) } finally { listEditorBusy.value = false } } function toggleFolder(id: string) { const next = new Set(expandedFolders.value); next.has(id) ? next.delete(id) : next.add(id); expandedFolders.value = next } diff --git a/frontend/src/CalendarIntegration.test.ts b/frontend/src/CalendarIntegration.test.ts index 1f42306..f7e54bf 100644 --- a/frontend/src/CalendarIntegration.test.ts +++ b/frontend/src/CalendarIntegration.test.ts @@ -4,7 +4,7 @@ const app=readFileSync('src/App.vue','utf8');const utils=readFileSync('src/lib/m describe('calendar shell integration',()=>{ it('puts calendar beside the other first-level tools on desktop and mobile',()=>{const nav=app.slice(app.indexOf('',app.indexOf('