You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

284 lines
8.4 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import {
App,
Button,
Card,
Form,
Input,
Popconfirm,
Select,
Space,
Spin,
Tag,
Typography,
} from "antd";
import {
createImapFilterApi,
deleteImapFilterApi,
getImapPullSettingsApi,
updateImapFilterApi,
updateImapPollIntervalApi,
} from "@/lib/client-api";
import type { ImapFilterKind, ImapFilterRuleItem } from "@/types";
const INTERVAL_PRESETS: Array<{ label: string; value: number }> = [
{ label: "3 分钟", value: 3 * 60_000 },
{ label: "5 分钟", value: 5 * 60_000 },
{ label: "10 分钟", value: 10 * 60_000 },
{ label: "15 分钟", value: 15 * 60_000 },
{ label: "30 分钟(默认)", value: 30 * 60_000 },
{ label: "1 小时", value: 60 * 60_000 },
{ label: "6 小时", value: 6 * 60 * 60_000 },
{ label: "12 小时", value: 12 * 60 * 60_000 },
{ label: "24 小时", value: 24 * 60 * 60_000 },
{ label: "3 天", value: 3 * 24 * 60 * 60_000 },
{ label: "7 天", value: 7 * 24 * 60 * 60_000 },
];
const KIND_ORDER: ImapFilterKind[] = [
"keyword",
"sender_whitelist",
"sender_blacklist",
"keyword_blacklist",
];
const KIND_LABEL: Record<string, string> = {
keyword: "关键词(白)",
sender_whitelist: "发件人白名单",
sender_blacklist: "发件人黑名单",
keyword_blacklist: "关键词黑名单",
};
const KIND_COLOR: Record<string, string> = {
keyword: "blue",
sender_whitelist: "green",
sender_blacklist: "magenta",
keyword_blacklist: "orange",
};
const KIND_OPTIONS = KIND_ORDER.map((value) => ({
value,
label: KIND_LABEL[value],
}));
export function ImapPullSettingsCard() {
const { message } = App.useApp();
const [loading, setLoading] = useState(true);
const [savingInterval, setSavingInterval] = useState(false);
const [adding, setAdding] = useState(false);
const [intervalMs, setIntervalMs] = useState(30 * 60_000);
const [filters, setFilters] = useState<ImapFilterRuleItem[]>([]);
const [form] = Form.useForm<{ kind: ImapFilterKind; value: string }>();
const grouped = useMemo(() => {
const map = new Map<string, ImapFilterRuleItem[]>();
for (const kind of KIND_ORDER) map.set(kind, []);
for (const f of filters) {
const list = map.get(f.kind) || [];
list.push(f);
map.set(f.kind, list);
}
return KIND_ORDER.map((kind) => ({
kind,
label: KIND_LABEL[kind],
color: KIND_COLOR[kind] || "default",
items: map.get(kind) || [],
})).filter((g) => g.items.length > 0);
}, [filters]);
const reload = useCallback(async () => {
setLoading(true);
const res = await getImapPullSettingsApi();
setLoading(false);
if (!res.ok) {
message.error(res.error.message);
return;
}
setIntervalMs(res.data.poll_interval_ms);
setFilters(res.data.filters || []);
}, [message]);
useEffect(() => {
void reload();
}, [reload]);
const onSaveInterval = async () => {
setSavingInterval(true);
const res = await updateImapPollIntervalApi(intervalMs);
setSavingInterval(false);
if (!res.ok) {
message.error(res.error.message);
return;
}
message.success("拉取间隔已保存(worker 下一轮生效)");
setIntervalMs(res.data.poll_interval_ms);
};
const onAdd = async () => {
const values = await form.validateFields();
setAdding(true);
const res = await createImapFilterApi({
kind: values.kind,
value: values.value,
enabled: true,
});
setAdding(false);
if (!res.ok) {
message.error(res.error.message);
return;
}
message.success("已添加规则");
form.resetFields(["value"]);
void reload();
};
const onToggle = async (row: ImapFilterRuleItem) => {
const res = await updateImapFilterApi(row.id, { enabled: !row.enabled });
if (!res.ok) message.error(res.error.message);
else void reload();
};
const onDelete = async (row: ImapFilterRuleItem) => {
const res = await deleteImapFilterApi(row.id);
if (!res.ok) message.error(res.error.message);
else {
message.success("已删除");
void reload();
}
};
return (
<Card title="自动拉取">
<Spin spinning={loading}>
<div>
<Space wrap align="center" size={8} style={{ marginBottom: 12 }}>
<Typography.Text>拉取间隔</Typography.Text>
<Select
style={{ width: 180 }}
value={intervalMs}
options={INTERVAL_PRESETS}
onChange={(v) => setIntervalMs(v)}
/>
<Button
type="primary"
size="small"
loading={savingInterval}
onClick={() => void onSaveInterval()}
>
保存间隔
</Button>
</Space>
<Typography.Text strong style={{ display: "block", marginBottom: 8 }}>
过滤规则
</Typography.Text>
<Form
form={form}
layout="inline"
style={{ marginBottom: 10 }}
initialValues={{ kind: "keyword" }}
>
<Form.Item
name="kind"
rules={[{ required: true, message: "选择类型" }]}
style={{ marginBottom: 8 }}
>
<Select style={{ width: 140 }} options={KIND_OPTIONS} />
</Form.Item>
<Form.Item
name="value"
rules={[{ required: true, message: "填写内容" }]}
style={{ marginBottom: 8 }}
>
<Input
placeholder="关键词或发件人邮箱"
style={{ width: 220 }}
allowClear
/>
</Form.Item>
<Form.Item style={{ marginBottom: 8 }}>
<Button
type="primary"
size="small"
loading={adding}
onClick={() => void onAdd()}
>
添加
</Button>
</Form.Item>
</Form>
{filters.length === 0 ? (
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
暂无规则(将使用业务相关兜底)
</Typography.Text>
) : (
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
{grouped.map((g) => (
<div
key={g.kind}
style={{
display: "flex",
flexWrap: "wrap",
alignItems: "center",
gap: 6,
}}
>
<Typography.Text
type="secondary"
style={{
fontSize: 12,
width: 96,
flexShrink: 0,
lineHeight: "22px",
}}
>
{g.label}
</Typography.Text>
{g.items.map((f) => (
<Tag
key={f.id}
color={f.enabled ? g.color : undefined}
closable
closeIcon={
<Popconfirm
title="删除该规则?"
onConfirm={() => void onDelete(f)}
okText="删除"
cancelText="取消"
>
<span
role="button"
aria-label="删除"
onClick={(e) => e.stopPropagation()}
>
×
</span>
</Popconfirm>
}
onClose={(e) => e.preventDefault()}
onClick={() => void onToggle(f)}
style={{
cursor: "pointer",
marginInlineEnd: 0,
opacity: f.enabled ? 1 : 0.45,
textDecoration: f.enabled ? "none" : "line-through",
userSelect: "none",
}}
title={f.enabled ? "点击停用" : "点击启用"}
>
{f.value}
</Tag>
))}
</div>
))}
</div>
)}
</div>
</Spin>
</Card>
);
}