refactor: migrate key forms to shadcn field

This commit is contained in:
hailin
2026-07-02 15:30:01 +08:00
parent 2de36c3da6
commit 09df56c21b
6 changed files with 66 additions and 35 deletions

View File

@@ -1,11 +1,11 @@
import * as React from 'react' import * as React from 'react'
import { AlertCircle, LockKeyhole, UserRound } from 'lucide-react' import { AlertCircle, LockKeyhole, UserRound } from 'lucide-react'
import { AppField } from '@/components/app/AppField'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { useAuth } from './AuthProvider' import { useAuth } from './AuthProvider'
@@ -70,33 +70,33 @@ export function LoginPage() {
<AlertDescription>{error}</AlertDescription> <AlertDescription>{error}</AlertDescription>
</Alert> </Alert>
) : null} ) : null}
<div className="grid gap-2"> <AppField label="账号" htmlFor="username" invalid={Boolean(error)} required>
<Label htmlFor="username">账号</Label>
<div className="relative"> <div className="relative">
<UserRound className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" /> <UserRound className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<Input <Input
id="username" id="username"
autoComplete="username" autoComplete="username"
aria-invalid={Boolean(error)}
className="pl-9" className="pl-9"
value={username} value={username}
onChange={(event) => setUsername(event.target.value)} onChange={(event) => setUsername(event.target.value)}
/> />
</div> </div>
</div> </AppField>
<div className="grid gap-2"> <AppField label="密码" htmlFor="password" invalid={Boolean(error)} required>
<Label htmlFor="password">密码</Label>
<div className="relative"> <div className="relative">
<LockKeyhole className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" /> <LockKeyhole className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<Input <Input
id="password" id="password"
type="password" type="password"
autoComplete="current-password" autoComplete="current-password"
aria-invalid={Boolean(error)}
className="pl-9" className="pl-9"
value={password} value={password}
onChange={(event) => setPassword(event.target.value)} onChange={(event) => setPassword(event.target.value)}
/> />
</div> </div>
</div> </AppField>
<Button type="submit" className="mt-1 w-full" disabled={submitting}> <Button type="submit" className="mt-1 w-full" disabled={submitting}>
{submitting ? '登录中...' : '登录'} {submitting ? '登录中...' : '登录'}
</Button> </Button>

View File

