feat: 优化通道组编辑交互
This commit is contained in:
@@ -0,0 +1,263 @@
|
||||
import { useId, useState } from 'react';
|
||||
import { CheckCircle2, Info, Search } from 'lucide-react';
|
||||
import type { AdminChannel } from '@/api/adminApi';
|
||||
import { Button, CarrierTag, Input, Modal, Select, Tag } from '@/components/ui';
|
||||
import { formatRateAmount, MONEY_UNITS_PER_YUAN } from '@/utils/currency';
|
||||
import { isValidPriority, normalizeRegion, type NationalRoute, type ProvinceRoute } from './model';
|
||||
import './RouteConfigModal.css';
|
||||
|
||||
type Carrier = 'mobile' | 'unicom' | 'telecom';
|
||||
export type RouteModalState = {
|
||||
type: 'province' | 'national';
|
||||
mode: 'create' | 'edit';
|
||||
route?: ProvinceRoute | NationalRoute;
|
||||
};
|
||||
|
||||
const carrierLabels: Record<Carrier, string> = { mobile: '移动', unicom: '联通', telecom: '电信' };
|
||||
|
||||
function isCarrierCompatible(channel: AdminChannel, carrier: Carrier) {
|
||||
return channel.carriers?.length
|
||||
? channel.carriers.includes(carrier)
|
||||
: !channel.carrier || channel.carrier === 'all' || channel.carrier === carrier;
|
||||
}
|
||||
|
||||
function connectionLabel(channel: AdminChannel) {
|
||||
if (channel.status === 'deleted') return { text: '已删除', tone: 'neutral' as const };
|
||||
if (channel.status !== 'active') return { text: '已停用', tone: 'neutral' as const };
|
||||
const states = channel.connectionStates ?? [];
|
||||
const connected = states.filter(
|
||||
(state) => state.status === 'connected' && state.desiredConnections > 0 && state.currentConnections > 0,
|
||||
);
|
||||
if (connected.length) {
|
||||
return {
|
||||
text: `已连接 · ${connected.reduce((sum, state) => sum + state.currentConnections, 0)} 条`,
|
||||
tone: 'success' as const,
|
||||
};
|
||||
}
|
||||
if (!states.length) return { text: '暂无连接回写', tone: 'neutral' as const };
|
||||
return {
|
||||
text: states.some((state) => state.status === 'connecting') ? '连接中' : '未连接',
|
||||
tone: 'warning' as const,
|
||||
};
|
||||
}
|
||||
|
||||
export function RouteConfigModal({
|
||||
channels,
|
||||
carrier,
|
||||
modal,
|
||||
occupiedChannelIds,
|
||||
nextPriority,
|
||||
onClose,
|
||||
onSubmit,
|
||||
}: {
|
||||
channels: AdminChannel[];
|
||||
carrier: Carrier;
|
||||
modal: RouteModalState;
|
||||
occupiedChannelIds: string[];
|
||||
nextPriority: number;
|
||||
onClose: () => void;
|
||||
onSubmit: (route: ProvinceRoute | NationalRoute) => string | null;
|
||||
}) {
|
||||
const provinceRoute = modal.type === 'province' ? (modal.route as ProvinceRoute | undefined) : undefined;
|
||||
const nationalRoute = modal.type === 'national' ? (modal.route as NationalRoute | undefined) : undefined;
|
||||
const initialPriority = String(nationalRoute?.priority ?? nextPriority);
|
||||
const [province, setProvince] = useState(provinceRoute?.province ?? '');
|
||||
const [priority, setPriority] = useState(initialPriority);
|
||||
const [channelId, setChannelId] = useState(modal.route?.channelId ?? '');
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const selectionName = useId();
|
||||
const selected = channels.find((channel) => channel.id === channelId);
|
||||
const dirty =
|
||||
province !== (provinceRoute?.province ?? '') ||
|
||||
priority !== initialPriority ||
|
||||
channelId !== (modal.route?.channelId ?? '');
|
||||
const provinces = channels
|
||||
.filter((channel) => isCarrierCompatible(channel, carrier))
|
||||
.map((channel) => channel.sendRegion)
|
||||
.filter((region): region is string => Boolean(region && normalizeRegion(region) !== '全国'));
|
||||
if (provinceRoute?.province) provinces.push(provinceRoute.province);
|
||||
const provinceOptions = [
|
||||
{ label: '请选择省份', value: '' },
|
||||
...Array.from(new Set(provinces))
|
||||
.sort()
|
||||
.map((region) => ({ label: region, value: region })),
|
||||
];
|
||||
|
||||
function unavailableReason(channel: AdminChannel) {
|
||||
if (channel.status === 'deleted') return '通道已删除';
|
||||
if (!isCarrierCompatible(channel, carrier)) return `不支持${carrierLabels[carrier]}`;
|
||||
if (channel.id !== modal.route?.channelId && occupiedChannelIds.includes(channel.id)) return '已在当前通道组中配置';
|
||||
if (modal.type === 'province') {
|
||||
if (!province) return '请先选择省份';
|
||||
if (normalizeRegion(channel.sendRegion) !== normalizeRegion(province)) return '通道地区与所选省份不匹配';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
const query = keyword.trim().toLocaleLowerCase();
|
||||
const visibleChannels = channels.filter((channel) =>
|
||||
`${channel.name} ${channel.code} ${channel.sendRegion ?? '全国'}`.toLocaleLowerCase().includes(query),
|
||||
);
|
||||
const availableCount = visibleChannels.filter((channel) => !unavailableReason(channel)).length;
|
||||
|
||||
function submit() {
|
||||
if (modal.type === 'province' && !province) {
|
||||
setError('请选择省份');
|
||||
return;
|
||||
}
|
||||
if (!selected) {
|
||||
setError(channelId ? '原通道不存在,请重新选择' : '请选择通道');
|
||||
return;
|
||||
}
|
||||
const reason = unavailableReason(selected);
|
||||
if (reason) {
|
||||
setError(reason);
|
||||
return;
|
||||
}
|
||||
if (modal.type === 'national' && (!priority.trim() || !isValidPriority(Number(priority)))) {
|
||||
setError('优先级需为 -2147483648 到 2147483647 的整数');
|
||||
return;
|
||||
}
|
||||
const route =
|
||||
modal.type === 'province'
|
||||
? { id: provinceRoute?.id ?? `p-${selectionName}`, province, channelId }
|
||||
: { id: nationalRoute?.id ?? `n-${selectionName}`, priority: Number(priority), channelId };
|
||||
setError(onSubmit(route) ?? '');
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="channel-route-editor"
|
||||
dirty={dirty}
|
||||
footer={({ requestClose }) => (
|
||||
<>
|
||||
<Button onClick={requestClose} variant="ghost">
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={submit}>确认</Button>
|
||||
</>
|
||||
)}
|
||||
onClose={onClose}
|
||||
open
|
||||
size="xl"
|
||||
title={modal.mode === 'edit' ? '编辑通道' : '添加通道'}
|
||||
>
|
||||
<div className="channel-route-editor__content">
|
||||
{modal.type === 'province' ? (
|
||||
<Select
|
||||
label="选择省份"
|
||||
required
|
||||
onChange={(event) => {
|
||||
setProvince(event.target.value);
|
||||
setChannelId('');
|
||||
setError('');
|
||||
}}
|
||||
options={provinceOptions}
|
||||
value={province}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
label="优先级"
|
||||
required
|
||||
type="number"
|
||||
step="1"
|
||||
min="-2147483648"
|
||||
max="2147483647"
|
||||
onChange={(event) => {
|
||||
setPriority(event.target.value);
|
||||
setError('');
|
||||
}}
|
||||
value={priority}
|
||||
hint="数值越小越先使用;失败补发会跳到下一优先级。"
|
||||
/>
|
||||
)}
|
||||
<Input
|
||||
label="搜索通道"
|
||||
onChange={(event) => setKeyword(event.target.value)}
|
||||
placeholder="输入通道名称、编号或地区"
|
||||
prefix={<Search aria-hidden="true" size={16} />}
|
||||
value={keyword}
|
||||
/>
|
||||
<p className="channel-route-editor__note">
|
||||
<Info aria-hidden="true" size={16} />
|
||||
<span>连接状态来自最近一次真实回写;暂时断连不影响配置,实际发送仍受可用性校验。</span>
|
||||
</p>
|
||||
<fieldset className="channel-route-editor__choices">
|
||||
<legend>
|
||||
选择通道 · {visibleChannels.length} 个结果,{availableCount} 个可选
|
||||
</legend>
|
||||
{visibleChannels.map((channel) => {
|
||||
const reason = unavailableReason(channel);
|
||||
const connection = connectionLabel(channel);
|
||||
return (
|
||||
<label
|
||||
className={`channel-route-editor__choice${channelId === channel.id ? ' is-selected' : ''}${reason ? ' is-disabled' : ''}`}
|
||||
key={channel.id}
|
||||
>
|
||||
<input
|
||||
aria-label={`选择通道 ${channel.name}(${channel.code})`}
|
||||
checked={channelId === channel.id}
|
||||
disabled={Boolean(reason)}
|
||||
name={selectionName}
|
||||
onChange={() => {
|
||||
setChannelId(channel.id);
|
||||
setError('');
|
||||
}}
|
||||
type="radio"
|
||||
value={channel.id}
|
||||
/>
|
||||
<span className="channel-route-editor__identity">
|
||||
<strong>{channel.name}</strong>
|
||||
<span className="channel-route-editor__code">{channel.code}</span>
|
||||
<span className="channel-route-editor__carriers">
|
||||
{(channel.carriers?.length ? channel.carriers : [channel.carrier ?? '未标记']).map((item) => (
|
||||
<CarrierTag carrier={item} key={item} />
|
||||
))}
|
||||
</span>
|
||||
</span>
|
||||
<span className="channel-route-editor__facts">
|
||||
<span>成本价格(元/条)</span>
|
||||
<strong>
|
||||
{channel.unitPrice == null ? '—' : formatRateAmount(channel.unitPrice / MONEY_UNITS_PER_YUAN)}
|
||||
</strong>
|
||||
</span>
|
||||
<span className="channel-route-editor__facts">
|
||||
<span>地区</span>
|
||||
<strong>{channel.sendRegion || '全国'}</strong>
|
||||
</span>
|
||||
<span className="channel-route-editor__connection">
|
||||
<Tag tone={connection.tone}>{connection.text}</Tag>
|
||||
{reason ? <span className="channel-route-editor__reason">{reason}</span> : null}
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
{visibleChannels.length === 0 ? (
|
||||
<p className="channel-route-editor__empty">
|
||||
{channels.length ? '没有匹配的通道,请调整搜索条件' : '暂无通道数据'}
|
||||
</p>
|
||||
) : null}
|
||||
</fieldset>
|
||||
{selected ? (
|
||||
<p className="channel-route-editor__selected">
|
||||
<CheckCircle2 aria-hidden="true" size={17} />
|
||||
<span>
|
||||
已选:{selected.name}({selected.code})
|
||||
</span>
|
||||
</p>
|
||||
) : null}
|
||||
{channelId && !selected ? (
|
||||
<p className="channel-route-editor__error" role="alert">
|
||||
原通道不存在,请重新选择
|
||||
</p>
|
||||
) : null}
|
||||
{error ? (
|
||||
<p className="channel-route-editor__error" role="alert">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user