Files
dukang/apps/admin-web/src/pages/WecomReportsPage.tsx
T
jacy 68a4b7f984
CI / verify (pull_request) Waiting to run
v4.0.14概览和日报
2026-09-02 21:11:58 +08:00

240 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}