MME6 优化移动我的页自适应视觉

这个提交包含在:
2026-05-11 23:13:33 +08:00
父节点 b80af78613
当前提交 6359612cd0
共修改 8 个文件,包含 174 行新增和 11 行删除
+1 -1
查看文件
@@ -60,7 +60,7 @@ export function AppShellLayout() {
{isMobileRoute ? null : <Sidebar />}
<main
className={`flex min-w-0 flex-1 flex-col ${isMobileRoute ? "overflow-hidden" : "lg:ml-72"}`}
className={`flex min-w-0 flex-1 flex-col ${isMobileRoute ? "hyb-mobile-shell overflow-hidden" : "lg:ml-72"}`}
style={isMobileRoute ? { height: "var(--mobile-viewport-height, 100vh)", maxHeight: "var(--mobile-viewport-height, 100vh)", minHeight: "var(--mobile-viewport-height, 100vh)" } : undefined}
>
{isMobileRoute ? null : <Topbar />}
+14 -9
查看文件
@@ -38,10 +38,11 @@ function MobileActionLink({
return (
<Link
to={to}
className="flex min-h-12 items-center gap-3 rounded-lg border border-zinc-200 bg-white px-3 py-2 text-left shadow-sm transition-colors hover:bg-zinc-50"
className="mobile-me-action-link"
data-testid="mobile-me-action-link"
>
<MobileIcon name={icon} className="size-8 rounded-lg bg-zinc-100 p-1.5 text-zinc-700" />
<span className="min-w-0">
<MobileIcon name={icon} className="mobile-me-action-icon" />
<span className="mobile-me-action-copy">
<span className="block text-sm font-semibold text-zinc-900">{label}</span>
{description ? <span className="mt-0.5 block text-xs leading-5 text-zinc-500">{description}</span> : null}
</span>
@@ -81,10 +82,14 @@ export function MobileMePage() {
<PageHeader title="我的" />
<SectionPanel title="个人信息">
<div className="flex items-center gap-3">
<EmployeeAvatar name={me?.name ?? "员工"} src={profile?.avatarUrl} size="md" />
<div className="min-w-0 space-y-1">
<div className="text-lg font-semibold text-zinc-900">{me?.name ?? "-"}</div>
<div className="mobile-me-profile-row" data-testid="mobile-me-profile-row">
<div className="mobile-me-avatar-slot" data-testid="mobile-me-avatar-slot">
<EmployeeAvatar name={me?.name ?? "员工"} src={profile?.avatarUrl} size="md" />
</div>
<div className="mobile-me-profile-copy" data-testid="mobile-me-profile-copy">
<div className="text-lg font-semibold leading-7 text-zinc-900" data-testid="mobile-me-profile-name">
{me?.name ?? "-"}
</div>
<div className="text-sm text-zinc-500">{departmentName}</div>
{me?.employeeNo ? <div className="text-xs text-zinc-400">员工号:{me.employeeNo}</div> : null}
</div>
@@ -108,7 +113,7 @@ export function MobileMePage() {
</SectionPanel>
<SectionPanel title="更多入口">
<div className="mobile-adaptive-grid" data-testid="mobile-more-links">
<div className="mobile-me-entry-grid" data-testid="mobile-more-links">
<MobileActionLink to="/mobile/notifications" label="通知中心" icon="notifications" />
<MobileActionLink to="/mobile/leave" label="请假" icon="event_available" />
<MobileActionLink to="/mobile/reimbursements" label="报销" icon="request_quote" />
@@ -120,7 +125,7 @@ export function MobileMePage() {
{canAccessAdmin ? (
<SectionPanel title="管理入口">
<div className="mobile-adaptive-grid">
<div className="mobile-me-entry-grid">
<MobileActionLink to="/mobile/me" label="人员管理" icon="group" />
<MobileActionLink to="/mobile/me" label="组织架构" icon="account_tree" />
<MobileActionLink to="/mobile/me" label="系统设置" icon="settings" />
+89
查看文件
@@ -16,6 +16,8 @@
--mobile-bottom-nav-height: 68px;
--mobile-sticky-action-height: 78px;
--mobile-icon-action-min-height: 72px;
--mobile-me-slot-gap: clamp(12px, 3.7vw, 16px);
--mobile-me-action-icon-slot: 36px;
}
.page-scaffold.mobile-fixed-action-page {
@@ -37,6 +39,10 @@
padding: var(--mobile-card-padding);
}
.hyb-mobile-shell .section-panel::before {
display: none;
}
.mobile-icon-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
@@ -113,6 +119,87 @@
min-width: 0;
}
.mobile-me-entry-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--mobile-card-gap);
min-width: 0;
}
.mobile-me-profile-row {
display: grid;
grid-template-columns: 44px minmax(0, 1fr);
column-gap: var(--mobile-me-slot-gap);
align-items: center;
min-width: 0;
}
.mobile-me-avatar-slot {
width: 44px;
min-width: 44px;
display: flex;
align-items: center;
justify-content: center;
}
.mobile-me-profile-copy {
min-width: 0;
}
.mobile-me-profile-copy > * {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mobile-me-action-link {
display: grid;
grid-template-columns: var(--mobile-me-action-icon-slot) minmax(0, 1fr);
column-gap: var(--mobile-me-slot-gap);
align-items: center;
min-width: 0;
min-height: 48px;
border: 1px solid rgb(228 228 231);
border-radius: 10px;
background: rgb(255 255 255);
padding: 8px 12px;
text-align: left;
box-shadow: 0 1px 2px rgb(15 23 42 / 0.06);
transition: background-color 150ms ease, border-color 150ms ease;
}
.mobile-me-action-link:hover {
background: rgb(250 250 250);
}
.mobile-me-action-icon {
width: var(--mobile-me-action-icon-slot);
height: var(--mobile-me-action-icon-slot);
border-radius: 10px;
background: rgb(244 244 245);
color: rgb(63 63 70);
padding: 7px;
}
.mobile-me-action-icon svg {
display: block;
width: 100%;
height: 100%;
}
.mobile-me-action-copy {
display: block;
min-width: 0;
}
.mobile-me-action-copy > * {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mobile-info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 7.5rem), 1fr));
@@ -151,6 +238,8 @@
--mobile-card-gap: 6px;
--mobile-card-padding: 10px;
--mobile-icon-action-min-height: 68px;
--mobile-me-slot-gap: 12px;
--mobile-me-action-icon-slot: 34px;
}
.mobile-icon-action__icon {
@@ -406,6 +406,38 @@ async function collectRenderingMetrics(page: Page) {
document.querySelector("[data-testid='mobile-reimbursement-sticky-invoice-add']"),
document.querySelector("[data-testid='reimbursement-submit']"),
].filter(Boolean).map((element) => element!.getBoundingClientRect());
const mobileShell = document.querySelector(".hyb-mobile-shell");
const avatarSlotRect = document.querySelector("[data-testid='mobile-me-avatar-slot']")?.getBoundingClientRect();
const profileNameRect = document.querySelector("[data-testid='mobile-me-profile-name']")?.getBoundingClientRect();
const actionGaps = Array.from(document.querySelectorAll("[data-testid='mobile-me-action-link']")).map((element) => {
const iconRect = element.querySelector(".mobile-me-action-icon")?.getBoundingClientRect();
const copyRect = element.querySelector(".mobile-me-action-copy")?.getBoundingClientRect();
const linkRect = element.getBoundingClientRect();
return {
gap: iconRect && copyRect ? copyRect.left - iconRect.right : 0,
iconWithin:
Boolean(iconRect) &&
iconRect!.left >= linkRect.left - 1 &&
iconRect!.right <= linkRect.right + 1 &&
iconRect!.top >= linkRect.top - 1 &&
iconRect!.bottom <= linkRect.bottom + 1,
copyWithin:
Boolean(copyRect) &&
copyRect!.left >= linkRect.left - 1 &&
copyRect!.right <= linkRect.right + 1 &&
copyRect!.top >= linkRect.top - 1 &&
copyRect!.bottom <= linkRect.bottom + 1,
};
});
const moreLinkRects = Array.from(document.querySelectorAll("[data-testid='mobile-more-links'] > *")).map((element) =>
element.getBoundingClientRect(),
);
const mobileSectionHasDecorativeTopLine = Array.from(document.querySelectorAll(".hyb-mobile-shell .section-panel")).some((element) => {
const before = window.getComputedStyle(element, "::before");
const opacity = Number.parseFloat(before.opacity || "1");
const height = Number.parseFloat(before.height || "0");
return before.display !== "none" && before.visibility !== "hidden" && opacity > 0.01 && height > 0;
});
return {
viewportWidth,
scrollWidth: Math.max(document.documentElement.scrollWidth, document.body?.scrollWidth ?? 0),
@@ -419,6 +451,18 @@ async function collectRenderingMetrics(page: Page) {
stickyButtonsSameRow:
stickyButtons.length === 2 ? Math.abs(stickyButtons[0].top - stickyButtons[1].top) <= 2 : stickyButtons.length <= 1,
stickyButtonMinHeight: stickyButtons.length ? Math.min(...stickyButtons.map((rect) => rect.height)) : 0,
mobileMe: {
shellScoped: Boolean(mobileShell),
profileGap: avatarSlotRect && profileNameRect ? profileNameRect.left - avatarSlotRect.right : null,
actionGapMin: actionGaps.length ? Math.min(...actionGaps.map((item) => item.gap)) : null,
actionGapMax: actionGaps.length ? Math.max(...actionGaps.map((item) => item.gap)) : null,
actionSlotsWithin: actionGaps.every((item) => item.iconWithin && item.copyWithin),
moreLinksTwoColumns:
moreLinkRects.length >= 2
? Math.abs(moreLinkRects[0].top - moreLinkRects[1].top) <= 4 && moreLinkRects[0].right <= moreLinkRects[1].left
: true,
hasDecorativeTopLine: mobileSectionHasDecorativeTopLine,
},
};
});
}
@@ -474,7 +518,18 @@ async function exerciseMobileRendering(page: Page, key: string, label: string) {
await page.goto("/mobile/me");
await expect(page.getByTestId("mobile-more-links")).toBeVisible();
await expectRenderingStable(page, label, `${key}-mobile-me`);
const meMetrics = await expectRenderingStable(page, label, `${key}-mobile-me`);
const minSlotGap = meMetrics.viewportWidth >= 390 ? 14 : 12;
expect(meMetrics.mobileMe.shellScoped, `${label} mobile shell scope`).toBe(true);
expect(meMetrics.mobileMe.profileGap, `${label} profile avatar/text gap`).not.toBeNull();
expect(meMetrics.mobileMe.profileGap ?? 0, `${label} profile avatar/text gap min`).toBeGreaterThanOrEqual(minSlotGap);
expect(meMetrics.mobileMe.profileGap ?? 0, `${label} profile avatar/text gap max`).toBeLessThanOrEqual(20);
expect(meMetrics.mobileMe.actionGapMin, `${label} action icon/text gap min`).not.toBeNull();
expect(meMetrics.mobileMe.actionGapMin ?? 0, `${label} action icon/text gap min`).toBeGreaterThanOrEqual(minSlotGap);
expect(meMetrics.mobileMe.actionGapMax ?? 0, `${label} action icon/text gap max`).toBeLessThanOrEqual(18);
expect(meMetrics.mobileMe.actionSlotsWithin, `${label} action slots within link`).toBe(true);
expect(meMetrics.mobileMe.moreLinksTwoColumns, `${label} more links keep two columns`).toBe(true);
expect(meMetrics.mobileMe.hasDecorativeTopLine, `${label} mobile section top line hidden`).toBe(false);
await page.goto("/mobile/reimbursements/wcm6-reimbursement");
await expect(page.getByTestId("mobile-reimbursement-detail")).toBeVisible();