From b2e42ebd825d833d12c059068ab5c5d1160608d3 Mon Sep 17 00:00:00 2001 From: hectorzhao Date: Tue, 30 Jun 2026 17:24:12 +0800 Subject: [PATCH] Split customer management modules --- src/apps/admin/AdminCustomersPage.tsx | 69 +- .../admin/AdminEnterpriseApplicationsPage.tsx | 189 ++++++ .../admin/AdminEnterpriseSignaturesPage.tsx | 606 +++++++++++++++++ .../admin/AdminEnterpriseTemplatesPage.tsx | 630 ++++++++++++++++++ src/layouts/AdminLayout.tsx | 12 +- src/routes/AppRoutes.tsx | 10 + src/styles/global.css | 79 +++ 7 files changed, 1587 insertions(+), 8 deletions(-) create mode 100644 src/apps/admin/AdminEnterpriseApplicationsPage.tsx create mode 100644 src/apps/admin/AdminEnterpriseSignaturesPage.tsx create mode 100644 src/apps/admin/AdminEnterpriseTemplatesPage.tsx diff --git a/src/apps/admin/AdminCustomersPage.tsx b/src/apps/admin/AdminCustomersPage.tsx index 0140f6f..7cfaa8d 100644 --- a/src/apps/admin/AdminCustomersPage.tsx +++ b/src/apps/admin/AdminCustomersPage.tsx @@ -1,22 +1,52 @@ import { useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { Building2, DollarSign, Plus, TrendingDown, TrendingUp } from 'lucide-react'; -import { Button, Input, Select, Table, Tag, type TableColumn } from '@/components/ui'; +import { Building2, DollarSign, Plus, Trash2, TrendingDown, TrendingUp } from 'lucide-react'; +import { Button, Input, Modal, Select, Table, Tag, type TableColumn } from '@/components/ui'; import { formatCurrency, getEnterpriseRecords, + saveEnterpriseRecords, statusOptions, toggleEnterpriseStatus, type EnterpriseRecord, } from './adminEnterpriseMock'; -export function AdminCustomersPage() { +type AdminCustomersPageProps = { + basePath?: string; +}; + +function ConfirmModal({ message, onCancel, onConfirm }: { message: string; onCancel: () => void; onConfirm: () => void }) { + return ( + + + + + )} + onClose={onCancel} + open + title="操作确认" + > +

{message}

+
+ ); +} + +export function AdminCustomersPage({ basePath = '/admin/customers' }: AdminCustomersPageProps) { const navigate = useNavigate(); const [records, setRecords] = useState(() => getEnterpriseRecords()); const [queryId, setQueryId] = useState(''); const [queryName, setQueryName] = useState(''); const [queryStatus, setQueryStatus] = useState('all'); const [filters, setFilters] = useState({ id: '', name: '', status: 'all' }); + const [confirmAction, setConfirmAction] = useState<{ type: 'toggle' | 'delete'; record: EnterpriseRecord } | null>(null); + + function deleteEnterprise(id: string) { + const nextRecords = records.filter((record) => record.id !== id); + saveEnterpriseRecords(nextRecords); + setRecords(nextRecords); + } const filteredRecords = useMemo(() => records.filter((record) => { const matchId = filters.id ? record.id.includes(filters.id) : true; @@ -61,26 +91,34 @@ export function AdminCustomersPage() { render: (record) => (
+
), }, @@ -93,7 +131,7 @@ export function AdminCustomersPage() {

管理

企业管理

- @@ -186,6 +224,23 @@ export function AdminCustomersPage() { + + {confirmAction ? ( + setConfirmAction(null)} + onConfirm={() => { + if (confirmAction.type === 'delete') { + deleteEnterprise(confirmAction.record.id); + } else { + setRecords(toggleEnterpriseStatus(confirmAction.record.id)); + } + setConfirmAction(null); + }} + /> + ) : null} ); } diff --git a/src/apps/admin/AdminEnterpriseApplicationsPage.tsx b/src/apps/admin/AdminEnterpriseApplicationsPage.tsx new file mode 100644 index 0000000..c264c64 --- /dev/null +++ b/src/apps/admin/AdminEnterpriseApplicationsPage.tsx @@ -0,0 +1,189 @@ +import { useMemo, useState } from 'react'; +import { Edit3, Plus, Trash2 } from 'lucide-react'; +import { useNavigate } from 'react-router-dom'; +import { Button, Modal, Table, Tabs, Tag, type TableColumn } from '@/components/ui'; + +type SmsApp = { + id: string; + name: string; + enterprise: string; + appId: string; + enabled: boolean; + sentToday: number; + deliveryRate: number; + unitPrice: number; + cmppStatus: 'connected' | 'disconnected' | 'inactive'; +}; + +type MmsApp = Omit & { + pointPrice: number; +}; + +type AppKind = 'sms' | 'mms'; + +const initialSmsApps: SmsApp[] = [ + { id: 'app-1', name: '营销推广平台', enterprise: '上海XXXXX科技有限公司', appId: 'AK_2024010912345678', enabled: true, sentToday: 1500, deliveryRate: 95, unitPrice: 0.05, cmppStatus: 'connected' }, + { id: 'app-2', name: '客户服务系统', enterprise: '重庆进载数智', appId: 'AK_2024010987654321', enabled: true, sentToday: 800, deliveryRate: 90, unitPrice: 0.06, cmppStatus: 'disconnected' }, + { id: 'app-3', name: '验证码服务', enterprise: '超感世纪互三网', appId: 'AK_2024010811223344', enabled: false, sentToday: 0, deliveryRate: 0, unitPrice: 0.04, cmppStatus: 'inactive' }, +]; + +const initialMmsApps: MmsApp[] = [ + { id: 'mms-app-1', name: '营销活动彩信', enterprise: '上海XXXXX科技有限公司', appId: 'MMS_2024020112345678', enabled: true, sentToday: 320, deliveryRate: 92, unitPrice: 0.15, pointPrice: 50 }, + { id: 'mms-app-2', name: '节日祝福彩信', enterprise: '重庆进载数智', appId: 'MMS_2024020187654321', enabled: true, sentToday: 180, deliveryRate: 88, unitPrice: 0.12, pointPrice: 30 }, + { id: 'mms-app-3', name: '会员权益彩信', enterprise: '四川骠骑企业管理', appId: 'MMS_2024020199001122', enabled: false, sentToday: 0, deliveryRate: 0, unitPrice: 0.18, pointPrice: 60 }, +]; + +function enabledTag(enabled: boolean) { + return {enabled ? '启用' : '停用'}; +} + +function ConfirmModal({ message, danger, onCancel, onConfirm }: { message: string; danger?: boolean; onCancel: () => void; onConfirm: () => void }) { + return ( + + + + + )} + onClose={onCancel} + open + title="操作确认" + > +

{message}

+
+ ); +} + +export function AdminEnterpriseApplicationsPage() { + const navigate = useNavigate(); + const [smsApps, setSmsApps] = useState(initialSmsApps); + const [mmsApps, setMmsApps] = useState(initialMmsApps); + const [confirmAction, setConfirmAction] = useState< + | { action: 'toggle'; kind: AppKind; id: string; name: string; enabled: boolean } + | { action: 'delete'; kind: AppKind; id: string; name: string } + | null + >(null); + + function confirmToggle(kind: AppKind, id: string) { + if (kind === 'sms') { + setSmsApps((current) => current.map((item) => item.id === id ? { ...item, enabled: !item.enabled } : item)); + return; + } + + setMmsApps((current) => current.map((item) => item.id === id ? { ...item, enabled: !item.enabled } : item)); + } + + function confirmDelete(kind: AppKind, id: string) { + if (kind === 'sms') { + setSmsApps((current) => current.filter((item) => item.id !== id)); + } else { + setMmsApps((current) => current.filter((item) => item.id !== id)); + } + } + + function runConfirmedAction() { + if (!confirmAction) { + return; + } + if (confirmAction.action === 'toggle') { + confirmToggle(confirmAction.kind, confirmAction.id); + } else { + confirmDelete(confirmAction.kind, confirmAction.id); + } + setConfirmAction(null); + } + + const smsColumns = useMemo>>(() => [ + { key: 'name', title: '应用名称', width: '180px', render: (record) => {record.name} }, + { key: 'enterprise', title: '企业名称', width: '220px', render: (record) => record.enterprise }, + { key: 'appId', title: 'AppID', width: '220px', render: (record) => record.appId }, + { key: 'sentToday', title: '今日发送', width: '120px', render: (record) => `${record.sentToday.toLocaleString('zh-CN')} 条` }, + { key: 'deliveryRate', title: '到达率', width: '110px', render: (record) => `${record.deliveryRate}%` }, + { key: 'unitPrice', title: '单价', width: '100px', render: (record) => `${record.unitPrice.toFixed(3)} 元` }, + { + key: 'cmppStatus', + title: 'CMPP状态', + width: '130px', + render: (record) => ( + + {record.cmppStatus === 'connected' ? '已连接' : record.cmppStatus === 'disconnected' ? '已断开' : '未开通'} + + ), + }, + { key: 'enabled', title: '状态', width: '100px', render: (record) => enabledTag(record.enabled) }, + { + key: 'actions', + title: '操作', + align: 'right', + width: '190px', + render: (record) => ( +
+ + + +
+ ), + }, + ], [navigate]); + + const mmsColumns = useMemo>>(() => [ + { key: 'name', title: '应用名称', width: '180px', render: (record) => {record.name} }, + { key: 'enterprise', title: '企业名称', width: '220px', render: (record) => record.enterprise }, + { key: 'appId', title: 'AppID', width: '220px', render: (record) => record.appId }, + { key: 'sentToday', title: '今日发送', width: '120px', render: (record) => `${record.sentToday.toLocaleString('zh-CN')} 条` }, + { key: 'deliveryRate', title: '到达率', width: '110px', render: (record) => `${record.deliveryRate}%` }, + { key: 'unitPrice', title: '单价', width: '100px', render: (record) => `${record.unitPrice.toFixed(3)} 元` }, + { key: 'pointPrice', title: '点数', width: '100px', render: (record) => `${record.pointPrice} 分` }, + { key: 'enabled', title: '状态', width: '100px', render: (record) => enabledTag(record.enabled) }, + { + key: 'actions', + title: '操作', + align: 'right', + width: '190px', + render: (record) => ( +
+ + + +
+ ), + }, + ], [navigate]); + + return ( +
+
+
+
客户管理 / 企业应用管理
+

企业应用管理

+
+ +
+ +
+ }, + { label: '彩信应用', value: 'mms', content:
}, + ]} + /> + + + {confirmAction ? ( + setConfirmAction(null)} + onConfirm={runConfirmedAction} + /> + ) : null} + + ); +} diff --git a/src/apps/admin/AdminEnterpriseSignaturesPage.tsx b/src/apps/admin/AdminEnterpriseSignaturesPage.tsx new file mode 100644 index 0000000..cfe4660 --- /dev/null +++ b/src/apps/admin/AdminEnterpriseSignaturesPage.tsx @@ -0,0 +1,606 @@ +import { useMemo, useState } from 'react'; +import { ChevronDown, ChevronLeft, ChevronRight, Edit3, FileText, Info, Plus, Search, Trash2, Upload } from 'lucide-react'; +import { Button, Input, Modal, Select, Table, Tabs, Tag, Textarea, type TableColumn } from '@/components/ui'; + +type CarrierStatus = 'approved' | 'pending' | 'rejected' | 'filing'; +type SignatureKind = 'sms' | 'mms'; + +type DrainageInfo = { + id: string; + siteName: string; + url: string; + mobile: CarrierStatus; + unicom: CarrierStatus; + telecom: CarrierStatus; + submittedAt: string; + remark: string; +}; + +type SignatureItem = { + id: string; + name: string; + enterprise: string; + application: string; + mobile: CarrierStatus; + unicom: CarrierStatus; + telecom: CarrierStatus; + drainage?: DrainageInfo[]; + updatedAt: string; +}; + +const statusLabelMap: Record = { + approved: '已通过', + pending: '审核中', + rejected: '已驳回', + filing: '待报备', +}; + +const statusToneMap: Record = { + approved: 'success', + pending: 'info', + rejected: 'danger', + filing: 'neutral', +}; + +const statusOptions = [ + { label: '已通过', value: 'approved' }, + { label: '审核中', value: 'pending' }, + { label: '已驳回', value: 'rejected' }, + { label: '待报备', value: 'filing' }, +]; + +const initialSmsSignatures: SignatureItem[] = [ + { + id: 'sig-1', + name: '【科技公司】', + enterprise: '上海XXXXX科技有限公司', + application: '营销推广平台', + mobile: 'approved', + unicom: 'approved', + telecom: 'approved', + updatedAt: '2026-01-08 11:00:00', + drainage: [ + { id: 'drain-1', siteName: '官网入口', url: 'https://www.example.com', mobile: 'approved', unicom: 'approved', telecom: 'approved', submittedAt: '2026-01-08 11:00:00', remark: '官网首页引流链接,三网报备通过。' }, + { id: 'drain-2', siteName: '促销活动页', url: 'https://sale.example.com', mobile: 'approved', unicom: 'pending', telecom: 'pending', submittedAt: '2026-01-09 10:30:00', remark: '活动页待联通、电信回执。' }, + ], + }, + { + id: 'sig-2', + name: '【客户服务】', + enterprise: '重庆进载数智', + application: '客户服务系统', + mobile: 'approved', + unicom: 'pending', + telecom: 'approved', + updatedAt: '2026-01-09 14:20:00', + drainage: [ + { id: 'drain-3', siteName: '客户服务中心', url: 'https://service.example.com', mobile: 'approved', unicom: 'filing', telecom: 'pending', submittedAt: '2026-01-09 14:20:00', remark: '服务入口链接报备中。' }, + ], + }, + { + id: 'sig-3', + name: '【促销活动】', + enterprise: '超感世纪互三网', + application: '营销推广平台', + mobile: 'rejected', + unicom: 'approved', + telecom: 'pending', + updatedAt: '2026-01-07 10:00:00', + drainage: [ + { id: 'drain-4', siteName: '促销专区', url: 'https://sale.example.com', mobile: 'rejected', unicom: 'approved', telecom: 'approved', submittedAt: '2026-01-07 10:00:00', remark: '移动侧驳回,需补充页面备案信息。' }, + ], + }, +]; + +const initialMmsSignatures: SignatureItem[] = [ + { id: 'mms-sig-1', name: '【活动推广】', enterprise: '上海XXXXX科技有限公司', application: '营销活动彩信', mobile: 'approved', unicom: 'approved', telecom: 'approved', updatedAt: '2026-01-12 09:10:00' }, + { id: 'mms-sig-2', name: '【节日祝福】', enterprise: '重庆进载数智', application: '节日祝福彩信', mobile: 'approved', unicom: 'pending', telecom: 'approved', updatedAt: '2026-01-13 16:35:00' }, + { id: 'mms-sig-3', name: '【优品发布】', enterprise: '四川骠骑企业管理', application: '营销活动彩信', mobile: 'pending', unicom: 'pending', telecom: 'pending', updatedAt: '2026-01-15 10:28:00' }, +]; + +function StatusTag({ status }: { status: CarrierStatus }) { + return {statusLabelMap[status]}; +} + +function paginate(items: T[], page: number, pageSize: number) { + return items.slice((page - 1) * pageSize, page * pageSize); +} + +function Pagination({ page, pageSize, total, onPageChange }: { page: number; pageSize: number; total: number; onPageChange: (page: number) => void }) { + const totalPages = Math.max(1, Math.ceil(total / pageSize)); + return ( +
+ {pageSize}条/页 + + + / {totalPages} + +
+ ); +} + +function ConfirmModal({ message, onCancel, onConfirm }: { message: string; onCancel: () => void; onConfirm: () => void }) { + return ( + + + + + )} + onClose={onCancel} + open + title="删除确认" + > +

