From 6c7c7088a9ade8a2a565dcc8d2e799f5ed0e7c36 Mon Sep 17 00:00:00 2001 From: shy3130 <415333856@qq.com> Date: Mon, 24 Aug 2026 14:21:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E6=95=B0=E6=8D=AE=E6=BA=90?= =?UTF-8?q?=E8=83=BD=E5=8A=9B=E5=8D=A1=E7=89=87=E6=A0=87=E7=AD=BE=E5=BC=8F?= =?UTF-8?q?=E8=B7=AF=E7=94=B1=20=E2=80=94=20=E6=AF=8F=E4=B8=AA=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E9=9B=86=E5=8D=95=E7=8B=AC=E9=80=89=E6=8B=A9=E6=8F=90?= =?UTF-8?q?=E4=BE=9B=E6=96=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 每个数据源详情展示能力卡片, 卡片上以可点标签列出所有具备该能力的 数据源(TickFlow 恒为候选), 点标签即刻切换该数据集路由, 跨源自由组合 - 右上角徽标显示当前提供方; 除权因子含「跟随日K」标签(默认态) - TickFlow 详情含全部能力 + 恢复默认; 插件 Key 配置区对齐 TickFlow 布局 - 插件详情「整体切换为该源」= 一键套用其全部适配能力, 其余回退 TickFlow --- frontend/src/pages/settings/DataSources.tsx | 702 ++++++++++++++++---- 1 file changed, 580 insertions(+), 122 deletions(-) diff --git a/frontend/src/pages/settings/DataSources.tsx b/frontend/src/pages/settings/DataSources.tsx index 942c623..e90454c 100644 --- a/frontend/src/pages/settings/DataSources.tsx +++ b/frontend/src/pages/settings/DataSources.tsx @@ -1,7 +1,7 @@ import { useState } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { motion, AnimatePresence } from 'framer-motion' -import { Check, Database, Plus, RefreshCw, Zap, FileWarning, Puzzle } from 'lucide-react' +import { Check, Database, Eye, EyeOff, KeyRound, Plus, RefreshCw, Zap, FileWarning, Puzzle, AlertCircle, CheckCircle2, Loader2, Save, Trash2 } from 'lucide-react' import { api, type DataSourceItem, type PluginDataSourceItem } from '@/lib/api' import { QK } from '@/lib/queryKeys' import { usePreferences } from '@/lib/useSharedQueries' @@ -14,6 +14,316 @@ const DATASET_LABEL: Record = { adj_factor: '除权', realtime: '实时', minute: '分钟', + financial: '财务', +} + +/** 数据集 → 路由偏好字段 + 默认值 + 展示标签 (financial 无后端路由消费方, 仅展示不参与切换) */ +/** 能力卡片定义: 数据集 + 说明 (路由选择嵌入每张卡片) */ +const CAPABILITY_CARDS = [ + { dataset: 'daily', label: '日K', desc: '历史 + 实时覆写' }, + { dataset: 'adj_factor', label: '除权因子', desc: '复权计算' }, + { dataset: 'realtime', label: '实时行情', desc: '全市场快照' }, + { dataset: 'minute', label: '分钟K', desc: '分时图 · 回测' }, +] as const + +/** 数据集 → 路由偏好字段 + 默认值 (financial 无后端路由消费方, 仅展示) */ +const DATASET_ROUTE: Record = { + daily: { field: 'daily_data_provider', def: 'tickflow' }, + adj_factor: { field: 'adj_factor_provider', def: 'same_as_daily' }, + minute: { field: 'minute_data_provider', def: 'tickflow' }, + realtime: { field: 'realtime_data_provider', def: 'tickflow' }, +} + +/** 卡片内静态数据集标签: 只展示该源适配了哪些数据集 (路由选择在下方能力卡片) */ +function DatasetChipRow({ datasets }: { datasets: string[] }) { + if (datasets.length === 0) return null + return ( +
+ 已适配 + {datasets.map(ds => ( + + {DATASET_LABEL[ds] || ds} + + ))} +
+ ) +} + +/** 按能力路由网格: 每个数据源详情下展示一张能力卡,卡片上以可点标签列出 + * 所有具备该能力的数据源 — 点谁,该数据集就立刻由谁提供,可跨源自由组合。 + * TickFlow 详情含全部能力; 其他源只列自己参与的能力。 */ +function SourceCapabilityGrid({ sourceName, sourceDisplay, datasets, candidatesOf, providerOf, displayOf, pending, onSelect, anyCustom, onReset }: { + sourceName: string + sourceDisplay: string + datasets: string[] + /** 该数据集的所有候选提供方 (含 TickFlow), 按推荐顺序 */ + candidatesOf: (dataset: string) => { name: string; display: string }[] + /** 该数据集当前的原始路由偏好值 (adj_factor 可能是 same_as_daily) */ + providerOf: (dataset: string) => string + displayOf: (name?: string) => string + pending?: boolean + onSelect: (dataset: string, provider: string) => void + anyCustom?: boolean + onReset?: () => void +}) { + const isDefault = sourceName === 'tickflow' + const dsList = isDefault ? [...datasets, 'financial'] : datasets + if (dsList.length === 0) return null + + return ( +
+
+ + {isDefault + ? '每个能力可单独选择提供方 — 点标签即刻切换,未单独设置的由 TickFlow 提供' + : `${sourceDisplay} 参与的能力 — 每个能力都可单独选择由哪个数据源提供`} + + {isDefault && anyCustom && ( + + )} +
+ +
+ {dsList.map(ds => { + const route = DATASET_ROUTE[ds] + const meta = CAPABILITY_CARDS.find(c => c.dataset === ds) + const label = meta?.label || DATASET_LABEL[ds] || ds + const desc = meta?.desc || '' + if (!route) { + return ( +
+
+
+
{label}
+ {desc &&
{desc}
} +
+ 固定 +
+
+ ) + } + const raw = providerOf(ds) + const candidates = candidatesOf(ds) + return ( +
+
+
+
{label}
+ {desc &&
{desc}
} +
+ {/* 右上角: 当前提供方 */} + + {displayOf(raw)} + +
+ {/* 提供方标签: 点谁该数据集就由谁提供,当前项高亮 */} +
+ {ds === 'adj_factor' && ( + + )} + {candidates.map(c => ( + + ))} +
+
+ ) + })} +
+
+ ) +} + +/** 提供方标签样式: 当前项高亮(accent), 其余弱化可点 */ +function tagCls(active: boolean) { + return `px-1.5 py-0.5 rounded text-[10px] transition-colors select-none disabled:opacity-50 cursor-pointer ${ + active + ? 'bg-accent/15 text-accent font-medium' + : 'bg-elevated/60 text-muted/70 hover:bg-accent/15 hover:text-accent' + }` +} + + +/** 详情组件所需的路由上下文(由面板构造) */ +interface RouteCtx { + /** 数据集 → 候选提供方列表 (含 TickFlow) */ + candidatesOf: (dataset: string) => { name: string; display: string }[] + /** 数据集 → 当前原始路由偏好值 (adj_factor 可能是 same_as_daily) */ + providerOf: (dataset: string) => string + displayOf: (name?: string) => string + pending: boolean + onSelect: (dataset: string, provider: string) => void + anyCustom: boolean + onReset: () => void +} + +/** 插件 API Key 配置区 (布局对齐 TickFlowKeyConfig: 状态 + 输入 + 保存并检测)。 + * 先探后存: 后端用候选 Key 实探一次, 无效不落盘; secrets.json 优先于 .env。 */ +function PluginKeyConfig({ plugin }: { plugin: PluginDataSourceItem }) { + const qc = useQueryClient() + const [keyInput, setKeyInput] = useState('') + const [revealing, setRevealing] = useState(false) + const [saved, setSaved] = useState(false) + + const invalidate = () => { + qc.invalidateQueries({ queryKey: QK.dataSources }) + qc.invalidateQueries({ queryKey: QK.capabilities }) + qc.invalidateQueries({ queryKey: QK.quoteStatus }) + } + + const save = useMutation({ + mutationFn: () => api.savePluginKey(plugin.name, keyInput.trim()), + onSuccess: (data) => { + invalidate() + if (data.ok) { + setKeyInput('') + setSaved(true) + setTimeout(() => setSaved(false), 2000) + } + }, + onError: (e: Error) => toast(`保存失败: ${e.message}`, 'error'), + }) + + const clear = useMutation({ + mutationFn: () => api.clearPluginKey(plugin.name), + onSuccess: (data) => { + invalidate() + if (data.ok) { + toast( + data.plugin_available + ? '已清除界面配置的 Key(.env 中的同名变量仍然生效)' + : 'Key 已清除,插件不再可用', + 'success', + ) + } + }, + onError: (e: Error) => toast(`清除失败: ${e.message}`, 'error'), + }) + + return ( +
+
+ +

API Key

+ {plugin.api_key_env} +
+

+ Key 保存为本地文件(secrets.json, 优先级高于 .env),不会上传任何第三方。保存前会先用该 Key + 实探一次数据接口,无效则不落盘。 +

+ + {/* 当前状态 */} +
+
+
状态
+
+ {plugin.available ? ( + <> + + 已配置 + {save.data?.ok && save.data.api_key_masked && ( + {save.data.api_key_masked} + )} + + ) : ( + <> + + 未配置 + {plugin.status} + + )} +
+
+ {plugin.available && ( + + )} +
+ + {/* 输入 */} +
{ e.preventDefault(); if (keyInput.trim()) save.mutate() }} + className="space-y-2" + > +
+ { setKeyInput(e.target.value); if (saved) setSaved(false) }} + autoComplete="off" + className="w-full px-3 py-2 pr-9 rounded-input bg-base border border-border text-sm font-mono focus:outline-none focus:border-accent transition-colors duration-150 ease-smooth" + /> + +
+ +
+ + {/* 无效 Key —— 先探后存: 探测失败时不存储 */} + {save.data && !save.data.ok && ( +
+ + {save.data.error || 'Key 无效,未保存'} +
+ )} + {save.isError && ( +
+ 保存失败:{String((save.error as Error).message)} +
+ )} +
+ ) } export function SettingsDataSourcesPanel() { @@ -48,8 +358,8 @@ export function SettingsDataSourcesPanel() { }) const switchProvider = useMutation({ - mutationFn: (name: string) => { - // tickflow: 5 个数据集全量重置为 tickflow + mutationFn: async (name: string) => { + // tickflow: 全量重置为默认路由 if (name === 'tickflow') { return api.updateDataProviders({ daily_data_provider: 'tickflow', @@ -59,19 +369,18 @@ export function SettingsDataSourcesPanel() { financial_data_provider: 'tickflow', }) } - // 非 tickflow: 按源声明的 datasets 动态切换。支持的数据集切到该源, - // 不支持的保持 tickflow, 使 preferences 与实际取数路由一致 - // (避免 UI 显示某源、后台却走 tickflow 的假象)。 + // 非 tickflow: 该源适配了哪些数据集就接管哪些, 其余回退默认 const supported = new Set( allItems.find(s => s.name === name)?.datasets ?? [] ) - const pick = (dataset: string) => (supported.has(dataset) ? name : 'tickflow') + const pick = (dataset: string) => + supported.has(dataset) ? name : (dataset === 'adj_factor' ? 'same_as_daily' : 'tickflow') return api.updateDataProviders({ daily_data_provider: pick('daily'), - adj_factor_provider: 'same_as_daily', // 除权始终跟随日K + adj_factor_provider: pick('adj_factor'), realtime_data_provider: pick('realtime'), minute_data_provider: pick('minute'), - financial_data_provider: pick('financial'), + financial_data_provider: 'tickflow', }) }, onSuccess: () => { @@ -141,6 +450,97 @@ export function SettingsDataSourcesPanel() { const selectedCustom = customList.find(s => s.name === selected) + // ===== 各数据集当前的有效提供方 (除权 same_as_daily = 跟随日K) ===== + // 用于"服务中"徽标: 改单个能力路由只影响对应数据集, 不再产生"当前数据源被切换"的表现 + const dailyPref = prefs.data?.daily_data_provider || 'tickflow' + const adjPref = prefs.data?.adj_factor_provider || 'same_as_daily' + const effProvider: Record = { + daily: dailyPref, + adj_factor: adjPref === 'same_as_daily' ? dailyPref : adjPref, + minute: prefs.data?.minute_data_provider || 'tickflow', + realtime: prefs.data?.realtime_data_provider || 'tickflow', + } + const servingDatasets = (name: string) => + Object.entries(effProvider).filter(([, v]) => v === name).map(([k]) => k) + const servingLabels = (name: string) => + servingDatasets(name).map(k => DATASET_LABEL[k] || k) + + const displayOf = (name?: string) => + name === 'tickflow' ? 'TickFlow' + : name === 'same_as_daily' ? '跟随日K' + : allItems.find(s => s.name === name)?.display_name || name || '' + + const invalidateRouting = () => { + qc.invalidateQueries({ queryKey: QK.preferences }) + qc.invalidateQueries({ queryKey: QK.capabilities }) + qc.invalidateQueries({ queryKey: QK.quoteStatus }) + } + + // 单个能力的提供方切换: 点标签即刻生效 (same_as_daily 仅除权有效 = 跟随日K) + const routeMut = useMutation({ + mutationFn: ({ dataset, provider }: { dataset: string; provider: string }) => { + const route = DATASET_ROUTE[dataset] + if (!route) throw new Error(`数据集 ${dataset} 不支持切换数据源`) + return api.updateDataProviders({ [route.field]: provider }) + }, + onSuccess: (_d, v) => { + invalidateRouting() + const label = DATASET_LABEL[v.dataset] || v.dataset + toast(`「${label}」已切换为 ${displayOf(v.provider)}`, 'success') + }, + onError: (e: Error) => toast(`路由切换失败: ${e.message}`, 'error'), + }) + + // 恢复默认: 路由全部回 TickFlow + const resetRouteMut = useMutation({ + mutationFn: () => api.updateDataProviders({ + daily_data_provider: 'tickflow', + adj_factor_provider: 'same_as_daily', + minute_data_provider: 'tickflow', + realtime_data_provider: 'tickflow', + }), + onSuccess: () => { + invalidateRouting() + toast('数据集路由已恢复默认(TickFlow)', 'success') + }, + onError: (e: Error) => toast(`恢复失败: ${e.message}`, 'error'), + }) + + const anyCustomRouting = Object.values(effProvider).some(v => v !== 'tickflow') || adjPref !== 'same_as_daily' + + // 某数据集的全部候选提供方 (TickFlow 恒在首位, 其余按声明该数据集的数据源列出) + const candidatesOf = (dataset: string) => { + const list = [{ name: 'tickflow', display: 'TickFlow' }] + for (const item of allItems) { + if (item.name !== 'tickflow' && item.datasets.includes(dataset)) { + list.push({ name: item.name, display: item.display_name || item.name }) + } + } + return list + } + + // 数据集 → 当前原始路由偏好值 (adj_factor 保留 same_as_daily 以驱动"跟随日K"标签态) + const providerOf = (dataset: string) => { + switch (dataset) { + case 'daily': return dailyPref + case 'adj_factor': return adjPref + case 'minute': return prefs.data?.minute_data_provider || 'tickflow' + case 'realtime': return prefs.data?.realtime_data_provider || 'tickflow' + default: return 'tickflow' + } + } + + // 传给各数据源详情的路由上下文(能力卡标签选择 + 当前提供方展示) + const routeCtx: RouteCtx = { + candidatesOf, + providerOf, + displayOf, + pending: routeMut.isPending, + onSelect: (dataset, provider) => routeMut.mutate({ dataset, provider }), + anyCustom: anyCustomRouting, + onReset: () => resetRouteMut.mutate(), + } + return (
{/* ===== 顶部: 当前数据源 + 数据源选择 (一个大卡片) ===== */} @@ -179,19 +579,10 @@ export function SettingsDataSourcesPanel() {
- {/* 当前数据源状态 */} -
- 当前 - - - {activeName === 'tickflow' ? 'TickFlow' : customList.find(s => s.name === activeName)?.display_name || activeName} - -
- {/* 数据源选择 - 横向卡片列表 */}
{allItems.map(item => { - const isActive = activeName === item.name + const serving = servingLabels(item.name) const isSelected = selected === item.name const plugin = pluginMap.get(item.name) const pluginUnavailable = plugin && !plugin.available @@ -201,7 +592,8 @@ export function SettingsDataSourcesPanel() {
{ - if (pluginUnavailable) return // 未安装的插件不可选中 + // 未就绪插件仅当支持界面配 Key 时可点开(进详情配置); 其余不可选 + if (pluginUnavailable && !plugin?.api_key_env) return setSelected(item.name) // 只有用户自定义源 (YAML) 才进编辑器; tickflow 和插件不可编辑 if (customList.some(c => c.name === item.name)) { @@ -209,7 +601,7 @@ export function SettingsDataSourcesPanel() { } }} className={`relative text-left rounded-lg border px-3.5 py-3 transition-all ${ - pluginUnavailable + pluginUnavailable && !plugin?.api_key_env ? 'border-border/40 bg-elevated/10 opacity-70' : isSelected ? 'border-accent/50 bg-accent/5 ring-1 ring-accent/20 cursor-pointer' @@ -217,21 +609,38 @@ export function SettingsDataSourcesPanel() { }`} >
- - + 0 ? 'bg-accent' : 'bg-transparent border border-muted/40' + }`} + /> + 0 ? 'font-medium text-foreground' : 'text-secondary'}`}> {item.display_name} + {serving.length > 0 && ( + + 服务中 + + )} {item.name === 'tickflow' && ( 第三方 )} {pluginNames.has(item.name) && ( 第三方 )} - {/* 右侧操作区: 插件未安装→安装按钮; 已激活→使用中; 否则→使用/卸载 */} + {/* 右侧操作区: 插件未安装→安装按钮(runtime=none 无依赖可装,显示配置提示); 否则→使用/卸载 */} {pluginUnavailable ? ( - installing ? ( + plugin?.runtime === 'none' ? ( + + 点击配置 Key + + ) : installing ? ( 安装中... @@ -244,10 +653,6 @@ export function SettingsDataSourcesPanel() { 安装 ) - ) : isActive ? ( - - 使用中 - ) : plugin ? ( /* 已安装插件: 使用 + 卸载 */
@@ -258,7 +663,7 @@ export function SettingsDataSourcesPanel() { > 使用 - {uninstalling ? ( + {plugin?.runtime !== 'none' && (uninstalling ? ( ) : ( - )} + ))}
) : (
- {item.name !== 'tickflow' && item.datasets.length > 0 && ( -
- 已适配 - {item.datasets.map(ds => ( - - {DATASET_LABEL[ds] || ds} - - ))} -
- )} - {item.name === 'tickflow' && ( -
- 已适配 - 日K · 除权 · 实时 · 分钟K -
- )} + {/* 数据集标签(静态): 该源适配的数据集, 路由选择在下方能力卡片 */} + {/* 未安装插件显示安装命令提示 */} {pluginUnavailable && plugin?.install_hint && (
{plugin.install_hint}
@@ -334,11 +725,13 @@ export function SettingsDataSourcesPanel() { )}
- 单击编辑 + 单击查看各源能力 · - 点「使用」切换为当前数据源 + 能力卡片上点标签,单独选择每个数据集的提供方 · - 未启用的数据集自动回退 TickFlow + 点「使用」一键套用该源全部能力 + · + 未单独设置的由 TickFlow 提供
@@ -353,39 +746,78 @@ export function SettingsDataSourcesPanel() { > {selected === 'tickflow' ? ( 0} onSwitch={() => switchProvider.mutate('tickflow')} switching={switchProvider.isPending} + route={routeCtx} /> ) : selected === '__new__' || customList.some(c => c.name === selected) ? ( - setSelected('tickflow')} - onSaved={() => { - qc.invalidateQueries({ queryKey: QK.dataSources }) - // 数据集声明变化 → 能力增广与实时模式立即刷新 - qc.invalidateQueries({ queryKey: QK.preferences }) - qc.invalidateQueries({ queryKey: QK.capabilities }) - qc.invalidateQueries({ queryKey: QK.quoteStatus }) - // 强制清除该源的详情缓存, 下次编辑重新拉取最新配置 - if (selected !== '__new__') { - qc.removeQueries({ queryKey: ['data-source-detail', selected] }) - } - if (selected === '__new__') setSelected(activeName === 'tickflow' ? 'tickflow' : activeName) - }} - activeName={activeName} - onActivate={(name) => switchProvider.mutate(name)} - onDelete={selected !== '__new__' && selectedCustom ? () => setConfirmDelete(selected) : undefined} - /> + selected === '__new__' ? ( + setSelected('tickflow')} + onSaved={() => { + qc.invalidateQueries({ queryKey: QK.dataSources }) + // 数据集声明变化 → 能力增广与实时模式立即刷新 + qc.invalidateQueries({ queryKey: QK.preferences }) + qc.invalidateQueries({ queryKey: QK.capabilities }) + qc.invalidateQueries({ queryKey: QK.quoteStatus }) + setSelected('tickflow') + }} + activeName={activeName} + onActivate={(name) => switchProvider.mutate(name)} + /> + ) : ( + /* 自定义源详情: 能力卡片(路由) + 编辑器 */ +
+
+
+ +

+ {selectedCustom?.display_name || selected} · 数据集能力与路由 +

+
+ +
+ setSelected('tickflow')} + onSaved={() => { + qc.invalidateQueries({ queryKey: QK.dataSources }) + // 数据集声明变化 → 能力增广与实时模式立即刷新 + qc.invalidateQueries({ queryKey: QK.preferences }) + qc.invalidateQueries({ queryKey: QK.capabilities }) + qc.invalidateQueries({ queryKey: QK.quoteStatus }) + // 强制清除该源的详情缓存, 下次编辑重新拉取最新配置 + qc.removeQueries({ queryKey: ['data-source-detail', selected] }) + }} + activeName={activeName} + onActivate={(name) => switchProvider.mutate(name)} + onDelete={selectedCustom ? () => setConfirmDelete(selected) : undefined} + /> +
+ ) ) : pluginList.find(x => x.name === selected) ? ( - /* 选中插件: 显示只读详情, 不进编辑器 */ + /* 选中插件: 信息 + 能力卡片(路由) + Key 配置 */ x.name === selected)!} - isActive={activeName === selected} + isActive={servingDatasets(selected).length > 0} onSwitch={() => switchProvider.mutate(selected)} switching={switchProvider.isPending} + route={routeCtx} /> ) : null} @@ -425,47 +857,73 @@ export function SettingsDataSourcesPanel() { ) } -function PluginDetail({ plugin, isActive, onSwitch, switching }: { +function PluginDetail({ plugin, isActive, onSwitch, switching, route }: { plugin: PluginDataSourceItem isActive: boolean onSwitch: () => void switching: boolean + route: RouteCtx }) { return ( -
-
-
- -
-
-
-

{plugin.display_name}

- 插件 · {plugin.runtime} +
+
+
+
+ +
+
+
+

{plugin.display_name}

+ 插件 · {plugin.runtime} +
+ {plugin.description &&

{plugin.description}

}
- {plugin.description &&

{plugin.description}

}
-
-
- {isActive ? ( - - 使用中 - - ) : ( - - )} -
-
+ + {/* 本源参与的能力: 标签选择提供方, 即刻生效 */} + + +
+ {isActive ? ( + + 服务中 + + ) : plugin.available ? ( + + ) : ( + {plugin.status} + )} +
+ + + {/* API Key 配置 (声明了 api_key_env 的插件) */} + {plugin.api_key_env && } +
) } -function TickFlowDetail({ active, onSwitch, switching }: { active: boolean; onSwitch: () => void; switching: boolean }) { +function TickFlowDetail({ active, onSwitch, switching, route }: { + active: boolean + onSwitch: () => void + switching: boolean + route: RouteCtx +}) { return (
@@ -479,35 +937,35 @@ function TickFlowDetail({ active, onSwitch, switching }: { active: boolean; onSw 第三方 {active && ( - 使用中 + 服务中 )}

- 项目默认数据源。日K、除权因子、实时行情、分钟K均由 TickFlow 提供,无需额外配置。 + 默认数据源,具备全部能力(日K · 除权 · 分钟 · 实时 · 财务)。在下方每个能力卡片上点标签,可单独选择该数据集由哪个数据源提供。

-
- {[ - { label: '日K', desc: '历史 + 实时覆写' }, - { label: '除权因子', desc: '复权计算' }, - { label: '实时行情', desc: '全市场快照' }, - { label: '分钟K', desc: '分时图 · 回测' }, - ].map(f => ( -
-
{f.label}
-
{f.desc}
-
- ))} -
+ {/* 全部能力的提供方标签选择 + 恢复默认 */} + {!active && (