UI6 修复 iPhone11 企业微信移动导航

这个提交包含在:
2026-05-11 11:36:01 +08:00
父节点 a9128e50d5
当前提交 16588b0dae
共修改 8 个文件,包含 75 行新增和 26 行删除
@@ -34,10 +34,10 @@ export function MobileBottomNav() {
return (
<nav
className="fixed inset-x-0 bottom-0 z-50 border-t border-zinc-200 bg-white/95 px-2 pb-[max(0.35rem,env(safe-area-inset-bottom))] pt-1.5 shadow-[0_-6px_18px_rgba(15,23,42,0.08)] backdrop-blur md:hidden"
className="pointer-events-auto fixed inset-x-0 bottom-0 z-[70] min-h-[calc(var(--mobile-bottom-nav-height,64px)+env(safe-area-inset-bottom,0px))] transform-gpu border-t border-zinc-200 bg-white/95 px-2 pb-[calc(0.35rem+env(safe-area-inset-bottom,0px))] pt-1.5 shadow-[0_-6px_18px_rgba(15,23,42,0.08)] backdrop-blur md:hidden"
data-testid="mobile-bottom-nav"
>
<div className="grid grid-cols-4 gap-1">
<div className="relative z-[71] grid grid-cols-4 gap-1">
{MOBILE_NAV_ITEMS.map((item) => {
const disabled = item.anyOf ? !hasAnyPermission(permissions, [...item.anyOf]) : false;
const badge = item.key === "approvals" && pendingCount > 0 ? resolveBadgeLabel(pendingCount) : null;
@@ -48,13 +48,13 @@ export function MobileBottomNav() {
key={item.key}
type="button"
data-testid={`mobile-nav-${item.key}`}
className="touch-target flex h-12 min-h-0 flex-col items-center justify-center gap-0.5 rounded-xl px-0.5 py-1 text-[11px] font-medium leading-none text-zinc-400"
className="touch-target flex h-12 min-h-0 min-w-0 select-none flex-col items-center justify-center gap-0.5 rounded-xl px-0.5 py-1 text-[11px] font-medium leading-none text-zinc-400"
disabled
aria-disabled="true"
title={`${item.label}不可用`}
>
<span className="material-symbols-outlined text-[20px]">{item.icon}</span>
<span className="whitespace-nowrap">{item.label}</span>
<span className="max-w-full overflow-hidden text-ellipsis whitespace-nowrap">{item.label}</span>
</button>
);
}
@@ -67,7 +67,7 @@ export function MobileBottomNav() {
data-testid={`mobile-nav-${item.key}`}
className={({ isActive }) =>
cn(
"touch-target flex h-12 min-h-0 flex-col items-center justify-center gap-0.5 rounded-xl px-0.5 py-1 text-[11px] font-medium leading-none transition-colors",
"touch-target pointer-events-auto flex h-12 min-h-0 min-w-0 select-none flex-col items-center justify-center gap-0.5 rounded-xl px-0.5 py-1 text-[11px] font-medium leading-none transition-colors",
isActive
? "bg-primary text-white shadow-sm"
: "text-zinc-500 hover:bg-zinc-50 hover:text-zinc-900",
@@ -89,7 +89,7 @@ export function MobileBottomNav() {
</span>
) : null}
</span>
<span className="whitespace-nowrap">{item.label}</span>
<span className="max-w-full overflow-hidden text-ellipsis whitespace-nowrap">{item.label}</span>
</>
)}
</NavLink>
+1 -1
查看文件
@@ -29,7 +29,7 @@ export type MobileNavItem = {
export const MOBILE_NAV_ITEMS: MobileNavItem[] = [
{ key: "home", to: "/mobile", label: "首页", icon: "home" },
{ key: "approvals", to: "/mobile/approvals", label: "审批", icon: "fact_check", anyOf: MOBILE_APPROVAL_PERMISSIONS },
{ key: "approvals", to: "/mobile/approvals", label: "审批", icon: "fact_check" },
{ key: "reimbursements", to: "/mobile/reimbursements", label: "报销", icon: "request_quote", anyOf: MOBILE_REIMBURSEMENT_PERMISSIONS },
{ key: "me", to: "/mobile/me", label: "我的", icon: "person" },
];
+7 -3
查看文件
@@ -23,19 +23,23 @@ export function AppShellLayout() {
}
const updateViewportHeight = () => {
const height = window.visualViewport?.height ?? window.innerHeight;
const height = Math.max(320, Math.floor(window.visualViewport?.height ?? window.innerHeight));
document.documentElement.style.setProperty("--mobile-viewport-height", `${height}px`);
};
updateViewportHeight();
const frameId = window.requestAnimationFrame(updateViewportHeight);
window.addEventListener("resize", updateViewportHeight);
window.addEventListener("orientationchange", updateViewportHeight);
window.visualViewport?.addEventListener("resize", updateViewportHeight);
window.visualViewport?.addEventListener("scroll", updateViewportHeight);
return () => {
window.cancelAnimationFrame(frameId);
window.removeEventListener("resize", updateViewportHeight);
window.removeEventListener("orientationchange", updateViewportHeight);
window.visualViewport?.removeEventListener("resize", updateViewportHeight);
window.visualViewport?.removeEventListener("scroll", updateViewportHeight);
};
}, [isMobileRoute]);
@@ -57,11 +61,11 @@ export function AppShellLayout() {
<main
className={`flex min-w-0 flex-1 flex-col ${isMobileRoute ? "overflow-hidden" : "lg:ml-72"}`}
style={isMobileRoute ? { height: "var(--mobile-viewport-height, 100vh)", minHeight: "var(--mobile-viewport-height, 100vh)" } : undefined}
style={isMobileRoute ? { height: "var(--mobile-viewport-height, 100vh)", maxHeight: "var(--mobile-viewport-height, 100vh)", minHeight: "var(--mobile-viewport-height, 100vh)" } : undefined}
>
{isMobileRoute ? null : <Topbar />}
<div className={`flex-1 px-4 py-5 ${isMobileRoute ? `min-h-0 overflow-y-auto pt-4 sm:px-5 sm:pt-5 ${showMobileBottomNav ? "pb-[calc(var(--mobile-bottom-nav-height,64px)+env(safe-area-inset-bottom,0px)+1rem)]" : "pb-5 sm:pb-6"}` : "sm:px-7 sm:py-7 lg:px-8"}`}>
<div className={`flex-1 px-4 py-5 ${isMobileRoute ? `min-h-0 overscroll-contain overflow-y-auto px-3 py-3 sm:px-5 sm:py-5 ${showMobileBottomNav ? "pb-[calc(var(--mobile-bottom-nav-height,64px)+env(safe-area-inset-bottom,0px)+1rem)]" : "pb-5 sm:pb-6"}` : "sm:px-7 sm:py-7 lg:px-8"}`}>
<div className="layout-container max-w-none">
<ErrorBoundary>
<Outlet />
+10 -5
查看文件
@@ -16,6 +16,7 @@ import {
type WorkflowTaskSummary,
} from "../api/workflow";
import { useAuth } from "../auth/AuthContext";
import { hasAnyPermission } from "../auth/permissions";
import { PageHeader } from "../components/common/PageHeader";
import { StatusBadge } from "../components/common/StatusBadge";
import { PageScaffold } from "../components/patterns/PageScaffold";
@@ -24,6 +25,7 @@ import { Input } from "../components/ui/input";
import { Spinner } from "../components/ui/spinner";
import { Tabs, TabsList, TabsTrigger } from "../components/ui/tabs";
import { Textarea } from "../components/ui/textarea";
import { MOBILE_APPROVAL_PERMISSIONS } from "../features/mobile/navigation";
import {
cacheWorkflowTaskDetail,
enqueueWorkflowActionDraft,
@@ -252,7 +254,7 @@ function matchesSearch(card: VisibleTaskCard, keyword: string) {
export function MobileApprovalsPage() {
const queryClient = useQueryClient();
const { me, accessToken, offlineAccessMode } = useAuth();
const { me, permissions, accessToken, offlineAccessMode } = useAuth();
const [searchParams, setSearchParams] = useSearchParams();
const deepLinkTaskId = searchParams.get("taskId")?.trim() ?? "";
@@ -275,7 +277,8 @@ export function MobileApprovalsPage() {
const detailSectionRef = useRef<HTMLElement | null>(null);
const ownerEmployeeId = me?.id ?? null;
const canUseRemote = isOnline && Boolean(accessToken);
const canAccessRemoteApprovals = hasAnyPermission(permissions, [...MOBILE_APPROVAL_PERMISSIONS]);
const canUseRemote = isOnline && Boolean(accessToken) && canAccessRemoteApprovals;
const remoteTaskStatus = taskTab === "completed" ? "completed" : "pending";
useEffect(() => {
@@ -944,9 +947,11 @@ export function MobileApprovalsPage() {
<p className="text-sm text-zinc-500">
{taskTab === "completed"
? "最近 30 天暂无符合筛选条件的已处理任务。"
: canUseRemote
? "暂无符合筛选条件的待办任务。"
: "暂无符合筛选条件的已缓存待办。"}
: !canAccessRemoteApprovals && isOnline
? "暂无审批待办;收到企业微信审批消息后可从这里查看。"
: canUseRemote
? "暂无符合筛选条件的待办任务。"
: "暂无符合筛选条件的已缓存待办。"}
</p>
) : null}
+1 -5
查看文件
@@ -358,11 +358,7 @@ export function AppRouter() {
<Route
path="mobile/approvals"
element={
<RequirePermission anyOf={["expense.approve", "leave.approve", "procurement.approve", "travel.approve"]}>
{withRouteFallback(<MobileApprovalsPage />)}
</RequirePermission>
}
element={withRouteFallback(<MobileApprovalsPage />)}
/>
<Route path="notifications" element={withRouteFallback(<NotificationsPage />)} />
@@ -3,7 +3,7 @@ import path from "node:path";
import { expect, test, type Page, type Route } from "@playwright/test";
const ROOT_DIR = path.resolve(process.cwd(), "..");
const SCREENSHOT_DIR = path.join(ROOT_DIR, "output/playwright/r5-real-device-feedback-mobile-ux");
const SCREENSHOT_DIR = path.join(ROOT_DIR, "output/playwright/r6-iphone11-wecom-mobile-layout-hotfix");
const DEVICE_CASES = [
{
key: "iphone11-ios14",
@@ -164,6 +164,26 @@ async function mockApis(page: Page) {
return;
}
if (pathName === "/api/workflow/tasks" && request.method() === "GET") {
await fulfillJson(route, {
page: 1,
pageSize: 100,
total: 0,
items: [],
});
return;
}
if (pathName === "/api/workflow/approvers" && request.method() === "GET") {
await fulfillJson(route, {
page: 1,
pageSize: 20,
total: 0,
items: [],
});
return;
}
if (pathName === "/api/expense-types") {
await fulfillJson(route, {
page: 1,
@@ -219,11 +239,22 @@ async function collectMobileMetrics(page: Page) {
const navItems = ["home", "approvals", "reimbursements", "me"].map((key) => {
const element = document.querySelector(`[data-testid='mobile-nav-${key}']`);
const rect = element?.getBoundingClientRect();
const centerX = rect ? rect.left + rect.width / 2 : 0;
const centerY = rect ? rect.top + rect.height / 2 : 0;
const hit = rect ? document.elementFromPoint(centerX, centerY) : null;
const hitOwner = hit instanceof Element ? hit.closest(`[data-testid='mobile-nav-${key}']`) : null;
return {
key,
visible: Boolean(rect && rect.width > 0 && rect.height > 0),
width: rect?.width ?? 0,
height: rect?.height ?? 0,
centerX,
centerY,
hitWithin: Boolean(element && hitOwner === element),
hitTarget:
hit instanceof HTMLElement
? hit.dataset.testid ?? hit.getAttribute("aria-label") ?? hit.tagName.toLowerCase()
: null,
};
});
const stickyActions = document.querySelector("[data-testid='mobile-reimbursement-sticky-actions']");
@@ -287,6 +318,19 @@ test.describe("R5 真机反馈移动交互", () => {
expect(item.visible, `${device.label} ${item.key} nav item visible`).toBe(true);
expect(item.width, `${device.label} ${item.key} nav item touch width`).toBeGreaterThanOrEqual(44);
expect(item.height, `${device.label} ${item.key} nav item touch height`).toBeGreaterThanOrEqual(44);
expect(item.hitWithin, `${device.label} ${item.key} nav item hit target: ${item.hitTarget}`).toBe(true);
}
await page.getByTestId("mobile-nav-approvals").click();
await expect(page).toHaveURL(/\/mobile\/approvals$/);
await expect(page.getByTestId("mobile-approvals-list")).toBeVisible();
await page.screenshot({ path: screenshotPath(`${device.key}-mobile-approvals-tab-hit`) });
const approvalsMetrics = await collectMobileMetrics(page);
expect(approvalsMetrics.bottomNavVisible).toBe(true);
expect(approvalsMetrics.bottomNavFixed).toBe(true);
expect(approvalsMetrics.scrollWidth).toBeLessThanOrEqual(approvalsMetrics.width + 2);
for (const item of approvalsMetrics.navItems) {
expect(item.hitWithin, `${device.label} ${item.key} approvals page hit target: ${item.hitTarget}`).toBe(true);
}
await page.goto("/mobile/reimbursements");
+1 -1
查看文件
@@ -46,7 +46,7 @@
| ID | 级别 | 角色 | 入口/设备 | 流程 | 复现步骤 | 当前处理 | 负责人 | 状态 | 关联 Issue |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| R6-ISSUE-001 | 阻断 | 审批人 / 员工 | iPhone 11 / 企业微信 WebView | 移动底部导航、审批入口 | iPhone 11 使用企业微信打开 OA,底部菜单第二项“审批”点不了,页面控件和文字看上去挤在一起 | 已建缺陷合同,待自动化复现和修复 | Codex / 待指派 | open | `issues/2026-05-11_10-46-10-r6-iphone11-wecom-mobile-layout-hotfix.csv` |
| R6-ISSUE-001 | 阻断 | 审批人 / 员工 | iPhone 11 / 企业微信 WebView | 移动底部导航、审批入口 | iPhone 11 使用企业微信打开 OA,底部菜单第二项“审批”点不了,页面控件和文字看上去挤在一起 | 本地已修复并通过 iPhone 11 / Redmi K60 视觉回归,待部署后真机复测 | Codex / 待指派 | open | `issues/2026-05-11_10-46-10-r6-iphone11-wecom-mobile-layout-hotfix.csv` |
级别定义:
@@ -1,7 +1,7 @@
ID,Title,Description,Acceptance,Test_Method,Tools,Dev_Status,Review1_Status,Regression_Status,Files,Dependencies,Notes
UI6-1,记录 iPhone 11 企业微信移动布局阻断,记录 R6 前发现的 iPhone 11 企业微信 WebView 页面拥挤和底部菜单第二项不可点击问题;本项只建档不改业务代码。,"1) 本 CSV 存在且校验通过;2) docs/trialrun/manual-trialrun-report.md 已登记 R6-ISSUE-001;3) R6-2 仍保持 TODO;4) 不改 client/server 业务代码。","流程级验证:python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-05-11_10-46-10-r6-iphone11-wecom-mobile-layout-hotfix.csv;人工核对 R6 问题登记含 iPhone 11 企业微信底部菜单第二项不可点和页面拥挤;git diff --check。",python3|git|manual,DONE,DONE,DONE,issues/2026-05-11_10-46-10-r6-iphone11-wecom-mobile-layout-hotfix.csv|docs/trialrun/manual-trialrun-report.md,[],本轮先建档;代码修复需用户批准后进入 UI6-2。
UI6-2,补 iPhone 11 企业微信命中测试,补强 Playwright 视觉和命中测试,覆盖 iPhone 11 iOS14 企业微信 UA 下底部第二项审批 Tab 可点击、无遮挡层吃点击、无横向滚动、页面控件不拥挤。,"1) 测试能在本地或公网 baseURL 下复现真实设备风险;2) 断言 mobile-nav-approvals 点击后进入 /mobile/approvals;3) 断言四个底部 Tab 的中心点 elementFromPoint 命中自身或其子元素;4) 截图输出到 output/playwright/r6-iphone11-wecom-mobile-layout-hotfix。","流程级验证:启动本地前端或设置 E2E_MODE=live 后运行移动视觉专项;检查 iPhone 11 与 Redmi K60 截图;确认当前问题可复现或测试覆盖真实风险。",playwright|manual,TODO,TODO,TODO,client/tests/e2e/*|output/playwright/r6-iphone11-wecom-mobile-layout-hotfix,"[""UI6-1""]",现有 r5-real-device-feedback-mobile-ux 覆盖固定和尺寸,但缺少底部第二项点击命中与 elementFromPoint 遮挡断言;直接运行时如无本地 3000 服务会连接失败。
UI6-3,修复底部 Tab 固定与点击命中,修复移动 Shell 在 iPhone 11 企业微信 WebView 下底部导航不在真实可点区域或被内容层遮挡的问题。,"1) 底部 Tab 固定在可见视口底部并预留 safe-area;2) 四个 Tab 等宽且触控高度不低于 44px;3) 第二项审批 Tab 可点击进入审批列表;4) 不影响 Redmi K60 和普通手机浏览器。","流程级验证:在 iPhone 11 企业微信 UA 下从 /mobile 点击底部审批,进入 /mobile/approvals 并显示 mobile-approvals-list;重复点击首页、报销、我的均可命中;无横向滚动。",playwright|manual,TODO,TODO,TODO,client/src/*,"[""UI6-2""]",修复方向优先检查 viewport height 变量、fixed bottom safe-area、z-index、内容区 padding-bottom、底部导航 pointer-events 和覆盖层。
UI6-4,修复 iPhone 11 页面拥挤和控件挤压,调整移动首页、审批、报销和我的页面的小屏密度,避免文字和控件在 iPhone 11 企业微信 WebView 内相互挤压。,"1) iPhone 11 下核心卡片和按钮不重叠;2) 文字不挤出容器且不遮挡相邻控件;3) 保留报销详情吸底双按钮同行,不降级为单列;4) 移动端不出现 PC 表格或 PC 壳。","流程级验证:Playwright 截图检查 /mobile、/mobile/approvals、/mobile/reimbursements、/mobile/me、/mobile/reimbursements/:id;断言 scrollWidth <= viewportWidth + 2;人工视觉确认控件间距可读可点。",playwright|manual,TODO,TODO,TODO,client/src/*,"[""UI6-2""]",核心主操作仍保持双按钮同行;不要用单列按钮回避 iPhone 11 适配问题。
UI6-5,本地构建与移动视觉回归,完成前端类型检查、构建和移动专项 E2E,覆盖 iPhone 11、Redmi K60、企业微信 UA、底部导航、审批入口、报销详情和 PDF 上传入口。,"1) cd client && npm run typecheck 通过;2) cd client && npm run build 通过;3) 移动视觉专项通过;4) 截图证据保存在 output/playwright/r6-iphone11-wecom-mobile-layout-hotfix。","流程级验证:cd client && npm run typecheck;cd client && npm run build;cd client && npm run test:e2e -- r5-real-device-feedback-mobile-ux.spec.ts r124-mobile-entry-matrix.spec.ts --workers=1 --reporter=list。",npm|playwright,TODO,TODO,TODO,client/tests/e2e/*|output/playwright/r6-iphone11-wecom-mobile-layout-hotfix,"[""UI6-3"",""UI6-4""]",如本地无 3000 服务,先启动受控本地前端或改用 live baseURL;失败需记录原因。
UI6-2,补 iPhone 11 企业微信命中测试,补强 Playwright 视觉和命中测试,覆盖 iPhone 11 iOS14 企业微信 UA 下底部第二项审批 Tab 可点击、无遮挡层吃点击、无横向滚动、页面控件不拥挤。,"1) 测试能在本地或公网 baseURL 下复现真实设备风险;2) 断言 mobile-nav-approvals 点击后进入 /mobile/approvals;3) 断言四个底部 Tab 的中心点 elementFromPoint 命中自身或其子元素;4) 截图输出到 output/playwright/r6-iphone11-wecom-mobile-layout-hotfix。","流程级验证:启动本地前端或设置 E2E_MODE=live 后运行移动视觉专项;检查 iPhone 11 与 Redmi K60 截图;确认当前问题可复现或测试覆盖真实风险。",playwright|manual,DONE,DONE,DONE,client/tests/e2e/*|output/playwright/r6-iphone11-wecom-mobile-layout-hotfix,"[""UI6-1""]",已补 iPhone 11 / Redmi K60 底部 Tab 中心点 elementFromPoint 命中断言,并验证 mobile-nav-approvals 点击进入 /mobile/approvals;本地 E2E 通过。
UI6-3,修复底部 Tab 固定与点击命中,修复移动 Shell 在 iPhone 11 企业微信 WebView 下底部导航不在真实可点区域或被内容层遮挡的问题。,"1) 底部 Tab 固定在可见视口底部并预留 safe-area;2) 四个 Tab 等宽且触控高度不低于 44px;3) 第二项审批 Tab 可点击进入审批列表;4) 不影响 Redmi K60 和普通手机浏览器。","流程级验证:在 iPhone 11 企业微信 UA 下从 /mobile 点击底部审批,进入 /mobile/approvals 并显示 mobile-approvals-list;重复点击首页、报销、我的均可命中;无横向滚动。",playwright|manual,DONE,DONE,DONE,client/src/*,"[""UI6-2""]",移动审批一级 Tab 已取消前端权限禁用和路由 403;底部导航提升层级、触控区域和 safe-area 预留;iPhone 11 / Redmi K60 E2E 通过。
UI6-4,修复 iPhone 11 页面拥挤和控件挤压,调整移动首页、审批、报销和我的页面的小屏密度,避免文字和控件在 iPhone 11 企业微信 WebView 内相互挤压。,"1) iPhone 11 下核心卡片和按钮不重叠;2) 文字不挤出容器且不遮挡相邻控件;3) 保留报销详情吸底双按钮同行,不降级为单列;4) 移动端不出现 PC 表格或 PC 壳。","流程级验证:Playwright 截图检查 /mobile、/mobile/approvals、/mobile/reimbursements、/mobile/me、/mobile/reimbursements/:id;断言 scrollWidth <= viewportWidth + 2;人工视觉确认控件间距可读可点。",playwright|manual,DONE,DONE,DONE,client/src/*,"[""UI6-2""]",移动 Shell 改为可视视口高度、紧凑小屏 padding 和 overscroll 约束;E2E 断言无横向滚动、无 PC 表格、报销详情吸底双按钮仍同行。
UI6-5,本地构建与移动视觉回归,完成前端类型检查、构建和移动专项 E2E,覆盖 iPhone 11、Redmi K60、企业微信 UA、底部导航、审批入口、报销详情和 PDF 上传入口。,"1) cd client && npm run typecheck 通过;2) cd client && npm run build 通过;3) 移动视觉专项通过;4) 截图证据保存在 output/playwright/r6-iphone11-wecom-mobile-layout-hotfix。","流程级验证:cd client && npm run typecheck;cd client && npm run build;cd client && npm run test:e2e -- r5-real-device-feedback-mobile-ux.spec.ts r124-mobile-entry-matrix.spec.ts --workers=1 --reporter=list。",npm|playwright,DONE,DONE,DONE,client/tests/e2e/*|output/playwright/r6-iphone11-wecom-mobile-layout-hotfix,"[""UI6-3"",""UI6-4""]",已通过 npm run typecheck、npm run build、r5-real-device-feedback-mobile-ux.spec.ts、r124-mobile-entry-matrix.spec.ts;UI6-6 部署与真机复测仍待执行。
UI6-6,部署复测并恢复 R6 Gate,部署修复后做 R4 复检和企业微信真机复测,只有 iPhone 11 底部导航与拥挤问题关闭后才允许继续 R6-3。,"1) 部署后 /ready 正常;2) 公网 bundle findings=0;3) iPhone 11 企业微信底部第二项可点;4) Redmi K60 不回归;5) R6-ISSUE-001 状态关闭或降级为非阻断。","流程级验证:bash deploy/trialrun/deploy.sh;LIVE_SMOKE_BASE_URL=https://oa.hyinda.com bash scripts/qa/run-live-smoke-readiness.sh;人工真机复测 iPhone 11 企业微信打开 OA 并点击底部审批;回填 docs/trialrun/manual-trialrun-report.md。",bash|playwright|manual,TODO,TODO,TODO,deploy/trialrun/*|docs/trialrun/manual-trialrun-report.md,"[""UI6-5""]",部署需单独批准;未部署前不得让用户按公网验证修复结果。
1 ID Title Description Acceptance Test_Method Tools Dev_Status Review1_Status Regression_Status Files Dependencies Notes
2 UI6-1 记录 iPhone 11 企业微信移动布局阻断 记录 R6 前发现的 iPhone 11 企业微信 WebView 页面拥挤和底部菜单第二项不可点击问题;本项只建档不改业务代码。 1) 本 CSV 存在且校验通过;2) docs/trialrun/manual-trialrun-report.md 已登记 R6-ISSUE-001;3) R6-2 仍保持 TODO;4) 不改 client/server 业务代码。 流程级验证:python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-05-11_10-46-10-r6-iphone11-wecom-mobile-layout-hotfix.csv;人工核对 R6 问题登记含 iPhone 11 企业微信底部菜单第二项不可点和页面拥挤;git diff --check。 python3|git|manual DONE DONE DONE issues/2026-05-11_10-46-10-r6-iphone11-wecom-mobile-layout-hotfix.csv|docs/trialrun/manual-trialrun-report.md [] 本轮先建档;代码修复需用户批准后进入 UI6-2。
3 UI6-2 补 iPhone 11 企业微信命中测试 补强 Playwright 视觉和命中测试,覆盖 iPhone 11 iOS14 企业微信 UA 下底部第二项审批 Tab 可点击、无遮挡层吃点击、无横向滚动、页面控件不拥挤。 1) 测试能在本地或公网 baseURL 下复现真实设备风险;2) 断言 mobile-nav-approvals 点击后进入 /mobile/approvals;3) 断言四个底部 Tab 的中心点 elementFromPoint 命中自身或其子元素;4) 截图输出到 output/playwright/r6-iphone11-wecom-mobile-layout-hotfix。 流程级验证:启动本地前端或设置 E2E_MODE=live 后运行移动视觉专项;检查 iPhone 11 与 Redmi K60 截图;确认当前问题可复现或测试覆盖真实风险。 playwright|manual TODO DONE TODO DONE TODO DONE client/tests/e2e/*|output/playwright/r6-iphone11-wecom-mobile-layout-hotfix ["UI6-1"] 现有 r5-real-device-feedback-mobile-ux 覆盖固定和尺寸,但缺少底部第二项点击命中与 elementFromPoint 遮挡断言;直接运行时如无本地 3000 服务会连接失败。 已补 iPhone 11 / Redmi K60 底部 Tab 中心点 elementFromPoint 命中断言,并验证 mobile-nav-approvals 点击进入 /mobile/approvals;本地 E2E 通过。
4 UI6-3 修复底部 Tab 固定与点击命中 修复移动 Shell 在 iPhone 11 企业微信 WebView 下底部导航不在真实可点区域或被内容层遮挡的问题。 1) 底部 Tab 固定在可见视口底部并预留 safe-area;2) 四个 Tab 等宽且触控高度不低于 44px;3) 第二项审批 Tab 可点击进入审批列表;4) 不影响 Redmi K60 和普通手机浏览器。 流程级验证:在 iPhone 11 企业微信 UA 下从 /mobile 点击底部审批,进入 /mobile/approvals 并显示 mobile-approvals-list;重复点击首页、报销、我的均可命中;无横向滚动。 playwright|manual TODO DONE TODO DONE TODO DONE client/src/* ["UI6-2"] 修复方向优先检查 viewport height 变量、fixed bottom safe-area、z-index、内容区 padding-bottom、底部导航 pointer-events 和覆盖层。 移动审批一级 Tab 已取消前端权限禁用和路由 403;底部导航提升层级、触控区域和 safe-area 预留;iPhone 11 / Redmi K60 E2E 通过。
5 UI6-4 修复 iPhone 11 页面拥挤和控件挤压 调整移动首页、审批、报销和我的页面的小屏密度,避免文字和控件在 iPhone 11 企业微信 WebView 内相互挤压。 1) iPhone 11 下核心卡片和按钮不重叠;2) 文字不挤出容器且不遮挡相邻控件;3) 保留报销详情吸底双按钮同行,不降级为单列;4) 移动端不出现 PC 表格或 PC 壳。 流程级验证:Playwright 截图检查 /mobile、/mobile/approvals、/mobile/reimbursements、/mobile/me、/mobile/reimbursements/:id;断言 scrollWidth <= viewportWidth + 2;人工视觉确认控件间距可读可点。 playwright|manual TODO DONE TODO DONE TODO DONE client/src/* ["UI6-2"] 核心主操作仍保持双按钮同行;不要用单列按钮回避 iPhone 11 适配问题。 移动 Shell 改为可视视口高度、紧凑小屏 padding 和 overscroll 约束;E2E 断言无横向滚动、无 PC 表格、报销详情吸底双按钮仍同行。
6 UI6-5 本地构建与移动视觉回归 完成前端类型检查、构建和移动专项 E2E,覆盖 iPhone 11、Redmi K60、企业微信 UA、底部导航、审批入口、报销详情和 PDF 上传入口。 1) cd client && npm run typecheck 通过;2) cd client && npm run build 通过;3) 移动视觉专项通过;4) 截图证据保存在 output/playwright/r6-iphone11-wecom-mobile-layout-hotfix。 流程级验证:cd client && npm run typecheck;cd client && npm run build;cd client && npm run test:e2e -- r5-real-device-feedback-mobile-ux.spec.ts r124-mobile-entry-matrix.spec.ts --workers=1 --reporter=list。 npm|playwright TODO DONE TODO DONE TODO DONE client/tests/e2e/*|output/playwright/r6-iphone11-wecom-mobile-layout-hotfix ["UI6-3","UI6-4"] 如本地无 3000 服务,先启动受控本地前端或改用 live baseURL;失败需记录原因。 已通过 npm run typecheck、npm run build、r5-real-device-feedback-mobile-ux.spec.ts、r124-mobile-entry-matrix.spec.ts;UI6-6 部署与真机复测仍待执行。
7 UI6-6 部署复测并恢复 R6 Gate 部署修复后做 R4 复检和企业微信真机复测,只有 iPhone 11 底部导航与拥挤问题关闭后才允许继续 R6-3。 1) 部署后 /ready 正常;2) 公网 bundle findings=0;3) iPhone 11 企业微信底部第二项可点;4) Redmi K60 不回归;5) R6-ISSUE-001 状态关闭或降级为非阻断。 流程级验证:bash deploy/trialrun/deploy.sh;LIVE_SMOKE_BASE_URL=https://oa.hyinda.com bash scripts/qa/run-live-smoke-readiness.sh;人工真机复测 iPhone 11 企业微信打开 OA 并点击底部审批;回填 docs/trialrun/manual-trialrun-report.md。 bash|playwright|manual TODO TODO TODO deploy/trialrun/*|docs/trialrun/manual-trialrun-report.md ["UI6-5"] 部署需单独批准;未部署前不得让用户按公网验证修复结果。