@@ -100,15 +100,16 @@ export function SolutionCreatePage() {
<div className="grid gap-4"> <div className="grid gap-4">
<AppSection title="基础信息"> <AppSection title="基础信息">
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
<AppField label="方案标题"> <AppField label="方案标题" htmlFor="solution-title" required>
<Input <Input
id="solution-title"
required required
placeholder="例如:认证回调阻塞处理方案" placeholder="例如:认证回调阻塞处理方案"
value={form.title} value={form.title}
onChange={(event) => updateField('title', event.target.value)} onChange={(event) => updateField('title', event.target.value)}
/> />
</AppField> </AppField>
<AppField label="状态"> <AppField label="状态" required>
<Select value={form.status} onValueChange={(value) => updateField('status', value)}> <Select value={form.status} onValueChange={(value) => updateField('status', value)}>
<SelectTrigger className="w-full"> <SelectTrigger className="w-full">
<SelectValue placeholder="选择状态" /> <SelectValue placeholder="选择状态" />
@@ -121,15 +122,17 @@ export function SolutionCreatePage() {
</SelectContent> </SelectContent>
</Select> </Select>
</AppField> </AppField>
<AppField label="关联系统"> <AppField label="关联系统" htmlFor="solution-related-system">
<Input <Input
id="solution-related-system"
placeholder="例如:CRM、认证中心、支付系统" placeholder="例如:CRM、认证中心、支付系统"
value={form.related_system} value={form.related_system}
onChange={(event) => updateField('related_system', event.target.value)} onChange={(event) => updateField('related_system', event.target.value)}
/> />
</AppField> </AppField>
<AppField label="环境范围"> <AppField label="环境范围" htmlFor="solution-environment-scope">
<Input <Input
id="solution-environment-scope"
placeholder="例如:生产环境 / Chrome" placeholder="例如:生产环境 / Chrome"
value={form.environment_scope} value={form.environment_scope}
onChange={(event) => updateField('environment_scope', event.target.value)} onChange={(event) => updateField('environment_scope', event.target.value)}
@@ -139,8 +142,9 @@ export function SolutionCreatePage() {
</AppSection> </AppSection>
<AppSection title="问题特征"> <AppSection title="问题特征">
<AppField label="问题特征"> <AppField label="问题特征" htmlFor="solution-problem-signature" required>
<Textarea <Textarea
id="solution-problem-signature"
required required
placeholder="描述可复用的问题表现、报错特征、触发条件。" placeholder="描述可复用的问题表现、报错特征、触发条件。"
value={form.problem_signature} value={form.problem_signature}
@@ -151,16 +155,18 @@ export function SolutionCreatePage() {
<AppSection title="根因与方案"> <AppSection title="根因与方案">
<div className="grid gap-4"> <div className="grid gap-4">
<AppField label="根因分析"> <AppField label="根因分析" htmlFor="solution-root-cause" required>
<Textarea <Textarea
id="solution-root-cause"
required required
placeholder="说明问题的根因或判断依据。" placeholder="说明问题的根因或判断依据。"
value={form.root_cause} value={form.root_cause}
onChange={(event) => updateField('root_cause', event.target.value)} onChange={(event) => updateField('root_cause', event.target.value)}
/> />
</AppField> </AppField>
<AppField label="处理方案"> <AppField label="处理方案" htmlFor="solution-content" required>
<Textarea <Textarea
id="solution-content"
required required
placeholder="写清楚处理步骤、验证方式和回滚注意事项。" placeholder="写清楚处理步骤、验证方式和回滚注意事项。"
value={form.solution} value={form.solution}
@@ -171,8 +177,9 @@ export function SolutionCreatePage() {
</AppSection> </AppSection>
<AppSection title="风险提示"> <AppSection title="风险提示">
<AppField label="风险提示"> <AppField label="风险提示" htmlFor="solution-risk-note">
<Textarea <Textarea
id="solution-risk-note"
placeholder="可选:说明操作风险、影响范围或上线前确认事项。" placeholder="可选:说明操作风险、影响范围或上线前确认事项。"
value={form.risk_note} value={form.risk_note}
onChange={(event) => updateField('risk_note', event.target.value)} onChange={(event) => updateField('risk_note', event.target.value)}

View File

@@ -1,5 +1,6 @@
import * as React from 'react' import * as React from 'react'
import { AppField } from '@/components/app/AppField'
import { AppDataTable, type AppDataTableColumn } from '@/components/app/AppDataTable' import { AppDataTable, type AppDataTableColumn } from '@/components/app/AppDataTable'
import { AppEmptyState } from '@/components/app/AppEmptyState' import { AppEmptyState } from '@/components/app/AppEmptyState'
import { AppPageHeader } from '@/components/app/AppPageHeader' import { AppPageHeader } from '@/components/app/AppPageHeader'
@@ -74,9 +75,11 @@ export function SystemRolesPage() {
<div className="mx-auto flex max-w-7xl flex-col gap-4"> <div className="mx-auto flex max-w-7xl flex-col gap-4">
<AppPageHeader title="角色权限" /> <AppPageHeader title="角色权限" />
<form className="grid gap-3 rounded-lg border border-border bg-surface p-3 md:grid-cols-[minmax(0,360px)_auto]" onSubmit={handleCreate}> <form className="grid gap-3 rounded-lg border border-border bg-surface p-3 md:grid-cols-[minmax(0,360px)_auto] md:items-end" onSubmit={handleCreate}>
<Input placeholder="角色名称" value={roleName} onChange={(event) => setRoleName(event.target.value)} required /> <AppField label="角色名称" htmlFor="system-role-name" required>
<Button type="submit">创建角色</Button> <Input id="system-role-name" value={roleName} onChange={(event) => setRoleName(event.target.value)} required />
</AppField>
<Button type="submit" className="w-full md:w-auto">创建角色</Button>
</form> </form>
{error ? <AppEmptyState title="无法加载角色" description={error} /> : null} {error ? <AppEmptyState title="无法加载角色" description={error} /> : null}

View File

@@ -1,5 +1,6 @@
import * as React from 'react' import * as React from 'react'
import { AppField } from '@/components/app/AppField'
import { AppDataTable, type AppDataTableColumn } from '@/components/app/AppDataTable' import { AppDataTable, type AppDataTableColumn } from '@/components/app/AppDataTable'
import { AppEmptyState } from '@/components/app/AppEmptyState' import { AppEmptyState } from '@/components/app/AppEmptyState'
import { AppPageHeader } from '@/components/app/AppPageHeader' import { AppPageHeader } from '@/components/app/AppPageHeader'
@@ -82,11 +83,17 @@ export function SystemUserGroupsPage() {
<div className="mx-auto flex max-w-7xl flex-col gap-4"> <div className="mx-auto flex max-w-7xl flex-col gap-4">
<AppPageHeader title="用户组管理" /> <AppPageHeader title="用户组管理" />
<form className="grid gap-3 rounded-lg border border-border bg-surface p-3 md:grid-cols-[1fr_1fr_2fr_auto]" onSubmit={handleCreate}> <form className="grid gap-3 rounded-lg border border-border bg-surface p-3 md:grid-cols-[1fr_1fr_2fr_auto] md:items-end" onSubmit={handleCreate}>
<Input placeholder="用户组名称" value={form.name} onChange={(event) => setForm({ ...form, name: event.target.value })} required /> <AppField label="用户组名称" htmlFor="system-group-name" required>
<Input placeholder="编码" value={form.code} onChange={(event) => setForm({ ...form, code: event.target.value })} required /> <Input id="system-group-name" value={form.name} onChange={(event) => setForm({ ...form, name: event.target.value })} required />
<Input placeholder="说明" value={form.description} onChange={(event) => setForm({ ...form, description: event.target.value })} /> </AppField>
<Button type="submit">创建用户组</Button> <AppField label="编码" htmlFor="system-group-code" required>
<Input id="system-group-code" value={form.code} onChange={(event) => setForm({ ...form, code: event.target.value })} required />
</AppField>
<AppField label="说明" htmlFor="system-group-description">
<Input id="system-group-description" value={form.description} onChange={(event) => setForm({ ...form, description: event.target.value })} />
</AppField>
<Button type="submit" className="w-full md:w-auto">创建用户组</Button>
</form> </form>
{error ? <AppEmptyState title="无法加载用户组" description={error} /> : null} {error ? <AppEmptyState title="无法加载用户组" description={error} /> : null}

View File

@@ -1,5 +1,6 @@
import * as React from 'react' import * as React from 'react'
import { AppField } from '@/components/app/AppField'
import { AppDataTable, type AppDataTableColumn } from '@/components/app/AppDataTable' import { AppDataTable, type AppDataTableColumn } from '@/components/app/AppDataTable'
import { AppEmptyState } from '@/components/app/AppEmptyState' import { AppEmptyState } from '@/components/app/AppEmptyState'
import { AppFilterBar } from '@/components/app/AppFilterBar' import { AppFilterBar } from '@/components/app/AppFilterBar'
@@ -141,12 +142,20 @@ export function SystemUsersPage() {
<div className="mx-auto flex max-w-7xl flex-col gap-4"> <div className="mx-auto flex max-w-7xl flex-col gap-4">
<AppPageHeader title="用户管理" /> <AppPageHeader title="用户管理" />
<form className="grid gap-3 rounded-lg border border-border bg-surface p-3 md:grid-cols-[1fr_1fr_1fr_1fr_auto]" onSubmit={handleCreateUser}> <form className="grid gap-3 rounded-lg border border-border bg-surface p-3 md:grid-cols-[1fr_1fr_1fr_1fr_auto] md:items-end" onSubmit={handleCreateUser}>
<Input placeholder="账号" value={form.username} onChange={(event) => setForm({ ...form, username: event.target.value })} required /> <AppField label="账号" htmlFor="system-user-username" required>
<Input placeholder="显示名称" value={form.display_name} onChange={(event) => setForm({ ...form, display_name: event.target.value })} /> <Input id="system-user-username" value={form.username} onChange={(event) => setForm({ ...form, username: event.target.value })} required />
<Input placeholder="邮箱" value={form.email} onChange={(event) => setForm({ ...form, email: event.target.value })} /> </AppField>
<Input placeholder="初始密码" type="password" value={form.password} onChange={(event) => setForm({ ...form, password: event.target.value })} required /> <AppField label="显示名称" htmlFor="system-user-display-name">
<Button type="submit">创建用户</Button> <Input id="system-user-display-name" value={form.display_name} onChange={(event) => setForm({ ...form, display_name: event.target.value })} />
</AppField>
<AppField label="邮箱" htmlFor="system-user-email">
<Input id="system-user-email" value={form.email} onChange={(event) => setForm({ ...form, email: event.target.value })} />
</AppField>
<AppField label="初始密码" htmlFor="system-user-password" required>
<Input id="system-user-password" type="password" value={form.password} onChange={(event) => setForm({ ...form, password: event.target.value })} required />
</AppField>
<Button type="submit" className="w-full md:w-auto">创建用户</Button>
</form> </form>
<AppFilterBar> <AppFilterBar>

View File

@@ -61,8 +61,9 @@ export function TicketCreatePage() {
) : null} ) : null}
<AppSection title="基础信息"> <AppSection title="基础信息">
<AppField label="工单标题"> <AppField label="工单标题" htmlFor="ticket-title" required>
<Input <Input
id="ticket-title"
required required
placeholder="例如:生产环境登录后跳转失败" placeholder="例如:生产环境登录后跳转失败"
value={form.title} value={form.title}
@@ -73,7 +74,7 @@ export function TicketCreatePage() {
<AppSection title="影响与优先级"> <AppSection title="影响与优先级">
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
<AppField label="优先级"> <AppField label="优先级" required>
<Select value={form.priority} onValueChange={(value) => updateField('priority', value as TicketInput['priority'])}> <Select value={form.priority} onValueChange={(value) => updateField('priority', value as TicketInput['priority'])}>
<SelectTrigger className="w-full"> <SelectTrigger className="w-full">
<SelectValue placeholder="请选择优先级" /> <SelectValue placeholder="请选择优先级" />
@@ -88,8 +89,9 @@ export function TicketCreatePage() {
</SelectContent> </SelectContent>
</Select> </Select>
</AppField> </AppField>
<AppField label="影响范围"> <AppField label="影响范围" htmlFor="ticket-impact-scope">
<Input <Input
id="ticket-impact-scope"
placeholder="例如:单个用户、某部门、全部用户" placeholder="例如:单个用户、某部门、全部用户"
value={form.impact_scope} value={form.impact_scope}
onChange={(event) => updateField('impact_scope', event.target.value)} onChange={(event) => updateField('impact_scope', event.target.value)}
@@ -100,15 +102,17 @@ export function TicketCreatePage() {
<AppSection title="环境与系统"> <AppSection title="环境与系统">
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
<AppField label="关联系统"> <AppField label="关联系统" htmlFor="ticket-related-system">
<Input <Input
id="ticket-related-system"
placeholder="例如:CRM、财务系统、数据中台" placeholder="例如:CRM、财务系统、数据中台"
value={form.related_system} value={form.related_system}
onChange={(event) => updateField('related_system', event.target.value)} onChange={(event) => updateField('related_system', event.target.value)}
/> />
</AppField> </AppField>
<AppField label="运行环境"> <AppField label="运行环境" htmlFor="ticket-environment">
<Input <Input
id="ticket-environment"
placeholder="例如:生产环境、测试环境、Windows 终端" placeholder="例如:生产环境、测试环境、Windows 终端"
value={form.environment} value={form.environment}
onChange={(event) => updateField('environment', event.target.value)} onChange={(event) => updateField('environment', event.target.value)}
@@ -118,8 +122,9 @@ export function TicketCreatePage() {
</AppSection> </AppSection>
<AppSection title="问题描述"> <AppSection title="问题描述">
<AppField label="详细描述"> <AppField label="详细描述" htmlFor="ticket-description" required>
<Textarea <Textarea
id="ticket-description"
required required
placeholder="请描述影响范围、报错信息、操作步骤和已尝试的处理方式。" placeholder="请描述影响范围、报错信息、操作步骤和已尝试的处理方式。"
value={form.description} value={form.description}