diff --git a/web/src/pages/admin/settings/storage-settings-page.tsx b/web/src/pages/admin/settings/storage-settings-page.tsx index 3c0cb29d4..85b08fe08 100644 --- a/web/src/pages/admin/settings/storage-settings-page.tsx +++ b/web/src/pages/admin/settings/storage-settings-page.tsx @@ -1,5 +1,5 @@ -import { App, Button, Form, Input, Select, Space, Switch, Tag } from "antd"; -import { Cloud, Info, KeyRound, ShieldCheck } from "lucide-react"; +import { App, Button, Form, Input, Segmented, Space, Tag } from "antd"; +import { Cloud, HardDrive, Info, KeyRound, ShieldCheck } from "lucide-react"; import { useEffect, useMemo, useState, type ReactNode } from "react"; import { getAdminOSSSetting, updateAdminOSSSetting, type AdminOSSSetting } from "@/services/api/auth"; @@ -7,7 +7,8 @@ import { useAdminContext } from "../admin-context"; import { AdminPageFrame } from "../components/admin-shell"; import { configuredSecretText, SettingsSectionCard } from "../components/admin-ui"; -type OSSFormValues = { enabled?: boolean; provider: "aliyun"; region?: string; endpoint?: string; bucket?: string; accessKeyId?: string; accessKeySecret?: string; pathPrefix?: string }; +type StorageMode = "local" | "oss"; +type OSSFormValues = { mode: StorageMode; region?: string; endpoint?: string; bucket?: string; accessKeyId?: string; accessKeySecret?: string; pathPrefix?: string }; export default function StorageSettingsPage() { const { message } = App.useApp(); @@ -16,6 +17,7 @@ export default function StorageSettingsPage() { const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [form] = Form.useForm(); + const mode = Form.useWatch("mode", form) || "local"; const userNameById = useMemo(() => new Map(references.users.map((user) => [user.id, user.displayName || user.username])), [references.users]); useEffect(() => { @@ -26,56 +28,73 @@ export default function StorageSettingsPage() { }, [form, message]); const save = async () => { - const values = await form.validateFields(); - if (values.enabled && !values.accessKeySecret?.trim() && !setting?.hasAccessKeySecret) return message.error("请填写 AccessKey Secret"); - if (values.enabled && !values.endpoint?.trim()) return message.error("请填写 OSS Endpoint"); - if (values.enabled && !values.bucket?.trim()) return message.error("请填写 OSS Bucket"); - if (values.enabled && !values.accessKeyId?.trim()) return message.error("请填写 AccessKey ID"); + await form.validateFields(); + // 本地模式会卸载 OSS 字段,读取完整 store 才能保留已保存的 OSS 配置。 + const values = form.getFieldsValue(true); + if (values.mode === "oss" && !values.accessKeySecret?.trim() && !setting?.hasAccessKeySecret) return message.error("请填写 AccessKey Secret"); + if (values.mode === "oss" && !values.endpoint?.trim()) return message.error("请填写 OSS Endpoint"); + if (values.mode === "oss" && !values.bucket?.trim()) return message.error("请填写 OSS Bucket"); + if (values.mode === "oss" && !values.accessKeyId?.trim()) return message.error("请填写 AccessKey ID"); setSaving(true); try { - const result = await updateAdminOSSSetting({ enabled: values.enabled === true, provider: values.provider, region: values.region?.trim() || "", endpoint: values.endpoint?.trim() || "", bucket: values.bucket?.trim() || "", accessKeyId: values.accessKeyId?.trim() || "", accessKeySecret: values.accessKeySecret?.trim() || "", publicBaseUrl: "", pathPrefix: values.pathPrefix?.trim() || "" }); + const result = await updateAdminOSSSetting({ enabled: values.mode === "oss", provider: "aliyun", region: values.region?.trim() || "", endpoint: values.endpoint?.trim() || "", bucket: values.bucket?.trim() || "", accessKeyId: values.accessKeyId?.trim() || "", accessKeySecret: values.accessKeySecret?.trim() || "", publicBaseUrl: "", pathPrefix: values.pathPrefix?.trim() || "" }); setSetting(result.setting); form.setFieldsValue(formValues(result.setting)); - message.success("OSS 配置已保存"); + message.success("存储配置已保存"); } catch (error) { - message.error(error instanceof Error ? error.message : "保存 OSS 配置失败"); + message.error(error instanceof Error ? error.message : "保存存储配置失败"); } finally { setSaving(false); } }; return ( - +
-
资源存储规则

启用后,新上传和生成的媒体由后端写入 OSS;未启用时写入后端数据卷。资源统一通过登录鉴权接口读取,不直接暴露 OSS 对象地址。

+
资源存储规则

存储类型只影响之后新增的媒体资源,不迁移或改写历史文件。服务器本地与 OSS 资源均通过登录鉴权接口读取。

} - title="平台 OSS" - description="配置平台媒体资源的默认存储位置。" - status={{setting?.enabled ? "已启用" : "未启用"}{setting?.hasAccessKeySecret ? configuredSecretText : "未保存密钥"}} - footer={<>
{setting?.updatedAt ? `上次更新:${formatTime(setting.updatedAt)}${setting.updatedBy ? ` · ${userNameById.get(setting.updatedBy) || setting.updatedBy}` : ""}` : "尚未保存 OSS 配置"}
} + title="平台存储" + description="选择平台新增媒体资源的默认写入方式。" + status={{setting?.enabled ? "阿里云 OSS" : "服务器本地"}{setting?.enabled ? {setting.hasAccessKeySecret ? configuredSecretText : "未保存密钥"} : null}} + footer={<>
{setting?.updatedAt ? `上次更新:${formatTime(setting.updatedAt)}${setting.updatedBy ? ` · ${userNameById.get(setting.updatedBy) || setting.updatedBy}` : ""}` : "尚未保存平台存储配置"}
} >
- - - - - - - + + + block + options={[ + { label: 服务器本地, value: "local" }, + { label: 阿里云 OSS, value: "oss" }, + ]} + /> + + {mode === "oss" ? ( + <> + + + + + + + + ) : ( +
+ 新增资源将写入后端数据目录的 resources/,部署时需持久化该数据卷。 +
+ )}
-
} text="新资源优先上传 OSS" />} text="AccessKey Secret 不回显" />} text="异常时自动本地降级" />
+
: } text={mode === "oss" ? "新资源写入阿里云 OSS" : "新资源写入服务器数据卷"} />} text="历史资源位置保持不变" />} text="资源读取继续校验登录态" />
); } -function formValues(setting?: AdminOSSSetting | null): OSSFormValues { return { enabled: setting?.enabled || false, provider: setting?.provider || "aliyun", region: setting?.region || "", endpoint: setting?.endpoint || "", bucket: setting?.bucket || "", accessKeyId: setting?.accessKeyId || "", accessKeySecret: "", pathPrefix: setting?.pathPrefix || "" }; } +function formValues(setting?: AdminOSSSetting | null): OSSFormValues { return { mode: setting?.enabled ? "oss" : "local", region: setting?.region || "", endpoint: setting?.endpoint || "", bucket: setting?.bucket || "", accessKeyId: setting?.accessKeyId || "", accessKeySecret: "", pathPrefix: setting?.pathPrefix || "" }; } function formatTime(value?: string) { return value ? new Date(value).toLocaleString("zh-CN", { hour12: false }) : "--"; } function Notice({ icon, text }: { icon: ReactNode; text: string }) { return
{icon}{text}
; }