feat: add carrier-aware signature retirement alerts

This commit is contained in:
hectorzhao
2026-08-10 20:54:05 +08:00
parent 232d1c22a3
commit 55aa054005
52 changed files with 3074 additions and 152 deletions
+15 -8
View File
@@ -1,8 +1,8 @@
import { useState } from 'react';
import { Button, Input, Modal, Select } from '@/components/ui';
import { isValidMoneyInput, moneyUnitsToYuan, yuanToMoneyUnits } from '@/utils/currency';
import { carrierLabelMap, cmppVersionOptions, regionOptions } from './channelModel';
import type { Carrier, ChannelModalState, LongMessageReceiptMode, SmsChannel } from './channelTypes';
import { baseCarrierOptions, cmppVersionOptions, regionOptions } from './channelModel';
import type { BaseCarrier, ChannelModalState, LongMessageReceiptMode, SmsChannel } from './channelTypes';
export function ChannelFormModal({
modal,
@@ -15,7 +15,8 @@ export function ChannelFormModal({
}) {
const channel = modal.channel;
const [name, setName] = useState(channel?.name ?? '');
const [carrier, setCarrier] = useState<Carrier>(channel?.carrier ?? 'mobile');
const [carriers, setCarriers] = useState<BaseCarrier[]>(channel?.carriers ?? ['mobile']);
const [carrierError, setCarrierError] = useState('');
const [unitPrice, setUnitPrice] = useState(channel ? moneyUnitsToYuan(channel.unitPrice).toFixed(4) : '0.0300');
const [unitPriceError, setUnitPriceError] = useState('');
const [region, setRegion] = useState(channel?.sendRegion ?? '全国');
@@ -36,6 +37,10 @@ export function ChannelFormModal({
const [longMessageReceiptMode, setLongMessageReceiptMode] = useState<LongMessageReceiptMode>(channel?.longMessageReceiptMode ?? 'per_segment');
function submit() {
if (carriers.length === 0) {
setCarrierError('至少选择一个运营商');
return;
}
if (!isValidMoneyInput(unitPrice)) {
setUnitPriceError('单价必须是非负金额,且最多保留小数点后 4 位');
return;
@@ -43,7 +48,8 @@ export function ChannelFormModal({
onSubmit({
id: channel?.id ?? String(Math.floor(10000 + Math.random() * 80000)),
name: name || '新建短信通道',
carrier,
carrier: carriers.length === 3 ? 'all' : carriers[0],
carriers,
sendRegion: region,
unitPrice: yuanToMoneyUnits(unitPrice),
status: channel?.status ?? 'connecting',
@@ -94,12 +100,13 @@ export function ChannelFormModal({
<Input label="* 通道名称" onChange={(event) => setName(event.target.value)} placeholder="请输入通道名称" value={name} />
<div className="sms-channel-radio-row">
<span>* </span>
{(['mobile', 'unicom', 'telecom', 'all'] as const).map((item) => (
<label key={item}>
<input checked={carrier === item} onChange={() => setCarrier(item)} type="radio" />
{carrierLabelMap[item]}
{baseCarrierOptions.map((item) => (
<label key={item.value}>
<input checked={carriers.includes(item.value)} onChange={() => { setCarriers((current) => current.includes(item.value) ? current.filter((carrier) => carrier !== item.value) : [...current, item.value]); setCarrierError(''); }} type="checkbox" />
{item.label}
</label>
))}
{carrierError ? <small className="form-error">{carrierError}</small> : null}
</div>
<Input error={unitPriceError} label="* 单价(元)" min="0" onChange={(event) => { setUnitPrice(event.target.value); setUnitPriceError(''); }} step="0.0001" type="number" value={unitPrice} />
<Select label="* 发送地区" onChange={(event) => setRegion(event.target.value)} options={regionOptions} value={region} />
+1 -1
View File
@@ -57,7 +57,7 @@ export function ChannelTable({
<span> ID{channel.id}</span>
</div>
<div className="sms-channel-carrier-price">
<Tag tone={carrierToneMap[channel.carrier]}>{carrierLabelMap[channel.carrier]}</Tag>
<div>{channel.carriers.map((carrier) => <Tag key={carrier} tone={carrierToneMap[carrier]}>{carrierLabelMap[carrier]}</Tag>)}</div>
<strong>{formatCents(channel.unitPrice)} </strong>
</div>
<div className="sms-channel-status-cell">
+14 -2
View File
@@ -1,5 +1,5 @@
import type { AdminChannel, ChannelQualityStat, CmppConnectionState } from '@/api/adminApi';
import type { Carrier, ChannelStatus, SmsChannel } from './channelTypes';
import type { BaseCarrier, Carrier, ChannelStatus, SmsChannel } from './channelTypes';
export const connectionStatusLabelMap: Record<string, string> = {
connected: '已连接',
@@ -44,6 +44,12 @@ export const carrierLabelMap: Record<Carrier, string> = {
all: '三网',
};
export const baseCarrierOptions: Array<{ label: string; value: BaseCarrier }> = [
{ label: '移动', value: 'mobile' },
{ label: '联通', value: 'unicom' },
{ label: '电信', value: 'telecom' },
];
export const carrierToneMap: Record<Carrier, 'info' | 'danger' | 'success' | 'neutral'> = {
mobile: 'info',
unicom: 'danger',
@@ -93,10 +99,16 @@ export function mapApiChannel(
connections: CmppConnectionState[] = channel.connectionStates ?? [],
quality?: ChannelQualityStat,
): SmsChannel {
const carriers: BaseCarrier[] = channel.carriers?.length
? channel.carriers as BaseCarrier[]
: channel.carrier === 'all'
? ['mobile', 'unicom', 'telecom'] as BaseCarrier[]
: [channel.carrier === 'unicom' || channel.carrier === 'telecom' ? channel.carrier : 'mobile'];
return {
id: channel.id,
name: channel.name,
carrier: channel.carrier === 'unicom' || channel.carrier === 'telecom' || channel.carrier === 'all' ? channel.carrier : 'mobile',
carriers,
sendRegion: channel.sendRegion ?? '全国',
unitPrice: channel.unitPrice,
status: resolveChannelStatus(channel, connections),
@@ -133,7 +145,7 @@ export function mapUiStatusToApi(channel: SmsChannel) {
export function buildChannelPayload(channel: SmsChannel, passwordCipher?: string) {
return {
name: channel.name,
carrier: channel.carrier,
carriers: channel.carriers,
sendRegion: channel.sendRegion,
gatewayHost: channel.gatewayHost,
gatewayPort: Number(channel.gatewayPort),
+2
View File
@@ -1,6 +1,7 @@
import type { ChannelConnectionLogResponse } from '@/api/adminApi';
export type Carrier = 'mobile' | 'unicom' | 'telecom' | 'all';
export type BaseCarrier = Exclude<Carrier, 'all'>;
export type ChannelStatus = 'normal' | 'stopped' | 'connecting' | 'failed';
export type LongMessageReceiptMode = 'per_segment' | 'message_level';
@@ -8,6 +9,7 @@ export type SmsChannel = {
id: string;
name: string;
carrier: Carrier;
carriers: BaseCarrier[];
sendRegion: string;
unitPrice: number;
status: ChannelStatus;