Files
lislgosms/src/apps/admin/channels/ChannelFormModal.tsx
T

284 lines
11 KiB
TypeScript

import { useState } from 'react';
import { Button, Input, Modal, Select } from '@/components/ui';
import { isValidMoneyInput, moneyUnitsToYuan, yuanToMoneyUnits } from '@/utils/currency';
import { baseCarrierOptions, cmppVersionOptions, regionOptions } from './channelModel';
import type { BaseCarrier, ChannelModalState, LongMessageReceiptMode, SmsChannel } from './channelTypes';
export function ChannelFormModal({
modal,
onClose,
onSubmit,
}: {
modal: ChannelModalState;
onClose: () => void;
onSubmit: (channel: SmsChannel) => void;
}) {
const channel = modal.channel;
const [name, setName] = useState(channel?.name ?? '');
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 ?? '全国');
const [gatewayHost, setGatewayHost] = useState(channel?.gatewayHost ?? '');
const [gatewayPort, setGatewayPort] = useState(channel?.gatewayPort ?? '7890');
const [businessCode, setBusinessCode] = useState(channel?.businessCode ?? 'SMS');
const [corpCode, setCorpCode] = useState(channel?.corpCode ?? '');
const [account, setAccount] = useState(channel?.account ?? '');
const [cmppVersion, setCmppVersion] = useState<'2.0' | '3.0'>(channel?.cmppVersion ?? '2.0');
const [password, setPassword] = useState('');
const [accessNo, setAccessNo] = useState(channel?.accessNo ?? '');
const [extensionDigits, setExtensionDigits] = useState(String(channel?.extensionDigits ?? 0));
const [flowLimit, setFlowLimit] = useState(String(channel?.rateLimitPerSecond ?? 100));
const [desiredConnections, setDesiredConnections] = useState(String(channel?.desiredConnections ?? 1));
const [windowSize, setWindowSize] = useState(String(channel?.windowSize ?? 16));
const [heartbeatIntervalSeconds, setHeartbeatIntervalSeconds] = useState(
String(channel?.heartbeatIntervalSeconds ?? 30),
);
const [heartbeatMissThreshold, setHeartbeatMissThreshold] = useState(String(channel?.heartbeatMissThreshold ?? 3));
const [longMessageReceiptMode, setLongMessageReceiptMode] = useState<LongMessageReceiptMode>(
channel?.longMessageReceiptMode ?? 'per_segment',
);
function submit() {
if (carriers.length === 0) {
setCarrierError('至少选择一个运营商');
return;
}
if (!isValidMoneyInput(unitPrice)) {
setUnitPriceError('单价必须是非负金额,且最多保留小数点后 4 位');
return;
}
onSubmit({
id: channel?.id ?? String(Math.floor(10000 + Math.random() * 80000)),
name: name || '新建短信通道',
carrier: carriers.length === 3 ? 'all' : carriers[0],
carriers,
sendRegion: region,
unitPrice: yuanToMoneyUnits(unitPrice),
status: channel?.status ?? 'connecting',
total: channel?.total ?? 0,
submitFailureRate: channel?.submitFailureRate ?? 0,
submitFailureCount: channel?.submitFailureCount ?? 0,
successRate: channel?.successRate ?? 0,
successCount: channel?.successCount ?? 0,
unknownRate: channel?.unknownRate ?? 0,
unknownCount: channel?.unknownCount ?? 0,
failureRate: channel?.failureRate ?? 0,
failureCount: channel?.failureCount ?? 0,
gatewayHost,
gatewayPort,
businessCode: businessCode.trim() || 'SMS',
corpCode,
account,
accessNo,
cmppVersion,
desiredConnections: Number(desiredConnections) || 1,
windowSize: Number(windowSize) || 16,
heartbeatIntervalSeconds: Number(heartbeatIntervalSeconds) || 30,
heartbeatMissThreshold: Number(heartbeatMissThreshold) || 3,
extensionDigits: Number(extensionDigits),
longMessageReceiptMode,
rateLimitPerSecond: Number(flowLimit),
passwordCipher: password || undefined,
});
}
return (
<Modal
closeOnBackdrop={modal.mode === 'edit'}
closeOnEscape={modal.mode === 'edit'}
footer={
<>
<Button onClick={onClose} variant="ghost">
关闭
</Button>
<Button onClick={submit}>确认</Button>
</>
}
onClose={onClose}
open
size="xl"
title={
<div className="template-modal-title">
<h2>{modal.mode === 'edit' ? '编辑通道' : '创建通道'}</h2>
</div>
}
>
<div className="sms-channel-form">
<section>
<h3>业务信息</h3>
<div className="sms-channel-form-grid">
<Input
label="* 通道名称"
onChange={(event) => setName(event.target.value)}
placeholder="请输入通道名称"
value={name}
/>
<div className="sms-channel-radio-row">
<span>* 运营商</span>
{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}
/>
</div>
</section>
<section>
<h3>参数配置</h3>
<div className="sms-channel-form-grid">
<Input disabled label="* 协议选择" value="CMPP" />
<div className="sms-channel-inline-field">
<Input
label="* 网关地址"
onChange={(event) => setGatewayHost(event.target.value)}
placeholder="请输入网关地址"
value={gatewayHost}
/>
<Input label="端口" onChange={(event) => setGatewayPort(event.target.value)} value={gatewayPort} />
</div>
<Input
hint="对应 CMPP Service_Id,最多 10 个 ASCII 字符。"
label="* 业务代码"
maxLength={10}
onChange={(event) => setBusinessCode(event.target.value.toUpperCase())}
value={businessCode}
/>
<Input
label="* 企业代码"
onChange={(event) => setCorpCode(event.target.value)}
placeholder="请输入企业代码"
value={corpCode}
/>
<Input
label="* 网关账号"
onChange={(event) => setAccount(event.target.value)}
placeholder="请输入网关账号"
value={account}
/>
<Select
label="* CMPP版本"
onChange={(event) => setCmppVersion(event.target.value as '2.0' | '3.0')}
options={cmppVersionOptions}
value={cmppVersion}
/>
<Input
hint={modal.mode === 'edit' ? '已配置的密码不会回显;留空保持不变,填写新密码才更新。' : undefined}
label="网关密码"
autoComplete="new-password"
name="cmpp-gateway-password"
onChange={(event) => setPassword(event.target.value)}
placeholder={modal.mode === 'edit' ? '留空不修改' : '请输入网关密码'}
required={modal.mode === 'create'}
type="password"
value={password}
/>
<div className="sms-channel-inline-field">
<Input
autoComplete="off"
label="* 接入号"
name="cmpp-access-number"
onChange={(event) => setAccessNo(event.target.value)}
placeholder="请输入通道接入号"
value={accessNo}
/>
<Input
label="扩展位数"
max="20"
min="0"
onChange={(event) => setExtensionDigits(event.target.value)}
type="number"
value={extensionDigits}
/>
</div>
<Input
label="* 通道流速"
max="2000"
min="1"
onChange={(event) => setFlowLimit(event.target.value)}
suffix="条/秒"
type="number"
value={flowLimit}
/>
<Input
label="* 期望连接数"
onChange={(event) => setDesiredConnections(event.target.value)}
placeholder="1"
value={desiredConnections}
/>
<Input
label="* 提交窗口"
onChange={(event) => setWindowSize(event.target.value)}
placeholder="16"
value={windowSize}
/>
<Input
hint="平台主动向供应商发送 ACTIVE_TEST 的间隔"
label="* 心跳间隔"
min="1"
onChange={(event) => setHeartbeatIntervalSeconds(event.target.value)}
suffix="秒"
type="number"
value={heartbeatIntervalSeconds}
/>
<Input
hint="连续未收到心跳响应达到该次数后重连"
label="* 心跳失败阈值"
min="1"
onChange={(event) => setHeartbeatMissThreshold(event.target.value)}
suffix="次"
type="number"
value={heartbeatMissThreshold}
/>
<Select
label="* 长短信成功回执口径"
onChange={(event) => setLongMessageReceiptMode(event.target.value as LongMessageReceiptMode)}
options={[
{ label: '逐分片回执(默认)', value: 'per_segment' },
{ label: '整条级回执(一条成功代表全部成功)', value: 'message_level' },
]}
value={longMessageReceiptMode}
/>
<p className="page-inline-hint">
仅在供应商明确约定长短信成功只返回一条整条级回执时选择“整条级”,否则保持逐分片。
</p>
</div>
</section>
</div>
</Modal>
);
}