From bc08fdcb9227eeb28984678ff135c8e103c976df Mon Sep 17 00:00:00 2001 From: chengkai3 Date: Sat, 18 Apr 2026 00:06:04 +0800 Subject: [PATCH] refactor(web): update admin pages and remove legacy ui components --- MEMORY.md | 1 + README.md | 17 +-- memory/2026-04-17.md | 18 +++ web/src/app/admin/chat/page.tsx | 4 +- web/src/app/admin/files/page.tsx | 8 +- web/src/app/admin/layout.tsx | 101 +++++++++------- web/src/app/admin/menus/page.tsx | 18 +-- web/src/app/admin/models/page.tsx | 116 +++++++++---------- web/src/app/admin/requirements/[id]/page.tsx | 18 +-- web/src/app/admin/requirements/new/page.tsx | 31 +++-- web/src/app/admin/requirements/page.tsx | 6 +- web/src/app/admin/roles/page.tsx | 18 ++- web/src/app/admin/todos/page.tsx | 18 ++- web/src/app/admin/users/page.tsx | 42 +++++-- web/src/app/page.tsx | 10 +- web/src/components/ui.tsx | 24 ---- web/src/components/ui/button.tsx | 62 ---------- web/src/components/ui/checkbox.tsx | 53 --------- web/src/components/ui/dialog.tsx | 74 ------------ web/src/components/ui/input.tsx | 13 --- web/src/components/ui/select.tsx | 90 -------------- web/src/components/ui/table.tsx | 41 ------- web/src/components/ui/textarea.tsx | 13 --- 23 files changed, 257 insertions(+), 539 deletions(-) delete mode 100644 web/src/components/ui.tsx delete mode 100644 web/src/components/ui/button.tsx delete mode 100644 web/src/components/ui/checkbox.tsx delete mode 100644 web/src/components/ui/dialog.tsx delete mode 100644 web/src/components/ui/input.tsx delete mode 100644 web/src/components/ui/select.tsx delete mode 100644 web/src/components/ui/table.tsx delete mode 100644 web/src/components/ui/textarea.tsx diff --git a/MEMORY.md b/MEMORY.md index 0c607a1..96537a6 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -93,6 +93,7 @@ - 阶段 B(表单控件统一)已覆盖:`/admin/requirements`、`/admin/requirements/new`、`/admin/requirements/[id]`、`/admin/menus`、`/admin/models`、`/admin/users`;上述页面不再使用原生 `select/textarea` 与 `control` 样式输入,统一走 `@/components/ui` 的 `Input/Select/TextArea` 组件。 - 阶段 B 收尾已完成:`/admin/chat` 的消息输入框已统一为 `TextArea`;当前 `web/src/app/admin/**` 已无原生 `select/textarea` 与 `control` 直连输入写法。 - 阶段 C(弹窗统一)已完成:`/admin/models` 与 `/admin/roles` 的手写遮罩弹窗(`fixed inset-0`)已统一迁移为 `@/components/ui` 的 `Dialog`,并保留原有新建/编辑/关闭与提交行为。 +- 在当前 `@radix-ui/themes` + TS 配置下,`TextField.Root` / `TextArea` 的 `onChange` 处理函数应显式标注 `ChangeEvent`;否则 `next build` 的 TypeScript 阶段可能出现 `Parameter implicitly has an 'any' type`,且输入/文本域混用时会触发事件类型不兼容。 ## 前端主题口径(2026-04-17) diff --git a/README.md b/README.md index f3d86b8..0104166 100644 --- a/README.md +++ b/README.md @@ -56,21 +56,10 @@ npm run dev ``` -## 常用命令 +## 前端 UI 约定 -```bash -# 启动前端 -npm run dev:web - -# 仅启动后端 -npm run dev:api - -# 构建前端 -npm run build:web - -# 前端 lint -npm run lint:web -``` +- 后台与业务页面统一使用 **Radix** 体系组件(优先 `@radix-ui/themes`,必要时使用 `@radix-ui/react-*` primitives)。 +- 不再使用 `web/src/components/ui/*` 自定义 UI 封装层(button/input/select/dialog/table/checkbox/textarea 等)。 ## 认证接口 diff --git a/memory/2026-04-17.md b/memory/2026-04-17.md index c4b45a1..408bb18 100644 --- a/memory/2026-04-17.md +++ b/memory/2026-04-17.md @@ -419,3 +419,21 @@ - `curl http://localhost:3000/admin` 响应中 `data-accent-color=\"indigo\"` 生效。 - 风险: - 部分页面仍存在 `sky-*` 辅助渐变色(非主强调色),如需完全统一到单一主题色可在下一步继续收口。 + +## Work Log (2026-04-17, 修复 web 构建 TypeScript onChange 类型阻断) + +- 背景: 用户反馈 `docker build web` 在 `web/src/app/admin/models/page.tsx` 报错,`TextField.Root` 的 `onChange` 使用了 `ChangeEvent` 导致类型不兼容。 +- 改动: + - `web/src/app/admin/models/page.tsx`:将路由规则“备注”输入从 `TextField.Root` 改为 `TextArea`,并保留 `ChangeEvent` 处理。 + - 进一步收口同类阻断(`onChange` 参数隐式 `any`): + - `web/src/app/admin/requirements/new/page.tsx` + - `web/src/app/admin/requirements/page.tsx` + - `web/src/app/admin/requirements/[id]/page.tsx` + - `web/src/app/admin/todos/page.tsx` + - `web/src/app/admin/roles/page.tsx` + - `web/src/app/admin/users/page.tsx` + - 统一为 `ChangeEvent` / `ChangeEvent`,并使用 `event.currentTarget` 取值。 +- 验证: + - `npm run build:web` 通过(Next.js 16 + TypeScript 校验通过,`/admin/models`、`/admin/requirements*`、`/admin/todos`、`/admin/roles`、`/admin/users` 均在构建路由列表中)。 +- 风险: + - 仅调整前端事件类型与输入组件语义,不改变接口请求结构与业务流程,风险低。 diff --git a/web/src/app/admin/chat/page.tsx b/web/src/app/admin/chat/page.tsx index 70e1969..4475a40 100644 --- a/web/src/app/admin/chat/page.tsx +++ b/web/src/app/admin/chat/page.tsx @@ -1,7 +1,7 @@ "use client"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { FormEvent, useEffect, useMemo, useRef, useState } from "react"; +import { ChangeEvent, FormEvent, useEffect, useMemo, useRef, useState } from "react"; import { useAuth } from "@/components/auth-provider"; import { TextArea } from "@radix-ui/themes"; @@ -253,7 +253,7 @@ export default function AdminChatPage() { className="w-full" placeholder="请输入你的问题..." value={draft} - onChange={(event) => setDraft(event.target.value)} + onChange={(event: ChangeEvent) => setDraft(event.currentTarget.value)} disabled={!effectiveSessionId || sendMessageMutation.isPending} />
diff --git a/web/src/app/admin/files/page.tsx b/web/src/app/admin/files/page.tsx index 9db1edb..823399b 100644 --- a/web/src/app/admin/files/page.tsx +++ b/web/src/app/admin/files/page.tsx @@ -504,7 +504,7 @@ export default function AdminFilesPage() {
setNewDirectoryName(event.target.value)} + onChange={(event: ChangeEvent) => setNewDirectoryName(event.currentTarget.value)} placeholder="新建目录名" className="w-full max-w-xs control" /> @@ -555,7 +555,7 @@ export default function AdminFilesPage() {
setRenameName(event.target.value)} + onChange={(event: ChangeEvent) => setRenameName(event.currentTarget.value)} placeholder="新名称" className="control w-48 px-2 py-1 text-xs" /> @@ -571,13 +571,13 @@ export default function AdminFilesPage() {
setMoveTargetParentPath(event.target.value)} + onChange={(event: ChangeEvent) => setMoveTargetParentPath(event.currentTarget.value)} placeholder="目标目录(如 /a/b)" className="control w-48 px-2 py-1 text-xs" /> setMoveNewName(event.target.value)} + onChange={(event: ChangeEvent) => setMoveNewName(event.currentTarget.value)} placeholder="新名称(可选)" className="control w-40 px-2 py-1 text-xs" /> diff --git a/web/src/app/admin/layout.tsx b/web/src/app/admin/layout.tsx index d82b6ec..9dea15d 100644 --- a/web/src/app/admin/layout.tsx +++ b/web/src/app/admin/layout.tsx @@ -23,6 +23,40 @@ function flattenMenuTree(tree: MenuTreeItem[]): MenuTreeItem[] { return result; } +function isActivePath(pathname: string, menuPath: string | null): boolean { + if (!menuPath) { + return false; + } + return pathname === menuPath || pathname.startsWith(`${menuPath}/`); +} + +function renderMenuNodes(items: MenuTreeItem[], pathname: string): React.ReactNode { + return items.map((item) => { + const active = isActivePath(pathname, item.path); + + return ( +
+ {item.path ? ( + + {item.name} + + ) : ( +
{item.name}
+ )} + + {item.children.length > 0 && ( +
+ {renderMenuNodes(item.children, pathname)} +
+ )} +
+ ); + }); +} + export default function AdminLayout({ children }: { children: React.ReactNode }) { const pathname = usePathname(); const { user, initializing, fetchWithAuth, logout } = useAuth(); @@ -36,12 +70,17 @@ export default function AdminLayout({ children }: { children: React.ReactNode }) setLoadingMenus(false); return; } + + setLoadingMenus(true); + setMenuError(""); + const response = await fetchWithAuth("/api/v1/admin/me/menus"); if (!response.ok) { setMenuError(await readApiError(response)); setLoadingMenus(false); return; } + const payload = (await response.json()) as MenuTreeItem[]; setMenuTree(payload); setLoadingMenus(false); @@ -63,7 +102,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode }) const flatMenus = useMemo(() => flattenMenuTree(menuTree), [menuTree]); const currentTitle = useMemo(() => { - const current = flatMenus.find((item) => item.path === pathname); + const current = flatMenus.find((item) => isActivePath(pathname, item.path)); return current?.name ?? "后台管理"; }, [flatMenus, pathname]); @@ -95,59 +134,41 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
-
+

