feat: overhaul mobile management layouts

This commit is contained in:
Cita
2026-03-21 20:42:25 +08:00
parent 6a19d809f0
commit 09fbf258e2
55 changed files with 4815 additions and 1030 deletions
+122 -77
View File
@@ -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]}
+282 -24
View File
@@ -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>
);
+1
View File
@@ -154,6 +154,7 @@ export type SortableChannelRowProps = {
loadingDecision: boolean;
isSavingPriority: boolean;
readOnly?: boolean;
mobile?: boolean;
tokenOptions: RouteTokenOption[];
activeTokenId: number;
isUpdatingToken: boolean;