feat: multi-module iteration

This commit is contained in:
2026-08-04 21:38:49 +08:00
parent 9d96c73246
commit 71f508e02b
1366 changed files with 202004 additions and 0 deletions
@@ -0,0 +1,405 @@
import { useState } from 'react';
import {
Avatar,
Button,
Checkbox,
Descriptions,
Form,
Input,
InputNumber,
Modal,
Popconfirm,
Select,
Space,
Switch,
Table,
Tag,
Typography,
message,
} from 'antd';
import type { ColumnsType } from 'antd/es/table';
import {
WECOM_PUSH_CONDITION_GROUPS,
WECOM_PUSH_CONDITION_LABELS,
type WecomMessagePushDto,
type WecomPushCondition,
} 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 (
<div style={{ width: '100%' }}>
{WECOM_PUSH_CONDITION_GROUPS.map((group) => (
<div key={group.key} style={{ marginBottom: 12 }}>
<Typography.Text strong>{group.label}</Typography.Text>
<div style={{ marginTop: 8 }}>
{group.conditions.map((c) => (
<div key={c}>
<Checkbox
checked={selected.has(c)}
onChange={(e) => toggle(c, e.target.checked)}
>
{WECOM_PUSH_CONDITION_LABELS[c]}
</Checkbox>
</div>
))}
</div>
</div>
))}
</div>
);
}
export default function WecomMessagePushesPage() {
const [filterForm] = Form.useForm();
const [form] = Form.useForm<FormValues>();
const [filters, setFilters] = useState<Record<string, string>>({});
const [modalOpen, setModalOpen] = useState(false);
const [editing, setEditing] = useState<WecomMessagePushDto | null>(null);
const [saving, setSaving] = useState(false);
const [detail, setDetail] = useState<WecomMessagePushDto | null>(null);
const [testingId, setTestingId] = useState<string | null>(null);
const { data, loading, page, pageSize, setPage, setPageSize, reload } =
useAdminList<WecomMessagePushDto>(
'/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<WecomMessagePushDto> = [
{
title: '名称',
dataIndex: 'name',
render: (name: string, row) => (
<Space>
<Avatar src={row.avatarUrl ?? undefined}>{name.slice(0, 1)}</Avatar>
<span>{name}</span>
</Space>
),
},
{
title: 'Webhook',
dataIndex: 'webhookUrlMasked',
ellipsis: true,
},
{
title: '推送条件',
dataIndex: 'pushConditions',
render: (conds: WecomPushCondition[]) =>
conds.map((c) => (
<Tag key={c} style={{ marginBottom: 4 }}>
{WECOM_PUSH_CONDITION_LABELS[c]}
</Tag>
)),
},
{
title: '启用',
dataIndex: 'enabled',
width: 72,
render: (v: boolean) => (v ? <Tag color="green"></Tag> : <Tag></Tag>),
},
{ title: '排序', dataIndex: 'sortOrder', width: 64 },
{
title: '操作',
key: 'actions',
width: 220,
render: (_, row) => (
<Space wrap>
<Button type="link" size="small" onClick={() => openEdit(row)}>
</Button>
<Button
type="link"
size="small"
loading={testingId === row.id}
onClick={() => void testPush(row.id)}
>
</Button>
<Button
type="link"
size="small"
onClick={() => {
void request<WecomMessagePushDto>(`/admin/wecom-message-pushes/${row.id}`).then(setDetail);
}}
>
</Button>
<Popconfirm title="确认删除?" onConfirm={() => void remove(row.id)}>
<Button type="link" size="small" danger>
</Button>
</Popconfirm>
</Space>
),
},
];
return (
<div>
<Typography.Title level={4}> · </Typography.Title>
<Typography.Paragraph type="secondary">
Webhook
.env Webhook URL
</Typography.Paragraph>
<Form
form={filterForm}
layout="inline"
style={{ marginBottom: 16 }}
onFinish={(v) => setFilters(v as Record<string, string>)}
>
<Form.Item name="name" label="名称">
<Input allowClear placeholder="搜索" />
</Form.Item>
<Form.Item name="enabled" label="启用">
<Select
allowClear
placeholder="全部"
style={{ width: 100 }}
options={[
{ value: 'true', label: '启用' },
{ value: 'false', label: '停用' },
]}
/>
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" htmlType="submit">
</Button>
<Button
onClick={() => {
filterForm.resetFields();
setFilters({});
}}
>
</Button>
<Button type="primary" onClick={openCreate}>
</Button>
</Space>
</Form.Item>
</Form>
<Table<WecomMessagePushDto>
rowKey="id"
loading={loading}
columns={columns}
dataSource={data?.items ?? []}
pagination={{
current: page,
pageSize,
total: data?.total ?? 0,
showSizeChanger: true,
onChange: (p, ps) => {
setPage(p);
setPageSize(ps);
},
}}
/>
<Modal
title={editing ? '编辑消息推送' : '新建消息推送'}
open={modalOpen}
onCancel={() => setModalOpen(false)}
onOk={() => void save()}
confirmLoading={saving}
width={640}
destroyOnClose
>
<Form form={form} layout="vertical">
<Form.Item name="name" label="名称" rules={[{ required: true, message: '请填写名称' }]}>
<Input placeholder="如:运营告警" />
</Form.Item>
<Form.Item name="avatarUrl" label="头像(HQ 列表展示)">
<OssUpload />
</Form.Item>
<Form.Item
name="webhookUrl"
label="Webhook URL"
rules={[{ required: true, message: '请填写 Webhook URL' }]}
>
<Input.Password placeholder="企微群机器人 Webhook" />
</Form.Item>
<Form.Item name="enabled" label="启用" valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item
name="mentionWecomUserId"
label="企微 userid(可选)"
extra="markdown 推送用 <@userid> @ 群成员;任务派发等场景生效"
>
<Input placeholder="如 woDacESQAAD..." allowClear />
</Form.Item>
<Form.Item
name="pushConditions"
label="推送条件"
rules={[
{
validator: (_, v: WecomPushCondition[] | undefined) =>
v?.length ? Promise.resolve() : Promise.reject(new Error('请至少勾选一项推送条件')),
},
]}
>
<WecomPushConditionPicker />
</Form.Item>
{conditionsWatch?.length ? (
<Typography.Text type="secondary" style={{ display: 'block', marginBottom: 16 }}>
{conditionsWatch.length}
{conditionsWatch.map((c) => WECOM_PUSH_CONDITION_LABELS[c]).join('、')}
</Typography.Text>
) : null}
<Form.Item name="sortOrder" label="排序">
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
</Form>
</Modal>
<Modal
title="消息推送详情"
open={!!detail}
onCancel={() => setDetail(null)}
footer={null}
width={560}
>
{detail ? (
<Descriptions column={1} bordered size="small">
<Descriptions.Item label="名称">{detail.name}</Descriptions.Item>
<Descriptions.Item label="Webhook">{detail.webhookUrlMasked}</Descriptions.Item>
<Descriptions.Item label="启用">{detail.enabled ? '是' : '否'}</Descriptions.Item>
<Descriptions.Item label="userid">{detail.mentionWecomUserId || '—'}</Descriptions.Item>
<Descriptions.Item label="推送条件">
{detail.pushConditions.map((c) => WECOM_PUSH_CONDITION_LABELS[c]).join('、')}
</Descriptions.Item>
<Descriptions.Item label="排序">{detail.sortOrder}</Descriptions.Item>
<Descriptions.Item label="创建时间">{fmtTime(detail.createdAt)}</Descriptions.Item>
<Descriptions.Item label="更新时间">{fmtTime(detail.updatedAt)}</Descriptions.Item>
</Descriptions>
) : null}
</Modal>
</div>
);
}