import { useCallback, useEffect, useState } from 'react'; import { Avatar, Button, Checkbox, Descriptions, Form, Input, InputNumber, Modal, Popconfirm, Select, Space, Switch, Table, Tabs, Tag, Typography, message, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import { WECOM_PUSH_CONDITION_GROUPS, WECOM_PUSH_CONDITION_LABELS, WECOM_TEMPLATE_EVENT_KEYS, WECOM_TEMPLATE_EVENT_LABELS, WECOM_TEMPLATE_PLACEHOLDERS, type WecomMessagePushDto, type WecomPushCondition, type WecomPushTemplateDto, type WecomTemplateEventKey, } from '@dukang/shared-types'; import { request } from '../lib/api'; import { fmtTime } from '../lib/constants'; import { useAdminList } from '../lib/useAdminList'; import OssUpload from '../components/OssUpload'; type FormValues = { name: string; avatarUrl?: string; webhookUrl: string; enabled: boolean; mentionWecomUserId?: string; pushConditions: WecomPushCondition[]; sortOrder: number; }; function WecomPushConditionPicker({ value, onChange, }: { value?: WecomPushCondition[]; onChange?: (next: WecomPushCondition[]) => void; }) { const selected = new Set(value ?? []); function toggle(cond: WecomPushCondition, checked: boolean) { const next = new Set(selected); if (checked) next.add(cond); else next.delete(cond); onChange?.([...next]); } return (
{WECOM_PUSH_CONDITION_GROUPS.map((group) => (
{group.label}
{group.conditions.map((c) => (
toggle(c, e.target.checked)} > {WECOM_PUSH_CONDITION_LABELS[c]}
))}
))}
); } function PushRoutesTab() { const [filterForm] = Form.useForm(); const [form] = Form.useForm(); const [filters, setFilters] = useState>({}); const [modalOpen, setModalOpen] = useState(false); const [editing, setEditing] = useState(null); const [saving, setSaving] = useState(false); const [detail, setDetail] = useState(null); const [testingId, setTestingId] = useState(null); const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList( '/admin/wecom-message-pushes', () => { const qs = new URLSearchParams(); if (filters.name) qs.set('name', filters.name); if (filters.enabled) qs.set('enabled', filters.enabled); return qs; }, [filters], ); const conditionsWatch = Form.useWatch('pushConditions', form) as WecomPushCondition[] | undefined; function openCreate() { setEditing(null); form.resetFields(); form.setFieldsValue({ enabled: true, pushConditions: ['alert.ops'], sortOrder: 0, }); setModalOpen(true); } function openEdit(row: WecomMessagePushDto) { setEditing(row); form.setFieldsValue({ name: row.name, avatarUrl: row.avatarUrl ?? undefined, webhookUrl: row.webhookUrl, enabled: row.enabled, mentionWecomUserId: row.mentionWecomUserId ?? undefined, pushConditions: row.pushConditions, sortOrder: row.sortOrder, }); setModalOpen(true); } async function save() { const values = await form.validateFields(); setSaving(true); try { const body = { name: values.name, avatarUrl: values.avatarUrl || null, webhookUrl: values.webhookUrl, enabled: values.enabled, mentionWecomUserId: values.mentionWecomUserId?.trim() || null, pushConditions: values.pushConditions, sortOrder: values.sortOrder ?? 0, }; if (editing) { await request(`/admin/wecom-message-pushes/${editing.id}`, { method: 'PUT', body: JSON.stringify(body), }); message.success('已更新'); } else { await request('/admin/wecom-message-pushes', { method: 'POST', body: JSON.stringify(body), }); message.success('已创建'); } setModalOpen(false); reload(); } catch (e) { message.error(e instanceof Error ? e.message : '保存失败'); } finally { setSaving(false); } } async function remove(id: string) { try { await request(`/admin/wecom-message-pushes/${id}`, { method: 'DELETE' }); message.success('已删除'); reload(); } catch (e) { message.error(e instanceof Error ? e.message : '删除失败'); } } async function testPush(id: string) { setTestingId(id); try { const res = await request<{ ok: boolean; message: string }>( `/admin/wecom-message-pushes/${id}/test`, { method: 'POST', body: '{}' }, ); message.success(res.message || '已发送测试消息'); } catch (e) { message.error(e instanceof Error ? e.message : '测试失败'); } finally { setTestingId(null); } } const columns: ColumnsType = [ { title: '名称', dataIndex: 'name', render: (name: string, row) => ( {name.slice(0, 1)} {name} ), }, { title: 'Webhook', dataIndex: 'webhookUrlMasked', ellipsis: true, }, { title: '推送条件', dataIndex: 'pushConditions', render: (conds: WecomPushCondition[]) => conds.map((c) => ( {WECOM_PUSH_CONDITION_LABELS[c]} )), }, { title: '启用', dataIndex: 'enabled', width: 72, render: (v: boolean) => (v ? : ), }, { title: '排序', dataIndex: 'sortOrder', width: 64 }, { title: '操作', key: 'actions', width: 220, render: (_, row) => ( void remove(row.id)}> ), }, ]; return ( <> 配置群机器人 Webhook:按推送条件订阅业务通知 / 告警。运行时不再读取 .env 中的 Webhook URL。
setFilters(v as Record)} > v?.length ? Promise.resolve() : Promise.reject(new Error('请至少勾选一项推送条件')), }, ]} > {conditionsWatch?.length ? ( 已选 {conditionsWatch.length} 项: {conditionsWatch.map((c) => WECOM_PUSH_CONDITION_LABELS[c]).join('、')} ) : null} setDetail(null)} footer={null} width={560} > {detail ? ( {detail.name} {detail.webhookUrlMasked} {detail.enabled ? '是' : '否'} {detail.mentionWecomUserId || '—'} {detail.pushConditions.map((c) => WECOM_PUSH_CONDITION_LABELS[c]).join('、')} {detail.sortOrder} {fmtTime(detail.createdAt)} {fmtTime(detail.updatedAt)} ) : null} ); } function TemplatesTab() { const [templates, setTemplates] = useState([]); const [loading, setLoading] = useState(false); const [eventKey, setEventKey] = useState('order.paid'); const [form] = Form.useForm<{ title: string; body: string; handleLabel: string }>(); const [saving, setSaving] = useState(false); const [testing, setTesting] = useState(false); const [preview, setPreview] = useState(''); const load = useCallback(async () => { setLoading(true); try { const list = await request('/admin/wecom-push-templates'); setTemplates(list); setEventKey((prev) => { const current = list.find((t) => t.eventKey === prev) ?? list[0]; if (current) { form.setFieldsValue({ title: current.title, body: current.body, handleLabel: current.handleLabel, }); return current.eventKey; } return prev; }); } catch (e) { message.error(e instanceof Error ? e.message : '加载模板失败'); } finally { setLoading(false); } }, [form]); useEffect(() => { void load(); }, [load]); function selectEvent(key: WecomTemplateEventKey) { setEventKey(key); const row = templates.find((t) => t.eventKey === key); if (row) { form.setFieldsValue({ title: row.title, body: row.body, handleLabel: row.handleLabel, }); setPreview(''); } } async function save() { const values = await form.validateFields(); setSaving(true); try { const updated = await request( `/admin/wecom-push-templates/${eventKey}`, { method: 'PUT', body: JSON.stringify(values), }, ); message.success('模板已保存'); setTemplates((prev) => prev.map((t) => (t.eventKey === eventKey ? updated : t))); } catch (e) { message.error(e instanceof Error ? e.message : '保存失败'); } finally { setSaving(false); } } async function reset() { setSaving(true); try { const updated = await request( `/admin/wecom-push-templates/${eventKey}/reset`, { method: 'POST', body: '{}' }, ); message.success('已恢复默认文案'); form.setFieldsValue({ title: updated.title, body: updated.body, handleLabel: updated.handleLabel, }); setTemplates((prev) => prev.map((t) => (t.eventKey === eventKey ? updated : t))); setPreview(''); } catch (e) { message.error(e instanceof Error ? e.message : '恢复失败'); } finally { setSaving(false); } } async function testSend() { setTesting(true); try { const res = await request<{ ok: boolean; message: string; preview: string }>( `/admin/wecom-push-templates/${eventKey}/test`, { method: 'POST', body: '{}' }, ); setPreview(res.preview || ''); message.success(res.message || '已发送'); } catch (e) { message.error(e instanceof Error ? e.message : '测试失败'); } finally { setTesting(false); } } const placeholders = WECOM_TEMPLATE_PLACEHOLDERS[eventKey] ?? []; return ( <> 每种业务事件全站一份文案,使用 {'{{orderNo}}'} 等形式占位。快链由系统注入 {'{{handleUrl}}'} ;须在「推送路由」中勾选对应条件并配置 Webhook 才会发出。
事件
{WECOM_TEMPLATE_EVENT_KEYS.map((k) => (
))}
`{{${p}}}`).join(' ')}`} > void reset()}>
{preview ? (
预览
                {preview}
              
) : null}
); } export default function WecomMessagePushesPage() { return (
企微机器人 · 消息推送 }, { key: 'templates', label: '通知模板', children: }, ]} />
); }