"use client"; import { useRouter } from "next/navigation"; import { useState } from "react"; import clsx from "clsx"; import type { SanitizedUserAttachmentStorageConfig } from "@/lib/boss-storage"; type StorageMode = SanitizedUserAttachmentStorageConfig["mode"]; type StorageDraft = { mode: StorageMode; ossProvider: "aliyun_oss"; accessKeyId: string; accessKeySecret: string; bucket: string; endpoint: string; region: string; prefix: string; }; function draftFromConfig(config: SanitizedUserAttachmentStorageConfig): StorageDraft { return { mode: config.mode, ossProvider: config.ossProvider ?? "aliyun_oss", accessKeyId: config.aliyunOss?.accessKeyId ?? "", accessKeySecret: "", bucket: config.aliyunOss?.bucket ?? "", endpoint: config.aliyunOss?.endpoint ?? "", region: config.aliyunOss?.region ?? "", prefix: config.aliyunOss?.prefix ?? "", }; } function Field({ label, value, onChange, placeholder, type = "text", }: { label: string; value: string; onChange: (value: string) => void; placeholder?: string; type?: "text" | "password"; }) { return ( ); } export function AttachmentStorageClient({ config, }: { config: SanitizedUserAttachmentStorageConfig; }) { const router = useRouter(); const [currentConfig, setCurrentConfig] = useState(config); const [draft, setDraft] = useState(() => draftFromConfig(config)); const [busyKey, setBusyKey] = useState<"save" | "validate" | null>(null); const [message, setMessage] = useState(""); async function submit(kind: "save" | "validate") { setBusyKey(kind); const body = draft.mode === "oss" ? { mode: "oss" as const, ossProvider: "aliyun_oss" as const, aliyunOss: { accessKeyId: draft.accessKeyId, bucket: draft.bucket, endpoint: draft.endpoint, region: draft.region, prefix: draft.prefix, ...(draft.accessKeySecret.trim() ? { accessKeySecret: draft.accessKeySecret } : {}), }, } : { mode: "server_file" as const, }; const response = await fetch( kind === "validate" ? "/api/v1/storage/config/validate" : "/api/v1/storage/config", { method: kind === "validate" ? "POST" : "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }, ); const result = (await response.json()) as { ok: boolean; message?: string; config?: SanitizedUserAttachmentStorageConfig; }; setBusyKey(null); if (result.ok) { const nextConfig = result.config ?? currentConfig; setCurrentConfig(nextConfig); setDraft(draftFromConfig(nextConfig)); setMessage("附件存储配置已保存。"); router.refresh(); return; } setMessage(result.message ?? "保存失败。"); } const modeLabel = currentConfig.mode === "server_file" ? "服务器文件存储" : "OSS"; const buttonLabel = draft.mode === "server_file" ? "切回服务器文件存储" : busyKey === "validate" ? "测试中" : "测试并保存"; return (
当前用户附件存储模式
当前模式:{modeLabel}
绑定账号:{currentConfig.account}
{currentConfig.mode === "oss" ? `OSS 提供方:阿里 OSS · 密钥${currentConfig.aliyunOss?.accessKeySecretConfigured ? "已保存" : "未保存"}` : "附件将继续写入服务器文件存储。"}
{(["server_file", "oss"] as const).map((mode) => { const active = draft.mode === mode; return ( ); })}
{draft.mode === "oss" ? (
阿里 OSS 配置
当前仅支持阿里 OSS。AccessKey Secret 不会回显,留空表示沿用已保存的密钥。
setDraft((current) => ({ ...current, accessKeyId: value }))} placeholder="请输入 AccessKey ID" /> setDraft((current) => ({ ...current, accessKeySecret: value }))} placeholder="请输入 AccessKey Secret" type="password" /> setDraft((current) => ({ ...current, bucket: value }))} placeholder="例如 boss-attachments" /> setDraft((current) => ({ ...current, endpoint: value }))} placeholder="例如 oss-cn-hangzhou.aliyuncs.com" /> setDraft((current) => ({ ...current, region: value }))} placeholder="例如 oss-cn-hangzhou" /> setDraft((current) => ({ ...current, prefix: value }))} placeholder="例如 boss/" />
) : (
服务器文件存储
切回后附件继续保存在服务器本地文件存储,不会走 OSS 配置校验。
)} {message ? (
{message}
) : null} {draft.mode === "oss" ? (
保存前请确认 Bucket、Endpoint 和 Region 都完整可用,测试通过后会同步写回当前账号配置。
) : null}
); }