完善运营端报备流程与页面标题

This commit is contained in:
hectorzhao
2026-06-30 18:32:18 +08:00
parent b2e42ebd82
commit 47e603d7d3
40 changed files with 917 additions and 83 deletions
+2 -3
View File
@@ -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>
+2 -3
View File
@@ -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">
+2 -3
View File
@@ -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>
+2 -3
View File
@@ -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')}>
+83 -6
View File
@@ -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> ExcelCSV</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">
+2 -2
View File
@@ -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>
+2 -3
View File
@@ -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>
+2 -3
View File
@@ -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`)}>
+2 -2
View File
@@ -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>
+2 -2
View File
@@ -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>
+2 -2
View File
@@ -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>
+2 -2
View File
@@ -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">
+2 -2
View File
@@ -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>
+2 -1
View File
@@ -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>
+2 -1
View File
@@ -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>
+2 -3
View File
@@ -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>
+2 -2
View File
@@ -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>
+2 -2
View File
@@ -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>
+136
View File
@@ -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>
);
}
+303
View File
@@ -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> ExcelCSV</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>
);
}
+2 -2
View File
@@ -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>
+2 -3
View File
@@ -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">
+2 -2
View File
@@ -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">
+2 -2
View File
@@ -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">
+2 -1
View File
@@ -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>
+2 -1
View File
@@ -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>
+2 -1
View File
@@ -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>
+2 -3
View File
@@ -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">
+2 -2
View File
@@ -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>
+70
View File
@@ -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
View File
@@ -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';
+10
View File
@@ -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,
+4
View File
@@ -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
View File
@@ -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);
} }