fix: connect remaining sms pages to real backend

This commit is contained in:
hectorzhao
2026-07-02 19:30:04 +08:00
parent 06562e42dd
commit 131f344ac4
50 changed files with 2074 additions and 6400 deletions
+84 -155
View File
@@ -1,165 +1,78 @@
import { useMemo, useState } from 'react';
import { Filter, Pencil, Plus, Search, Trash2 } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
import { Plus, Search } from 'lucide-react';
import { Breadcrumb, Button, Input, Modal, Select, Table, Textarea, Tag, type TableColumn } from '@/components/ui';
import { adminApi, type DictionaryItem } from '@/api/adminApi';
type DrainageFieldType = '字符串' | '整数' | '文件' | '网址' | '电话' | '日期';
type DrainageField = {
id: string;
name: string;
type: DrainageFieldType;
description: string;
channels: number;
channel: 'sms' | 'mms' | 'all';
type DrainageField = DictionaryItem & {
code?: string;
name?: string;
fieldType?: string;
required?: boolean;
description?: string | null;
};
const typeOptions = [
{ label: '全部类型', value: 'all' },
{ label: '字符串', value: '字符串' },
{ label: '整数', value: '整数' },
{ label: '文件', value: '文件' },
{ label: '网址', value: '网址' },
{ label: '电话', value: '电话' },
{ label: '日期', value: '日期' },
{ label: '字符串', value: 'string' },
{ label: '整数', value: 'number' },
{ label: '文件', value: 'file' },
{ label: '网址', value: 'url' },
{ label: '电话', value: 'phone' },
{ label: '日期', value: 'date' },
];
const channelOptions = [
{ label: '全部通道', value: 'all' },
{ label: '短信通道', value: 'sms' },
{ label: '彩信通道', value: 'mms' },
];
const initialFields: DrainageField[] = [
{ id: 'DRF20260630001', name: '应用ID', type: '字符串', description: '在应用集成中创建的短信应用 ID', channels: 1, channel: 'sms' },
{ id: 'DRF20260630002', name: '应用密匙', type: '字符串', description: '应用密匙或数字签名', channels: 1, channel: 'sms' },
{ id: 'DRF20260630003', name: '短信签名', type: '字符串', description: '短信签名,【】符号可省略', channels: 1, channel: 'sms' },
{ id: 'DRF20260630004', name: '短信用途', type: '整数', description: '0-行业通知短信、1-营销推广短信', channels: 1, channel: 'sms' },
{ id: 'DRF20260630005', name: '证明材料', type: '文件', description: '上传营业执照、授权书等证明材料', channels: 1, channel: 'all' },
{ id: 'DRF20260630006', name: '引流链接', type: '网址', description: '短信内链接地址', channels: 2, channel: 'all' },
{ id: 'DRF20260630007', name: '引流号码', type: '电话', description: '引流号码1', channels: 3, channel: 'sms' },
{ id: 'DRF20260630008', name: '机主姓名', type: '字符串', description: '引流号码1机主姓名', channels: 3, channel: 'sms' },
{ id: 'DRF20260630009', name: 'ICP备案号', type: '字符串', description: '域名ICP备案号', channels: 1, channel: 'all' },
{ id: 'DRF20260630010', name: '拨测日期', type: '日期', description: '引流号码拨测日期', channels: 2, channel: 'sms' },
];
function createFieldId() {
return `DRF${Date.now()}`;
}
type FieldFormModalProps = {
item?: DrainageField;
onClose: () => void;
onSubmit: (item: DrainageField) => void;
};
function FieldFormModal({ item, onClose, onSubmit }: FieldFormModalProps) {
const [form, setForm] = useState<DrainageField>(() => item ?? {
id: createFieldId(),
name: '',
type: '字符串',
description: '',
channels: 1,
channel: 'sms',
});
function updateField<Key extends keyof DrainageField>(key: Key, value: DrainageField[Key]) {
setForm((current) => ({ ...current, [key]: value }));
}
return (
<Modal
footer={(
<>
<Button onClick={onClose} variant="ghost"></Button>
<Button onClick={() => onSubmit(form)}></Button>
</>
)}
onClose={onClose}
open
title={item ? '编辑报备字段' : '添加报备字段'}
>
<div className="admin-system-modal-form">
<Input label="字段名称" onChange={(event) => updateField('name', event.target.value)} value={form.name} />
<Select
label="字段类型"
onChange={(event) => updateField('type', event.target.value as DrainageFieldType)}
options={typeOptions.filter((option) => option.value !== 'all')}
value={form.type}
/>
<Select
label="适用通道"
onChange={(event) => updateField('channel', event.target.value as DrainageField['channel'])}
options={channelOptions}
value={form.channel}
/>
<Input label="使用通道数" min={1} onChange={(event) => updateField('channels', Number(event.target.value) || 1)} type="number" value={form.channels} />
<Textarea
className="admin-system-modal-form__wide"
label="描述"
onChange={(event) => updateField('description', event.target.value)}
rows={4}
value={form.description}
/>
</div>
</Modal>
);
}
export function AdminDrainageFieldsPage() {
const [fields, setFields] = useState(initialFields);
const [fields, setFields] = useState<DrainageField[]>([]);
const [keyword, setKeyword] = useState('');
const [channel, setChannel] = useState('all');
const [type, setType] = useState('all');
const [editingField, setEditingField] = useState<DrainageField | null>(null);
const [creating, setCreating] = useState(false);
const [code, setCode] = useState('');
const [name, setName] = useState('');
const [fieldType, setFieldType] = useState('string');
const [description, setDescription] = useState('');
const [error, setError] = useState('');
function loadData() {
adminApi.listDrainageFields()
.then((items) => {
setFields(items as DrainageField[]);
setError('');
})
.catch((failure: Error) => setError(failure.message || '报备字段加载失败'));
}
useEffect(() => {
loadData();
}, []);
const filteredFields = useMemo(
() => fields.filter((field) => {
const matchesKeyword = [field.name, field.type, field.description].some((value) => value.includes(keyword));
const matchesChannel = channel === 'all' || field.channel === channel || field.channel === 'all';
const matchesType = type === 'all' || field.type === type;
return matchesKeyword && matchesChannel && matchesType;
const matchesKeyword = !keyword || [field.code, field.name, field.fieldType, field.description].some((value) => String(value ?? '').includes(keyword));
const matchesType = type === 'all' || field.fieldType === type;
return matchesKeyword && matchesType;
}),
[channel, fields, keyword, type],
[fields, keyword, type],
);
function upsertField(nextField: DrainageField) {
setFields((current) => {
const exists = current.some((item) => item.id === nextField.id);
if (exists) {
return current.map((item) => (item.id === nextField.id ? nextField : item));
}
return [nextField, ...current];
});
setEditingField(null);
setCreating(false);
function createField() {
adminApi.createDrainageField({ code, name, fieldType, description, status: 'active' })
.then(() => {
setCode('');
setName('');
setFieldType('string');
setDescription('');
setCreating(false);
loadData();
})
.catch((failure: Error) => setError(failure.message || '报备字段新增失败'));
}
const columns = useMemo<Array<TableColumn<DrainageField>>>(() => [
{ key: 'name', title: '字段名称', width: '190px', render: (record) => <strong>{record.name}</strong> },
{ key: 'type', title: '字段类型', width: '160px', render: (record) => <span className="admin-drainage-type">{record.type}</span> },
{ key: 'description', title: '描述', render: (record) => record.description },
{ key: 'channels', title: '使用通道数', width: '170px', render: (record) => <Tag>{record.channels} </Tag> },
{
key: 'actions',
title: '操作',
width: '140px',
align: 'right',
render: (record) => (
<div className="admin-drainage-actions">
<Button icon={<Pencil size={17} />} onClick={() => setEditingField(record)} size="sm" variant="ghost"></Button>
<Button
icon={<Trash2 size={17} />}
onClick={() => setFields((current) => current.filter((item) => item.id !== record.id))}
size="sm"
variant="danger"
>
</Button>
</div>
),
},
{ key: 'code', title: '字段代码', width: '160px', render: (record) => <strong>{record.code}</strong> },
{ key: 'name', title: '字段名称', width: '190px', render: (record) => record.name ?? '-' },
{ key: 'type', title: '字段类型', width: '160px', render: (record) => <span className="admin-drainage-type">{record.fieldType}</span> },
{ key: 'description', title: '描述', render: (record) => record.description ?? '-' },
{ key: 'required', title: '是否必填', width: '120px', render: (record) => <Tag tone={record.required ? 'warning' : 'info'}>{record.required ? '必填' : '选填'}</Tag> },
], []);
return (
@@ -170,31 +83,47 @@ export function AdminDrainageFieldsPage() {
<h1></h1>
</div>
</div>
{error ? <p className="form-error">{error}</p> : null}
<div className="surface admin-drainage-toolbar">
<Input onChange={(event) => setKeyword(event.target.value)} placeholder="搜索字段名称、代码或描述..." prefix={<Search size={16} />} value={keyword} />
<Select
onChange={(event) => setChannel(event.target.value)}
options={channelOptions}
value={channel}
/>
<Select onChange={(event) => setType(event.target.value)} options={typeOptions} value={type} />
<Button icon={<Plus size={18} />} onClick={() => setCreating(true)}></Button>
</div>
<div className="surface admin-system-table-card admin-drainage-table-card">
<Table columns={columns} data={filteredFields} emptyText="暂无字段" rowKey="id" />
<div className="admin-drainage-pagination">
<span>10/</span>
<Button icon={<Filter size={16} />} iconOnly variant="ghost"></Button>
<Button size="sm">1</Button>
<span>...</span>
<Button size="sm" variant="ghost">1</Button>
</div>
</div>
{creating ? <FieldFormModal onClose={() => setCreating(false)} onSubmit={upsertField} /> : null}
{editingField ? <FieldFormModal item={editingField} onClose={() => setEditingField(null)} onSubmit={upsertField} /> : null}
<Modal
footer={(
<>
<Button onClick={() => setCreating(false)} variant="ghost"></Button>
<Button disabled={!code || !name} onClick={createField}></Button>
</>
)}
onClose={() => setCreating(false)}
open={creating}
title="添加报备字段"
>
<div className="admin-system-modal-form">
<Input label="字段代码" onChange={(event) => setCode(event.target.value)} value={code} />
<Input label="字段名称" onChange={(event) => setName(event.target.value)} value={name} />
<Select
label="字段类型"
onChange={(event) => setFieldType(event.target.value)}
options={typeOptions.filter((option) => option.value !== 'all')}
value={fieldType}
/>
<Textarea
className="admin-system-modal-form__wide"
label="描述"
onChange={(event) => setDescription(event.target.value)}
rows={4}
value={description}
/>
</div>
</Modal>
</section>
);
}