feat: v2.3.0 设置内配置 Cloudflare OAuth 客户端 + 全列表批量删除

- 设置新增「Cloudflare 接入」tab:Client ID / Client Secret(Secret AES-GCM 加密入库,
  只回显末 4 位),并展示可复制的授权回调地址;settings 优先、env 变量兜底,
  保存后立即生效无需重新部署;DELETE /api/settings/cf-oauth 清除
- 四个资源列表(账号/域名/邮箱/收件箱)加复选框批量选择与批量删除
- 新增 POST /api/{accounts,domains,mailboxes,emails}/batch-delete:
  账号软删+撤销凭据;域名/邮箱级联删除其下邮件;邮件连 R2 原文与附件一并清理
- 抽出 deleteEmailById / deleteEmailsByMailbox / revokeAccount 供单条与批量共用
- 点击复选框不再触发行点击(避免误开详情抽屉)
- 新增 tests/batch.test.ts(6 例)与 settings OAuth 凭据用例(2 例);ui-smoke 扩到 17 步
This commit is contained in:
bigcode
2026-09-30 15:53:18 +08:00
parent e2c3c353cf
commit 3be6e483b4
19 changed files with 1001 additions and 79 deletions
+33 -1
View File
@@ -15,6 +15,7 @@ import {
} from "antd";
import { DeleteOutlined, PlusOutlined, ReloadOutlined, SearchOutlined } from "@ant-design/icons";
import {
batchDeleteAccounts,
deleteAccount,
getAccount,
listAccounts,
@@ -24,6 +25,7 @@ import {
type AccountDetail,
type AccountRow,
} from "../api.ts";
import { BatchActions, useRowSelection } from "../components/BatchActions.tsx";
/** # 成员变量: 账号状态 -> Tag 颜色 **/
const STATUS_COLOR: Record<string, string> = {
@@ -52,6 +54,8 @@ export function AccountsPage() {
const [q, setQ] = useState("");
const [detail, setDetail] = useState<AccountDetail | null>(null);
const [detailOpen, setDetailOpen] = useState(false);
const [deleting, setDeleting] = useState(false);
const { rowSelection, ids: selectedIds, count, clear } = useRowSelection();
const load = useCallback(async () => {
setLoading(true);
@@ -120,6 +124,26 @@ export function AccountsPage() {
</span>
</div>
<Space>
<BatchActions
count={count}
onClear={clear}
loading={deleting}
title={`删除选中的 ${count} 个账号?`}
description="账号的 Worker 凭据会被撤销(已部署的 Email Worker 随后无法再向 Console 投递);域名与邮箱数据保留。"
onDelete={async () => {
setDeleting(true);
try {
const res = await batchDeleteAccounts(selectedIds);
message.success(`已删除 ${res.deleted} 个账号`);
clear();
void load();
} catch (err) {
message.error(err instanceof Error ? err.message : "批量删除失败");
} finally {
setDeleting(false);
}
}}
/>
<Button icon={<ReloadOutlined />} onClick={() => void load()} />
<Button type="primary" icon={<PlusOutlined />} onClick={() => void startOAuth()}>
添加账号
@@ -132,8 +156,16 @@ export function AccountsPage() {
rowKey="id"
loading={loading}
dataSource={filtered}
rowSelection={rowSelection}
pagination={false}
onRow={(row) => ({ onClick: () => void openDetail(row.id), style: { cursor: "pointer" } })}
onRow={(row) => ({
onClick: (e) => {
// 点击复选框不打开详情抽屉
if ((e.target as HTMLElement).closest(".ant-table-selection-cell")) return;
void openDetail(row.id);
},
style: { cursor: "pointer" },
})}
columns={[
{ title: "名称", dataIndex: "name" },
{