import { App, Button, Form, Input, Segmented, Select, Space, Switch, Tag } from "antd"; import { Cloud, Globe, HardDrive, LocateFixed, Wifi } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { changesRequireOSSRetest, DEFAULT_OSS_PATH_PREFIX, getS3PresetHints, S3_PRESET_OPTIONS, type OSSConnectionTestResult, type S3Preset } from "@/lib/oss-settings"; import { getAdminOSSSetting, testAdminOSSConnection, updateAdminOSSSetting, type AdminOSSSetting } from "@/services/api/auth"; import { useAdminContext } from "../admin-context"; import { AdminPageFrame } from "../components/admin-shell"; import { AdminStatusBadge, configuredSecretText, SettingsSectionCard } from "../components/admin-ui"; type StorageMode = "local" | AdminOSSSetting["provider"]; type OSSFormValues = { mode: StorageMode; s3Preset?: S3Preset; publicBaseUrl?: string; region?: string; endpoint?: string; cdnBaseUrl?: string; bucket?: string; accessKeyId?: string; accessKeySecret?: string; sessionToken?: string; pathPrefix?: string; pathStyle?: boolean; allowUserS3?: boolean; }; export default function StorageSettingsPage() { const { message } = App.useApp(); const { references } = useAdminContext(); const [setting, setSetting] = useState(null); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [testing, setTesting] = useState(false); const [testResult, setTestResult] = useState(null); const [testStale, setTestStale] = useState(false); const [form] = Form.useForm(); const mode = Form.useWatch("mode", form) || "local"; const isObjectStorage = mode !== "local"; const isTencentCOS = mode === "tencent"; const isQiniuKodo = mode === "qiniu"; const isS3 = mode === "s3"; const s3Preset = Form.useWatch("s3Preset", form) || "custom"; const accessKeyIdLabel = isTencentCOS ? "SecretId" : isQiniuKodo ? "AccessKey" : "AccessKey ID"; const accessKeySecretLabel = isTencentCOS ? "SecretKey" : isQiniuKodo ? "SecretKey" : "AccessKey Secret"; const hasCurrentProviderSecret = Boolean(setting && setting.provider === mode && setting.hasAccessKeySecret); const userNameById = useMemo(() => new Map(references.users.map((user) => [user.id, user.displayName || user.username])), [references.users]); useEffect(() => { void getAdminOSSSetting() .then(({ setting: value }) => { setSetting(value); form.setFieldsValue(formValues(value)); setTestResult(value.testedAt ? { ok: true, testedAt: value.testedAt, testedDigest: value.testedDigest } : null); }) .catch((error) => message.error(error instanceof Error ? error.message : "读取对象存储配置失败")) .finally(() => setLoading(false)); }, [form, message]); const save = async () => { await form.validateFields(); const values = form.getFieldsValue(true); if (values.mode === "local" && !values.publicBaseUrl?.trim()) return message.error("请填写服务器访问地址"); if (values.mode !== "local" && !values.accessKeySecret?.trim() && !hasCurrentProviderSecret) return message.error(`请填写 ${accessKeySecretLabel}`); if (values.mode !== "local" && !values.bucket?.trim()) return message.error("请填写对象存储 Bucket"); if (values.mode !== "local" && !values.accessKeyId?.trim()) return message.error(`请填写 ${accessKeyIdLabel}`); if (values.mode === "aliyun" && !values.endpoint?.trim()) return message.error("请填写阿里云 OSS Endpoint"); if (values.mode === "tencent" && !values.endpoint?.trim() && !values.region?.trim()) return message.error("请填写腾讯云 COS Region 或 Endpoint"); if (values.mode === "qiniu" && !values.endpoint?.trim()) return message.error("请填写七牛云 Kodo 上传 Endpoint"); if (values.mode === "s3" && !values.region?.trim()) return message.error("请填写 S3 Region"); if (values.mode === "s3" && !values.endpoint?.trim()) return message.error("请填写 S3 Endpoint 服务根 URL"); setSaving(true); try { const result = await updateAdminOSSSetting({ enabled: values.mode !== "local", provider: values.mode === "local" ? setting?.provider || "aliyun" : values.mode, s3Preset: values.s3Preset || "custom", region: values.region?.trim() || "", endpoint: values.endpoint?.trim() || "", cdnBaseUrl: values.cdnBaseUrl?.trim() || "", bucket: values.bucket?.trim() || "", accessKeyId: values.accessKeyId?.trim() || "", accessKeySecret: values.accessKeySecret?.trim() || "", sessionToken: values.sessionToken?.trim() || "", pathStyle: values.pathStyle === true, allowUserS3: values.allowUserS3 === true, publicBaseUrl: values.publicBaseUrl?.trim() || "", pathPrefix: values.pathPrefix?.trim() || DEFAULT_OSS_PATH_PREFIX, }); setSetting(result.setting); form.setFieldsValue(formValues(result.setting)); setTestResult(result.setting.testedAt ? { ok: true, testedAt: result.setting.testedAt, testedDigest: result.setting.testedDigest } : null); setTestStale(false); message.success("存储配置已保存"); } catch (error) { message.error(error instanceof Error ? error.message : "保存存储配置失败"); } finally { setSaving(false); } }; const testConnection = async () => { const values = await form.validateFields(); if (values.mode === "local") return; setTesting(true); try { const result = await testAdminOSSConnection(connectionInput(values)); setTestResult(result); setTestStale(false); result.ok ? message.success(result.message || "连接测试通过") : message.error(result.message || "连接测试失败"); } catch (error) { setTestResult({ ok: false, message: error instanceof Error ? error.message : "连接测试失败" }); setTestStale(false); } finally { setTesting(false); } }; return (
} title="平台存储" status={ {setting?.enabled ? : null} } footer={ <>
{setting?.updatedAt ? `上次更新:${formatTime(setting.updatedAt)}${setting.updatedBy ? ` · ${userNameById.get(setting.updatedBy) || setting.updatedBy}` : ""}` : "尚未保存平台存储配置"}
} >
changesRequireOSSRetest(changed) && setTestStale(true)}> block options={[ { label: 服务器本地, value: "local" }, { label: 阿里云 OSS, value: "aliyun" }, { label: 腾讯云 COS, value: "tencent" }, { label: 七牛云 Kodo, value: "qiniu" }, { label: S3 兼容存储, value: "s3" }, ]} onChange={(value) => { const nextMode = value as StorageMode; const switchingProvider = nextMode !== "local" && ((mode !== "local" && mode !== nextMode) || (mode === "local" && setting?.provider !== nextMode)); if (switchingProvider) form.setFieldsValue({ s3Preset: "custom", region: "", endpoint: "", cdnBaseUrl: "", bucket: "", accessKeyId: "", accessKeySecret: "", sessionToken: "", pathStyle: false }); }} /> {isObjectStorage ? (
{isS3 ? (
{isS3 ? (
) : null}
) : ( } /> )}
); } function formValues(setting?: AdminOSSSetting | null): OSSFormValues { return { mode: setting?.enabled ? setting.provider : "local", s3Preset: setting?.s3Preset || "custom", publicBaseUrl: setting?.publicBaseUrl || "", region: setting?.region || "", endpoint: setting?.endpoint || "", cdnBaseUrl: setting?.cdnBaseUrl || "", bucket: setting?.bucket || "", accessKeyId: setting?.accessKeyId || "", accessKeySecret: "", sessionToken: "", pathPrefix: setting?.pathPrefix || DEFAULT_OSS_PATH_PREFIX, pathStyle: setting?.pathStyle === true, allowUserS3: setting?.allowUserS3 === true, }; } function storageProviderLabel(provider?: AdminOSSSetting["provider"] | StorageMode) { return provider === "s3" ? "S3 兼容存储" : provider === "tencent" ? "腾讯云 COS" : provider === "qiniu" ? "七牛云 Kodo" : provider === "aliyun" ? "阿里云 OSS" : "服务器本地"; } function formatTime(value?: string) { return value ? new Date(value).toLocaleString("zh-CN", { hour12: false }) : "--"; } function connectionInput(values: OSSFormValues) { return { provider: values.mode === "local" ? "aliyun" as const : values.mode, s3Preset: values.s3Preset, region: values.region?.trim() || "", endpoint: values.endpoint?.trim() || "", cdnBaseUrl: values.cdnBaseUrl?.trim() || "", bucket: values.bucket?.trim() || "", accessKeyId: values.accessKeyId?.trim() || "", accessKeySecret: values.accessKeySecret?.trim() || "", sessionToken: values.sessionToken?.trim() || "", pathPrefix: values.pathPrefix?.trim() || DEFAULT_OSS_PATH_PREFIX, pathStyle: values.pathStyle === true, }; } function ConnectionTestStatus({ result, stale }: { result: OSSConnectionTestResult | null; stale: boolean }) { if (stale) return 关键配置已变更,需重新测试; if (!result) return 尚未测试连接; return {result.ok ? `测试通过${result.testedAt ? ` · ${formatTime(result.testedAt)}` : ""}` : result.message || "测试失败"}; }