@@ -0,0 +1,239 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
Tabs,
|
||||
TimePicker,
|
||||
Typography,
|
||||
message,
|
||||
} from 'antd';
|
||||
import dayjs, { type Dayjs } from 'dayjs';
|
||||
import {
|
||||
WECOM_REPORT_KIND_LABELS,
|
||||
WECOM_REPORT_KINDS,
|
||||
WECOM_REPORT_WEEKDAY_OPTIONS,
|
||||
type WecomReportKind,
|
||||
type WecomReportPreviewDto,
|
||||
type WecomReportPushDto,
|
||||
} from '@dukang/shared-types';
|
||||
import { request } from '../lib/api';
|
||||
import { fmtTime } from '../lib/constants';
|
||||
|
||||
type FormValues = {
|
||||
name: string;
|
||||
webhookUrl: string;
|
||||
enabled: boolean;
|
||||
mentionWecomUserId?: string;
|
||||
sendTime: Dayjs;
|
||||
sendWeekday: number;
|
||||
sendMonthDay: number;
|
||||
};
|
||||
|
||||
function ReportKindPane({
|
||||
kind,
|
||||
row,
|
||||
onSaved,
|
||||
}: {
|
||||
kind: WecomReportKind;
|
||||
row: WecomReportPushDto | undefined;
|
||||
onSaved: () => void;
|
||||
}) {
|
||||
const [form] = Form.useForm<FormValues>();
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [previewing, setPreviewing] = useState(false);
|
||||
const [sending, setSending] = useState(false);
|
||||
const [preview, setPreview] = useState<WecomReportPreviewDto | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!row) return;
|
||||
form.setFieldsValue({
|
||||
name: row.name,
|
||||
webhookUrl: row.webhookUrl,
|
||||
enabled: row.enabled,
|
||||
mentionWecomUserId: row.mentionWecomUserId ?? undefined,
|
||||
sendTime: dayjs().hour(row.sendHour).minute(row.sendMinute).second(0),
|
||||
sendWeekday: row.sendWeekday,
|
||||
sendMonthDay: row.sendMonthDay,
|
||||
});
|
||||
}, [form, row]);
|
||||
|
||||
async function save() {
|
||||
const values = await form.validateFields();
|
||||
setSaving(true);
|
||||
try {
|
||||
await request(`/admin/wecom-reports/${kind}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
name: values.name,
|
||||
webhookUrl: values.webhookUrl,
|
||||
enabled: values.enabled,
|
||||
mentionWecomUserId: values.mentionWecomUserId?.trim() || null,
|
||||
sendHour: values.sendTime.hour(),
|
||||
sendMinute: values.sendTime.minute(),
|
||||
sendWeekday: values.sendWeekday,
|
||||
sendMonthDay: values.sendMonthDay,
|
||||
}),
|
||||
});
|
||||
message.success('已保存');
|
||||
onSaved();
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '保存失败');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function openPreview() {
|
||||
setPreviewing(true);
|
||||
try {
|
||||
const data = await request<WecomReportPreviewDto>(`/admin/wecom-reports/${kind}/preview`, {
|
||||
method: 'POST',
|
||||
body: '{}',
|
||||
});
|
||||
setPreview(data);
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '预览失败');
|
||||
} finally {
|
||||
setPreviewing(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendNow() {
|
||||
setSending(true);
|
||||
try {
|
||||
const res = await request<{ message: string }>(`/admin/wecom-reports/${kind}/send`, {
|
||||
method: 'POST',
|
||||
body: '{}',
|
||||
});
|
||||
message.success(res.message || '已发送');
|
||||
onSaved();
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '发送失败');
|
||||
} finally {
|
||||
setSending(false);
|
||||
}
|
||||
}
|
||||
|
||||
const label = WECOM_REPORT_KIND_LABELS[kind];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form form={form} layout="vertical" style={{ maxWidth: 640 }}>
|
||||
<Form.Item name="enabled" label="启用定时推送" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item name="name" label="名称" rules={[{ required: true, message: '请填写名称' }]}>
|
||||
<Input maxLength={64} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="webhookUrl"
|
||||
label="群机器人 Webhook"
|
||||
extra="企业微信群「群机器人」Webhook,与「消息推送」分开配置。"
|
||||
rules={[{ required: true, message: '请填写 Webhook' }]}
|
||||
>
|
||||
<Input.TextArea rows={2} />
|
||||
</Form.Item>
|
||||
<Form.Item name="mentionWecomUserId" label="@成员 userid">
|
||||
<Input placeholder="可选,企业微信成员账号" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="sendTime"
|
||||
label="发送时刻"
|
||||
extra={
|
||||
kind === 'daily'
|
||||
? '按北京时间当天该时刻发送当日数据。'
|
||||
: kind === 'weekly'
|
||||
? '按北京时间该星期该时刻发送上一自然周。'
|
||||
: '按北京时间每月该日该时刻发送上一自然月。'
|
||||
}
|
||||
rules={[{ required: true, message: '请选择时刻' }]}
|
||||
>
|
||||
<TimePicker format="HH:mm" minuteStep={1} allowClear={false} />
|
||||
</Form.Item>
|
||||
{kind === 'weekly' ? (
|
||||
<Form.Item name="sendWeekday" label="发送星期" rules={[{ required: true }]}>
|
||||
<Select options={WECOM_REPORT_WEEKDAY_OPTIONS} />
|
||||
</Form.Item>
|
||||
) : null}
|
||||
{kind === 'monthly' ? (
|
||||
<Form.Item name="sendMonthDay" label="每月几号" rules={[{ required: true }]}>
|
||||
<InputNumber min={1} max={31} />
|
||||
</Form.Item>
|
||||
) : null}
|
||||
<Typography.Paragraph type="secondary">
|
||||
{row?.lastSentAt
|
||||
? `上次发送:${fmtTime(row.lastSentAt)}(${row.lastSentPeriod ?? '—'})`
|
||||
: '尚未发送'}
|
||||
</Typography.Paragraph>
|
||||
<Space wrap>
|
||||
<Button type="primary" loading={saving} onClick={() => void save()}>
|
||||
保存
|
||||
</Button>
|
||||
<Button loading={previewing} onClick={() => void openPreview()}>
|
||||
预览
|
||||
</Button>
|
||||
<Button loading={sending} onClick={() => void sendNow()}>
|
||||
立即发送
|
||||
</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
<Modal
|
||||
title={`预览${label}`}
|
||||
open={!!preview}
|
||||
onCancel={() => setPreview(null)}
|
||||
footer={<Button onClick={() => setPreview(null)}>关闭</Button>}
|
||||
width={560}
|
||||
>
|
||||
<Typography.Paragraph type="secondary">
|
||||
区间 {preview?.rangeLabel}
|
||||
</Typography.Paragraph>
|
||||
<pre style={{ whiteSpace: 'pre-wrap', margin: 0 }}>{preview?.markdown}</pre>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function WecomReportsPage() {
|
||||
const [rows, setRows] = useState<WecomReportPushDto[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const load = useCallback(() => {
|
||||
setLoading(true);
|
||||
return request<WecomReportPushDto[]>('/admin/wecom-reports')
|
||||
.then(setRows)
|
||||
.catch((e) => message.error(e instanceof Error ? e.message : '加载失败'))
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [load]);
|
||||
|
||||
const byKind = new Map(rows.map((r) => [r.kind, r]));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Typography.Title level={4}>企微机器人 · 报告</Typography.Title>
|
||||
<Typography.Paragraph type="secondary">
|
||||
日报 / 周报 / 月报走企微群机器人 Webhook,配置与「消息推送」相互独立。口径与概览一致:数量为期末存量,新增为区间内发生额;订单金额按已付
|
||||
payAmount,核销按核销单。
|
||||
</Typography.Paragraph>
|
||||
<Card loading={loading}>
|
||||
<Tabs
|
||||
items={WECOM_REPORT_KINDS.map((kind) => ({
|
||||
key: kind,
|
||||
label: WECOM_REPORT_KIND_LABELS[kind],
|
||||
children: <ReportKindPane kind={kind} row={byKind.get(kind)} onSaved={() => void load()} />,
|
||||
}))}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user