feat(platform): 模型管理与后台能力 - 新增逻辑模型路由和响应拦截
This commit is contained in:
1 parent
0002cdd2cb
commit
8c5f56a551
89 files changed
+21451
-1170
No files matched your search
@@ -7,7 +7,7 @@ import { useUserStore } from "@/stores/use-user-store";
|
||||
|
||||
type OSSFormValues = {
|
||||
enabled?: boolean;
|
||||
provider: "aliyun" | "tencent";
|
||||
provider: "aliyun" | "tencent" | "qiniu";
|
||||
region?: string;
|
||||
endpoint?: string;
|
||||
cdnBaseUrl?: string;
|
||||
@@ -27,9 +27,10 @@ export function UserOSSSettingsForm() {
|
||||
const savedAt = formatSavedAt(setting?.updatedAt);
|
||||
const provider = Form.useWatch("provider", form) || "aliyun";
|
||||
const isTencentCOS = provider === "tencent";
|
||||
const isQiniuKodo = provider === "qiniu";
|
||||
const hasCurrentProviderSecret = Boolean(setting && setting.provider === provider && setting.hasAccessKeySecret);
|
||||
const accessKeyIdLabel = isTencentCOS ? "SecretId" : "AccessKey ID";
|
||||
const accessKeySecretLabel = isTencentCOS ? "SecretKey" : "AccessKey Secret";
|
||||
const accessKeyIdLabel = isTencentCOS ? "SecretId" : isQiniuKodo ? "AccessKey" : "AccessKey ID";
|
||||
const accessKeySecretLabel = isTencentCOS ? "SecretKey" : isQiniuKodo ? "SecretKey" : "AccessKey Secret";
|
||||
|
||||
useEffect(() => {
|
||||
if (!actor?.id) return;
|
||||
@@ -85,7 +86,7 @@ export function UserOSSSettingsForm() {
|
||||
<Cloud className="size-4" />
|
||||
我的对象存储
|
||||
</div>
|
||||
<p className="mt-1 max-w-3xl text-xs leading-5 text-foreground/55">支持阿里云 OSS 与腾讯云 COS。启用后,新上传和新生成的媒体优先写入你的存储桶;停用时回退到平台存储。历史资源继续使用创建时的存储与密钥配置,同一存储位置的 CDN 域名会跟随当前配置。</p>
|
||||
<p className="mt-1 max-w-3xl text-xs leading-5 text-foreground/55">启用后,新上传和新生成的媒体优先写入你的存储桶;停用时回退到平台存储。</p>
|
||||
</div>
|
||||
<div className="flex shrink-0 gap-2">
|
||||
<Tag color={setting?.enabled ? "success" : "default"}>{setting?.enabled ? "已启用" : "未启用"}</Tag>
|
||||
@@ -101,21 +102,21 @@ export function UserOSSSettingsForm() {
|
||||
</Form.Item>
|
||||
<Form.Item name="provider" label="存储服务" rules={[{ required: true, message: "请选择存储服务" }]} className="mb-3">
|
||||
<Select
|
||||
options={[{ label: "阿里云 OSS", value: "aliyun" }, { label: "腾讯云 COS", value: "tencent" }]}
|
||||
options={[{ label: "阿里云 OSS", value: "aliyun" }, { label: "腾讯云 COS", value: "tencent" }, { label: "七牛云 Kodo", value: "qiniu" }]}
|
||||
onChange={(nextProvider: OSSFormValues["provider"]) => {
|
||||
if (nextProvider !== provider) form.setFieldsValue({ region: "", endpoint: "", cdnBaseUrl: "", bucket: "", accessKeyId: "", accessKeySecret: "" });
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="region" label="Region" className="mb-3">
|
||||
<Input spellCheck={false} placeholder={isTencentCOS ? "ap-guangzhou" : "oss-cn-hangzhou"} />
|
||||
<Input spellCheck={false} placeholder={isTencentCOS ? "ap-guangzhou" : isQiniuKodo ? "z0 / cn-east-1" : "oss-cn-hangzhou"} />
|
||||
</Form.Item>
|
||||
<Form.Item name="endpoint" label="Endpoint" extra={isTencentCOS ? "可留空,系统会根据 Region 生成标准 COS Endpoint。" : undefined} className="mb-3">
|
||||
<Input inputMode="url" spellCheck={false} placeholder={isTencentCOS ? "https://cos.ap-guangzhou.myqcloud.com" : "https://oss-cn-hangzhou.aliyuncs.com"} />
|
||||
<Form.Item name="endpoint" label={isQiniuKodo ? "上传 Endpoint" : "Endpoint"} extra={isTencentCOS ? "可留空,系统会根据 Region 生成标准 COS Endpoint。" : undefined} className="mb-3">
|
||||
<Input inputMode="url" spellCheck={false} placeholder={isTencentCOS ? "https://cos.ap-guangzhou.myqcloud.com" : isQiniuKodo ? "https://up-z0.qiniup.com" : "https://oss-cn-hangzhou.aliyuncs.com"} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="cdnBaseUrl"
|
||||
label="CDN 加速域名"
|
||||
label={isQiniuKodo ? "绑定域名" : "CDN 加速域名"}
|
||||
extra={isTencentCOS
|
||||
? "选填。上传仍走 Endpoint,下载与预览改走 CDN;私有桶需开启 CDN 私有存储桶访问。CDN URL 不附带 COS 签名,未配置 CDN URL 鉴权时链接将长期可访问。"
|
||||
: "选填。上传仍走 Endpoint,下载与预览改走 CDN;阿里云私有 Bucket 需开启 CDN 私有 Bucket 回源。CDN URL 不附带 OSS 签名,未配置 CDN URL 鉴权时链接将长期可访问。"}
|
||||
@@ -125,16 +126,16 @@ export function UserOSSSettingsForm() {
|
||||
<Input inputMode="url" spellCheck={false} placeholder="https://media.example.com" />
|
||||
</Form.Item>
|
||||
<Form.Item name="bucket" label="Bucket" className="mb-3">
|
||||
<Input spellCheck={false} placeholder={isTencentCOS ? "my-canvas-assets-1250000000" : "my-canvas-assets"} />
|
||||
<Input spellCheck={false} placeholder={isTencentCOS ? "my-canvas-assets-1250000000" : isQiniuKodo ? "七牛云存储空间名称" : "my-canvas-assets"} />
|
||||
</Form.Item>
|
||||
<Form.Item name="pathPrefix" label="路径前缀" className="mb-3">
|
||||
<Input spellCheck={false} placeholder="infinite-canvas" />
|
||||
</Form.Item>
|
||||
<Form.Item name="accessKeyId" label={accessKeyIdLabel} className="mb-3 xl:col-span-1">
|
||||
<Input autoComplete="off" spellCheck={false} placeholder={isTencentCOS ? "腾讯云 SecretId" : "阿里云 AccessKey ID"} />
|
||||
<Input autoComplete="off" spellCheck={false} placeholder={isTencentCOS ? "腾讯云 SecretId" : isQiniuKodo ? "七牛云 AccessKey" : "阿里云 AccessKey ID"} />
|
||||
</Form.Item>
|
||||
<Form.Item name="accessKeySecret" label={hasCurrentProviderSecret ? `${accessKeySecretLabel}(留空保留)` : accessKeySecretLabel} className="mb-3 xl:col-span-2">
|
||||
<Input.Password autoComplete="new-password" spellCheck={false} placeholder={hasCurrentProviderSecret ? "留空保留已加密密钥" : isTencentCOS ? "腾讯云 SecretKey" : "阿里云 AccessKey Secret"} />
|
||||
<Input.Password autoComplete="new-password" spellCheck={false} placeholder={hasCurrentProviderSecret ? "留空保留已加密密钥" : isTencentCOS ? "腾讯云 SecretKey" : isQiniuKodo ? "七牛云 SecretKey" : "阿里云 AccessKey Secret"} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Button, Pagination } from "antd";
|
||||
import { RotateCcw } from "lucide-react";
|
||||
import { ListFilter, RotateCcw } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function WorkspacePage({ children, className, grid = false, fluid = false }: { children: ReactNode; className?: string; grid?: boolean; fluid?: boolean }) {
|
||||
export function WorkspacePage({ children, className, grid = false, fluid = false, scroll = true }: { children: ReactNode; className?: string; grid?: boolean; fluid?: boolean; scroll?: boolean }) {
|
||||
return (
|
||||
<main className={cn("app-user-content app-workspace-scroll h-full overflow-y-auto text-foreground", grid && "app-workspace-grid", className)}>
|
||||
<main className={cn("app-user-content h-full text-foreground", scroll && "app-workspace-scroll overflow-y-auto", grid && "app-workspace-grid", className)}>
|
||||
<div className={fluid ? "h-full w-full" : "w-full px-3 py-3 sm:px-4 sm:py-4 xl:px-5"}>{children}</div>
|
||||
</main>
|
||||
);
|
||||
@@ -29,11 +30,28 @@ export function PageHeader({ title, description, meta, actions }: { title: strin
|
||||
);
|
||||
}
|
||||
|
||||
export function ListToolbar({ children, trailing, active, onReset, className }: { children: ReactNode; trailing?: ReactNode; active?: boolean; onReset?: () => void; className?: string }) {
|
||||
export function ListToolbar({ children, filters, activeFilters, trailing, active, onReset, className }: { children: ReactNode; filters?: ReactNode; activeFilters?: ReactNode; trailing?: ReactNode; active?: boolean; onReset?: () => void; className?: string }) {
|
||||
const [filtersOpen, setFiltersOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (active) setFiltersOpen(true);
|
||||
}, [active]);
|
||||
|
||||
return (
|
||||
<div className={cn("mt-3 flex min-h-12 flex-col gap-2 pb-3 lg:flex-row lg:items-center lg:justify-between", className)}>
|
||||
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-2.5">{children}</div>
|
||||
<div className="flex shrink-0 flex-wrap items-center gap-2">
|
||||
<div className={cn("admin-list-toolbar mt-3 flex min-h-12 flex-col gap-2 pb-3 lg:flex-row lg:items-center lg:justify-between", className)}>
|
||||
<div className="admin-list-toolbar-main flex min-w-0 flex-1 flex-wrap items-center gap-2.5">
|
||||
{children}
|
||||
{filters ? (
|
||||
<>
|
||||
<Button type="default" className="admin-filter-toggle" aria-expanded={filtersOpen} icon={<ListFilter className="size-3.5" />} onClick={() => setFiltersOpen((open) => !open)}>
|
||||
筛选{active ? <span className="admin-filter-active-dot" aria-label="有已应用筛选" /> : null}
|
||||
</Button>
|
||||
<div className={cn("admin-list-toolbar-filters flex flex-wrap items-center gap-2", filtersOpen && "is-open")}>{filters}</div>
|
||||
</>
|
||||
) : null}
|
||||
{activeFilters ? <div className="admin-list-toolbar-chips">{activeFilters}</div> : null}
|
||||
</div>
|
||||
<div className="admin-list-toolbar-actions flex shrink-0 flex-wrap items-center gap-2">
|
||||
{active && onReset ? <Button type="text" icon={<RotateCcw className="size-3.5" />} onClick={onReset}>重置</Button> : null}
|
||||
{trailing}
|
||||
</div>
|
||||
@@ -49,19 +67,22 @@ export function CollectionGrid({ children, className }: { children: ReactNode; c
|
||||
return <div className={cn("mt-4 grid grid-cols-1 gap-4 sm:grid-cols-[repeat(auto-fill,minmax(248px,1fr))]", className)}>{children}</div>;
|
||||
}
|
||||
|
||||
export function PaginationBar({ current, pageSize, total, onChange, pageSizeOptions = [20, 50, 100] }: { current: number; pageSize: number; total: number; onChange: (page: number, pageSize: number) => void; pageSizeOptions?: number[] }) {
|
||||
if (total <= pageSize && current === 1) return null;
|
||||
export function PaginationBar({ current, pageSize, total, onChange, pageSizeOptions = [20, 50, 100], alwaysShow = false }: { current: number; pageSize: number; total: number; onChange: (page: number, pageSize: number) => void; pageSizeOptions?: number[]; alwaysShow?: boolean }) {
|
||||
if (!alwaysShow && total <= pageSize && current === 1) return null;
|
||||
const start = total === 0 ? 0 : (current - 1) * pageSize + 1;
|
||||
const end = total === 0 ? 0 : Math.min(current * pageSize, total);
|
||||
return (
|
||||
<div className="app-pagination-bar mt-4 flex min-h-10 min-w-0 items-center justify-end px-2 py-1.5">
|
||||
<div className="app-pagination-bar admin-pagination-bar min-w-0">
|
||||
<span className="admin-pagination-total">{total === 0 ? "共 0 条" : `${start}-${end} / 共 ${total} 条`}</span>
|
||||
<Pagination
|
||||
size="small"
|
||||
current={current}
|
||||
pageSize={pageSize}
|
||||
total={total}
|
||||
showSizeChanger
|
||||
showSizeChanger={pageSizeOptions.length > 1}
|
||||
responsive
|
||||
showLessItems
|
||||
pageSizeOptions={pageSizeOptions.map(String)}
|
||||
showTotal={(value, range) => `${range[0]}-${range[1]} / 共 ${value} 条`}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -37,7 +37,7 @@ export function WorkspaceSidebarFooter({ expandedClassName, collapsedClassName,
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await logout();
|
||||
await applyUserSession({ user: null, systemChannels: [] });
|
||||
await applyUserSession({ user: null, logicalModels: [] });
|
||||
setMenuOpen(false);
|
||||
message.success("已退出登录");
|
||||
navigate("/login", { replace: true });
|
||||
|
||||
Reference in new issue
Block a user