add zero-channel route placeholders

This commit is contained in:
cita
2026-03-16 19:25:02 +08:00
parent 83c6b3460a
commit 52441f2634
6 changed files with 405 additions and 57 deletions
+74 -44
View File
@@ -114,6 +114,7 @@ function RouteCardInner({
}: RouteCardProps) {
const routeIcon = resolveRouteIcon(route);
const exactRoute = isExactModelPattern(route.modelPattern);
const readOnlyRoute = route.kind === 'zero_channel' || route.readOnly === true || route.isVirtual === true;
const title = resolveRouteTitle(route);
const routingStrategy = route.routingStrategy === 'round_robin' ? 'round_robin' : 'weighted';
const routingStrategyOptions = [
@@ -187,22 +188,34 @@ function RouteCardInner({
<span className="badge badge-muted" style={{ fontSize: 10, flexShrink: 0 }}>{route.modelPattern}</span>
) : null}
<button
className={`badge route-enable-toggle ${route.enabled ? 'is-enabled' : 'is-disabled'}`}
style={{ fontSize: 11, cursor: 'pointer', border: 'none', flexShrink: 0, minWidth: 36, textAlign: 'center' }}
onClick={(e) => { e.stopPropagation(); onToggleEnabled(route); }}
data-tooltip={route.enabled ? '点击禁用此路由' : '点击启用此路由'}
>
{route.enabled ? tr('启用') : tr('禁用')}
</button>
{readOnlyRoute ? (
<span className="badge badge-muted" style={{ fontSize: 10, flexShrink: 0 }}>
{tr('未生成')}
</span>
) : (
<button
className={`badge route-enable-toggle ${route.enabled ? 'is-enabled' : 'is-disabled'}`}
style={{ fontSize: 11, cursor: 'pointer', border: 'none', flexShrink: 0, minWidth: 36, textAlign: 'center' }}
onClick={(e) => { e.stopPropagation(); onToggleEnabled(route); }}
data-tooltip={route.enabled ? '点击禁用此路由' : '点击启用此路由'}
>
{route.enabled ? tr('启用') : tr('禁用')}
</button>
)}
<span className="badge badge-info" style={{ fontSize: 10, flexShrink: 0 }}>
{route.channelCount} {tr('通道')}
</span>
<span className="badge badge-muted" style={{ fontSize: 10, flexShrink: 0 }}>
{routingStrategy === 'round_robin' ? tr('轮询') : tr('权重随机')}
</span>
{readOnlyRoute ? (
<span className="badge badge-warning" style={{ fontSize: 10, flexShrink: 0 }}>
{tr('0 通道')}
</span>
) : (
<span className="badge badge-muted" style={{ fontSize: 10, flexShrink: 0 }}>
{routingStrategy === 'round_robin' ? tr('轮询') : tr('权重随机')}
</span>
)}
<svg
width="14" height="14" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="2"
@@ -239,27 +252,38 @@ function RouteCardInner({
{route.displayName && route.displayName.trim() !== route.modelPattern ? (
<span className="badge badge-muted" style={{ fontSize: 10 }}>{route.modelPattern}</span>
) : null}
<button
className={`badge route-enable-toggle ${route.enabled ? 'is-enabled' : 'is-disabled'}`}
style={{ fontSize: 11, cursor: 'pointer', border: 'none' }}
onClick={(e) => { e.stopPropagation(); onToggleEnabled(route); }}
data-tooltip={route.enabled ? '点击禁用此路由' : '点击启用此路由'}
>
{route.enabled ? tr('启用') : tr('禁用')}
</button>
{readOnlyRoute ? (
<span className="badge badge-muted" style={{ fontSize: 10 }}>
{tr('未生成')}
</span>
) : (
<button
className={`badge route-enable-toggle ${route.enabled ? 'is-enabled' : 'is-disabled'}`}
style={{ fontSize: 11, cursor: 'pointer', border: 'none' }}
onClick={(e) => { e.stopPropagation(); onToggleEnabled(route); }}
data-tooltip={route.enabled ? '点击禁用此路由' : '点击启用此路由'}
>
{route.enabled ? tr('启用') : tr('禁用')}
</button>
)}
<span className="badge badge-info" style={{ fontSize: 10 }}>
{route.channelCount} {tr('通道')}
</span>
{readOnlyRoute && (
<span className="badge badge-warning" style={{ fontSize: 10 }}>
{tr('0 通道')}
</span>
)}
{savingPriority && (
<span className="badge badge-warning" style={{ fontSize: 10 }}>{tr('排序保存中')}</span>
)}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
{!exactRoute && (
{!readOnlyRoute && !exactRoute && (
<button onClick={() => onEdit(route)} className="btn btn-link">{tr('编辑群组')}</button>
)}
<button onClick={() => onDelete(route.id)} className="btn btn-link btn-link-danger">{tr('删除路由')}</button>
{!readOnlyRoute && <button onClick={() => onDelete(route.id)} className="btn btn-link btn-link-danger">{tr('删除路由')}</button>}
<button
onClick={() => onToggleExpand(route.id)}
className="btn btn-ghost"
@@ -283,22 +307,24 @@ function RouteCardInner({
</div>
)}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, marginBottom: 12, flexWrap: 'wrap' }}>
<div style={{ fontSize: 12, color: 'var(--color-text-secondary)' }}>
{tr('路由策略')}
{!readOnlyRoute && (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, marginBottom: 12, flexWrap: 'wrap' }}>
<div style={{ fontSize: 12, color: 'var(--color-text-secondary)' }}>
{tr('路由策略')}
</div>
<div style={{ minWidth: 220, maxWidth: 320, flex: '1 1 220px' }}>
<ModernSelect
size="sm"
value={routingStrategy}
disabled={updatingRoutingStrategy}
onChange={(nextValue) => onRoutingStrategyChange(route, nextValue as RouteRoutingStrategy)}
options={routingStrategyOptions.map((option) => ({ ...option }))}
placeholder={tr('选择路由策略')}
emptyLabel={tr('暂无可选策略')}
/>
</div>
</div>
<div style={{ minWidth: 220, maxWidth: 320, flex: '1 1 220px' }}>
<ModernSelect
size="sm"
value={routingStrategy}
disabled={updatingRoutingStrategy}
onChange={(nextValue) => onRoutingStrategyChange(route, nextValue as RouteRoutingStrategy)}
options={routingStrategyOptions.map((option) => ({ ...option }))}
placeholder={tr('选择路由策略')}
emptyLabel={tr('暂无可选策略')}
/>
</div>
</div>
)}
{/* Missing token hints + Add channel button */}
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 8, marginBottom: 10, flexWrap: 'wrap' }}>
@@ -340,13 +366,15 @@ function RouteCardInner({
)}
</div>
) : <div />}
<button
onClick={() => onAddChannel(route.id)}
className="btn btn-ghost"
style={{ fontSize: 12, padding: '6px 10px', color: 'var(--color-primary)', border: '1px solid var(--color-border)', whiteSpace: 'nowrap' }}
>
+ {tr('添加通道')}
</button>
{!readOnlyRoute && (
<button
onClick={() => onAddChannel(route.id)}
className="btn btn-ghost"
style={{ fontSize: 12, padding: '6px 10px', color: 'var(--color-primary)', border: '1px solid var(--color-border)', whiteSpace: 'nowrap' }}
>
+ {tr('添加通道')}
</button>
)}
</div>
{/* Channel list */}
@@ -444,7 +472,9 @@ function RouteCardInner({
</DndContext>
</div>
) : (
<div style={{ fontSize: 13, color: 'var(--color-text-muted)', paddingLeft: 4 }}>暂无通道</div>
<div style={{ fontSize: 13, color: 'var(--color-text-muted)', paddingLeft: 4 }}>
{readOnlyRoute ? tr('暂无通道,先补齐令牌后再重建路由。') : tr('暂无通道')}
</div>
)}
</div>
);
+4
View File
@@ -5,6 +5,7 @@ export type RouteSortBy = 'modelPattern' | 'channelCount';
export type RouteSortDir = 'asc' | 'desc';
export type GroupFilter = null | '__all__' | number;
export type RouteRoutingStrategy = 'weighted' | 'round_robin';
export type RouteRowKind = 'persisted' | 'zero_channel';
export type RouteChannelDraft = {
accountId: number;
@@ -67,6 +68,9 @@ export type RouteSummaryRow = {
siteNames: string[];
decisionSnapshot: RouteDecision | null;
decisionRefreshedAt: string | null;
kind?: RouteRowKind;
readOnly?: boolean;
isVirtual?: boolean;
};
export type RouteDecisionCandidate = {