Initial commit: 风影 OA 全栈源码(API/Web/Flutter/IM)
含 Phase 1.1 IM seq 排序、断线重连、多端已读同步与微信式语音转文字 UI。 排除 node_modules、构建产物、安装包与 .env 密钥。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,577 @@
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Descriptions,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
Table,
|
||||
Tabs,
|
||||
Tag,
|
||||
AutoComplete,
|
||||
message,
|
||||
} from 'antd';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { api } from '../api/client';
|
||||
import { PageHeader } from './ResourcePage';
|
||||
import { StatusTag } from '../StatusTag';
|
||||
|
||||
type SignRow = {
|
||||
signName?: string;
|
||||
SignName?: string;
|
||||
auditStatus?: string | number;
|
||||
AuditStatus?: string | number;
|
||||
createDate?: string;
|
||||
CreateDate?: string;
|
||||
reason?: string;
|
||||
Reason?: string;
|
||||
signSource?: number;
|
||||
SignSource?: number;
|
||||
signType?: number;
|
||||
SignType?: number;
|
||||
businessType?: string;
|
||||
BusinessType?: string;
|
||||
};
|
||||
type TplRow = {
|
||||
templateCode?: string;
|
||||
TemplateCode?: string;
|
||||
templateName?: string;
|
||||
TemplateName?: string;
|
||||
auditStatus?: string | number;
|
||||
AuditStatus?: string | number;
|
||||
templateContent?: string;
|
||||
TemplateContent?: string;
|
||||
templateType?: number;
|
||||
TemplateType?: number;
|
||||
createDate?: string;
|
||||
CreateDate?: string;
|
||||
reason?: string;
|
||||
Reason?: string;
|
||||
};
|
||||
|
||||
const SIGN_SOURCE: Record<number, string> = {
|
||||
0: '企事业单位全称或简称',
|
||||
1: '工信部备案网站全称或简称',
|
||||
2: 'App 全称或简称',
|
||||
3: '公众号或小程序全称或简称',
|
||||
4: '电商平台店铺名',
|
||||
5: '商标全称或简称',
|
||||
};
|
||||
|
||||
const SIGN_TYPE: Record<number, string> = { 0: '验证码', 1: '通用' };
|
||||
const TPL_TYPE: Record<number, string> = {
|
||||
0: '验证码',
|
||||
1: '通知短信',
|
||||
2: '推广短信',
|
||||
3: '国际/港澳台',
|
||||
7: '数字短信',
|
||||
};
|
||||
|
||||
function auditText(v: unknown) {
|
||||
const s = String(v ?? '');
|
||||
if (s === '1' || s === 'AUDIT_STATE_PASS' || s === 'PASS') return { color: 'success', text: '已通过' };
|
||||
if (s === '0' || s === 'AUDIT_STATE_INIT' || s === 'AUDITING') return { color: 'processing', text: '审核中' };
|
||||
if (s === '2' || s === 'AUDIT_STATE_NOT_PASS' || s === 'NOT_PASS') return { color: 'error', text: '未通过' };
|
||||
if (s === '3' || s === 'CANCEL' || s === 'AUDIT_STATE_CANCEL') return { color: 'default', text: '已取消' };
|
||||
return { color: 'default', text: s ? zh(s) : '—' };
|
||||
}
|
||||
|
||||
function unwrapList<T>(data: unknown, keys: string[]): T[] {
|
||||
const body = (data as { data?: unknown })?.data ?? data;
|
||||
if (!body || typeof body !== 'object') return [];
|
||||
const obj = body as Record<string, unknown>;
|
||||
for (const k of keys) {
|
||||
const v = obj[k];
|
||||
if (Array.isArray(v)) return v as T[];
|
||||
}
|
||||
if (Array.isArray(body)) return body as T[];
|
||||
return [];
|
||||
}
|
||||
|
||||
function zh(s: string) {
|
||||
return s;
|
||||
}
|
||||
|
||||
function dash(v?: string | null) {
|
||||
return v && String(v).trim() ? v : '未绑定';
|
||||
}
|
||||
|
||||
export default function SmsPage() {
|
||||
const qc = useQueryClient();
|
||||
const cfg = useQuery({ queryKey: ['/sms/config'], queryFn: () => api.get('/sms/config') });
|
||||
const signs = useQuery({
|
||||
queryKey: ['/sms/signs'],
|
||||
queryFn: () => api.get('/sms/signs'),
|
||||
retry: false,
|
||||
});
|
||||
const tpls = useQuery({
|
||||
queryKey: ['/sms/templates'],
|
||||
queryFn: () => api.get('/sms/templates'),
|
||||
retry: false,
|
||||
});
|
||||
const quals = useQuery({
|
||||
queryKey: ['/sms/qualifications'],
|
||||
queryFn: () => api.get('/sms/qualifications'),
|
||||
retry: false,
|
||||
});
|
||||
const logs = useQuery({ queryKey: ['/sms/logs'], queryFn: () => api.get('/sms/logs') });
|
||||
const events = useQuery({ queryKey: ['/sms/events'], queryFn: () => api.get('/sms/events') });
|
||||
const [signForm] = Form.useForm();
|
||||
const [tplForm] = Form.useForm();
|
||||
const info = (cfg.data?.data ?? {}) as {
|
||||
configured?: boolean;
|
||||
defaultSign?: string;
|
||||
note?: string;
|
||||
region?: string;
|
||||
endpoint?: string;
|
||||
ramLogin?: string;
|
||||
accessKeyIdMasked?: string;
|
||||
secretConfigured?: boolean;
|
||||
securityPhone?: string;
|
||||
securityEmail?: string;
|
||||
presets?: { code: string; name: string }[];
|
||||
};
|
||||
const signRows = unwrapList<SignRow>(signs.data, ['smsSignList', 'SmsSignList']);
|
||||
const tplRows = unwrapList<TplRow>(tpls.data, ['smsTemplateList', 'SmsTemplateList']);
|
||||
const qualRows = unwrapList<{
|
||||
qualificationId?: string;
|
||||
QualificationId?: string;
|
||||
qualificationName?: string;
|
||||
QualificationName?: string;
|
||||
companyName?: string;
|
||||
CompanyName?: string;
|
||||
state?: string;
|
||||
State?: string;
|
||||
}>(quals.data, ['qualificationInfos', 'QualificationInfos', 'records']);
|
||||
const logRows = unwrapList<{
|
||||
id: string;
|
||||
phones: string;
|
||||
signName: string;
|
||||
templateCode: string;
|
||||
eventKey?: string;
|
||||
code: string;
|
||||
message?: string;
|
||||
createdAt: string;
|
||||
}>(logs.data, ['items']);
|
||||
const eventPack = (events.data?.data ?? {}) as {
|
||||
configured?: boolean;
|
||||
defaultSign?: string;
|
||||
presets?: { code: string; name: string }[];
|
||||
items?: {
|
||||
key: string;
|
||||
group: string;
|
||||
name: string;
|
||||
when: string;
|
||||
to: string;
|
||||
enabled: boolean;
|
||||
automatic?: boolean;
|
||||
templateCode: string;
|
||||
signName: string;
|
||||
sample: Record<string, string>;
|
||||
vars?: string;
|
||||
}[];
|
||||
};
|
||||
const eventRows = eventPack.items || [];
|
||||
const eventMut = useMutation({
|
||||
mutationFn: (body: Record<string, unknown>) => api.post('/sms/events', body),
|
||||
onSuccess: () => {
|
||||
message.success('已保存步骤关联');
|
||||
qc.invalidateQueries({ queryKey: ['/sms/events'] });
|
||||
},
|
||||
onError: (e: Error) => message.error(e.message),
|
||||
});
|
||||
const aliyunErr = (signs.error as Error | undefined)?.message || (tpls.error as Error | undefined)?.message;
|
||||
|
||||
const signMut = useMutation({
|
||||
mutationFn: (body: Record<string, unknown>) => api.post('/sms/signs', body),
|
||||
onSuccess: () => {
|
||||
message.success('已提交签名申请,阿里云审核中');
|
||||
qc.invalidateQueries({ queryKey: ['/sms/signs'] });
|
||||
},
|
||||
onError: (e: Error) => message.error(e.message),
|
||||
});
|
||||
const tplMut = useMutation({
|
||||
mutationFn: (body: Record<string, unknown>) => api.post('/sms/templates', body),
|
||||
onSuccess: () => {
|
||||
message.success('已提交模板申请,阿里云审核中');
|
||||
qc.invalidateQueries({ queryKey: ['/sms/templates'] });
|
||||
},
|
||||
onError: (e: Error) => message.error(e.message),
|
||||
});
|
||||
|
||||
const passedQuals = qualRows
|
||||
.map((q) => ({
|
||||
value: String(q.qualificationId || q.QualificationId || ''),
|
||||
label: String(q.qualificationName || q.QualificationName || q.companyName || q.CompanyName || q.qualificationId || ''),
|
||||
}))
|
||||
.filter((q) => q.value);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="短信设置"
|
||||
description="登录验证码、投标待办等步骤在这里开关。手工群发请到个人办公「发送短信」。签名和模板仍走阿里云审核。"
|
||||
extra={<Tag color={info.configured ? 'success' : 'error'}>{info.configured ? '已配置密钥' : '未配置密钥'}</Tag>}
|
||||
/>
|
||||
{!info.configured ? <Alert type="warning" showIcon message="尚未配置阿里云短信密钥" style={{ marginBottom: 12 }} /> : null}
|
||||
{aliyunErr ? (
|
||||
<Alert type="error" showIcon message="阿里云短信接口不可用" description={aliyunErr} style={{ marginBottom: 12 }} />
|
||||
) : null}
|
||||
|
||||
<Tabs
|
||||
items={[
|
||||
{
|
||||
key: 'events',
|
||||
label: '步骤关联',
|
||||
children: (
|
||||
<>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 12 }}
|
||||
message="只有登录和投标步骤可自动发短信;公告、日报、报销、人事、用章、工作安排等一律只能到「发送工作通知短信」手工发送。投标短信只发给当前步骤实际选定的办理人。"
|
||||
/>
|
||||
<Table
|
||||
className="ops-table"
|
||||
rowKey="key"
|
||||
loading={events.isLoading}
|
||||
dataSource={eventRows}
|
||||
pagination={false}
|
||||
columns={[
|
||||
{ title: '分组', dataIndex: 'group', width: 90 },
|
||||
{ title: '步骤', dataIndex: 'name', width: 140 },
|
||||
{ title: '何时发', dataIndex: 'when' },
|
||||
{ title: '发给谁', dataIndex: 'to', width: 140 },
|
||||
{
|
||||
title: '变量',
|
||||
dataIndex: 'vars',
|
||||
width: 180,
|
||||
render: (_: unknown, r) => r.vars || Object.keys(r.sample || {}).join('、') || '—',
|
||||
},
|
||||
{
|
||||
title: '模板',
|
||||
dataIndex: 'templateCode',
|
||||
width: 220,
|
||||
render: (_: unknown, r) => (
|
||||
<AutoComplete
|
||||
value={r.templateCode}
|
||||
style={{ width: '100%' }}
|
||||
options={(eventPack.presets || info.presets || []).map((p) => ({
|
||||
value: p.code,
|
||||
label: `${p.name}(${p.code})`,
|
||||
}))}
|
||||
onBlur={(e) => {
|
||||
const v = (e.target as HTMLInputElement).value?.trim();
|
||||
if (v && v !== r.templateCode) eventMut.mutate({ eventKey: r.key, templateCode: v });
|
||||
}}
|
||||
onSelect={(v) => eventMut.mutate({ eventKey: r.key, templateCode: String(v) })}
|
||||
disabled={r.automatic === false}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '启用',
|
||||
dataIndex: 'enabled',
|
||||
width: 80,
|
||||
render: (v: boolean, r) => (
|
||||
<Switch
|
||||
checked={v}
|
||||
disabled={r.automatic === false}
|
||||
loading={eventMut.isPending}
|
||||
onChange={(enabled) => eventMut.mutate({ eventKey: r.key, enabled })}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'account',
|
||||
label: '账号设置',
|
||||
children: (
|
||||
<Card size="small" className="sms-account" title="阿里云 RAM / 短信通道">
|
||||
<Descriptions bordered size="small" column={1}>
|
||||
<Descriptions.Item label="RAM 登录名称">{info.ramLogin || '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="登录密码">不写入本系统,仅控制台登录使用</Descriptions.Item>
|
||||
<Descriptions.Item label="AccessKey ID">{info.accessKeyIdMasked || '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="AccessKey Secret">
|
||||
{info.secretConfigured ? '已配置(密文,不在页面展示)' : '未配置'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="地域">{info.region || 'cn-hangzhou'}</Descriptions.Item>
|
||||
<Descriptions.Item label="接口地址">{info.endpoint || 'dysmsapi.aliyuncs.com'}</Descriptions.Item>
|
||||
<Descriptions.Item label="默认短信签名">{info.defaultSign || '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="SecurityPhoneDevice">{dash(info.securityPhone)}</Descriptions.Item>
|
||||
<Descriptions.Item label="SecurityEmailDevice">{dash(info.securityEmail)}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<p style={{ marginTop: 12, color: 'var(--mute)', fontSize: 12 }}>{info.note}</p>
|
||||
</Card>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'signs',
|
||||
label: '签名',
|
||||
children: (
|
||||
<>
|
||||
<Space style={{ marginBottom: 12 }}>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
signForm.resetFields();
|
||||
signForm.setFieldsValue({
|
||||
signName: info.defaultSign,
|
||||
signSource: 0,
|
||||
signType: 1,
|
||||
qualificationId: passedQuals[0]?.value,
|
||||
remark: '江苏风影随行科技办公平台向内部员工发送工作通知,签名为本公司全称或简称。',
|
||||
});
|
||||
Modal.confirm({
|
||||
title: '申请短信签名',
|
||||
width: 520,
|
||||
icon: null,
|
||||
content: (
|
||||
<Form form={signForm} layout="vertical" style={{ marginTop: 12 }}>
|
||||
<Form.Item name="signName" label="签名名称" rules={[{ required: true }]} extra="2–12 个字,不要带括号和测试字样。">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="signSource" label="签名来源" rules={[{ required: true }]}>
|
||||
<Select
|
||||
options={Object.entries(SIGN_SOURCE).map(([value, label]) => ({
|
||||
value: Number(value),
|
||||
label,
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="signType" label="签名类型">
|
||||
<Select
|
||||
options={[
|
||||
{ value: 1, label: '通用' },
|
||||
{ value: 0, label: '验证码' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="qualificationId"
|
||||
label="企业资质"
|
||||
extra="从阿里云「资质管理」已通过的记录里选。没有请先在控制台做资质认证。"
|
||||
>
|
||||
{passedQuals.length ? (
|
||||
<Select options={passedQuals} allowClear placeholder="选择已通过的资质" />
|
||||
) : (
|
||||
<Input placeholder="填写资质编号 QualificationId" />
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item name="remark" label="申请说明">
|
||||
<Input.TextArea rows={3} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
),
|
||||
onOk: async () => signMut.mutateAsync(await signForm.validateFields()),
|
||||
});
|
||||
}}
|
||||
>
|
||||
申请签名
|
||||
</Button>
|
||||
<Button onClick={() => signs.refetch()}>刷新列表</Button>
|
||||
</Space>
|
||||
<Table
|
||||
className="ops-table"
|
||||
rowKey={(r) => String(r.signName || r.SignName)}
|
||||
loading={signs.isLoading}
|
||||
dataSource={signRows}
|
||||
pagination={false}
|
||||
locale={{ emptyText: '还没有签名,点申请签名。' }}
|
||||
columns={[
|
||||
{ title: '签名', render: (_: unknown, r: SignRow) => r.signName || r.SignName },
|
||||
{
|
||||
title: '来源',
|
||||
width: 180,
|
||||
render: (_: unknown, r: SignRow) =>
|
||||
SIGN_SOURCE[Number(r.signSource ?? r.SignSource)] || r.businessType || r.BusinessType || '—',
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
width: 90,
|
||||
render: (_: unknown, r: SignRow) => SIGN_TYPE[Number(r.signType ?? r.SignType)] || '—',
|
||||
},
|
||||
{
|
||||
title: '审核',
|
||||
width: 110,
|
||||
render: (_: unknown, r: SignRow) => {
|
||||
const s = auditText(r.auditStatus ?? r.AuditStatus);
|
||||
return <Tag color={s.color}>{s.text}</Tag>;
|
||||
},
|
||||
},
|
||||
{ title: '申请时间', width: 170, render: (_: unknown, r: SignRow) => r.createDate || r.CreateDate || '—' },
|
||||
{ title: '原因', render: (_: unknown, r: SignRow) => r.reason || r.Reason || '—' },
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'templates',
|
||||
label: '模板',
|
||||
children: (
|
||||
<>
|
||||
<Space style={{ marginBottom: 12 }}>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
tplForm.resetFields();
|
||||
tplForm.setFieldsValue({
|
||||
templateName: '办公工作通知',
|
||||
templateType: 1,
|
||||
relatedSignName: info.defaultSign,
|
||||
templateContent: '办公通知:${content}',
|
||||
templateRule: '{"content":"others"}',
|
||||
applySceneContent: 'https://oa.fysxkj.com',
|
||||
remark:
|
||||
'江苏风影随行科技办公平台向内部员工发送会议、审批待办、工作汇报、用章、报销等到岗通知。变量 content 为通知摘要,例如「请明日上午9点参加项目例会」。',
|
||||
});
|
||||
Modal.confirm({
|
||||
title: '向阿里云申请短信模板',
|
||||
width: 640,
|
||||
icon: null,
|
||||
content: (
|
||||
<Form form={tplForm} layout="vertical" style={{ marginTop: 12 }}>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 12 }}
|
||||
message={'有变量必须填 TemplateRule,例如 {"content":"others"}。模板通过后不能改内容,只能重新申请。'}
|
||||
/>
|
||||
<Form.Item name="templateName" label="模板名称" rules={[{ required: true }]}>
|
||||
<Input maxLength={30} />
|
||||
</Form.Item>
|
||||
<Form.Item name="templateType" label="短信类型" rules={[{ required: true }]}>
|
||||
<Select
|
||||
options={Object.entries(TPL_TYPE).map(([value, label]) => ({
|
||||
value: Number(value),
|
||||
label,
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="relatedSignName"
|
||||
label="关联签名"
|
||||
extra="只能选已通过的签名。此处关联仅用于审核,发送时仍用步骤关联里的签名。"
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="templateContent"
|
||||
label="模板内容"
|
||||
rules={[{ required: true }]}
|
||||
extra="变量写成 ${content}。通知类 others 最长 35 字。"
|
||||
>
|
||||
<Input.TextArea rows={4} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="templateRule"
|
||||
label="变量属性 TemplateRule"
|
||||
extra='模板有变量时必填 JSON。常用:others 通知正文/名称;user_nick 姓名;other_number2 单号;numberCaptcha 验证码;time 时间;address 地址;name 个人姓名。'
|
||||
>
|
||||
<Input.TextArea rows={2} placeholder='{"content":"others"}' />
|
||||
</Form.Item>
|
||||
<Form.Item name="applySceneContent" label="业务场景">
|
||||
<Input placeholder="https://oa.fysxkj.com" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="remark"
|
||||
label="申请说明"
|
||||
extra="写清使用场景,并给出填入变量后的完整短信示例,否则容易被拒。"
|
||||
>
|
||||
<Input.TextArea rows={3} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
),
|
||||
onOk: async () => {
|
||||
const v = await tplForm.validateFields();
|
||||
const content = String(v.templateContent || '');
|
||||
const hasVar = /\$\{[a-zA-Z0-9_]+\}/.test(content);
|
||||
if (hasVar && !String(v.templateRule || '').trim()) {
|
||||
throw new Error('模板含变量时必须填写 TemplateRule');
|
||||
}
|
||||
return tplMut.mutateAsync(v);
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
申请模板
|
||||
</Button>
|
||||
<Button onClick={() => tpls.refetch()}>刷新列表</Button>
|
||||
</Space>
|
||||
<Table
|
||||
className="ops-table"
|
||||
rowKey={(r) => String(r.templateCode || r.TemplateCode)}
|
||||
loading={tpls.isLoading}
|
||||
dataSource={tplRows}
|
||||
pagination={false}
|
||||
locale={{ emptyText: '还没有模板,点申请模板,或先用已有编号发送。' }}
|
||||
columns={[
|
||||
{ title: '模板编号', width: 160, render: (_: unknown, r: TplRow) => r.templateCode || r.TemplateCode },
|
||||
{ title: '名称', render: (_: unknown, r: TplRow) => r.templateName || r.TemplateName },
|
||||
{
|
||||
title: '类型',
|
||||
width: 110,
|
||||
render: (_: unknown, r: TplRow) => TPL_TYPE[Number(r.templateType ?? r.TemplateType)] || '—',
|
||||
},
|
||||
{
|
||||
title: '审核',
|
||||
width: 110,
|
||||
render: (_: unknown, r: TplRow) => {
|
||||
const s = auditText(r.auditStatus ?? r.AuditStatus);
|
||||
return <Tag color={s.color}>{s.text}</Tag>;
|
||||
},
|
||||
},
|
||||
{ title: '内容', ellipsis: true, render: (_: unknown, r: TplRow) => r.templateContent || r.TemplateContent || '—' },
|
||||
{ title: '原因', ellipsis: true, render: (_: unknown, r: TplRow) => r.reason || r.Reason || '—' },
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'logs',
|
||||
label: '发送记录',
|
||||
children: (
|
||||
<Table
|
||||
className="ops-table"
|
||||
rowKey="id"
|
||||
loading={logs.isLoading}
|
||||
dataSource={logRows.length ? logRows : ((logs.data?.data ?? []) as typeof logRows)}
|
||||
pagination={false}
|
||||
locale={{ emptyText: '还没有从本系统发出的短信。' }}
|
||||
columns={[
|
||||
{ title: '手机号', dataIndex: 'phones', width: 180 },
|
||||
{ title: '签名', dataIndex: 'signName', width: 160 },
|
||||
{ title: '模板', dataIndex: 'templateCode', width: 150 },
|
||||
{ title: '步骤', dataIndex: 'eventKey', width: 140, render: (v: string) => v || '手工发送' },
|
||||
{
|
||||
title: '结果',
|
||||
dataIndex: 'code',
|
||||
width: 90,
|
||||
render: (v: string) => <StatusTag value={v} />,
|
||||
},
|
||||
{ title: '说明', dataIndex: 'message', render: (v: string) => v || '—' },
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'createdAt',
|
||||
width: 170,
|
||||
render: (v: string) => (v ? String(v).replace('T', ' ').slice(0, 19) : '—'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user