Files
boss/src/components/attachment-storage-client.tsx

252 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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 (
<label className="space-y-1">
<div className="text-[12px] text-[#8C8C8C]">{label}</div>
<input
type={type}
value={value}
onChange={(event) => onChange(event.target.value)}
placeholder={placeholder}
className="w-full rounded-xl border border-[#E5E5EA] bg-[#F7F8FA] px-3 py-2 text-[14px] text-[#111111] outline-none"
/>
</label>
);
}
export function AttachmentStorageClient({
config,
}: {
config: SanitizedUserAttachmentStorageConfig;
}) {
const router = useRouter();
const [currentConfig, setCurrentConfig] = useState<SanitizedUserAttachmentStorageConfig>(config);
const [draft, setDraft] = useState<StorageDraft>(() => 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 (
<div className="space-y-3 px-[18px] pb-6">
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="text-[16px] font-semibold text-[#111111]"></div>
<div className="mt-2 text-[13px] leading-6 text-[#57606A]">
<span className="font-semibold text-[#111111]">{modeLabel}</span>
<br />
{currentConfig.account}
<br />
{currentConfig.mode === "oss"
? `OSS 提供方:阿里 OSS · 密钥${currentConfig.aliyunOss?.accessKeySecretConfigured ? "已保存" : "未保存"}`
: "附件将继续写入服务器文件存储。"}
</div>
<div className="mt-3 flex gap-2">
{(["server_file", "oss"] as const).map((mode) => {
const active = draft.mode === mode;
return (
<button
key={mode}
type="button"
onClick={() => setDraft((current) => ({ ...current, mode }))}
className={clsx(
"rounded-full px-3 py-2 text-[12px] font-semibold",
active ? "bg-[#07C160] text-white" : "bg-[#F5F5F7] text-[#57606A]",
)}
>
{mode === "server_file" ? "服务器文件存储" : "OSS"}
</button>
);
})}
</div>
</div>
{draft.mode === "oss" ? (
<div className="space-y-3 rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="text-[16px] font-semibold text-[#111111]"> OSS </div>
<div className="rounded-2xl bg-[#F7F8FA] px-4 py-3 text-[12px] leading-6 text-[#57606A]">
OSSAccessKey Secret 沿
</div>
<Field
label="AccessKey ID"
value={draft.accessKeyId}
onChange={(value) => setDraft((current) => ({ ...current, accessKeyId: value }))}
placeholder="请输入 AccessKey ID"
/>
<Field
label="AccessKey Secret"
value={draft.accessKeySecret}
onChange={(value) => setDraft((current) => ({ ...current, accessKeySecret: value }))}
placeholder="请输入 AccessKey Secret"
type="password"
/>
<Field
label="Bucket"
value={draft.bucket}
onChange={(value) => setDraft((current) => ({ ...current, bucket: value }))}
placeholder="例如 boss-attachments"
/>
<Field
label="Endpoint"
value={draft.endpoint}
onChange={(value) => setDraft((current) => ({ ...current, endpoint: value }))}
placeholder="例如 oss-cn-hangzhou.aliyuncs.com"
/>
<Field
label="Region"
value={draft.region}
onChange={(value) => setDraft((current) => ({ ...current, region: value }))}
placeholder="例如 oss-cn-hangzhou"
/>
<Field
label="Prefix可选"
value={draft.prefix}
onChange={(value) => setDraft((current) => ({ ...current, prefix: value }))}
placeholder="例如 boss/"
/>
<div className="grid grid-cols-2 gap-3">
<button
type="button"
onClick={() => void submit("validate")}
disabled={busyKey !== null}
className="rounded-full bg-[#07C160] px-4 py-2 text-[13px] font-semibold text-white disabled:opacity-60"
>
{buttonLabel}
</button>
<button
type="button"
onClick={() => void submit("save")}
disabled={busyKey !== null}
className="rounded-full border border-[#E5E5EA] bg-white px-4 py-2 text-[13px] font-semibold text-[#111111] disabled:opacity-60"
>
{busyKey === "save" ? "保存中" : "仅保存"}
</button>
</div>
</div>
) : (
<div className="space-y-3 rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="text-[16px] font-semibold text-[#111111]"></div>
<div className="rounded-2xl bg-[#F7F8FA] px-4 py-3 text-[12px] leading-6 text-[#57606A]">
OSS
</div>
<button
type="button"
onClick={() => void submit("save")}
disabled={busyKey !== null}
className="rounded-full bg-[#07C160] px-4 py-2 text-[13px] font-semibold text-white disabled:opacity-60"
>
{busyKey === "save" ? "保存中" : buttonLabel}
</button>
</div>
)}
{message ? (
<div className="rounded-2xl bg-[#F7F8FA] px-4 py-3 text-[12px] leading-6 text-[#57606A]">
{message}
</div>
) : null}
{draft.mode === "oss" ? (
<div className="rounded-2xl bg-[#F7F8FA] px-4 py-3 text-[12px] leading-6 text-[#57606A]">
BucketEndpoint Region
</div>
) : null}
</div>
);
}