feat(web): 首页换成和工作区同一套外壳,补上账号设置页
CI / backend (push) Canceled after 0s

首页原来是居中窄栏,点进工作区又变全屏,进出像换了个网站。现在把首页也做成
全宽 + 左侧栏(SideRail 抽出来给首页和账号页共用),工作区卡片改成网格,
新建工作区是网格里的虚线卡片——宽度不再跳。

账号页 /account:基本信息(头像首字母、昵称、只读手机号、加入时间)、
登录密码(设置/修改 + 确认输入)、退出登录。工作区侧栏底部的用户块点进去就是它。

e2e account.mjs 14 项全绿,其中一条断言进工作区后侧栏宽度和首页差值 < 1px。
This commit is contained in:
Steven
2026-10-31 04:05:05 +08:00
parent a5510cdb79
commit 096acb00e7
11 changed files with 799 additions and 122 deletions
+19 -5
View File
@@ -11,6 +11,7 @@ import {
Inbox,
Logo,
Logout,
Settings,
TicketIcon,
Users,
X,
@@ -209,13 +210,26 @@ export function AppShell({
<div className="border-t border-line-200 px-3 py-3">
<div className="flex items-center gap-2">
<span className="grid h-7 w-7 place-items-center rounded-full bg-ink-200 text-2xs font-semibold text-ink-700">
<Link
to="/account"
title="账号设置"
className="grid h-7 w-7 shrink-0 place-items-center rounded-full bg-ink-200 text-2xs font-semibold text-ink-700 transition hover:bg-ink-300"
>
{initial(user?.nickname ?? "我")}
</span>
<div className="min-w-0 flex-1">
<div className="truncate text-xs font-medium text-ink-800">{user?.nickname}</div>
</Link>
<Link to="/account" className="group min-w-0 flex-1">
<div className="truncate text-xs font-medium text-ink-800 group-hover:text-primary-700">
{user?.nickname}
</div>
<div className="truncate text-2xs text-ink-400">{phoneMasked}</div>
</div>
</Link>
<Link
to="/account"
title="账号设置"
className="rounded-md p-1.5 text-ink-400 transition hover:bg-ink-100 hover:text-ink-700"
>
<Settings size={15} />
</Link>
<button
onClick={signOut}
title="退出登录"
+116
View File
@@ -0,0 +1,116 @@
import { Link } from "react-router-dom";
import { useAuth } from "../App";
import { Logout, Logo, Settings } from "./icons";
/**
* 工作区里侧栏长什么样,工作区外面(工作区列表、账号设置)就长什么样。
*
* 之前首页是居中窄栏、进了工作区又是全宽,宽度跳变一次、导航位置换一次,
* 像是两个产品。这里把外壳收成一处,首页与账号页复用它。
*/
export function SideRail({ activeId }: { activeId?: string | null }) {
const { user, workspaces, signOut } = useAuth();
const masked = user?.phone ? user.phone.replace(/^(\d{3})\d{4}(\d{4})$/, "$1 **** $2") : "";
return (
<>
{/* 桌面:左侧栏 */}
<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">
<Link to="/" className="flex items-center gap-2.5">
<Logo size={30} />
<div className="leading-tight">
<div className="text-sm font-semibold tracking-tight text-ink-900">达道Task</div>
<div className="text-2xs text-ink-400">任务 · 工单 · 知识</div>
</div>
</Link>
</div>
<div className="mt-1 px-3">
<div className="px-2 pb-1 text-2xs font-medium text-ink-400">我的工作区</div>
</div>
<nav className="mt-1 flex flex-1 flex-col gap-0.5 overflow-y-auto px-3">
{workspaces.map((w) => {
const isActive = w.id === activeId;
return (
<Link
key={w.id}
to={`/w/${w.id}`}
className={`flex items-center gap-2 rounded-lg px-2 py-1.5 text-xs transition ${
isActive
? "bg-paper-200/70 font-medium text-ink-900"
: "text-ink-600 hover:bg-paper-100"
}`}
>
<span className="grid h-5 w-5 shrink-0 place-items-center rounded bg-line-100 text-2xs font-semibold text-ink-600">
{w.name.trim().slice(0, 1).toUpperCase()}
</span>
<span className="truncate">{w.name}</span>
</Link>
);
})}
{workspaces.length === 0 ? (
<p className="px-2 py-1.5 text-2xs leading-relaxed text-ink-400">
还没有工作区,右边建一个就能开始。
</p>
) : null}
<div className="my-1 h-px bg-line-100" />
<Link
to="/account"
className="flex items-center gap-2 rounded-lg px-2 py-1.5 text-xs text-ink-500 transition hover:bg-paper-100 hover:text-ink-800"
>
<Settings size={14} className="text-ink-400" />
账号设置
</Link>
</nav>
<div className="border-t border-line-200 px-3 py-3">
<div className="flex items-center gap-2">
<span className="grid h-7 w-7 shrink-0 place-items-center rounded-full bg-ink-200 text-2xs font-semibold text-ink-700">
{(user?.nickname ?? "我").trim().slice(0, 1).toUpperCase()}
</span>
<div className="min-w-0 flex-1">
<div className="truncate text-xs font-medium text-ink-800">{user?.nickname}</div>
<div className="truncate text-2xs text-ink-400">{masked}</div>
</div>
<button
onClick={signOut}
title="退出登录"
className="rounded-md p-1.5 text-ink-400 transition hover:bg-ink-100 hover:text-ink-700"
>
<Logout size={15} />
</button>
</div>
</div>
</aside>
{/* 移动端:顶条 */}
<header className="sticky top-0 z-20 border-b border-line-200 bg-paper-50/95 backdrop-blur lg:hidden">
<div className="flex items-center justify-between px-4 py-2.5">
<Link to="/" className="flex items-center gap-2">
<Logo size={24} />
<span className="text-sm font-semibold text-ink-900">达道Task</span>
</Link>
<div className="flex items-center gap-1">
<Link
to="/account"
className="rounded-md p-1.5 text-ink-400 hover:bg-ink-100"
aria-label="账号设置"
>
<Settings size={16} />
</Link>
<button
onClick={signOut}
className="rounded-md p-1.5 text-ink-400 hover:bg-ink-100"
aria-label="退出登录"
>
<Logout size={16} />
</button>
</div>
</div>
</header>
</>
);
}
+14
View File
@@ -220,6 +220,20 @@ export const Logout = (p: IconProps) => (
</svg>
);
export const Settings = (p: IconProps) => (
<svg {...base(p)}>
<circle cx="12" cy="12" r="3" />
<path d="M12 3.5v2M12 18.5v2M20.5 12h-2M5.5 12h-2M17.9 6.1l-1.4 1.4M7.5 16.5l-1.4 1.4M17.9 17.9l-1.4-1.4M7.5 7.5 6.1 6.1" />
</svg>
);
export const User = (p: IconProps) => (
<svg {...base(p)}>
<circle cx="12" cy="8.5" r="3.5" />
<path d="M5 20c0-3.3 3.1-5.5 7-5.5s7 2.2 7 5.5" />
</svg>
);
export const Flag = (p: IconProps) => (
<svg {...base(p)}>
<path d="M6 21V4.5M6 5h10.5l-1.7 3.6L16.5 12H6" />