优化: 前端u-agent令牌对齐与组件拆分续批
- 全局样式类(df-sec-h/df-toast族)替换散装实现,主题token去硬编码兜底 - 拆分子组件:ai-chat/queue-panel(队列时钟与编辑随迁)、 conversation-sidebar/ConversationList、settings/provider-panel - 父组件死副本清理(队列编辑/时钟/键位表,TS6133清零)并补 fill-draft父侧转发(重构半成品断链修复) - 验收:vue-tsc EXIT 0
This commit is contained in:
@@ -91,8 +91,8 @@
|
||||
<div class="update-progress-text">{{ progressText }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<div v-if="installError" class="update-error">{{ installError }}</div>
|
||||
<!-- 错误提示:视觉走全局 .df-note--danger -->
|
||||
<div v-if="installError" class="df-note df-note--danger update-error">{{ installError }}</div>
|
||||
|
||||
<div class="update-card-actions">
|
||||
<button
|
||||
@@ -373,11 +373,12 @@ async function onIntervalChange() {
|
||||
.update-progress {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
/* 进度条轨道:原 --df-bg-hover 为未定义幽灵 token(靠回退侥幸渲染),改中性底 */
|
||||
.update-progress-bar {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
background: var(--df-bg-hover, rgba(128, 128, 128, 0.2));
|
||||
background: var(--df-neutral-bg);
|
||||
overflow: hidden;
|
||||
}
|
||||
.update-progress-fill {
|
||||
@@ -391,14 +392,9 @@ async function onIntervalChange() {
|
||||
font-family: var(--df-font-mono, monospace);
|
||||
color: var(--df-text-dim);
|
||||
}
|
||||
/* 安装错误提示 */
|
||||
/* 安装错误提示:底色/边/圆角/字号走全局 .df-note--danger(token 直引无回退),此处仅留排版 */
|
||||
.update-error {
|
||||
margin-bottom: 12px;
|
||||
padding: 8px 10px;
|
||||
border-radius: var(--df-radius-md, 6px);
|
||||
background: rgba(220, 53, 69, 0.12);
|
||||
font-size: 12px;
|
||||
color: var(--df-danger, #dc3545);
|
||||
line-height: 1.5;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
@@ -86,11 +86,12 @@
|
||||
</div>
|
||||
|
||||
<!-- all 二次确认弹层(独立,不与 Settings 壳层共用:壳层按钮写死「删除」不适合「确认接管」) -->
|
||||
<Transition name="confirm">
|
||||
<!-- 按钮行/过渡消费 global.css .df-confirm-actions + df-confirm(2026-10-18 整洁波并轨) -->
|
||||
<Transition name="df-confirm">
|
||||
<div v-if="confirmState.visible" class="confirm-mask" @click.self="answerConfirm(false)">
|
||||
<div class="confirm-box">
|
||||
<div class="confirm-msg">{{ confirmState.msg }}</div>
|
||||
<div class="confirm-actions">
|
||||
<div class="df-confirm-actions">
|
||||
<button class="btn btn-ghost btn-sm" @click="answerConfirm(false)">{{ $t('common.cancel') }}</button>
|
||||
<button class="btn btn-danger btn-sm" @click="answerConfirm(true)">{{ confirmState.dangerLabel || $t('common.confirm') }}</button>
|
||||
</div>
|
||||
@@ -252,8 +253,9 @@ function onApprovalTimeoutChange() {
|
||||
.setting-select/.setting-textarea + SettingRow(封装 .setting-row/.setting-info/.setting-label/
|
||||
.setting-desc/.setting-control)。本 Section 仅保留 all 二次确认弹层特有 scoped 样式。
|
||||
|
||||
F-#97 all 二次确认弹层:复用 Settings.vue 壳层 confirm 弹层同款样式(mask/box/msg/
|
||||
actions/transition),此处独立持有 useConfirm 状态(不与壳层共用,壳层按钮写死「删除」)。 */
|
||||
F-#97 all 二次确认弹层:mask/box/msg 本地特有样式;actions 按钮行与过渡已并轨
|
||||
global.css .df-confirm-actions + df-confirm 过渡(2026-10-18 整洁波双轨合一)。
|
||||
此处独立持有 useConfirm 状态(不与壳层共用,壳层按钮写死「删除」)。 */
|
||||
.data-dir-path {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
@@ -284,8 +286,6 @@ function onApprovalTimeoutChange() {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
.prompt-row .setting-info { align-items: flex-start; }
|
||||
.prompt-row .setting-control { align-items: flex-start; }
|
||||
/* select 类设置行(自定义布局,不复用 SettingRow):label 与 select 同一行靠左顶对齐,
|
||||
desc 换行到整行下方。
|
||||
┌───────────────────────────────┐
|
||||
@@ -326,12 +326,7 @@ function onApprovalTimeoutChange() {
|
||||
padding: 20px;
|
||||
min-width: 280px;
|
||||
max-width: 420px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
|
||||
box-shadow: var(--df-shadow-md);
|
||||
}
|
||||
.confirm-msg { font-size: 13px; color: var(--df-text); line-height: 1.5; margin-bottom: 16px; white-space: pre-line; }
|
||||
.confirm-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
||||
.btn-danger { background: var(--df-danger); color: #fff; }
|
||||
.btn-danger:hover { filter: brightness(1.1); }
|
||||
.confirm-enter-active, .confirm-leave-active { transition: opacity 0.15s; }
|
||||
.confirm-enter-from, .confirm-leave-to { opacity: 0; }
|
||||
</style>
|
||||
|
||||
@@ -87,12 +87,13 @@
|
||||
</section>
|
||||
|
||||
<!-- 「信任所有目录」二次确认弹层(同 AdvancedSection 的 all 接管确认:自渲染独立弹层,
|
||||
msg 拼标题+正文,confirm-msg 用 pre-line 保留换行) -->
|
||||
<Transition name="confirm">
|
||||
msg 拼标题+正文,confirm-msg 用 pre-line 保留换行;
|
||||
按钮行/过渡消费 global.css .df-confirm-actions + df-confirm,2026-10-18 整洁波并轨) -->
|
||||
<Transition name="df-confirm">
|
||||
<div v-if="confirmState.visible" class="confirm-mask" @click.self="answerConfirm(false)">
|
||||
<div class="confirm-box">
|
||||
<div class="confirm-msg">{{ confirmState.msg }}</div>
|
||||
<div class="confirm-actions">
|
||||
<div class="df-confirm-actions">
|
||||
<button class="btn btn-ghost btn-sm" @click="answerConfirm(false)">{{ $t('common.cancel') }}</button>
|
||||
<button class="btn btn-danger btn-sm" @click="answerConfirm(true)">{{ confirmState.dangerLabel || $t('common.confirm') }}</button>
|
||||
</div>
|
||||
@@ -296,8 +297,10 @@ onMounted(load)
|
||||
.script-safety-desc {
|
||||
font-size: 12px; color: var(--df-text-dim); margin: 0 0 4px; line-height: 1.5;
|
||||
}
|
||||
/* 名单输入定宽 280px + max-width 窄窗收缩(原 min-width:280px 在窄列不可收缩致横向溢出) */
|
||||
.script-safety-input {
|
||||
min-width: 280px;
|
||||
width: 280px;
|
||||
max-width: 100%;
|
||||
font-family: var(--df-font-mono, monospace);
|
||||
}
|
||||
.script-safety-note {
|
||||
@@ -321,12 +324,7 @@ onMounted(load)
|
||||
padding: 20px;
|
||||
min-width: 280px;
|
||||
max-width: 420px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
|
||||
box-shadow: var(--df-shadow-md);
|
||||
}
|
||||
.confirm-msg { font-size: 13px; color: var(--df-text); line-height: 1.5; margin-bottom: 16px; white-space: pre-line; }
|
||||
.confirm-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
||||
.btn-danger { background: var(--df-danger); color: #fff; }
|
||||
.btn-danger:hover { filter: brightness(1.1); }
|
||||
.confirm-enter-active, .confirm-leave-active { transition: opacity 0.15s; }
|
||||
.confirm-enter-from, .confirm-leave-to { opacity: 0; }
|
||||
</style>
|
||||
|
||||
@@ -263,13 +263,10 @@ onMounted(loadConnections)
|
||||
<style scoped>
|
||||
/* 阶段1/3 UX 重构:panel/panel-header/empty-hint/btn 系列/setting-select/setting-input
|
||||
已迁移到 settings.css 全局。
|
||||
本 scoped 仅保留 ConnectionPanel 特有样式:connection-card/conn 系列/type 系列/form 系列/
|
||||
btn-link 及 form-field 内输入控件布局覆盖等。 */
|
||||
本 scoped 仅保留 ConnectionPanel 特有样式:connection-card/conn 系列/type 系列/form 系列
|
||||
及 form-field 内输入控件布局覆盖等。 */
|
||||
|
||||
/* ===== 链接按钮(本面板特有,未进全局) ===== */
|
||||
.btn-link { background: none; border: none; color: var(--df-accent); font-size: 12px; cursor: pointer; padding: 2px 6px; }
|
||||
.btn-link:hover { text-decoration: underline; }
|
||||
.btn-link-danger { color: var(--df-danger); }
|
||||
/* 链接按钮 .btn-link/.btn-link-danger 已并轨 global.css(样式大重构去重) */
|
||||
|
||||
/* ===== 标题组(panel-header 内 标题+badge 一组,与右侧按钮 space-between) ===== */
|
||||
.panel-title-group { display: flex; align-items: center; gap: 8px; }
|
||||
@@ -307,10 +304,10 @@ onMounted(loadConnections)
|
||||
border-radius: var(--df-radius-sm);
|
||||
font-weight: 500;
|
||||
}
|
||||
.type-mysql { background: rgba(100,181,246,0.15); color: var(--df-info); }
|
||||
.type-mysql { background: var(--df-info-bg); color: var(--df-info); }
|
||||
.type-ssh { background: var(--df-accent-soft); color: var(--df-accent); }
|
||||
.type-redis { background: rgba(255,107,107,0.15); color: var(--df-danger); }
|
||||
.type-mongo { background: rgba(100,255,218,0.15); color: var(--df-success); }
|
||||
.type-redis { background: var(--df-danger-bg); color: var(--df-danger); }
|
||||
.type-mongo { background: var(--df-success-bg); color: var(--df-success); }
|
||||
|
||||
.conn-host, .conn-user {
|
||||
font-size: 12px;
|
||||
@@ -318,7 +315,7 @@ onMounted(loadConnections)
|
||||
font-family: var(--df-font-mono);
|
||||
}
|
||||
.conn-user {
|
||||
background: rgba(90,99,128,0.2);
|
||||
background: var(--df-neutral-bg);
|
||||
padding: 1px 6px;
|
||||
border-radius: var(--df-radius-sm);
|
||||
}
|
||||
|
||||
@@ -142,9 +142,11 @@ async function save() {
|
||||
<style scoped>
|
||||
/* panel/panel-header/btn 系列/setting-input 均在 settings.css 全局,此处仅本面板特有样式 */
|
||||
.feishu-settings { display: flex; flex-direction: column; gap: 4px; }
|
||||
.feishu-input { width: 280px; }
|
||||
/* 定宽输入补 max-width,窄窗收缩防横向溢出 */
|
||||
.feishu-input { width: 280px; max-width: 100%; }
|
||||
.feishu-whitelist {
|
||||
width: 320px;
|
||||
max-width: 100%;
|
||||
min-height: 64px;
|
||||
resize: vertical;
|
||||
font-family: var(--df-font-mono, monospace);
|
||||
@@ -153,7 +155,7 @@ async function save() {
|
||||
margin: 0 0 4px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--df-text-tertiary, #888);
|
||||
color: var(--df-text-dim);
|
||||
}
|
||||
.actions { display: flex; gap: 8px; margin-top: 8px; }
|
||||
</style>
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
</select>
|
||||
</SettingRow>
|
||||
<SettingRow v-if="knowledgeConfig.vector_enabled" ref="rowEmbeddingModel" :label="$t('settings.labelEmbeddingModel')" :desc="$t('settings.descEmbeddingModel')">
|
||||
<input type="text" :value="knowledgeConfig.embedding_model ?? ''" @input="knowledgeConfig.embedding_model = ($event.target as HTMLInputElement).value || null" @change="markSaved('embeddingModel')" class="setting-input" :placeholder="$t('settings.phEmbeddingModel')" style="width:220px" />
|
||||
<input type="text" :value="knowledgeConfig.embedding_model ?? ''" @input="knowledgeConfig.embedding_model = ($event.target as HTMLInputElement).value || null" @change="markSaved('embeddingModel')" class="setting-input" :placeholder="$t('settings.phEmbeddingModel')" style="width:220px; max-width:100%" />
|
||||
</SettingRow>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -5,55 +5,9 @@
|
||||
<h2>{{ $t('settings.panelAiProvider') }}</h2>
|
||||
<button class="btn btn-ghost btn-sm" @click="openProviderForm()">{{ $t('settings.addProvider') }}</button>
|
||||
</div>
|
||||
<div class="provider-list" v-if="aiProviders.length > 0">
|
||||
<div class="provider-card" :class="{ 'provider-card--default': p.is_default, 'provider-card--disabled': p.enabled === false }" v-for="p in aiProviders" :key="p.id">
|
||||
<div class="provider-top">
|
||||
<div class="provider-info">
|
||||
<span class="provider-name">{{ p.name }}</span>
|
||||
<span v-if="p.is_default" class="default-badge">{{ $t('settings.badgeDefault') }}</span>
|
||||
<span v-if="p.enabled === false" class="pool-badge">{{ $t('settings.badgePoolDisabled') }}</span>
|
||||
</div>
|
||||
<div class="provider-actions">
|
||||
<button class="btn-link" @click="setDefaultProvider(p.id)" v-if="!p.is_default">{{ $t('settings.actionSetDefault') }}</button>
|
||||
<button class="btn-link" @click="openProviderForm(p)">{{ $t('common.edit') }}</button>
|
||||
<button class="btn-link btn-link-danger" @click="deleteProvider(p.id)">{{ $t('common.delete') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="provider-detail">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">{{ $t('settings.detailBaseUrl') }}</span>
|
||||
<span class="detail-value">{{ p.base_url }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">{{ $t('settings.detailModel') }}</span>
|
||||
<span class="detail-value">{{ p.default_model }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">{{ $t('settings.detailApiKey') }}</span>
|
||||
<span class="detail-value mask">{{ maskKey(p.api_key) }}</span>
|
||||
</div>
|
||||
<!-- F-260614-04c 负载均衡池:enabled toggle + weight 即时调 IPC,落库即重建 caps -->
|
||||
<div class="detail-row detail-row--pool">
|
||||
<span class="detail-label" :title="$t('settings.descPoolEnabled')">{{ $t('settings.detailPoolEnabled') }}</span>
|
||||
<div class="detail-value detail-value--control">
|
||||
<label class="toggle toggle--sm" :title="$t('settings.descPoolEnabled')">
|
||||
<input type="checkbox" :checked="p.enabled !== false" @change="onPoolToggle(p, ($event.target as HTMLInputElement).checked)" />
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-row detail-row--pool" v-if="p.enabled !== false">
|
||||
<span class="detail-label" :title="$t('settings.descPoolWeight')">{{ $t('settings.detailPoolWeight') }}</span>
|
||||
<div class="detail-value detail-value--control">
|
||||
<input type="number" class="setting-input setting-input--sm" min="0" max="100"
|
||||
:value="p.weight ?? 50"
|
||||
@change="onPoolWeightChange(p, ($event.target as HTMLInputElement).valueAsNumber)" />
|
||||
<span class="pool-weight-hint">0-100</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 卡片列表 + 负载均衡池控件拆至 provider-panel/provider-list.vue(C4c) -->
|
||||
<ProviderList v-if="aiProviders.length > 0" ref="providerListRef" :providers="aiProviders"
|
||||
@edit="openProviderForm" @delete="deleteProvider" @set-default="setDefaultProvider" @toast="relayToast" />
|
||||
<div class="empty-hint" v-else>{{ $t('settings.emptyProvider') }}</div>
|
||||
</section>
|
||||
|
||||
@@ -127,8 +81,8 @@
|
||||
</div>
|
||||
<!-- P0-2: 保存失败持久错误横幅(常驻表单内,非自隐 toast)。
|
||||
含分类建议:keyring/迁移失败 → 查系统钥匙串权限;其它 → 通用重试。
|
||||
用户看到错误同时表单保持编辑态,改完点保存即可重试。 -->
|
||||
<div v-if="providerForm.saveError" class="save-error-banner">
|
||||
用户看到错误同时表单保持编辑态,改完点保存即可重试。视觉走全局 .df-note--danger。 -->
|
||||
<div v-if="providerForm.saveError" class="df-note df-note--danger">
|
||||
<pre class="save-error-msg">{{ providerForm.saveError }}</pre>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
@@ -140,59 +94,10 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ═══ 拉取到的模型列表(F-01 阶段6)═══ -->
|
||||
<!-- ═══ 拉取到的模型列表(F-01 阶段6;表格+标签标注拆至 provider-panel/model-table.vue)═══ -->
|
||||
<div class="form-field form-field--full" v-if="providerForm.models.length > 0">
|
||||
<label class="form-label">{{ $t('settings.modelListTitle', { count: providerForm.models.length }) }}</label>
|
||||
<table class="model-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-enabled">{{ $t('settings.colEnabled') }}</th>
|
||||
<th class="col-model">{{ $t('settings.colModel') }}</th>
|
||||
<th class="col-ability">{{ $t('settings.colAbility') }}</th>
|
||||
<th class="col-weight">{{ $t('settings.labelWeight') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="m in providerForm.models" :key="m.model_id">
|
||||
<td class="col-enabled">
|
||||
<label class="toggle toggle--sm">
|
||||
<input type="checkbox" v-model="m.enabled" />
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</td>
|
||||
<td class="col-model">
|
||||
<span class="model-id">{{ m.model_id }}</span>
|
||||
</td>
|
||||
<td class="col-ability">
|
||||
<!-- 能力/模态标签(可编辑:点 × 删除;点 + 添加;手动改后标为「手动」) -->
|
||||
<div class="model-tags">
|
||||
<span v-for="md in m.modalities" v-show="!redundantTags.modalities.has(md)" :key="'md-' + md"
|
||||
class="tag tag-modality" :title="$t('settings.tagTooltipModality')">
|
||||
{{ $t('settings.tagModality.' + md) }}<span class="tag-remove" role="button" tabindex="0" :aria-label="$t('common.delete')" @click.stop="removeModality(m, md)" @keydown.enter.prevent="removeModality(m, md)" @keydown.space.prevent="removeModality(m, md)">×</span>
|
||||
</span>
|
||||
<span v-for="cap in m.capabilities" v-show="!redundantTags.capabilities.has(cap)" :key="'cap-' + cap"
|
||||
class="tag tag-capability" :title="$t('settings.tagTooltipCapability')">
|
||||
{{ $t('settings.tagCapability.' + cap) }}<span class="tag-remove" role="button" tabindex="0" :aria-label="$t('common.delete')" @click.stop="removeCapability(m, cap)" @keydown.enter.prevent="removeCapability(m, cap)" @keydown.space.prevent="removeCapability(m, cap)">×</span>
|
||||
</span>
|
||||
<span v-if="m.probe_source && m.probe_source !== 'user_set'" v-show="!redundantTags.sources.has(m.probe_source)"
|
||||
class="tag tag-source" :title="$t('settings.tagTooltipSource')">{{ $t('settings.tagSource.' + m.probe_source) }}</span>
|
||||
<span v-if="m.probe_source === 'user_set'" class="tag tag-source" :title="$t('settings.tagTooltipSource')">{{ $t('settings.tagSource.user_set') }}</span>
|
||||
<span class="tag-add" role="button" tabindex="0" :title="$t('settings.addTagTitle')" :aria-label="$t('settings.addTagTitle')" @click.stop="startAddTag(m)" @keydown.enter.prevent="startAddTag(m)" @keydown.space.prevent="startAddTag(m)">+</span>
|
||||
</div>
|
||||
<!-- 添加能力内联菜单(点+展开;选未有的能力/模态即添加) -->
|
||||
<div v-if="addingTagModel === m.model_id" class="add-tag-menu">
|
||||
<span v-for="opt in addableTags(m)" :key="opt.kind + '-' + opt.key"
|
||||
class="tag" :class="opt.kind === 'modality' ? 'tag-modality' : 'tag-capability'"
|
||||
@click.stop="addTag(m, opt.key, opt.kind)">{{ opt.label }}</span>
|
||||
<span v-if="addableTags(m).length === 0" class="add-tag-empty">{{ $t('settings.addTagNone') }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="col-weight">
|
||||
<input type="number" class="setting-input setting-input--sm" min="0" max="100" v-model.number="m.weight" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<ModelTable :models="providerForm.models" />
|
||||
<div class="model-list-hint">{{ $t('settings.modelListHint') }}</div>
|
||||
<!-- M32: 模型列表 enabled/weight 改了未保存时显提示(关闭/切走将丢弃) -->
|
||||
<div v-if="isFormDirty" class="model-list-unsaved-hint">{{ $t('settings.modelListUnsavedHint') }}</div>
|
||||
@@ -205,11 +110,17 @@
|
||||
import { reactive, ref, computed, onMounted, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { aiApi } from '@/api'
|
||||
import type { AiProviderConfig, ModelConfig, Modality, Capability, ProbeSource } from '@/api/types'
|
||||
import type { AiProviderConfig, ModelConfig } from '@/api/types'
|
||||
import { PROVIDER_PRESETS, CUSTOM_PRESET_ID, findPreset } from './providerPresets'
|
||||
import ProviderList from './provider-panel/provider-list.vue'
|
||||
import ModelTable from './provider-panel/model-table.vue'
|
||||
|
||||
// ============================================================
|
||||
// AI 提供商域 — 真实后端 CRUD + c 负载均衡池(enabled / weight)
|
||||
// C4c 拆分:卡片列表+池控件 → provider-panel/provider-list.vue;
|
||||
// 模型表格+能力标签标注 → provider-panel/model-table.vue。
|
||||
// 本件保留:表单状态机(校验红框/温度与权重钳制/快照脏判)、保存/测试连接/预设回填、
|
||||
// 列表 CRUD 与 loadProviders 闭环、对壳 defineExpose 契约。
|
||||
// ============================================================
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -286,37 +197,7 @@ const providerForm = reactive({
|
||||
savedSnapshot: '' as string,
|
||||
})
|
||||
|
||||
/**
|
||||
* 冗余标签判定:全模型共有的 modality/capability/probe_source 无区分度,
|
||||
* 每行都渲染会刷屏(如全部命中预设表 → 全是「预设」;对话模型默认 ToolUse → 全是「工具调用」)。
|
||||
* 计算「所有模型都含」的值集合,模板渲染时跳过 → 只显示有区分度的标签。
|
||||
*/
|
||||
const redundantTags = computed(() => {
|
||||
const models = providerForm.models
|
||||
// SA-6: 去冗余的前提是「模型间对比」——仅 1 个模型时它的所有标签都满足「全模型共有」,
|
||||
// 无脑隐藏会把唯一模型的能力标签全抹掉(行内空白,用户看不到该模型能力)。
|
||||
// 故 ≥2 个模型起才去冗余;0/1 个一律全显(sources 分支本就 <2 直接放行,此处对齐)。
|
||||
if (models.length < 2) return { modalities: new Set<string>(), capabilities: new Set<string>(), sources: new Set<string>() }
|
||||
const common = (pick: (m: ModelConfig) => string[]) => {
|
||||
const sets = models.map(m => new Set(pick(m)))
|
||||
const first = sets[0]
|
||||
const result = new Set<string>()
|
||||
for (const v of first) {
|
||||
if (sets.every(s => s.has(v))) result.add(v)
|
||||
}
|
||||
return result
|
||||
}
|
||||
return {
|
||||
modalities: common(m => m.modalities),
|
||||
capabilities: common(m => m.capabilities),
|
||||
// 探测来源:全模型相同的值也冗余(如全 PresetTable)
|
||||
sources: (() => {
|
||||
const vals = models.map(m => m.probe_source).filter((v): v is ProbeSource => !!v)
|
||||
if (vals.length < 2) return new Set<string>()
|
||||
return new Set([...new Set(vals)].filter(v => vals.every(x => x === v)))
|
||||
})(),
|
||||
}
|
||||
})
|
||||
// (冗余标签判定 redundantTags 随模型表格拆至 provider-panel/model-table.vue)
|
||||
|
||||
/**
|
||||
* L19/M32: 表单脏判定——当前表单状态与已落库快照不一致即脏。
|
||||
@@ -349,59 +230,7 @@ function formBusinessSnapshot(): string {
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 模型能力手动标注(表格内编辑 tags;改动即标 UserSet,手动优先)
|
||||
// ============================================================
|
||||
|
||||
/** 当前展开「添加能力」菜单的模型名;null=无展开 */
|
||||
const addingTagModel = ref<string | null>(null)
|
||||
|
||||
/** 可添加的模态(与 tagModality 对齐;text 是对话基础模态,允许删但一般不主动加) */
|
||||
const ADDABLE_MODALITIES: Modality[] = ['text', 'vision', 'audio', 'video']
|
||||
/** 可添加的能力 */
|
||||
const ADDABLE_CAPABILITIES: Capability[] = ['tool_use', 'embedding', 'code_gen']
|
||||
|
||||
/** 某模型可添加的可选 tag(模态 + 能力,排除已选的) */
|
||||
function addableTags(m: ModelConfig): { key: string; kind: 'modality' | 'capability'; label: string }[] {
|
||||
const mods = ADDABLE_MODALITIES
|
||||
.filter(x => !m.modalities.includes(x))
|
||||
.map(x => ({ key: x, kind: 'modality' as const, label: t('settings.tagModality.' + x) }))
|
||||
const caps = ADDABLE_CAPABILITIES
|
||||
.filter(x => !m.capabilities.includes(x))
|
||||
.map(x => ({ key: x, kind: 'capability' as const, label: t('settings.tagCapability.' + x) }))
|
||||
return [...mods, ...caps]
|
||||
}
|
||||
|
||||
/** 标记模型为「手动标注」(probe_source=user_set),下次探测不覆盖 */
|
||||
function markManual(m: ModelConfig) {
|
||||
m.probe_source = 'user_set'
|
||||
addingTagModel.value = null
|
||||
}
|
||||
|
||||
function removeModality(m: ModelConfig, md: Modality) {
|
||||
m.modalities = m.modalities.filter(x => x !== md)
|
||||
markManual(m)
|
||||
}
|
||||
|
||||
function removeCapability(m: ModelConfig, cap: Capability) {
|
||||
m.capabilities = m.capabilities.filter(x => x !== cap)
|
||||
markManual(m)
|
||||
}
|
||||
|
||||
function addTag(m: ModelConfig, key: string, kind: 'modality' | 'capability') {
|
||||
if (kind === 'modality') {
|
||||
const md = key as Modality
|
||||
if (!m.modalities.includes(md)) m.modalities = [...m.modalities, md]
|
||||
} else {
|
||||
const cap = key as Capability
|
||||
if (!m.capabilities.includes(cap)) m.capabilities = [...m.capabilities, cap]
|
||||
}
|
||||
markManual(m)
|
||||
}
|
||||
|
||||
function startAddTag(m: ModelConfig) {
|
||||
addingTagModel.value = addingTagModel.value === m.model_id ? null : m.model_id
|
||||
}
|
||||
// (模型能力手动标注 tag 编辑块随模型表格拆至 provider-panel/model-table.vue)
|
||||
|
||||
/**
|
||||
* L19: 关闭表单入口——脏表单二次确认,清表单后关闭。
|
||||
@@ -677,64 +506,15 @@ async function setDefaultProvider(id: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// c: 负载均衡池可编辑层(enabled / weight)
|
||||
// 卡片即时调 IPC(对齐 syncConcurrencyConfig 模式);落库后后端 reload_provider_caps
|
||||
// 重建 per_provider caps,下条消息即按新配置 acquire。
|
||||
// 不走 saveProvider(全量 INSERT OR REPLACE 会触发 R-PD-1 密钥迁移分支,与 c 轻量 UPDATE 路径冲突)。
|
||||
// ============================================================
|
||||
// 计时器按 provider id 分键:单例计时器下 300ms 内先后改两家权重,B 的变更会清掉
|
||||
// A 尚未落库的保存,先改的一方静默丢失(走查 P2-8)。
|
||||
const _poolTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
// (负载均衡池 enabled/weight 即时 IPC 块随卡片列表拆至 provider-panel/provider-list.vue)
|
||||
|
||||
/**
|
||||
* toggle 即时生效(开关语义=立即入/出池,无 debounce 必要;但失败需回滚视图)。
|
||||
* 乐观更新本地数组,失败 revert + toast。
|
||||
*/
|
||||
async function onPoolToggle(p: AiProviderConfig, enabled: boolean) {
|
||||
const prev = p.enabled
|
||||
p.enabled = enabled
|
||||
try {
|
||||
// 防御钳制:p.weight 理论上已被 onPoolWeightChange 钳过,此处兜底防脏数据直传 IPC
|
||||
const w = Math.min(100, Math.max(0, p.weight ?? 50))
|
||||
await aiApi.updateProviderPool(p.id, enabled, w)
|
||||
emit('toast', t('settings.toastPoolUpdateOk'), 'info')
|
||||
} catch (e) {
|
||||
p.enabled = prev // revert
|
||||
console.error(t('settings.toastPoolUpdateFail', { msg: errMsg(e) }), e)
|
||||
emit('toast', t('settings.toastPoolUpdateFail', { msg: errMsg(e) }), 'error')
|
||||
}
|
||||
/** 子件 toast 上抛中继(池控件 IPC 结果反馈) */
|
||||
function relayToast(msg: string, type: 'error' | 'warning' | 'info' | 'success') {
|
||||
emit('toast', msg, type)
|
||||
}
|
||||
|
||||
/**
|
||||
* weight 变更 debounce 300ms(防快速连续输入触发频繁 IPC),clamp [0,100] 对齐后端。
|
||||
* 双 clamp:前端 clamp 视觉输入 + 后端 command 再 clamp(防御性)。
|
||||
*/
|
||||
function onPoolWeightChange(p: AiProviderConfig, raw: number) {
|
||||
// NaN 兜底(清空输入):回退 50,防 IPC 传 NaN 后端 serde u32 失败
|
||||
const clamped = Math.min(100, Math.max(0, Number.isFinite(raw) ? Math.trunc(raw) : 50))
|
||||
const prev = p.weight
|
||||
p.weight = clamped
|
||||
// 仅重置同一 provider 的待发计时;跨 provider 互不干扰
|
||||
const prevTimer = _poolTimers.get(p.id)
|
||||
if (prevTimer) clearTimeout(prevTimer)
|
||||
_poolTimers.set(p.id, setTimeout(async () => {
|
||||
_poolTimers.delete(p.id)
|
||||
try {
|
||||
await aiApi.updateProviderPool(p.id, p.enabled !== false, clamped)
|
||||
emit('toast', t('settings.toastPoolUpdateOk'), 'info')
|
||||
} catch (e) {
|
||||
p.weight = prev // revert
|
||||
console.error(t('settings.toastPoolUpdateFail', { msg: errMsg(e) }), e)
|
||||
emit('toast', t('settings.toastPoolUpdateFail', { msg: errMsg(e) }), 'error')
|
||||
}
|
||||
}, 300))
|
||||
}
|
||||
|
||||
function maskKey(key: string): string {
|
||||
if (!key || key.length < 8) return '••••••••'
|
||||
return key.slice(0, 4) + '••••••••' + key.slice(-4)
|
||||
}
|
||||
/** 卡片列表子件引用(池 debounce 计时器下沉子件,卸载清理经此委托) */
|
||||
const providerListRef = useTemplateRef<InstanceType<typeof ProviderList>>('providerListRef')
|
||||
|
||||
/** 暴露给 shell:loadProviders 供外部主动刷新 / Knowledge 读 aiProviders / 卸载清 timer。
|
||||
* L19: isFormDirty + closeProviderForm 暴露,供外部(如导航切走前)查询/触发拦截。
|
||||
@@ -744,7 +524,7 @@ defineExpose({
|
||||
loadProviders,
|
||||
isFormDirty,
|
||||
closeProviderForm,
|
||||
clearPoolTimer() { for (const t of _poolTimers.values()) clearTimeout(t); _poolTimers.clear() },
|
||||
clearPoolTimer() { providerListRef.value?.clearPoolTimer() },
|
||||
})
|
||||
|
||||
// 阶段3 UX 重构:本面板改为 ai-model 类懒挂载,不再由 Settings.vue onMounted 调用,
|
||||
@@ -754,92 +534,24 @@ onMounted(loadProviders)
|
||||
|
||||
<style scoped>
|
||||
/* 阶段1/3 UX 重构:panel/panel-header/empty-hint/btn 系列/setting-select/
|
||||
setting-input/toggle/toggle-slider 已迁移到 settings.css 全局。
|
||||
本 scoped 仅保留 ProviderPanel 特有样式:provider-card/default-badge/
|
||||
pool-badge/detail 系列/form 系列/btn-link/toggle--sm 及输入控件布局覆盖等。
|
||||
setting-input/toggle/toggle-slider 已迁移到 settings.css 全局,btn-link 已并轨 global.css。
|
||||
C4c 拆分:provider 卡片/池控件样式随 provider-list.vue、模型表格/标签样式随
|
||||
model-table.vue 下沉子件;本 scoped 仅保留表单区样式(form 系列/红框/错误横幅/
|
||||
模型列表区容器与提示行)。.setting-input--sm/.toggle--sm 为两子件共用,各持一份。
|
||||
注:注释列举类名时勿写星斜杠序列(会提前闭合 CSS 注释)。 */
|
||||
|
||||
/* ===== 链接按钮(本面板特有,未进全局) ===== */
|
||||
.btn-link { background: none; border: none; color: var(--df-accent); font-size: 12px; cursor: pointer; padding: 2px 6px; }
|
||||
.btn-link:hover { text-decoration: underline; }
|
||||
.btn-link-danger { color: var(--df-danger); }
|
||||
|
||||
/* ===== Provider 卡片 ===== */
|
||||
.provider-list { display: flex; flex-direction: column; gap: 12px; }
|
||||
.provider-card {
|
||||
background: var(--df-bg);
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius);
|
||||
padding: 14px 16px;
|
||||
}
|
||||
.provider-card--default {
|
||||
border-color: var(--df-accent);
|
||||
box-shadow: inset 3px 0 0 var(--df-accent);
|
||||
}
|
||||
.provider-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.provider-info { display: flex; align-items: center; gap: 8px; }
|
||||
.provider-name { font-size: 14px; font-weight: 500; color: var(--df-text); }
|
||||
.default-badge {
|
||||
font-size: 10px;
|
||||
padding: 1px 6px;
|
||||
border-radius: var(--df-radius-xs);
|
||||
background: rgba(100,255,218,0.15);
|
||||
color: var(--df-success);
|
||||
font-weight: 500;
|
||||
}
|
||||
.provider-actions { display: flex; gap: 4px; }
|
||||
|
||||
.provider-detail { display: flex; flex-direction: column; gap: 6px; }
|
||||
.detail-row { display: flex; align-items: flex-start; gap: 12px; }
|
||||
.detail-label {
|
||||
font-size: 12px;
|
||||
color: var(--df-text-dim);
|
||||
min-width: 60px;
|
||||
}
|
||||
.detail-value {
|
||||
font-size: 12px;
|
||||
color: var(--df-text-secondary);
|
||||
font-family: var(--df-font-mono);
|
||||
}
|
||||
.detail-value.mask { color: var(--df-text-dim); letter-spacing: 1px; }
|
||||
|
||||
/* Provider 卡片 disabled 态(c:enabled=false 视觉弱化) */
|
||||
.provider-card--disabled { opacity: 0.6; }
|
||||
.pool-badge {
|
||||
font-size: 10px;
|
||||
padding: 1px 6px;
|
||||
border-radius: var(--df-radius-xs);
|
||||
background: rgba(255,107,107,0.15);
|
||||
color: var(--df-danger);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 负载均衡池行(toggle / weight 控件) */
|
||||
.detail-row--pool { align-items: center; }
|
||||
.detail-value--control { display: flex; align-items: center; gap: 8px; font-family: var(--df-font-mono); }
|
||||
.pool-weight-hint { font-size: 11px; color: var(--df-text-dim); }
|
||||
|
||||
/* 表单(本面板特有输入控件样式;.setting-select 走全局) */
|
||||
.form-grid { display: flex; flex-direction: column; gap: 14px; }
|
||||
/* 表单(本面板特有输入控件样式;.setting-select 走全局)。
|
||||
--form-label-w 为标签列宽单一来源:hint 缩进用 calc(label 列宽+行 gap) 对齐 input 位,
|
||||
免手算魔法数。 */
|
||||
.form-grid { --form-label-w: 72px; display: flex; flex-direction: column; gap: 14px; }
|
||||
.form-field { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
|
||||
.form-label { flex: 0 0 72px; font-size: 12px; font-weight: 500; color: var(--df-text-secondary); text-align: right; }
|
||||
.form-label { flex: 0 0 var(--form-label-w); font-size: 12px; font-weight: 500; color: var(--df-text-secondary); text-align: right; }
|
||||
/* 字段级辅助说明(默认模型提示等):弱化色 + 小字,与 label/input 区分层级。
|
||||
横向布局下占满整行换到 label+input 下方。 */
|
||||
.form-field-hint { flex-basis: 100%; margin-left: 82px; font-size: 11px; color: var(--df-text-dim); line-height: 1.5; }
|
||||
横向布局下占满整行换到 label+input 下方,缩进与 input 左缘结构对齐。 */
|
||||
.form-field-hint { flex-basis: 100%; margin-left: calc(var(--form-label-w) + 10px); font-size: 11px; color: var(--df-text-dim); line-height: 1.5; }
|
||||
.form-actions { display: flex; justify-content: flex-end; padding-top: 4px; }
|
||||
|
||||
/* 保存失败持久错误横幅(常驻 form-actions 上方,醒目于自隐 toast) */
|
||||
.save-error-banner {
|
||||
background: var(--df-danger-bg);
|
||||
border: 0.5px solid var(--df-danger);
|
||||
border-radius: var(--df-radius-sm);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
/* 保存失败持久错误横幅:视觉(底色/边/圆角/内距)走全局 .df-note--danger,此处仅留内部排版 */
|
||||
.save-error-msg {
|
||||
margin: 0;
|
||||
font-family: var(--df-font-sans);
|
||||
@@ -860,12 +572,6 @@ onMounted(loadProviders)
|
||||
/* 走查 P3-10:校验缺失字段红框(@input 即清,用户开始修正就不再红) */
|
||||
.setting-input--invalid { border-color: var(--df-danger) !important; }
|
||||
.setting-input--invalid:focus { border-color: var(--df-danger) !important; box-shadow: 0 0 0 1px var(--df-danger); }
|
||||
.setting-input--sm { flex: 0 0 auto; min-width: 0; width: 70px; padding: 4px 8px; font-size: 12px; }
|
||||
|
||||
/* Toggle 小号变体(本面板负载均衡池用;.toggle 基础走全局) */
|
||||
.toggle--sm { width: 32px; height: 18px; }
|
||||
.toggle--sm .toggle-slider::before { width: 12px; height: 12px; left: 3px; bottom: 3px; }
|
||||
.toggle--sm input:checked + .toggle-slider::before { transform: translateX(14px); }
|
||||
|
||||
/* M32: 模型列表有未保存调整时的提示(弱 warning 色,贴合 model-list-hint 之下) */
|
||||
.model-list-unsaved-hint {
|
||||
@@ -874,86 +580,6 @@ onMounted(loadProviders)
|
||||
line-height: 1.5;
|
||||
color: var(--df-warning);
|
||||
}
|
||||
|
||||
/* ===== 模型列表(表格形式:表头 + 数据行) =====
|
||||
原 model-list/model-row 等无任何样式定义(裸渲染),本批补齐并改为表格。
|
||||
风格对齐 provider-card(浅底 / 圆角 / 0.5px 边框)。 */
|
||||
.model-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius);
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
}
|
||||
.model-table th,
|
||||
.model-table td {
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.model-table thead th {
|
||||
background: var(--df-bg);
|
||||
font-weight: 500;
|
||||
color: var(--df-text-dim);
|
||||
font-size: 11px;
|
||||
border-bottom: 0.5px solid var(--df-border);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.model-table tbody tr { border-bottom: 0.5px solid var(--df-border); }
|
||||
.model-table tbody tr:last-child { border-bottom: none; }
|
||||
.model-table tbody tr:hover { background: var(--df-accent-bg); }
|
||||
/* 列宽:启用窄、模型/能力弹性、权重窄 */
|
||||
.model-table .col-enabled { width: 56px; text-align: center; }
|
||||
.model-table .col-weight { width: 110px; }
|
||||
.model-table .col-model { min-width: 140px; }
|
||||
.col-enabled .toggle--sm { display: inline-block; }
|
||||
.model-id {
|
||||
font-size: 13px;
|
||||
font-family: var(--df-font-mono);
|
||||
color: var(--df-text);
|
||||
font-weight: 500;
|
||||
word-break: break-all;
|
||||
}
|
||||
/* 标签列:小圆角 chip,三类语义色区分 */
|
||||
.model-tags { display: flex; flex-wrap: wrap; gap: 4px; }
|
||||
.tag {
|
||||
font-size: 10px;
|
||||
padding: 1px 6px;
|
||||
border-radius: var(--df-radius-xs);
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.tag-modality { background: rgba(100,181,246,0.15); color: var(--df-info); }
|
||||
.tag-capability { background: rgba(108,99,255,0.15); color: var(--df-accent); }
|
||||
.tag-source { background: rgba(90,99,128,0.2); color: var(--df-text-dim); }
|
||||
/* 可编辑 tag:×删除按钮(悬停显红)+ 添加按钮 */
|
||||
.tag-remove { margin-left: 3px; cursor: pointer; opacity: 0.6; font-weight: 700; }
|
||||
.tag-remove:hover { opacity: 1; color: var(--df-danger); }
|
||||
.tag-add {
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 1px dashed var(--df-border);
|
||||
color: var(--df-text-dim);
|
||||
font-weight: 700;
|
||||
}
|
||||
.tag-add:hover { border-color: var(--df-accent); color: var(--df-accent); }
|
||||
/* 添加能力内联菜单(点+展开):浅底容器包可选 tag */
|
||||
.add-tag-menu {
|
||||
margin-top: 4px;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
background: var(--df-bg);
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius-sm);
|
||||
}
|
||||
.add-tag-menu .tag { cursor: pointer; }
|
||||
.add-tag-menu .tag:hover { filter: brightness(1.2); }
|
||||
.add-tag-empty { font-size: 10px; color: var(--df-text-dim); padding: 2px 4px; }
|
||||
/* 权重输入框:表格内固定窄宽 */
|
||||
.model-table .setting-input--sm { width: 64px; }
|
||||
.model-list-hint {
|
||||
margin-top: 8px;
|
||||
font-size: 11px;
|
||||
|
||||
@@ -115,10 +115,12 @@ async function save() {
|
||||
<style scoped>
|
||||
/* panel/panel-header/btn 系列/setting-input 均在 settings.css 全局,此处仅本面板特有样式 */
|
||||
.uclaw-settings { display: flex; flex-direction: column; gap: 4px; }
|
||||
.uclaw-input { width: 320px; font-family: var(--df-font-mono, monospace); }
|
||||
/* 定宽输入补 max-width,窄窗收缩防横向溢出 */
|
||||
.uclaw-input { width: 320px; max-width: 100%; font-family: var(--df-font-mono, monospace); }
|
||||
/* 原 var(--df-text-tertiary) 为幽灵 token(全 styles 未定义,渲染成继承色),改用已定义的弱化色 */
|
||||
.uclaw-hint {
|
||||
font-size: 11px;
|
||||
color: var(--df-text-tertiary);
|
||||
color: var(--df-text-dim);
|
||||
line-height: 1.6;
|
||||
margin-top: 4px;
|
||||
white-space: pre-line;
|
||||
|
||||
@@ -145,7 +145,8 @@ function onPollSecsChange(raw: string) {
|
||||
<style scoped>
|
||||
/* panel/panel-header/toggle/setting-input 均在 settings.css 全局,此处仅本面板特有样式 */
|
||||
.unattended-settings { display: flex; flex-direction: column; gap: 4px; }
|
||||
.unattended-input { width: 280px; }
|
||||
/* 定宽输入补 max-width,窄窗收缩防横向溢出(--num 变体继承同款收缩) */
|
||||
.unattended-input { width: 280px; max-width: 100%; }
|
||||
.unattended-input--num { width: 110px; }
|
||||
.unattended-radios { display: flex; gap: 16px; flex-wrap: wrap; }
|
||||
.unattended-radio {
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
<template>
|
||||
<!-- 拉取到的模型列表表格(F-01 阶段6)。自 ProviderPanel 拆出(C4c 纯搬迁):
|
||||
表格渲染 + 能力/模态标签手动标注(改动即标 UserSet)自治在本件。
|
||||
models 为主文件 providerForm.models 的引用,行内 v-model/标签增删直接改
|
||||
item 字段(共享响应式对象),脏态由主文件 isFormDirty 统一比对。 -->
|
||||
<table class="model-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-enabled">{{ $t('settings.colEnabled') }}</th>
|
||||
<th class="col-model">{{ $t('settings.colModel') }}</th>
|
||||
<th class="col-ability">{{ $t('settings.colAbility') }}</th>
|
||||
<th class="col-weight">{{ $t('settings.labelWeight') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="m in models" :key="m.model_id">
|
||||
<td class="col-enabled">
|
||||
<label class="toggle toggle--sm">
|
||||
<input type="checkbox" v-model="m.enabled" />
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</td>
|
||||
<td class="col-model">
|
||||
<span class="model-id">{{ m.model_id }}</span>
|
||||
</td>
|
||||
<td class="col-ability">
|
||||
<!-- 能力/模态标签(可编辑:点 × 删除;点 + 添加;手动改后标为「手动」) -->
|
||||
<div class="model-tags">
|
||||
<span v-for="md in m.modalities" v-show="!redundantTags.modalities.has(md)" :key="'md-' + md"
|
||||
class="tag tag-modality" :title="$t('settings.tagTooltipModality')">
|
||||
{{ $t('settings.tagModality.' + md) }}<span class="tag-remove" role="button" tabindex="0" :aria-label="$t('common.delete')" @click.stop="removeModality(m, md)" @keydown.enter.prevent="removeModality(m, md)" @keydown.space.prevent="removeModality(m, md)">×</span>
|
||||
</span>
|
||||
<span v-for="cap in m.capabilities" v-show="!redundantTags.capabilities.has(cap)" :key="'cap-' + cap"
|
||||
class="tag tag-capability" :title="$t('settings.tagTooltipCapability')">
|
||||
{{ $t('settings.tagCapability.' + cap) }}<span class="tag-remove" role="button" tabindex="0" :aria-label="$t('common.delete')" @click.stop="removeCapability(m, cap)" @keydown.enter.prevent="removeCapability(m, cap)" @keydown.space.prevent="removeCapability(m, cap)">×</span>
|
||||
</span>
|
||||
<span v-if="m.probe_source && m.probe_source !== 'user_set'" v-show="!redundantTags.sources.has(m.probe_source)"
|
||||
class="tag tag-source" :title="$t('settings.tagTooltipSource')">{{ $t('settings.tagSource.' + m.probe_source) }}</span>
|
||||
<span v-if="m.probe_source === 'user_set'" class="tag tag-source" :title="$t('settings.tagTooltipSource')">{{ $t('settings.tagSource.user_set') }}</span>
|
||||
<span class="tag-add" role="button" tabindex="0" :title="$t('settings.addTagTitle')" :aria-label="$t('settings.addTagTitle')" @click.stop="startAddTag(m)" @keydown.enter.prevent="startAddTag(m)" @keydown.space.prevent="startAddTag(m)">+</span>
|
||||
</div>
|
||||
<!-- 添加能力内联菜单(点+展开;选未有的能力/模态即添加) -->
|
||||
<div v-if="addingTagModel === m.model_id" class="add-tag-menu">
|
||||
<span v-for="opt in addableTags(m)" :key="opt.kind + '-' + opt.key"
|
||||
class="tag" :class="opt.kind === 'modality' ? 'tag-modality' : 'tag-capability'"
|
||||
@click.stop="addTag(m, opt.key, opt.kind)">{{ opt.label }}</span>
|
||||
<span v-if="addableTags(m).length === 0" class="add-tag-empty">{{ $t('settings.addTagNone') }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="col-weight">
|
||||
<input type="number" class="setting-input setting-input--sm" min="0" max="100" v-model.number="m.weight" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { ModelConfig, Modality, Capability, ProbeSource } from '@/api/types'
|
||||
|
||||
// ============================================================
|
||||
// 模型能力手动标注(表格内编辑 tags;改动即标 UserSet,手动优先)
|
||||
// —— 原 ProviderPanel 整块搬迁(C4c 纯搬迁,共享函数不新造)。
|
||||
// ============================================================
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
/** 主文件 providerForm.models 引用(行内直改 item 字段,保存闭环留主文件 saveProvider) */
|
||||
models: ModelConfig[]
|
||||
}>()
|
||||
|
||||
/**
|
||||
* 冗余标签判定:全模型共有的 modality/capability/probe_source 无区分度,
|
||||
* 每行都渲染会刷屏(如全部命中预设表 → 全是「预设」;对话模型默认 ToolUse → 全是「工具调用」)。
|
||||
* 计算「所有模型都含」的值集合,模板渲染时跳过 → 只显示有区分度的标签。
|
||||
*/
|
||||
const redundantTags = computed(() => {
|
||||
const models = props.models
|
||||
// SA-6: 去冗余的前提是「模型间对比」——仅 1 个模型时它的所有标签都满足「全模型共有」,
|
||||
// 无脑隐藏会把唯一模型的能力标签全抹掉(行内空白,用户看不到该模型能力)。
|
||||
// 故 ≥2 个模型起才去冗余;0/1 个一律全显(sources 分支本就 <2 直接放行,此处对齐)。
|
||||
if (models.length < 2) return { modalities: new Set<string>(), capabilities: new Set<string>(), sources: new Set<string>() }
|
||||
const common = (pick: (m: ModelConfig) => string[]) => {
|
||||
const sets = models.map(m => new Set(pick(m)))
|
||||
const first = sets[0]
|
||||
const result = new Set<string>()
|
||||
for (const v of first) {
|
||||
if (sets.every(s => s.has(v))) result.add(v)
|
||||
}
|
||||
return result
|
||||
}
|
||||
return {
|
||||
modalities: common(m => m.modalities),
|
||||
capabilities: common(m => m.capabilities),
|
||||
// 探测来源:全模型相同的值也冗余(如全 PresetTable)
|
||||
sources: (() => {
|
||||
const vals = models.map(m => m.probe_source).filter((v): v is ProbeSource => !!v)
|
||||
if (vals.length < 2) return new Set<string>()
|
||||
return new Set([...new Set(vals)].filter(v => vals.every(x => x === v)))
|
||||
})(),
|
||||
}
|
||||
})
|
||||
|
||||
/** 当前展开「添加能力」菜单的模型名;null=无展开 */
|
||||
const addingTagModel = ref<string | null>(null)
|
||||
|
||||
/** 可添加的模态(与 tagModality 对齐;text 是对话基础模态,允许删但一般不主动加) */
|
||||
const ADDABLE_MODALITIES: Modality[] = ['text', 'vision', 'audio', 'video']
|
||||
/** 可添加的能力 */
|
||||
const ADDABLE_CAPABILITIES: Capability[] = ['tool_use', 'embedding', 'code_gen']
|
||||
|
||||
/** 某模型可添加的可选 tag(模态 + 能力,排除已选的) */
|
||||
function addableTags(m: ModelConfig): { key: string; kind: 'modality' | 'capability'; label: string }[] {
|
||||
const mods = ADDABLE_MODALITIES
|
||||
.filter(x => !m.modalities.includes(x))
|
||||
.map(x => ({ key: x, kind: 'modality' as const, label: t('settings.tagModality.' + x) }))
|
||||
const caps = ADDABLE_CAPABILITIES
|
||||
.filter(x => !m.capabilities.includes(x))
|
||||
.map(x => ({ key: x, kind: 'capability' as const, label: t('settings.tagCapability.' + x) }))
|
||||
return [...mods, ...caps]
|
||||
}
|
||||
|
||||
/** 标记模型为「手动标注」(probe_source=user_set),下次探测不覆盖 */
|
||||
function markManual(m: ModelConfig) {
|
||||
m.probe_source = 'user_set'
|
||||
addingTagModel.value = null
|
||||
}
|
||||
|
||||
function removeModality(m: ModelConfig, md: Modality) {
|
||||
m.modalities = m.modalities.filter(x => x !== md)
|
||||
markManual(m)
|
||||
}
|
||||
|
||||
function removeCapability(m: ModelConfig, cap: Capability) {
|
||||
m.capabilities = m.capabilities.filter(x => x !== cap)
|
||||
markManual(m)
|
||||
}
|
||||
|
||||
function addTag(m: ModelConfig, key: string, kind: 'modality' | 'capability') {
|
||||
if (kind === 'modality') {
|
||||
const md = key as Modality
|
||||
if (!m.modalities.includes(md)) m.modalities = [...m.modalities, md]
|
||||
} else {
|
||||
const cap = key as Capability
|
||||
if (!m.capabilities.includes(cap)) m.capabilities = [...m.capabilities, cap]
|
||||
}
|
||||
markManual(m)
|
||||
}
|
||||
|
||||
function startAddTag(m: ModelConfig) {
|
||||
addingTagModel.value = addingTagModel.value === m.model_id ? null : m.model_id
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 自 ProviderPanel scoped 拆出(C4c 纯搬迁):模型表格/标签样式随区块走;
|
||||
.setting-input--sm 与 .toggle--sm 为本件与 provider-list 子件共用,两处各持一份
|
||||
(scoped 不跨组件,全局化去重留待后续统一处理)。 */
|
||||
|
||||
/* ===== 模型列表(表格形式:表头 + 数据行) =====
|
||||
原 model-list/model-row 等无任何样式定义(裸渲染),本批补齐并改为表格。
|
||||
风格对齐 provider-card(浅底 / 圆角 / 0.5px 边框)。 */
|
||||
.model-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius);
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
}
|
||||
.model-table th,
|
||||
.model-table td {
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.model-table thead th {
|
||||
background: var(--df-bg);
|
||||
font-weight: 500;
|
||||
color: var(--df-text-dim);
|
||||
font-size: 11px;
|
||||
border-bottom: 0.5px solid var(--df-border);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.model-table tbody tr { border-bottom: 0.5px solid var(--df-border); }
|
||||
.model-table tbody tr:last-child { border-bottom: none; }
|
||||
.model-table tbody tr:hover { background: var(--df-accent-bg); }
|
||||
/* 列宽:启用窄、模型/能力弹性、权重窄 */
|
||||
.model-table .col-enabled { width: 56px; text-align: center; }
|
||||
.model-table .col-weight { width: 110px; }
|
||||
.model-table .col-model { min-width: 140px; }
|
||||
.col-enabled .toggle--sm { display: inline-block; }
|
||||
.model-id {
|
||||
font-size: 13px;
|
||||
font-family: var(--df-font-mono);
|
||||
color: var(--df-text);
|
||||
font-weight: 500;
|
||||
word-break: break-all;
|
||||
}
|
||||
/* 标签列:小圆角 chip,三类语义色区分 */
|
||||
.model-tags { display: flex; flex-wrap: wrap; gap: 4px; }
|
||||
.tag {
|
||||
font-size: 10px;
|
||||
padding: 1px 6px;
|
||||
border-radius: var(--df-radius-xs);
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.tag-modality { background: var(--df-info-bg); color: var(--df-info); }
|
||||
.tag-capability { background: var(--df-accent-soft); color: var(--df-accent); }
|
||||
.tag-source { background: var(--df-neutral-bg); color: var(--df-text-dim); }
|
||||
/* 可编辑 tag:×删除按钮(悬停显红)+ 添加按钮 */
|
||||
.tag-remove { margin-left: 3px; cursor: pointer; opacity: 0.6; font-weight: 700; }
|
||||
.tag-remove:hover { opacity: 1; color: var(--df-danger); }
|
||||
.tag-add {
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 1px dashed var(--df-border);
|
||||
color: var(--df-text-dim);
|
||||
font-weight: 700;
|
||||
}
|
||||
.tag-add:hover { border-color: var(--df-accent); color: var(--df-accent); }
|
||||
/* 添加能力内联菜单(点+展开):浅底容器包可选 tag */
|
||||
.add-tag-menu {
|
||||
margin-top: 4px;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
background: var(--df-bg);
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius-sm);
|
||||
}
|
||||
.add-tag-menu .tag { cursor: pointer; }
|
||||
.add-tag-menu .tag:hover { filter: brightness(1.2); }
|
||||
.add-tag-empty { font-size: 10px; color: var(--df-text-dim); padding: 2px 4px; }
|
||||
/* 权重输入框:表格内固定窄宽 */
|
||||
.model-table .setting-input--sm { width: 64px; }
|
||||
|
||||
/* 行内窄输入(权重等):定高 26px 档(--df-h-input),与 .setting-input 主体高度齐平 */
|
||||
.setting-input--sm { flex: 0 0 auto; min-width: 0; width: 70px; height: var(--df-h-input); padding: 4px 8px; font-size: 12px; }
|
||||
|
||||
/* Toggle 小号变体(表格启用开关用;.toggle 基础走全局) */
|
||||
.toggle--sm { width: 32px; height: 18px; }
|
||||
.toggle--sm .toggle-slider::before { width: 12px; height: 12px; left: 3px; bottom: 3px; }
|
||||
.toggle--sm input:checked + .toggle-slider::before { transform: translateX(14px); }
|
||||
</style>
|
||||
@@ -0,0 +1,213 @@
|
||||
<template>
|
||||
<!-- provider 卡片列表。自 ProviderPanel 拆出(C4c 纯搬迁):
|
||||
卡片渲染 + 负载均衡池控件(enabled/weight 即时 IPC)自治在本件,
|
||||
增删改/设默认经 emit 上抛主文件(共享 loadProviders 闭环)。 -->
|
||||
<div class="provider-list">
|
||||
<div class="provider-card" :class="{ 'provider-card--default': p.is_default, 'provider-card--disabled': p.enabled === false }" v-for="p in providers" :key="p.id">
|
||||
<div class="provider-top">
|
||||
<div class="provider-info">
|
||||
<span class="provider-name">{{ p.name }}</span>
|
||||
<span v-if="p.is_default" class="default-badge">{{ $t('settings.badgeDefault') }}</span>
|
||||
<span v-if="p.enabled === false" class="pool-badge">{{ $t('settings.badgePoolDisabled') }}</span>
|
||||
</div>
|
||||
<div class="provider-actions">
|
||||
<button class="btn-link" @click="emit('set-default', p.id)" v-if="!p.is_default">{{ $t('settings.actionSetDefault') }}</button>
|
||||
<button class="btn-link" @click="emit('edit', p)">{{ $t('common.edit') }}</button>
|
||||
<button class="btn-link btn-link-danger" @click="emit('delete', p.id)">{{ $t('common.delete') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="provider-detail">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">{{ $t('settings.detailBaseUrl') }}</span>
|
||||
<span class="detail-value">{{ p.base_url }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">{{ $t('settings.detailModel') }}</span>
|
||||
<span class="detail-value">{{ p.default_model }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">{{ $t('settings.detailApiKey') }}</span>
|
||||
<span class="detail-value mask">{{ maskKey(p.api_key) }}</span>
|
||||
</div>
|
||||
<!-- F-260614-04c 负载均衡池:enabled toggle + weight 即时调 IPC,落库即重建 caps -->
|
||||
<div class="detail-row detail-row--pool">
|
||||
<span class="detail-label" :title="$t('settings.descPoolEnabled')">{{ $t('settings.detailPoolEnabled') }}</span>
|
||||
<div class="detail-value detail-value--control">
|
||||
<label class="toggle toggle--sm" :title="$t('settings.descPoolEnabled')">
|
||||
<input type="checkbox" :checked="p.enabled !== false" @change="onPoolToggle(p, ($event.target as HTMLInputElement).checked)" />
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-row detail-row--pool" v-if="p.enabled !== false">
|
||||
<span class="detail-label" :title="$t('settings.descPoolWeight')">{{ $t('settings.detailPoolWeight') }}</span>
|
||||
<div class="detail-value detail-value--control">
|
||||
<input type="number" class="setting-input setting-input--sm" min="0" max="100"
|
||||
:value="p.weight ?? 50"
|
||||
@change="onPoolWeightChange(p, ($event.target as HTMLInputElement).valueAsNumber)" />
|
||||
<span class="pool-weight-hint">0-100</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { aiApi } from '@/api'
|
||||
import type { AiProviderConfig } from '@/api/types'
|
||||
|
||||
// ============================================================
|
||||
// c: 负载均衡池可编辑层(enabled / weight)——原 ProviderPanel 整块搬迁
|
||||
// 卡片即时调 IPC(对齐 syncConcurrencyConfig 模式);落库后后端 reload_provider_caps
|
||||
// 重建 per_provider caps,下条消息即按新配置 acquire。
|
||||
// 不走 saveProvider(全量 INSERT OR REPLACE 会触发 R-PD-1 密钥迁移分支,与 c 轻量 UPDATE 路径冲突)。
|
||||
// ============================================================
|
||||
const { t } = useI18n()
|
||||
|
||||
defineProps<{
|
||||
/** 主文件 aiProviders 引用;池控件直接改 item 字段(共享响应式对象,乐观更新),列表整体替换仍由主文件驱动 */
|
||||
providers: AiProviderConfig[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'edit', p: AiProviderConfig): void
|
||||
(e: 'delete', id: string): void
|
||||
(e: 'set-default', id: string): void
|
||||
(e: 'toast', msg: string, type: 'error' | 'warning' | 'info' | 'success'): void
|
||||
}>()
|
||||
|
||||
function errMsg(e: unknown): string { return e instanceof Error ? e.message : String(e) }
|
||||
|
||||
// 计时器按 provider id 分键:单例计时器下 300ms 内先后改两家权重,B 的变更会清掉
|
||||
// A 尚未落库的保存,先改的一方静默丢失(走查 P2-8)。
|
||||
const _poolTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
|
||||
/**
|
||||
* toggle 即时生效(开关语义=立即入/出池,无 debounce 必要;但失败需回滚视图)。
|
||||
* 乐观更新本地数组,失败 revert + toast。
|
||||
*/
|
||||
async function onPoolToggle(p: AiProviderConfig, enabled: boolean) {
|
||||
const prev = p.enabled
|
||||
p.enabled = enabled
|
||||
try {
|
||||
// 防御钳制:p.weight 理论上已被 onPoolWeightChange 钳过,此处兜底防脏数据直传 IPC
|
||||
const w = Math.min(100, Math.max(0, p.weight ?? 50))
|
||||
await aiApi.updateProviderPool(p.id, enabled, w)
|
||||
emit('toast', t('settings.toastPoolUpdateOk'), 'info')
|
||||
} catch (e) {
|
||||
p.enabled = prev // revert
|
||||
console.error(t('settings.toastPoolUpdateFail', { msg: errMsg(e) }), e)
|
||||
emit('toast', t('settings.toastPoolUpdateFail', { msg: errMsg(e) }), 'error')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* weight 变更 debounce 300ms(防快速连续输入触发频繁 IPC),clamp [0,100] 对齐后端。
|
||||
* 双 clamp:前端 clamp 视觉输入 + 后端 command 再 clamp(防御性)。
|
||||
*/
|
||||
function onPoolWeightChange(p: AiProviderConfig, raw: number) {
|
||||
// NaN 兜底(清空输入):回退 50,防 IPC 传 NaN 后端 serde u32 失败
|
||||
const clamped = Math.min(100, Math.max(0, Number.isFinite(raw) ? Math.trunc(raw) : 50))
|
||||
const prev = p.weight
|
||||
p.weight = clamped
|
||||
// 仅重置同一 provider 的待发计时;跨 provider 互不干扰
|
||||
const prevTimer = _poolTimers.get(p.id)
|
||||
if (prevTimer) clearTimeout(prevTimer)
|
||||
_poolTimers.set(p.id, setTimeout(async () => {
|
||||
_poolTimers.delete(p.id)
|
||||
try {
|
||||
await aiApi.updateProviderPool(p.id, p.enabled !== false, clamped)
|
||||
emit('toast', t('settings.toastPoolUpdateOk'), 'info')
|
||||
} catch (e) {
|
||||
p.weight = prev // revert
|
||||
console.error(t('settings.toastPoolUpdateFail', { msg: errMsg(e) }), e)
|
||||
emit('toast', t('settings.toastPoolUpdateFail', { msg: errMsg(e) }), 'error')
|
||||
}
|
||||
}, 300))
|
||||
}
|
||||
|
||||
function maskKey(key: string): string {
|
||||
if (!key || key.length < 8) return '••••••••'
|
||||
return key.slice(0, 4) + '••••••••' + key.slice(-4)
|
||||
}
|
||||
|
||||
/** 主文件 defineExpose.clearPoolTimer 委托到此处(壳卸载清 timer 的对外契约不变)。 */
|
||||
defineExpose({
|
||||
clearPoolTimer() { for (const t of _poolTimers.values()) clearTimeout(t); _poolTimers.clear() },
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 自 ProviderPanel scoped 拆出(C4c 纯搬迁):provider 卡片/池控件样式随区块走;
|
||||
.setting-input--sm 与 .toggle--sm 为本件与 model-table 子件共用,两处各持一份
|
||||
(scoped 不跨组件,全局化去重留待后续统一处理)。 */
|
||||
|
||||
/* ===== Provider 卡片 ===== */
|
||||
.provider-list { display: flex; flex-direction: column; gap: 12px; }
|
||||
.provider-card {
|
||||
background: var(--df-bg);
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius);
|
||||
padding: 14px 16px;
|
||||
}
|
||||
.provider-card--default {
|
||||
border-color: var(--df-accent);
|
||||
box-shadow: inset 3px 0 0 var(--df-accent);
|
||||
}
|
||||
.provider-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.provider-info { display: flex; align-items: center; gap: 8px; }
|
||||
.provider-name { font-size: 14px; font-weight: 500; color: var(--df-text); }
|
||||
.default-badge {
|
||||
font-size: 10px;
|
||||
padding: 1px 6px;
|
||||
border-radius: var(--df-radius-xs);
|
||||
background: var(--df-success-bg);
|
||||
color: var(--df-success);
|
||||
font-weight: 500;
|
||||
}
|
||||
.provider-actions { display: flex; gap: 4px; }
|
||||
|
||||
.provider-detail { display: flex; flex-direction: column; gap: 6px; }
|
||||
.detail-row { display: flex; align-items: flex-start; gap: 12px; }
|
||||
.detail-label {
|
||||
font-size: 12px;
|
||||
color: var(--df-text-dim);
|
||||
min-width: 60px;
|
||||
}
|
||||
.detail-value {
|
||||
font-size: 12px;
|
||||
color: var(--df-text-secondary);
|
||||
font-family: var(--df-font-mono);
|
||||
}
|
||||
.detail-value.mask { color: var(--df-text-dim); letter-spacing: 1px; }
|
||||
|
||||
/* Provider 卡片 disabled 态(c:enabled=false 视觉弱化) */
|
||||
.provider-card--disabled { opacity: 0.6; }
|
||||
.pool-badge {
|
||||
font-size: 10px;
|
||||
padding: 1px 6px;
|
||||
border-radius: var(--df-radius-xs);
|
||||
background: var(--df-danger-bg);
|
||||
color: var(--df-danger);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 负载均衡池行(toggle / weight 控件) */
|
||||
.detail-row--pool { align-items: center; }
|
||||
.detail-value--control { display: flex; align-items: center; gap: 8px; font-family: var(--df-font-mono); }
|
||||
.pool-weight-hint { font-size: 11px; color: var(--df-text-dim); }
|
||||
|
||||
/* 行内窄输入(权重等):定高 26px 档(--df-h-input),与 .setting-input 主体高度齐平 */
|
||||
.setting-input--sm { flex: 0 0 auto; min-width: 0; width: 70px; height: var(--df-h-input); padding: 4px 8px; font-size: 12px; }
|
||||
|
||||
/* Toggle 小号变体(本面板负载均衡池用;.toggle 基础走全局) */
|
||||
.toggle--sm { width: 32px; height: 18px; }
|
||||
.toggle--sm .toggle-slider::before { width: 12px; height: 12px; left: 3px; bottom: 3px; }
|
||||
.toggle--sm input:checked + .toggle-slider::before { transform: translateX(14px); }
|
||||
</style>
|
||||
@@ -120,15 +120,8 @@
|
||||
.toggle input:checked + .toggle-slider { background: var(--df-accent); }
|
||||
.toggle input:checked + .toggle-slider::before { transform: translateX(18px); background: #fff; }
|
||||
|
||||
/* ===== 按钮(原 Settings.vue,列表型 Panel 共用) ===== */
|
||||
.btn { padding: 8px 16px; border: none; border-radius: var(--df-radius-sm); font-size: 13px; cursor: pointer; transition: background 0.15s, color 0.15s; }
|
||||
.btn-primary { background: var(--df-accent); color: #fff; }
|
||||
.btn-primary:hover { background: var(--df-accent-hover); }
|
||||
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.btn-ghost { background: transparent; color: var(--df-text-secondary); border: 0.5px solid var(--df-border); }
|
||||
.btn-ghost:hover { background: var(--df-bg-card); color: var(--df-text); }
|
||||
.btn-ghost:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.btn-sm { padding: 4px 12px; font-size: 12px; }
|
||||
/* ===== 按钮 =====
|
||||
样式大重构 R0(2026-10-18):.btn 族副本已删除,并轨 global.css 唯一基类(高度档 --df-h-btn)。 */
|
||||
|
||||
/* ===== 空提示(列表型 Panel 共用) ===== */
|
||||
.empty-hint { color: var(--df-text-dim); font-size: 13px; padding: 16px 0; text-align: center; }
|
||||
|
||||
Reference in New Issue
Block a user