forked from boos/BoosAPI
feat: overhaul mobile management layouts
This commit is contained in:
@@ -40,6 +40,7 @@ type RouteCardProps = {
|
||||
route: RouteSummaryRow;
|
||||
brand: BrandInfo | null;
|
||||
expanded: boolean;
|
||||
compact?: boolean;
|
||||
onToggleExpand: (routeId: number) => void;
|
||||
onEdit: (route: RouteSummaryRow) => void;
|
||||
onDelete: (routeId: number) => void;
|
||||
@@ -100,6 +101,7 @@ function RouteCardInner({
|
||||
route,
|
||||
brand,
|
||||
expanded,
|
||||
compact = false,
|
||||
onToggleExpand,
|
||||
onEdit,
|
||||
onDelete,
|
||||
@@ -263,86 +265,127 @@ function RouteCardInner({
|
||||
|
||||
// Expanded card
|
||||
return (
|
||||
<div className="card route-card-expanded" style={{ padding: 16 }}>
|
||||
{/* Header */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12, gap: 8, flexWrap: 'wrap' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||
<code style={{ fontWeight: 600, fontSize: 13, background: 'var(--color-bg)', padding: '4px 10px', borderRadius: 6, color: 'var(--color-text-primary)', display: 'inline-flex', alignItems: 'center', gap: 5 }}>
|
||||
{routeIcon.kind === 'brand' ? (
|
||||
<BrandGlyph icon={routeIcon.value} alt={title} size={20} fallbackText={title} />
|
||||
) : routeIcon.kind === 'text' ? (
|
||||
<span style={{ width: 20, height: 20, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', borderRadius: 8, background: 'var(--color-bg-card)', fontSize: 14, lineHeight: 1 }}>
|
||||
{routeIcon.value}
|
||||
</span>
|
||||
) : routeIcon.kind === 'auto' && brand ? (
|
||||
<BrandGlyph brand={brand} alt={title} size={20} fallbackText={title} />
|
||||
) : routeIcon.kind === 'auto' ? (
|
||||
<InlineBrandIcon model={route.modelPattern} size={20} />
|
||||
<div className={`card route-card-expanded ${compact ? 'route-card-expanded-compact' : ''}`.trim()} style={{ padding: compact ? 14 : 16 }}>
|
||||
{!compact ? (
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12, gap: 8, flexWrap: 'wrap' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||
<code style={{ fontWeight: 600, fontSize: 13, background: 'var(--color-bg)', padding: '4px 10px', borderRadius: 6, color: 'var(--color-text-primary)', display: 'inline-flex', alignItems: 'center', gap: 5 }}>
|
||||
{routeIcon.kind === 'brand' ? (
|
||||
<BrandGlyph icon={routeIcon.value} alt={title} size={20} fallbackText={title} />
|
||||
) : routeIcon.kind === 'text' ? (
|
||||
<span style={{ width: 20, height: 20, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', borderRadius: 8, background: 'var(--color-bg-card)', fontSize: 14, lineHeight: 1 }}>
|
||||
{routeIcon.value}
|
||||
</span>
|
||||
) : routeIcon.kind === 'auto' && brand ? (
|
||||
<BrandGlyph brand={brand} alt={title} size={20} fallbackText={title} />
|
||||
) : routeIcon.kind === 'auto' ? (
|
||||
<InlineBrandIcon model={route.modelPattern} size={20} />
|
||||
) : null}
|
||||
{title}
|
||||
</code>
|
||||
{route.displayName && route.displayName.trim() !== route.modelPattern ? (
|
||||
<span className="badge badge-muted" style={{ fontSize: 10 }}>{route.modelPattern}</span>
|
||||
) : null}
|
||||
{title}
|
||||
</code>
|
||||
{route.displayName && route.displayName.trim() !== route.modelPattern ? (
|
||||
<span className="badge badge-muted" style={{ fontSize: 10 }}>{route.modelPattern}</span>
|
||||
) : null}
|
||||
{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>
|
||||
)}
|
||||
{explicitGroupRoute && explicitGroupSourceCount > 0 ? (
|
||||
<>
|
||||
<span className="badge badge-info" style={{ fontSize: 10 }}>
|
||||
{explicitGroupSourceCount} {tr('来源模型')}
|
||||
</span>
|
||||
{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>
|
||||
)}
|
||||
{explicitGroupRoute && explicitGroupSourceCount > 0 ? (
|
||||
<>
|
||||
<span className="badge badge-info" style={{ fontSize: 10 }}>
|
||||
{explicitGroupSourceCount} {tr('来源模型')}
|
||||
</span>
|
||||
<span className="badge badge-muted" style={{ fontSize: 10 }}>
|
||||
{route.channelCount} {tr('通道')}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<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 }}>
|
||||
{!readOnlyRoute && (explicitGroupRoute || !exactRoute) && (
|
||||
<button onClick={() => onEdit(route)} className="btn btn-link">{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"
|
||||
style={{ padding: '4px 8px', border: '1px solid var(--color-border)' }}
|
||||
data-tooltip={tr('收起')}
|
||||
>
|
||||
<svg
|
||||
width="14" height="14" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="2"
|
||||
style={{ transform: 'rotate(180deg)' }}
|
||||
aria-hidden
|
||||
>
|
||||
<path d="m5 7 5 6 5-6" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginBottom: 12 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 10, flexWrap: 'wrap' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--color-text-primary)' }}>
|
||||
{tr('路由详情与通道管理')}
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--color-text-muted)' }}>
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
{!readOnlyRoute && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap', justifyContent: 'flex-end' }}>
|
||||
{!exactRoute && (
|
||||
<button onClick={() => onEdit(route)} className="btn btn-link">{explicitGroupRoute ? tr('编辑群组') : tr('编辑路由')}</button>
|
||||
)}
|
||||
<button onClick={() => onDelete(route.id)} className="btn btn-link btn-link-danger">{tr('删除路由')}</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
|
||||
{readOnlyRoute ? (
|
||||
<span className="badge badge-muted" style={{ fontSize: 10 }}>{tr('未生成')}</span>
|
||||
) : (
|
||||
<span className={`badge ${route.enabled ? 'badge-success' : 'badge-muted'}`} style={{ fontSize: 10 }}>
|
||||
{route.enabled ? tr('启用') : tr('禁用')}
|
||||
</span>
|
||||
)}
|
||||
<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>
|
||||
)}
|
||||
{explicitGroupRoute && explicitGroupSourceCount > 0 ? (
|
||||
<span className="badge badge-muted" style={{ fontSize: 10 }}>
|
||||
{explicitGroupSourceCount} {tr('来源模型')}
|
||||
</span>
|
||||
) : null}
|
||||
{savingPriority ? <span className="badge badge-warning" style={{ fontSize: 10 }}>{tr('排序保存中')}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
{!readOnlyRoute && (explicitGroupRoute || !exactRoute) && (
|
||||
<button onClick={() => onEdit(route)} className="btn btn-link">{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"
|
||||
style={{ padding: '4px 8px', border: '1px solid var(--color-border)' }}
|
||||
data-tooltip={tr('收起')}
|
||||
>
|
||||
<svg
|
||||
width="14" height="14" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="2"
|
||||
style={{ transform: 'rotate(180deg)' }}
|
||||
aria-hidden
|
||||
>
|
||||
<path d="m5 7 5 6 5-6" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{explicitGroupRoute ? (
|
||||
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 10 }}>
|
||||
@@ -355,11 +398,11 @@ function RouteCardInner({
|
||||
) : null}
|
||||
|
||||
{!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)' }}>
|
||||
<div style={{ display: 'flex', alignItems: compact ? 'stretch' : 'center', flexDirection: compact ? 'column' : 'row', justifyContent: 'space-between', gap: 12, marginBottom: 12, flexWrap: 'wrap' }}>
|
||||
<div style={{ fontSize: 12, color: 'var(--color-text-secondary)', minWidth: compact ? '100%' : undefined }}>
|
||||
{tr('路由策略')}
|
||||
</div>
|
||||
<div style={{ minWidth: 220, maxWidth: 320, flex: '1 1 220px' }}>
|
||||
<div style={{ minWidth: compact ? '100%' : 220, maxWidth: compact ? '100%' : 320, flex: compact ? '1 1 100%' : '1 1 220px', width: compact ? '100%' : undefined }}>
|
||||
<ModernSelect
|
||||
size="sm"
|
||||
value={routingStrategy}
|
||||
@@ -374,7 +417,7 @@ function RouteCardInner({
|
||||
)}
|
||||
|
||||
{/* Missing token hints + Add channel button */}
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 8, marginBottom: 10, flexWrap: 'wrap' }}>
|
||||
<div style={{ display: 'flex', alignItems: compact ? 'stretch' : 'flex-start', flexDirection: compact ? 'column' : 'row', justifyContent: 'space-between', gap: 8, marginBottom: 10, flexWrap: 'wrap' }}>
|
||||
{!channelManagementDisabled && (missingTokenSiteItems.length > 0 || missingTokenGroupItems.length > 0) ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, flex: 1 }}>
|
||||
{missingTokenSiteItems.length > 0 && (
|
||||
@@ -417,7 +460,7 @@ function RouteCardInner({
|
||||
<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' }}
|
||||
style={{ fontSize: 12, padding: '6px 10px', color: 'var(--color-primary)', border: '1px solid var(--color-border)', whiteSpace: compact ? 'normal' : 'nowrap', width: compact ? '100%' : 'auto' }}
|
||||
>
|
||||
+ {tr('添加通道')}
|
||||
</button>
|
||||
@@ -498,6 +541,7 @@ function RouteCardInner({
|
||||
loadingDecision={loadingDecision}
|
||||
isSavingPriority={!!savingPriority}
|
||||
readOnly
|
||||
mobile={compact}
|
||||
tokenOptions={tokenOptions}
|
||||
activeTokenId={activeTokenId}
|
||||
isUpdatingToken={!!updatingChannel[channel.id]}
|
||||
@@ -521,6 +565,7 @@ function RouteCardInner({
|
||||
isExactRoute={exactRoute}
|
||||
loadingDecision={loadingDecision}
|
||||
isSavingPriority={savingPriority}
|
||||
mobile={compact}
|
||||
tokenOptions={tokenOptions}
|
||||
activeTokenId={activeTokenId}
|
||||
isUpdatingToken={!!updatingChannel[channel.id]}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CSSProperties } from 'react';
|
||||
import { useState, type CSSProperties } from 'react';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import ModernSelect from '../../components/ModernSelect.js';
|
||||
@@ -18,6 +18,7 @@ export function SortableChannelRow({
|
||||
loadingDecision,
|
||||
isSavingPriority,
|
||||
readOnly = false,
|
||||
mobile = false,
|
||||
tokenOptions,
|
||||
activeTokenId,
|
||||
isUpdatingToken,
|
||||
@@ -44,10 +45,10 @@ export function SortableChannelRow({
|
||||
opacity: isDragging ? 0.72 : 1,
|
||||
zIndex: isDragging ? 10 : 1,
|
||||
display: 'grid',
|
||||
gridTemplateColumns: readOnly ? 'minmax(0, 1fr)' : 'minmax(0, 1fr) auto auto',
|
||||
alignItems: 'center',
|
||||
gridTemplateColumns: readOnly || mobile ? 'minmax(0, 1fr)' : 'minmax(0, 1fr) auto auto',
|
||||
alignItems: mobile ? 'stretch' : 'center',
|
||||
gap: 8,
|
||||
padding: '8px 12px',
|
||||
padding: mobile ? '10px 12px' : '8px 12px',
|
||||
borderLeft: '2px solid var(--color-primary)',
|
||||
borderRadius: '0 var(--radius-sm) var(--radius-sm) 0',
|
||||
background: isDragging ? 'rgba(59,130,246,0.08)' : 'rgba(79,70,229,0.02)',
|
||||
@@ -65,9 +66,221 @@ export function SortableChannelRow({
|
||||
},
|
||||
);
|
||||
|
||||
const [mobileDetailsOpen, setMobileDetailsOpen] = useState(false);
|
||||
|
||||
if (mobile) {
|
||||
return (
|
||||
<div ref={setNodeRef} style={{ ...rowStyle, display: 'block' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 10 }}>
|
||||
<button
|
||||
type="button"
|
||||
ref={setActivatorNodeRef}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
disabled={isSavingPriority || readOnly}
|
||||
className="btn btn-ghost"
|
||||
style={{
|
||||
width: 22,
|
||||
minWidth: 22,
|
||||
height: 22,
|
||||
padding: 0,
|
||||
border: '1px solid var(--color-border-light)',
|
||||
color: 'var(--color-text-muted)',
|
||||
cursor: isSavingPriority || readOnly ? 'not-allowed' : 'grab',
|
||||
opacity: readOnly ? 0.65 : 1,
|
||||
marginTop: 2,
|
||||
}}
|
||||
data-tooltip={readOnly ? '来源群组继承通道优先级,不能在这里拖动' : '拖拽调整优先级'}
|
||||
aria-label="拖拽调整优先级"
|
||||
>
|
||||
<svg width="12" height="12" fill="currentColor" viewBox="0 0 12 12" aria-hidden>
|
||||
<circle cx="3" cy="2" r="1" />
|
||||
<circle cx="9" cy="2" r="1" />
|
||||
<circle cx="3" cy="6" r="1" />
|
||||
<circle cx="9" cy="6" r="1" />
|
||||
<circle cx="3" cy="10" r="1" />
|
||||
<circle cx="9" cy="10" r="1" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, minWidth: 0, flex: 1 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||
<span
|
||||
className="badge"
|
||||
style={{
|
||||
fontSize: 10,
|
||||
fontWeight: 700,
|
||||
letterSpacing: 0.1,
|
||||
...getPriorityTagStyle(channel.priority ?? 0),
|
||||
}}
|
||||
>
|
||||
P{channel.priority ?? 0}
|
||||
</span>
|
||||
|
||||
<span style={{ fontWeight: 600, color: 'var(--color-text-primary)', fontSize: 14, minWidth: 0 }}>
|
||||
{channel.account?.username || `account-${channel.accountId}`}
|
||||
</span>
|
||||
|
||||
<span className="badge badge-muted" style={{ fontSize: 10 }}>
|
||||
{channel.site?.name || 'unknown'}
|
||||
</span>
|
||||
|
||||
<span style={{ fontSize: 11, color: 'var(--color-text-muted)', marginLeft: 'auto' }}>
|
||||
成功/失败 <span style={{ color: 'var(--color-success)', fontWeight: 600 }}>{channel.successCount || 0}</span>
|
||||
<span style={{ color: 'var(--color-text-muted)', margin: '0 2px' }}>/</span>
|
||||
<span style={{ color: 'var(--color-danger)', fontWeight: 600 }}>{channel.failCount || 0}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
|
||||
<span
|
||||
className="badge"
|
||||
style={{
|
||||
fontSize: 10,
|
||||
background: tokenBinding.badgeTone === 'info'
|
||||
? 'color-mix(in srgb, var(--color-info) 15%, transparent)'
|
||||
: 'color-mix(in srgb, var(--color-warning) 15%, transparent)',
|
||||
color: tokenBinding.badgeTone === 'info' ? 'var(--color-info)' : 'var(--color-warning)',
|
||||
}}
|
||||
>
|
||||
{tokenBinding.bindingModeLabel}
|
||||
</span>
|
||||
|
||||
<span
|
||||
className="badge"
|
||||
style={{
|
||||
fontSize: 10,
|
||||
background: 'color-mix(in srgb, var(--color-primary) 10%, transparent)',
|
||||
color: 'var(--color-primary)',
|
||||
maxWidth: 220,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
data-tooltip={`当前生效:${tokenBinding.effectiveTokenName}`}
|
||||
>
|
||||
当前生效:{tokenBinding.effectiveTokenName}
|
||||
</span>
|
||||
|
||||
{channel.sourceModel ? (
|
||||
<span className="badge badge-info" style={{ fontSize: 10 }}>
|
||||
{channel.sourceModel}
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
{channel.manualOverride ? (
|
||||
<span
|
||||
className="badge badge-warning"
|
||||
style={{ fontSize: 10 }}
|
||||
data-tooltip="该通道由用户手动添加,而非系统自动生成"
|
||||
>
|
||||
手动配置
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||
<span style={{ fontSize: 11, color: 'var(--color-text-muted)', whiteSpace: 'nowrap' }}>选中概率</span>
|
||||
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 6, minWidth: 120 }}>
|
||||
<div
|
||||
data-tooltip={decisionState.probability <= 0 ? decisionState.reasonText : undefined}
|
||||
style={{
|
||||
width: 80,
|
||||
height: 6,
|
||||
background: 'var(--color-border)',
|
||||
borderRadius: 999,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: `${Math.max(0, Math.min(100, decisionState.probability))}%`,
|
||||
height: '100%',
|
||||
background: getProbabilityColor(decisionState.probability),
|
||||
borderRadius: 999,
|
||||
transition: 'width 0.3s ease',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
data-tooltip={decisionState.probability <= 0 ? decisionState.reasonText : undefined}
|
||||
style={{
|
||||
fontSize: 11,
|
||||
color: decisionState.probability > 0 ? 'var(--color-text-secondary)' : decisionState.reasonColor,
|
||||
fontVariantNumeric: 'tabular-nums',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{decisionState.probability.toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{!readOnly && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-link"
|
||||
onClick={() => setMobileDetailsOpen((current) => !current)}
|
||||
style={{ marginLeft: 'auto' }}
|
||||
>
|
||||
{mobileDetailsOpen ? '收起配置' : '配置通道'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!readOnly && mobileDetailsOpen && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, paddingTop: 8, borderTop: '1px solid var(--color-border-light)' }}>
|
||||
<div style={{ width: '100%' }}>
|
||||
<ModernSelect
|
||||
size="sm"
|
||||
value={String(activeTokenId || 0)}
|
||||
onChange={(nextValue) => onTokenDraftChange(channel.id, Number.parseInt(nextValue, 10) || 0)}
|
||||
disabled={isUpdatingToken}
|
||||
options={[
|
||||
{
|
||||
value: '0',
|
||||
label: tokenBinding.followOptionLabel,
|
||||
description: tokenBinding.followOptionDescription,
|
||||
},
|
||||
...tokenOptions.map((token) => ({
|
||||
value: String(token.id),
|
||||
label: buildFixedTokenOptionLabel(token, { includeDefaultTag: true }),
|
||||
description: buildFixedTokenOptionDescription(token),
|
||||
})),
|
||||
]}
|
||||
placeholder="选择令牌绑定方式"
|
||||
/>
|
||||
<div style={{ marginTop: 4, fontSize: 11, color: 'var(--color-text-muted)', lineHeight: 1.4 }}>
|
||||
{tokenBinding.helperText}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 12, flexWrap: 'wrap' }}>
|
||||
<button
|
||||
onClick={onSaveToken}
|
||||
disabled={isUpdatingToken}
|
||||
className="btn btn-link btn-link-info"
|
||||
>
|
||||
{isUpdatingToken ? <span className="spinner spinner-sm" /> : '保存'}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={onDeleteChannel}
|
||||
className="btn btn-link btn-link-danger"
|
||||
>
|
||||
移除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef} style={rowStyle}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, fontSize: 13, flexWrap: 'wrap', minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: mobile ? 'stretch' : 'center', flexDirection: mobile ? 'column' : 'row', gap: 10, fontSize: 13, flexWrap: 'wrap', minWidth: 0 }}>
|
||||
<button
|
||||
type="button"
|
||||
ref={setActivatorNodeRef}
|
||||
@@ -163,7 +376,7 @@ export function SortableChannelRow({
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, width: '100%', marginTop: 4 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, width: '100%', marginTop: mobile ? 0 : 4, flexWrap: 'wrap' }}>
|
||||
<span style={{ fontSize: 11, color: 'var(--color-text-muted)', whiteSpace: 'nowrap' }}>选中概率</span>
|
||||
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 6, minWidth: 120 }}>
|
||||
<div
|
||||
@@ -209,9 +422,9 @@ export function SortableChannelRow({
|
||||
</div>
|
||||
|
||||
{!readOnly ? (
|
||||
<>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<div style={{ minWidth: 220, flex: 1 }}>
|
||||
mobile ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, width: '100%' }}>
|
||||
<div style={{ width: '100%' }}>
|
||||
<ModernSelect
|
||||
size="sm"
|
||||
value={String(activeTokenId || 0)}
|
||||
@@ -235,22 +448,67 @@ export function SortableChannelRow({
|
||||
{tokenBinding.helperText}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onSaveToken}
|
||||
disabled={isUpdatingToken}
|
||||
className="btn btn-link btn-link-info"
|
||||
>
|
||||
{isUpdatingToken ? <span className="spinner spinner-sm" /> : '保存'}
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 12, flexWrap: 'wrap' }}>
|
||||
<button
|
||||
onClick={onSaveToken}
|
||||
disabled={isUpdatingToken}
|
||||
className="btn btn-link btn-link-info"
|
||||
>
|
||||
{isUpdatingToken ? <span className="spinner spinner-sm" /> : '保存'}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={onDeleteChannel}
|
||||
className="btn btn-link btn-link-danger"
|
||||
>
|
||||
移除
|
||||
</button>
|
||||
</>
|
||||
<button
|
||||
onClick={onDeleteChannel}
|
||||
className="btn btn-link btn-link-danger"
|
||||
>
|
||||
移除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<div style={{ minWidth: 220, flex: 1 }}>
|
||||
<ModernSelect
|
||||
size="sm"
|
||||
value={String(activeTokenId || 0)}
|
||||
onChange={(nextValue) => onTokenDraftChange(channel.id, Number.parseInt(nextValue, 10) || 0)}
|
||||
disabled={isUpdatingToken}
|
||||
options={[
|
||||
{
|
||||
value: '0',
|
||||
label: tokenBinding.followOptionLabel,
|
||||
description: tokenBinding.followOptionDescription,
|
||||
},
|
||||
...tokenOptions.map((token) => ({
|
||||
value: String(token.id),
|
||||
label: buildFixedTokenOptionLabel(token, { includeDefaultTag: true }),
|
||||
description: buildFixedTokenOptionDescription(token),
|
||||
})),
|
||||
]}
|
||||
placeholder="选择令牌绑定方式"
|
||||
/>
|
||||
<div style={{ marginTop: 4, fontSize: 11, color: 'var(--color-text-muted)', lineHeight: 1.4 }}>
|
||||
{tokenBinding.helperText}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onSaveToken}
|
||||
disabled={isUpdatingToken}
|
||||
className="btn btn-link btn-link-info"
|
||||
>
|
||||
{isUpdatingToken ? <span className="spinner spinner-sm" /> : '保存'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={onDeleteChannel}
|
||||
className="btn btn-link btn-link-danger"
|
||||
>
|
||||
移除
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -154,6 +154,7 @@ export type SortableChannelRowProps = {
|
||||
loadingDecision: boolean;
|
||||
isSavingPriority: boolean;
|
||||
readOnly?: boolean;
|
||||
mobile?: boolean;
|
||||
tokenOptions: RouteTokenOption[];
|
||||
activeTokenId: number;
|
||||
isUpdatingToken: boolean;
|
||||
|
||||
Reference in New Issue
Block a user