后台管理

{currentTitle}

- 返回首页 -
+ +
+
+

{user.username}

+

{user.email}

+
+ + 返回首页 +
+ {menuError && (
diff --git a/web/src/app/admin/menus/page.tsx b/web/src/app/admin/menus/page.tsx
index 999b746..36eb8bf 100644
--- a/web/src/app/admin/menus/page.tsx
+++ b/web/src/app/admin/menus/page.tsx
@@ -1,6 +1,6 @@
 "use client";
 
-import { useEffect, useMemo, useState, useCallback } from "react";
+import { ChangeEvent, useEffect, useMemo, useState, useCallback } from "react";
 import Link from "next/link";
 
 import { useAuth } from "@/components/auth-provider";
@@ -280,7 +280,7 @@ export default function AdminMenusPage() {
             关键词
              setKeyword(event.target.value)}
+              onChange={(event: ChangeEvent) => setKeyword(event.currentTarget.value)}
               placeholder="按编码/名称/路径/权限筛选"
               className="w-full"
             />
@@ -392,7 +392,7 @@ export default function AdminMenusPage() {
                setForm((prev) => ({ ...prev, code: event.target.value }))}
+                onChange={(event: ChangeEvent) => setForm((prev) => ({ ...prev, code: event.currentTarget.value }))}
                 className="w-full"
               />
             
@@ -400,7 +400,7 @@ export default function AdminMenusPage() {
               菜单名称
                setForm((prev) => ({ ...prev, name: event.target.value }))}
+                onChange={(event: ChangeEvent) => setForm((prev) => ({ ...prev, name: event.currentTarget.value }))}
                 className="w-full"
               />
             
@@ -408,7 +408,7 @@ export default function AdminMenusPage() {
               路由路径
                setForm((prev) => ({ ...prev, path: event.target.value }))}
+                onChange={(event: ChangeEvent) => setForm((prev) => ({ ...prev, path: event.currentTarget.value }))}
                 placeholder="/admin/example"
                 className="w-full"
               />
@@ -417,7 +417,7 @@ export default function AdminMenusPage() {
               图标名
                setForm((prev) => ({ ...prev, icon: event.target.value }))}
+                onChange={(event: ChangeEvent) => setForm((prev) => ({ ...prev, icon: event.currentTarget.value }))}
                 placeholder="LayoutDashboard"
                 className="w-full"
               />
@@ -458,7 +458,7 @@ export default function AdminMenusPage() {
               排序
                setForm((prev) => ({ ...prev, sort_order: event.target.value }))}
+                onChange={(event: ChangeEvent) => setForm((prev) => ({ ...prev, sort_order: event.currentTarget.value }))}
                 type="number"
                 className="w-full"
               />
@@ -477,7 +477,7 @@ export default function AdminMenusPage() {
               组件标识
                setForm((prev) => ({ ...prev, component: event.target.value }))}
+                onChange={(event: ChangeEvent) => setForm((prev) => ({ ...prev, component: event.currentTarget.value }))}
                 placeholder="app/admin/users/page"
                 className="w-full"
               />
@@ -486,7 +486,7 @@ export default function AdminMenusPage() {
               权限码
                setForm((prev) => ({ ...prev, permission_code: event.target.value }))}
+                onChange={(event: ChangeEvent) => setForm((prev) => ({ ...prev, permission_code: event.currentTarget.value }))}
                 placeholder="menu.read"
                 className="w-full"
               />
diff --git a/web/src/app/admin/models/page.tsx b/web/src/app/admin/models/page.tsx
index 5989698..5df05aa 100644
--- a/web/src/app/admin/models/page.tsx
+++ b/web/src/app/admin/models/page.tsx
@@ -2,7 +2,7 @@
 
 import { useMutation, useQuery, useQueryClient, type QueryClient } from "@tanstack/react-query";
 import Link from "next/link";
-import { useCallback, useMemo, useState } from "react";
+import { ChangeEvent, useCallback, useMemo, useState } from "react";
 
 import { useAuth } from "@/components/auth-provider";
 import { Dialog, Select, TextArea, TextField } from "@radix-ui/themes";
@@ -553,7 +553,7 @@ export default function AdminModelsPage() {
           
setKeyword(event.target.value)} + onChange={(event: ChangeEvent) => setKeyword(event.currentTarget.value)} placeholder="搜索 code/name/provider" className="w-full" /> @@ -793,7 +793,7 @@ export default function AdminModelsPage() { {canManage && ( - { if (!open) { @@ -803,7 +803,7 @@ export default function AdminModelsPage() { } }} > - +

{editingModelId ? "编辑模型" : "新建模型"}

@@ -825,83 +825,81 @@ export default function AdminModelsPage() {