feat(web): 薄 Web 面 sundynix-web —— 注册/组织/团队/账单自助入口
CI / Go · build + vet + test (pull_request) Failing after 30m3s
CI / Frontend · tsc (sundynix-admin) (pull_request) Failing after 9m42s
CI / Frontend · tsc (sundynix-desktop/frontend) (pull_request) Failing after 16m53s
CI / Frontend · tsc (sundynix-web) (pull_request) Has been cancelled
CI / mcp-py · sandbox guard (pull_request) Has been cancelled

SaaS P3 收口第二刀(设计见 SAAS_DESIGN.md §8)。第三个产品面:desktop=用户
工作产品、admin=平台超管控制塔、web=租户客户的自助柜台——做「装桌面端之前
就要能用」的那些事,三者不合并。

- 骨架抄 admin(HashRouter+路由注册表+me()/sdx:logout 鉴权门+vite/vitest
  单文件配置);UI 整目录搬 desktop 的 ui/ 组件+ink/brand 主题 token(亮暗
  双主题),品牌观感与桌面端一致;api.ts 抄 desktop 的 auth/tenant/usage 段
  (纯 fetch 零 Wails 依赖),成员管理/建组织打新的租户自助接口。
- 页面:登录注册(一页两态)/概览(组织+角色+余额+桌面端下载指引)/团队(名册+
  邀请+改角色+移除,写控件按角色显隐、真闸在后端)/组织(列表+自助新建+切换)/
  用量与账单(余额 hero+趋势+合计+最近消耗,数据全来自现成 /me/usage;充值
  只放说明占位,支付 P5 再接,不做假入口)。
- 工程:vite :5175、launch.json 配置、make webface、ci.yml web 矩阵纳入。
- 测试:tsc 干净;vitest 3 例(fmtCredits 去尾零——测试先行抓出 1.50 毛刺;
  ASSIGNABLE_ROLES 不含 owner)。浏览器 live 全流程:甲(owner)登录→概览→
  团队邀请乙→乙(member)登录写控件全隐藏、名册只读,组织/用量页真数据渲染。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-07-16 17:47:23 +08:00
parent 65340616df
commit 7c67256f87
36 changed files with 6505 additions and 2 deletions
+62
View File
@@ -0,0 +1,62 @@
import { Link } from "react-router-dom";
import { Users, Building2, Coins, ArrowUpRight, Download } from "lucide-react";
import { useTenant } from "../shell/AppShell";
import { fmtCredits, ROLE_LABEL } from "../api";
import { Badge } from "../ui";
// 概览:当前组织 + 我的角色 + 余额,和去往各功能的入口。桌面端下载指引放这
// (薄 Web 面的定位就是「装桌面端之前能用的那些事」)。
export function Overview() {
const { ctx, tenants } = useTenant();
const cards = [
{ to: "/team", icon: Users, title: "团队", desc: "邀请成员、分配角色" },
{ to: "/orgs", icon: Building2, title: "组织", desc: `我所属的 ${tenants.length} 个组织` },
{ to: "/usage", icon: Coins, title: "用量与账单", desc: "余额、消耗趋势与明细" },
];
return (
<div className="mx-auto max-w-3xl p-8">
<h1 className="text-xl font-semibold tracking-tight text-slate-100">
{ctx?.tenant?.name ?? "…"}
</h1>
<div className="mt-2 flex items-center gap-2 text-xs text-slate-500">
{ctx?.role && <Badge tone="accent">{ROLE_LABEL[ctx.role] ?? ctx.role}</Badge>}
<span> {ctx?.tenant?.plan ?? "-"}</span>
<span>·</span>
<span>
<span className="tabular-nums text-slate-300">{fmtCredits(ctx?.credit_balance_micro ?? 0)}</span>
</span>
</div>
<div className="mt-8 grid grid-cols-1 gap-3 md:grid-cols-3">
{cards.map((c) => {
const Icon = c.icon;
return (
<Link key={c.to} to={c.to}
className="group flex flex-col rounded-xl border border-line bg-ink-900 p-4 shadow-card transition-colors hover:border-brand/40">
<div className="flex items-center justify-between">
<Icon className="h-5 w-5 text-slate-300 transition-colors group-hover:text-brand-400" strokeWidth={1.8} />
<ArrowUpRight className="h-4 w-4 text-slate-700 transition-colors group-hover:text-slate-400" />
</div>
<div className="mt-3 text-sm font-medium text-slate-100">{c.title}</div>
<div className="mt-1 text-xs text-slate-500">{c.desc}</div>
</Link>
);
})}
</div>
<div className="mt-8 flex items-start gap-3 rounded-xl border border-line bg-ink-900 p-4">
<Download className="mt-0.5 h-4 w-4 shrink-0 text-slate-500" />
<div className="text-xs leading-relaxed text-slate-500">
<span className="text-slate-300"></span>
sundynix
<a className="text-brand-400 hover:underline" href="https://github.com/blizzardzhang/sundynix-agentix/releases/latest" target="_blank" rel="noreferrer">
</a>
</div>
</div>
</div>
);
}