fix(admin): 管理端交互 - 立即切换并支持中文筛选输入
This commit is contained in:
@@ -18,7 +18,7 @@ document.body.style.fontFamily = '"SF Pro Display","SF Pro Text","PingFang SC","
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<AppProviders>
|
||||
<RouterProvider router={router} />
|
||||
<RouterProvider router={router} useTransitions={false} />
|
||||
</AppProviders>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { App, Button, Form, Input, InputNumber, Modal, Spin, Switch } from "antd
|
||||
import { ChevronRight, Copy, Pencil, Plus, Power, Search, Trash2 } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useSearchParams } from "react-router";
|
||||
import { AdminSearchInput } from "../components/admin-search-input";
|
||||
|
||||
import { ChannelHeadersEditor, validateChannelHeaders } from "@/components/channel-headers-editor";
|
||||
import { refreshSystemChannels } from "@/lib/user-session";
|
||||
@@ -255,7 +256,7 @@ export default function ChannelsPage() {
|
||||
<Button block type="primary" icon={<Plus className="size-4" />} onClick={() => openDrawer()}>
|
||||
新增渠道
|
||||
</Button>
|
||||
<Input
|
||||
<AdminSearchInput
|
||||
id="admin-channel-search"
|
||||
aria-label="搜索系统渠道"
|
||||
autoComplete="off"
|
||||
@@ -263,7 +264,7 @@ export default function ChannelsPage() {
|
||||
prefix={<Search className="size-4 text-foreground/40" />}
|
||||
value={keyword}
|
||||
placeholder="搜索渠道名称或地址"
|
||||
onChange={(event) => updateUrl({ filter: event.target.value }, true)}
|
||||
onValueChange={(value) => updateUrl({ filter: value }, true)}
|
||||
/>
|
||||
<Select
|
||||
aria-label="筛选渠道状态"
|
||||
|
||||
32
web/src/pages/admin/components/admin-search-input.tsx
Normal file
32
web/src/pages/admin/components/admin-search-input.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Input, type InputProps } from "antd";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
// URL-backed filters must not replace the IME's uncommitted composition text.
|
||||
export function AdminSearchInput({ value = "", onValueChange, ...props }: Omit<InputProps, "value" | "defaultValue" | "onChange" | "onCompositionStart" | "onCompositionEnd"> & { value?: string; onValueChange: (value: string) => void }) {
|
||||
const [draft, setDraft] = useState(value);
|
||||
const composing = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!composing.current) setDraft(value);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<Input
|
||||
{...props}
|
||||
value={draft}
|
||||
onCompositionStart={() => {
|
||||
composing.current = true;
|
||||
}}
|
||||
onCompositionEnd={(event) => {
|
||||
composing.current = false;
|
||||
const next = event.currentTarget.value;
|
||||
setDraft(next);
|
||||
onValueChange(next);
|
||||
}}
|
||||
onChange={(event) => {
|
||||
const next = event.target.value;
|
||||
setDraft(next);
|
||||
if (!composing.current) onValueChange(next);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -142,6 +142,7 @@ function LogDetail({ log, providerRequestId, videoURL, resultText, querying, onP
|
||||
<InfoItem label="耗时" value={<span className="tabular-nums">{formatDuration(log.durationMs)}</span>} />
|
||||
<InfoItem label="HTTP 状态" value={log.statusCode || "未记录"} />
|
||||
<InfoItem label="请求地址" value={<span className="break-all font-mono text-xs">{log.method} {log.path || "未记录"}</span>} />
|
||||
<InfoItem label="上游渠道 URL" value={<span className="break-all font-mono text-xs">{log.upstreamUrl || "未记录"}</span>} />
|
||||
</div>
|
||||
{log.error || log.errorCode ? (
|
||||
<div className="api-log-detail-error">
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useDebouncedValue } from "@/hooks/use-debounced-value";
|
||||
import { adminResourceFileUrl, deleteAdminResources, downloadAdminResource, getAdminStorageStats, listAdminResources, type AdminStorageResource, type AdminStorageStats } from "@/services/api/admin-storage";
|
||||
import { AdminBatchBar, AdminDataTable, AdminFilterChip, AdminStatTile, AdminStatusBadge, AdminTableEmpty } from "./admin-ui";
|
||||
import { Select } from "@/components/ui/base/select";
|
||||
import { AdminSearchInput } from "./admin-search-input";
|
||||
|
||||
const pageSizes = [20, 50, 100];
|
||||
|
||||
@@ -201,7 +202,7 @@ export default function StorageResourcesPanel() {
|
||||
<AdminDataTable
|
||||
toolbar={
|
||||
<div className="admin-storage-resource-filters">
|
||||
<Input
|
||||
<AdminSearchInput
|
||||
aria-label="搜索资源"
|
||||
autoComplete="off"
|
||||
allowClear
|
||||
@@ -209,9 +210,9 @@ export default function StorageResourcesPanel() {
|
||||
prefix={<Search className="size-4 text-foreground/40" />}
|
||||
value={keyword}
|
||||
placeholder="资源 ID 或对象路径"
|
||||
onChange={(event) => updateUrl({ filter: event.target.value, page: 1 }, true)}
|
||||
onValueChange={(value) => updateUrl({ filter: value, page: 1 }, true)}
|
||||
/>
|
||||
<Input aria-label="按用户名、昵称、邮箱或用户 ID 筛选" autoComplete="off" allowClear className="w-64" value={userQuery} placeholder="用户名 / 昵称 / 邮箱 / 用户 ID" onChange={(event) => updateUrl({ user: event.target.value, userId: "", page: 1 }, true)} />
|
||||
<AdminSearchInput aria-label="按用户名、昵称、邮箱或用户 ID 筛选" autoComplete="off" allowClear className="w-64" value={userQuery} placeholder="用户名 / 昵称 / 邮箱 / 用户 ID" onValueChange={(value) => updateUrl({ user: value, userId: "", page: 1 }, true)} />
|
||||
<Select aria-label="筛选资源类型" className="w-32" value={kind} onChange={(value) => updateUrl({ kind: value, page: 1 })} options={kindOptions} />
|
||||
<Select aria-label="筛选资源状态" className="w-32" value={status} onChange={(value) => updateUrl({ status: value, page: 1 })} options={statusOptions} />
|
||||
<Select aria-label="筛选存储类型" className="w-36" value={provider} onChange={(value) => updateUrl({ provider: value, page: 1 })} options={providerOptions} />
|
||||
|
||||
@@ -18,6 +18,7 @@ import { AdminBatchBar, AdminDataTable, AdminExportButton, AdminFilterChip, Admi
|
||||
import { logBillingLabel, logStatus, normalizeLogView } from "./log-view";
|
||||
import "./logs-page.css";
|
||||
import { Select } from "@/components/ui/base/select";
|
||||
import { AdminSearchInput } from "../components/admin-search-input";
|
||||
|
||||
export default function LogsPage() {
|
||||
const { message } = App.useApp();
|
||||
@@ -186,6 +187,8 @@ export default function LogsPage() {
|
||||
title="请求明细"
|
||||
description="模型生成与结果下载记录;仅计费调用扣除积分"
|
||||
actions={
|
||||
<>
|
||||
<Button icon={<RefreshCw className="size-4" />} loading={loading} onClick={() => { setLoading(true); setRetry((value) => value + 1); }}>刷新列表</Button>
|
||||
<AdminExportButton
|
||||
exportFile={() => exportAdminApiLogs({ recordType, capability: capability === "all" ? undefined : capability, keyword: debouncedKeyword || undefined, status: status === "all" ? undefined : status })}
|
||||
fileName={() => `请求明细-${new Date().toISOString().slice(0, 10)}.csv`}
|
||||
@@ -193,6 +196,7 @@ export default function LogsPage() {
|
||||
successMessage="已按当前筛选导出请求明细"
|
||||
errorMessage="导出请求明细失败"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{loadError ? <Alert type="error" showIcon title="请求明细读取失败" description={loadError} action={<Button size="small" onClick={() => setRetry((value) => value + 1)}>重试</Button>} /> : null}
|
||||
@@ -200,13 +204,13 @@ export default function LogsPage() {
|
||||
className={view === "all" ? "admin-logs-full" : "admin-logs-compact"}
|
||||
trailing={<Segmented aria-label="请求明细视图" value={view} onChange={(value) => updateUrl({ view: value })} options={[{ label: "排障", value: "troubleshoot" }, { label: "计费", value: "billing" }, { label: "全部字段", value: "all" }]} />}
|
||||
toolbar={
|
||||
<Input
|
||||
<AdminSearchInput
|
||||
allowClear
|
||||
className="app-list-search"
|
||||
prefix={<Search className="size-4 text-foreground/40" />}
|
||||
value={keyword}
|
||||
placeholder="搜索用户、渠道、模型、路径或请求号"
|
||||
onChange={(event) => updateUrl({ filter: event.target.value, page: 1 }, true)}
|
||||
onValueChange={(value) => updateUrl({ filter: value, page: 1 }, true)}
|
||||
/>
|
||||
}
|
||||
toolbarActiveFilters={
|
||||
|
||||
@@ -4,6 +4,7 @@ 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";
|
||||
import { useSearchParams } from "react-router";
|
||||
import { AdminSearchInput } from "../components/admin-search-input";
|
||||
|
||||
import { AdminDrawer } from "@/pages/admin/ui/overlays";
|
||||
import { PromptCodeEditor, type PromptCodeEditorHandle } from "@/components/prompt/prompt-code-editor";
|
||||
@@ -178,7 +179,7 @@ export default function StoryboardPromptsPage() {
|
||||
return (
|
||||
<AdminPageFrame title="提示词模板" description="平台创作策略与版本管理" actions={<Button type="primary" icon={<Plus className="size-4" />} disabled={!definitions.length} onClick={() => openDrawer()}>新建版本</Button>}>
|
||||
<AdminDataTable
|
||||
toolbar={<Input allowClear className="app-list-search" prefix={<Search className="size-4 text-foreground/40" />} value={keyword} aria-label="搜索提示词模板" placeholder="搜索模板或内容" onChange={(event) => updateUrl({ filter: event.target.value })} />}
|
||||
toolbar={<AdminSearchInput allowClear className="app-list-search" prefix={<Search className="size-4 text-foreground/40" />} value={keyword} aria-label="搜索提示词模板" placeholder="搜索模板或内容" onValueChange={(value) => updateUrl({ filter: value })} />}
|
||||
toolbarActive={hasFilters}
|
||||
toolbarFilters={<><Select aria-label="筛选提示词类型" className="w-40" value={operationFilter} onChange={(value) => updateUrl({ operation: value })} options={[{ label: "全部类型", value: "all" }, ...definitions.map((item) => ({ label: item.label, value: item.operation }))]} /><Select aria-label="筛选提示词状态" className="w-32" value={status} onChange={(value) => updateUrl({ status: value })} options={[{ label: "全部状态", value: "all" }, { label: "启用中", value: "enabled" }, { label: "历史版本", value: "disabled" }]} /></>}
|
||||
onReset={() => updateUrl({ filter: "", operation: "all", status: "all" })}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { AdminUserDetailDrawer } from "../components/admin-user-detail-drawer";
|
||||
import { createUserColumns, userColumnOptions, type UserColumnKey } from "./users-columns";
|
||||
import { AdminUserCreateDrawer, AdminUserEditDrawer } from "./users-drawer";
|
||||
import { Select } from "@/components/ui/base/select";
|
||||
import { AdminSearchInput } from "../components/admin-search-input";
|
||||
|
||||
const columnStorageKey = "admin-users-visible-columns";
|
||||
const allColumnKeys = userColumnOptions.map((item) => item.key);
|
||||
@@ -149,14 +150,14 @@ export default function UsersPanel({ onUserChanged }: { onUserChanged?: (user: L
|
||||
<AdminDataTable
|
||||
toolbar={
|
||||
<>
|
||||
<Input
|
||||
<AdminSearchInput
|
||||
allowClear
|
||||
className="app-list-search"
|
||||
prefix={<Search className="size-4 text-foreground/40" />}
|
||||
value={state.filter}
|
||||
aria-label="搜索用户"
|
||||
placeholder="搜索用户名、名称或邮箱"
|
||||
onChange={(event) => update({ filter: event.target.value, page: 1 }, true)}
|
||||
onValueChange={(value) => update({ filter: value, page: 1 }, true)}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user