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
@@ -0,0 +1,206 @@
import { useMemo, useState } from 'react';
import { Search, Smartphone } from 'lucide-react';
import {
Button,
DateRangeInput,
Input,
Modal,
Pagination,
Table,
type DateRangeValue,
type TableColumn,
} from '@/components/ui';
type UplinkMessage = {
id: string;
phone: string;
receivedAt: string;
content: string;
channel: string;
accessNo: string;
matchedRecord?: MatchedSendRecord;
};
type MatchedSendRecord = {
id: string;
sentAt: string;
enterprise: string;
application: string;
accessNo: string;
content: string;
};
const matchedRecord: MatchedSendRecord = {
id: 'MT20260119001',
sentAt: '2026-01-19 12:25:28',
enterprise: '上海XXX有限公司',
application: 'XXX催收',
accessNo: '1069558812',
content: '【XXX科技】如果内容很长,换行 如果内容很长,换行 如果内容很长,换行 如果内容很长,换行 如果内容很长,换行 如果内容很长,换行 如果内容很长,换行 拒收请回复R',
};
const uplinkMessages: UplinkMessage[] = [
{ id: 'MO20260112001', phone: '13500000888', receivedAt: '2026-01-12 19:27:10', content: 'R', channel: '通道名称通道名称通道名称', accessNo: '106912246726', matchedRecord },
{ id: 'MO20260112002', phone: '13755558888', receivedAt: '2026-01-12 19:27:19', content: 'R', channel: '通道名称通道名称通道名称', accessNo: '106912246712', matchedRecord },
{ id: 'MO20260112003', phone: '18800000555', receivedAt: '2026-01-12 19:27:19', content: '到家了', channel: '通道名称通道名称通道名称', accessNo: '106912246732' },
{ id: 'MO20260112004', phone: '', receivedAt: '2026-01-12 19:27:19', content: 'XX', channel: '通道名称通道名称通道名称', accessNo: '106912346745' },
{ id: 'MO20260112005', phone: '', receivedAt: '2026-01-12 19:27:19', content: 'XX', channel: '', accessNo: '' },
{ id: 'MO20260112006', phone: '', receivedAt: '2026-01-12 19:27:19', content: 'XX', channel: '', accessNo: '' },
{ id: 'MO20260112007', phone: '', receivedAt: '2026-01-12 19:27:19', content: '', channel: '', accessNo: '' },
{ id: 'MO20260112008', phone: '', receivedAt: '2026-01-12 19:27:19', content: '', channel: '', accessNo: '' },
];
function getDate(value: string) {
return value.slice(0, 10);
}
function UplinkDetailModal({ message, onClose }: { message: UplinkMessage; onClose: () => void }) {
return (
<Modal
footer={<Button onClick={onClose} variant="ghost"></Button>}
onClose={onClose}
open
size="xl"
title="上行短信详情"
>
<div className="admin-uplink-detail">
<section className="admin-uplink-info-card">
<h3></h3>
<div className="admin-uplink-info-grid">
<div>
<span></span>
<strong>{message.phone || '-'}</strong>
</div>
<div>
<span></span>
<strong>{message.receivedAt}</strong>
</div>
<div>
<span></span>
<strong>{message.channel || '-'}</strong>
</div>
<div>
<span></span>
<strong>{message.accessNo || '-'}</strong>
</div>
<div className="admin-uplink-info-grid__full">
<span></span>
<strong>{message.content || '-'}</strong>
</div>
</div>
</section>
<section className="admin-uplink-match-section">
<h3></h3>
<p>7</p>
{message.matchedRecord ? (
<article className="admin-uplink-match-card">
<div className="admin-uplink-match-grid">
<div>
<span></span>
<strong>{message.matchedRecord.sentAt}</strong>
</div>
<div>
<span></span>
<strong>{message.matchedRecord.enterprise}</strong>
</div>
<div>
<span></span>
<strong>{message.matchedRecord.application}</strong>
</div>
<div>
<span></span>
<strong>{message.matchedRecord.accessNo}</strong>
</div>
</div>
<div className="admin-uplink-match-content">
<span></span>
<p>{message.matchedRecord.content}</p>
</div>
<button type="button"></button>
</article>
) : (
<div className="admin-uplink-empty-match"></div>
)}
</section>
</div>
</Modal>
);
}
export function AdminSmsUplinkRecordsPage() {
const [dateRange, setDateRange] = useState<DateRangeValue>({});
const [phoneKeyword, setPhoneKeyword] = useState('');
const [contentKeyword, setContentKeyword] = useState('');
const [selectedMessage, setSelectedMessage] = useState<UplinkMessage | null>(null);
const filteredMessages = useMemo(
() => uplinkMessages.filter((item) => {
const receivedDate = getDate(item.receivedAt);
const matchesStartDate = !dateRange.start || receivedDate >= dateRange.start;
const matchesEndDate = !dateRange.end || receivedDate <= dateRange.end;
const matchesPhone = !phoneKeyword || item.phone.includes(phoneKeyword);
const matchesContent = !contentKeyword || item.content.includes(contentKeyword);
return matchesStartDate && matchesEndDate && matchesPhone && matchesContent;
}),
[contentKeyword, dateRange.end, dateRange.start, phoneKeyword],
);
function resetFilters() {
setDateRange({});
setPhoneKeyword('');
setContentKeyword('');
}
const columns: Array<TableColumn<UplinkMessage>> = [
{
key: 'select',
title: '',
width: '72px',
align: 'center',
render: () => <input aria-label="选择上行记录" className="admin-uplink-checkbox" type="checkbox" />,
},
{ key: 'phone', title: '手机号码', width: '170px', render: (record) => <strong>{record.phone}</strong> },
{ key: 'receivedAt', title: '上行时间', width: '220px', render: (record) => <strong>{record.receivedAt}</strong> },
{ key: 'content', title: '上行内容', render: (record) => <span className="uplink-content">{record.content}</span> },
{ key: 'channel', title: '上行通道', width: '260px', render: (record) => <strong>{record.channel}</strong> },
{ key: 'accessNo', title: '上行接入号', width: '180px', render: (record) => <strong>{record.accessNo}</strong> },
{
key: 'actions',
title: '操作',
width: '140px',
align: 'center',
render: (record) => (
<button className="admin-uplink-detail-link" onClick={() => setSelectedMessage(record)} type="button"></button>
),
},
];
return (
<section className="page-stack admin-uplink-page">
<div className="page-heading">
<div>
<div className="breadcrumb-line"> / <strong></strong></div>
<h1></h1>
</div>
</div>
<div className="surface admin-uplink-filter">
<DateRangeInput label="上行时间" onChange={setDateRange} value={dateRange} />
<Input label="手机号码" onChange={(event) => setPhoneKeyword(event.target.value)} prefix={<Smartphone size={16} />} value={phoneKeyword} />
<Input label="上行内容" onChange={(event) => setContentKeyword(event.target.value)} value={contentKeyword} />
<div className="admin-uplink-filter__actions">
<Button icon={<Search size={16} />}></Button>
<Button onClick={resetFilters} variant="ghost"></Button>
</div>
</div>
<div className="surface admin-uplink-table-card">
<Table columns={columns} data={filteredMessages} emptyText="暂无上行短信记录" rowKey="id" />
<Pagination total={filteredMessages.length} />
</div>
{selectedMessage ? <UplinkDetailModal message={selectedMessage} onClose={() => setSelectedMessage(null)} /> : null}
</section>
);
}