优化: 设置域体验完善
- 校验失败红框定位字段,保存反馈改持久化成功后 - 数值越界钳制,双提示队列化不再重叠 - 搜索防抖跳分类,托盘气泡文案进多语言
This commit is contained in:
@@ -67,7 +67,10 @@
|
||||
<div class="form-grid">
|
||||
<div class="form-field">
|
||||
<label class="form-label">{{ $t('settings.labelName') }}</label>
|
||||
<input v-model="providerForm.name" class="setting-input" :placeholder="$t('settings.phProviderName')" />
|
||||
<!-- 走查 P3-10:校验失败定位字段——红框 + @input 清错(示范级,仅本表单) -->
|
||||
<input ref="inputName" v-model="providerForm.name" class="setting-input"
|
||||
:class="{ 'setting-input--invalid': fieldErrors.name }"
|
||||
:placeholder="$t('settings.phProviderName')" @input="fieldErrors.name = false" />
|
||||
</div>
|
||||
<!-- 项 D:厂商预设下拉 — 选中自动填充 providerType/baseUrl/defaultModel(用户可改覆盖)。
|
||||
新建态独立"快捷选择";编辑态回填后切预设仍可覆盖(等价快捷填充),不阻塞手改。
|
||||
@@ -88,12 +91,16 @@
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="form-label">{{ $t('settings.labelBaseUrl') }}</label>
|
||||
<input v-model="providerForm.baseUrl" class="setting-input" :placeholder="$t('settings.phBaseUrl')" />
|
||||
<input ref="inputBaseUrl" v-model="providerForm.baseUrl" class="setting-input"
|
||||
:class="{ 'setting-input--invalid': fieldErrors.baseUrl }"
|
||||
:placeholder="$t('settings.phBaseUrl')" @input="fieldErrors.baseUrl = false" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="form-label">{{ $t('settings.labelApiKey') }}</label>
|
||||
<!-- 走查 P3-9:type 恒 password 无法核对已粘贴密钥,加显隐切换 -->
|
||||
<input v-model="providerForm.apiKey" class="setting-input" :type="showApiKey ? 'text' : 'password'" :placeholder="$t('settings.phApiKey')" />
|
||||
<input ref="inputApiKey" v-model="providerForm.apiKey" class="setting-input" :type="showApiKey ? 'text' : 'password'"
|
||||
:class="{ 'setting-input--invalid': fieldErrors.apiKey }"
|
||||
:placeholder="$t('settings.phApiKey')" @input="fieldErrors.apiKey = false" />
|
||||
<button type="button" class="btn-link" :title="$t(showApiKey ? 'settings.apiKeyHide' : 'settings.apiKeyShow')"
|
||||
@click="showApiKey = !showApiKey">
|
||||
{{ $t(showApiKey ? 'settings.apiKeyHide' : 'settings.apiKeyShow') }}
|
||||
@@ -104,7 +111,9 @@
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="form-label">{{ $t('settings.labelDefaultModel') }}</label>
|
||||
<input v-model="providerForm.defaultModel" class="setting-input" :placeholder="$t('settings.phDefaultModel')" />
|
||||
<input ref="inputDefaultModel" v-model="providerForm.defaultModel" class="setting-input"
|
||||
:class="{ 'setting-input--invalid': fieldErrors.defaultModel }"
|
||||
:placeholder="$t('settings.phDefaultModel')" @input="fieldErrors.defaultModel = false" />
|
||||
<!-- 方向 A(轻量):defaultModel 仅作"建议起点"——预设 model id 易过时(DeepSeek chat 停用 /
|
||||
Gemini 2.5→3.5 等),提示用户点「测试连接」拉真实模型列表覆盖,降低踩过时默认值的困惑。 -->
|
||||
<div class="form-field-hint">{{ $t('settings.defaultModelHint') }}</div>
|
||||
@@ -112,6 +121,7 @@
|
||||
<!-- 供应商级采样温度覆盖:部分受限模型(如 Kimi 系)仅接受 temperature=1,留空走默认 0.7 -->
|
||||
<div class="form-field">
|
||||
<label class="form-label">{{ $t('settings.labelSamplingTemp') }}</label>
|
||||
<!-- 走查 P3-14:原生 min/max 只约束步进按钮,手输越界由 saveProvider clamp + toast 提示 -->
|
||||
<input type="number" v-model.number="providerForm.temperature" class="setting-input"
|
||||
:placeholder="$t('settings.phSamplingTemp')" step="0.1" min="0" max="2" />
|
||||
</div>
|
||||
@@ -192,7 +202,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, computed, onMounted } from 'vue'
|
||||
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'
|
||||
@@ -228,6 +238,25 @@ const aiProviders = ref<AiProviderConfig[]>([])
|
||||
// 密钥显隐切换(走查 P3-9):默认隐藏;重开表单时复位,防上一家 provider 的显钥状态延续
|
||||
const showApiKey = ref(false)
|
||||
|
||||
// 走查 P3-10:校验失败只 toast 不定位字段——补字段级红框 + 滚动聚焦首个缺失字段。
|
||||
// 仅保存必填四项(name/baseUrl/apiKey/defaultModel)做示范级实现;红框在对应输入框
|
||||
// @input 时即清(用户开始修正就不该继续红着)。
|
||||
const fieldErrors = reactive({ name: false, baseUrl: false, apiKey: false, defaultModel: false })
|
||||
const inputName = useTemplateRef<HTMLInputElement>('inputName')
|
||||
const inputBaseUrl = useTemplateRef<HTMLInputElement>('inputBaseUrl')
|
||||
const inputApiKey = useTemplateRef<HTMLInputElement>('inputApiKey')
|
||||
const inputDefaultModel = useTemplateRef<HTMLInputElement>('inputDefaultModel')
|
||||
|
||||
/** 聚焦首个红框字段(focus 默认行为即滚动到该元素,无需额外 scrollIntoView) */
|
||||
function focusFirstError() {
|
||||
const el = fieldErrors.name ? inputName.value
|
||||
: fieldErrors.baseUrl ? inputBaseUrl.value
|
||||
: fieldErrors.apiKey ? inputApiKey.value
|
||||
: fieldErrors.defaultModel ? inputDefaultModel.value
|
||||
: null
|
||||
el?.focus()
|
||||
}
|
||||
|
||||
function errMsg(e: unknown): string { return e instanceof Error ? e.message : String(e) }
|
||||
|
||||
const providerForm = reactive({
|
||||
@@ -433,6 +462,7 @@ function openProviderForm(p?: AiProviderConfig) {
|
||||
}
|
||||
providerForm.fetching = false
|
||||
providerForm.saveError = null // 重开表单清旧错误横幅
|
||||
fieldErrors.name = fieldErrors.baseUrl = fieldErrors.apiKey = fieldErrors.defaultModel = false // 重开表单清字段红框
|
||||
showApiKey.value = false // 密钥默认隐藏,不延续上次表单的显钥状态(防窥)
|
||||
providerForm.visible = true
|
||||
// L19/M32: 记初始快照(在 visible=true、字段回填完成后),isFormDirty 据此比对。
|
||||
@@ -547,10 +577,28 @@ function classifySaveError(raw: string): { title: string; detail: string } {
|
||||
async function saveProvider() {
|
||||
// apiKey:新建必填;编辑可留空(不改,FR-S1 后端保留原 DB 值)
|
||||
const needKey = !providerForm.editId
|
||||
if (!providerForm.name || !providerForm.baseUrl || (needKey && !providerForm.apiKey) || !providerForm.defaultModel) {
|
||||
// 走查 P3-10:逐字段判定缺失,红框标出并滚动聚焦首个缺失字段(toast 保留,双通道反馈)
|
||||
fieldErrors.name = !providerForm.name
|
||||
fieldErrors.baseUrl = !providerForm.baseUrl
|
||||
fieldErrors.apiKey = needKey && !providerForm.apiKey
|
||||
fieldErrors.defaultModel = !providerForm.defaultModel
|
||||
if (fieldErrors.name || fieldErrors.baseUrl || fieldErrors.apiKey || fieldErrors.defaultModel) {
|
||||
emit('toast', t('settings.toastSaveIncomplete'), 'warning')
|
||||
focusFirstError()
|
||||
return
|
||||
}
|
||||
// 走查 P3-14:采样温度钳制 [0,2](原生 min/max 只约束步进按钮,手输 99 照存)。
|
||||
// 越界有限值 → clamp + toast 告知修正值;NaN/清空残留 → 归一 null(走默认 0.7,同步清显示)
|
||||
let temperatureOverride: number | undefined
|
||||
if (typeof providerForm.temperature === 'number' && Number.isFinite(providerForm.temperature)) {
|
||||
if (providerForm.temperature < 0 || providerForm.temperature > 2) {
|
||||
providerForm.temperature = Math.min(2, Math.max(0, providerForm.temperature))
|
||||
emit('toast', t('settings.toastTempClamped', { v: providerForm.temperature }), 'warning')
|
||||
}
|
||||
temperatureOverride = providerForm.temperature
|
||||
} else {
|
||||
providerForm.temperature = null
|
||||
}
|
||||
providerForm.saving = true
|
||||
try {
|
||||
await aiApi.saveProvider({
|
||||
@@ -561,11 +609,7 @@ async function saveProvider() {
|
||||
apiKey: providerForm.apiKey,
|
||||
defaultModel: providerForm.defaultModel,
|
||||
modelConfigs: providerForm.models,
|
||||
// 采样温度非空且为有效数字才带(清空输入 v-model.number 可能残留 ''/NaN,不过滤会污染后端 config)
|
||||
temperatureOverride:
|
||||
typeof providerForm.temperature === 'number' && Number.isFinite(providerForm.temperature)
|
||||
? providerForm.temperature
|
||||
: undefined,
|
||||
temperatureOverride,
|
||||
})
|
||||
// 成功明确反馈——清错误横幅 + 关表单 + 绿色「已保存」toast
|
||||
providerForm.saveError = null
|
||||
@@ -788,6 +832,9 @@ onMounted(loadProviders)
|
||||
/* 输入/下拉在 form-field 内 flex 填满(基础样式走全局 settings.css .setting-input/.setting-select) */
|
||||
.form-field .setting-input,
|
||||
.form-field .setting-select { flex: 1; min-width: 0; }
|
||||
/* 走查 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 基础走全局) */
|
||||
|
||||
Reference in New Issue
Block a user