完善运营端报备流程与页面标题
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { BarChart3 } from 'lucide-react';
|
||||
import { Button, Chart, Tag } from '@/components/ui';
|
||||
import { Breadcrumb, Button, Chart, Tag } from '@/components/ui';
|
||||
import { auditTrend, channelShare, customerGrowth, hourlySendTrend } from '@/mock/chartData';
|
||||
import { adminService } from '@/mock';
|
||||
import { createBarOption, createLineOption, createPieOption } from '@/theme/chartOptions';
|
||||
@@ -49,8 +49,7 @@ export function AdminAnalyticsPage() {
|
||||
<section className="page-stack">
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<p className="eyebrow">运营</p>
|
||||
<h1>数据统计</h1>
|
||||
<Breadcrumb items={['数据统计']} />
|
||||
</div>
|
||||
<Button icon={<BarChart3 size={16} />} variant="ghost">导出报表</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Table, Tag, type TableColumn } from '@/components/ui';
|
||||
import { Breadcrumb, Table, Tag, type TableColumn } from '@/components/ui';
|
||||
import { adminService, type Customer } from '@/mock';
|
||||
|
||||
const columns: Array<TableColumn<Customer>> = [
|
||||
@@ -26,8 +26,7 @@ export function AdminBillingPage() {
|
||||
<section className="page-stack">
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<p className="eyebrow">配置</p>
|
||||
<h1>账单流水</h1>
|
||||
<Breadcrumb items={['账单流水']} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="surface">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Info, Plus, Trash2 } from 'lucide-react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { Button, Input, Modal, Select, Table, Tag } from '@/components/ui';
|
||||
import { Breadcrumb, Button, Input, Modal, Select, Table, Tag } from '@/components/ui';
|
||||
import type { TableColumn } from '@/components/ui';
|
||||
|
||||
type Carrier = 'mobile' | 'unicom' | 'telecom';
|
||||
@@ -212,8 +212,7 @@ export function AdminChannelGroupFormPage() {
|
||||
<div className="page-stack channel-group-form-page">
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<div className="eyebrow">通道管理 / 通道组 / {editing ? '编辑通道组' : '添加通道组'}</div>
|
||||
<h1>{editing ? '编辑短信通道组' : '添加短信通道组'}</h1>
|
||||
<Breadcrumb items={[editing ? '编辑短信通道组' : '添加短信通道组']} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Edit3, Layers3, Plus, Search, Trash2, UsersRound } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Button, Input, Pagination } from '@/components/ui';
|
||||
import { Breadcrumb, Button, Input, Pagination } from '@/components/ui';
|
||||
|
||||
type ChannelGroup = {
|
||||
id: string;
|
||||
@@ -105,8 +105,7 @@ export function AdminChannelGroupsPage() {
|
||||
<div className="page-stack sms-channel-group-page">
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<div className="eyebrow">通道管理 / 短信通道组管理</div>
|
||||
<h1>短信通道组管理</h1>
|
||||
<Breadcrumb items={['短信通道组管理']} />
|
||||
</div>
|
||||
<Button icon={<Plus size={16} />} onClick={() => navigate('/admin/channel-groups/new')}>
|
||||
添加通道组
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
ChevronUp,
|
||||
Eye,
|
||||
FileSliders,
|
||||
FileUp,
|
||||
GripVertical,
|
||||
Pencil,
|
||||
Plus,
|
||||
@@ -14,7 +15,7 @@ import {
|
||||
Trash2,
|
||||
} from 'lucide-react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { Button, DateRangeInput, Input, Modal, Select, Tag, Textarea } from '@/components/ui';
|
||||
import { Breadcrumb, Button, DateRangeInput, Input, Modal, Select, Tag, Textarea } from '@/components/ui';
|
||||
import type { DateRangeValue } from '@/components/ui';
|
||||
|
||||
type ReportStatus = 'success' | 'failed' | 'reporting' | 'unreported' | 'withdrawn' | 'abandoned';
|
||||
@@ -129,11 +130,34 @@ const drainageFieldPool: ReportField[] = [
|
||||
{ id: 'authorization', label: '授权证明', type: '文件' },
|
||||
];
|
||||
|
||||
const signatureFieldPool: ReportField[] = [
|
||||
{ id: 'signatureName', label: '短信签名', type: '文本' },
|
||||
{ id: 'signatureBasis', label: '签名依据', type: '文本' },
|
||||
{ id: 'qualificationFile', label: '资质凭证', type: '文件' },
|
||||
{ id: 'companyName', label: '公司名称', type: '文本' },
|
||||
{ id: 'creditCode', label: '统一社会信用代码', type: '文本' },
|
||||
{ id: 'legalName', label: '法人姓名', type: '文本' },
|
||||
{ id: 'legalId', label: '法人身份证号', type: '文本' },
|
||||
{ id: 'legalIdFront', label: '法人身份证人像面', type: '图片' },
|
||||
{ id: 'legalIdBack', label: '法人身份证国徽面', type: '图片' },
|
||||
{ id: 'managerName', label: '责任人姓名', type: '文本' },
|
||||
{ id: 'managerPhone', label: '责任人手机号', type: '文本' },
|
||||
{ id: 'managerId', label: '责任人身份证号', type: '文本' },
|
||||
{ id: 'authorization', label: '授权委托书', type: '文件' },
|
||||
];
|
||||
|
||||
const initialSelectedDrainageFields: SelectedReportField[] = [
|
||||
{ id: 'companyName', label: '公司名称', type: '文本', required: false, mapping: '' },
|
||||
{ id: 'legalId', label: '法人身份证号', type: '文本', required: false, mapping: '' },
|
||||
];
|
||||
|
||||
const initialSelectedSignatureFields: SelectedReportField[] = [
|
||||
{ id: 'signatureName', label: '短信签名', type: '文本', required: true, mapping: 'sign_name' },
|
||||
{ id: 'qualificationFile', label: '资质凭证', type: '文件', required: true, mapping: 'license_file' },
|
||||
{ id: 'companyName', label: '公司名称', type: '文本', required: true, mapping: 'enterprise_name' },
|
||||
{ id: 'creditCode', label: '统一社会信用代码', type: '文本', required: true, mapping: 'credit_code' },
|
||||
];
|
||||
|
||||
const emptyStats: DeliveryStats = {
|
||||
successRate: 0,
|
||||
successCount: 0,
|
||||
@@ -269,19 +293,25 @@ function SignatureDetailModal({ report, onClose }: { report: SignatureReport; on
|
||||
|
||||
function DrainageFieldConfigModal({
|
||||
fields,
|
||||
fieldPool,
|
||||
onChange,
|
||||
onClose,
|
||||
title = '配置报备字段',
|
||||
description = '从字段池中选择字段,并配置是否必填',
|
||||
}: {
|
||||
fields: SelectedReportField[];
|
||||
fieldPool: ReportField[];
|
||||
onChange: (fields: SelectedReportField[]) => void;
|
||||
onClose: () => void;
|
||||
title?: string;
|
||||
description?: string;
|
||||
}) {
|
||||
const [draft, setDraft] = useState(fields);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const availableFields = useMemo(() => drainageFieldPool.filter((field) => (
|
||||
const availableFields = useMemo(() => fieldPool.filter((field) => (
|
||||
!draft.some((item) => item.id === field.id)
|
||||
&& (!searchText || field.label.includes(searchText))
|
||||
)), [draft, searchText]);
|
||||
)), [draft, fieldPool, searchText]);
|
||||
|
||||
function addField(field: ReportField) {
|
||||
setDraft((items) => [...items, { ...field, required: false, mapping: '' }]);
|
||||
@@ -312,7 +342,7 @@ function DrainageFieldConfigModal({
|
||||
onClose={onClose}
|
||||
open
|
||||
size="xl"
|
||||
title={<div className="channel-field-config-title"><h2>配置报备字段</h2><p>从字段池中选择字段,并配置是否必填</p></div>}
|
||||
title={<div className="channel-field-config-title"><h2>{title}</h2><p>{description}</p></div>}
|
||||
>
|
||||
<div className="channel-field-config">
|
||||
<section className="channel-field-pool">
|
||||
@@ -366,6 +396,33 @@ function DrainageFieldConfigModal({
|
||||
);
|
||||
}
|
||||
|
||||
function ReceiptImportModal({ onClose, onSubmit }: { onClose: () => void; onSubmit: () => void }) {
|
||||
return (
|
||||
<Modal
|
||||
footer={<><Button onClick={onClose} variant="ghost">取消</Button><Button onClick={onSubmit}>确认导入</Button></>}
|
||||
onClose={onClose}
|
||||
open
|
||||
size="xl"
|
||||
title={<div className="template-modal-title"><h2>导入报备回执</h2><p>同步运营商返回的签名和引流信息报备状态。</p></div>}
|
||||
>
|
||||
<div className="report-receipt-modal">
|
||||
<div className="report-upload-drop">
|
||||
<FileUp size={38} />
|
||||
<strong>选择回执文件</strong>
|
||||
<span>支持 Excel、CSV。导入后会按签名、引流内容和通道匹配当前报备记录。</span>
|
||||
</div>
|
||||
<div className="report-receipt-preview">
|
||||
<h3>匹配预览</h3>
|
||||
<div><span>可更新签名</span><strong>12 条</strong></div>
|
||||
<div><span>可更新引流信息</span><strong>5 条</strong></div>
|
||||
<div><span>需人工确认</span><strong>2 条</strong></div>
|
||||
</div>
|
||||
<Textarea label="导入备注" placeholder="记录运营商工单号、回执来源或人工说明" rows={4} />
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export function AdminChannelReportPage() {
|
||||
const navigate = useNavigate();
|
||||
const { channelId = '88827' } = useParams();
|
||||
@@ -380,7 +437,10 @@ export function AdminChannelReportPage() {
|
||||
const [nextRemark, setNextRemark] = useState('');
|
||||
const [detail, setDetail] = useState<ReportDetail | null>(null);
|
||||
const [fieldConfigOpen, setFieldConfigOpen] = useState(false);
|
||||
const [signatureFieldConfigOpen, setSignatureFieldConfigOpen] = useState(false);
|
||||
const [receiptOpen, setReceiptOpen] = useState(false);
|
||||
const [drainageFields, setDrainageFields] = useState(initialSelectedDrainageFields);
|
||||
const [signatureFields, setSignatureFields] = useState(initialSelectedSignatureFields);
|
||||
|
||||
const filteredReports = useMemo(() => reports.filter((report) => {
|
||||
const matchesKeyword = !keyword || report.name.includes(keyword) || report.drainage.some((item) => item.value.includes(keyword));
|
||||
@@ -433,14 +493,15 @@ export function AdminChannelReportPage() {
|
||||
return (
|
||||
<section className="page-stack channel-report-page">
|
||||
<div className="surface channel-report-hero">
|
||||
<div className="breadcrumb-line"><span>短信通道管理</span><span>/</span><strong>通道报备详情</strong></div>
|
||||
<Breadcrumb items={[channelNames[channelId] ?? `短信通道 ${channelId}`]} />
|
||||
<div className="channel-report-heading">
|
||||
<Button icon={<ChevronLeft size={16} />} onClick={() => navigate('/admin/channels')} variant="ghost">返回列表</Button>
|
||||
<h1>{channelNames[channelId] ?? `短信通道 ${channelId}`}</h1>
|
||||
<Button icon={<ChevronRight size={16} />} variant="ghost">下一个</Button>
|
||||
<div className="channel-report-config-actions">
|
||||
<Button icon={<FileUp size={16} />} onClick={() => setReceiptOpen(true)} variant="secondary">导入回执</Button>
|
||||
<Button icon={<Settings2 size={16} />} onClick={() => setFieldConfigOpen(true)} variant="ghost">个性化引流信息报备字段</Button>
|
||||
<Button icon={<FileSliders size={16} />} variant="ghost">个性化签名报备字段</Button>
|
||||
<Button icon={<FileSliders size={16} />} onClick={() => setSignatureFieldConfigOpen(true)} variant="ghost">个性化签名报备字段</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -543,12 +604,28 @@ export function AdminChannelReportPage() {
|
||||
|
||||
{fieldConfigOpen ? (
|
||||
<DrainageFieldConfigModal
|
||||
description="这些字段会驱动客户端引流资料补充,并映射到当前通道的导出模板。"
|
||||
fieldPool={drainageFieldPool}
|
||||
fields={drainageFields}
|
||||
onChange={setDrainageFields}
|
||||
onClose={() => setFieldConfigOpen(false)}
|
||||
title="个性化引流信息报备字段"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{signatureFieldConfigOpen ? (
|
||||
<DrainageFieldConfigModal
|
||||
description="这些字段会驱动客户端签名资料补充,并映射到当前通道的签名报备导出模板。"
|
||||
fieldPool={signatureFieldPool}
|
||||
fields={signatureFields}
|
||||
onChange={setSignatureFields}
|
||||
onClose={() => setSignatureFieldConfigOpen(false)}
|
||||
title="个性化签名报备字段"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{receiptOpen ? <ReceiptImportModal onClose={() => setReceiptOpen(false)} onSubmit={() => setReceiptOpen(false)} /> : null}
|
||||
|
||||
{detail?.kind === 'drainage' ? (
|
||||
<Modal footer={<Button onClick={() => setDetail(null)} variant="ghost">关闭</Button>} onClose={() => setDetail(null)} open size="md" title="引流信息报备详情">
|
||||
<div className="channel-report-detail">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Eye, Info, Pencil, Plus, Power, Search, Send, Trash2 } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Button, Input, Modal, Select, Tag, Textarea } from '@/components/ui';
|
||||
import { Breadcrumb, Button, Input, Modal, Select, Tag, Textarea } from '@/components/ui';
|
||||
|
||||
type Carrier = 'mobile' | 'unicom' | 'telecom';
|
||||
type ChannelStatus = 'normal' | 'stopped' | 'connecting' | 'failed';
|
||||
@@ -394,7 +394,7 @@ export function AdminChannelsPage() {
|
||||
return (
|
||||
<section className="page-stack sms-channel-page">
|
||||
<div className="page-heading">
|
||||
<div className="breadcrumb-line"><strong>短信通道管理</strong></div>
|
||||
<Breadcrumb items={['短信通道管理']} />
|
||||
<Button icon={<Plus size={16} />} onClick={() => setModal({ mode: 'create' })}>添加通道</Button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { ImagePlus } from 'lucide-react';
|
||||
import { Button, Input, Select, Textarea } from '@/components/ui';
|
||||
import { Breadcrumb, Button, Input, Select, Textarea } from '@/components/ui';
|
||||
import {
|
||||
cityOptionsByProvince,
|
||||
createEnterprise,
|
||||
@@ -83,8 +83,7 @@ export function AdminCustomerFormPage() {
|
||||
<section className="page-stack enterprise-form-page">
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<p className="eyebrow">企业管理 / {isEdit ? '编辑企业' : '创建企业'}</p>
|
||||
<h1>企业信息</h1>
|
||||
<Breadcrumb items={[isEdit ? '编辑企业' : '创建企业']} />
|
||||
<p>填写企业基本信息和联系人信息,用于运营端企业档案管理。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Building2, DollarSign, Plus, Trash2, TrendingDown, TrendingUp } from 'lucide-react';
|
||||
import { Button, Input, Modal, Select, Table, Tag, type TableColumn } from '@/components/ui';
|
||||
import { Breadcrumb, Button, Input, Modal, Select, Table, Tag, type TableColumn } from '@/components/ui';
|
||||
import {
|
||||
formatCurrency,
|
||||
getEnterpriseRecords,
|
||||
@@ -128,8 +128,7 @@ export function AdminCustomersPage({ basePath = '/admin/customers' }: AdminCusto
|
||||
<section className="page-stack">
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<p className="eyebrow">管理</p>
|
||||
<h1>企业管理</h1>
|
||||
<Breadcrumb items={['企业管理']} />
|
||||
</div>
|
||||
<Button icon={<Plus size={16} />} onClick={() => navigate(`${basePath}/new`)}>
|
||||
添加企业
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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';
|
||||
import { Breadcrumb, Button, Input, Modal, Select, Table, Textarea, Tag, type TableColumn } from '@/components/ui';
|
||||
|
||||
type DrainageFieldType = '字符串' | '整数' | '文件' | '网址' | '电话' | '日期';
|
||||
|
||||
@@ -166,7 +166,7 @@ export function AdminDrainageFieldsPage() {
|
||||
<section className="page-stack admin-system-page admin-drainage-page">
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<div className="breadcrumb-line">基础配置 / <strong>引流信息报备字段库</strong></div>
|
||||
<Breadcrumb items={['基础配置', '引流信息报备字段库']} />
|
||||
<h1>引流信息报备字段库</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Edit3, Plus, Trash2 } from 'lucide-react';
|
||||
import { Edit3, Plus, Search, Trash2 } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Button, Modal, Table, Tabs, Tag, type TableColumn } from '@/components/ui';
|
||||
import { Breadcrumb, Button, Input, Modal, Table, Tabs, Tag, type TableColumn } from '@/components/ui';
|
||||
|
||||
type SmsApp = {
|
||||
id: string;
|
||||
@@ -59,6 +59,7 @@ export function AdminEnterpriseApplicationsPage() {
|
||||
const navigate = useNavigate();
|
||||
const [smsApps, setSmsApps] = useState(initialSmsApps);
|
||||
const [mmsApps, setMmsApps] = useState(initialMmsApps);
|
||||
const [enterpriseKeyword, setEnterpriseKeyword] = useState('');
|
||||
const [confirmAction, setConfirmAction] = useState<
|
||||
| { action: 'toggle'; kind: AppKind; id: string; name: string; enabled: boolean }
|
||||
| { action: 'delete'; kind: AppKind; id: string; name: string }
|
||||
@@ -94,6 +95,16 @@ export function AdminEnterpriseApplicationsPage() {
|
||||
setConfirmAction(null);
|
||||
}
|
||||
|
||||
const filteredSmsApps = useMemo(
|
||||
() => smsApps.filter((item) => !enterpriseKeyword || item.enterprise.includes(enterpriseKeyword)),
|
||||
[enterpriseKeyword, smsApps],
|
||||
);
|
||||
|
||||
const filteredMmsApps = useMemo(
|
||||
() => mmsApps.filter((item) => !enterpriseKeyword || item.enterprise.includes(enterpriseKeyword)),
|
||||
[enterpriseKeyword, mmsApps],
|
||||
);
|
||||
|
||||
const smsColumns = useMemo<Array<TableColumn<SmsApp>>>(() => [
|
||||
{ key: 'name', title: '应用名称', width: '180px', render: (record) => <strong>{record.name}</strong> },
|
||||
{ key: 'enterprise', title: '企业名称', width: '220px', render: (record) => record.enterprise },
|
||||
@@ -159,17 +170,28 @@ export function AdminEnterpriseApplicationsPage() {
|
||||
<section className="page-stack admin-customer-split-page">
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<div className="breadcrumb-line">客户管理 / <strong>企业应用管理</strong></div>
|
||||
<Breadcrumb items={['客户管理', '企业应用管理']} />
|
||||
<h1>企业应用管理</h1>
|
||||
</div>
|
||||
<Button icon={<Plus size={16} />} onClick={() => navigate('/admin/customers/2763/sms-apps/new')}>添加应用</Button>
|
||||
</div>
|
||||
|
||||
<div className="surface admin-split-filter">
|
||||
<Input
|
||||
label="企业名称"
|
||||
onChange={(event) => setEnterpriseKeyword(event.target.value)}
|
||||
placeholder="请输入企业名称"
|
||||
prefix={<Search size={16} />}
|
||||
value={enterpriseKeyword}
|
||||
/>
|
||||
<Button onClick={() => setEnterpriseKeyword('')} variant="ghost">重置</Button>
|
||||
</div>
|
||||
|
||||
<div className="surface section-stack">
|
||||
<Tabs
|
||||
items={[
|
||||
{ label: '短信应用', value: 'sms', content: <Table columns={smsColumns} data={smsApps} rowKey="id" /> },
|
||||
{ label: '彩信应用', value: 'mms', content: <Table columns={mmsColumns} data={mmsApps} rowKey="id" /> },
|
||||
{ label: '短信应用', value: 'sms', content: <Table columns={smsColumns} data={filteredSmsApps} rowKey="id" /> },
|
||||
{ label: '彩信应用', value: 'mms', content: <Table columns={mmsColumns} data={filteredMmsApps} rowKey="id" /> },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Check, FileSearch, Search, X } from 'lucide-react';
|
||||
import { Button, Input, Select, Table, Tag, type TableColumn } from '@/components/ui';
|
||||
import { Breadcrumb, Button, Input, Select, Table, Tag, type TableColumn } from '@/components/ui';
|
||||
|
||||
type EnterpriseAuditStatus = 'pending' | 'approved' | 'rejected';
|
||||
|
||||
@@ -90,7 +90,7 @@ export function AdminEnterpriseAuditPage() {
|
||||
|
||||
return (
|
||||
<section className="page-stack admin-audit-page">
|
||||
<div className="breadcrumb-line"><span>审核中心</span><span>/</span><strong>企业认证审核</strong></div>
|
||||
<Breadcrumb items={['审核中心', '企业认证审核']} />
|
||||
<div className="surface audit-filter-card">
|
||||
<div className="audit-filter-grid audit-filter-grid--enterprise">
|
||||
<Input label="企业名称/信用代码" onChange={(event) => setKeyword(event.target.value)} placeholder="请输入企业名称或统一社会信用代码" value={keyword} />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Trash2 } from 'lucide-react';
|
||||
import { Button, Table, type TableColumn } from '@/components/ui';
|
||||
import { Breadcrumb, Button, Table, type TableColumn } from '@/components/ui';
|
||||
|
||||
type EnterpriseBlacklistItem = {
|
||||
id: string;
|
||||
@@ -46,7 +46,7 @@ export function AdminEnterpriseBlacklistPage() {
|
||||
<section className="page-stack admin-security-page">
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<div className="breadcrumb-line">安全控制 / <strong>企业黑名单</strong></div>
|
||||
<Breadcrumb items={['安全控制', '企业黑名单']} />
|
||||
<h1>企业黑名单</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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';
|
||||
import { Breadcrumb, Button, Input, Modal, Select, Table, Tabs, Tag, Textarea, type TableColumn } from '@/components/ui';
|
||||
|
||||
type CarrierStatus = 'approved' | 'pending' | 'rejected' | 'filing';
|
||||
type SignatureKind = 'sms' | 'mms';
|
||||
@@ -545,7 +545,7 @@ export function AdminEnterpriseSignaturesPage() {
|
||||
<section className="page-stack admin-customer-split-page">
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<div className="breadcrumb-line">客户管理 / <strong>企业签名管理</strong></div>
|
||||
<Breadcrumb items={['客户管理', '企业签名管理']} />
|
||||
<h1>企业签名管理</h1>
|
||||
</div>
|
||||
<Button icon={<Plus size={16} />} onClick={() => setSignatureModal({ kind: activeTab })}>添加签名</Button>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { ChevronLeft, ChevronRight, Edit3, Eye, FileText, ImageIcon, Info, Music, Plus, Search, Trash2, Video } from 'lucide-react';
|
||||
import { Button, Input, Modal, Select, Tabs, Tag, Textarea } from '@/components/ui';
|
||||
import { Breadcrumb, Button, Input, Modal, Select, Tabs, Tag, Textarea } from '@/components/ui';
|
||||
|
||||
type TemplateKind = 'sms' | 'mms';
|
||||
|
||||
@@ -577,7 +577,7 @@ export function AdminEnterpriseTemplatesPage() {
|
||||
<section className="page-stack admin-customer-split-page">
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<div className="breadcrumb-line">客户管理 / <strong>企业模板管理</strong></div>
|
||||
<Breadcrumb items={['客户管理', '企业模板管理']} />
|
||||
<h1>企业模板管理</h1>
|
||||
</div>
|
||||
<Button icon={<Plus size={16} />} onClick={() => (activeTab === 'sms' ? setSmsModal(null) : setMmsModal(null))}>添加模板</Button>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Trash2 } from 'lucide-react';
|
||||
import { Button, Table, type TableColumn } from '@/components/ui';
|
||||
import { Breadcrumb, Button, Table, type TableColumn } from '@/components/ui';
|
||||
|
||||
type GlobalBlacklistItem = {
|
||||
id: string;
|
||||
@@ -42,7 +42,7 @@ export function AdminGlobalBlacklistPage() {
|
||||
<section className="page-stack admin-security-page">
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<div className="breadcrumb-line">安全控制 / <strong>全局黑名单</strong></div>
|
||||
<Breadcrumb items={['安全控制', '全局黑名单']} />
|
||||
<h1>全局黑名单</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Breadcrumb,
|
||||
Button,
|
||||
Chart,
|
||||
Modal,
|
||||
@@ -180,8 +181,7 @@ export function AdminHome() {
|
||||
<section className="page-stack admin-dashboard">
|
||||
<div className="overview-hero admin-dashboard-hero">
|
||||
<div>
|
||||
<p className="eyebrow">运营端</p>
|
||||
<h1>数据概览</h1>
|
||||
<Breadcrumb items={['数据概览']} />
|
||||
<p className="muted">按业务口径查看平台发送、签名、消费、审核和通道运行情况。</p>
|
||||
</div>
|
||||
<div className="page-actions">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { ArrowLeft, RefreshCw } from 'lucide-react';
|
||||
import { Button, Input, Select } from '@/components/ui';
|
||||
import { Breadcrumb, Button, Input, Select } from '@/components/ui';
|
||||
import { getEnterpriseRecords } from './adminEnterpriseMock';
|
||||
|
||||
const channelOptions = {
|
||||
@@ -63,8 +63,7 @@ export function AdminMmsApplicationFormPage() {
|
||||
<section className="page-stack admin-app-form-page">
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<p className="eyebrow">企业管理 / {isEdit ? '编辑彩信应用' : '添加彩信应用'}</p>
|
||||
<h1>{isEdit ? '编辑彩信应用' : '添加彩信应用'}</h1>
|
||||
<Breadcrumb items={[isEdit ? '编辑彩信应用' : '添加彩信应用']} />
|
||||
<p>{enterprise?.name ?? '当前企业'} 的彩信应用配置。</p>
|
||||
</div>
|
||||
<Button icon={<ArrowLeft size={16} />} onClick={goBack} variant="ghost">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Ban, Pencil, Plus, Power, Search, Trash2 } from 'lucide-react';
|
||||
import { Button, Input, Modal, Pagination, Select, Table, Tag, Textarea } from '@/components/ui';
|
||||
import { Breadcrumb, Button, Input, Modal, Pagination, Select, Table, Tag, Textarea } from '@/components/ui';
|
||||
import type { TableColumn } from '@/components/ui';
|
||||
|
||||
type Carrier = 'mobile' | 'unicom' | 'telecom';
|
||||
@@ -175,7 +175,7 @@ export function AdminMmsChannelsPage() {
|
||||
return (
|
||||
<section className="page-stack mms-channel-page">
|
||||
<div className="page-heading">
|
||||
<div><div className="breadcrumb-line"><strong>彩信通道管理</strong></div></div>
|
||||
<div><Breadcrumb items={['彩信通道管理']} /></div>
|
||||
<Button icon={<Plus size={16} />} onClick={() => setModal({ mode: 'create' })}>添加通道</Button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Download, Eye, Search, Smartphone } from 'lucide-react';
|
||||
import {
|
||||
Breadcrumb,
|
||||
Button,
|
||||
DateRangeInput,
|
||||
Input,
|
||||
@@ -225,7 +226,7 @@ export function AdminMmsRecordsPage() {
|
||||
<section className="page-stack admin-sms-records-page admin-mms-records-page">
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<div className="breadcrumb-line">数据详单 / <strong>彩信记录</strong></div>
|
||||
<Breadcrumb items={['数据详单', '彩信记录']} />
|
||||
<h1>彩信记录</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { BarChart3, CalendarClock, Eye, FileImage, ImageIcon, Search, TrendingUp } from 'lucide-react';
|
||||
import {
|
||||
Breadcrumb,
|
||||
Button,
|
||||
DateRangeInput,
|
||||
DetailInfoGrid,
|
||||
@@ -445,7 +446,7 @@ export function AdminMmsTaskProgressPage() {
|
||||
<section className="page-stack admin-mms-task-page">
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<div className="breadcrumb-line">发送任务 / <strong>彩信任务进度</strong></div>
|
||||
<Breadcrumb items={['发送任务', '彩信任务进度']} />
|
||||
<h1>彩信任务进度</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Activity } from 'lucide-react';
|
||||
import { Button, Table, Tag, type TableColumn } from '@/components/ui';
|
||||
import { Breadcrumb, Button, Table, Tag, type TableColumn } from '@/components/ui';
|
||||
import { adminService, type Channel } from '@/mock';
|
||||
|
||||
const columns: Array<TableColumn<Channel>> = [
|
||||
@@ -23,8 +23,7 @@ export function AdminMonitorPage() {
|
||||
<section className="page-stack">
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<p className="eyebrow">运营</p>
|
||||
<h1>发送监控</h1>
|
||||
<Breadcrumb items={['发送监控']} />
|
||||
</div>
|
||||
<Button icon={<Activity size={16} />} variant="ghost">实时刷新</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Plus, Search, Trash2 } from 'lucide-react';
|
||||
import { Button, Input, Modal, Select, Table, type TableColumn } from '@/components/ui';
|
||||
import { Breadcrumb, Button, Input, Modal, Select, Table, type TableColumn } from '@/components/ui';
|
||||
|
||||
type PhoneSegment = {
|
||||
id: string;
|
||||
@@ -131,7 +131,7 @@ export function AdminPhoneSegmentsPage() {
|
||||
<section className="page-stack admin-system-page">
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<div className="breadcrumb-line">系统管理 / <strong>手机号段库</strong></div>
|
||||
<Breadcrumb items={['系统管理', '手机号段库']} />
|
||||
<h1>手机号段库</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { ChevronLeft, ChevronRight, Search } from 'lucide-react';
|
||||
import { Button, DateRangeInput, Input, Select, type DateRangeValue } from '@/components/ui';
|
||||
import { Breadcrumb, Button, DateRangeInput, Input, Select, type DateRangeValue } from '@/components/ui';
|
||||
|
||||
type RechargeRecord = {
|
||||
id: string;
|
||||
@@ -61,7 +61,7 @@ export function AdminRechargeRecordsPage() {
|
||||
<section className="page-stack admin-recharge-page">
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<div className="breadcrumb-line">数据详单 / <strong>充值记录</strong></div>
|
||||
<Breadcrumb items={['数据详单', '充值记录']} />
|
||||
<h1>充值记录</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Clock3, Eye, Search } from 'lucide-react';
|
||||
import { Breadcrumb, Button, DateRangeInput, Input, Modal, Select, Table, Tag, type DateRangeValue, type TableColumn } from '@/components/ui';
|
||||
|
||||
type RecordStatus = 'unreported' | 'reporting' | 'success' | 'failed' | 'withdrawn' | 'abandoned';
|
||||
|
||||
type ReportRecord = {
|
||||
id: string;
|
||||
taskId: string;
|
||||
channel: string;
|
||||
enterprise: string;
|
||||
type: '签名' | '引流信息';
|
||||
content: string;
|
||||
carrier: '移动' | '联通' | '电信';
|
||||
status: RecordStatus;
|
||||
submittedAt: string;
|
||||
reportedAt?: string;
|
||||
updatedAt: string;
|
||||
reason?: string;
|
||||
};
|
||||
|
||||
const statusMeta: Record<RecordStatus, { label: string; tone: 'neutral' | 'info' | 'success' | 'warning' | 'danger' }> = {
|
||||
unreported: { label: '未报备', tone: 'neutral' },
|
||||
reporting: { label: '报备中', tone: 'warning' },
|
||||
success: { label: '报备成功', tone: 'success' },
|
||||
failed: { label: '报备失败', tone: 'danger' },
|
||||
withdrawn: { label: '被清退', tone: 'danger' },
|
||||
abandoned: { label: '放弃报备', tone: 'warning' },
|
||||
};
|
||||
|
||||
const statusOptions = [
|
||||
{ label: '全部状态', value: 'all' },
|
||||
{ label: '未报备', value: 'unreported' },
|
||||
{ label: '报备中', value: 'reporting' },
|
||||
{ label: '报备成功', value: 'success' },
|
||||
{ label: '报备失败', value: 'failed' },
|
||||
{ label: '被清退', value: 'withdrawn' },
|
||||
{ label: '放弃报备', value: 'abandoned' },
|
||||
];
|
||||
|
||||
const carrierOptions = [
|
||||
{ label: '全部运营商', value: 'all' },
|
||||
{ label: '移动', value: '移动' },
|
||||
{ label: '联通', value: '联通' },
|
||||
{ label: '电信', value: '电信' },
|
||||
];
|
||||
|
||||
const initialRecords: ReportRecord[] = [
|
||||
{ id: 'RPT-REC-001', taskId: 'RPT-TASK-20260628-003', channel: '移动-行北-上海甲医院-38', enterprise: '超感世纪互三网', type: '签名', content: '【促销活动】', carrier: '移动', status: 'success', submittedAt: '2026-06-28 12:20:16', reportedAt: '2026-06-29 10:05:44', updatedAt: '2026-06-29 10:05:44' },
|
||||
{ id: 'RPT-REC-002', taskId: 'RPT-TASK-20260629-006', channel: '联通-行政-杭州甲医院-37', enterprise: '重庆进载数智', type: '引流信息', content: 'https://service.example.com', carrier: '联通', status: 'reporting', submittedAt: '2026-06-29 16:02:11', updatedAt: '2026-06-29 16:02:11' },
|
||||
{ id: 'RPT-REC-003', taskId: 'RPT-TASK-20260629-006', channel: '联通-行政-杭州甲医院-37', enterprise: '重庆进载数智', type: '签名', content: '【客户服务】', carrier: '联通', status: 'failed', submittedAt: '2026-06-29 16:02:11', reportedAt: '2026-06-30 09:12:35', updatedAt: '2026-06-30 09:12:35', reason: '责任人身份证照片不清晰。' },
|
||||
{ id: 'RPT-REC-004', taskId: 'RPT-TASK-20260630-001', channel: '行北-集市三甲医院-39', enterprise: '上海XXXXX科技有限公司', type: '签名', content: '【科技公司】', carrier: '移动', status: 'unreported', submittedAt: '2026-06-30 09:12:00', updatedAt: '2026-06-30 09:12:00' },
|
||||
];
|
||||
|
||||
function RecordDetailModal({ record, onClose }: { record: ReportRecord; onClose: () => void }) {
|
||||
return (
|
||||
<Modal footer={<Button onClick={onClose}>关闭</Button>} onClose={onClose} open size="xl" title={<div className="template-modal-title"><h2>报备记录详情</h2><p>{record.id}</p></div>}>
|
||||
<div className="report-record-detail">
|
||||
<div className="detail-grid">
|
||||
<div><span>任务编号</span><strong>{record.taskId}</strong></div>
|
||||
<div><span>通道</span><strong>{record.channel}</strong></div>
|
||||
<div><span>企业</span><strong>{record.enterprise}</strong></div>
|
||||
<div><span>报备对象</span><strong>{record.type} {record.content}</strong></div>
|
||||
<div><span>运营商</span><strong>{record.carrier}</strong></div>
|
||||
<div><span>当前状态</span><Tag tone={statusMeta[record.status].tone}>{statusMeta[record.status].label}</Tag></div>
|
||||
<div><span>提交时间</span><strong>{record.submittedAt}</strong></div>
|
||||
<div><span>回执时间</span><strong>{record.reportedAt ?? '-'}</strong></div>
|
||||
<div className="detail-grid__wide"><span>失败/备注原因</span><strong>{record.reason ?? '-'}</strong></div>
|
||||
</div>
|
||||
<section className="report-history">
|
||||
<h3><Clock3 size={17} />状态历史</h3>
|
||||
<div><span>{record.submittedAt}</span><strong>生成记录</strong><em>由报备任务生成并等待导出。</em></div>
|
||||
<div><span>{record.reportedAt ?? record.updatedAt}</span><strong>{statusMeta[record.status].label}</strong><em>{record.reason ?? '运营商回执同步。'}</em></div>
|
||||
</section>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export function AdminReportRecordsPage() {
|
||||
const [records] = useState(initialRecords);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [carrier, setCarrier] = useState('all');
|
||||
const [status, setStatus] = useState('all');
|
||||
const [dateRange, setDateRange] = useState<DateRangeValue>({});
|
||||
const [detail, setDetail] = useState<ReportRecord | null>(null);
|
||||
|
||||
const filteredRecords = useMemo(() => records.filter((record) => {
|
||||
const text = `${record.taskId}${record.channel}${record.enterprise}${record.content}`;
|
||||
const date = record.submittedAt.slice(0, 10);
|
||||
return (!keyword || text.includes(keyword))
|
||||
&& (carrier === 'all' || record.carrier === carrier)
|
||||
&& (status === 'all' || record.status === status)
|
||||
&& (!dateRange.start || date >= dateRange.start)
|
||||
&& (!dateRange.end || date <= dateRange.end);
|
||||
}), [carrier, dateRange.end, dateRange.start, keyword, records, status]);
|
||||
|
||||
const columns: Array<TableColumn<ReportRecord>> = [
|
||||
{ key: 'object', title: '报备对象', width: '260px', render: (record) => <div className="admin-task-enterprise"><strong>{record.content}</strong><span>{record.type} / {record.enterprise}</span></div> },
|
||||
{ key: 'task', title: '任务编号', width: '190px', render: (record) => <strong className="admin-task-id">{record.taskId}</strong> },
|
||||
{ key: 'channel', title: '通道', width: '230px', render: (record) => record.channel },
|
||||
{ key: 'carrier', title: '运营商', width: '90px', render: (record) => <Tag tone="info">{record.carrier}</Tag> },
|
||||
{ key: 'status', title: '状态', width: '110px', render: (record) => <Tag tone={statusMeta[record.status].tone}>{statusMeta[record.status].label}</Tag> },
|
||||
{ key: 'time', title: '时间', width: '190px', render: (record) => <div className="report-task-time"><span>提交 {record.submittedAt}</span><span>回执 {record.reportedAt ?? '-'}</span></div> },
|
||||
{ key: 'reason', title: '备注', render: (record) => record.reason ?? '-' },
|
||||
{ key: 'actions', title: '操作', align: 'right', width: '110px', render: (record) => <Button icon={<Eye size={14} />} onClick={() => setDetail(record)} size="sm" variant="ghost">详情</Button> },
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="page-stack admin-sms-task-page report-record-page">
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<Breadcrumb items={['报备任务', '报备记录']} />
|
||||
<h1>报备记录</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="surface admin-task-filter">
|
||||
<Input label="任务/通道/企业/签名" onChange={(event) => setKeyword(event.target.value)} placeholder="请输入关键字" value={keyword} />
|
||||
<Select label="运营商" onChange={(event) => setCarrier(event.target.value)} options={carrierOptions} value={carrier} />
|
||||
<Select label="报备状态" onChange={(event) => setStatus(event.target.value)} options={statusOptions} value={status} />
|
||||
<DateRangeInput label="提交时间" onChange={setDateRange} value={dateRange} />
|
||||
<div className="admin-task-filter__actions">
|
||||
<Button icon={<Search size={16} />}>查询</Button>
|
||||
<Button onClick={() => { setKeyword(''); setCarrier('all'); setStatus('all'); setDateRange({}); }} variant="ghost">重置</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="surface admin-task-table-card report-task-table-card">
|
||||
<Table columns={columns} data={filteredRecords} emptyText="暂无报备记录" rowKey="id" />
|
||||
</div>
|
||||
|
||||
{detail ? <RecordDetailModal onClose={() => setDetail(null)} record={detail} /> : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Download, Eye, FileUp, Plus, Search, Settings2 } from 'lucide-react';
|
||||
import { Breadcrumb, Button, DateRangeInput, Input, Modal, Select, Table, Tag, Textarea, type DateRangeValue, type TableColumn } from '@/components/ui';
|
||||
|
||||
type ReportTaskStatus = 'draft' | 'ready' | 'exported' | 'reporting' | 'partial' | 'completed' | 'failed';
|
||||
|
||||
type ReportTask = {
|
||||
id: string;
|
||||
channel: string;
|
||||
carrier: '移动' | '联通' | '电信';
|
||||
enterprise: string;
|
||||
signatureCount: number;
|
||||
drainageCount: number;
|
||||
missingCount: number;
|
||||
status: ReportTaskStatus;
|
||||
createdAt: string;
|
||||
exportedAt?: string;
|
||||
receiptAt?: string;
|
||||
owner: string;
|
||||
remark: string;
|
||||
};
|
||||
|
||||
const statusMeta: Record<ReportTaskStatus, { label: string; tone: 'neutral' | 'info' | 'success' | 'warning' | 'danger' }> = {
|
||||
draft: { label: '待生成', tone: 'neutral' },
|
||||
ready: { label: '待导出', tone: 'info' },
|
||||
exported: { label: '已导出', tone: 'warning' },
|
||||
reporting: { label: '报备中', tone: 'warning' },
|
||||
partial: { label: '部分完成', tone: 'info' },
|
||||
completed: { label: '已完成', tone: 'success' },
|
||||
failed: { label: '有失败', tone: 'danger' },
|
||||
};
|
||||
|
||||
const carrierOptions = [
|
||||
{ label: '全部运营商', value: 'all' },
|
||||
{ label: '移动', value: '移动' },
|
||||
{ label: '联通', value: '联通' },
|
||||
{ label: '电信', value: '电信' },
|
||||
];
|
||||
|
||||
const statusOptions = [
|
||||
{ label: '全部状态', value: 'all' },
|
||||
{ label: '待生成', value: 'draft' },
|
||||
{ label: '待导出', value: 'ready' },
|
||||
{ label: '已导出', value: 'exported' },
|
||||
{ label: '报备中', value: 'reporting' },
|
||||
{ label: '部分完成', value: 'partial' },
|
||||
{ label: '已完成', value: 'completed' },
|
||||
{ label: '有失败', value: 'failed' },
|
||||
];
|
||||
|
||||
const initialTasks: ReportTask[] = [
|
||||
{
|
||||
id: 'RPT-TASK-20260630-001',
|
||||
channel: '行北-集市三甲医院-39',
|
||||
carrier: '移动',
|
||||
enterprise: '上海XXXXX科技有限公司',
|
||||
signatureCount: 18,
|
||||
drainageCount: 9,
|
||||
missingCount: 0,
|
||||
status: 'ready',
|
||||
createdAt: '2026-06-30 09:12:00',
|
||||
owner: '运营',
|
||||
remark: '字段版本 V3,等待导出提交运营商。',
|
||||
},
|
||||
{
|
||||
id: 'RPT-TASK-20260629-006',
|
||||
channel: '联通-行政-杭州甲医院-37',
|
||||
carrier: '联通',
|
||||
enterprise: '重庆进载数智',
|
||||
signatureCount: 12,
|
||||
drainageCount: 4,
|
||||
missingCount: 2,
|
||||
status: 'exported',
|
||||
createdAt: '2026-06-29 15:24:00',
|
||||
exportedAt: '2026-06-29 16:02:11',
|
||||
owner: '李青',
|
||||
remark: '2 条引流链接缺 ICP 备案截图。',
|
||||
},
|
||||
{
|
||||
id: 'RPT-TASK-20260628-003',
|
||||
channel: '移动-行北-上海甲医院-38',
|
||||
carrier: '移动',
|
||||
enterprise: '超感世纪互三网',
|
||||
signatureCount: 7,
|
||||
drainageCount: 3,
|
||||
missingCount: 0,
|
||||
status: 'completed',
|
||||
createdAt: '2026-06-28 11:36:00',
|
||||
exportedAt: '2026-06-28 12:20:16',
|
||||
receiptAt: '2026-06-29 10:05:44',
|
||||
owner: '运营',
|
||||
remark: '已导入移动回执,全部通过。',
|
||||
},
|
||||
];
|
||||
|
||||
function StatusTag({ status }: { status: ReportTaskStatus }) {
|
||||
return <Tag tone={statusMeta[status].tone}>{statusMeta[status].label}</Tag>;
|
||||
}
|
||||
|
||||
function ReceiptImportModal({ onClose, onSubmit }: { onClose: () => void; onSubmit: () => void }) {
|
||||
return (
|
||||
<Modal
|
||||
footer={<><Button onClick={onClose} variant="ghost">取消</Button><Button onClick={onSubmit}>确认导入</Button></>}
|
||||
onClose={onClose}
|
||||
open
|
||||
size="xl"
|
||||
title={<div className="template-modal-title"><h2>导入报备回执</h2><p>上传运营商回执并预览状态匹配结果。</p></div>}
|
||||
>
|
||||
<div className="report-receipt-modal">
|
||||
<div className="report-upload-drop">
|
||||
<FileUp size={38} />
|
||||
<strong>选择回执文件</strong>
|
||||
<span>支持 Excel、CSV,字段包含签名、引流信息、通道状态和驳回原因。</span>
|
||||
</div>
|
||||
<div className="report-receipt-preview">
|
||||
<h3>导入预览</h3>
|
||||
<div><span>匹配成功</span><strong>26 条</strong></div>
|
||||
<div><span>待人工确认</span><strong>3 条</strong></div>
|
||||
<div><span>未匹配</span><strong>1 条</strong></div>
|
||||
</div>
|
||||
<Textarea label="导入备注" placeholder="记录回执来源、运营商工单号或人工处理说明" rows={4} />
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function GenerateTaskModal({ onClose, onSubmit }: { onClose: () => void; onSubmit: (task: ReportTask) => void }) {
|
||||
const [channel, setChannel] = useState('行北-集市三甲医院-39');
|
||||
const [carrier, setCarrier] = useState<'移动' | '联通' | '电信'>('移动');
|
||||
const [enterprise, setEnterprise] = useState('全部资料完整企业');
|
||||
|
||||
function submit() {
|
||||
onSubmit({
|
||||
id: `RPT-TASK-${Date.now()}`,
|
||||
channel,
|
||||
carrier,
|
||||
enterprise,
|
||||
signatureCount: 15,
|
||||
drainageCount: 6,
|
||||
missingCount: enterprise === '全部资料完整企业' ? 0 : 2,
|
||||
status: enterprise === '全部资料完整企业' ? 'ready' : 'draft',
|
||||
createdAt: '2026-06-30 10:00:00',
|
||||
owner: '运营',
|
||||
remark: '由候选签名池生成,按当前通道字段版本校验。',
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
footer={<><Button onClick={onClose} variant="ghost">取消</Button><Button onClick={submit}>生成任务</Button></>}
|
||||
onClose={onClose}
|
||||
open
|
||||
size="xl"
|
||||
title={<div className="template-modal-title"><h2>生成通道签名报备任务</h2><p>按通道字段配置筛选资料完整的签名和引流信息。</p></div>}
|
||||
>
|
||||
<div className="report-task-generate">
|
||||
<section>
|
||||
<h3>任务范围</h3>
|
||||
<div className="admin-system-modal-form">
|
||||
<Select label="报备通道" onChange={(event) => setChannel(event.target.value)} options={[
|
||||
{ label: '行北-集市三甲医院-39', value: '行北-集市三甲医院-39' },
|
||||
{ label: '联通-行政-杭州甲医院-37', value: '联通-行政-杭州甲医院-37' },
|
||||
{ label: '移动-行北-上海甲医院-38', value: '移动-行北-上海甲医院-38' },
|
||||
]} value={channel} />
|
||||
<Select label="运营商" onChange={(event) => setCarrier(event.target.value as '移动' | '联通' | '电信')} options={carrierOptions.filter((item) => item.value !== 'all')} value={carrier} />
|
||||
<Select label="候选企业" onChange={(event) => setEnterprise(event.target.value)} options={[
|
||||
{ label: '全部资料完整企业', value: '全部资料完整企业' },
|
||||
{ label: '上海XXXXX科技有限公司', value: '上海XXXXX科技有限公司' },
|
||||
{ label: '重庆进载数智', value: '重庆进载数智' },
|
||||
]} value={enterprise} />
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h3>生成预览</h3>
|
||||
<div className="report-task-preview">
|
||||
<div><span>可生成签名</span><strong>15</strong></div>
|
||||
<div><span>可生成引流信息</span><strong>6</strong></div>
|
||||
<div><span>缺资料</span><strong>{enterprise === '全部资料完整企业' ? 0 : 2}</strong></div>
|
||||
<div><span>导出模板</span><strong>通道字段版本 V3</strong></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function TaskDetailModal({ task, onClose }: { task: ReportTask; onClose: () => void }) {
|
||||
return (
|
||||
<Modal footer={<Button onClick={onClose}>关闭</Button>} onClose={onClose} open size="xl" title={<div className="template-modal-title"><h2>报备任务详情</h2><p>{task.id}</p></div>}>
|
||||
<div className="admin-task-detail report-task-detail">
|
||||
<div className="admin-task-metrics">
|
||||
<div className="admin-task-metric"><span>签名</span><strong>{task.signatureCount}</strong></div>
|
||||
<div className="admin-task-metric admin-task-metric--primary"><span>引流信息</span><strong>{task.drainageCount}</strong></div>
|
||||
<div className="admin-task-metric"><span>缺资料</span><strong>{task.missingCount}</strong></div>
|
||||
<div className="admin-task-metric admin-task-metric--success"><span>当前状态</span><strong>{statusMeta[task.status].label}</strong></div>
|
||||
</div>
|
||||
<section className="admin-task-card">
|
||||
<h3><Settings2 size={18} />任务信息</h3>
|
||||
<dl className="admin-task-info-list">
|
||||
<div><dt>通道</dt><dd>{task.channel}</dd></div>
|
||||
<div><dt>运营商</dt><dd>{task.carrier}</dd></div>
|
||||
<div><dt>企业范围</dt><dd>{task.enterprise}</dd></div>
|
||||
<div><dt>创建时间</dt><dd>{task.createdAt}</dd></div>
|
||||
<div><dt>导出时间</dt><dd>{task.exportedAt ?? '-'}</dd></div>
|
||||
<div><dt>回执导入</dt><dd>{task.receiptAt ?? '-'}</dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export function AdminReportTasksPage() {
|
||||
const [tasks, setTasks] = useState(initialTasks);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [carrier, setCarrier] = useState('all');
|
||||
const [status, setStatus] = useState('all');
|
||||
const [dateRange, setDateRange] = useState<DateRangeValue>({});
|
||||
const [generateOpen, setGenerateOpen] = useState(false);
|
||||
const [receiptTask, setReceiptTask] = useState<ReportTask | 'batch' | null>(null);
|
||||
const [detailTask, setDetailTask] = useState<ReportTask | null>(null);
|
||||
|
||||
const filteredTasks = useMemo(() => tasks.filter((task) => {
|
||||
const text = `${task.id}${task.channel}${task.enterprise}`;
|
||||
const date = task.createdAt.slice(0, 10);
|
||||
return (!keyword || text.includes(keyword))
|
||||
&& (carrier === 'all' || task.carrier === carrier)
|
||||
&& (status === 'all' || task.status === status)
|
||||
&& (!dateRange.start || date >= dateRange.start)
|
||||
&& (!dateRange.end || date <= dateRange.end);
|
||||
}), [carrier, dateRange.end, dateRange.start, keyword, status, tasks]);
|
||||
|
||||
function exportTask(taskId: string) {
|
||||
setTasks((current) => current.map((task) => (
|
||||
task.id === taskId ? { ...task, status: 'exported', exportedAt: '2026-06-30 10:18:00' } : task
|
||||
)));
|
||||
}
|
||||
|
||||
function importReceipt() {
|
||||
if (receiptTask && receiptTask !== 'batch') {
|
||||
setTasks((current) => current.map((task) => (
|
||||
task.id === receiptTask.id ? { ...task, status: 'partial', receiptAt: '2026-06-30 10:36:00' } : task
|
||||
)));
|
||||
}
|
||||
setReceiptTask(null);
|
||||
}
|
||||
|
||||
const columns: Array<TableColumn<ReportTask>> = [
|
||||
{ key: 'id', title: '任务编号', width: '190px', render: (record) => <strong className="admin-task-id">{record.id}</strong> },
|
||||
{ key: 'scope', title: '通道/企业', width: '280px', render: (record) => <div className="admin-task-enterprise"><strong>{record.channel}</strong><span>{record.enterprise}</span></div> },
|
||||
{ key: 'carrier', title: '运营商', width: '90px', render: (record) => <Tag tone="info">{record.carrier}</Tag> },
|
||||
{ key: 'counts', title: '资料数量', width: '190px', render: (record) => <div className="admin-task-counts"><span>签名 {record.signatureCount}</span><span>引流 {record.drainageCount}</span><strong>缺 {record.missingCount}</strong></div> },
|
||||
{ key: 'status', title: '状态', width: '110px', render: (record) => <StatusTag status={record.status} /> },
|
||||
{ key: 'time', title: '流转时间', width: '210px', render: (record) => <div className="report-task-time"><span>创建 {record.createdAt}</span><span>导出 {record.exportedAt ?? '-'}</span><span>回执 {record.receiptAt ?? '-'}</span></div> },
|
||||
{
|
||||
key: 'actions',
|
||||
title: '操作',
|
||||
align: 'right',
|
||||
width: '260px',
|
||||
render: (record) => (
|
||||
<div className="admin-task-actions">
|
||||
<Button icon={<Eye size={14} />} onClick={() => setDetailTask(record)} size="sm" variant="ghost">详情</Button>
|
||||
<Button disabled={record.missingCount > 0} icon={<Download size={14} />} onClick={() => exportTask(record.id)} size="sm" variant="ghost">导出资料</Button>
|
||||
<Button icon={<FileUp size={14} />} onClick={() => setReceiptTask(record)} size="sm" variant="ghost">导入回执</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="page-stack admin-sms-task-page report-task-page">
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<Breadcrumb items={['报备任务', '报备任务']} />
|
||||
<h1>报备任务</h1>
|
||||
</div>
|
||||
<div className="report-page-actions">
|
||||
<Button icon={<FileUp size={16} />} onClick={() => setReceiptTask('batch')} variant="secondary">批量导入回执</Button>
|
||||
<Button icon={<Plus size={16} />} onClick={() => setGenerateOpen(true)}>生成报备任务</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="surface admin-task-filter">
|
||||
<Input label="任务/通道/企业" onChange={(event) => setKeyword(event.target.value)} placeholder="请输入任务编号、通道或企业" value={keyword} />
|
||||
<Select label="运营商" onChange={(event) => setCarrier(event.target.value)} options={carrierOptions} value={carrier} />
|
||||
<Select label="任务状态" onChange={(event) => setStatus(event.target.value)} options={statusOptions} value={status} />
|
||||
<DateRangeInput label="创建时间" onChange={setDateRange} value={dateRange} />
|
||||
<div className="admin-task-filter__actions">
|
||||
<Button icon={<Search size={16} />}>查询</Button>
|
||||
<Button onClick={() => { setKeyword(''); setCarrier('all'); setStatus('all'); setDateRange({}); }} variant="ghost">重置</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="surface admin-task-table-card report-task-table-card">
|
||||
<Table columns={columns} data={filteredTasks} emptyText="暂无报备任务" rowKey="id" />
|
||||
</div>
|
||||
|
||||
{generateOpen ? <GenerateTaskModal onClose={() => setGenerateOpen(false)} onSubmit={(task) => { setTasks((current) => [task, ...current]); setGenerateOpen(false); }} /> : null}
|
||||
{receiptTask ? <ReceiptImportModal onClose={() => setReceiptTask(null)} onSubmit={importReceipt} /> : null}
|
||||
{detailTask ? <TaskDetailModal onClose={() => setDetailTask(null)} task={detailTask} /> : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Trash2 } from 'lucide-react';
|
||||
import { Button, Table, Tag, type TableColumn } from '@/components/ui';
|
||||
import { Breadcrumb, Button, Table, Tag, type TableColumn } from '@/components/ui';
|
||||
|
||||
type SensitiveLevel = 'low' | 'medium' | 'high';
|
||||
|
||||
@@ -58,7 +58,7 @@ export function AdminSensitiveWordsPage() {
|
||||
<section className="page-stack admin-security-page">
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<div className="breadcrumb-line">安全控制 / <strong>敏感词管理</strong></div>
|
||||
<Breadcrumb items={['安全控制', '敏感词管理']} />
|
||||
<h1>敏感词管理</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { Save } from 'lucide-react';
|
||||
import { Button, Input, Select } from '@/components/ui';
|
||||
import { Breadcrumb, Button, Input, Select } from '@/components/ui';
|
||||
|
||||
export function AdminSettingsPage() {
|
||||
return (
|
||||
<section className="page-stack">
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<p className="eyebrow">配置</p>
|
||||
<h1>系统配置</h1>
|
||||
<Breadcrumb items={['系统配置']} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="surface content-grid">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Eye, Search } from 'lucide-react';
|
||||
import { Button, Input, Modal, Select, Table, Tag, type TableColumn } from '@/components/ui';
|
||||
import { Breadcrumb, Button, Input, Modal, Select, Table, Tag, type TableColumn } from '@/components/ui';
|
||||
|
||||
type MmsAuditStatus = 'pending' | 'approved' | 'rejected';
|
||||
|
||||
@@ -135,7 +135,7 @@ export function AdminSignatureAuditPage() {
|
||||
|
||||
return (
|
||||
<section className="page-stack admin-audit-page">
|
||||
<div className="breadcrumb-line"><span>审核中心</span><span>/</span><strong>彩信模板审核</strong></div>
|
||||
<Breadcrumb items={['审核中心', '彩信模板审核']} />
|
||||
|
||||
<div className="surface audit-filter-card">
|
||||
<div className="audit-filter-grid audit-filter-grid--enterprise">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { ArrowLeft, Info, RefreshCw } from 'lucide-react';
|
||||
import { Button, Input, Select } from '@/components/ui';
|
||||
import { Breadcrumb, Button, Input, Select } from '@/components/ui';
|
||||
import { getEnterpriseRecords } from './adminEnterpriseMock';
|
||||
|
||||
type QueueType = 'priority' | 'normal';
|
||||
@@ -77,8 +77,7 @@ export function AdminSmsApplicationFormPage() {
|
||||
<section className="page-stack admin-app-form-page">
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<p className="eyebrow">企业管理 / {isEdit ? '编辑短信应用' : '添加短信应用'}</p>
|
||||
<h1>{isEdit ? '编辑短信应用' : '添加短信应用'}</h1>
|
||||
<Breadcrumb items={[isEdit ? '编辑短信应用' : '添加短信应用']} />
|
||||
<p>{enterprise?.name ?? '当前企业'} 的短信应用配置。</p>
|
||||
</div>
|
||||
<Button icon={<ArrowLeft size={16} />} onClick={goBack} variant="ghost">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { CalendarDays, Check, Search, X } from 'lucide-react';
|
||||
import { Button, Input, Modal, Select, Table, Tag, Textarea, type TableColumn } from '@/components/ui';
|
||||
import { Breadcrumb, Button, Input, Modal, Select, Table, Tag, Textarea, type TableColumn } from '@/components/ui';
|
||||
|
||||
type SmsAuditStatus = 'pending' | 'approved' | 'rejected';
|
||||
|
||||
@@ -288,7 +288,7 @@ export function AdminSmsAuditPage() {
|
||||
|
||||
return (
|
||||
<section className="page-stack admin-audit-page">
|
||||
<div className="breadcrumb-line"><span>审核中心</span><span>/</span><strong>短信审核</strong></div>
|
||||
<Breadcrumb items={['审核中心', '短信审核']} />
|
||||
<div className="surface sms-audit-filter">
|
||||
<h2>筛选条件</h2>
|
||||
<div className="audit-filter-grid audit-filter-grid--sms">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Download, MessageSquare, Search, Smartphone } from 'lucide-react';
|
||||
import {
|
||||
Breadcrumb,
|
||||
Button,
|
||||
DateRangeInput,
|
||||
Input,
|
||||
@@ -243,7 +244,7 @@ export function AdminSmsRecordsPage() {
|
||||
<section className="page-stack admin-sms-records-page">
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<div className="breadcrumb-line">数据详单 / <strong>短信记录</strong></div>
|
||||
<Breadcrumb items={['数据详单', '短信记录']} />
|
||||
<h1>短信记录</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Fragment, useMemo, useState } from 'react';
|
||||
import { BarChart3, CalendarClock, Eye, MapPin, Search, Send, Smartphone, StopCircle, TrendingUp } from 'lucide-react';
|
||||
import {
|
||||
Breadcrumb,
|
||||
Button,
|
||||
DateRangeInput,
|
||||
InlineTextPreview,
|
||||
@@ -448,7 +449,7 @@ export function AdminSmsTaskProgressPage() {
|
||||
<section className="page-stack admin-sms-task-page">
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<div className="breadcrumb-line">发送任务 / <strong>短信任务进度</strong></div>
|
||||
<Breadcrumb items={['发送任务', '短信任务进度']} />
|
||||
<h1>短信任务进度</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Search, Smartphone } from 'lucide-react';
|
||||
import {
|
||||
Breadcrumb,
|
||||
Button,
|
||||
DateRangeInput,
|
||||
Input,
|
||||
@@ -180,7 +181,7 @@ export function AdminSmsUplinkRecordsPage() {
|
||||
<section className="page-stack admin-uplink-page">
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<div className="breadcrumb-line">数据详单 / <strong>短信上行记录</strong></div>
|
||||
<Breadcrumb items={['数据详单', '短信上行记录']} />
|
||||
<h1>短信上行记录</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Check, X } from 'lucide-react';
|
||||
import { Button, Table, Tag, type TableColumn } from '@/components/ui';
|
||||
import { Breadcrumb, Button, Table, Tag, type TableColumn } from '@/components/ui';
|
||||
import { adminService } from '@/mock';
|
||||
import type { AuditItem, AuditStatus } from '@/mock';
|
||||
|
||||
@@ -69,8 +69,7 @@ export function AdminTemplateAuditPage() {
|
||||
<section className="page-stack">
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<p className="eyebrow">管理</p>
|
||||
<h1>模板审核</h1>
|
||||
<Breadcrumb items={['短信模板审核']} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="surface">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Plus, Search } from 'lucide-react';
|
||||
import { Button, Input, Modal, Select, Table, Tag, type TableColumn } from '@/components/ui';
|
||||
import { Breadcrumb, Button, Input, Modal, Select, Table, Tag, type TableColumn } from '@/components/ui';
|
||||
|
||||
type UserStatus = 'enabled' | 'disabled';
|
||||
|
||||
@@ -159,7 +159,7 @@ export function AdminUsersPage() {
|
||||
<section className="page-stack admin-system-page">
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<div className="breadcrumb-line">系统管理 / <strong>用户管理</strong></div>
|
||||
<Breadcrumb items={['系统管理', '用户管理']} />
|
||||
<h1>用户管理</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user