diff --git a/src/app/me/page.tsx b/src/app/me/page.tsx index 9087c0b..0346815 100644 --- a/src/app/me/page.tsx +++ b/src/app/me/page.tsx @@ -21,6 +21,11 @@ export default async function MePage() {
+ + + + + + ); +} diff --git a/src/components/attachment-storage-client.tsx b/src/components/attachment-storage-client.tsx new file mode 100644 index 0000000..4de37d7 --- /dev/null +++ b/src/components/attachment-storage-client.tsx @@ -0,0 +1,241 @@ +"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 [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 }; + setBusyKey(null); + setMessage(result.ok ? "附件存储配置已保存。" : result.message ?? "保存失败。"); + if (result.ok) { + router.refresh(); + } + } + + const modeLabel = draft.mode === "server_file" ? "服务器文件存储" : "OSS"; + const buttonLabel = + draft.mode === "server_file" + ? "切回服务器文件存储" + : busyKey === "validate" + ? "测试中" + : "测试并保存"; + + return ( +
+
+
当前用户附件存储模式
+
+ 当前模式:{modeLabel} +
+ 绑定账号:{config.account} +
+ {draft.mode === "oss" + ? `OSS 提供方:阿里 OSS · 密钥${config.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} +
+ ); +}