完善运营端报备流程与页面标题
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { BarChart3 } from 'lucide-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 { auditTrend, channelShare, customerGrowth, hourlySendTrend } from '@/mock/chartData';
|
||||||
import { adminService } from '@/mock';
|
import { adminService } from '@/mock';
|
||||||
import { createBarOption, createLineOption, createPieOption } from '@/theme/chartOptions';
|
import { createBarOption, createLineOption, createPieOption } from '@/theme/chartOptions';
|
||||||
@@ -49,8 +49,7 @@ export function AdminAnalyticsPage() {
|
|||||||
<section className="page-stack">
|
<section className="page-stack">
|
||||||
<div className="page-heading">
|
<div className="page-heading">
|
||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">运营</p>
|
<Breadcrumb items={['数据统计']} />
|
||||||
<h1>数据统计</h1>
|
|
||||||
</div>
|
</div>
|
||||||
<Button icon={<BarChart3 size={16} />} variant="ghost">导出报表</Button>
|
<Button icon={<BarChart3 size={16} />} variant="ghost">导出报表</Button>
|
||||||
</div>
|
</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';
|
import { adminService, type Customer } from '@/mock';
|
||||||
|
|
||||||
const columns: Array<TableColumn<Customer>> = [
|
const columns: Array<TableColumn<Customer>> = [
|
||||||
@@ -26,8 +26,7 @@ export function AdminBillingPage() {
|
|||||||
<section className="page-stack">
|
<section className="page-stack">
|
||||||
<div className="page-heading">
|
<div className="page-heading">
|
||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">配置</p>
|
<Breadcrumb items={['账单流水']} />
|
||||||
<h1>账单流水</h1>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="surface">
|
<div className="surface">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { Info, Plus, Trash2 } from 'lucide-react';
|
import { Info, Plus, Trash2 } from 'lucide-react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
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';
|
import type { TableColumn } from '@/components/ui';
|
||||||
|
|
||||||
type Carrier = 'mobile' | 'unicom' | 'telecom';
|
type Carrier = 'mobile' | 'unicom' | 'telecom';
|
||||||
@@ -212,8 +212,7 @@ export function AdminChannelGroupFormPage() {
|
|||||||
<div className="page-stack channel-group-form-page">
|
<div className="page-stack channel-group-form-page">
|
||||||
<div className="page-heading">
|
<div className="page-heading">
|
||||||
<div>
|
<div>
|
||||||
<div className="eyebrow">通道管理 / 通道组 / {editing ? '编辑通道组' : '添加通道组'}</div>
|
<Breadcrumb items={[editing ? '编辑短信通道组' : '添加短信通道组']} />
|
||||||
<h1>{editing ? '编辑短信通道组' : '添加短信通道组'}</h1>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { Edit3, Layers3, Plus, Search, Trash2, UsersRound } from 'lucide-react';
|
import { Edit3, Layers3, Plus, Search, Trash2, UsersRound } from 'lucide-react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Button, Input, Pagination } from '@/components/ui';
|
import { Breadcrumb, Button, Input, Pagination } from '@/components/ui';
|
||||||
|
|
||||||
type ChannelGroup = {
|
type ChannelGroup = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -105,8 +105,7 @@ export function AdminChannelGroupsPage() {
|
|||||||
<div className="page-stack sms-channel-group-page">
|
<div className="page-stack sms-channel-group-page">
|
||||||
<div className="page-heading">
|
<div className="page-heading">
|
||||||
<div>
|
<div>
|
||||||
<div className="eyebrow">通道管理 / 短信通道组管理</div>
|
<Breadcrumb items={['短信通道组管理']} />
|
||||||
<h1>短信通道组管理</h1>
|
|
||||||
</div>
|
</div>
|
||||||
<Button icon={<Plus size={16} />} onClick={() => navigate('/admin/channel-groups/new')}>
|
<Button icon={<Plus size={16} />} onClick={() => navigate('/admin/channel-groups/new')}>
|
||||||
添加通道组
|
添加通道组
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
ChevronUp,
|
ChevronUp,
|
||||||
Eye,
|
Eye,
|
||||||
FileSliders,
|
FileSliders,
|
||||||
|
FileUp,
|
||||||
GripVertical,
|
GripVertical,
|
||||||
Pencil,
|
Pencil,
|
||||||
Plus,
|
Plus,
|
||||||
@@ -14,7 +15,7 @@ import {
|
|||||||
Trash2,
|
Trash2,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
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';
|
import type { DateRangeValue } from '@/components/ui';
|
||||||
|
|
||||||
type ReportStatus = 'success' | 'failed' | 'reporting' | 'unreported' | 'withdrawn' | 'abandoned';
|
type ReportStatus = 'success' | 'failed' | 'reporting' | 'unreported' | 'withdrawn' | 'abandoned';
|
||||||
@@ -129,11 +130,34 @@ const drainageFieldPool: ReportField[] = [
|
|||||||
{ id: 'authorization', label: '授权证明', type: '文件' },
|
{ 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[] = [
|
const initialSelectedDrainageFields: SelectedReportField[] = [
|
||||||
{ id: 'companyName', label: '公司名称', type: '文本', required: false, mapping: '' },
|
{ id: 'companyName', label: '公司名称', type: '文本', required: false, mapping: '' },
|
||||||
{ id: 'legalId', 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 = {
|
const emptyStats: DeliveryStats = {
|
||||||
successRate: 0,
|
successRate: 0,
|
||||||
successCount: 0,
|
successCount: 0,
|
||||||
@@ -269,19 +293,25 @@ function SignatureDetailModal({ report, onClose }: { report: SignatureReport; on
|
|||||||
|
|
||||||
function DrainageFieldConfigModal({
|
function DrainageFieldConfigModal({
|
||||||
fields,
|
fields,
|
||||||
|
fieldPool,
|
||||||
onChange,
|
onChange,
|
||||||
onClose,
|
onClose,
|
||||||
|
title = '配置报备字段',
|
||||||
|
description = '从字段池中选择字段,并配置是否必填',
|
||||||
}: {
|
}: {
|
||||||
fields: SelectedReportField[];
|
fields: SelectedReportField[];
|
||||||
|
fieldPool: ReportField[];
|
||||||
onChange: (fields: SelectedReportField[]) => void;
|
onChange: (fields: SelectedReportField[]) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
}) {
|
}) {
|
||||||
const [draft, setDraft] = useState(fields);
|
const [draft, setDraft] = useState(fields);
|
||||||
const [searchText, setSearchText] = useState('');
|
const [searchText, setSearchText] = useState('');
|
||||||
const availableFields = useMemo(() => drainageFieldPool.filter((field) => (
|
const availableFields = useMemo(() => fieldPool.filter((field) => (
|
||||||
!draft.some((item) => item.id === field.id)
|
!draft.some((item) => item.id === field.id)
|
||||||
&& (!searchText || field.label.includes(searchText))
|
&& (!searchText || field.label.includes(searchText))
|
||||||
)), [draft, searchText]);
|
)), [draft, fieldPool, searchText]);
|
||||||
|
|
||||||
function addField(field: ReportField) {
|
function addField(field: ReportField) {
|
||||||
setDraft((items) => [...items, { ...field, required: false, mapping: '' }]);
|
setDraft((items) => [...items, { ...field, required: false, mapping: '' }]);
|
||||||
@@ -312,7 +342,7 @@ function DrainageFieldConfigModal({
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
open
|
open
|
||||||
size="xl"
|
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">
|
<div className="channel-field-config">
|
||||||
<section className="channel-field-pool">
|
<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() {
|
export function AdminChannelReportPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { channelId = '88827' } = useParams();
|
const { channelId = '88827' } = useParams();
|
||||||
@@ -380,7 +437,10 @@ export function AdminChannelReportPage() {
|
|||||||
const [nextRemark, setNextRemark] = useState('');
|
const [nextRemark, setNextRemark] = useState('');
|
||||||
const [detail, setDetail] = useState<ReportDetail | null>(null);
|
const [detail, setDetail] = useState<ReportDetail | null>(null);
|
||||||
const [fieldConfigOpen, setFieldConfigOpen] = useState(false);
|
const [fieldConfigOpen, setFieldConfigOpen] = useState(false);
|
||||||
|
const [signatureFieldConfigOpen, setSignatureFieldConfigOpen] = useState(false);
|
||||||
|
const [receiptOpen, setReceiptOpen] = useState(false);
|
||||||
const [drainageFields, setDrainageFields] = useState(initialSelectedDrainageFields);
|
const [drainageFields, setDrainageFields] = useState(initialSelectedDrainageFields);
|
||||||
|
const [signatureFields, setSignatureFields] = useState(initialSelectedSignatureFields);
|
||||||
|
|
||||||
const filteredReports = useMemo(() => reports.filter((report) => {
|
const filteredReports = useMemo(() => reports.filter((report) => {
|
||||||
const matchesKeyword = !keyword || report.name.includes(keyword) || report.drainage.some((item) => item.value.includes(keyword));
|
const matchesKeyword = !keyword || report.name.includes(keyword) || report.drainage.some((item) => item.value.includes(keyword));
|
||||||
@@ -433,14 +493,15 @@ export function AdminChannelReportPage() {
|
|||||||
return (
|
return (
|
||||||
<section className="page-stack channel-report-page">
|
<section className="page-stack channel-report-page">
|
||||||
<div className="surface channel-report-hero">
|
<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">
|
<div className="channel-report-heading">
|
||||||
<Button icon={<ChevronLeft size={16} />} onClick={() => navigate('/admin/channels')} variant="ghost">返回列表</Button>
|
<Button icon={<ChevronLeft size={16} />} onClick={() => navigate('/admin/channels')} variant="ghost">返回列表</Button>
|
||||||
<h1>{channelNames[channelId] ?? `短信通道 ${channelId}`}</h1>
|
<h1>{channelNames[channelId] ?? `短信通道 ${channelId}`}</h1>
|
||||||
<Button icon={<ChevronRight size={16} />} variant="ghost">下一个</Button>
|
<Button icon={<ChevronRight size={16} />} variant="ghost">下一个</Button>
|
||||||
<div className="channel-report-config-actions">
|
<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={<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -543,12 +604,28 @@ export function AdminChannelReportPage() {
|
|||||||
|
|
||||||
{fieldConfigOpen ? (
|
{fieldConfigOpen ? (
|
||||||
<DrainageFieldConfigModal
|
<DrainageFieldConfigModal
|
||||||
|
description="这些字段会驱动客户端引流资料补充,并映射到当前通道的导出模板。"
|
||||||
|
fieldPool={drainageFieldPool}
|
||||||
fields={drainageFields}
|
fields={drainageFields}
|
||||||
onChange={setDrainageFields}
|
onChange={setDrainageFields}
|
||||||
onClose={() => setFieldConfigOpen(false)}
|
onClose={() => setFieldConfigOpen(false)}
|
||||||
|
title="个性化引流信息报备字段"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : 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' ? (
|
{detail?.kind === 'drainage' ? (
|
||||||
<Modal footer={<Button onClick={() => setDetail(null)} variant="ghost">关闭</Button>} onClose={() => setDetail(null)} open size="md" title="引流信息报备详情">
|
<Modal footer={<Button onClick={() => setDetail(null)} variant="ghost">关闭</Button>} onClose={() => setDetail(null)} open size="md" title="引流信息报备详情">
|
||||||
<div className="channel-report-detail">
|
<div className="channel-report-detail">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { Eye, Info, Pencil, Plus, Power, Search, Send, Trash2 } from 'lucide-react';
|
import { Eye, Info, Pencil, Plus, Power, Search, Send, Trash2 } from 'lucide-react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
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 Carrier = 'mobile' | 'unicom' | 'telecom';
|
||||||
type ChannelStatus = 'normal' | 'stopped' | 'connecting' | 'failed';
|
type ChannelStatus = 'normal' | 'stopped' | 'connecting' | 'failed';
|
||||||
@@ -394,7 +394,7 @@ export function AdminChannelsPage() {
|
|||||||
return (
|
return (
|
||||||
<section className="page-stack sms-channel-page">
|
<section className="page-stack sms-channel-page">
|
||||||
<div className="page-heading">
|
<div className="page-heading">
|
||||||
<div className="breadcrumb-line"><strong>短信通道管理</strong></div>
|
<Breadcrumb items={['短信通道管理']} />
|
||||||
<Button icon={<Plus size={16} />} onClick={() => setModal({ mode: 'create' })}>添加通道</Button>
|
<Button icon={<Plus size={16} />} onClick={() => setModal({ mode: 'create' })}>添加通道</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { ImagePlus } from 'lucide-react';
|
import { ImagePlus } from 'lucide-react';
|
||||||
import { Button, Input, Select, Textarea } from '@/components/ui';
|
import { Breadcrumb, Button, Input, Select, Textarea } from '@/components/ui';
|
||||||
import {
|
import {
|
||||||
cityOptionsByProvince,
|
cityOptionsByProvince,
|
||||||
createEnterprise,
|
createEnterprise,
|
||||||
@@ -83,8 +83,7 @@ export function AdminCustomerFormPage() {
|
|||||||
<section className="page-stack enterprise-form-page">
|
<section className="page-stack enterprise-form-page">
|
||||||
<div className="page-heading">
|
<div className="page-heading">
|
||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">企业管理 / {isEdit ? '编辑企业' : '创建企业'}</p>
|
<Breadcrumb items={[isEdit ? '编辑企业' : '创建企业']} />
|
||||||
<h1>企业信息</h1>
|
|
||||||
<p>填写企业基本信息和联系人信息,用于运营端企业档案管理。</p>
|
<p>填写企业基本信息和联系人信息,用于运营端企业档案管理。</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Building2, DollarSign, Plus, Trash2, TrendingDown, TrendingUp } from 'lucide-react';
|
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 {
|
import {
|
||||||
formatCurrency,
|
formatCurrency,
|
||||||
getEnterpriseRecords,
|
getEnterpriseRecords,
|
||||||
@@ -128,8 +128,7 @@ export function AdminCustomersPage({ basePath = '/admin/customers' }: AdminCusto
|
|||||||
<section className="page-stack">
|
<section className="page-stack">
|
||||||
<div className="page-heading">
|
<div className="page-heading">
|
||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">管理</p>
|
<Breadcrumb items={['企业管理']} />
|
||||||
<h1>企业管理</h1>
|
|
||||||
</div>
|
</div>
|
||||||
<Button icon={<Plus size={16} />} onClick={() => navigate(`${basePath}/new`)}>
|
<Button icon={<Plus size={16} />} onClick={() => navigate(`${basePath}/new`)}>
|
||||||
添加企业
|
添加企业
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { Filter, Pencil, Plus, Search, Trash2 } from 'lucide-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 = '字符串' | '整数' | '文件' | '网址' | '电话' | '日期';
|
type DrainageFieldType = '字符串' | '整数' | '文件' | '网址' | '电话' | '日期';
|
||||||
|
|
||||||
@@ -166,7 +166,7 @@ export function AdminDrainageFieldsPage() {
|
|||||||
<section className="page-stack admin-system-page admin-drainage-page">
|
<section className="page-stack admin-system-page admin-drainage-page">
|
||||||
<div className="page-heading">
|
<div className="page-heading">
|
||||||
<div>
|
<div>
|
||||||
<div className="breadcrumb-line">基础配置 / <strong>引流信息报备字段库</strong></div>
|
<Breadcrumb items={['基础配置', '引流信息报备字段库']} />
|
||||||
<h1>引流信息报备字段库</h1>
|
<h1>引流信息报备字段库</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useMemo, useState } from 'react';
|
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 { 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 = {
|
type SmsApp = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -59,6 +59,7 @@ export function AdminEnterpriseApplicationsPage() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [smsApps, setSmsApps] = useState(initialSmsApps);
|
const [smsApps, setSmsApps] = useState(initialSmsApps);
|
||||||
const [mmsApps, setMmsApps] = useState(initialMmsApps);
|
const [mmsApps, setMmsApps] = useState(initialMmsApps);
|
||||||
|
const [enterpriseKeyword, setEnterpriseKeyword] = useState('');
|
||||||
const [confirmAction, setConfirmAction] = useState<
|
const [confirmAction, setConfirmAction] = useState<
|
||||||
| { action: 'toggle'; kind: AppKind; id: string; name: string; enabled: boolean }
|
| { action: 'toggle'; kind: AppKind; id: string; name: string; enabled: boolean }
|
||||||
| { action: 'delete'; kind: AppKind; id: string; name: string }
|
| { action: 'delete'; kind: AppKind; id: string; name: string }
|
||||||
@@ -94,6 +95,16 @@ export function AdminEnterpriseApplicationsPage() {
|
|||||||
setConfirmAction(null);
|
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>>>(() => [
|
const smsColumns = useMemo<Array<TableColumn<SmsApp>>>(() => [
|
||||||
{ key: 'name', title: '应用名称', width: '180px', render: (record) => <strong>{record.name}</strong> },
|
{ key: 'name', title: '应用名称', width: '180px', render: (record) => <strong>{record.name}</strong> },
|
||||||
{ key: 'enterprise', title: '企业名称', width: '220px', render: (record) => record.enterprise },
|
{ key: 'enterprise', title: '企业名称', width: '220px', render: (record) => record.enterprise },
|
||||||
@@ -159,17 +170,28 @@ export function AdminEnterpriseApplicationsPage() {
|
|||||||
<section className="page-stack admin-customer-split-page">
|
<section className="page-stack admin-customer-split-page">
|
||||||
<div className="page-heading">
|
<div className="page-heading">
|
||||||
<div>
|
<div>
|
||||||
<div className="breadcrumb-line">客户管理 / <strong>企业应用管理</strong></div>
|
<Breadcrumb items={['客户管理', '企业应用管理']} />
|
||||||
<h1>企业应用管理</h1>
|
<h1>企业应用管理</h1>
|
||||||
</div>
|
</div>
|
||||||
<Button icon={<Plus size={16} />} onClick={() => navigate('/admin/customers/2763/sms-apps/new')}>添加应用</Button>
|
<Button icon={<Plus size={16} />} onClick={() => navigate('/admin/customers/2763/sms-apps/new')}>添加应用</Button>
|
||||||
</div>
|
</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">
|
<div className="surface section-stack">
|
||||||
<Tabs
|
<Tabs
|
||||||
items={[
|
items={[
|
||||||
{ label: '短信应用', value: 'sms', content: <Table columns={smsColumns} data={smsApps} rowKey="id" /> },
|
{ label: '短信应用', value: 'sms', content: <Table columns={smsColumns} data={filteredSmsApps} rowKey="id" /> },
|
||||||
{ label: '彩信应用', value: 'mms', content: <Table columns={mmsColumns} data={mmsApps} rowKey="id" /> },
|
{ label: '彩信应用', value: 'mms', content: <Table columns={mmsColumns} data={filteredMmsApps} rowKey="id" /> },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { Check, FileSearch, Search, X } from 'lucide-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';
|
type EnterpriseAuditStatus = 'pending' | 'approved' | 'rejected';
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ export function AdminEnterpriseAuditPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="page-stack admin-audit-page">
|
<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="surface audit-filter-card">
|
||||||
<div className="audit-filter-grid audit-filter-grid--enterprise">
|
<div className="audit-filter-grid audit-filter-grid--enterprise">
|
||||||
<Input label="企业名称/信用代码" onChange={(event) => setKeyword(event.target.value)} placeholder="请输入企业名称或统一社会信用代码" value={keyword} />
|
<Input label="企业名称/信用代码" onChange={(event) => setKeyword(event.target.value)} placeholder="请输入企业名称或统一社会信用代码" value={keyword} />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { Trash2 } from 'lucide-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 = {
|
type EnterpriseBlacklistItem = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -46,7 +46,7 @@ export function AdminEnterpriseBlacklistPage() {
|
|||||||
<section className="page-stack admin-security-page">
|
<section className="page-stack admin-security-page">
|
||||||
<div className="page-heading">
|
<div className="page-heading">
|
||||||
<div>
|
<div>
|
||||||
<div className="breadcrumb-line">安全控制 / <strong>企业黑名单</strong></div>
|
<Breadcrumb items={['安全控制', '企业黑名单']} />
|
||||||
<h1>企业黑名单</h1>
|
<h1>企业黑名单</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { ChevronDown, ChevronLeft, ChevronRight, Edit3, FileText, Info, Plus, Search, Trash2, Upload } from 'lucide-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 CarrierStatus = 'approved' | 'pending' | 'rejected' | 'filing';
|
||||||
type SignatureKind = 'sms' | 'mms';
|
type SignatureKind = 'sms' | 'mms';
|
||||||
@@ -545,7 +545,7 @@ export function AdminEnterpriseSignaturesPage() {
|
|||||||
<section className="page-stack admin-customer-split-page">
|
<section className="page-stack admin-customer-split-page">
|
||||||
<div className="page-heading">
|
<div className="page-heading">
|
||||||
<div>
|
<div>
|
||||||
<div className="breadcrumb-line">客户管理 / <strong>企业签名管理</strong></div>
|
<Breadcrumb items={['客户管理', '企业签名管理']} />
|
||||||
<h1>企业签名管理</h1>
|
<h1>企业签名管理</h1>
|
||||||
</div>
|
</div>
|
||||||
<Button icon={<Plus size={16} />} onClick={() => setSignatureModal({ kind: activeTab })}>添加签名</Button>
|
<Button icon={<Plus size={16} />} onClick={() => setSignatureModal({ kind: activeTab })}>添加签名</Button>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { ChevronLeft, ChevronRight, Edit3, Eye, FileText, ImageIcon, Info, Music, Plus, Search, Trash2, Video } from 'lucide-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';
|
type TemplateKind = 'sms' | 'mms';
|
||||||
|
|
||||||
@@ -577,7 +577,7 @@ export function AdminEnterpriseTemplatesPage() {
|
|||||||
<section className="page-stack admin-customer-split-page">
|
<section className="page-stack admin-customer-split-page">
|
||||||
<div className="page-heading">
|
<div className="page-heading">
|
||||||
<div>
|
<div>
|
||||||
<div className="breadcrumb-line">客户管理 / <strong>企业模板管理</strong></div>
|
<Breadcrumb items={['客户管理', '企业模板管理']} />
|
||||||
<h1>企业模板管理</h1>
|
<h1>企业模板管理</h1>
|
||||||
</div>
|
</div>
|
||||||
<Button icon={<Plus size={16} />} onClick={() => (activeTab === 'sms' ? setSmsModal(null) : setMmsModal(null))}>添加模板</Button>
|
<Button icon={<Plus size={16} />} onClick={() => (activeTab === 'sms' ? setSmsModal(null) : setMmsModal(null))}>添加模板</Button>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { Trash2 } from 'lucide-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 = {
|
type GlobalBlacklistItem = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -42,7 +42,7 @@ export function AdminGlobalBlacklistPage() {
|
|||||||
<section className="page-stack admin-security-page">
|
<section className="page-stack admin-security-page">
|
||||||
<div className="page-heading">
|
<div className="page-heading">
|
||||||
<div>
|
<div>
|
||||||
<div className="breadcrumb-line">安全控制 / <strong>全局黑名单</strong></div>
|
<Breadcrumb items={['安全控制', '全局黑名单']} />
|
||||||
<h1>全局黑名单</h1>
|
<h1>全局黑名单</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
|
Breadcrumb,
|
||||||
Button,
|
Button,
|
||||||
Chart,
|
Chart,
|
||||||
Modal,
|
Modal,
|
||||||
@@ -180,8 +181,7 @@ export function AdminHome() {
|
|||||||
<section className="page-stack admin-dashboard">
|
<section className="page-stack admin-dashboard">
|
||||||
<div className="overview-hero admin-dashboard-hero">
|
<div className="overview-hero admin-dashboard-hero">
|
||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">运营端</p>
|
<Breadcrumb items={['数据概览']} />
|
||||||
<h1>数据概览</h1>
|
|
||||||
<p className="muted">按业务口径查看平台发送、签名、消费、审核和通道运行情况。</p>
|
<p className="muted">按业务口径查看平台发送、签名、消费、审核和通道运行情况。</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="page-actions">
|
<div className="page-actions">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { ArrowLeft, RefreshCw } from 'lucide-react';
|
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';
|
import { getEnterpriseRecords } from './adminEnterpriseMock';
|
||||||
|
|
||||||
const channelOptions = {
|
const channelOptions = {
|
||||||
@@ -63,8 +63,7 @@ export function AdminMmsApplicationFormPage() {
|
|||||||
<section className="page-stack admin-app-form-page">
|
<section className="page-stack admin-app-form-page">
|
||||||
<div className="page-heading">
|
<div className="page-heading">
|
||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">企业管理 / {isEdit ? '编辑彩信应用' : '添加彩信应用'}</p>
|
<Breadcrumb items={[isEdit ? '编辑彩信应用' : '添加彩信应用']} />
|
||||||
<h1>{isEdit ? '编辑彩信应用' : '添加彩信应用'}</h1>
|
|
||||||
<p>{enterprise?.name ?? '当前企业'} 的彩信应用配置。</p>
|
<p>{enterprise?.name ?? '当前企业'} 的彩信应用配置。</p>
|
||||||
</div>
|
</div>
|
||||||
<Button icon={<ArrowLeft size={16} />} onClick={goBack} variant="ghost">
|
<Button icon={<ArrowLeft size={16} />} onClick={goBack} variant="ghost">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { Ban, Pencil, Plus, Power, Search, Trash2 } from 'lucide-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';
|
import type { TableColumn } from '@/components/ui';
|
||||||
|
|
||||||
type Carrier = 'mobile' | 'unicom' | 'telecom';
|
type Carrier = 'mobile' | 'unicom' | 'telecom';
|
||||||
@@ -175,7 +175,7 @@ export function AdminMmsChannelsPage() {
|
|||||||
return (
|
return (
|
||||||
<section className="page-stack mms-channel-page">
|
<section className="page-stack mms-channel-page">
|
||||||
<div className="page-heading">
|
<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>
|
<Button icon={<Plus size={16} />} onClick={() => setModal({ mode: 'create' })}>添加通道</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { Download, Eye, Search, Smartphone } from 'lucide-react';
|
import { Download, Eye, Search, Smartphone } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
|
Breadcrumb,
|
||||||
Button,
|
Button,
|
||||||
DateRangeInput,
|
DateRangeInput,
|
||||||
Input,
|
Input,
|
||||||
@@ -225,7 +226,7 @@ export function AdminMmsRecordsPage() {
|
|||||||
<section className="page-stack admin-sms-records-page admin-mms-records-page">
|
<section className="page-stack admin-sms-records-page admin-mms-records-page">
|
||||||
<div className="page-heading">
|
<div className="page-heading">
|
||||||
<div>
|
<div>
|
||||||
<div className="breadcrumb-line">数据详单 / <strong>彩信记录</strong></div>
|
<Breadcrumb items={['数据详单', '彩信记录']} />
|
||||||
<h1>彩信记录</h1>
|
<h1>彩信记录</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { BarChart3, CalendarClock, Eye, FileImage, ImageIcon, Search, TrendingUp } from 'lucide-react';
|
import { BarChart3, CalendarClock, Eye, FileImage, ImageIcon, Search, TrendingUp } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
|
Breadcrumb,
|
||||||
Button,
|
Button,
|
||||||
DateRangeInput,
|
DateRangeInput,
|
||||||
DetailInfoGrid,
|
DetailInfoGrid,
|
||||||
@@ -445,7 +446,7 @@ export function AdminMmsTaskProgressPage() {
|
|||||||
<section className="page-stack admin-mms-task-page">
|
<section className="page-stack admin-mms-task-page">
|
||||||
<div className="page-heading">
|
<div className="page-heading">
|
||||||
<div>
|
<div>
|
||||||
<div className="breadcrumb-line">发送任务 / <strong>彩信任务进度</strong></div>
|
<Breadcrumb items={['发送任务', '彩信任务进度']} />
|
||||||
<h1>彩信任务进度</h1>
|
<h1>彩信任务进度</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Activity } from 'lucide-react';
|
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';
|
import { adminService, type Channel } from '@/mock';
|
||||||
|
|
||||||
const columns: Array<TableColumn<Channel>> = [
|
const columns: Array<TableColumn<Channel>> = [
|
||||||
@@ -23,8 +23,7 @@ export function AdminMonitorPage() {
|
|||||||
<section className="page-stack">
|
<section className="page-stack">
|
||||||
<div className="page-heading">
|
<div className="page-heading">
|
||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">运营</p>
|
<Breadcrumb items={['发送监控']} />
|
||||||
<h1>发送监控</h1>
|
|
||||||
</div>
|
</div>
|
||||||
<Button icon={<Activity size={16} />} variant="ghost">实时刷新</Button>
|
<Button icon={<Activity size={16} />} variant="ghost">实时刷新</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { Plus, Search, Trash2 } from 'lucide-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 = {
|
type PhoneSegment = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -131,7 +131,7 @@ export function AdminPhoneSegmentsPage() {
|
|||||||
<section className="page-stack admin-system-page">
|
<section className="page-stack admin-system-page">
|
||||||
<div className="page-heading">
|
<div className="page-heading">
|
||||||
<div>
|
<div>
|
||||||
<div className="breadcrumb-line">系统管理 / <strong>手机号段库</strong></div>
|
<Breadcrumb items={['系统管理', '手机号段库']} />
|
||||||
<h1>手机号段库</h1>
|
<h1>手机号段库</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { ChevronLeft, ChevronRight, Search } from 'lucide-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 = {
|
type RechargeRecord = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -61,7 +61,7 @@ export function AdminRechargeRecordsPage() {
|
|||||||
<section className="page-stack admin-recharge-page">
|
<section className="page-stack admin-recharge-page">
|
||||||
<div className="page-heading">
|
<div className="page-heading">
|
||||||
<div>
|
<div>
|
||||||
<div className="breadcrumb-line">数据详单 / <strong>充值记录</strong></div>
|
<Breadcrumb items={['数据详单', '充值记录']} />
|
||||||
<h1>充值记录</h1>
|
<h1>充值记录</h1>
|
||||||
</div>
|
</div>
|
||||||
</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 { useMemo, useState } from 'react';
|
||||||
import { Trash2 } from 'lucide-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';
|
type SensitiveLevel = 'low' | 'medium' | 'high';
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ export function AdminSensitiveWordsPage() {
|
|||||||
<section className="page-stack admin-security-page">
|
<section className="page-stack admin-security-page">
|
||||||
<div className="page-heading">
|
<div className="page-heading">
|
||||||
<div>
|
<div>
|
||||||
<div className="breadcrumb-line">安全控制 / <strong>敏感词管理</strong></div>
|
<Breadcrumb items={['安全控制', '敏感词管理']} />
|
||||||
<h1>敏感词管理</h1>
|
<h1>敏感词管理</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { Save } from 'lucide-react';
|
import { Save } from 'lucide-react';
|
||||||
import { Button, Input, Select } from '@/components/ui';
|
import { Breadcrumb, Button, Input, Select } from '@/components/ui';
|
||||||
|
|
||||||
export function AdminSettingsPage() {
|
export function AdminSettingsPage() {
|
||||||
return (
|
return (
|
||||||
<section className="page-stack">
|
<section className="page-stack">
|
||||||
<div className="page-heading">
|
<div className="page-heading">
|
||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">配置</p>
|
<Breadcrumb items={['系统配置']} />
|
||||||
<h1>系统配置</h1>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="surface content-grid">
|
<div className="surface content-grid">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { Eye, Search } from 'lucide-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';
|
type MmsAuditStatus = 'pending' | 'approved' | 'rejected';
|
||||||
|
|
||||||
@@ -135,7 +135,7 @@ export function AdminSignatureAuditPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="page-stack admin-audit-page">
|
<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="surface audit-filter-card">
|
||||||
<div className="audit-filter-grid audit-filter-grid--enterprise">
|
<div className="audit-filter-grid audit-filter-grid--enterprise">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { ArrowLeft, Info, RefreshCw } from 'lucide-react';
|
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';
|
import { getEnterpriseRecords } from './adminEnterpriseMock';
|
||||||
|
|
||||||
type QueueType = 'priority' | 'normal';
|
type QueueType = 'priority' | 'normal';
|
||||||
@@ -77,8 +77,7 @@ export function AdminSmsApplicationFormPage() {
|
|||||||
<section className="page-stack admin-app-form-page">
|
<section className="page-stack admin-app-form-page">
|
||||||
<div className="page-heading">
|
<div className="page-heading">
|
||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">企业管理 / {isEdit ? '编辑短信应用' : '添加短信应用'}</p>
|
<Breadcrumb items={[isEdit ? '编辑短信应用' : '添加短信应用']} />
|
||||||
<h1>{isEdit ? '编辑短信应用' : '添加短信应用'}</h1>
|
|
||||||
<p>{enterprise?.name ?? '当前企业'} 的短信应用配置。</p>
|
<p>{enterprise?.name ?? '当前企业'} 的短信应用配置。</p>
|
||||||
</div>
|
</div>
|
||||||
<Button icon={<ArrowLeft size={16} />} onClick={goBack} variant="ghost">
|
<Button icon={<ArrowLeft size={16} />} onClick={goBack} variant="ghost">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { CalendarDays, Check, Search, X } from 'lucide-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';
|
type SmsAuditStatus = 'pending' | 'approved' | 'rejected';
|
||||||
|
|
||||||
@@ -288,7 +288,7 @@ export function AdminSmsAuditPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="page-stack admin-audit-page">
|
<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">
|
<div className="surface sms-audit-filter">
|
||||||
<h2>筛选条件</h2>
|
<h2>筛选条件</h2>
|
||||||
<div className="audit-filter-grid audit-filter-grid--sms">
|
<div className="audit-filter-grid audit-filter-grid--sms">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { Download, MessageSquare, Search, Smartphone } from 'lucide-react';
|
import { Download, MessageSquare, Search, Smartphone } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
|
Breadcrumb,
|
||||||
Button,
|
Button,
|
||||||
DateRangeInput,
|
DateRangeInput,
|
||||||
Input,
|
Input,
|
||||||
@@ -243,7 +244,7 @@ export function AdminSmsRecordsPage() {
|
|||||||
<section className="page-stack admin-sms-records-page">
|
<section className="page-stack admin-sms-records-page">
|
||||||
<div className="page-heading">
|
<div className="page-heading">
|
||||||
<div>
|
<div>
|
||||||
<div className="breadcrumb-line">数据详单 / <strong>短信记录</strong></div>
|
<Breadcrumb items={['数据详单', '短信记录']} />
|
||||||
<h1>短信记录</h1>
|
<h1>短信记录</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Fragment, useMemo, useState } from 'react';
|
import { Fragment, useMemo, useState } from 'react';
|
||||||
import { BarChart3, CalendarClock, Eye, MapPin, Search, Send, Smartphone, StopCircle, TrendingUp } from 'lucide-react';
|
import { BarChart3, CalendarClock, Eye, MapPin, Search, Send, Smartphone, StopCircle, TrendingUp } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
|
Breadcrumb,
|
||||||
Button,
|
Button,
|
||||||
DateRangeInput,
|
DateRangeInput,
|
||||||
InlineTextPreview,
|
InlineTextPreview,
|
||||||
@@ -448,7 +449,7 @@ export function AdminSmsTaskProgressPage() {
|
|||||||
<section className="page-stack admin-sms-task-page">
|
<section className="page-stack admin-sms-task-page">
|
||||||
<div className="page-heading">
|
<div className="page-heading">
|
||||||
<div>
|
<div>
|
||||||
<div className="breadcrumb-line">发送任务 / <strong>短信任务进度</strong></div>
|
<Breadcrumb items={['发送任务', '短信任务进度']} />
|
||||||
<h1>短信任务进度</h1>
|
<h1>短信任务进度</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { Search, Smartphone } from 'lucide-react';
|
import { Search, Smartphone } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
|
Breadcrumb,
|
||||||
Button,
|
Button,
|
||||||
DateRangeInput,
|
DateRangeInput,
|
||||||
Input,
|
Input,
|
||||||
@@ -180,7 +181,7 @@ export function AdminSmsUplinkRecordsPage() {
|
|||||||
<section className="page-stack admin-uplink-page">
|
<section className="page-stack admin-uplink-page">
|
||||||
<div className="page-heading">
|
<div className="page-heading">
|
||||||
<div>
|
<div>
|
||||||
<div className="breadcrumb-line">数据详单 / <strong>短信上行记录</strong></div>
|
<Breadcrumb items={['数据详单', '短信上行记录']} />
|
||||||
<h1>短信上行记录</h1>
|
<h1>短信上行记录</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { Check, X } from 'lucide-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 { adminService } from '@/mock';
|
||||||
import type { AuditItem, AuditStatus } from '@/mock';
|
import type { AuditItem, AuditStatus } from '@/mock';
|
||||||
|
|
||||||
@@ -69,8 +69,7 @@ export function AdminTemplateAuditPage() {
|
|||||||
<section className="page-stack">
|
<section className="page-stack">
|
||||||
<div className="page-heading">
|
<div className="page-heading">
|
||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">管理</p>
|
<Breadcrumb items={['短信模板审核']} />
|
||||||
<h1>模板审核</h1>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="surface">
|
<div className="surface">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { Plus, Search } from 'lucide-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';
|
type UserStatus = 'enabled' | 'disabled';
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ export function AdminUsersPage() {
|
|||||||
<section className="page-stack admin-system-page">
|
<section className="page-stack admin-system-page">
|
||||||
<div className="page-heading">
|
<div className="page-heading">
|
||||||
<div>
|
<div>
|
||||||
<div className="breadcrumb-line">系统管理 / <strong>用户管理</strong></div>
|
<Breadcrumb items={['系统管理', '用户管理']} />
|
||||||
<h1>用户管理</h1>
|
<h1>用户管理</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import {
|
||||||
|
Activity,
|
||||||
|
BarChart3,
|
||||||
|
Building2,
|
||||||
|
ClipboardList,
|
||||||
|
FileCheck2,
|
||||||
|
FilePenLine,
|
||||||
|
Gauge,
|
||||||
|
Hash,
|
||||||
|
ImageIcon,
|
||||||
|
Layers3,
|
||||||
|
ListChecks,
|
||||||
|
MessageSquare,
|
||||||
|
Phone,
|
||||||
|
RadioTower,
|
||||||
|
ReceiptText,
|
||||||
|
Settings,
|
||||||
|
Shield,
|
||||||
|
ShieldCheck,
|
||||||
|
ShieldOff,
|
||||||
|
TrendingUp,
|
||||||
|
Users,
|
||||||
|
UserX,
|
||||||
|
type LucideIcon,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
type BreadcrumbProps = {
|
||||||
|
items: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Breadcrumb({ items }: BreadcrumbProps) {
|
||||||
|
const title = items[items.length - 1] ?? '';
|
||||||
|
const Icon = iconForTitle(title);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav aria-label={title} className="breadcrumb-line">
|
||||||
|
<span className="breadcrumb-line__icon">
|
||||||
|
<Icon aria-hidden size={22} strokeWidth={2.4} />
|
||||||
|
</span>
|
||||||
|
<h1>{title}</h1>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function iconForTitle(title: string): LucideIcon {
|
||||||
|
if (title.includes('发送监控')) return Activity;
|
||||||
|
if (title.includes('数据统计')) return BarChart3;
|
||||||
|
if (title.includes('运营看板') || title.includes('数据概览')) return Gauge;
|
||||||
|
if (title.includes('企业认证')) return ShieldCheck;
|
||||||
|
if (title.includes('短信审核') || title.includes('短信记录') || title.includes('上行短信')) return MessageSquare;
|
||||||
|
if (title.includes('模板审核') || title.includes('企业模板') || title.includes('模板')) return FileCheck2;
|
||||||
|
if (title.includes('彩信')) return ImageIcon;
|
||||||
|
if (title.includes('短信通道') || title.includes('通道报备')) return RadioTower;
|
||||||
|
if (title.includes('通道组')) return Layers3;
|
||||||
|
if (title.includes('报备记录')) return ListChecks;
|
||||||
|
if (title.includes('报备任务')) return ClipboardList;
|
||||||
|
if (title.includes('任务进度')) return TrendingUp;
|
||||||
|
if (title.includes('充值') || title.includes('账单')) return ReceiptText;
|
||||||
|
if (title.includes('企业黑名单')) return UserX;
|
||||||
|
if (title.includes('全局黑名单')) return ShieldOff;
|
||||||
|
if (title.includes('敏感词')) return Shield;
|
||||||
|
if (title.includes('用户')) return Users;
|
||||||
|
if (title.includes('手机号段')) return Phone;
|
||||||
|
if (title.includes('引流信息字段')) return Hash;
|
||||||
|
if (title.includes('系统配置')) return Settings;
|
||||||
|
if (title.includes('企业应用')) return Layers3;
|
||||||
|
if (title.includes('企业签名') || title.includes('签名')) return FilePenLine;
|
||||||
|
if (title.includes('企业') || title.includes('客户')) return Building2;
|
||||||
|
return FileCheck2;
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export { Button } from './Button';
|
export { Button } from './Button';
|
||||||
|
export { Breadcrumb } from './Breadcrumb';
|
||||||
export { Chart } from './Chart';
|
export { Chart } from './Chart';
|
||||||
export { DateRangeInput } from './DateRangeInput';
|
export { DateRangeInput } from './DateRangeInput';
|
||||||
export { DateTimeInput } from './DateTimeInput';
|
export { DateTimeInput } from './DateTimeInput';
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ import {
|
|||||||
Gauge,
|
Gauge,
|
||||||
Hash,
|
Hash,
|
||||||
ImageIcon,
|
ImageIcon,
|
||||||
|
ListChecks,
|
||||||
Layers3,
|
Layers3,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
Phone,
|
Phone,
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
RadioTower,
|
RadioTower,
|
||||||
ReceiptText,
|
ReceiptText,
|
||||||
|
ClipboardList,
|
||||||
Send,
|
Send,
|
||||||
Settings,
|
Settings,
|
||||||
Shield,
|
Shield,
|
||||||
@@ -71,6 +73,14 @@ export function AdminLayout() {
|
|||||||
{ label: '短信通道组管理', to: '/admin/channel-groups', icon: Layers3 },
|
{ label: '短信通道组管理', to: '/admin/channel-groups', icon: Layers3 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '报备任务',
|
||||||
|
icon: ClipboardList,
|
||||||
|
items: [
|
||||||
|
{ label: '报备任务', to: '/admin/report-tasks', icon: ClipboardList },
|
||||||
|
{ label: '报备记录', to: '/admin/report-records', icon: ListChecks },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '发送任务',
|
title: '发送任务',
|
||||||
icon: Send,
|
icon: Send,
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ import { AdminMmsTaskProgressPage } from '@/apps/admin/AdminMmsTaskProgressPage'
|
|||||||
import { AdminMonitorPage } from '@/apps/admin/AdminMonitorPage';
|
import { AdminMonitorPage } from '@/apps/admin/AdminMonitorPage';
|
||||||
import { AdminPhoneSegmentsPage } from '@/apps/admin/AdminPhoneSegmentsPage';
|
import { AdminPhoneSegmentsPage } from '@/apps/admin/AdminPhoneSegmentsPage';
|
||||||
import { AdminRechargeRecordsPage } from '@/apps/admin/AdminRechargeRecordsPage';
|
import { AdminRechargeRecordsPage } from '@/apps/admin/AdminRechargeRecordsPage';
|
||||||
|
import { AdminReportRecordsPage } from '@/apps/admin/AdminReportRecordsPage';
|
||||||
|
import { AdminReportTasksPage } from '@/apps/admin/AdminReportTasksPage';
|
||||||
import { AdminSensitiveWordsPage } from '@/apps/admin/AdminSensitiveWordsPage';
|
import { AdminSensitiveWordsPage } from '@/apps/admin/AdminSensitiveWordsPage';
|
||||||
import { AdminSettingsPage } from '@/apps/admin/AdminSettingsPage';
|
import { AdminSettingsPage } from '@/apps/admin/AdminSettingsPage';
|
||||||
import { AdminSmsAuditPage } from '@/apps/admin/AdminSmsAuditPage';
|
import { AdminSmsAuditPage } from '@/apps/admin/AdminSmsAuditPage';
|
||||||
@@ -107,6 +109,8 @@ export function AppRoutes() {
|
|||||||
<Route path="signatures" element={<AdminSignatureAuditPage />} />
|
<Route path="signatures" element={<AdminSignatureAuditPage />} />
|
||||||
<Route path="enterprise-audit" element={<AdminEnterpriseAuditPage />} />
|
<Route path="enterprise-audit" element={<AdminEnterpriseAuditPage />} />
|
||||||
<Route path="sms-audit" element={<AdminSmsAuditPage />} />
|
<Route path="sms-audit" element={<AdminSmsAuditPage />} />
|
||||||
|
<Route path="report-tasks" element={<AdminReportTasksPage />} />
|
||||||
|
<Route path="report-records" element={<AdminReportRecordsPage />} />
|
||||||
<Route path="sms-task-progress" element={<AdminSmsTaskProgressPage />} />
|
<Route path="sms-task-progress" element={<AdminSmsTaskProgressPage />} />
|
||||||
<Route path="mms-task-progress" element={<AdminMmsTaskProgressPage />} />
|
<Route path="mms-task-progress" element={<AdminMmsTaskProgressPage />} />
|
||||||
<Route path="sms-records" element={<AdminSmsRecordsPage />} />
|
<Route path="sms-records" element={<AdminSmsRecordsPage />} />
|
||||||
|
|||||||
+221
-4
@@ -4787,14 +4787,59 @@ h3 {
|
|||||||
|
|
||||||
.breadcrumb-line {
|
.breadcrumb-line {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: var(--color-text-muted);
|
align-self: flex-start;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--space-3);
|
gap: var(--space-4);
|
||||||
font-weight: var(--font-weight-semibold);
|
max-width: 100%;
|
||||||
|
min-height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumb-line strong {
|
.breadcrumb-line__icon {
|
||||||
|
align-items: center;
|
||||||
|
background: var(--color-selected-soft);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
color: var(--color-selected);
|
color: var(--color-selected);
|
||||||
|
display: inline-flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
height: 40px;
|
||||||
|
justify-content: center;
|
||||||
|
width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-line h1 {
|
||||||
|
align-items: center;
|
||||||
|
color: var(--color-text-strong);
|
||||||
|
display: flex;
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
gap: var(--space-4);
|
||||||
|
letter-spacing: 0;
|
||||||
|
line-height: 1.25;
|
||||||
|
margin: 0;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-heading > div:has(.breadcrumb-line) > h1 {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-report-heading h1 {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-enterprise-profile__main {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-enterprise-profile__icon {
|
||||||
|
height: 40px;
|
||||||
|
width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-enterprise-profile__main h1 {
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
letter-spacing: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-audit-page {
|
.admin-audit-page {
|
||||||
@@ -6439,6 +6484,178 @@ h3 {
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.report-page-actions {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-task-table-card .ui-table {
|
||||||
|
min-width: 1180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-task-table-card .ui-table th:last-child,
|
||||||
|
.report-task-table-card .ui-table td:last-child {
|
||||||
|
background: var(--color-surface);
|
||||||
|
box-shadow: -8px 0 12px rgb(15 23 42 / 6%);
|
||||||
|
position: sticky;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-task-table-card .ui-table th:last-child {
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-task-time {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
display: grid;
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
gap: 4px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-task-time span {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-task-generate {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-task-generate section,
|
||||||
|
.report-task-detail .admin-task-card {
|
||||||
|
background: var(--color-bg-subtle);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-task-generate h3,
|
||||||
|
.report-receipt-preview h3,
|
||||||
|
.report-history h3 {
|
||||||
|
color: var(--color-text-strong);
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0 0 var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-task-preview {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-4);
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-task-preview > div,
|
||||||
|
.report-receipt-preview > div {
|
||||||
|
background: var(--color-surface);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-task-preview span,
|
||||||
|
.report-receipt-preview span {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
display: block;
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-task-preview strong,
|
||||||
|
.report-receipt-preview strong {
|
||||||
|
color: var(--color-text-strong);
|
||||||
|
font-size: var(--font-size-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-receipt-modal {
|
||||||
|
align-items: start;
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-5);
|
||||||
|
grid-template-columns: minmax(260px, 0.9fr) minmax(320px, 1.1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-receipt-modal .ui-field {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-upload-drop {
|
||||||
|
align-content: center;
|
||||||
|
background: var(--color-bg-subtle);
|
||||||
|
border: 1px dashed var(--color-border-strong);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-2);
|
||||||
|
min-height: 180px;
|
||||||
|
padding: var(--space-6);
|
||||||
|
place-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-upload-drop svg {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-upload-drop strong {
|
||||||
|
color: var(--color-text-strong);
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-upload-drop span {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-receipt-preview {
|
||||||
|
background: var(--color-surface);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-record-detail {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-history {
|
||||||
|
background: var(--color-bg-subtle);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-history h3 {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-history > div {
|
||||||
|
align-items: center;
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-4);
|
||||||
|
grid-template-columns: 170px 120px 1fr;
|
||||||
|
padding: var(--space-3) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-history span,
|
||||||
|
.report-history em {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-history strong {
|
||||||
|
color: var(--color-text-strong);
|
||||||
|
}
|
||||||
|
|
||||||
.admin-task-template-row .ui-inline-text-preview {
|
.admin-task-template-row .ui-inline-text-preview {
|
||||||
background: var(--color-bg-subtle);
|
background: var(--color-bg-subtle);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user