feat(web): add me storage page
This commit is contained in:
241
src/components/attachment-storage-client.tsx
Normal file
241
src/components/attachment-storage-client.tsx
Normal file
@@ -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 (
|
||||
<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 [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 };
|
||||
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 (
|
||||
<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 />
|
||||
绑定账号:{config.account}
|
||||
<br />
|
||||
{draft.mode === "oss"
|
||||
? `OSS 提供方:阿里 OSS · 密钥${config.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]">
|
||||
当前仅支持阿里 OSS。AccessKey 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]">
|
||||
保存前请确认 Bucket、Endpoint 和 Region 都完整可用,测试通过后会同步写回当前账号配置。
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user