fix: harden real backend admin workflows and ui
This commit is contained in:
@@ -2,10 +2,11 @@ import { useEffect, useMemo, useState } from 'react';
|
||||
import { Copy, Edit3, Plus, Search, Settings2, Trash2 } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Breadcrumb, Button, Input, Modal, Table, Tabs, Tag, type TableColumn } from '@/components/ui';
|
||||
import { adminApi, type ApplicationCmppParams, type CmppConnectionState, type EnterpriseApplication } from '@/api/adminApi';
|
||||
import { adminApi, type ApplicationCmppParams, type CmppConnectionState, type EnterpriseApplication, type TenantOption } from '@/api/adminApi';
|
||||
|
||||
type SmsApp = {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
name: string;
|
||||
enterprise: string;
|
||||
appId: string;
|
||||
@@ -43,18 +44,6 @@ type CmppConnection = {
|
||||
pendingWindow: number;
|
||||
};
|
||||
|
||||
type MmsApp = Omit<SmsApp, 'cmppStatus' | 'cmppConnections' | 'cmppParams'> & {
|
||||
pointPrice: number;
|
||||
};
|
||||
|
||||
type AppKind = 'sms' | 'mms';
|
||||
|
||||
const initialMmsApps: MmsApp[] = [
|
||||
{ id: 'mms-app-1', name: '营销活动彩信', enterprise: '上海XXXXX科技有限公司', appId: 'MMS_2024020112345678', enabled: true, sentToday: 320, deliveryRate: 92, unitPrice: 0.15, pointPrice: 50 },
|
||||
{ id: 'mms-app-2', name: '节日祝福彩信', enterprise: '重庆进载数智', appId: 'MMS_2024020187654321', enabled: true, sentToday: 180, deliveryRate: 88, unitPrice: 0.12, pointPrice: 30 },
|
||||
{ id: 'mms-app-3', name: '会员权益彩信', enterprise: '四川骠骑企业管理', appId: 'MMS_2024020199001122', enabled: false, sentToday: 0, deliveryRate: 0, unitPrice: 0.18, pointPrice: 60 },
|
||||
];
|
||||
|
||||
function enabledTag(enabled: boolean) {
|
||||
return <Tag tone={enabled ? 'success' : 'neutral'}>{enabled ? '启用' : '停用'}</Tag>;
|
||||
}
|
||||
@@ -77,6 +66,52 @@ function ConfirmModal({ message, danger, onCancel, onConfirm }: { message: strin
|
||||
);
|
||||
}
|
||||
|
||||
function AddApplicationModal({
|
||||
tenants,
|
||||
loading,
|
||||
selectedTenantId,
|
||||
onChange,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
}: {
|
||||
tenants: TenantOption[];
|
||||
loading: boolean;
|
||||
selectedTenantId: string;
|
||||
onChange: (tenantId: string) => void;
|
||||
onCancel: () => void;
|
||||
onConfirm: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Modal
|
||||
footer={(
|
||||
<>
|
||||
<Button onClick={onCancel} variant="ghost">取消</Button>
|
||||
<Button disabled={!selectedTenantId || loading} onClick={onConfirm}>下一步</Button>
|
||||
</>
|
||||
)}
|
||||
onClose={onCancel}
|
||||
open
|
||||
title={<div className="template-modal-title"><h2>新建企业应用</h2><p>先选择真实企业,再配置短信应用和三网通道组。</p></div>}
|
||||
>
|
||||
<div className="form-grid app-create-modal">
|
||||
<label className="field">
|
||||
<span>所属企业</span>
|
||||
<select disabled={loading} onChange={(event) => onChange(event.target.value)} value={selectedTenantId}>
|
||||
<option value="">{loading ? '企业加载中...' : '请选择真实企业'}</option>
|
||||
{tenants.map((tenant) => (
|
||||
<option key={tenant.id} value={tenant.id}>{tenant.name}({tenant.code})</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<div className="app-create-modal__hint">
|
||||
<strong>{selectedTenantId ? tenants.find((tenant) => tenant.id === selectedTenantId)?.name : '请选择要开通短信应用的企业'}</strong>
|
||||
<span>下一步会进入应用参数、客户单价、IP 白名单和运营商通道组配置。</span>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
const connectionStateMeta: Record<CmppConnection['state'], { label: string; tone: 'success' | 'warning' | 'neutral' }> = {
|
||||
open: { label: '已连接', tone: 'success' },
|
||||
closed: { label: '已断开', tone: 'neutral' },
|
||||
@@ -177,19 +212,19 @@ function CmppConnectionModal({
|
||||
<Table
|
||||
columns={[
|
||||
{ key: 'id', title: '连接ID', width: '150px', render: (record: CmppConnection) => <strong>{record.id}</strong> },
|
||||
{ key: 'state', title: '状态', width: '100px', render: (record: CmppConnection) => <Tag tone={connectionStateMeta[record.state].tone}>{connectionStateMeta[record.state].label}</Tag> },
|
||||
{ key: 'state', title: '状态', width: '130px', render: (record: CmppConnection) => <Tag tone={connectionStateMeta[record.state].tone}>{connectionStateMeta[record.state].label}</Tag> },
|
||||
{ key: 'bindType', title: '绑定类型', width: '120px', render: (record: CmppConnection) => record.bindType },
|
||||
{ key: 'clientIp', title: '客户端IP', width: '170px', render: (record: CmppConnection) => record.clientIp },
|
||||
{ key: 'sourceAddr', title: '企业代码', width: '120px', render: (record: CmppConnection) => record.sourceAddr },
|
||||
{ key: 'establishedAt', title: '连接建立时间', width: '180px', render: (record: CmppConnection) => record.establishedAt },
|
||||
{ key: 'lastHeartbeatAt', title: '上次心跳', width: '180px', render: (record: CmppConnection) => record.lastHeartbeatAt },
|
||||
{ key: 'lastSubmitAt', title: '上次提交', width: '180px', render: (record: CmppConnection) => record.lastSubmitAt },
|
||||
{ key: 'pendingWindow', title: '窗口占用', align: 'right', width: '100px', render: (record: CmppConnection) => record.pendingWindow },
|
||||
{ key: 'pendingWindow', title: '窗口占用', align: 'right', width: '120px', render: (record: CmppConnection) => record.pendingWindow },
|
||||
{
|
||||
key: 'actions',
|
||||
title: '操作',
|
||||
align: 'right',
|
||||
width: '100px',
|
||||
width: '120px',
|
||||
render: (record: CmppConnection) => (
|
||||
<Button icon={<Trash2 size={14} />} onClick={() => onDeleteConnection(record.id)} size="sm" variant="danger">删除</Button>
|
||||
),
|
||||
@@ -207,15 +242,18 @@ function CmppConnectionModal({
|
||||
export function AdminEnterpriseApplicationsPage() {
|
||||
const navigate = useNavigate();
|
||||
const [smsApps, setSmsApps] = useState<SmsApp[]>([]);
|
||||
const [mmsApps, setMmsApps] = useState(initialMmsApps);
|
||||
const [enterpriseKeyword, setEnterpriseKeyword] = useState('');
|
||||
const [connectionApp, setConnectionApp] = useState<SmsApp | null>(null);
|
||||
const [paramsApp, setParamsApp] = useState<SmsApp | null>(null);
|
||||
const [paramsDetail, setParamsDetail] = useState<ApplicationCmppParams | null>(null);
|
||||
const [error, setError] = useState('');
|
||||
const [addModalOpen, setAddModalOpen] = useState(false);
|
||||
const [tenants, setTenants] = useState<TenantOption[]>([]);
|
||||
const [tenantsLoading, setTenantsLoading] = useState(false);
|
||||
const [selectedTenantId, setSelectedTenantId] = useState('');
|
||||
const [confirmAction, setConfirmAction] = useState<
|
||||
| { action: 'toggle'; kind: AppKind; id: string; name: string; enabled: boolean }
|
||||
| { action: 'delete'; kind: AppKind; id: string; name: string }
|
||||
| { action: 'toggle'; id: string; name: string; enabled: boolean }
|
||||
| { action: 'delete'; id: string; name: string }
|
||||
| null
|
||||
>(null);
|
||||
|
||||
@@ -234,36 +272,47 @@ export function AdminEnterpriseApplicationsPage() {
|
||||
void loadSmsApps();
|
||||
}, [enterpriseKeyword]);
|
||||
|
||||
async function confirmToggle(kind: AppKind, id: string) {
|
||||
if (kind === 'sms') {
|
||||
const app = smsApps.find((item) => item.id === id);
|
||||
if (app) {
|
||||
await adminApi.changeApplicationStatus(id, app.enabled ? 'disabled' : 'active', '运营端企业应用管理');
|
||||
await loadSmsApps();
|
||||
async function openAddModal() {
|
||||
setAddModalOpen(true);
|
||||
if (tenants.length === 0) {
|
||||
setTenantsLoading(true);
|
||||
try {
|
||||
setTenants((await adminApi.listTenants()).filter((tenant) => tenant.status !== 'deleted'));
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '企业列表加载失败');
|
||||
} finally {
|
||||
setTenantsLoading(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setMmsApps((current) => current.map((item) => item.id === id ? { ...item, enabled: !item.enabled } : item));
|
||||
}
|
||||
|
||||
async function confirmDelete(kind: AppKind, id: string) {
|
||||
if (kind === 'sms') {
|
||||
await adminApi.changeApplicationStatus(id, 'deleted', '运营端删除应用');
|
||||
await loadSmsApps();
|
||||
} else {
|
||||
setMmsApps((current) => current.filter((item) => item.id !== id));
|
||||
function confirmAddApplication() {
|
||||
if (selectedTenantId) {
|
||||
navigate(`/admin/customers/${selectedTenantId}/sms-apps/new`);
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmToggle(id: string) {
|
||||
const app = smsApps.find((item) => item.id === id);
|
||||
if (app) {
|
||||
await adminApi.changeApplicationStatus(id, app.enabled ? 'disabled' : 'active', '运营端企业应用管理');
|
||||
await loadSmsApps();
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmDelete(id: string) {
|
||||
await adminApi.changeApplicationStatus(id, 'deleted', '运营端删除应用');
|
||||
await loadSmsApps();
|
||||
}
|
||||
|
||||
async function runConfirmedAction() {
|
||||
if (!confirmAction) {
|
||||
return;
|
||||
}
|
||||
if (confirmAction.action === 'toggle') {
|
||||
await confirmToggle(confirmAction.kind, confirmAction.id);
|
||||
await confirmToggle(confirmAction.id);
|
||||
} else {
|
||||
await confirmDelete(confirmAction.kind, confirmAction.id);
|
||||
await confirmDelete(confirmAction.id);
|
||||
}
|
||||
setConfirmAction(null);
|
||||
}
|
||||
@@ -286,18 +335,13 @@ export function AdminEnterpriseApplicationsPage() {
|
||||
[enterpriseKeyword, smsApps],
|
||||
);
|
||||
|
||||
const filteredMmsApps = useMemo(
|
||||
() => mmsApps.filter((item) => !enterpriseKeyword || item.enterprise.includes(enterpriseKeyword)),
|
||||
[enterpriseKeyword, mmsApps],
|
||||
);
|
||||
|
||||
const smsColumns = useMemo<Array<TableColumn<SmsApp>>>(() => [
|
||||
{ key: 'name', title: '应用名称', width: '180px', render: (record) => <strong>{record.name}</strong> },
|
||||
{ key: 'enterprise', title: '企业名称', width: '220px', render: (record) => record.enterprise },
|
||||
{ key: 'appId', title: 'AppID', width: '220px', render: (record) => record.appId },
|
||||
{ key: 'sentToday', title: '今日发送', width: '120px', render: (record) => `${record.sentToday.toLocaleString('zh-CN')} 条` },
|
||||
{ key: 'deliveryRate', title: '到达率', width: '110px', render: (record) => `${record.deliveryRate}%` },
|
||||
{ key: 'unitPrice', title: '单价', width: '100px', render: (record) => `${record.unitPrice.toFixed(3)} 元` },
|
||||
{ key: 'deliveryRate', title: '到达率', width: '130px', render: (record) => `${record.deliveryRate}%` },
|
||||
{ key: 'unitPrice', title: '单价', width: '130px', render: (record) => `${record.unitPrice.toFixed(3)} 元` },
|
||||
{
|
||||
key: 'cmppStatus',
|
||||
title: 'CMPP状态',
|
||||
@@ -317,7 +361,7 @@ export function AdminEnterpriseApplicationsPage() {
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{ key: 'enabled', title: '状态', width: '100px', render: (record) => enabledTag(record.enabled) },
|
||||
{ key: 'enabled', title: '状态', width: '130px', render: (record) => enabledTag(record.enabled) },
|
||||
{
|
||||
key: 'actions',
|
||||
title: '操作',
|
||||
@@ -325,37 +369,11 @@ export function AdminEnterpriseApplicationsPage() {
|
||||
width: '190px',
|
||||
render: (record) => (
|
||||
<div className="table-actions">
|
||||
<Button icon={<Edit3 size={15} />} onClick={() => navigate(`/admin/customers/2763/sms-apps/${record.id}/edit`)} size="sm" variant="ghost">编辑</Button>
|
||||
<Button onClick={() => setConfirmAction({ action: 'toggle', kind: 'sms', id: record.id, name: record.name, enabled: record.enabled })} size="sm" variant="secondary">
|
||||
<Button icon={<Edit3 size={15} />} onClick={() => navigate(`/admin/customers/${record.tenantId}/sms-apps/${record.id}/edit`)} size="sm" variant="ghost">编辑</Button>
|
||||
<Button onClick={() => setConfirmAction({ action: 'toggle', id: record.id, name: record.name, enabled: record.enabled })} size="sm" variant="secondary">
|
||||
{record.enabled ? '停用' : '启用'}
|
||||
</Button>
|
||||
<Button icon={<Trash2 size={15} />} onClick={() => setConfirmAction({ action: 'delete', kind: 'sms', id: record.id, name: record.name })} size="sm" variant="danger">删除</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
], [navigate]);
|
||||
|
||||
const mmsColumns = useMemo<Array<TableColumn<MmsApp>>>(() => [
|
||||
{ key: 'name', title: '应用名称', width: '180px', render: (record) => <strong>{record.name}</strong> },
|
||||
{ key: 'enterprise', title: '企业名称', width: '220px', render: (record) => record.enterprise },
|
||||
{ key: 'appId', title: 'AppID', width: '220px', render: (record) => record.appId },
|
||||
{ key: 'sentToday', title: '今日发送', width: '120px', render: (record) => `${record.sentToday.toLocaleString('zh-CN')} 条` },
|
||||
{ key: 'deliveryRate', title: '到达率', width: '110px', render: (record) => `${record.deliveryRate}%` },
|
||||
{ key: 'unitPrice', title: '单价', width: '100px', render: (record) => `${record.unitPrice.toFixed(3)} 元` },
|
||||
{ key: 'pointPrice', title: '点数', width: '100px', render: (record) => `${record.pointPrice} 分` },
|
||||
{ key: 'enabled', title: '状态', width: '100px', render: (record) => enabledTag(record.enabled) },
|
||||
{
|
||||
key: 'actions',
|
||||
title: '操作',
|
||||
align: 'right',
|
||||
width: '190px',
|
||||
render: (record) => (
|
||||
<div className="table-actions">
|
||||
<Button icon={<Edit3 size={15} />} onClick={() => navigate(`/admin/customers/2763/mms-apps/${record.id}/edit`)} size="sm" variant="ghost">编辑</Button>
|
||||
<Button onClick={() => setConfirmAction({ action: 'toggle', kind: 'mms', id: record.id, name: record.name, enabled: record.enabled })} size="sm" variant="secondary">
|
||||
{record.enabled ? '停用' : '启用'}
|
||||
</Button>
|
||||
<Button icon={<Trash2 size={15} />} onClick={() => setConfirmAction({ action: 'delete', kind: 'mms', id: record.id, name: record.name })} size="sm" variant="danger">删除</Button>
|
||||
<Button icon={<Trash2 size={15} />} onClick={() => setConfirmAction({ action: 'delete', id: record.id, name: record.name })} size="sm" variant="danger">删除</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -368,7 +386,7 @@ export function AdminEnterpriseApplicationsPage() {
|
||||
<Breadcrumb items={['客户管理', '企业应用管理']} />
|
||||
<h1>企业应用管理</h1>
|
||||
</div>
|
||||
<Button icon={<Plus size={16} />} onClick={() => navigate('/admin/customers/2763/sms-apps/new')}>添加应用</Button>
|
||||
<Button icon={<Plus size={16} />} onClick={() => { void openAddModal(); }}>添加应用</Button>
|
||||
</div>
|
||||
|
||||
<div className="surface admin-split-filter">
|
||||
@@ -388,7 +406,7 @@ export function AdminEnterpriseApplicationsPage() {
|
||||
<Tabs
|
||||
items={[
|
||||
{ label: '短信应用', value: 'sms', content: <Table columns={smsColumns} data={filteredSmsApps} rowKey="id" /> },
|
||||
{ label: '彩信应用', value: 'mms', pending: true, content: <Table columns={mmsColumns} data={filteredMmsApps} rowKey="id" /> },
|
||||
{ label: '彩信应用', value: 'mms', pending: true, content: <div className="ui-table__empty">彩信应用待后端能力确认,本页不展示演示数据。</div> },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
@@ -403,6 +421,16 @@ export function AdminEnterpriseApplicationsPage() {
|
||||
onConfirm={() => { void runConfirmedAction(); }}
|
||||
/>
|
||||
) : null}
|
||||
{addModalOpen ? (
|
||||
<AddApplicationModal
|
||||
loading={tenantsLoading}
|
||||
onCancel={() => setAddModalOpen(false)}
|
||||
onChange={setSelectedTenantId}
|
||||
onConfirm={confirmAddApplication}
|
||||
selectedTenantId={selectedTenantId}
|
||||
tenants={tenants}
|
||||
/>
|
||||
) : null}
|
||||
{connectionApp ? (
|
||||
<CmppConnectionModal
|
||||
app={connectionApp}
|
||||
@@ -419,13 +447,14 @@ function mapApplication(application: EnterpriseApplication): SmsApp {
|
||||
const connections = (application.cmppConnections ?? []).map(mapConnection);
|
||||
return {
|
||||
id: application.id,
|
||||
tenantId: application.tenantId,
|
||||
name: application.name,
|
||||
enterprise: application.tenant?.name ?? application.tenantId,
|
||||
appId: application.id,
|
||||
enabled: application.status === 'active',
|
||||
sentToday: application.sentToday ?? 0,
|
||||
deliveryRate: application.deliveryRate ?? 0,
|
||||
unitPrice: 0,
|
||||
unitPrice: (application.customerUnitPrice ?? 0) / 100,
|
||||
cmppStatus: application.cmppStatus === 'connected' ? 'connected' : application.cmppStatus === 'inactive' ? 'inactive' : 'disconnected',
|
||||
cmppParams: { host: '', port: 0, enterpriseCode: application.tenant?.code ?? application.tenantId, account: application.tenant?.code ?? application.tenantId, password: '', accessNumber: '', maxConnections: 0, heartbeatSeconds: 30, windowSize: 16, protocolVersion: 'CMPP 3.0' },
|
||||
cmppConnections: connections,
|
||||
|
||||
Reference in New Issue
Block a user