feat(ui): 自研 Celadon 组件族入库与 AntD 消费全量替换 - base(Kbd/StatusBadge/IconButton/ToolButton)+product(EmptyState/Callout/SegmentedControl/Checkbox/Select/Tooltip/Switch), composio 视觉基底, globals.css 覆盖清理 (ADR-0008)

This commit is contained in:
maomaochong-ai
2026-09-06 20:28:51 +08:00
parent 0dcd8e1c72
commit c3e2758736
128 changed files with 2791 additions and 737 deletions

View File

@@ -1,4 +1,5 @@
import { App, Button, Form, Input, InputNumber, Modal, Select, Switch } from "antd";
import { App, Button, Form, Input, InputNumber, Modal, Select } from "antd";
import { Switch } from "@/components/ui/base/switch";
import type { FormInstance } from "antd";
import type { ColumnsType } from "antd/es/table";
import { Pencil, Plus, Power, Search, Trash2 } from "lucide-react";

View File

@@ -1,4 +1,5 @@
import { App, Button, Form, Input, Select, Skeleton, Switch } from "antd";
import { App, Button, Form, Input, Select, Skeleton } from "antd";
import { Switch } from "@/components/ui/base/switch";
import { AlertTriangle, BadgeCheck, ChevronDown, Globe2, KeyRound, LockKeyhole, RefreshCw, RotateCcw, Save, ShieldCheck, UserPlus, UsersRound } from "lucide-react";
import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
import { useBlocker } from "react-router";

View File

@@ -1,5 +1,6 @@
import { useEffect, useRef, useState, type ChangeEvent } from "react";
import { App, Button, Form, Input, Modal, Select, Switch } from "antd";
import { App, Button, Form, Input, Modal, Select } from "antd";
import { Switch } from "@/components/ui/base/switch";
import type { InputRef } from "antd";
import type { ColumnsType } from "antd/es/table";
import { PencilLine, Pin, Plus, RefreshCw, Search, Send, Upload, X } from "lucide-react";

View File