{message}

+
+ ); +} + +function SignatureUploadBox({ label, compact = false }: { label: string; compact?: boolean }) { + return ( +
+ {label} + + {compact ? '上传文件' : '点击上传 或拖拽文件到此处'} + {!compact ? 支持 PNG、JPG、JPEG 格式,大小不超过 3M : null} +
+ ); +} + +function SignatureFormModal({ + item, + kind, + onClose, + onSubmit, +}: { + item?: SignatureItem; + kind: SignatureKind; + onClose: () => void; + onSubmit: (item: SignatureItem) => void; +}) { + const [name, setName] = useState(item?.name ?? ''); + const [enterprise, setEnterprise] = useState(item?.enterprise ?? ''); + const [application, setApplication] = useState(item?.application ?? ''); + const [mobile, setMobile] = useState(item?.mobile ?? 'filing'); + const [unicom, setUnicom] = useState(item?.unicom ?? 'filing'); + const [telecom, setTelecom] = useState(item?.telecom ?? 'filing'); + + function submit() { + onSubmit({ + id: item?.id ?? `${kind}-sig-${Date.now()}`, + name, + enterprise, + application, + mobile, + unicom, + telecom, + drainage: kind === 'sms' ? (item?.drainage ?? []) : undefined, + updatedAt: '2026-06-30 10:00:00', + }); + } + + return ( + + + + + )} + onClose={onClose} + open + size="xl" + title={( +
+

{item ? '编辑签名' : '添加签名'}

+

{item ? '修改短信签名的相关信息' : '新增短信签名的相关信息'}

+
+ )} + > +
+
+

