feat(web): 重做设计系统与全部页面,前端观感对齐当下审美

用户反馈"UI 太丑、太简陋"。不是换配色,而是先立 token 再让页面去用:

- tailwind.config.js:纸白 paper / 墨绿 primary / 陶土橙 clay + 语义色
  sage/wheat/rust/sky,字号阶、圆角阶、投影、动效
- index.css:卡片、按钮五种变体、输入、pill/chip、导航、骨架屏
- 新增 icons.tsx(线性图标集)与 ui.tsx(Button/Sheet/Toast/空态/骨架/PageHeader)
- 新增 AppShell 取代 Layout + WorkspaceNav:桌面侧栏 + 移动顶部横条,工单未读角标
- 六个页面全部重写:登录、首页、任务、看板、工单、知识、成员、邀请落地
- 详情抽屉改用 Sheet,工单态补齐需求人/办理人/退回次数/动作按钮

e2e 因 DOM 升级同步调整(抽屉按 role=dialog 定位、下拉改用 aria-label),
三套用例(7 + 8 + 7 项)全部重跑通过;tsc 0 错,构建 45 modules / gzip 76.70 kB。
This commit is contained in:
dadao
2026-10-28 18:37:35 +08:00
parent f683436689
commit 5be55fdccb
21 changed files with 2520 additions and 968 deletions
+134 -59
View File
@@ -1,12 +1,26 @@
import { useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { useAuth } from "../App";
import { Layout } from "../components/Layout";
import { ArrowRight, ChevronRight, Logo, Logout, Plus } from "../components/icons";
import { Button, EmptyState, Field, InlineAlert } from "../components/ui";
import { ApiError, api } from "../lib/client";
import type { Workspace } from "../lib/types";
function roleLabel(role: string): string {
return { owner: "所有者", admin: "管理员", member: "成员", guest: "访客" }[role] ?? role;
}
function greeting() {
const h = new Date().getHours();
if (h < 6) return "夜深了";
if (h < 11) return "早上好";
if (h < 14) return "中午好";
if (h < 18) return "下午好";
return "晚上好";
}
export function HomePage() {
const { user, workspaces, loading, reload } = useAuth();
const { user, workspaces, loading, reload, signOut } = useAuth();
const navigate = useNavigate();
const [name, setName] = useState("");
const [error, setError] = useState<string | null>(null);
@@ -34,64 +48,125 @@ export function HomePage() {
}
return (
<Layout>
<div className="grid gap-6 md:grid-cols-[1.6fr_1fr]">
<section>
<h2 className="mb-3 text-sm font-medium text-ink-500">我的工作区</h2>
{loading ? (
<p className="text-sm text-ink-500">加载中…</p>
) : workspaces.length === 0 ? (
<div className="card text-sm text-ink-500">
还没有工作区。右侧创建一个,个人用也一样——先把"任务"放进去。
</div>
) : (
<ul className="space-y-3">
{workspaces.map((ws) => (
<li key={ws.id}>
<Link
to={`/w/${ws.id}`}
className="card flex items-center justify-between transition hover:border-ink-500"
>
<div>
<p className="font-medium">{ws.name}</p>
<p className="mt-1 text-xs text-ink-500">
{ws.slug} · {ws.plan === "free" ? "免费版" : ws.plan}
</p>
</div>
<span className="rounded-full bg-ink-100 px-2 py-1 text-xs text-ink-700">
{roleLabel(ws.role)}
</span>
</Link>
</li>
))}
</ul>
)}
</section>
<div className="min-h-full bg-paper-100">
<header className="sticky top-0 z-20 border-b border-line-200 bg-paper-50/90 backdrop-blur">
<div className="mx-auto flex w-full max-w-5xl items-center justify-between px-5 py-3 lg:px-10">
<div className="flex items-center gap-2.5">
<Logo size={26} />
<span className="text-sm font-semibold tracking-tight text-ink-900">达道Task</span>
</div>
<div className="flex items-center gap-3">
<span className="hidden text-xs text-ink-500 sm:inline">
{user?.nickname}
{user?.phone ? ` · ${user.phone.replace(/^(\d{3})\d{4}(\d{4})$/, "$1****$2")}` : ""}
</span>
<Button variant="subtle" size="sm" onClick={signOut} title="退出登录">
<Logout size={14} />
退出
</Button>
</div>
</div>
</header>
<aside className="card h-fit">
<h2 className="text-sm font-medium">新建工作区</h2>
<form className="mt-3 space-y-3" onSubmit={createWorkspace}>
<input
className="input"
placeholder="例如:达道科技 / 我的事项"
value={name}
maxLength={64}
onChange={(e) => setName(e.target.value)}
/>
{error && <p className="text-sm text-red-600">{error}</p>}
<button className="btn-primary w-full" disabled={busy || !name.trim()}>
{busy ? "创建中…" : "创建"}
</button>
</form>
<p className="mt-4 border-t border-ink-100 pt-3 text-xs text-ink-500">
收到邀请链接(/join/…)直接打开即可加入。
<main className="mx-auto w-full max-w-5xl px-5 py-8 lg:px-10 lg:py-12">
<div className="mb-9">
<p className="text-xs font-medium tracking-wide text-clay-600">
{new Date().toLocaleDateString("zh-CN", { month: "long", day: "numeric", weekday: "long" })}
</p>
</aside>
</div>
</Layout>
<h1 className="mt-1.5 font-display text-3xl font-semibold">
{greeting()},{user?.nickname ?? ""}
</h1>
<p className="mt-2 max-w-xl text-sm leading-relaxed text-ink-500">
选一个工作区继续。任务、工单、知识都在里面,他们是同一件事的三个阶段。
</p>
</div>
<div className="grid gap-8 lg:grid-cols-[1.55fr_1fr]">
<section>
<div className="mb-3 flex items-center justify-between">
<h2 className="text-sm font-medium text-ink-600">我的工作区</h2>
<span className="text-2xs text-ink-400">{workspaces.length} 个</span>
</div>
{loading ? (
<div className="grid gap-3 sm:grid-cols-2">
{[0, 1].map((i) => (
<div key={i} className="card h-[92px] animate-pulse bg-paper-50" />
))}
</div>
) : workspaces.length === 0 ? (
<div className="card">
<EmptyState
icon={<Plus size={18} />}
title="还没有工作区"
hint="个人用也一样:先建一个,把「任务」放进去。需要别人帮忙时再升级成工单。"
/>
</div>
) : (
<ul className="grid gap-3 sm:grid-cols-2">
{workspaces.map((ws) => (
<li key={ws.id}>
<Link
to={`/w/${ws.id}`}
className="group card flex items-center gap-3 p-4 transition hover:-translate-y-0.5 hover:border-primary-300 hover:shadow-pop"
>
<span className="grid h-10 w-10 shrink-0 place-items-center rounded-xl bg-primary-900 text-sm font-semibold text-paper-50">
{ws.name.trim().slice(0, 1).toUpperCase()}
</span>
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-medium text-ink-900">
{ws.name}
</span>
<span className="mt-0.5 block text-2xs text-ink-400">
{roleLabel(ws.role)} · {ws.plan === "free" ? "免费版" : ws.plan}
</span>
</span>
<ChevronRight
size={16}
className="shrink-0 text-ink-300 transition group-hover:translate-x-0.5 group-hover:text-primary-500"
/>
</Link>
</li>
))}
</ul>
)}
</section>
<aside className="card h-fit p-5">
<div className="flex items-center gap-2">
<span className="grid h-7 w-7 place-items-center rounded-lg bg-clay-50 text-clay-600">
<Plus size={15} />
</span>
<h2 className="text-sm font-medium text-ink-800">新建工作区</h2>
</div>
<p className="mt-2 text-xs leading-relaxed text-ink-400">
一个工作区对应一支团队或一个生活领域。成员之间共享任务、工单与知识。
</p>
<form className="mt-4 space-y-3" onSubmit={createWorkspace}>
<Field label="名称">
<input
className="input"
placeholder="例如:达道科技 / 我的事项"
value={name}
maxLength={64}
onChange={(e) => setName(e.target.value)}
/>
</Field>
{error && <InlineAlert>{error}</InlineAlert>}
<Button type="submit" variant="primary" className="w-full" disabled={busy || !name.trim()}>
{busy ? "创建中…" : "创建"}
{busy ? null : <ArrowRight size={14} />}
</Button>
</form>
<div className="mt-4 border-t border-line-100 pt-3 text-2xs leading-relaxed text-ink-400">
收到邀请链接(<span className="font-mono">/join/…</span>)直接打开即可加入,
不需要别人手动拉你。
</div>
</aside>
</div>
</main>
</div>
);
}
function roleLabel(role: string): string {
return { owner: "负责人", admin: "管理员", member: "成员", guest: "访客" }[role] ?? role;
}