@@ -1,4 +1,5 @@
import { ConfigProvider, Dropdown, Tooltip } from "antd";
import { ConfigProvider, Dropdown } from "antd";
import { Tooltip } from "@/components/ui/base/tooltip";
import type { MenuProps } from "antd";
import {
ArrowLeft,
@@ -131,7 +132,7 @@ export function AdminShell() {
<main className="admin-shell app-user-workspace flex h-full min-h-0 overflow-hidden text-foreground">
<aside className={cn("app-workspace-sidebar admin-sidebar hidden shrink-0 flex-col overflow-hidden lg:flex", collapsed && "is-collapsed")}>
<div className="admin-sidebar-identity shrink-0">
<Tooltip mouseEnterDelay={0.1} title={collapsed ? "查看更新日志" : undefined} placement="right" rootClassName="app-workspace-sidebar-tooltip">
<Tooltip delay={100} title={collapsed ? "查看更新日志" : undefined} placement="right">
<AppChangelogButton
className={cn("admin-sidebar-brand-button", collapsed && "is-collapsed")}
icon={<BrandLogoFrame className="admin-sidebar-brand-mark grid shrink-0 place-items-center bg-foreground text-background" logoClassName="size-5 object-contain" alt="" fallback={<InfinityIcon className="size-4" />} />}
@@ -145,7 +146,7 @@ export function AdminShell() {
</div>
<AdminNavigation collapsed={collapsed} />
<div className="admin-sidebar-footer shrink-0">
<Tooltip mouseEnterDelay={0.1} title={collapsed ? "返回创作台" : undefined} placement="right" rootClassName="app-workspace-sidebar-tooltip">
<Tooltip delay={100} title={collapsed ? "返回创作台" : undefined} placement="right">
<NavLink
to="/"
aria-label={collapsed ? "返回创作台" : undefined}
@@ -157,7 +158,7 @@ export function AdminShell() {
</Tooltip>
</div>
</aside>
<Tooltip mouseEnterDelay={0.1} title={collapsed ? "展开侧栏" : "收起侧栏"} placement="right" rootClassName="app-workspace-sidebar-tooltip">
<Tooltip delay={100} title={collapsed ? "展开侧栏" : "收起侧栏"} placement="right">
<button type="button" className={cn("admin-sidebar-edge-toggle hidden lg:grid", collapsed && "is-collapsed")} onClick={toggleCollapsed} aria-label={collapsed ? "展开侧栏" : "收起侧栏"} aria-expanded={!collapsed}>
{collapsed ? <ChevronRight className="size-3.5" aria-hidden="true" /> : <ChevronLeft className="size-3.5" aria-hidden="true" />}
</button>
@@ -293,7 +294,7 @@ function AdminNavigation({ collapsed }: { collapsed: boolean }) {
return (
<div key={group.label} className="admin-nav-group">
{!collapsed ? (
<div className="admin-nav-group-label mb-1 px-2.5 text-[var(--fs-tiny)] font-medium text-foreground/38">
<div className="admin-nav-group-label">
<span>{group.label}</span>
</div>
) : (
@@ -301,16 +302,16 @@ function AdminNavigation({ collapsed }: { collapsed: boolean }) {
)}
<div className={cn("admin-nav-group-items space-y-0.5", collapsed && "is-collapsed")}>
{visibleItems.map((item) => (
<Tooltip key={item.path} mouseEnterDelay={0.1} title={collapsed ? item.label : undefined} placement="right" rootClassName="app-workspace-sidebar-tooltip">
<Tooltip key={item.path} delay={100} title={collapsed ? item.label : undefined} placement="right">
<NavLink
to={item.path}
end={item.path === "/admin"}
aria-label={collapsed ? item.label : undefined}
className={({ isActive }) =>
cn(
"app-workspace-nav-link flex h-8 items-center rounded-md text-[var(--fs-body)] transition-colors",
"app-workspace-nav-link flex h-8 items-center rounded-md transition-colors",
collapsed ? "justify-center px-0" : "gap-2.5 px-2.5",
isActive ? "is-active font-medium" : "text-foreground/62 hover:bg-surface-hover hover:text-foreground",
isActive ? "is-active" : "text-foreground/62 hover:bg-surface-hover hover:text-foreground",
)
}
>

View File

@@ -1,3 +1,5 @@
import { StatusBadge } from "@/components/ui/base/badges";
import { EmptyState } from "@/components/ui/product/empty-state";
import { App, Button, Dropdown, Table } from "antd";
import type { ButtonProps, MenuProps, TableProps } from "antd";
import { saveAs } from "file-saver";
@@ -12,12 +14,18 @@ export const configuredSecretText = "已配置 · 留空不改";
export type AdminStatusTone = "neutral" | "success" | "warning" | "error" | "info";
/** admin 状态徽章:归并到自研 StatusBadge(outline 圆点胶囊),admin 全部页面零改动统一视觉。
* tone 归一:info 在 admin 中本就是前景中性(--admin-status-info: var(--foreground))→ neutral。 */
const adminToneToBadgeTone: Record<AdminStatusTone, "neutral" | "success" | "warning" | "error"> = {
neutral: "neutral",
success: "success",
warning: "warning",
error: "error",
info: "neutral",
};
export function AdminStatusBadge({ label, tone = "neutral", title }: { label: string; tone?: AdminStatusTone; title?: string }) {
return (
<span className="admin-status-badge" data-tone={tone} title={title}>
{label}
</span>
);
return <StatusBadge tone={adminToneToBadgeTone[tone]} title={title} label={label} />;
}
export function AdminStatTile({ label, value, detail, trend }: { label: string; value: string | number; detail?: string; trend?: { value: string; tone?: AdminStatusTone } }) {
@@ -133,14 +141,14 @@ export function AdminExportButton({
export function AdminTableEmpty({ filtered = false, title, description, action }: { filtered?: boolean; title?: string; description?: string; action?: ReactNode }) {
return (
<div className="flex min-h-40 flex-col items-center justify-center px-6 py-8 text-center">
<span className="grid size-9 place-items-center rounded-md bg-muted/35 text-foreground/45">
<SearchX className="size-4" />
</span>
<div className="mt-3 text-sm font-medium">{title || (filtered ? "没有符合筛选条件的数据" : "暂无数据")}</div>
{description ? <p className="mt-1 max-w-sm text-xs leading-5 text-foreground/50">{description}</p> : null}
{action ? <div className="mt-4">{action}</div> : null}
</div>
<EmptyState
size="compact"
icon={SearchX}
title={title || (filtered ? "没有符合筛选条件的数据" : "暂无数据")}
description={description}
action={action}
className="min-h-40 w-full justify-center"
/>
);
}

View File

@@ -1,5 +1,7 @@
import { useEffect, useState } from "react";
import { App, Button, Descriptions, Drawer, Empty, Progress, Skeleton, Tabs } from "antd";
import { App, Button, Descriptions, Drawer, Progress, Skeleton, Tabs } from "antd";
import { IconButton } from "@/components/ui/base/buttons";
import { EmptyState } from "@/components/ui/product/empty-state";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { PaginationBar } from "@/components/layout/workspace-page";
@@ -97,8 +99,8 @@ export function AdminUserDetailDrawer({ userId, onClose, previousUserId, nextUse
rootClassName="admin-drawer"
extra={onNavigate ? (
<div className="flex items-center gap-1">
<Button type="text" size="small" aria-label="上一条用户" disabled={!previousUserId} icon={<ChevronLeft className="size-4" />} onClick={() => previousUserId && onNavigate(previousUserId)} />
<Button type="text" size="small" aria-label="下一条用户" disabled={!nextUserId} icon={<ChevronRight className="size-4" />} onClick={() => nextUserId && onNavigate(nextUserId)} />
<IconButton size="sm" variant="ghost" aria-label="上一条用户" disabled={!previousUserId} icon={ChevronLeft} onClick={() => previousUserId && onNavigate(previousUserId)} />
<IconButton size="sm" variant="ghost" aria-label="下一条用户" disabled={!nextUserId} icon={ChevronRight} onClick={() => nextUserId && onNavigate(nextUserId)} />
</div>
) : null}
>
@@ -225,7 +227,7 @@ export function AdminUserDetailDrawer({ userId, onClose, previousUserId, nextUse
]}
/>
) : (
<Empty description="没有用户详情" />
<EmptyState size="compact" title="没有用户详情" />
)}
</Drawer>
);

View File

@@ -1,5 +1,7 @@
import { App, Button, DatePicker, Drawer, Form, Input, Modal, Select, Tabs, Tag } from "antd";
import { Tooltip } from "@/components/ui/base/tooltip";
import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react";
import { App, Button, DatePicker, Drawer, Form, Input, Modal, Select, Tabs, Tag, Tooltip } from "antd";
import type { ColumnsType } from "antd/es/table";
import dayjs, { type Dayjs } from "dayjs";
import { AlertTriangle, BarChart3, CircleDollarSign, Clock3, Gauge, Pencil, Plus, RefreshCw, Settings2, Trash2, UsersRound, Workflow } from "lucide-react";

View File

@@ -1,5 +1,6 @@
import { useEffect, useState } from "react";
import { App, Button, Descriptions, Drawer, Empty, Skeleton, Tabs, Typography } from "antd";
import { App, Button, Descriptions, Drawer, Skeleton, Tabs, Typography } from "antd";
import { EmptyState } from "@/components/ui/product/empty-state";
import { RefreshCw } from "lucide-react";
import { formatCredits } from "@/constant/credits";
@@ -49,7 +50,7 @@ export function ApiLogDetailDrawer({ logId, onClose, onLogUpdated }: { logId: st
return (
<Drawer title="请求详情" open={Boolean(logId)} onClose={onClose} size="min(920px, 100vw)" destroyOnHidden rootClassName="admin-drawer">
{loading ? <Skeleton active paragraph={{ rows: 12 }} /> : log ? <LogDetail log={log} querying={querying} onQueryProviderTask={queryProviderTask} /> : <Empty description="没有请求详情" />}
{loading ? <Skeleton active paragraph={{ rows: 12 }} /> : log ? <LogDetail log={log} querying={querying} onQueryProviderTask={queryProviderTask} /> : <EmptyState size="compact" title="没有请求详情" />}
</Drawer>
);
}
@@ -125,7 +126,7 @@ function requestKindText(value: ApiCallLog["requestKind"]) {
}
function PayloadPanel({ value, empty }: { value?: string; empty: string }) {
if (!value) return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={empty} />;
if (!value) return <EmptyState size="compact" title={empty} />;
return (
<div className="relative">
<div className="absolute right-3 top-2 z-10">

View File

@@ -1,4 +1,6 @@
import { App, Button, Form, Input, InputNumber, Select, Skeleton, Switch } from "antd";
import { App, Button, Form, Input, InputNumber, Skeleton } from "antd";
import { Select } from "@/components/ui/base/select";
import { Switch } from "@/components/ui/base/switch";
import { AlertTriangle, AtSign, BadgeCheck, KeyRound, MailCheck, RefreshCw, RotateCcw, Save, Send, Server } from "lucide-react";
import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
import { useBlocker } from "react-router";

View File

@@ -1,5 +1,6 @@
import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
import { App, Button, Skeleton, Switch } from "antd";
import { App, Button, Skeleton } from "antd";
import { Switch } from "@/components/ui/base/switch";
import { AlertTriangle, Clapperboard, Coins, ListChecks, LockKeyhole, MonitorCog, PlugZap, RadioTower, RefreshCw, ShieldCheck, Sparkles } from "lucide-react";
import { cn } from "@/lib/utils";

View File

@@ -1,4 +1,6 @@
import { Alert, Button, InputNumber, Select, Switch, Tag } from "antd";
import { Button, InputNumber, Select, Tag } from "antd";
import { Switch } from "@/components/ui/base/switch";
import { Callout } from "@/components/ui/product/callout";
import { RotateCcw } from "lucide-react";
import type { ReactNode } from "react";
@@ -233,7 +235,7 @@ export function CapabilityScopeEditor({ capability, sourceSpecs, value, onChange
</Button>
</div>
{sourceError ? <Alert type="warning" showIcon message="当前选择需要调整" description={`${sourceError}。可重新选择,或采用全部可用范围。`} /> : null}
{sourceError ? <Callout tone="warning" title="当前选择需要调整">{`${sourceError}。可重新选择,或采用全部可用范围。`}</Callout> : null}
{source.operations?.length ? (
<CapabilityBlock title="生成方式">
@@ -263,7 +265,7 @@ export function CapabilityScopeEditor({ capability, sourceSpecs, value, onChange
</div>
</div>
<div className="col-span-12 sm:col-span-1">
<Switch size="small" checked={Boolean(selected)} onChange={(enabled) => updateInput(current, definition.name, enabled ? limit : undefined, update)} />
<Switch size="sm" checked={Boolean(selected)} onChange={(enabled) => updateInput(current, definition.name, enabled ? limit : undefined, update)} />
</div>
<div className="col-span-6 sm:col-span-3">
<NumberInput
@@ -310,7 +312,7 @@ export function CapabilityScopeEditor({ capability, sourceSpecs, value, onChange
</CapabilityBlock>
) : null}
{!sourceInputs.length && !sourceOptions.length && !source.operations?.length ? <Alert type="info" showIcon message="该类型暂无额外能力参数" description="线路仍可按优先级和权重参与路由。" /> : null}
{!sourceInputs.length && !sourceOptions.length && !source.operations?.length ? <Callout tone="info" title="该类型暂无额外能力参数">线路仍可按优先级和权重参与路由。</Callout> : null}
</div>
);
}
@@ -481,7 +483,7 @@ function OptionRuleEditor({ definition, source, value, onChange }: { definition:
<div className="text-xs text-foreground/45">{constraintSummary(source, definition.unit)}</div>
</div>
<div className="col-span-12 sm:col-span-1">
<Switch size="small" checked={Boolean(value)} onChange={(enabled) => onChange(enabled ? source : undefined)} />
<Switch size="sm" checked={Boolean(value)} onChange={(enabled) => onChange(enabled ? source : undefined)} />
</div>
<div className="col-span-12 sm:col-span-6">
{source.values ? (

View File

@@ -1,4 +1,5 @@
import { App, Button, Input, Modal, Select } from "antd";
import { IconButton } from "@/components/ui/base/buttons";
import type { ColumnsType } from "antd/es/table";
import { Download, Eye, Play, Search } from "lucide-react";
import { saveAs } from "file-saver";
@@ -294,7 +295,7 @@ function MediaResult({ log, onPreview }: { log: ApiCallLog; onPreview: (url: str
) : null}
{!previewUnavailable && log.mediaCount > 1 ? <span className="absolute bottom-0.5 right-0.5 rounded-sm bg-black/65 px-1 text-[var(--fs-micro)] leading-4 text-white">{log.mediaCount}</span> : null}
</button>
<Button type="text" size="small" className="!size-7 !min-w-7 !p-0" icon={<Download className="size-3.5" />} onClick={() => downloadMedia(url, kind)} title="下载原文件" aria-label="下载原文件" />
<IconButton size="sm" variant="ghost" icon={Download} onClick={() => downloadMedia(url, kind)} title="下载原文件" aria-label="下载原文件" />
</div>
);
}

View File

@@ -1,5 +1,7 @@
import { AlipayCircleFilled, WechatFilled } from "@ant-design/icons";
import { Alert, App, Button, DatePicker, Drawer, Form, Input, InputNumber, Select, Switch, Tabs } from "antd";
import { Callout } from "@/components/ui/product/callout";
import { App, Button, DatePicker, Drawer, Form, Input, InputNumber, Select, Tabs } from "antd";
import { Switch } from "@/components/ui/base/switch";
import type { ColumnsType } from "antd/es/table";
import dayjs, { type Dayjs } from "dayjs";
import { Eye, Plus, RefreshCw, Search, Settings2, XCircle } from "lucide-react";
@@ -497,7 +499,7 @@ export default function AdminPaymentsPage() {
}
scroll
>
<Alert className="my-4" type="info" showIcon message="平台不提供支付退款" description="管理端仅提供查单、关单和对账。关单前始终先向渠道查单;对账发现已支付未入账订单时会幂等补发积分。" />
<Callout className="my-4" tone="info" title="平台不提供支付退款">管理端仅提供查单、关单和对账。关单前始终先向渠道查单;对账发现已支付未入账订单时会幂等补发积分。</Callout>
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
@@ -610,12 +612,7 @@ export default function AdminPaymentsPage() {
>
{providerDrawer ? (
<Form form={providerForm} layout="vertical" requiredMark="optional">
<Alert
className="mb-4"
type={providerDrawer.pluginEnabled ? "info" : "warning"}
showIcon
message={providerDrawer.pluginEnabled ? "密钥会加密保存,历史订单固定使用创建时的配置版本。" : "该宿主插件当前已在插件管理中停用;保存配置后仍需开放插件才能接受新订单。"}
/>
<Callout className="mb-4" tone={providerDrawer.pluginEnabled ? "info" : "warning"} title={providerDrawer.pluginEnabled ? "密钥会加密保存,历史订单固定使用创建时的配置版本。" : "该宿主插件当前已在插件管理中停用;保存配置后仍需开放插件才能接受新订单。"} />
<Form.Item name="enabled" label="渠道配置启用" valuePropName="checked">
<Switch />
</Form.Item>
@@ -703,7 +700,7 @@ export default function AdminPaymentsPage() {
setDetailItems([]);
}}
>
{detailRun?.error ? <Alert className="mb-4" type="error" showIcon message="对账执行失败" description={detailRun.error} /> : null}
{detailRun?.error ? <Callout className="mb-4" tone="error" title="对账执行失败">{detailRun.error}</Callout> : null}
<AdminDataTable
toolbar={
<Select

View File

@@ -1,4 +1,5 @@
import { App, Button, Input, Select, Switch } from "antd";
import { App, Button, Input, Select } from "antd";
import { Switch } from "@/components/ui/base/switch";
import { AlipayCircleFilled, WechatFilled } from "@ant-design/icons";
import type { ColumnsType } from "antd/es/table";
import { CloudUpload, PlugZap, RefreshCw, Search, Trash2, UsersRound } from "lucide-react";

View File

@@ -1,4 +1,5 @@
import { App, Button, Form, Input, Skeleton, Switch } from "antd";
import { App, Button, Form, Input, Skeleton } from "antd";
import { Switch } from "@/components/ui/base/switch";
import { Copyright, Globe2, Image as ImageIcon, MonitorPlay, Moon, Palette, RefreshCw, RotateCcw, Save, Search, Sun, Type, Undo2, Upload } from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode, type RefObject } from "react";
import { useBlocker } from "react-router";

View File

@@ -1,4 +1,5 @@
import { App, Button, Form, Input, Skeleton, Switch } from "antd";
import { App, Button, Form, Input, Skeleton } from "antd";
import { Switch } from "@/components/ui/base/switch";
import { AlertTriangle, CloudUpload, KeyRound, RefreshCw, RotateCcw, Save } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react";
import { useBlocker } from "react-router";

View File

@@ -1,4 +1,8 @@
import { Button, Form, Input, InputNumber, Segmented, Select, Tooltip } from "antd";
import { Button, Form, Input, InputNumber } from "antd";
import { Select } from "@/components/ui/base/select";
import { IconButton } from "@/components/ui/base/buttons";
import { SegmentedControl } from "@/components/ui/base/segmented-control";
import { Tooltip } from "@/components/ui/base/tooltip";
import { Check, Copy, LockKeyhole, Moon, Plus, Sparkles, Sun, Trash2 } from "lucide-react";
import { useMemo, useState } from "react";
@@ -82,7 +86,7 @@ export function SkinThemeEditor({
</button>
<div className="admin-skin-card-actions">
<Tooltip title="复制这套主题">
<Button type="text" size="small" aria-label={`复制 ${theme.name}`} icon={<Copy className="size-3.5" />} disabled={disabled || themes.length >= 16} onClick={() => onDuplicate(theme.id)} />
<IconButton size="sm" variant="ghost" icon={Copy} aria-label={`复制 ${theme.name}`} disabled={disabled || themes.length >= 16} onClick={() => onDuplicate(theme.id)} />
</Tooltip>
<Tooltip title={theme.locked ? "系统默认主题不可删除" : "删除这套主题"}>
<Button
@@ -134,7 +138,8 @@ export function SkinThemeEditor({
<strong>颜色体系</strong>
<span>同一用途的控件统一消费下列语义颜色</span>
</div>
<Segmented<SkinThemeMode>
<SegmentedControl<SkinThemeMode>
ariaLabel="皮肤预览模式"
value={mode}
options={[
{

View File

@@ -1,4 +1,5 @@
import { App, Button, Input, Skeleton, Switch } from "antd";
import { App, Button, Input, Skeleton } from "antd";
import { Switch } from "@/components/ui/base/switch";
import { AlertTriangle, BadgeCheck, CircleCheck, CloudDownload, KeyRound, RefreshCw, RotateCcw, Save, Server, ShieldCheck, Trash2, Wifi } from "lucide-react";
import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
import { useBlocker } from "react-router";

View File

@@ -1,4 +1,5 @@
import { App, Button, Input, Skeleton, Switch } from "antd";
import { App, Button, Input, Skeleton } from "antd";
import { Switch } from "@/components/ui/base/switch";
import { AlertTriangle, ArrowDown, ArrowUp, BadgeCheck, Eye, ListOrdered, Plus, RefreshCw, RotateCcw, Save, Search, ShieldAlert, ShieldCheck, Trash2 } from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useBlocker } from "react-router";

View File

@@ -1,4 +1,6 @@
import { App, Button, Form, Input, Select, Skeleton, Switch } from "antd";
import { App, Button, Form, Input, Skeleton } from "antd";
import { Select } from "@/components/ui/base/select";
import { Switch } from "@/components/ui/base/switch";
import { AlertTriangle, BadgeCheck, Check, Cloud, Database, Globe2, HardDrive, KeyRound, LocateFixed, RefreshCw, RotateCcw, Save, Server, ShieldCheck, Wifi } from "lucide-react";
import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
import { useBlocker } from "react-router";

View File

@@ -1,4 +1,6 @@
import { Alert, App, Button, Drawer, Form, Input, Popconfirm, Select, Tabs, Tag } from "antd";
import { App, Button, Drawer, Form, Input, Popconfirm, Select, Tabs } from "antd";
import { StatusBadge } from "@/components/ui/base/badges";
import { Callout } from "@/components/ui/product/callout";
import type { ColumnsType } from "antd/es/table";
import { Braces, Copy, FileJson, FileText, Plus, Power, Search, ShieldCheck, Trash2 } from "lucide-react";
import { useEffect, useMemo, useRef, useState } from "react";
@@ -212,19 +214,13 @@ export default function StoryboardPromptsPage() {
</div>
{pendingOperation ? (
<Alert
type="warning"
showIcon
title="当前版本有未保存修改"
description={`切换到“${definitionByOperation.get(pendingOperation)?.label || pendingOperation}”会丢弃当前草稿。`}
action={<div className="flex gap-2"><Button size="small" onClick={() => setPendingOperation("")}>继续编辑</Button><Button size="small" danger onClick={() => loadDraftBaseline(pendingOperation)}>放弃并切换</Button></div>}
/>
<Callout tone="warning" title="当前版本有未保存修改" action={<div className="flex gap-2"><Button size="small" onClick={() => setPendingOperation("")}>继续编辑</Button><Button size="small" danger onClick={() => loadDraftBaseline(pendingOperation)}>放弃并切换</Button></div>}>{`切换到“${definitionByOperation.get(pendingOperation)?.label || pendingOperation}”会丢弃当前草稿。`}</Callout>
) : null}
<div className="grid min-h-0 flex-1 gap-0 lg:grid-cols-3">
<section className="flex min-h-0 flex-col border-b border-border p-4 lg:col-span-2 lg:border-b-0 lg:border-r">
<div className="mb-3 flex shrink-0 flex-wrap items-start justify-between gap-3">
<div className="flex flex-wrap items-center gap-2"><h3 className="text-sm font-semibold">模板内容</h3>{dirty ? <Tag variant="filled" color="warning">未保存</Tag> : null}</div>
<div className="flex flex-wrap items-center gap-2"><h3 className="text-sm font-semibold">模板内容</h3>{dirty ? <StatusBadge variant="filled" tone="warning" label="未保存" /> : null}</div>
<div className="flex flex-wrap justify-end gap-2">
{selectedDefinition?.variables.map((variable) => (
<Button key={variable.placeholder} size="small" icon={<Braces className="size-3.5" />} onClick={() => editorRef.current?.insertText(variable.placeholder)}>

View File

@@ -1,5 +1,5 @@
import type { ColumnsType } from "antd/es/table";
import { Eye, Pencil, Power } from "lucide-react";
import { Eye, Pencil, Power, Trash2 } from "lucide-react";
import { formatCredits } from "@/constant/credits";
import { IdentityProviderBadge } from "@/components/layout/identity-provider-badge";
@@ -24,12 +24,14 @@ export function createUserColumns({
onView,
onEdit,
onToggleStatus,
onPurge,
}: {
actorId?: string;
visibleColumns: Set<UserColumnKey>;
onView: (user: AdminUser) => void;
onEdit: (user: AdminUser) => void;
onToggleStatus: (user: AdminUser) => Promise<void>;
onPurge: (user: AdminUser) => void;
}): ColumnsType<AdminUser> {
const columns: Array<ColumnsType<AdminUser>[number] & { key: UserColumnKey }> = [
{
@@ -79,6 +81,19 @@ export function createUserColumns({
},
onClick: () => onToggleStatus(user),
},
{
key: "purge",
label: "删除用户",
icon: <Trash2 className="size-3.5" />,
danger: true,
disabled: user.id === actorId,
confirm: {
title: "彻底删除这个用户?",
description: "该账号及其全部关联数据将被永久移除且不可恢复。停用可保留数据,仅当账号无资金流水和创作内容时才允许删除。",
okText: "彻底删除",
},
onClick: () => onPurge(user),
},
]}
/>
),

View File

@@ -1,4 +1,5 @@
import { App, Button, Drawer, Form, Input, Select } from "antd";
import { App, Button, Drawer, Form, Input } from "antd";
import { Select } from "@/components/ui/base/select";
import { useEffect, useState } from "react";
import { createAdminUser, updateAdminUser, type AdminUser, type LocalUser } from "@/services/api/auth";

View File

@@ -1,10 +1,11 @@
import { App, Button, Checkbox, Dropdown, Input, Select } from "antd";
import { App, Button, Dropdown, Input, Select } from "antd";
import { Checkbox } from "@/components/ui/base/checkbox";
import { Ban, Search, Settings2, UserPlus } from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { PaginationBar } from "@/components/layout/workspace-page";
import { useDebouncedValue } from "@/hooks/use-debounced-value";
import { bulkDisableAdminUsers, deleteAdminUser, listAdminUsers, updateAdminUser, type AdminUser, type LocalUser } from "@/services/api/auth";
import { bulkDisableAdminUsers, deleteAdminUser, listAdminUsers, purgeAdminUser, updateAdminUser, type AdminUser, type LocalUser } from "@/services/api/auth";
import { useUserStore } from "@/stores/use-user-store";
import { AdminBatchBar, AdminDataTable, AdminTableEmpty } from "../components/admin-ui";
import { useTableUrlState } from "../lib/use-table-url-state";
@@ -101,13 +102,27 @@ export default function UsersPanel({ onUserChanged }: { onUserChanged?: (user: L
}
}, [message, replaceUser]);
const purgeUser = useCallback(async (user: AdminUser) => {
try {
await purgeAdminUser(user.id);
setUsers((items) => items.filter((item) => item.id !== user.id));
setTotal((value) => Math.max(0, value - 1));
setSelectedUserIds((ids) => ids.filter((id) => id !== user.id));
if (detailUserId === user.id) setDetailUserId(null);
message.success(`用户 ${user.displayName || user.username} 已彻底删除`);
} catch (error) {
message.error(error instanceof Error ? error.message : "删除用户失败");
}
}, [detailUserId, message]);
const columns = useMemo(() => createUserColumns({
actorId: actor?.id,
visibleColumns,
onView: (user) => setDetailUserId(user.id),
onEdit: (user) => { setCreateUserOpen(false); setEditingUser(user); },
onToggleStatus: toggleStatus,
}), [actor?.id, toggleStatus, visibleColumns]);
onPurge: purgeUser,
}), [actor?.id, purgeUser, toggleStatus, visibleColumns]);
const resetFilters = () => update({ filter: "", role: "all", status: "all", page: 1 });
@@ -182,6 +197,7 @@ export default function UsersPanel({ onUserChanged }: { onUserChanged?: (user: L
{userColumnOptions.map((option) => (
<label key={option.key} className="flex cursor-pointer items-center gap-2 rounded px-2 py-1.5 text-sm hover:bg-muted/60">
<Checkbox
bare
checked={visibleColumns.has(option.key)}
disabled={option.locked}
onChange={(event) => setVisibleColumns((current) => {