feat: 优化通道组编辑交互

This commit is contained in:
hectorzhao
2026-09-05 22:55:09 +08:00
parent 839dba8d9b
commit 1e05a643e5
14 changed files with 1752 additions and 462 deletions
@@ -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>
);
}