|
|
"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>
|
|
|
);
|
|
|
}
|