基本信息

+
+ + 签名需履行报备,并遵照管理部门审核结果方可使用。请用 PNG、JPG 或 JPEG 格式的正版文件,且大小不超过 3M。 +
+
+ setName(event.target.value)} placeholder="请输入短信签名,如【XXXX公司】" value={name} /> +
+ +
+ +
+

公司信息

+
+ setEnterprise(event.target.value)} placeholder="请输入公司名称" value={enterprise} /> + + + + + +
+
+ +
+

责任人信息

+
+ setApplication(event.target.value)} placeholder="请输入应用名称" value={application} /> + + + + +
+
+ +
+

三网报备状态

+
+ setUnicom(event.target.value as CarrierStatus)} options={statusOptions} value={unicom} /> + update('url', event.target.value)} + placeholder="请输入引流网址" + value={form.url} + /> +
+ +
    +
  1. 本页面中所填的信息需与短信内容应用所包含的网站或服务保持一致;
  2. +
  3. 图片仅支持 PNG、JPG 或 JPEG 格式的正版文件,且大小不超过 3M;
  4. +
  5. 文件格式支持 pdf 格式或者图片,且大小不超过 10M。
  6. +
+
+
+ + + update('siteName', event.target.value)} placeholder="请输入公司名称" value={form.siteName} /> + + + +
+ 字段名称7: +
+ + 未选择文件 +
+
+ + + + update('unicom', event.target.value as CarrierStatus)} options={statusOptions} value={form.unicom} /> + update('submittedAt', event.target.value)} value={form.submittedAt} /> +