246 lines
9.4 KiB
TypeScript
246 lines
9.4 KiB
TypeScript
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 { 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,
|
||
onClose,
|
||
onSubmit,
|
||
}: {
|
||
channels: AdminChannel[];
|
||
carrier: Carrier;
|
||
modal: RouteModalState;
|
||
occupiedChannelIds: string[];
|
||
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 [province, setProvince] = useState(provinceRoute?.province ?? '');
|
||
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 ?? '') || channelId !== (modal.route?.channelId ?? '');
|
||
const provinces = channels
|
||
.filter(
|
||
(channel) =>
|
||
channel.status !== 'deleted' &&
|
||
isCarrierCompatible(channel, carrier) &&
|
||
(channel.id === modal.route?.channelId || !occupiedChannelIds.includes(channel.id)),
|
||
)
|
||
.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) =>
|
||
!unavailableReason(channel) &&
|
||
`${channel.name} ${channel.code} ${channel.sendRegion ?? '全国'}`.toLocaleLowerCase().includes(query),
|
||
);
|
||
|
||
function submit() {
|
||
if (modal.type === 'province' && !province) {
|
||
setError('请选择省份');
|
||
return;
|
||
}
|
||
if (!selected) {
|
||
setError(channelId ? '原通道不存在,请重新选择' : '请选择通道');
|
||
return;
|
||
}
|
||
const reason = unavailableReason(selected);
|
||
if (reason) {
|
||
setError(reason);
|
||
return;
|
||
}
|
||
const route =
|
||
modal.type === 'province'
|
||
? { id: provinceRoute?.id ?? `p-${selectionName}`, province, channelId }
|
||
: { id: nationalRoute?.id ?? `n-${selectionName}`, priority: nationalRoute?.priority ?? 0, 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}
|
||
/>
|
||
) : null}
|
||
<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} 个可选</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">
|
||
{modal.type === 'province' && !province
|
||
? '请先选择省份'
|
||
: query
|
||
? '没有匹配的可选通道,请调整搜索条件'
|
||
: '暂无可添加的通道'}
|
||
</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>
|
||
);
|
||
}
|