refactor(web): update admin pages and remove legacy ui components

This commit is contained in:
chengkai3
2026-04-18 00:06:04 +08:00
parent a737e5f542
commit bc08fdcb92
23 changed files with 257 additions and 539 deletions
+1
View File
@@ -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<HTMLInputElement | HTMLTextAreaElement>`;否则 `next build` 的 TypeScript 阶段可能出现 `Parameter implicitly has an 'any' type`,且输入/文本域混用时会触发事件类型不兼容。
## 前端主题口径(2026-04-17
+3 -14
View File
@@ -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 等)。
## 认证接口
+18
View File
@@ -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<HTMLTextAreaElement>` 导致类型不兼容。
- 改动:
- `web/src/app/admin/models/page.tsx`:将路由规则“备注”输入从 `TextField.Root` 改为 `TextArea`,并保留 `ChangeEvent<HTMLTextAreaElement>` 处理。
- 进一步收口同类阻断(`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<HTMLInputElement>` / `ChangeEvent<HTMLTextAreaElement>`,并使用 `event.currentTarget` 取值。
- 验证:
- `npm run build:web` 通过(Next.js 16 + TypeScript 校验通过,`/admin/models``/admin/requirements*``/admin/todos``/admin/roles``/admin/users` 均在构建路由列表中)。
- 风险:
- 仅调整前端事件类型与输入组件语义,不改变接口请求结构与业务流程,风险低。
+2 -2
View File
@@ -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<HTMLTextAreaElement>) => setDraft(event.currentTarget.value)}
disabled={!effectiveSessionId || sendMessageMutation.isPending}
/>
<div className="mt-3 flex items-center justify-end gap-2">
+4 -4
View File
@@ -504,7 +504,7 @@ export default function AdminFilesPage() {
<div className="mt-4 flex flex-wrap items-center gap-2">
<input
value={newDirectoryName}
onChange={(event) => setNewDirectoryName(event.target.value)}
onChange={(event: ChangeEvent<HTMLInputElement>) => setNewDirectoryName(event.currentTarget.value)}
placeholder="新建目录名"
className="w-full max-w-xs control"
/>
@@ -555,7 +555,7 @@ export default function AdminFilesPage() {
<div className="flex flex-wrap items-center gap-2">
<input
value={renameName}
onChange={(event) => setRenameName(event.target.value)}
onChange={(event: ChangeEvent<HTMLInputElement>) => setRenameName(event.currentTarget.value)}
placeholder="新名称"
className="control w-48 px-2 py-1 text-xs"
/>
@@ -571,13 +571,13 @@ export default function AdminFilesPage() {
<div className="flex flex-wrap items-center gap-2">
<input
value={moveTargetParentPath}
onChange={(event) => setMoveTargetParentPath(event.target.value)}
onChange={(event: ChangeEvent<HTMLInputElement>) => setMoveTargetParentPath(event.currentTarget.value)}
placeholder="目标目录(如 /a/b"
className="control w-48 px-2 py-1 text-xs"
/>
<input
value={moveNewName}
onChange={(event) => setMoveNewName(event.target.value)}
onChange={(event: ChangeEvent<HTMLInputElement>) => setMoveNewName(event.currentTarget.value)}
placeholder="新名称(可选)"
className="control w-40 px-2 py-1 text-xs"
/>
+62 -41
View File
@@ -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 (
<div key={item.id} className="space-y-1">
{item.path ? (
<Link
href={item.path}
className={`block rounded-lg px-3 py-2 text-sm font-medium transition ${active ? "bg-indigo-500 text-white shadow-[0_10px_24px_rgba(79,70,229,0.28)]" : "text-slate-700 hover:bg-indigo-50"}`}
>
{item.name}
</Link>
) : (
<div className="px-3 py-2 text-sm font-medium text-muted">{item.name}</div>
)}
{item.children.length > 0 && (
<div className="ml-3 space-y-1 border-l border-[var(--border)] pl-3">
{renderMenuNodes(item.children, pathname)}
</div>
)}
</div>
);
});
}
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 })
<aside className="border-r border-[var(--border)] bg-white/70 p-6 backdrop-blur-xl md:sticky md:top-0 md:h-screen md:overflow-y-auto">
<div className="mb-8">
<Link href="/" className="text-xl font-bold tracking-tight text-slate-900">fquiz admin</Link>
<p className="mt-2 text-sm text-muted">{user.username} · {user.email}</p>
<p className="mt-2 text-sm text-muted"></p>
</div>
<nav className="space-y-2">
{menuTree.map((item) => (
<div key={item.id} className="space-y-1">
{item.path ? (
<Link
href={item.path}
className={`block rounded-lg px-3 py-2 text-sm font-medium transition ${pathname === item.path ? "bg-indigo-500 text-white shadow-[0_10px_24px_rgba(79,70,229,0.28)]" : "text-slate-700 hover:bg-indigo-50"}`}
>
{item.name}
</Link>
) : (
<div className="px-3 py-2 text-sm font-medium text-muted">{item.name}</div>
)}
{item.children.length > 0 && (
<div className="ml-3 space-y-1 border-l border-[var(--border)] pl-3">
{item.children.map((child) => (
<Link
key={child.id}
href={child.path ?? "/admin"}
className={`block rounded-lg px-3 py-2 text-sm transition ${pathname === child.path ? "bg-indigo-500 text-white shadow-[0_8px_20px_rgba(79,70,229,0.28)]" : "text-slate-700 hover:bg-indigo-50"}`}
>
{child.name}
</Link>
))}
</div>
)}
</div>
))}
{renderMenuNodes(menuTree, pathname)}
</nav>
<div className="mt-8 space-y-3 border-t border-[var(--border)] pt-6">
<div className="mt-8 space-y-2 border-t border-[var(--border)] pt-6">
<p className="text-xs text-muted">{user.role_codes.join(", ") || "-"}</p>
<p className="text-xs text-muted">{user.status || "-"}</p>
</div>
</aside>
<main className="p-4 md:p-6">
<header className="surface-card mb-6 flex flex-wrap items-start justify-between gap-4 bg-gradient-to-br from-white/95 via-indigo-50/65 to-sky-50/80">
<div>
<p className="text-sm text-muted"></p>
<h1 className="text-2xl font-bold tracking-tight">{currentTitle}</h1>
</div>
<div className="ml-auto flex flex-wrap items-center justify-end gap-2">
<div className="min-w-[160px] text-right">
<p className="text-sm font-semibold text-slate-900">{user.username}</p>
<p className="text-xs text-muted">{user.email}</p>
</div>
<button
className="btn-secondary w-full"
className="btn-secondary btn-small"
onClick={() => void logout()}
type="button"
>
退
</button>
<Link href="/" className="btn-secondary btn-small"></Link>
</div>
</aside>
<main className="p-4 md:p-6">
<div className="surface-card mb-6 flex items-center justify-between gap-4 bg-gradient-to-br from-white/95 via-indigo-50/65 to-sky-50/80">
<div>
<p className="text-sm text-muted"></p>
<h1 className="text-2xl font-bold tracking-tight">{currentTitle}</h1>
</div>
<Link href="/" className="btn-secondary"></Link>
</div>
</header>
{menuError && (
<pre className="notice notice-error mb-6">
+9 -9
View File
@@ -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() {
<span className="text-muted"></span>
<TextField.Root
value={keyword}
onChange={(event) => setKeyword(event.target.value)}
onChange={(event: ChangeEvent<HTMLInputElement>) => setKeyword(event.currentTarget.value)}
placeholder="按编码/名称/路径/权限筛选"
className="w-full"
/>
@@ -392,7 +392,7 @@ export default function AdminMenusPage() {
<TextField.Root
value={form.code}
disabled={editingMenuId !== null}
onChange={(event) => setForm((prev) => ({ ...prev, code: event.target.value }))}
onChange={(event: ChangeEvent<HTMLInputElement>) => setForm((prev) => ({ ...prev, code: event.currentTarget.value }))}
className="w-full"
/>
</label>
@@ -400,7 +400,7 @@ export default function AdminMenusPage() {
<span></span>
<TextField.Root
value={form.name}
onChange={(event) => setForm((prev) => ({ ...prev, name: event.target.value }))}
onChange={(event: ChangeEvent<HTMLInputElement>) => setForm((prev) => ({ ...prev, name: event.currentTarget.value }))}
className="w-full"
/>
</label>
@@ -408,7 +408,7 @@ export default function AdminMenusPage() {
<span></span>
<TextField.Root
value={form.path}
onChange={(event) => setForm((prev) => ({ ...prev, path: event.target.value }))}
onChange={(event: ChangeEvent<HTMLInputElement>) => setForm((prev) => ({ ...prev, path: event.currentTarget.value }))}
placeholder="/admin/example"
className="w-full"
/>
@@ -417,7 +417,7 @@ export default function AdminMenusPage() {
<span></span>
<TextField.Root
value={form.icon}
onChange={(event) => setForm((prev) => ({ ...prev, icon: event.target.value }))}
onChange={(event: ChangeEvent<HTMLInputElement>) => setForm((prev) => ({ ...prev, icon: event.currentTarget.value }))}
placeholder="LayoutDashboard"
className="w-full"
/>
@@ -458,7 +458,7 @@ export default function AdminMenusPage() {
<span></span>
<TextField.Root
value={form.sort_order}
onChange={(event) => setForm((prev) => ({ ...prev, sort_order: event.target.value }))}
onChange={(event: ChangeEvent<HTMLInputElement>) => setForm((prev) => ({ ...prev, sort_order: event.currentTarget.value }))}
type="number"
className="w-full"
/>
@@ -477,7 +477,7 @@ export default function AdminMenusPage() {
<span></span>
<TextField.Root
value={form.component}
onChange={(event) => setForm((prev) => ({ ...prev, component: event.target.value }))}
onChange={(event: ChangeEvent<HTMLInputElement>) => setForm((prev) => ({ ...prev, component: event.currentTarget.value }))}
placeholder="app/admin/users/page"
className="w-full"
/>
@@ -486,7 +486,7 @@ export default function AdminMenusPage() {
<span></span>
<TextField.Root
value={form.permission_code}
onChange={(event) => setForm((prev) => ({ ...prev, permission_code: event.target.value }))}
onChange={(event: ChangeEvent<HTMLInputElement>) => setForm((prev) => ({ ...prev, permission_code: event.currentTarget.value }))}
placeholder="menu.read"
className="w-full"
/>
+57 -59
View File
@@ -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() {
<div className="grid gap-2 md:grid-cols-2">
<TextField.Root
value={keyword}
onChange={(event) => setKeyword(event.target.value)}
onChange={(event: ChangeEvent<HTMLInputElement>) => setKeyword(event.currentTarget.value)}
placeholder="搜索 code/name/provider"
className="w-full"
/>
@@ -793,7 +793,7 @@ export default function AdminModelsPage() {
</section>
{canManage && (
<Dialog
<Dialog.Root
open={showModelModal}
onOpenChange={(open: boolean) => {
if (!open) {
@@ -803,7 +803,7 @@ export default function AdminModelsPage() {
}
}}
>
<DialogContent className="max-h-[90vh] w-full max-w-3xl overflow-auto">
<Dialog.Content className="max-h-[90vh] w-full max-w-3xl overflow-auto">
<div className="mb-4 flex items-center justify-between gap-4">
<div>
<h2 className="text-lg font-semibold">{editingModelId ? "编辑模型" : "新建模型"}</h2>
@@ -825,83 +825,81 @@ export default function AdminModelsPage() {
<div className="grid gap-4 md:grid-cols-2">
<label className="space-y-2 text-sm">
<span></span>
<Input
<TextField.Root
value={modelForm.code}
disabled={editingModelId !== null}
onChange={(event) => setModelForm((prev) => ({ ...prev, code: event.target.value }))}
onChange={(event: ChangeEvent<HTMLInputElement>) => setModelForm((prev) => ({ ...prev, code: event.currentTarget.value }))}
placeholder="openai.gpt-5"
className="w-full"
/>
</label>
<label className="space-y-2 text-sm">
<span></span>
<Input
<TextField.Root
value={modelForm.name}
onChange={(event) => setModelForm((prev) => ({ ...prev, name: event.target.value }))}
onChange={(event: ChangeEvent<HTMLInputElement>) => setModelForm((prev) => ({ ...prev, name: event.currentTarget.value }))}
placeholder="GPT-5 主模型"
className="w-full"
/>
</label>
<label className="space-y-2 text-sm">
<span>Provider</span>
<Select
<Select.Root
value={modelForm.provider}
onValueChange={(value: string) => setModelForm((prev) => ({ ...prev, provider: value }))}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="请选择 Provider" />
</SelectTrigger>
<SelectContent>
<Select.Trigger className="w-full">
</Select.Trigger>
<Select.Content>
{providerOptions.map((item) => (
<SelectItem key={item.value} value={item.value}>
<Select.Item key={item.value} value={item.value}>
{item.label}
</SelectItem>
</Select.Item>
))}
</SelectContent>
</Select>
</Select.Content>
</Select.Root>
</label>
<label className="space-y-2 text-sm">
<span>Provider Model</span>
<Input
<TextField.Root
value={modelForm.provider_model}
onChange={(event) => setModelForm((prev) => ({ ...prev, provider_model: event.target.value }))}
onChange={(event: ChangeEvent<HTMLInputElement>) => setModelForm((prev) => ({ ...prev, provider_model: event.currentTarget.value }))}
placeholder="gpt-5"
className="w-full"
/>
</label>
<label className="space-y-2 text-sm">
<span></span>
<Select
<Select.Root
value={modelForm.status}
disabled={editingModelId !== null}
onValueChange={(value: string) => setModelForm((prev) => ({ ...prev, status: value as ModelStatus }))}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="请选择状态" />
</SelectTrigger>
<SelectContent>
<Select.Trigger className="w-full">
</Select.Trigger>
<Select.Content>
{MODEL_STATUS_OPTIONS.map((item) => (
<SelectItem key={item} value={item}>
<Select.Item key={item} value={item}>
{formatModelStatus(item)}
</SelectItem>
</Select.Item>
))}
</SelectContent>
</Select>
</Select.Content>
</Select.Root>
</label>
<label className="space-y-2 text-sm">
<span></span>
<Input
<TextField.Root
value={modelForm.capabilities}
onChange={(event) => setModelForm((prev) => ({ ...prev, capabilities: event.target.value }))}
onChange={(event: ChangeEvent<HTMLInputElement>) => setModelForm((prev) => ({ ...prev, capabilities: event.currentTarget.value }))}
placeholder="chat,reasoning"
className="w-full"
/>
</label>
<label className="space-y-2 text-sm md:col-span-2">
<span>Base URL</span>
<Input
<TextField.Root
value={modelForm.base_url}
onChange={(event) => setModelForm((prev) => ({ ...prev, base_url: event.target.value }))}
onChange={(event: ChangeEvent<HTMLInputElement>) => setModelForm((prev) => ({ ...prev, base_url: event.currentTarget.value }))}
placeholder="https://api.example.com"
className="w-full"
/>
@@ -909,9 +907,9 @@ export default function AdminModelsPage() {
{!editingModelId && (
<label className="space-y-2 text-sm md:col-span-2">
<span> API Key</span>
<Input
<TextField.Root
value={modelForm.api_key}
onChange={(event) => setModelForm((prev) => ({ ...prev, api_key: event.target.value }))}
onChange={(event: ChangeEvent<HTMLInputElement>) => setModelForm((prev) => ({ ...prev, api_key: event.currentTarget.value }))}
placeholder="sk-..."
className="w-full"
/>
@@ -922,7 +920,7 @@ export default function AdminModelsPage() {
<TextArea
rows={4}
value={modelForm.description}
onChange={(event) => setModelForm((prev) => ({ ...prev, description: event.target.value }))}
onChange={(event: ChangeEvent<HTMLTextAreaElement>) => setModelForm((prev) => ({ ...prev, description: event.currentTarget.value }))}
placeholder="模型用途、限制、成本策略..."
className="w-full"
/>
@@ -939,12 +937,12 @@ export default function AdminModelsPage() {
{saveModelMutation.isPending ? "提交中..." : editingModelId ? "保存模型" : "创建模型"}
</button>
</div>
</DialogContent>
</Dialog>
</Dialog.Content>
</Dialog.Root>
)}
{canManage && (
<Dialog
<Dialog.Root
open={showRouteModal}
onOpenChange={(open: boolean) => {
if (!open) {
@@ -954,7 +952,7 @@ export default function AdminModelsPage() {
}
}}
>
<DialogContent className="max-h-[90vh] w-full max-w-3xl overflow-auto">
<Dialog.Content className="max-h-[90vh] w-full max-w-3xl overflow-auto">
<div className="mb-4 flex items-center justify-between gap-4">
<div>
<h2 className="text-lg font-semibold">{editingRouteId ? "编辑路由规则" : "新建路由规则"}</h2>
@@ -976,37 +974,36 @@ export default function AdminModelsPage() {
<div className="grid gap-4 md:grid-cols-2">
<label className="space-y-2 text-sm">
<span></span>
<Select
<Select.Root
value={routeForm.route_type}
onValueChange={(value: string) => setRouteForm((prev) => ({ ...prev, route_type: value as ModelRouteType }))}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="请选择路由类型" />
</SelectTrigger>
<SelectContent>
<Select.Trigger className="w-full">
</Select.Trigger>
<Select.Content>
{ROUTE_TYPE_OPTIONS.map((item) => (
<SelectItem key={item} value={item}>
<Select.Item key={item} value={item}>
{item}
</SelectItem>
</Select.Item>
))}
</SelectContent>
</Select>
</Select.Content>
</Select.Root>
</label>
<label className="space-y-2 text-sm">
<span>Route Key</span>
<Input
<TextField.Root
value={routeForm.route_type === "GLOBAL" ? GLOBAL_ROUTE_KEY : routeForm.route_key}
disabled={routeForm.route_type === "GLOBAL"}
onChange={(event) => setRouteForm((prev) => ({ ...prev, route_key: event.target.value }))}
onChange={(event: ChangeEvent<HTMLInputElement>) => setRouteForm((prev) => ({ ...prev, route_key: event.currentTarget.value }))}
placeholder="chat.default"
className="w-full"
/>
</label>
<label className="space-y-2 text-sm">
<span> Code</span>
<Input
<TextField.Root
value={routeForm.target_model_code}
onChange={(event) => setRouteForm((prev) => ({ ...prev, target_model_code: event.target.value }))}
onChange={(event: ChangeEvent<HTMLInputElement>) => setRouteForm((prev) => ({ ...prev, target_model_code: event.currentTarget.value }))}
list="model-code-options"
className="w-full"
/>
@@ -1018,19 +1015,20 @@ export default function AdminModelsPage() {
</label>
<label className="space-y-2 text-sm">
<span></span>
<Input
<TextField.Root
type="number"
value={routeForm.priority}
onChange={(event) => setRouteForm((prev) => ({ ...prev, priority: event.target.value }))}
onChange={(event: ChangeEvent<HTMLInputElement>) => setRouteForm((prev) => ({ ...prev, priority: event.currentTarget.value }))}
className="w-full"
/>
</label>
<label className="space-y-2 text-sm md:col-span-2">
<span></span>
<Input
<TextArea
value={routeForm.note}
onChange={(event) => setRouteForm((prev) => ({ ...prev, note: event.target.value }))}
onChange={(event: ChangeEvent<HTMLTextAreaElement>) => setRouteForm((prev) => ({ ...prev, note: event.currentTarget.value }))}
placeholder="例如:客服场景优先使用"
rows={2}
className="w-full"
/>
</label>
@@ -1038,7 +1036,7 @@ export default function AdminModelsPage() {
<input
type="checkbox"
checked={routeForm.enabled}
onChange={(event) => setRouteForm((prev) => ({ ...prev, enabled: event.target.checked }))}
onChange={(event: ChangeEvent<HTMLInputElement>) => setRouteForm((prev) => ({ ...prev, enabled: event.currentTarget.checked }))}
/>
<span></span>
</label>
@@ -1054,8 +1052,8 @@ export default function AdminModelsPage() {
{saveRouteMutation.isPending ? "提交中..." : editingRouteId ? "保存规则" : "创建规则"}
</button>
</div>
</DialogContent>
</Dialog>
</Dialog.Content>
</Dialog.Root>
)}
</div>
+9 -9
View File
@@ -3,7 +3,7 @@
import { useMutation, useQuery, useQueryClient, type QueryClient } from "@tanstack/react-query";
import Link from "next/link";
import { useParams } from "next/navigation";
import { useCallback, useMemo, useState } from "react";
import { ChangeEvent, useCallback, useMemo, useState } from "react";
import { useAuth } from "@/components/auth-provider";
import { Button, Select, TextArea, TextField } from "@radix-ui/themes";
@@ -119,7 +119,7 @@ function RequirementEditSection({
<span></span>
<TextField.Root
value={title}
onChange={(event) => setTitle(event.target.value)}
onChange={(event: ChangeEvent<HTMLInputElement>) => setTitle(event.currentTarget.value)}
className="w-full"
/>
</label>
@@ -128,7 +128,7 @@ function RequirementEditSection({
<span></span>
<TextArea
value={description}
onChange={(event) => setDescription(event.target.value)}
onChange={(event: ChangeEvent<HTMLTextAreaElement>) => setDescription(event.currentTarget.value)}
rows={8}
className="w-full"
/>
@@ -153,7 +153,7 @@ function RequirementEditSection({
<TextField.Root
type="datetime-local"
value={dueAt}
onChange={(event) => setDueAt(event.target.value)}
onChange={(event: ChangeEvent<HTMLInputElement>) => setDueAt(event.currentTarget.value)}
className="w-full"
/>
</label>
@@ -162,7 +162,7 @@ function RequirementEditSection({
<span></span>
<TextField.Root
value={projectName}
onChange={(event) => setProjectName(event.target.value)}
onChange={(event: ChangeEvent<HTMLInputElement>) => setProjectName(event.currentTarget.value)}
className="w-full"
/>
</label>
@@ -171,7 +171,7 @@ function RequirementEditSection({
<span></span>
<TextField.Root
value={moduleName}
onChange={(event) => setModuleName(event.target.value)}
onChange={(event: ChangeEvent<HTMLInputElement>) => setModuleName(event.currentTarget.value)}
className="w-full"
/>
</label>
@@ -180,7 +180,7 @@ function RequirementEditSection({
<span></span>
<TextField.Root
value={source}
onChange={(event) => setSource(event.target.value)}
onChange={(event: ChangeEvent<HTMLInputElement>) => setSource(event.currentTarget.value)}
className="w-full"
/>
</label>
@@ -333,7 +333,7 @@ function RequirementActionsSection({
</Select.Root>
<TextArea
value={transitionNote}
onChange={(event) => setTransitionNote(event.target.value)}
onChange={(event: ChangeEvent<HTMLTextAreaElement>) => setTransitionNote(event.currentTarget.value)}
rows={3}
placeholder="流转备注(可选)"
className="w-full"
@@ -417,7 +417,7 @@ function RequirementCommentSection({
</Select.Root>
<TextArea
value={commentContent}
onChange={(event) => setCommentContent(event.target.value)}
onChange={(event: ChangeEvent<HTMLTextAreaElement>) => setCommentContent(event.currentTarget.value)}
rows={6}
placeholder="写点处理说明、分析结论或修订意见"
className="w-full"
+24 -7
View File
@@ -3,7 +3,7 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useCallback, useState } from "react";
import { ChangeEvent, useCallback, useState } from "react";
import { useAuth } from "@/components/auth-provider";
import { Button, Select, TextArea, TextField } from "@radix-ui/themes";
@@ -123,7 +123,7 @@ export default function RequirementCreatePage() {
<span></span>
<TextField.Root
value={title}
onChange={(event) => setTitle(event.target.value)}
onChange={(event: ChangeEvent<HTMLInputElement>) => setTitle(event.currentTarget.value)}
className="w-full"
/>
</label>
@@ -132,7 +132,7 @@ export default function RequirementCreatePage() {
<span></span>
<TextArea
value={description}
onChange={(event) => setDescription(event.target.value)}
onChange={(event: ChangeEvent<HTMLTextAreaElement>) => setDescription(event.currentTarget.value)}
rows={8}
className="w-full"
/>
@@ -168,17 +168,29 @@ export default function RequirementCreatePage() {
<label className="space-y-2 text-sm">
<span></span>
<TextField.Root value={projectName} onChange={(event) => setProjectName(event.target.value)} className="w-full" />
<TextField.Root
value={projectName}
onChange={(event: ChangeEvent<HTMLInputElement>) => setProjectName(event.currentTarget.value)}
className="w-full"
/>
</label>
<label className="space-y-2 text-sm">
<span></span>
<TextField.Root value={moduleName} onChange={(event) => setModuleName(event.target.value)} className="w-full" />
<TextField.Root
value={moduleName}
onChange={(event: ChangeEvent<HTMLInputElement>) => setModuleName(event.currentTarget.value)}
className="w-full"
/>
</label>
<label className="space-y-2 text-sm">
<span></span>
<TextField.Root value={source} onChange={(event) => setSource(event.target.value)} className="w-full" />
<TextField.Root
value={source}
onChange={(event: ChangeEvent<HTMLInputElement>) => setSource(event.currentTarget.value)}
className="w-full"
/>
</label>
<label className="space-y-2 text-sm">
@@ -202,7 +214,12 @@ export default function RequirementCreatePage() {
<label className="space-y-2 text-sm">
<span></span>
<TextField.Root type="datetime-local" value={dueAt} onChange={(event) => setDueAt(event.target.value)} className="w-full" />
<TextField.Root
type="datetime-local"
value={dueAt}
onChange={(event: ChangeEvent<HTMLInputElement>) => setDueAt(event.currentTarget.value)}
className="w-full"
/>
</label>
</div>
+4 -2
View File
@@ -2,7 +2,7 @@
import { useMutation, useQuery, useQueryClient } 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 { Select, TextField } from "@radix-ui/themes";
@@ -177,7 +177,9 @@ export default function RequirementsPage() {
<div className="mt-4 grid gap-3 md:grid-cols-4">
<TextField.Root
value={filters.keyword}
onChange={(event) => setFilters((prev) => ({ ...prev, keyword: event.target.value }))}
onChange={(event: ChangeEvent<HTMLInputElement>) =>
setFilters((prev) => ({ ...prev, keyword: event.currentTarget.value }))
}
placeholder="关键词 / 编号"
className="w-full"
/>
+12 -6
View File
@@ -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";
@@ -301,7 +301,9 @@ export default function AdminRolesPage() {
<TextField.Root
value={form.code}
disabled={editingRoleId !== null}
onChange={(event) => setForm((prev) => ({ ...prev, code: event.target.value }))}
onChange={(event: ChangeEvent<HTMLInputElement>) =>
setForm((prev) => ({ ...prev, code: event.currentTarget.value }))
}
className="w-full"
/>
</label>
@@ -309,7 +311,9 @@ export default function AdminRolesPage() {
<span></span>
<TextField.Root
value={form.name}
onChange={(event) => setForm((prev) => ({ ...prev, name: event.target.value }))}
onChange={(event: ChangeEvent<HTMLInputElement>) =>
setForm((prev) => ({ ...prev, name: event.currentTarget.value }))
}
className="w-full"
/>
</label>
@@ -317,7 +321,9 @@ export default function AdminRolesPage() {
<span></span>
<TextField.Root
value={form.permission_codes}
onChange={(event) => setForm((prev) => ({ ...prev, permission_codes: event.target.value }))}
onChange={(event: ChangeEvent<HTMLInputElement>) =>
setForm((prev) => ({ ...prev, permission_codes: event.currentTarget.value }))
}
placeholder={permissions.map((item) => item.code).join(", ")}
className="w-full"
/>
@@ -332,10 +338,10 @@ export default function AdminRolesPage() {
<input
type="checkbox"
checked={checked}
onChange={(event) => {
onChange={(event: ChangeEvent<HTMLInputElement>) => {
setForm((prev) => ({
...prev,
menu_ids: event.target.checked
menu_ids: event.currentTarget.checked
? [...prev.menu_ids, item.value]
: prev.menu_ids.filter((menuId) => menuId !== item.value),
}));
+13 -5
View File
@@ -2,7 +2,7 @@
import { useMutation, useQuery, useQueryClient } 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 { Button, Dialog, Select, Table, TextArea, TextField } from "@radix-ui/themes";
@@ -264,7 +264,9 @@ export default function TodoPage() {
<div className="mt-4 grid gap-3 md:grid-cols-4">
<TextField.Root
value={filters.keyword}
onChange={(event) => setFilters((prev) => ({ ...prev, keyword: event.target.value }))}
onChange={(event: ChangeEvent<HTMLInputElement>) =>
setFilters((prev) => ({ ...prev, keyword: event.currentTarget.value }))
}
placeholder="关键词"
className="w-full"
/>
@@ -404,14 +406,18 @@ export default function TodoPage() {
<TextField.Root
className="w-full"
value={createDraft.title}
onChange={(event) => setCreateDraft((prev) => ({ ...prev, title: event.target.value }))}
onChange={(event: ChangeEvent<HTMLInputElement>) =>
setCreateDraft((prev) => ({ ...prev, title: event.currentTarget.value }))
}
placeholder="标题"
/>
<TextArea
className="w-full"
rows={4}
value={createDraft.description}
onChange={(event) => setCreateDraft((prev) => ({ ...prev, description: event.target.value }))}
onChange={(event: ChangeEvent<HTMLTextAreaElement>) =>
setCreateDraft((prev) => ({ ...prev, description: event.currentTarget.value }))
}
placeholder="描述"
/>
@@ -456,7 +462,9 @@ export default function TodoPage() {
type="datetime-local"
className="w-full"
value={createDraft.due_at}
onChange={(event) => setCreateDraft((prev) => ({ ...prev, due_at: event.target.value }))}
onChange={(event: ChangeEvent<HTMLInputElement>) =>
setCreateDraft((prev) => ({ ...prev, due_at: event.currentTarget.value }))
}
/>
<Select.Root
+35 -7
View File
@@ -2,7 +2,7 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import Link from "next/link";
import { FormEvent, useCallback, useMemo, useState } from "react";
import { ChangeEvent, FormEvent, useCallback, useMemo, useState } from "react";
import { useAuth } from "@/components/auth-provider";
import { Button, TextField } from "@radix-ui/themes";
@@ -232,10 +232,38 @@ export default function AdminUsersPage() {
<h2 className="text-lg font-semibold"></h2>
<p className="mt-1 text-sm text-muted"> ID </p>
<form className="mt-4 grid gap-3 md:grid-cols-2" onSubmit={handleCreateUser}>
<TextField.Root placeholder="用户 ID(例如 ck001" value={newUserId} onChange={(e) => setNewUserId(e.target.value)} minLength={3} maxLength={64} required />
<TextField.Root placeholder="邮箱" type="email" value={newEmail} onChange={(e) => setNewEmail(e.target.value)} required />
<TextField.Root placeholder="用户名" value={newUsername} onChange={(e) => setNewUsername(e.target.value)} minLength={3} maxLength={64} required />
<TextField.Root placeholder="初始密码(至少8位)" type="password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} minLength={8} maxLength={128} required />
<TextField.Root
placeholder="用户 ID(例如 ck001"
value={newUserId}
onChange={(event: ChangeEvent<HTMLInputElement>) => setNewUserId(event.currentTarget.value)}
minLength={3}
maxLength={64}
required
/>
<TextField.Root
placeholder="邮箱"
type="email"
value={newEmail}
onChange={(event: ChangeEvent<HTMLInputElement>) => setNewEmail(event.currentTarget.value)}
required
/>
<TextField.Root
placeholder="用户名"
value={newUsername}
onChange={(event: ChangeEvent<HTMLInputElement>) => setNewUsername(event.currentTarget.value)}
minLength={3}
maxLength={64}
required
/>
<TextField.Root
placeholder="初始密码(至少8位)"
type="password"
value={newPassword}
onChange={(event: ChangeEvent<HTMLInputElement>) => setNewPassword(event.currentTarget.value)}
minLength={8}
maxLength={128}
required
/>
<div className="md:col-span-2">
<Button type="submit" disabled={createUserMutation.isPending}>
{createUserMutation.isPending ? "创建中..." : "创建用户"}
@@ -283,8 +311,8 @@ export default function AdminUsersPage() {
checked={checked}
disabled={savingUserId === item.id}
className="accent-indigo-600"
onChange={(event) => {
const nextRoles = event.target.checked
onChange={(event: ChangeEvent<HTMLInputElement>) => {
const nextRoles = event.currentTarget.checked
? [...item.role_codes, roleCode]
: item.role_codes.filter((code) => code !== roleCode);
updateRolesMutation.mutate({ userId: item.id, roleCodes: nextRoles });
+5 -5
View File
@@ -1,7 +1,7 @@
"use client";
import Link from "next/link";
import { FormEvent, useEffect, useState } from "react";
import { FormEvent, ChangeEvent, useEffect, useState } from "react";
import { useAuth } from "@/components/auth-provider";
import { API_BASE_URL, getApiBaseUrl, readApiError } from "@/lib/api";
@@ -201,7 +201,7 @@ export default function Home() {
type="email"
autoComplete="username"
value={email}
onChange={(event) => setEmail(event.target.value)}
onChange={(event: ChangeEvent<HTMLInputElement>) => setEmail(event.currentTarget.value)}
required
/>
{mode === "register" && (
@@ -210,7 +210,7 @@ export default function Home() {
placeholder="Username"
type="text"
value={username}
onChange={(event) => setUsername(event.target.value)}
onChange={(event: ChangeEvent<HTMLInputElement>) => setUsername(event.currentTarget.value)}
minLength={3}
maxLength={64}
required
@@ -222,7 +222,7 @@ export default function Home() {
type="password"
autoComplete={mode === "login" ? "current-password" : "new-password"}
value={password}
onChange={(event) => setPassword(event.target.value)}
onChange={(event: ChangeEvent<HTMLInputElement>) => setPassword(event.currentTarget.value)}
minLength={8}
maxLength={128}
required
@@ -232,7 +232,7 @@ export default function Home() {
<input
type="checkbox"
checked={rememberPassword}
onChange={(event) => setRememberPassword(event.target.checked)}
onChange={(event: ChangeEvent<HTMLInputElement>) => setRememberPassword(event.currentTarget.checked)}
/>
</label>
-24
View File
@@ -1,24 +0,0 @@
export { Button, type ButtonProps } from "@/components/ui/button";
export { Checkbox, type CheckboxProps } from "@/components/ui/checkbox";
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
export { Input } from "@/components/ui/input";
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
export { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
export { Textarea, Textarea as TextArea } from "@/components/ui/textarea";
-62
View File
@@ -1,62 +0,0 @@
import { cva, type VariantProps } from "class-variance-authority";
import type { ButtonHTMLAttributes, MouseEventHandler } from "react";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm transition disabled:cursor-not-allowed disabled:opacity-60",
{
variants: {
variant: {
default: "btn-primary",
secondary: "btn-secondary",
destructive: "btn-danger",
ghost: "btn-ghost",
},
size: {
default: "px-4 py-2 font-semibold",
sm: "btn-small px-3 py-1 font-medium",
lg: "px-6 py-2.5 font-semibold",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
type ButtonProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, "onClick"> &
VariantProps<typeof buttonVariants> & {
onClick?: MouseEventHandler<HTMLButtonElement>;
onPress?: () => void;
isDisabled?: boolean;
};
function Button({
className,
variant,
size,
onClick,
onPress,
isDisabled,
disabled,
type,
...props
}: ButtonProps) {
const resolvedDisabled = isDisabled ?? disabled;
const handleClick = onClick ?? (onPress ? () => onPress() : undefined);
return (
<button
className={cn(buttonVariants({ variant, size }), className)}
disabled={resolvedDisabled}
onClick={handleClick}
type={type ?? "button"}
{...props}
/>
);
}
export { Button, buttonVariants };
export type { ButtonProps };
-53
View File
@@ -1,53 +0,0 @@
import * as React from "react";
import { cn } from "@/lib/utils";
type CheckboxProps = Omit<
React.InputHTMLAttributes<HTMLInputElement>,
"type" | "onChange" | "checked" | "defaultChecked"
> & {
children?: React.ReactNode;
isSelected?: boolean;
defaultSelected?: boolean;
isDisabled?: boolean;
onChange?: (selected: boolean) => void;
onValueChange?: (selected: boolean) => void;
};
function Checkbox({
className,
children,
isSelected,
defaultSelected,
isDisabled,
disabled,
onChange,
onValueChange,
...props
}: CheckboxProps) {
const resolvedDisabled = isDisabled ?? disabled;
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const next = event.target.checked;
onChange?.(next);
onValueChange?.(next);
};
return (
<label className={cn("inline-flex items-center gap-2", className)}>
<input
{...props}
checked={isSelected}
className="checkbox-control"
defaultChecked={defaultSelected}
disabled={resolvedDisabled}
onChange={handleChange}
type="checkbox"
/>
{children ? <span>{children}</span> : null}
</label>
);
}
export { Checkbox };
export type { CheckboxProps };
-74
View File
@@ -1,74 +0,0 @@
import * as DialogPrimitive from "@radix-ui/react-dialog";
import * as React from "react";
import { cn } from "@/lib/utils";
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn("dialog-overlay fixed inset-0 z-50 backdrop-blur-[1px]", className)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"dialog-content fixed left-1/2 top-1/2 z-50 grid w-full max-w-2xl -translate-x-1/2 -translate-y-1/2 gap-4 rounded-2xl border p-6",
className,
)}
{...props}
/>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
function DialogHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn("flex flex-col space-y-1.5 text-left", className)} {...props} />;
}
function DialogFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)} {...props} />;
}
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title ref={ref} className={cn("text-lg font-semibold", className)} {...props} />
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description ref={ref} className={cn("text-sm text-muted", className)} {...props} />
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
};
-13
View File
@@ -1,13 +0,0 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type = "text", ...props }, ref) => {
return <input ref={ref} type={type} className={cn("control", className)} {...props} />;
},
);
Input.displayName = "Input";
export { Input };
-90
View File
@@ -1,90 +0,0 @@
import * as SelectPrimitive from "@radix-ui/react-select";
import * as React from "react";
import { cn } from "@/lib/utils";
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"control inline-flex w-full items-center justify-between gap-2 text-left",
"data-[placeholder]:text-muted",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<span aria-hidden className="text-xs text-muted">
</span>
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"select-content relative z-50 max-h-80 min-w-[12rem] overflow-hidden rounded-xl border p-1",
position === "popper" && "translate-y-1",
className,
)}
position={position}
{...props}
>
<SelectPrimitive.Viewport className="p-1">{children}</SelectPrimitive.Viewport>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label ref={ref} className={cn("px-2 py-1 text-xs font-medium text-muted", className)} {...props} />
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"select-item relative flex w-full cursor-default select-none items-center rounded-md py-1.5 pl-2 pr-8 text-sm outline-none",
className,
)}
{...props}
>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator></SelectPrimitive.ItemIndicator>
</span>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
};
-41
View File
@@ -1,41 +0,0 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table ref={ref} className={cn("w-full caption-bottom text-sm table-modern", className)} {...props} />
</div>
),
);
Table.displayName = "Table";
const TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => <thead ref={ref} className={cn("table-head [&_tr]:border-b", className)} {...props} />,
);
TableHeader.displayName = "TableHeader";
const TableBody = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => <tbody ref={ref} className={cn("table-body [&_tr:last-child]:border-0", className)} {...props} />,
);
TableBody.displayName = "TableBody";
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
({ className, ...props }, ref) => <tr ref={ref} className={cn("border-b", className)} {...props} />,
);
TableRow.displayName = "TableRow";
const TableHead = React.forwardRef<HTMLTableCellElement, React.ThHTMLAttributes<HTMLTableCellElement>>(
({ className, ...props }, ref) => (
<th ref={ref} className={cn("h-10 px-3 text-left align-middle text-xs font-semibold", className)} {...props} />
),
);
TableHead.displayName = "TableHead";
const TableCell = React.forwardRef<HTMLTableCellElement, React.TdHTMLAttributes<HTMLTableCellElement>>(
({ className, ...props }, ref) => <td ref={ref} className={cn("px-3 py-2 align-middle", className)} {...props} />,
);
TableCell.displayName = "TableCell";
export { Table, TableBody, TableCell, TableHead, TableHeader, TableRow };
-13
View File
@@ -1,13 +0,0 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Textarea = React.forwardRef<HTMLTextAreaElement, React.ComponentProps<"textarea">>(
({ className, ...props }, ref) => {
return <textarea ref={ref} className={cn("control min-h-24", className)} {...props} />;
},
);
Textarea.displayName = "Textarea";
export { Textarea };