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
+333
View File
@@ -0,0 +1,333 @@
import { useState } from 'react';
import { ChevronDown, ChevronRight, Edit3, FilePenLine, Info, Plus, Search, Trash2, Upload } from 'lucide-react';
import { Button, Input, Modal, Select, Tag } from '@/components/ui';
type CarrierStatus = 'approved' | 'pending' | 'rejected';
type DrainageInfo = {
id: string;
siteName: string;
content: string;
mobile: CarrierStatus;
unicom: CarrierStatus;
telecom: CarrierStatus;
submittedAt: string;
};
type SignatureItem = {
id: string;
name: string;
application: string;
accent: 'green' | 'blue' | 'red' | 'gray';
mobile: CarrierStatus;
unicom: CarrierStatus;
telecom: CarrierStatus;
drainage: DrainageInfo[];
};
const statusLabelMap: Record<CarrierStatus, string> = {
approved: '已通过',
pending: '审核中',
rejected: '已驳回',
};
const statusToneMap: Record<CarrierStatus, 'success' | 'info' | 'danger'> = {
approved: 'success',
pending: 'info',
rejected: 'danger',
};
const initialSignatures: SignatureItem[] = [
{
id: 'sig-1',
name: '【科技公司】',
application: '营销推广',
accent: 'green',
mobile: 'approved',
unicom: 'approved',
telecom: 'approved',
drainage: [
{ id: 'drain-1', siteName: '官方网站', content: 'https://www.example.com', mobile: 'approved', unicom: 'approved', telecom: 'approved', submittedAt: '2024-01-08 11:00:00' },
{ id: 'drain-2', siteName: '促销活动页', content: 'https://promo.example.com', mobile: 'approved', unicom: 'pending', telecom: 'pending', submittedAt: '2024-01-09 10:30:00' },
],
},
{
id: 'sig-2',
name: '【客户服务】',
application: '通知服务',
accent: 'blue',
mobile: 'approved',
unicom: 'pending',
telecom: 'approved',
drainage: [
{ id: 'drain-3', siteName: '客服入口', content: 'https://service.example.com', mobile: 'approved', unicom: 'pending', telecom: 'approved', submittedAt: '2024-01-10 09:12:00' },
],
},
{
id: 'sig-3',
name: '【验证码】',
application: '验证码',
accent: 'green',
mobile: 'approved',
unicom: 'approved',
telecom: 'approved',
drainage: [],
},
{
id: 'sig-4',
name: '【促销活动】',
application: '百<>会员推广',
accent: 'red',
mobile: 'rejected',
unicom: 'approved',
telecom: 'pending',
drainage: [
{ id: 'drain-4', siteName: '会员活动页', content: 'https://vip.example.com', mobile: 'rejected', unicom: 'approved', telecom: 'pending', submittedAt: '2024-01-11 13:42:00' },
],
},
{
id: 'sig-5',
name: '【会员中心】',
application: '会员服务',
accent: 'gray',
mobile: 'pending',
unicom: 'pending',
telecom: 'pending',
drainage: [],
},
];
function UploadBox({ label, compact = false }: { label?: string; compact?: boolean }) {
return (
<div className={compact ? 'signature-upload signature-upload--compact' : 'signature-upload'}>
{label ? <span>{label}</span> : null}
<Upload size={compact ? 30 : 42} />
<strong>{compact ? '上传文件' : '点击上传 或拖拽文件到此处'}</strong>
{!compact ? <small> PNGJPGJPEG 3M</small> : null}
</div>
);
}
function SignatureForm({ signature }: { signature?: SignatureItem }) {
return (
<div className="signature-form">
<section>
<h3></h3>
<div className="signature-alert">
<Info size={18} />
<span>使PNGJPG或JPEG格式的正版文件3M</span>
</div>
<div className="signature-form-grid">
<Select label="* 签名依据" options={[{ label: '请选择签名依据', value: '' }, { label: '企事业单位证明', value: 'company' }]} defaultValue={signature ? 'company' : ''} />
<Input label="* 短信签名" defaultValue={signature?.name ?? ''} placeholder="请输入短信签名,如【XXXX公司】" />
</div>
<UploadBox label="* 资质凭证" />
</section>
<section>
<h3></h3>
<div className="signature-form-grid">
<Input label="* 公司名称" defaultValue={signature?.application ?? ''} placeholder="请输入公司名称" />
<Input label="* 统一社会信用代码" placeholder="请输入统一社会信用代码" />
<Input label="* 法人姓名" placeholder="请输入法人姓名" />
<Input label="法人身份证号" placeholder="请输入法人身份证号" />
<UploadBox compact label="法人身份证照片-人像面" />
<UploadBox compact label="法人身份证照片-国徽面" />
</div>
</section>
<section>
<h3></h3>
<div className="signature-form-grid">
<Input label="* 责任人姓名" placeholder="请输入责任人姓名" />
<Input label="* 责任人手机号" placeholder="请输入责任人手机号" />
<Input label="* 责任人身份证号" placeholder="请输入责任人身份证号" />
<Input label="责任人邮箱" placeholder="请输入责任人邮箱" />
<UploadBox compact label="责任人身份证照片-人像面" />
<UploadBox compact label="责任人身份证照片-国徽面" />
</div>
</section>
</div>
);
}
export function ClientSignaturesPage() {
const [signatures, setSignatures] = useState(initialSignatures);
const [keyword, setKeyword] = useState('');
const [expandedId, setExpandedId] = useState('sig-1');
const [signatureModal, setSignatureModal] = useState<{ mode: 'add' | 'edit'; signature?: SignatureItem } | null>(null);
const [editingDrainage, setEditingDrainage] = useState<{ signature: SignatureItem; drainage?: DrainageInfo } | null>(null);
const filteredSignatures = signatures.filter((item) => (
item.name.includes(keyword) || item.application.includes(keyword)
));
function deleteSignature(id: string) {
setSignatures((items) => items.filter((item) => item.id !== id));
}
function deleteDrainage(signatureId: string, drainageId: string) {
setSignatures((items) => items.map((item) => (
item.id === signatureId ? { ...item, drainage: item.drainage.filter((drainage) => drainage.id !== drainageId) } : item
)));
}
return (
<section className="page-stack">
<div className="signature-page-header">
<div className="sms-send-title">
<span className="sms-send-title__icon">
<FilePenLine size={22} />
</span>
<h1></h1>
</div>
<Button icon={<Plus size={17} />} onClick={() => setSignatureModal({ mode: 'add' })}></Button>
</div>
<div className="signature-search-row">
<Input
onChange={(event) => setKeyword(event.target.value)}
placeholder="搜索签名名称或用途"
prefix={<Search size={17} />}
value={keyword}
/>
</div>
<div className="signature-list">
{filteredSignatures.map((signature) => {
const expanded = expandedId === signature.id;
return (
<article className={`signature-card signature-card--${signature.accent}`} key={signature.id}>
<div className="signature-summary">
<button aria-label="展开签名" onClick={() => setExpandedId(expanded ? '' : signature.id)} type="button">
{expanded ? <ChevronDown size={18} /> : <ChevronRight size={18} />}
</button>
<div>
<span></span>
<strong>{signature.name}</strong>
</div>
<div>
<span></span>
<strong>{signature.application}</strong>
</div>
<div>
<span></span>
<Tag tone={statusToneMap[signature.mobile]}>{statusLabelMap[signature.mobile]}</Tag>
</div>
<div>
<span></span>
<Tag tone={statusToneMap[signature.unicom]}>{statusLabelMap[signature.unicom]}</Tag>
</div>
<div>
<span></span>
<Tag tone={statusToneMap[signature.telecom]}>{statusLabelMap[signature.telecom]}</Tag>
</div>
<div>
<span></span>
<strong>{signature.drainage.length} </strong>
</div>
<div className="signature-actions">
<Button icon={<Edit3 size={16} />} onClick={() => setSignatureModal({ mode: 'edit', signature })} size="sm" variant="ghost"></Button>
<Button icon={<Trash2 size={16} />} onClick={() => deleteSignature(signature.id)} size="sm" variant="ghost"></Button>
</div>
</div>
{expanded ? (
<div className="drainage-panel">
<h2></h2>
<div className="drainage-table">
<div className="drainage-table__head">
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
{signature.drainage.map((item) => (
<div className="drainage-table__row" key={item.id}>
<strong>{item.siteName}</strong>
<a href={item.content}>{item.content}</a>
<Tag tone={statusToneMap[item.mobile]}>{statusLabelMap[item.mobile]}</Tag>
<Tag tone={statusToneMap[item.unicom]}>{statusLabelMap[item.unicom]}</Tag>
<Tag tone={statusToneMap[item.telecom]}>{statusLabelMap[item.telecom]}</Tag>
<span className="muted">{item.submittedAt}</span>
<span className="drainage-row-actions">
<button onClick={() => setEditingDrainage({ signature, drainage: item })} type="button"></button>
<button onClick={() => deleteDrainage(signature.id, item.id)} type="button"></button>
</span>
</div>
))}
</div>
<div className="drainage-panel__footer">
<Button icon={<Plus size={16} />} onClick={() => setEditingDrainage({ signature })} size="sm" variant="ghost">
</Button>
</div>
</div>
) : null}
</article>
);
})}
</div>
<Modal
footer={
<>
<Button variant="ghost" onClick={() => setSignatureModal(null)}></Button>
<Button onClick={() => setSignatureModal(null)}></Button>
</>
}
onClose={() => setSignatureModal(null)}
open={Boolean(signatureModal)}
size="xl"
title={<div className="signature-modal-title"><h2>{signatureModal?.mode === 'edit' ? '编辑签名' : '添加签名'}</h2><p></p></div>}
>
<SignatureForm signature={signatureModal?.signature} />
</Modal>
<Modal
footer={
<>
<Button variant="ghost" onClick={() => setEditingDrainage(null)}></Button>
<Button onClick={() => setEditingDrainage(null)}></Button>
</>
}
onClose={() => setEditingDrainage(null)}
open={Boolean(editingDrainage)}
size="xl"
title={<div className="signature-modal-title"><h2></h2><p>{editingDrainage?.signature.name}</p></div>}
>
{editingDrainage ? (
<div className="signature-form">
<section>
<h3></h3>
<Input label="* 引流信息:" defaultValue={editingDrainage.drainage?.content ?? 'https://www.example.com'} />
<div className="signature-alert">
<Info size={18} />
<span>1 使2 PNGJPG或JPEG格式的正版文件3M3 pdf格式或者图片10M</span>
</div>
<div className="signature-form-grid">
<UploadBox compact label="* 字段名称1" />
<Input label="* 字段名称2" defaultValue={editingDrainage.drainage?.siteName ?? '官方网站'} />
<Input label="* 字段名称3" placeholder="请输入公司名称" />
<Input label="* 字段名称4" placeholder="请输入统一社会信用代码" />
<Input label="* 字段名称5" placeholder="请输入法人姓名" />
<Input label="* 字段名称6" placeholder="请输入法人身份证号" />
<div className="signature-file-line">
<span>7</span>
<Button size="sm"></Button>
<small></small>
</div>
<Input label="* 字段名称8" placeholder="请输入责任人身份证号" />
<Input label="* 字段名称9" placeholder="请输入责任人姓名" />
<Input label="* 字段名称10" placeholder="请输入责任人手机号" />
</div>
</section>
</div>
) : null}
</Modal>
</section>
);
}