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:
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
}
|
||||
>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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={[
|
||||
{
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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)}>
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user