CI / backend (push) Canceled after 0s
首页原来是居中窄栏,点进工作区又变全屏,进出像换了个网站。现在把首页也做成 全宽 + 左侧栏(SideRail 抽出来给首页和账号页共用),工作区卡片改成网格, 新建工作区是网格里的虚线卡片——宽度不再跳。 账号页 /account:基本信息(头像首字母、昵称、只读手机号、加入时间)、 登录密码(设置/修改 + 确认输入)、退出登录。工作区侧栏底部的用户块点进去就是它。 e2e account.mjs 14 项全绿,其中一条断言进工作区后侧栏宽度和首页差值 < 1px。
194 lines
7.7 KiB
TypeScript
194 lines
7.7 KiB
TypeScript
import { useEffect, useState } from "react";
|
||
import { useNavigate } from "react-router-dom";
|
||
import { useAuth } from "../App";
|
||
import { ArrowRight, ChevronRight, Plus } from "../components/icons";
|
||
import { SideRail } from "../components/SideRail";
|
||
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 navigate = useNavigate();
|
||
const [name, setName] = useState("");
|
||
const [creating, setCreating] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [busy, setBusy] = useState(false);
|
||
|
||
useEffect(() => {
|
||
if (!loading && !user) navigate("/login", { replace: true });
|
||
}, [loading, user, navigate]);
|
||
|
||
async function createWorkspace(event: React.FormEvent) {
|
||
event.preventDefault();
|
||
if (!name.trim()) return;
|
||
setBusy(true);
|
||
setError(null);
|
||
try {
|
||
const ws = await api.post<Workspace>("/workspaces", { name: name.trim() });
|
||
setName("");
|
||
setCreating(false);
|
||
await reload();
|
||
navigate(`/w/${ws.id}`);
|
||
} catch (err) {
|
||
setError(err instanceof ApiError ? err.message : "创建失败");
|
||
} finally {
|
||
setBusy(false);
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="flex min-h-full flex-col bg-paper-100 lg:flex-row">
|
||
<SideRail />
|
||
|
||
<main className="min-w-0 flex-1">
|
||
<div className="w-full px-5 py-6 lg:px-8 lg:py-8 xl:px-12 2xl:px-16">
|
||
<div className="mb-7">
|
||
<p className="text-xs font-medium tracking-wide text-clay-600">
|
||
{new Date().toLocaleDateString("zh-CN", {
|
||
month: "long",
|
||
day: "numeric",
|
||
weekday: "long",
|
||
})}
|
||
</p>
|
||
<h1 className="mt-1.5 font-display text-3xl font-semibold">
|
||
{greeting()},{user?.nickname ?? ""}
|
||
</h1>
|
||
<p className="mt-2 max-w-2xl text-sm leading-relaxed text-ink-500">
|
||
选一个工作区继续。任务、工单、知识都在里面,他们是同一件事的三个阶段。
|
||
</p>
|
||
</div>
|
||
|
||
<div className="mb-3 flex items-center justify-between">
|
||
<h2 className="text-sm font-medium text-ink-600">我的工作区</h2>
|
||
<div className="flex items-center gap-3">
|
||
<span className="text-2xs text-ink-400">{workspaces.length} 个</span>
|
||
{!creating ? (
|
||
<Button variant="ghost" size="sm" onClick={() => setCreating(true)}>
|
||
<Plus size={14} />
|
||
新建
|
||
</Button>
|
||
) : null}
|
||
</div>
|
||
</div>
|
||
|
||
{loading ? (
|
||
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||
{[0, 1, 2].map((i) => (
|
||
<div key={i} className="card h-[92px] animate-pulse bg-paper-50" />
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||
{workspaces.map((ws) => (
|
||
<button
|
||
key={ws.id}
|
||
onClick={() => navigate(`/w/${ws.id}`)}
|
||
className="group card flex items-center gap-3 p-4 text-left 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"
|
||
/>
|
||
</button>
|
||
))}
|
||
|
||
{creating ? (
|
||
<form
|
||
onSubmit={createWorkspace}
|
||
className="card space-y-3 border-clay-500/30 p-4 sm:col-span-2 xl:col-span-1"
|
||
>
|
||
<div className="flex items-center gap-2">
|
||
<span className="grid h-6 w-6 place-items-center rounded-md bg-clay-50 text-clay-600">
|
||
<Plus size={14} />
|
||
</span>
|
||
<span className="text-sm font-medium text-ink-800">新建工作区</span>
|
||
</div>
|
||
<Field label="名称">
|
||
<input
|
||
className="input"
|
||
placeholder="例如:达道科技 / 我的事项"
|
||
value={name}
|
||
maxLength={64}
|
||
autoFocus
|
||
onChange={(e) => setName(e.target.value)}
|
||
/>
|
||
</Field>
|
||
{error && <InlineAlert>{error}</InlineAlert>}
|
||
<div className="flex items-center gap-2">
|
||
<Button type="submit" variant="primary" size="sm" disabled={busy || !name.trim()}>
|
||
{busy ? "创建中…" : "创建"}
|
||
{busy ? null : <ArrowRight size={14} />}
|
||
</Button>
|
||
<Button
|
||
type="button"
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => {
|
||
setCreating(false);
|
||
setError(null);
|
||
}}
|
||
>
|
||
取消
|
||
</Button>
|
||
</div>
|
||
</form>
|
||
) : (
|
||
<button
|
||
onClick={() => setCreating(true)}
|
||
className="flex min-h-[92px] items-center justify-center gap-2 rounded-xl border border-dashed border-line-300 text-sm text-ink-400 transition hover:border-primary-300 hover:text-primary-600"
|
||
>
|
||
<Plus size={16} />
|
||
新建工作区
|
||
</button>
|
||
)}
|
||
|
||
{workspaces.length === 0 && !creating ? (
|
||
<div className="card sm:col-span-2 xl:col-span-3">
|
||
<EmptyState
|
||
icon={<Plus size={18} />}
|
||
title="还没有工作区"
|
||
hint="个人用也一样:先建一个,把「任务」放进去。需要别人帮忙时再升级成工单。"
|
||
/>
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
)}
|
||
|
||
<p className="mt-6 text-2xs leading-relaxed text-ink-400">
|
||
收到邀请链接(<span className="font-mono">/join/…</span>)直接打开即可加入,不需要别人手动拉你。
|
||
</p>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
);
|
||
}
|