From 2407eeec7c94eb29421ba1b9143956e40e08279b Mon Sep 17 00:00:00 2001 From: npmrun <1549469775@qq.com> Date: Mon, 25 May 2026 11:39:11 +0800 Subject: [PATCH] docs: add admin users management design spec Co-Authored-By: Claude Opus 4.7 --- app/components/TopNav.vue | 12 +- app/pages/auth/index.vue | 807 --------------------- app/pages/auth/login.vue | 582 +++++++++++++++ app/pages/auth/register.vue | 510 +++++++++++++ app/utils/auth-routes.ts | 4 +- .../2026-05-25-admin-users-management-design.md | 99 +++ .../specs/2026-05-25-auth-pages-redesign-design.md | 91 +++ packages/drizzle-pkg/db.sqlite | Bin 262144 -> 282624 bytes 8 files changed, 1290 insertions(+), 815 deletions(-) delete mode 100644 app/pages/auth/index.vue create mode 100644 app/pages/auth/login.vue create mode 100644 app/pages/auth/register.vue create mode 100644 docs/superpowers/specs/2026-05-25-admin-users-management-design.md create mode 100644 docs/superpowers/specs/2026-05-25-auth-pages-redesign-design.md diff --git a/app/components/TopNav.vue b/app/components/TopNav.vue index 91a4341..3f38b7a 100644 --- a/app/components/TopNav.vue +++ b/app/components/TopNav.vue @@ -61,17 +61,17 @@ watch(() => route.path, () => {
  • - 登录 - 注册 + 登录 + 注册
  • -
    +
  • {{ user?.username }} -
  • +
    {{ user?.username }} diff --git a/app/pages/auth/index.vue b/app/pages/auth/index.vue deleted file mode 100644 index 18e8c4f..0000000 --- a/app/pages/auth/index.vue +++ /dev/null @@ -1,807 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/pages/auth/login.vue b/app/pages/auth/login.vue new file mode 100644 index 0000000..f24e460 --- /dev/null +++ b/app/pages/auth/login.vue @@ -0,0 +1,582 @@ + + + + + \ No newline at end of file diff --git a/app/pages/auth/register.vue b/app/pages/auth/register.vue new file mode 100644 index 0000000..33fdc56 --- /dev/null +++ b/app/pages/auth/register.vue @@ -0,0 +1,510 @@ + + + + + \ No newline at end of file diff --git a/app/utils/auth-routes.ts b/app/utils/auth-routes.ts index 4a8f032..8924aa4 100644 --- a/app/utils/auth-routes.ts +++ b/app/utils/auth-routes.ts @@ -1,5 +1,5 @@ -const PUBLIC_ROUTE_EXACT = new Set(["/", "/auth"]); -const GUEST_ONLY_ROUTE_EXACT = new Set(["/auth"]); +const PUBLIC_ROUTE_EXACT = new Set(["/", "/auth/login", "/auth/register"]); +const GUEST_ONLY_ROUTE_EXACT = new Set(["/auth/login", "/auth/register"]); /** 公开主页 /@slug 与仅链接分享 /p/slug/t/token */ const PUBLIC_ROUTE_PREFIXES: string[] = ["/@", "/p/"]; diff --git a/docs/superpowers/specs/2026-05-25-admin-users-management-design.md b/docs/superpowers/specs/2026-05-25-admin-users-management-design.md new file mode 100644 index 0000000..49c23a3 --- /dev/null +++ b/docs/superpowers/specs/2026-05-25-admin-users-management-design.md @@ -0,0 +1,99 @@ +# Admin Users Management — Design Spec + +## 概述 + +后台管理系统的用户管理模块,提供管理员增删改查用户的能力。 + +## 路由与布局 + +- **布局**:`app/layouts/admin.vue` — 左侧边栏 + 右侧 `` 内容区 +- **边栏菜单**:用户管理、Scheduled Tasks +- **访问控制**:所有 `/admin/*` 路由需要 admin 角色(`requireAdmin`) +- **边栏宽度**:240px 固定,内容区 flex-1 + +## 页面:用户列表 `/admin/users` + +### 路由 +- `app/pages/admin/users/index.vue` + +### 顶部操作栏 +- 搜索框(按 username / email 过滤,客户端筛选) +- 新建用户按钮(coral CTA) + +### 表格列 +| 列 | 来源 | +|----|------| +| ID | `users.id` | +| Username | `users.username` | +| Email | `users.email` | +| Role | `users.role`(badge 显示:user=蓝/ admin=珊瑚色) | +| Status | `users.status`(badge 显示:active=绿 / inactive=灰) | +| Created | `users.createdAt`(格式化日期) | + +### 行操作 +- 编辑(打开编辑弹窗) +- 删除(确认对话框后执行) + +### 分页 +- 每页 20 条,支持 prev/next + +## 弹窗:新建用户 + +### 字段 +| 字段 | 类型 | 校验 | +|------|------|------| +| Username | text | 必填,唯一 | +| Email | text | 必填,Email 格式 | +| Role | select | user / admin,默认 user | +| Status | select | active / inactive,默认 active | + +### 提交 +- POST `/api/admin/users` +- 成功后关闭弹窗,刷新列表 + +## 弹窗:编辑用户 + +### 字段(均只读展示) +| 字段 | 行为 | +|------|------| +| Username | 只读文本 | +| Email | 只读文本 | +| Role | select — user / admin | +| Status | select — active / inactive | + +### 提交 +- PATCH `/api/admin/users/:id` +- 成功后关闭弹窗,刷新列表 + +## API 设计 + +### `GET /api/admin/users` +- 调用 `requireAdmin` +- 返回用户列表(排除 password 字段) +- Query: `page`, `limit` + +### `POST /api/admin/users` +- 调用 `requireAdmin` +- Body: `{ username, email, role, status }` +- 创建用户(初始 password 可随机生成或留空由管理员重置) + +### `PATCH /api/admin/users/:id` +- 调用 `requireAdmin` +- Body: `{ role, status }` + +### `DELETE /api/admin/users/:id` +- 调用 `requireAdmin` +- 删除用户(级联删除 sessions) + +## 视觉风格 + +遵循 DESIGN.md: +- 页面底色:`#faf9f5`(canvas) +- 按钮:coral `#cc785c` +- 表格:白色底 + hairline 边框 +- Badge:role 和 status 用小药丸标签 + +## 依赖 + +- 复用 `server/utils/admin-guard.ts` 的 `requireAdmin` +- 用户 schema 已有(`packages/drizzle-pkg/lib/schema/auth.ts`) diff --git a/docs/superpowers/specs/2026-05-25-auth-pages-redesign-design.md b/docs/superpowers/specs/2026-05-25-auth-pages-redesign-design.md new file mode 100644 index 0000000..c80bbe0 --- /dev/null +++ b/docs/superpowers/specs/2026-05-25-auth-pages-redesign-design.md @@ -0,0 +1,91 @@ +# 登录注册页面重写设计 + +## 日期 +2026-05-25 + +## 目标 +重写 `/auth/login` 和 `/auth/register` 页面为独立的极简居中卡片表单,不依赖其他页面信息。 + +## 设计风格 +方案一:极简居中卡片 + +## 结构 + +### 页面结构 +- 全屏 `{colors.canvas}` = #faf9f5 背景 +- 居中单卡片,`{rounded.lg}`(12px),白底 + 微妙阴影 +- 卡片最大宽度 420px,内边距 40px + +### 背景装饰 +- 32px 圆点网格纹理(`{colors.hairline}` 色,opacity 0.5) +- 右上角 + 左下角各一模糊光晕(coral + amber,opacity ~10-12%) + +### 表单内容(login) +- 标题:"欢迎回来" +- 副标题:"继续您的探索之旅" +- 用户名/邮箱输入框(浮动 label) +- 密码输入框 + 忘记密码链接 +- 验证码输入框 + 验证码图片 +- 记住我复选框 +- 提交按钮 +- 分割线 + 社交登录按钮 +- 底部链接:"还没有账户?立即注册" + +### 表单内容(register) +- 标题:"创建账户" +- 副标题:"加入我们开始策展" +- 用户名输入框 +- 密码输入框 +- 确认密码输入框 +- 验证码输入框 + 验证码图片 +- 提交按钮 +- 分割线 + 社交登录按钮 +- 底部链接:"已有账户?立即登录" + +## 交互细节 +- 提交按钮 hover 光泽扫过动效 +- 输入框 focus 边框变 coral + 外发光(4px coral 10% alpha) +- 验证码点击刷新 + +## 组件样式 + +### 卡片 +``` +background: {colors.canvas} +border-radius: 12px +box-shadow: 0 2px 12px rgba(20,20,19,0.06) +padding: 40px +max-width: 420px +``` + +### 输入框 +``` +background: {colors.canvas} +border: 1.5px solid {colors.hairline} +border-radius: 8px +padding: 20px 16px 8px +font-size: 16px +transition: border-color 0.2s ease, box-shadow 0.2s ease +focus: border-color = {colors.primary}, box-shadow = 0 0 0 4px rgba(204, 120, 92, 0.1) +``` + +### 提交按钮 +``` +background: {colors.primary} +color: {colors.on-primary} +border-radius: 8px +padding: 18px 24px +hover: {colors.primary-active} +active: scale(0.98) +``` + +### 背景光晕 +``` +光晕1(右上): background: {colors.primary}, top: -150px, right: 10%, opacity: 0.12, blur: 80px, 尺寸: 500px +光晕2(左下): background: {colors.accent-amber}, bottom: -100px, left: 20%, opacity: 0.08, blur: 80px, 尺寸: 400px +网格纹理: radial-gradient, 32px 间隔, opacity 0.5 +``` + +## 实现文件 +- `app/pages/auth/login.vue` +- `app/pages/auth/register.vue` \ No newline at end of file diff --git a/packages/drizzle-pkg/db.sqlite b/packages/drizzle-pkg/db.sqlite index f0d8f9ffcc76c4693a11f5c42fc315af22180cd8..504a985a5aee3f6a8bae621004e844406914808d 100644 GIT binary patch delta 19707 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 BE3p6o delta 3553 zcmZ8jd3;UR7Cw9JedZ}CF+`ePa)k^MW6Vjks8YmOK?o9JN(|+-v=XJ17D*4b(hO8v z^&*B_j=6@GM(ar)LP7>Z#5}Z0AFtlq=Z4?=dH0{Y&)#bf>s#OY*2>89&B*Y{^cTb{ z9LL3GxuQY5vOV#QG(naS@Vs?+u-iJB7DcqYZ2 zF|6nnM}v%IoThKWY^{TlX!bDc8#j!7=6bzE-((gUt<9MR@whV=>!=BRP>MqdGYq6P zuRy58M(soJ5TL0CkXt~XvhQYe+IsNX7*_J?5XU%zF0Mc!NVfLnEJG%ZX@a~v!@N5U zKem%AWM(nx+FAD46Fcjex)%UW9%5H4&MGK8T* zxB&cl@Z$^kkNJ^&YhEH1F_-%T z&FhKWc2*N-enw*yYL(_T^@LwL-!Z%wX~#)#OHIT-#eaw!#W`Y}7%coHoEE+l(uE;H zm~$z+av?vQZ%N}^s5@QlLT)$dKn#2p@5PyT0*=OY;U*k{)i4EKhX&{_`Vp-|)9HQ} zT5O+8LB9d&x|I zy|eCHODt_7BUC;0wt7SgQP-%cYIoINd7%8P43xvZ!VJf@<4beUH69Zrd z>V*QihujH=1~exV)pIkqk{q|%b!anY%$SZz% z;~n(19^?c&4+{d@Hyhn=ovvBj0PY0h9>N+Y#iY5$1!KFh*hnziIOwqK`ch3(d#H^V zI>$|3$yR16Zz->#`|=<1FYLXhxE|bwBZi`1(rtZNt-tKCUWL%=y~tqN&=(?TBGV~*x-ZN@^n(T91~_5+)`0^& z)k{!Q{>Mtn zyU*Z(bXx@~ppXtt09Mn8*I<*A(Sxk?0d5*mfEaMEOa$RPS*~>@Nu(1rBtG~WK7cJe z5yxOZp#To6H?#@0tkCL6Uoq~)KbM3^xMlKOp>aWOY-~qwvJj5yqOLO9qUnaJUy@F8 zzoS=mTm3;_Ms_Pll`ohy^pqYe0rKy1p`0tvlKTiP1(7f3J>CaK(fviZvKE`%V%&@_ zXK+|uSk>@Cn0kOEUblbZUYIn;Ll9kd7C$1=V=jz-vd|I9QTCP++#878hsk84$q-B% zm*Q}M-qbY-9@B1prj_1*}7w{AGZPm?$ZOQ*OK z(l^ov(m<)X_)I)w4ih(v8U4^P#_*TY*WTvjFahi6Bwy~UJrw*oyQ zcH(&Gk7(%x=;r|6oQ9=TTB-Z>MDrtJs-Npi)f6=bR;hmcXw8S#OT^vj$VBXJKrZp+ zdWF4bc?w__5Hpk@IZWhLQITD^shs-02ZF60KZhT2=0 zYe~tBab2A?|6u0xg=9RO)?uIa=<#?K(0uAZJ==bgg0q3X?worwpFJl!o(0U9!Xgsj zymQ(Pwzp~u&Y=79*`X_@;*-QIVJT3w*V&&>7p3BPwQ=x%l+l1(o?C}7-_Udg)kl7gjeAwGXhT~e&hh3 z%y;Im2%idL%#W0Jh4!K>ZWk9DWsdTj!yN5rm9e)WDK06}%oi`9-%+k!Now?+Qar2^ z+hD4HrjNsKkQluank7{B)qQwYYT=%kAT7`iYAg9Y!cKgPOw=X`y8MV(#x^Y$AC`En zKEDDtRPU%qjYaAgCQ;K+p;5*?meGsEVE^>mZ5&C9_d<{Wf1*NeFpYl8k?Ul-BpFB8 z+0*exL_OD7OfQBpXqj!L#`-U4VshnJucG-eM9vNO)1 z8XjOBomkF_X=OI6o{f&7AX=7#7SKMSPEeb%_s*kh8Do|Wq2gyijzzZZM zC4#H9HvXkfx0_mDxug83tW~BdJ-x+@>9;)FtK79M$z0Zq-4g8qDd2goh#wsFW**bQ zuH|?LU9yn*xQeuzL+C^=sTBP=7H$<~@T#6n21Rvr53y(|? Aq5uE@