fix(web): 宽屏下侧栏贴边而内容固定居中,中间空出一大块
CI / backend (push) Canceled after 0s

问题:侧栏贴左边,内容区却用 mx-auto max-w-6xl 居中,窗口一宽就出现
"侧栏到内容之间一大片空白、内容右侧又一大片空白"的割裂感。2K 屏上尤其明显。

做法(用户给的两个方向里选"侧栏贴边 + 内容自适应"):

- AppShell 内容区去掉居中与固定宽度,改为随窗口铺开,断点加宽内边距
  (lg 32 / xl 48 / 2xl 64),侧栏在 xl 从 240 加宽到 264
- 宽了之后不能只是把元素拉长,所以同步改成"用宽度"而不是"被拉长":
  - 任务行:md 以上标题左、状态/优先级/截止/标签右对齐成一行,窄屏仍折两行
  - 知识、工单列表:md 两列 / 2xl 三列,卡片不再横跨整屏
  - 看板本来就是四等分列,自适应后自然铺满
- PageHeader 的描述文字限 max-w-3xl,宽屏下不出现过长的行宽

e2e/shots.mjs 支持 VIEWPORT=2560x1400 SUFFIX=-wide 覆盖视口,用来核对宽屏。
三个 e2e 复跑全绿;tsc 0 错,构建 242.02 kB / gzip 76.78 kB。
This commit is contained in:
dadao
2026-10-28 22:49:49 +08:00
parent aa396f2c26
commit c94e0424ed
6 changed files with 27 additions and 12 deletions
+10 -2
View File
@@ -46,10 +46,14 @@ await call(`/workspaces/${wsId}/knowledge`, { title: "报销单必须贴发票
const me = await call("/workspaces/" + wsId + "/members", null, token, "GET").catch(() => null); const me = await call("/workspaces/" + wsId + "/members", null, token, "GET").catch(() => null);
// 视口可覆盖,用来核对宽屏布局:VIEWPORT=2560x1400 SUFFIX=-wide node e2e/shots.mjs
const [VW, VH] = (process.env.VIEWPORT ?? "1440x960").split("x").map(Number);
const SUFFIX = process.env.SUFFIX ?? "";
const browser = await chromium.launch({ const browser = await chromium.launch({
executablePath: "C:\\Users\\Steven\\AppData\\Local\\ms-playwright\\chromium-1223\\chrome-win64\\chrome.exe", executablePath: "C:\\Users\\Steven\\AppData\\Local\\ms-playwright\\chromium-1223\\chrome-win64\\chrome.exe",
}); });
const page = await browser.newPage({ viewport: { width: 1440, height: 960 }, deviceScaleFactor: 1 }); const page = await browser.newPage({ viewport: { width: VW, height: VH }, deviceScaleFactor: 1 });
await page.goto(WEB + "/login"); await page.goto(WEB + "/login");
await page.evaluate(([t, r]) => { await page.evaluate(([t, r]) => {
localStorage.setItem("dadao.access", t); localStorage.setItem("dadao.access", t);
@@ -61,13 +65,17 @@ async function shot(url, name, prep) {
await page.goto(WEB + url, { waitUntil: "networkidle" }); await page.goto(WEB + url, { waitUntil: "networkidle" });
if (prep) await prep(); if (prep) await prep();
await page.waitForTimeout(600); await page.waitForTimeout(600);
const file = `e2e/artifacts/ui-${name}.png`; const file = `e2e/artifacts/ui-${name}${SUFFIX}.png`;
await page.screenshot({ path: file, fullPage: false }); await page.screenshot({ path: file, fullPage: false });
shots.push(file); shots.push(file);
} }
await shot("/", "home"); await shot("/", "home");
await shot(`/w/${wsId}`, "tasks"); await shot(`/w/${wsId}`, "tasks");
await shot(`/w/${wsId}`, "tasks-all", async () => {
await page.getByRole("button", { name: "全部", exact: true }).click();
await page.waitForTimeout(500);
});
await shot(`/w/${wsId}/board`, "board"); await shot(`/w/${wsId}/board`, "board");
await shot(`/w/${wsId}/tickets`, "tickets"); await shot(`/w/${wsId}/tickets`, "tickets");
await shot(`/w/${wsId}/knowledge`, "knowledge"); await shot(`/w/${wsId}/knowledge`, "knowledge");
+7 -2
View File
@@ -105,7 +105,7 @@ export function AppShell({
return ( return (
<div className="flex min-h-full flex-col bg-paper-100 lg:flex-row"> <div className="flex min-h-full flex-col bg-paper-100 lg:flex-row">
{/* —— 侧栏(桌面)—— */} {/* —— 侧栏(桌面)—— */}
<aside className="sticky top-0 z-20 hidden h-screen w-60 shrink-0 flex-col border-r border-line-200 bg-paper-50 lg:flex"> <aside className="sticky top-0 z-20 hidden h-screen w-60 shrink-0 flex-col border-r border-line-200 bg-paper-50 lg:flex xl:w-[264px]">
<div className="px-5 pb-4 pt-5"> <div className="px-5 pb-4 pt-5">
<Link to="/" className="flex items-center gap-2.5"> <Link to="/" className="flex items-center gap-2.5">
<Logo size={30} /> <Logo size={30} />
@@ -246,7 +246,12 @@ export function AppShell({
</header> </header>
<main className="min-w-0 flex-1"> <main className="min-w-0 flex-1">
<div className="mx-auto w-full max-w-6xl px-5 py-6 lg:px-9 lg:py-8">{children}</div> {/*
内容区随窗口变宽(不再固定 max-w-6xl 居中):
侧栏贴左,内容从侧栏右侧一直铺到右边距,宽屏下不留"两边各一大块空白"。
文字类的阅读宽度靠各自的 max-w-3xl 约束,列表/看板这类适合铺开的结构则吃满。
*/}
<div className="w-full px-5 py-6 lg:px-8 lg:py-8 xl:px-12 2xl:px-16">{children}</div>
</main> </main>
</div> </div>
); );
+4 -3
View File
@@ -173,12 +173,13 @@ export function PageHeader({
}) { }) {
return ( return (
<header className="mb-6 flex flex-wrap items-end justify-between gap-3"> <header className="mb-6 flex flex-wrap items-end justify-between gap-3">
<div> <div className="min-w-0">
{eyebrow ? <div className="mb-1 text-2xs font-medium text-ink-400">{eyebrow}</div> : null} {eyebrow ? <div className="mb-1 text-2xs font-medium text-ink-400">{eyebrow}</div> : null}
<h1 className="text-2xl font-semibold">{title}</h1> <h1 className="text-2xl font-semibold">{title}</h1>
{desc ? <p className="mt-1 text-sm text-ink-500">{desc}</p> : null} {/* 宽屏下正文行宽不跟着拉长,40~60 字一行最易读 */}
{desc ? <p className="mt-1 max-w-3xl text-sm text-ink-500">{desc}</p> : null}
</div> </div>
{actions ? <div className="flex items-center gap-2">{actions}</div> : null} {actions ? <div className="flex shrink-0 items-center gap-2">{actions}</div> : null}
</header> </header>
); );
} }
+1 -1
View File
@@ -221,7 +221,7 @@ export function KnowledgePage() {
/> />
</div> </div>
) : ( ) : (
<ul className="space-y-2"> <ul className="grid gap-2 md:grid-cols-2 2xl:grid-cols-3">
{docs.map((d) => ( {docs.map((d) => (
<li <li
key={d.id} key={d.id}
+4 -3
View File
@@ -256,16 +256,17 @@ export function TasksPage() {
/> />
)} )}
<div className="min-w-0 flex-1"> {/* 宽屏把元信息推到右侧,一行读完;窄屏仍折到标题下面 */}
<div className="min-w-0 flex-1 md:flex md:items-center md:gap-3">
<button <button
className={`block w-full truncate text-left text-sm transition-colors ${ className={`block w-full truncate text-left text-sm transition-colors md:flex-1 ${
done ? "text-ink-400 line-through" : "text-ink-900 hover:text-primary-700" done ? "text-ink-400 line-through" : "text-ink-900 hover:text-primary-700"
}`} }`}
onClick={() => setOpenId(item.id)} onClick={() => setOpenId(item.id)}
> >
{item.title} {item.title}
</button> </button>
<div className="mt-1.5 flex flex-wrap items-center gap-1.5 text-2xs"> <div className="mt-1.5 flex flex-wrap items-center gap-1.5 text-2xs md:mt-0 md:shrink-0 md:justify-end">
<StatusPill status={item.status} label={STATUS_LABEL[item.status]} /> <StatusPill status={item.status} label={STATUS_LABEL[item.status]} />
{item.priority > 0 && ( {item.priority > 0 && (
<span className="chip"> <span className="chip">
+1 -1
View File
@@ -149,7 +149,7 @@ export function TicketsPage() {
/> />
</div> </div>
) : ( ) : (
<ul className="space-y-2"> <ul className="grid gap-2 md:grid-cols-2 2xl:grid-cols-3">
{tickets.map((t) => ( {tickets.map((t) => (
<li <li
key={t.id} key={t.id}