Initial CMPP frontend prototype
This commit is contained in:
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { AdminOverview, AuditItem, Channel, Customer } from '@/mock/types';
|
||||
|
||||
export const adminOverview: AdminOverview = {
|
||||
todaySubmissions: 128,
|
||||
pendingAudits: 36,
|
||||
averageWaitMinutes: 12,
|
||||
channelHealth: 99.2,
|
||||
};
|
||||
|
||||
export const auditItems: AuditItem[] = [
|
||||
{
|
||||
id: 'AUD-2401',
|
||||
customer: '上海云舟科技',
|
||||
type: '模板',
|
||||
content: '验证码 ${code},5 分钟内有效。',
|
||||
risk: 'low',
|
||||
status: 'pending',
|
||||
submittedAt: '2026-06-16 09:12',
|
||||
},
|
||||
{
|
||||
id: 'AUD-2402',
|
||||
customer: '杭州星澜商贸',
|
||||
type: '签名',
|
||||
content: '星澜会员中心',
|
||||
risk: 'medium',
|
||||
status: 'pending',
|
||||
submittedAt: '2026-06-16 09:04',
|
||||
},
|
||||
{
|
||||
id: 'AUD-2403',
|
||||
customer: '深圳北辰出行',
|
||||
type: '模板',
|
||||
content: '您的优惠券已到账,点击链接查看。',
|
||||
risk: 'high',
|
||||
status: 'pending',
|
||||
submittedAt: '2026-06-16 08:46',
|
||||
},
|
||||
];
|
||||
|
||||
export const customers: Customer[] = [
|
||||
{ id: 'CUS-1001', name: '上海云舟科技', contact: '赵先生', balance: 286420, status: 'active' },
|
||||
{ id: 'CUS-1002', name: '杭州星澜商贸', contact: '王女士', balance: 94220, status: 'active' },
|
||||
{ id: 'CUS-1003', name: '深圳北辰出行', contact: '陈先生', balance: 0, status: 'suspended' },
|
||||
];
|
||||
|
||||
export const channels: Channel[] = [
|
||||
{ id: 'CH-01', name: '华东主通道', region: '华东', successRate: 99.1, latencyMs: 128, enabled: true },
|
||||
{ id: 'CH-02', name: '华南通道', region: '华南', successRate: 98.6, latencyMs: 164, enabled: true },
|
||||
{ id: 'CH-03', name: '备用通道', region: '全国', successRate: 97.8, latencyMs: 210, enabled: true },
|
||||
];
|
||||
@@ -0,0 +1,48 @@
|
||||
import { adminOverview, auditItems, channels, customers } from '@/mock/adminData';
|
||||
import { readLocalData, writeLocalData } from '@/mock/storage';
|
||||
import type { AuditItem, AuditStatus, Channel, Customer } from '@/mock/types';
|
||||
|
||||
const keys = {
|
||||
audits: 'admin:audits',
|
||||
customers: 'admin:customers',
|
||||
channels: 'admin:channels',
|
||||
};
|
||||
|
||||
export const adminService = {
|
||||
getOverview() {
|
||||
return adminOverview;
|
||||
},
|
||||
|
||||
getAudits(status?: AuditStatus) {
|
||||
const records = readLocalData<AuditItem[]>(keys.audits, auditItems);
|
||||
return status ? records.filter((item) => item.status === status) : records;
|
||||
},
|
||||
|
||||
updateAuditStatus(id: string, status: AuditStatus) {
|
||||
const records = readLocalData<AuditItem[]>(keys.audits, auditItems);
|
||||
const nextRecords = records.map((item) => (item.id === id ? { ...item, status } : item));
|
||||
writeLocalData(keys.audits, nextRecords);
|
||||
return nextRecords;
|
||||
},
|
||||
|
||||
getCustomers() {
|
||||
return readLocalData<Customer[]>(keys.customers, customers);
|
||||
},
|
||||
|
||||
getChannels() {
|
||||
return readLocalData<Channel[]>(keys.channels, channels);
|
||||
},
|
||||
|
||||
saveChannels(nextChannels: Channel[]) {
|
||||
writeLocalData(keys.channels, nextChannels);
|
||||
},
|
||||
|
||||
toggleChannel(id: string) {
|
||||
const records = readLocalData<Channel[]>(keys.channels, channels);
|
||||
const nextRecords = records.map((item) => (
|
||||
item.id === id ? { ...item, enabled: !item.enabled } : item
|
||||
));
|
||||
writeLocalData(keys.channels, nextRecords);
|
||||
return nextRecords;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
export const hourlySendTrend = [
|
||||
{ time: '00:00', sent: 8200, success: 8020 },
|
||||
{ time: '03:00', sent: 5400, success: 5280 },
|
||||
{ time: '06:00', sent: 9600, success: 9410 },
|
||||
{ time: '09:00', sent: 28600, success: 28110 },
|
||||
{ time: '12:00', sent: 21400, success: 21030 },
|
||||
{ time: '15:00', sent: 24600, success: 24180 },
|
||||
{ time: '18:00', sent: 31800, success: 31260 },
|
||||
{ time: '21:00', sent: 17600, success: 17270 },
|
||||
];
|
||||
|
||||
export const channelShare = [
|
||||
{ name: '华东主通道', value: 46 },
|
||||
{ name: '华南通道', value: 31 },
|
||||
{ name: '备用通道', value: 23 },
|
||||
];
|
||||
|
||||
export const auditTrend = [
|
||||
{ day: '06-10', pending: 18, approved: 86, rejected: 7 },
|
||||
{ day: '06-11', pending: 24, approved: 92, rejected: 9 },
|
||||
{ day: '06-12', pending: 22, approved: 88, rejected: 6 },
|
||||
{ day: '06-13', pending: 29, approved: 104, rejected: 12 },
|
||||
{ day: '06-14', pending: 31, approved: 116, rejected: 10 },
|
||||
{ day: '06-15', pending: 27, approved: 108, rejected: 8 },
|
||||
{ day: '06-16', pending: 36, approved: 128, rejected: 11 },
|
||||
];
|
||||
|
||||
export const customerGrowth = [
|
||||
{ month: '1月', active: 42, new: 8 },
|
||||
{ month: '2月', active: 51, new: 9 },
|
||||
{ month: '3月', active: 63, new: 12 },
|
||||
{ month: '4月', active: 74, new: 11 },
|
||||
{ month: '5月', active: 86, new: 12 },
|
||||
{ month: '6月', active: 101, new: 15 },
|
||||
];
|
||||
@@ -0,0 +1,145 @@
|
||||
import type { BatchTask, BillingPlan, ClientOverview, Invoice, RecentMessage, Signature, SmsTemplate } from '@/mock/types';
|
||||
|
||||
export const clientOverview: ClientOverview = {
|
||||
availableMessages: 286420,
|
||||
todaySent: 74166,
|
||||
successRate: 98.7,
|
||||
pendingAudits: 12,
|
||||
};
|
||||
|
||||
export const recentMessages: RecentMessage[] = [
|
||||
{
|
||||
id: 'MSG-1024',
|
||||
scene: '登录验证码',
|
||||
count: 18240,
|
||||
channel: '华东主通道',
|
||||
status: 'success',
|
||||
createdAt: '2026-06-16 09:24',
|
||||
},
|
||||
{
|
||||
id: 'MSG-1023',
|
||||
scene: '营销活动通知',
|
||||
count: 46800,
|
||||
channel: '备用通道',
|
||||
status: 'warning',
|
||||
createdAt: '2026-06-16 08:40',
|
||||
},
|
||||
{
|
||||
id: 'MSG-1022',
|
||||
scene: '订单状态提醒',
|
||||
count: 9126,
|
||||
channel: '华南通道',
|
||||
status: 'info',
|
||||
createdAt: '2026-06-15 18:12',
|
||||
},
|
||||
];
|
||||
|
||||
export const smsTemplates: SmsTemplate[] = [
|
||||
{
|
||||
id: 'TPL-301',
|
||||
name: '登录验证码',
|
||||
type: '验证码',
|
||||
content: '您的验证码为 ${code},5 分钟内有效。',
|
||||
status: 'approved',
|
||||
updatedAt: '2026-06-15 13:20',
|
||||
},
|
||||
{
|
||||
id: 'TPL-302',
|
||||
name: '订单发货提醒',
|
||||
type: '通知短信',
|
||||
content: '您的订单 ${orderNo} 已发货,请留意物流信息。',
|
||||
status: 'pending',
|
||||
updatedAt: '2026-06-16 08:18',
|
||||
},
|
||||
{
|
||||
id: 'TPL-303',
|
||||
name: '会员权益通知',
|
||||
type: '营销短信',
|
||||
content: '会员专属权益已到账,登录小程序查看详情。',
|
||||
status: 'draft',
|
||||
updatedAt: '2026-06-14 17:05',
|
||||
},
|
||||
];
|
||||
|
||||
export const signatures: Signature[] = [
|
||||
{ id: 'SIG-101', name: '云舟科技', company: '上海云舟科技有限公司', status: 'approved' },
|
||||
{ id: 'SIG-102', name: '星澜会员中心', company: '杭州星澜商贸有限公司', status: 'pending' },
|
||||
{ id: 'SIG-103', name: '北辰出行', company: '深圳北辰出行服务有限公司', status: 'rejected' },
|
||||
];
|
||||
|
||||
export const billingPlans: BillingPlan[] = [
|
||||
{ id: 'PLAN-01', name: '基础包', messages: 10000, price: 680 },
|
||||
{ id: 'PLAN-02', name: '增长包', messages: 50000, price: 2980, highlight: true },
|
||||
{ id: 'PLAN-03', name: '企业包', messages: 200000, price: 10800 },
|
||||
];
|
||||
|
||||
export const invoices: Invoice[] = [
|
||||
{ id: 'INV-20260601', title: '增长包充值', amount: 2980, messages: 50000, status: 'paid', createdAt: '2026-06-01 10:12' },
|
||||
{ id: 'INV-20260518', title: '基础包充值', amount: 680, messages: 10000, status: 'paid', createdAt: '2026-05-18 14:26' },
|
||||
{ id: 'INV-20260502', title: '企业包充值', amount: 10800, messages: 200000, status: 'pending', createdAt: '2026-05-02 09:48' },
|
||||
];
|
||||
|
||||
export const batchTasks: BatchTask[] = [
|
||||
{
|
||||
id: 'TASK20260312001',
|
||||
applicationName: '营销推广平台',
|
||||
submittedAt: '2026-03-12 10:30:15',
|
||||
templateContent: '【启瑞物业】尊敬的业主,您本月物业费500元,请及时缴纳。感谢您的配合!',
|
||||
phoneCount: 1500,
|
||||
wordCount: 45,
|
||||
sendType: 'immediate',
|
||||
sentCount: 1500,
|
||||
totalCount: 1500,
|
||||
status: 'completed',
|
||||
},
|
||||
{
|
||||
id: 'TASK20260312002',
|
||||
applicationName: '客服系统',
|
||||
submittedAt: '2026-03-12 11:15:30',
|
||||
templateContent: '【客服中心】尊敬的张三,您已成功预约上门维修服务,时间:2026-03-15 14:00,地点:XX小区1号楼。',
|
||||
phoneCount: 300,
|
||||
wordCount: 58,
|
||||
sendType: 'scheduled',
|
||||
scheduledAt: '2026-03-13 09:00:00',
|
||||
sentCount: 158,
|
||||
totalCount: 300,
|
||||
status: 'sending',
|
||||
},
|
||||
{
|
||||
id: 'TASK20260312003',
|
||||
applicationName: '验证码服务',
|
||||
submittedAt: '2026-03-12 14:20:45',
|
||||
templateContent: '【验证码】您的验证码是123456,5分钟内有效,请勿泄露给他人。',
|
||||
phoneCount: 5000,
|
||||
wordCount: 34,
|
||||
sendType: 'immediate',
|
||||
sentCount: 3200,
|
||||
totalCount: 5000,
|
||||
status: 'sending',
|
||||
},
|
||||
{
|
||||
id: 'TASK20260311001',
|
||||
applicationName: '营销推广平台',
|
||||
submittedAt: '2026-03-11 16:45:00',
|
||||
templateContent: '【启瑞物业】您好!春季业主大会将于2026-03-20在小区会议室举行,欢迎参加。咨询电话:400-8888-8888',
|
||||
phoneCount: 2000,
|
||||
wordCount: 62,
|
||||
sendType: 'scheduled',
|
||||
scheduledAt: '2026-03-12 08:00:00',
|
||||
sentCount: 2000,
|
||||
totalCount: 2000,
|
||||
status: 'completed',
|
||||
},
|
||||
{
|
||||
id: 'TASK20260311002',
|
||||
applicationName: '营销推广平台',
|
||||
submittedAt: '2026-03-11 09:30:20',
|
||||
templateContent: '【启瑞物业】尊敬的客户,值此佳节之际,祝您节日快乐,阖家幸福!',
|
||||
phoneCount: 800,
|
||||
wordCount: 38,
|
||||
sendType: 'immediate',
|
||||
sentCount: 500,
|
||||
totalCount: 800,
|
||||
status: 'terminated',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,70 @@
|
||||
import { batchTasks, billingPlans, clientOverview, invoices, recentMessages, signatures, smsTemplates } from '@/mock/clientData';
|
||||
import { readLocalData, writeLocalData } from '@/mock/storage';
|
||||
import type { BatchTask, RecentMessage, Signature, SmsTemplate } from '@/mock/types';
|
||||
|
||||
const keys = {
|
||||
recentMessages: 'client:recent-messages',
|
||||
templates: 'client:templates',
|
||||
signatures: 'client:signatures',
|
||||
batchTasks: 'client:batch-tasks',
|
||||
};
|
||||
|
||||
export const clientService = {
|
||||
getOverview() {
|
||||
return clientOverview;
|
||||
},
|
||||
|
||||
getRecentMessages() {
|
||||
return readLocalData<RecentMessage[]>(keys.recentMessages, recentMessages);
|
||||
},
|
||||
|
||||
saveRecentMessages(nextMessages: RecentMessage[]) {
|
||||
writeLocalData(keys.recentMessages, nextMessages);
|
||||
},
|
||||
|
||||
addRecentMessage(message: RecentMessage) {
|
||||
const records = readLocalData<RecentMessage[]>(keys.recentMessages, recentMessages);
|
||||
const nextRecords = [message, ...records];
|
||||
writeLocalData(keys.recentMessages, nextRecords);
|
||||
return nextRecords;
|
||||
},
|
||||
|
||||
getTemplates() {
|
||||
return readLocalData<SmsTemplate[]>(keys.templates, smsTemplates);
|
||||
},
|
||||
|
||||
saveTemplates(nextTemplates: SmsTemplate[]) {
|
||||
writeLocalData(keys.templates, nextTemplates);
|
||||
},
|
||||
|
||||
getSignatures() {
|
||||
return readLocalData<Signature[]>(keys.signatures, signatures);
|
||||
},
|
||||
|
||||
saveSignatures(nextSignatures: Signature[]) {
|
||||
writeLocalData(keys.signatures, nextSignatures);
|
||||
},
|
||||
|
||||
getBillingPlans() {
|
||||
return billingPlans;
|
||||
},
|
||||
|
||||
getInvoices() {
|
||||
return invoices;
|
||||
},
|
||||
|
||||
getBatchTasks() {
|
||||
return readLocalData<BatchTask[]>(keys.batchTasks, batchTasks);
|
||||
},
|
||||
|
||||
terminateBatchTask(id: string) {
|
||||
const records = readLocalData<BatchTask[]>(keys.batchTasks, batchTasks);
|
||||
const nextRecords = records.map((item) => (
|
||||
item.id === id && item.status === 'sending'
|
||||
? { ...item, status: 'terminated' as const }
|
||||
: item
|
||||
));
|
||||
writeLocalData(keys.batchTasks, nextRecords);
|
||||
return nextRecords;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
export { adminService } from './adminService';
|
||||
export { clientService } from './clientService';
|
||||
export type {
|
||||
AdminOverview,
|
||||
AuditItem,
|
||||
AuditStatus,
|
||||
AuditType,
|
||||
BatchSendType,
|
||||
BatchTask,
|
||||
BatchTaskStatus,
|
||||
BillingPlan,
|
||||
Channel,
|
||||
ClientOverview,
|
||||
Customer,
|
||||
Invoice,
|
||||
MessageStatus,
|
||||
RecentMessage,
|
||||
RiskLevel,
|
||||
Signature,
|
||||
SmsTemplate,
|
||||
TemplateStatus,
|
||||
} from './types';
|
||||
@@ -0,0 +1,35 @@
|
||||
const STORAGE_PREFIX = 'cmpp-platform-v2';
|
||||
|
||||
export function readLocalData<T>(key: string, fallback: T): T {
|
||||
if (typeof window === 'undefined') {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const rawValue = window.localStorage.getItem(`${STORAGE_PREFIX}:${key}`);
|
||||
|
||||
if (!rawValue) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(rawValue) as T;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
export function writeLocalData<T>(key: string, value: T) {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
window.localStorage.setItem(`${STORAGE_PREFIX}:${key}`, JSON.stringify(value));
|
||||
}
|
||||
|
||||
export function resetLocalData(key: string) {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
window.localStorage.removeItem(`${STORAGE_PREFIX}:${key}`);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
export type MessageStatus = 'success' | 'warning' | 'info' | 'danger';
|
||||
|
||||
export type RecentMessage = {
|
||||
id: string;
|
||||
scene: string;
|
||||
count: number;
|
||||
channel: string;
|
||||
status: MessageStatus;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
export type ClientOverview = {
|
||||
availableMessages: number;
|
||||
todaySent: number;
|
||||
successRate: number;
|
||||
pendingAudits: number;
|
||||
};
|
||||
|
||||
export type TemplateStatus = 'draft' | 'pending' | 'approved' | 'rejected';
|
||||
|
||||
export type SmsTemplate = {
|
||||
id: string;
|
||||
name: string;
|
||||
type: '验证码' | '通知短信' | '营销短信';
|
||||
content: string;
|
||||
status: TemplateStatus;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type Signature = {
|
||||
id: string;
|
||||
name: string;
|
||||
company: string;
|
||||
status: TemplateStatus;
|
||||
};
|
||||
|
||||
export type RiskLevel = 'low' | 'medium' | 'high';
|
||||
export type AuditType = '模板' | '签名';
|
||||
export type AuditStatus = 'pending' | 'approved' | 'rejected';
|
||||
|
||||
export type AuditItem = {
|
||||
id: string;
|
||||
customer: string;
|
||||
type: AuditType;
|
||||
content: string;
|
||||
risk: RiskLevel;
|
||||
status: AuditStatus;
|
||||
submittedAt: string;
|
||||
};
|
||||
|
||||
export type AdminOverview = {
|
||||
todaySubmissions: number;
|
||||
pendingAudits: number;
|
||||
averageWaitMinutes: number;
|
||||
channelHealth: number;
|
||||
};
|
||||
|
||||
export type Customer = {
|
||||
id: string;
|
||||
name: string;
|
||||
contact: string;
|
||||
balance: number;
|
||||
status: 'active' | 'suspended';
|
||||
};
|
||||
|
||||
export type Channel = {
|
||||
id: string;
|
||||
name: string;
|
||||
region: string;
|
||||
successRate: number;
|
||||
latencyMs: number;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
export type BillingPlan = {
|
||||
id: string;
|
||||
name: string;
|
||||
messages: number;
|
||||
price: number;
|
||||
highlight?: boolean;
|
||||
};
|
||||
|
||||
export type Invoice = {
|
||||
id: string;
|
||||
title: string;
|
||||
amount: number;
|
||||
messages: number;
|
||||
status: 'paid' | 'pending' | 'failed';
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
export type BatchTaskStatus = 'completed' | 'sending' | 'terminated';
|
||||
export type BatchSendType = 'immediate' | 'scheduled';
|
||||
|
||||
export type BatchTask = {
|
||||
id: string;
|
||||
applicationName: string;
|
||||
submittedAt: string;
|
||||
templateContent: string;
|
||||
phoneCount: number;
|
||||
wordCount: number;
|
||||
sendType: BatchSendType;
|
||||
scheduledAt?: string;
|
||||
sentCount: number;
|
||||
totalCount: number;
|
||||
status: BatchTaskStatus;
|
||||
};
|
||||
Reference in New Issue
Block a user