feat: add carrier-aware signature retirement alerts
This commit is contained in:
@@ -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} />
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user