feat(web): 主题皮肤系统与 LLM 设置面板重构
- useThemeSkin 重构主题皮肤应用逻辑,支持企业 AI 风格主题切换 - settingsModelHelper 新增主题与模型表字段映射,useSettings 适配 - LlmSettingsPanel/SettingsView 面板重构,支持多模型行编辑与主题区块 - settings-view.css 适配主题样式,新增 settings-theme-section 测试,更新 settings-llm-section 测试
This commit is contained in:
@@ -1,316 +1,178 @@
|
||||
<template>
|
||||
<div class="model-grid">
|
||||
<!-- 主模型配置 -->
|
||||
<section class="settings-card">
|
||||
<div class="card-head">
|
||||
<div class="model-config-surface">
|
||||
<section class="settings-card model-table-card">
|
||||
<div class="card-head model-table-toolbar">
|
||||
<div class="card-title-with-icon">
|
||||
<div class="model-icon-box purple">
|
||||
<i class="mdi mdi-brain"></i>
|
||||
<span class="model-icon-text">AI</span>
|
||||
</div>
|
||||
<div>
|
||||
<h4>主模型配置</h4>
|
||||
<p>用于 AI 助手和主业务排队调度的默认模型接入。</p>
|
||||
<h4>模型配置</h4>
|
||||
<p>集中维护大语言模型、Embedding 和 Rerank 模型接入。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-head-actions">
|
||||
<button
|
||||
class="test-button"
|
||||
type="button"
|
||||
:disabled="isModelTesting('main')"
|
||||
@click="testModelConnection('main')"
|
||||
>
|
||||
<i :class="isModelTesting('main') ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-connection'"></i>
|
||||
<span>{{ isModelTesting('main') ? '测试中...' : '测试模型' }}</span>
|
||||
<button class="add-model-button" type="button" @click="openAddModelDialog">
|
||||
<i class="mdi mdi-plus"></i>
|
||||
<span>添加模型</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-grid">
|
||||
<label class="field">
|
||||
<span><em>*</em> 供应商</span>
|
||||
<EnterpriseSelect
|
||||
v-model="llmForm.mainProvider"
|
||||
:options="providerOptions"
|
||||
placeholder="选择供应商"
|
||||
@change="applyProviderPreset('main')"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span><em>*</em> 模型名称</span>
|
||||
<input v-model="llmForm.mainModel" type="text" placeholder="请输入主模型名称" />
|
||||
</label>
|
||||
|
||||
<label class="field field-full">
|
||||
<span><em>*</em> 接口地址</span>
|
||||
<input v-model="llmForm.mainEndpoint" type="text" placeholder="请输入模型接口地址" />
|
||||
</label>
|
||||
|
||||
<label class="field field-full">
|
||||
<span>API Key</span>
|
||||
<input
|
||||
v-model="llmForm.mainApiKey"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
@focus="clearModelSecretMask('main')"
|
||||
:placeholder="llmForm.mainApiKeyConfigured ? '已配置,如需修改请重新输入' : '保存后不会保留在草稿中'"
|
||||
/>
|
||||
<small v-if="llmForm.mainApiKeyConfigured" class="secret-bound-state">
|
||||
<i class="mdi mdi-database-lock"></i>
|
||||
<span>已从数据库加密加载,测试会使用已保存密钥。</span>
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="getModelTestState('main').message" class="test-feedback" :class="`is-${getModelTestState('main').status}`">
|
||||
<i
|
||||
:class="
|
||||
getModelTestState('main').status === 'success'
|
||||
? 'mdi mdi-check-circle'
|
||||
: getModelTestState('main').status === 'testing'
|
||||
? 'mdi mdi-loading mdi-spin'
|
||||
: 'mdi mdi-alert-circle'
|
||||
"
|
||||
></i>
|
||||
<span>{{ getModelTestState('main').message }}</span>
|
||||
<div class="model-table-wrap">
|
||||
<table class="model-config-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>模型类型</th>
|
||||
<th>供应商</th>
|
||||
<th>model_id</th>
|
||||
<th>接口地址</th>
|
||||
<th>API Key</th>
|
||||
<th>连通性</th>
|
||||
<th class="model-action-col">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="model in modelRows" :key="model.slot">
|
||||
<td>
|
||||
<span class="model-type-pill">
|
||||
<span>{{ getModelTypeLabel(model.type) }}</span>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<strong class="model-provider-name">{{ model.provider }}</strong>
|
||||
</td>
|
||||
<td>
|
||||
<code class="model-id-text">{{ model.modelId }}</code>
|
||||
</td>
|
||||
<td>
|
||||
<span class="model-url-text" :title="model.url">{{ model.url }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="secret-state" :class="{ configured: model.apiKeyConfigured }">
|
||||
<i :class="model.apiKeyConfigured ? 'mdi mdi-database-lock' : 'mdi mdi-key-outline'"></i>
|
||||
<span>{{ model.apiKeyConfigured ? '已配置' : '未配置' }}</span>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
v-if="getModelTestState(model.slot).message"
|
||||
class="test-feedback-inline"
|
||||
:class="`is-${getModelTestState(model.slot).status}`"
|
||||
>
|
||||
<i
|
||||
:class="
|
||||
getModelTestState(model.slot).status === 'success'
|
||||
? 'mdi mdi-check-circle'
|
||||
: getModelTestState(model.slot).status === 'testing'
|
||||
? 'mdi mdi-loading mdi-spin'
|
||||
: 'mdi mdi-alert-circle'
|
||||
"
|
||||
></i>
|
||||
<span>{{ getModelTestState(model.slot).message }}</span>
|
||||
</span>
|
||||
<span v-else class="test-feedback-inline is-idle">待测试</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="model-row-actions">
|
||||
<button
|
||||
class="icon-action-button"
|
||||
type="button"
|
||||
:disabled="isModelTesting(model.slot)"
|
||||
title="测试模型"
|
||||
@click="testModelConnection(model)"
|
||||
>
|
||||
<span>{{ isModelTesting(model.slot) ? '测试中' : '测试' }}</span>
|
||||
</button>
|
||||
<button class="icon-action-button" type="button" title="编辑模型" @click="openEditModelDialog(model)">
|
||||
<span>编辑</span>
|
||||
</button>
|
||||
<button
|
||||
class="icon-action-button danger"
|
||||
type="button"
|
||||
title="删除模型"
|
||||
:disabled="isFixedModelSlot(model.slot)"
|
||||
@click="removeModel(model)"
|
||||
>
|
||||
<span>删除</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 备份模型配置 -->
|
||||
<section class="settings-card">
|
||||
<div class="card-head">
|
||||
<div class="card-title-with-icon">
|
||||
<div class="model-icon-box orange">
|
||||
<i class="mdi mdi-lifebuoy"></i>
|
||||
</div>
|
||||
<div v-if="modelDialogOpen" class="model-dialog-overlay" @click.self="closeModelDialog">
|
||||
<section class="model-dialog" role="dialog" aria-modal="true" aria-labelledby="model-dialog-title">
|
||||
<header class="model-dialog-head">
|
||||
<div>
|
||||
<h4>备份模型配置</h4>
|
||||
<p>主模型不可用或限频时用于兜底切换的备用模型接入。</p>
|
||||
<h4 id="model-dialog-title">{{ modelDraft.slot ? '编辑模型' : '添加模型' }}</h4>
|
||||
<p>配置供应商、URL、密钥、model_id 和模型类型。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-head-actions">
|
||||
<button
|
||||
class="test-button"
|
||||
type="button"
|
||||
:disabled="isModelTesting('backup')"
|
||||
@click="testModelConnection('backup')"
|
||||
>
|
||||
<i :class="isModelTesting('backup') ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-connection'"></i>
|
||||
<span>{{ isModelTesting('backup') ? '测试中...' : '测试模型' }}</span>
|
||||
<button class="icon-action-button" type="button" title="关闭" @click="closeModelDialog">
|
||||
<span>关闭</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="form-grid">
|
||||
<label class="field">
|
||||
<span><em>*</em> 供应商</span>
|
||||
<EnterpriseSelect
|
||||
v-model="llmForm.backupProvider"
|
||||
:options="providerOptions"
|
||||
placeholder="选择供应商"
|
||||
@change="applyProviderPreset('backup')"
|
||||
/>
|
||||
</label>
|
||||
<div class="form-grid model-dialog-form">
|
||||
<label class="field">
|
||||
<span><em>*</em> 供应商</span>
|
||||
<EnterpriseSelect
|
||||
v-model="modelDraft.provider"
|
||||
:options="providerOptions"
|
||||
placeholder="选择供应商"
|
||||
@change="applyProviderPresetToDraft"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span><em>*</em> 模型名称</span>
|
||||
<input v-model="llmForm.backupModel" type="text" placeholder="请输入备份模型名称" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span><em>*</em> 接口地址</span>
|
||||
<input v-model="modelDraft.url" type="text" placeholder="https://api.example.com/v1" />
|
||||
</label>
|
||||
|
||||
<label class="field field-full">
|
||||
<span><em>*</em> 接口地址</span>
|
||||
<input v-model="llmForm.backupEndpoint" type="text" placeholder="请输入模型接口地址" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>API Key</span>
|
||||
<input
|
||||
v-model="modelDraft.apiKey"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
:placeholder="modelDraft.apiKeyConfigured ? '已配置,如需修改请重新输入' : '保存后加密存储'"
|
||||
@focus="clearDraftSecretMask"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="field field-full">
|
||||
<span>API Key</span>
|
||||
<input
|
||||
v-model="llmForm.backupApiKey"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
@focus="clearModelSecretMask('backup')"
|
||||
:placeholder="llmForm.backupApiKeyConfigured ? '已配置,如需修改请重新输入' : '保存后不会保留在草稿中'"
|
||||
/>
|
||||
<small v-if="llmForm.backupApiKeyConfigured" class="secret-bound-state">
|
||||
<i class="mdi mdi-database-lock"></i>
|
||||
<span>已从数据库加密加载,测试会使用已保存密钥。</span>
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
<label class="field">
|
||||
<span><em>*</em> model_id</span>
|
||||
<input v-model="modelDraft.modelId" type="text" placeholder="例如 gpt-5.4-mini" />
|
||||
</label>
|
||||
|
||||
<div v-if="getModelTestState('backup').message" class="test-feedback" :class="`is-${getModelTestState('backup').status}`">
|
||||
<i
|
||||
:class="
|
||||
getModelTestState('backup').status === 'success'
|
||||
? 'mdi mdi-check-circle'
|
||||
: getModelTestState('backup').status === 'testing'
|
||||
? 'mdi mdi-loading mdi-spin'
|
||||
: 'mdi mdi-alert-circle'
|
||||
"
|
||||
></i>
|
||||
<span>{{ getModelTestState('backup').message }}</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Embedding 模型配置 -->
|
||||
<section class="settings-card">
|
||||
<div class="card-head">
|
||||
<div class="card-title-with-icon">
|
||||
<div class="model-icon-box cyan">
|
||||
<i class="mdi mdi-vector-combine"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Embedding 模型配置</h4>
|
||||
<p>用于向量检索、知识库召回和语义匹配的嵌入模型设置。</p>
|
||||
<div class="field field-full">
|
||||
<span><em>*</em> 模型类型</span>
|
||||
<div class="model-type-segment" :class="{ disabled: isEditingFixedModel }">
|
||||
<button
|
||||
v-for="option in MODEL_TYPE_OPTIONS"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
:class="{ active: modelDraft.type === option.value }"
|
||||
:disabled="isEditingFixedModel"
|
||||
@click="selectDraftModelType(option.value)"
|
||||
>
|
||||
<span>{{ option.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-head-actions">
|
||||
<button
|
||||
class="test-button"
|
||||
type="button"
|
||||
:disabled="isModelTesting('embedding')"
|
||||
@click="testModelConnection('embedding')"
|
||||
>
|
||||
<i :class="isModelTesting('embedding') ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-connection'"></i>
|
||||
<span>{{ isModelTesting('embedding') ? '测试中...' : '测试模型' }}</span>
|
||||
|
||||
<footer class="model-dialog-actions">
|
||||
<button class="secondary-button" type="button" @click="closeModelDialog">取消</button>
|
||||
<button class="save-button compact" type="button" @click="saveModelDialog">
|
||||
<span>保存模型</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-grid">
|
||||
<label class="field">
|
||||
<span><em>*</em> 供应商</span>
|
||||
<EnterpriseSelect
|
||||
v-model="llmForm.embeddingProvider"
|
||||
:options="providerOptions"
|
||||
placeholder="选择供应商"
|
||||
@change="applyProviderPreset('embedding')"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span><em>*</em> 模型名称</span>
|
||||
<input v-model="llmForm.embeddingModel" type="text" placeholder="请输入 Embedding 模型名称" />
|
||||
</label>
|
||||
|
||||
<label class="field field-full">
|
||||
<span><em>*</em> 接口地址</span>
|
||||
<input v-model="llmForm.embeddingEndpoint" type="text" placeholder="请输入模型接口地址" />
|
||||
</label>
|
||||
|
||||
<label class="field field-full">
|
||||
<span>API Key</span>
|
||||
<input
|
||||
v-model="llmForm.embeddingApiKey"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
@focus="clearModelSecretMask('embedding')"
|
||||
:placeholder="llmForm.embeddingApiKeyConfigured ? '已配置,如需修改请重新输入' : '保存后不会保留在草稿中'"
|
||||
/>
|
||||
<small v-if="llmForm.embeddingApiKeyConfigured" class="secret-bound-state">
|
||||
<i class="mdi mdi-database-lock"></i>
|
||||
<span>已从数据库加密加载,测试会使用已保存密钥。</span>
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="getModelTestState('embedding').message"
|
||||
class="test-feedback"
|
||||
:class="`is-${getModelTestState('embedding').status}`"
|
||||
>
|
||||
<i
|
||||
:class="
|
||||
getModelTestState('embedding').status === 'success'
|
||||
? 'mdi mdi-check-circle'
|
||||
: getModelTestState('embedding').status === 'testing'
|
||||
? 'mdi mdi-loading mdi-spin'
|
||||
: 'mdi mdi-alert-circle'
|
||||
"
|
||||
></i>
|
||||
<span>{{ getModelTestState('embedding').message }}</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Reranker 模型配置 -->
|
||||
<section class="settings-card">
|
||||
<div class="card-head">
|
||||
<div class="card-title-with-icon">
|
||||
<div class="model-icon-box teal">
|
||||
<i class="mdi mdi-filter-variant"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Reranker 模型配置</h4>
|
||||
<p>用于检索结果重排和语义精排的 Reranker 模型设置。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-head-actions">
|
||||
<button
|
||||
class="test-button"
|
||||
type="button"
|
||||
:disabled="isModelTesting('reranker')"
|
||||
@click="testModelConnection('reranker')"
|
||||
>
|
||||
<i :class="isModelTesting('reranker') ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-connection'"></i>
|
||||
<span>{{ isModelTesting('reranker') ? '测试中...' : '测试模型' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-grid">
|
||||
<label class="field">
|
||||
<span><em>*</em> 供应商</span>
|
||||
<EnterpriseSelect
|
||||
v-model="llmForm.rerankerProvider"
|
||||
:options="providerOptions"
|
||||
placeholder="选择供应商"
|
||||
@change="applyProviderPreset('reranker')"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span><em>*</em> 模型名称</span>
|
||||
<input v-model="llmForm.rerankerModel" type="text" placeholder="请输入 Reranker 模型名称" />
|
||||
</label>
|
||||
|
||||
<label class="field field-full">
|
||||
<span><em>*</em> 接口地址</span>
|
||||
<input v-model="llmForm.rerankerEndpoint" type="text" placeholder="请输入模型接口地址" />
|
||||
</label>
|
||||
|
||||
<label class="field field-full">
|
||||
<span>API Key</span>
|
||||
<input
|
||||
v-model="llmForm.rerankerApiKey"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
@focus="clearModelSecretMask('reranker')"
|
||||
:placeholder="llmForm.rerankerApiKeyConfigured ? '已配置,如需修改请重新输入' : '保存后不会保留在草稿中'"
|
||||
/>
|
||||
<small v-if="llmForm.rerankerApiKeyConfigured" class="secret-bound-state">
|
||||
<i class="mdi mdi-database-lock"></i>
|
||||
<span>已从数据库加密加载,测试会使用已保存密钥。</span>
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="getModelTestState('reranker').message"
|
||||
class="test-feedback"
|
||||
:class="`is-${getModelTestState('reranker').status}`"
|
||||
>
|
||||
<i
|
||||
:class="
|
||||
getModelTestState('reranker').status === 'success'
|
||||
? 'mdi mdi-check-circle'
|
||||
: getModelTestState('reranker').status === 'testing'
|
||||
? 'mdi mdi-loading mdi-spin'
|
||||
: 'mdi mdi-alert-circle'
|
||||
"
|
||||
></i>
|
||||
<span>{{ getModelTestState('reranker').message }}</span>
|
||||
</div>
|
||||
</section>
|
||||
</footer>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user