fix: harden real backend admin workflows and ui
This commit is contained in:
@@ -1,8 +1,16 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { adminApi, type ChannelGroup } from '@/api/adminApi';
|
||||
import { Breadcrumb, Button, Input, Select } from '@/components/ui';
|
||||
import { ArrowLeft, RadioTower } from 'lucide-react';
|
||||
import { adminApi, type ChannelGroup, type DictionaryItem, type EnterpriseApplication } from '@/api/adminApi';
|
||||
import { Breadcrumb, Button, Input, Select, Tag } from '@/components/ui';
|
||||
|
||||
type Carrier = 'mobile' | 'unicom' | 'telecom';
|
||||
|
||||
const carrierMeta: Record<Carrier, { label: string; description: string }> = {
|
||||
mobile: { label: '移动', description: '移动号码只会进入移动通道组' },
|
||||
unicom: { label: '联通', description: '联通号码只会进入联通通道组' },
|
||||
telecom: { label: '电信', description: '电信号码只会进入电信通道组' },
|
||||
};
|
||||
|
||||
export function AdminSmsApplicationFormPage() {
|
||||
const navigate = useNavigate();
|
||||
@@ -20,70 +28,128 @@ export function AdminSmsApplicationFormPage() {
|
||||
const [unicomGroupId, setUnicomGroupId] = useState('');
|
||||
const [telecomGroupId, setTelecomGroupId] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
adminApi.listChannelGroups()
|
||||
.then((items) => setGroups(items.filter((item) => item.status !== 'disabled' && item.status !== 'deleted')))
|
||||
.catch((failure: Error) => setError(failure.message || '通道组加载失败'));
|
||||
}, []);
|
||||
let cancelled = false;
|
||||
async function loadForm() {
|
||||
try {
|
||||
const [groupItems, application, routeRules] = await Promise.all([
|
||||
adminApi.listChannelGroups(),
|
||||
isEdit && appId ? adminApi.getEnterpriseApplication(appId) : Promise.resolve<EnterpriseApplication | null>(null),
|
||||
isEdit ? adminApi.listChannelRouteRules() : Promise.resolve<DictionaryItem[]>([]),
|
||||
]);
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
setGroups(groupItems.filter((item) => item.status !== 'disabled' && item.status !== 'deleted'));
|
||||
if (application) {
|
||||
if (enterpriseId && application.tenantId !== enterpriseId) {
|
||||
setError('应用不属于当前企业,已停止加载');
|
||||
return;
|
||||
}
|
||||
hydrateApplication(application, routeRules);
|
||||
}
|
||||
} catch (failure) {
|
||||
if (!cancelled) {
|
||||
setError(failure instanceof Error ? failure.message : '短信应用加载失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void loadForm();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [appId, enterpriseId, isEdit]);
|
||||
|
||||
function goBack() {
|
||||
navigate('/admin/enterprise-applications');
|
||||
}
|
||||
|
||||
function submit() {
|
||||
function hydrateApplication(application: EnterpriseApplication, routeRules: DictionaryItem[]) {
|
||||
setAppName(application.name);
|
||||
setScene(application.scene ?? '');
|
||||
setDailyLimit(application.dailyLimit ? String(application.dailyLimit) : '');
|
||||
setCustomerUnitPrice(((application.customerUnitPrice ?? 0) / 100).toFixed(4));
|
||||
setPhoneDailyLimit(application.maxPhonesPerTask ? String(application.maxPhonesPerTask) : '');
|
||||
setMismatchPolicy(application.templateMismatchMode ?? 'reject');
|
||||
setIpAddress(application.ipAllowlist?.map((item) => item.ipCidr).join('\n') ?? '');
|
||||
|
||||
const activeRules = routeRules.filter((rule) => (
|
||||
rule.applicationId === application.id
|
||||
&& rule.status !== 'deleted'
|
||||
&& !rule.province
|
||||
&& !rule.channelId
|
||||
));
|
||||
setMobileGroupId(getRouteGroupId(activeRules, 'mobile'));
|
||||
setUnicomGroupId(getRouteGroupId(activeRules, 'unicom'));
|
||||
setTelecomGroupId(getRouteGroupId(activeRules, 'telecom'));
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (!enterpriseId) {
|
||||
setError('缺少企业 ID');
|
||||
return;
|
||||
}
|
||||
if (isEdit) {
|
||||
setError('短信应用编辑接口待补,当前不做本地模拟保存');
|
||||
return;
|
||||
}
|
||||
const selectedGroups = [
|
||||
{ carrier: 'mobile', groupId: mobileGroupId },
|
||||
{ carrier: 'unicom', groupId: unicomGroupId },
|
||||
{ carrier: 'telecom', groupId: telecomGroupId },
|
||||
{ carrier: 'mobile' as Carrier, groupId: mobileGroupId },
|
||||
{ carrier: 'unicom' as Carrier, groupId: unicomGroupId },
|
||||
{ carrier: 'telecom' as Carrier, groupId: telecomGroupId },
|
||||
].filter((item) => item.groupId);
|
||||
if (selectedGroups.length === 0) {
|
||||
setError('请至少配置一个运营商通道组');
|
||||
return;
|
||||
}
|
||||
adminApi.createEnterpriseApplication({
|
||||
tenantId: enterpriseId,
|
||||
const payload = {
|
||||
name: appName,
|
||||
scene,
|
||||
dailyLimit: Number(dailyLimit) || undefined,
|
||||
customerUnitPrice: Math.round(Number(customerUnitPrice || 0) * 100),
|
||||
maxPhonesPerTask: Number(phoneDailyLimit) || undefined,
|
||||
templateMismatchMode: mismatchPolicy,
|
||||
ipAllowlist: ipAddress ? [ipAddress] : [],
|
||||
})
|
||||
.then(async (application) => {
|
||||
await Promise.all(selectedGroups.map((item, index) => adminApi.createChannelRouteRule({
|
||||
tenantId: enterpriseId,
|
||||
applicationId: application.id,
|
||||
groupId: item.groupId,
|
||||
ipAllowlist: parseIpAllowlist(ipAddress),
|
||||
};
|
||||
|
||||
setSaving(true);
|
||||
setError('');
|
||||
try {
|
||||
const application = isEdit && appId
|
||||
? await adminApi.updateEnterpriseApplication(appId, payload)
|
||||
: await adminApi.createEnterpriseApplication({ tenantId: enterpriseId, ...payload });
|
||||
await adminApi.replaceApplicationRouteRules(application.id, {
|
||||
routes: selectedGroups.map((item, index) => ({
|
||||
carrier: item.carrier,
|
||||
groupId: item.groupId,
|
||||
priority: (index + 1) * 10,
|
||||
status: 'active',
|
||||
})));
|
||||
goBack();
|
||||
})
|
||||
.catch((failure: Error) => setError(failure.message || '短信应用保存失败'));
|
||||
})),
|
||||
});
|
||||
goBack();
|
||||
} catch (failure) {
|
||||
setError(failure instanceof Error ? failure.message : '短信应用保存失败');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
const groupOptionsByCarrier = (carrier: ChannelGroup['carrier']) => [
|
||||
{ label: '不配置', value: '' },
|
||||
...groups.filter((group) => group.carrier === carrier).map((group) => ({ label: group.name, value: group.id })),
|
||||
];
|
||||
const selectedGroupCount = [mobileGroupId, unicomGroupId, telecomGroupId].filter(Boolean).length;
|
||||
const routeCards: Array<{ carrier: Carrier; groupId: string; onChange: (value: string) => void }> = [
|
||||
{ carrier: 'mobile', groupId: mobileGroupId, onChange: setMobileGroupId },
|
||||
{ carrier: 'unicom', groupId: unicomGroupId, onChange: setUnicomGroupId },
|
||||
{ carrier: 'telecom', groupId: telecomGroupId, onChange: setTelecomGroupId },
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="page-stack admin-app-form-page">
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<Breadcrumb items={[isEdit ? '编辑短信应用' : '添加短信应用']} />
|
||||
<p>短信应用写入真实应用表;编辑能力待后端接口补齐后开放。</p>
|
||||
<p>短信应用和三网通道组配置写入真实后台接口。</p>
|
||||
</div>
|
||||
<Button icon={<ArrowLeft size={16} />} onClick={goBack} variant="ghost">返回企业应用管理</Button>
|
||||
</div>
|
||||
@@ -91,7 +157,7 @@ export function AdminSmsApplicationFormPage() {
|
||||
|
||||
<div className="surface admin-app-form-card">
|
||||
<section className="ui-detail-section">
|
||||
<div className="ui-detail-section__header"><h3>业务信息</h3></div>
|
||||
<div className="ui-detail-section__header"><h3>业务信息</h3><p>先填写应用基础信息,保存后将生成真实企业应用。</p></div>
|
||||
<div className="admin-app-form-grid">
|
||||
<Input label="应用名称" onChange={(event) => setAppName(event.target.value)} placeholder="请输入应用名称" required value={appName} />
|
||||
<Input label="应用场景" onChange={(event) => setScene(event.target.value)} placeholder="行业通知/营销推广/验证码" value={scene} />
|
||||
@@ -109,18 +175,62 @@ export function AdminSmsApplicationFormPage() {
|
||||
required
|
||||
value={mismatchPolicy}
|
||||
/>
|
||||
<Input label="IP 白名单" onChange={(event) => setIpAddress(event.target.value)} placeholder="例如 192.168.1.100/32" value={ipAddress} />
|
||||
<Select label="移动通道组" onChange={(event) => setMobileGroupId(event.target.value)} options={groupOptionsByCarrier('mobile')} value={mobileGroupId} />
|
||||
<Select label="联通通道组" onChange={(event) => setUnicomGroupId(event.target.value)} options={groupOptionsByCarrier('unicom')} value={unicomGroupId} />
|
||||
<Select label="电信通道组" onChange={(event) => setTelecomGroupId(event.target.value)} options={groupOptionsByCarrier('telecom')} value={telecomGroupId} />
|
||||
<Input label="IP 白名单" onChange={(event) => setIpAddress(event.target.value)} placeholder="多个 IP/CIDR 可用逗号、空格或换行分隔" value={ipAddress} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="ui-detail-section">
|
||||
<div className="ui-detail-section__header">
|
||||
<div>
|
||||
<h3>运营商通道组</h3>
|
||||
<p>至少选择一个运营商通道组;每个运营商只能绑定同运营商通道组。</p>
|
||||
</div>
|
||||
<Tag tone={selectedGroupCount > 0 ? 'success' : 'warning'}>{selectedGroupCount}/3 已配置</Tag>
|
||||
</div>
|
||||
<div className="admin-app-route-grid">
|
||||
{routeCards.map((card) => {
|
||||
const available = groups.filter((group) => group.carrier === card.carrier);
|
||||
const meta = carrierMeta[card.carrier];
|
||||
return (
|
||||
<div className={['admin-app-route-card', card.groupId ? 'is-selected' : ''].filter(Boolean).join(' ')} key={card.carrier}>
|
||||
<header>
|
||||
<span><RadioTower size={18} /></span>
|
||||
<div>
|
||||
<strong>{meta.label}通道组</strong>
|
||||
<small>{meta.description}</small>
|
||||
</div>
|
||||
<Tag tone={card.groupId ? 'success' : available.length ? 'neutral' : 'warning'}>{card.groupId ? '已选择' : `${available.length} 个可选`}</Tag>
|
||||
</header>
|
||||
<Select
|
||||
label={`${meta.label}通道组`}
|
||||
onChange={(event) => card.onChange(event.target.value)}
|
||||
options={groupOptionsByCarrier(card.carrier)}
|
||||
value={card.groupId}
|
||||
/>
|
||||
{!available.length ? <p>暂无可用{meta.label}通道组,请先在通道组管理创建。</p> : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="enterprise-form-footer">
|
||||
<Button disabled={!appName || isEdit} onClick={submit}>{isEdit ? '编辑待补接口' : '创建应用'}</Button>
|
||||
<Button disabled={!appName || selectedGroupCount === 0 || saving} onClick={() => { void submit(); }}>{saving ? '保存中...' : isEdit ? '保存应用' : '创建应用'}</Button>
|
||||
<Button onClick={goBack} variant="ghost">取消</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function getRouteGroupId(routeRules: DictionaryItem[], carrier: Carrier) {
|
||||
const rule = routeRules.find((item) => item.carrier === carrier);
|
||||
return typeof rule?.groupId === 'string' ? rule.groupId : '';
|
||||
}
|
||||
|
||||
function parseIpAllowlist(value: string) {
|
||||
return value
|
||||
.split(/[\s,,]+/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user