Initial CMPP frontend prototype

This commit is contained in:
hectorzhao
2026-06-30 16:09:46 +08:00
commit 2f3c274a30
98 changed files with 25255 additions and 0 deletions
+200
View File
@@ -0,0 +1,200 @@
import { useMemo, useState } from 'react';
import { Filter, Pencil, Plus, Search, Trash2 } from 'lucide-react';
import { Button, Input, Modal, Select, Table, Textarea, Tag, type TableColumn } from '@/components/ui';
type DrainageFieldType = '字符串' | '整数' | '文件' | '网址' | '电话' | '日期';
type DrainageField = {
id: string;
name: string;
type: DrainageFieldType;
description: string;
channels: number;
channel: 'sms' | 'mms' | 'all';
};
const typeOptions = [
{ label: '全部类型', value: 'all' },
{ label: '字符串', value: '字符串' },
{ label: '整数', value: '整数' },
{ label: '文件', value: '文件' },
{ label: '网址', value: '网址' },
{ label: '电话', value: '电话' },
{ label: '日期', value: '日期' },
];
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 [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 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;
}),
[channel, 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);
}
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} />} iconOnly onClick={() => setEditingField(record)} variant="ghost"></Button>
<Button
icon={<Trash2 size={17} />}
iconOnly
onClick={() => setFields((current) => current.filter((item) => item.id !== record.id))}
variant="danger"
>
</Button>
</div>
),
},
], []);
return (
<section className="page-stack admin-system-page admin-drainage-page">
<div className="page-heading">
<div>
<div className="breadcrumb-line"> / <strong></strong></div>
<h1></h1>
</div>
</div>
<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}
</section>
);
}