refactor(settings): 将设置页面重构为模态框形式并优化样式
重构设置功能为模态框形式,移除原设置页面路由及相关导航链接 新增 SettingsModal 组件集中管理设置功能,优化移动端适配 调整 ModelProvidersComponent 样式,移除冗余按钮和样式 统一用户管理组件样式,优化颜色和布局
This commit is contained in:
parent
69cd29803c
commit
9ca01763ec
@ -343,9 +343,12 @@ const onGoingConvMessages = computed(() => {
|
|||||||
: [];
|
: [];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const historyConversations = computed(() => {
|
||||||
|
return MessageProcessor.convertServerHistoryToMessages(currentThreadMessages.value);
|
||||||
|
});
|
||||||
|
|
||||||
const conversations = computed(() => {
|
const conversations = computed(() => {
|
||||||
const historyConvs = MessageProcessor.convertServerHistoryToMessages(currentThreadMessages.value);
|
const historyConvs = historyConversations.value;
|
||||||
const threadState = currentThreadState.value;
|
|
||||||
|
|
||||||
// 如果有进行中的消息且线程状态显示正在流式处理,添加进行中的对话
|
// 如果有进行中的消息且线程状态显示正在流式处理,添加进行中的对话
|
||||||
if (onGoingConvMessages.value.length > 0) {
|
if (onGoingConvMessages.value.length > 0) {
|
||||||
@ -369,7 +372,6 @@ const isMediumContainer = computed(() => localUIState.containerWidth <= 768);
|
|||||||
|
|
||||||
// ==================== SCROLL & RESIZE HANDLING ====================
|
// ==================== SCROLL & RESIZE HANDLING ====================
|
||||||
const chatContainerRef = ref(null);
|
const chatContainerRef = ref(null);
|
||||||
const messageInputRef = ref(null);
|
|
||||||
const scrollController = new ScrollController('.chat');
|
const scrollController = new ScrollController('.chat');
|
||||||
let resizeObserver = null;
|
let resizeObserver = null;
|
||||||
|
|
||||||
|
|||||||
349
web/src/components/BasicSettingsSection.vue
Normal file
349
web/src/components/BasicSettingsSection.vue
Normal file
@ -0,0 +1,349 @@
|
|||||||
|
<template>
|
||||||
|
<div class="basic-settings-section">
|
||||||
|
<h3 class="section-title">检索配置</h3>
|
||||||
|
<div class="section">
|
||||||
|
<div class="card card-select">
|
||||||
|
<span class="label">{{ items?.default_model?.des || '默认对话模型' }}</span>
|
||||||
|
<ModelSelectorComponent
|
||||||
|
@select-model="handleChatModelSelect"
|
||||||
|
:model_spec="configStore.config?.default_model"
|
||||||
|
placeholder="请选择默认模型"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="card card-select">
|
||||||
|
<span class="label">{{ items?.fast_model.des }}</span>
|
||||||
|
<ModelSelectorComponent
|
||||||
|
@select-model="handleFastModelSelect"
|
||||||
|
:model_spec="configStore.config?.fast_model"
|
||||||
|
placeholder="请选择模型"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="card card-select">
|
||||||
|
<span class="label">{{ items?.embed_model.des }}</span>
|
||||||
|
<EmbeddingModelSelector
|
||||||
|
:value="configStore.config?.embed_model"
|
||||||
|
@change="handleChange('embed_model', $event)"
|
||||||
|
style="width: 320px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="card card-select">
|
||||||
|
<span class="label">{{ items?.reranker.des }}</span>
|
||||||
|
<a-select style="width: 320px"
|
||||||
|
:value="configStore.config?.reranker"
|
||||||
|
@change="handleChange('reranker', $event)"
|
||||||
|
>
|
||||||
|
<a-select-option
|
||||||
|
v-for="(name, idx) in rerankerChoices" :key="idx"
|
||||||
|
:value="name">{{ name }}
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="section-title">内容审查配置</h3>
|
||||||
|
<div class="section">
|
||||||
|
<div class="card">
|
||||||
|
<span class="label">{{ items?.enable_content_guard.des }}</span>
|
||||||
|
<a-switch
|
||||||
|
:checked="configStore.config?.enable_content_guard"
|
||||||
|
@change="handleChange('enable_content_guard', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="card" v-if="configStore.config?.enable_content_guard">
|
||||||
|
<span class="label">{{ items?.enable_content_guard_llm.des }}</span>
|
||||||
|
<a-switch
|
||||||
|
:checked="configStore.config?.enable_content_guard_llm"
|
||||||
|
@change="handleChange('enable_content_guard_llm', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="card card-select" v-if="configStore.config?.enable_content_guard && configStore.config?.enable_content_guard_llm">
|
||||||
|
<span class="label">{{ items?.content_guard_llm_model.des }}</span>
|
||||||
|
<ModelSelectorComponent
|
||||||
|
@select-model="handleContentGuardModelSelect"
|
||||||
|
:model_spec="configStore.config?.content_guard_llm_model"
|
||||||
|
placeholder="请选择模型"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 服务链接部分 -->
|
||||||
|
<div v-if="userStore.isAdmin">
|
||||||
|
<h3 class="section-title">服务链接</h3>
|
||||||
|
<p class="service-description">快速访问系统相关的外部服务,需要将 localhost 替换为实际的 IP 地址。</p>
|
||||||
|
<div class="services-grid">
|
||||||
|
<div class="service-link-card">
|
||||||
|
<div class="service-info">
|
||||||
|
<h4>Neo4j 浏览器</h4>
|
||||||
|
<p>图数据库管理界面</p>
|
||||||
|
</div>
|
||||||
|
<a-button type="default" @click="openLink('http://localhost:7474/')" :icon="h(GlobalOutlined)">
|
||||||
|
访问
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="service-link-card">
|
||||||
|
<div class="service-info">
|
||||||
|
<h4>API 接口文档</h4>
|
||||||
|
<p>系统接口文档和调试工具</p>
|
||||||
|
</div>
|
||||||
|
<a-button type="default" @click="openLink('http://localhost:5050/docs')" :icon="h(GlobalOutlined)">
|
||||||
|
访问
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="service-link-card">
|
||||||
|
<div class="service-info">
|
||||||
|
<h4>MinIO 对象存储</h4>
|
||||||
|
<p>文件存储管理控制台</p>
|
||||||
|
</div>
|
||||||
|
<a-button type="default" @click="openLink('http://localhost:9001')" :icon="h(GlobalOutlined)">
|
||||||
|
访问
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="service-link-card">
|
||||||
|
<div class="service-info">
|
||||||
|
<h4>Milvus WebUI</h4>
|
||||||
|
<p>向量数据库管理界面</p>
|
||||||
|
</div>
|
||||||
|
<a-button type="default" @click="openLink('http://localhost:9091/webui/')" :icon="h(GlobalOutlined)">
|
||||||
|
访问
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, h } from 'vue'
|
||||||
|
import { useConfigStore } from '@/stores/config'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import { GlobalOutlined } from '@ant-design/icons-vue'
|
||||||
|
import ModelSelectorComponent from '@/components/ModelSelectorComponent.vue'
|
||||||
|
import EmbeddingModelSelector from '@/components/EmbeddingModelSelector.vue'
|
||||||
|
|
||||||
|
const configStore = useConfigStore()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const items = computed(() => configStore.config._config_items)
|
||||||
|
|
||||||
|
const rerankerChoices = computed(() => {
|
||||||
|
return Object.keys(configStore?.config?.reranker_names || {}) || []
|
||||||
|
})
|
||||||
|
|
||||||
|
const preHandleChange = (key, e) => {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChange = (key, e) => {
|
||||||
|
if (!preHandleChange(key, e)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
configStore.setConfigValue(key, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChanges = (items) => {
|
||||||
|
for (const key in items) {
|
||||||
|
if (!preHandleChange(key, items[key])) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
configStore.setConfigValues(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChatModelSelect = (spec) => {
|
||||||
|
if (typeof spec === 'string' && spec) {
|
||||||
|
configStore.setConfigValue('default_model', spec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFastModelSelect = (spec) => {
|
||||||
|
if (typeof spec === 'string' && spec) {
|
||||||
|
configStore.setConfigValue('fast_model', spec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleContentGuardModelSelect = (spec) => {
|
||||||
|
if (typeof spec === 'string' && spec) {
|
||||||
|
configStore.setConfigValue('content_guard_llm_model', spec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openLink = (url) => {
|
||||||
|
window.open(url, '_blank')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.basic-settings-section {
|
||||||
|
.settings-content {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
color: var(--gray-900);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 24px 0 12px 0;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-description {
|
||||||
|
color: var(--gray-600);
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin-top: 10px;
|
||||||
|
background-color: var(--gray-0);
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
border: 1px solid var(--gray-150);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
margin-right: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--gray-800);
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.card-select {
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.services-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-link-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid var(--gray-150);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--gray-0);
|
||||||
|
transition: all 0.2s;
|
||||||
|
min-height: 70px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 1px 8px var(--gray-200);
|
||||||
|
border-color: var(--main-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-info {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 16px;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
color: var(--gray-900);
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--gray-600);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动端适配
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.basic-settings-section {
|
||||||
|
.section {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px 0;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.card-select {
|
||||||
|
.label {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.services-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-link-card {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
min-height: auto;
|
||||||
|
padding: 12px;
|
||||||
|
|
||||||
|
.service-info {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 小屏幕进一步优化
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.basic-settings-section {
|
||||||
|
.section-title {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
.label {
|
||||||
|
font-size: 14px;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-link-card {
|
||||||
|
.service-info {
|
||||||
|
h4 {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -108,23 +108,12 @@
|
|||||||
<div class="provider-actions">
|
<div class="provider-actions">
|
||||||
<a-button
|
<a-button
|
||||||
type="text"
|
type="text"
|
||||||
size="small"
|
|
||||||
class="expand-button"
|
class="expand-button"
|
||||||
@click.stop="openProviderConfig(item)"
|
@click.stop="openProviderConfig(item)"
|
||||||
title="配置模型"
|
title="配置模型"
|
||||||
>
|
>
|
||||||
<SettingOutlined />
|
<SettingOutlined />
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button
|
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
class="expand-button"
|
|
||||||
@click.stop="toggleExpand(item)"
|
|
||||||
>
|
|
||||||
<span class="icon-wrapper" :class="{'rotated': expandedModels[item]}">
|
|
||||||
<DownCircleOutlined />
|
|
||||||
</span>
|
|
||||||
</a-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body-wrapper" :class="{'expanded': expandedModels[item]}">
|
<div class="card-body-wrapper" :class="{'expanded': expandedModels[item]}">
|
||||||
@ -784,20 +773,6 @@ const testCustomProvider = async (providerId, modelName) => {
|
|||||||
.custom-providers-section {
|
.custom-providers-section {
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
|
|
||||||
.section-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--gray-900);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-description {
|
.section-description {
|
||||||
margin: 0 0 16px 0;
|
margin: 0 0 16px 0;
|
||||||
color: var(--gray-600);
|
color: var(--gray-600);
|
||||||
@ -850,13 +825,7 @@ const testCustomProvider = async (providerId, modelName) => {
|
|||||||
|
|
||||||
.provider-actions {
|
.provider-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 0px;
|
||||||
|
|
||||||
.ant-btn {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -898,19 +867,23 @@ const testCustomProvider = async (providerId, modelName) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--gray-900);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.builtin-providers-section {
|
.builtin-providers-section {
|
||||||
.section-header {
|
.section-header {
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--gray-900);
|
|
||||||
}
|
|
||||||
|
|
||||||
.providers-stats {
|
.providers-stats {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -923,13 +896,13 @@ const testCustomProvider = async (providerId, modelName) => {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|
||||||
&.available {
|
&.available {
|
||||||
background: rgba(34, 197, 94, 0.1);
|
background: var(--color-success-50);
|
||||||
color: #16a34a;
|
color: var(--color-success-700);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.unavailable {
|
&.unavailable {
|
||||||
background: rgba(249, 115, 22, 0.1);
|
background: var(--color-warning-50);
|
||||||
color: #ea580c;
|
color: var(--color-warning-700);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1007,13 +980,13 @@ const testCustomProvider = async (providerId, modelName) => {
|
|||||||
& > span {
|
& > span {
|
||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
user-select: all;
|
user-select: all;
|
||||||
background-color: rgba(251, 146, 60, 0.15);
|
background-color: var(--color-warning-50);
|
||||||
color: #d97706;
|
color: var(--color-warning-700);
|
||||||
padding: 3px 8px;
|
padding: 3px 8px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
border: 1px solid rgba(251, 146, 60, 0.2);
|
border: 1px solid var(--color-warning-100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1027,14 +1000,9 @@ const testCustomProvider = async (providerId, modelName) => {
|
|||||||
background: var(--gray-0);
|
background: var(--gray-0);
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--gray-50);
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-title-container {
|
.model-title-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
@ -1056,8 +1024,8 @@ const testCustomProvider = async (providerId, modelName) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.provider-id {
|
.provider-id {
|
||||||
background: var(--gray-100);
|
background: var(--gray-50);
|
||||||
color: var(--gray-600);
|
color: var(--gray-900);
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
@ -1095,7 +1063,7 @@ const testCustomProvider = async (providerId, modelName) => {
|
|||||||
filter: grayscale(100%);
|
filter: grayscale(100%);
|
||||||
transition: filter 0.2s ease;
|
transition: filter 0.2s ease;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
background-color: white;
|
background-color: white; /* 确保图标背景为白色 */
|
||||||
border: 1px solid var(--gray-200);
|
border: 1px solid var(--gray-200);
|
||||||
|
|
||||||
img {
|
img {
|
||||||
@ -1161,13 +1129,13 @@ const testCustomProvider = async (providerId, modelName) => {
|
|||||||
& > span {
|
& > span {
|
||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
user-select: all;
|
user-select: all;
|
||||||
background-color: rgba(251, 146, 60, 0.15);
|
background-color: var(--color-warning-50);
|
||||||
color: #d97706;
|
color: var(--color-warning-700);
|
||||||
padding: 3px 8px;
|
padding: 3px 8px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
border: 1px solid rgba(251, 146, 60, 0.2);
|
border: 1px solid var(--color-warning-100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1215,7 +1183,6 @@ const testCustomProvider = async (providerId, modelName) => {
|
|||||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background: var(--gray-10);
|
|
||||||
|
|
||||||
// 普通模型卡片样式
|
// 普通模型卡片样式
|
||||||
.card-models {
|
.card-models {
|
||||||
|
|||||||
313
web/src/components/SettingsModal.vue
Normal file
313
web/src/components/SettingsModal.vue
Normal file
@ -0,0 +1,313 @@
|
|||||||
|
<template>
|
||||||
|
<a-modal
|
||||||
|
v-model:open="visible"
|
||||||
|
title="系统设置"
|
||||||
|
width="90%"
|
||||||
|
:style="{ maxWidth: '1200px', minWidth: '320px' }"
|
||||||
|
:footer="null"
|
||||||
|
@cancel="handleClose"
|
||||||
|
class="settings-modal"
|
||||||
|
:destroyOnClose="true"
|
||||||
|
:bodyStyle="{ padding: 0 }"
|
||||||
|
>
|
||||||
|
<div class="settings-container">
|
||||||
|
<!-- 侧边栏 (Desktop) -->
|
||||||
|
<div class="settings-sider" v-if="!isMobile">
|
||||||
|
<div
|
||||||
|
class="sider-item"
|
||||||
|
:class="{ activesec: activeTab === 'base' }"
|
||||||
|
@click="activeTab = 'base'"
|
||||||
|
v-if="userStore.isSuperAdmin"
|
||||||
|
>
|
||||||
|
<SettingOutlined class="icon" />
|
||||||
|
<span>基本设置</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="sider-item"
|
||||||
|
:class="{ activesec: activeTab === 'model' }"
|
||||||
|
@click="activeTab = 'model'"
|
||||||
|
v-if="userStore.isSuperAdmin"
|
||||||
|
>
|
||||||
|
<CodeOutlined class="icon" />
|
||||||
|
<span>模型配置</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="sider-item"
|
||||||
|
:class="{ activesec: activeTab === 'user' }"
|
||||||
|
@click="activeTab = 'user'"
|
||||||
|
v-if="userStore.isAdmin"
|
||||||
|
>
|
||||||
|
<UserOutlined class="icon" />
|
||||||
|
<span>用户管理</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 顶部导航 (Mobile) -->
|
||||||
|
<div class="settings-mobile-nav" v-else>
|
||||||
|
<div
|
||||||
|
class="nav-item"
|
||||||
|
:class="{ active: activeTab === 'base' }"
|
||||||
|
@click="activeTab = 'base'"
|
||||||
|
v-if="userStore.isSuperAdmin"
|
||||||
|
>
|
||||||
|
基本设置
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="nav-item"
|
||||||
|
:class="{ active: activeTab === 'model' }"
|
||||||
|
@click="activeTab = 'model'"
|
||||||
|
v-if="userStore.isSuperAdmin"
|
||||||
|
>
|
||||||
|
模型配置
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="nav-item"
|
||||||
|
:class="{ active: activeTab === 'user' }"
|
||||||
|
@click="activeTab = 'user'"
|
||||||
|
v-if="userStore.isAdmin"
|
||||||
|
>
|
||||||
|
用户管理
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 内容区域 -->
|
||||||
|
<div class="settings-content-wrapper">
|
||||||
|
<div class="settings-content">
|
||||||
|
<div v-show="activeTab === 'base'" v-if="userStore.isSuperAdmin">
|
||||||
|
<BasicSettingsSection />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="activeTab === 'model'" v-if="userStore.isSuperAdmin">
|
||||||
|
<h3>模型配置</h3>
|
||||||
|
<p>请在 <code>.env</code> 文件中配置对应的 APIKEY,并重新启动服务</p>
|
||||||
|
<a-divider />
|
||||||
|
<ModelProvidersComponent />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="activeTab === 'user'" v-if="userStore.isAdmin">
|
||||||
|
<UserManagementComponent />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import {
|
||||||
|
SettingOutlined,
|
||||||
|
CodeOutlined,
|
||||||
|
UserOutlined
|
||||||
|
} from '@ant-design/icons-vue'
|
||||||
|
import BasicSettingsSection from '@/components/BasicSettingsSection.vue'
|
||||||
|
import ModelProvidersComponent from '@/components/ModelProvidersComponent.vue'
|
||||||
|
import UserManagementComponent from '@/components/UserManagementComponent.vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:visible', 'close'])
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const activeTab = ref('base')
|
||||||
|
const windowWidth = ref(window?.innerWidth || 0)
|
||||||
|
|
||||||
|
const isMobile = computed(() => windowWidth.value <= 768)
|
||||||
|
|
||||||
|
const visible = computed({
|
||||||
|
get: () => props.visible,
|
||||||
|
set: (value) => emit('update:visible', value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateWindowWidth = () => {
|
||||||
|
windowWidth.value = window?.innerWidth || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('resize', updateWindowWidth)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', updateWindowWidth)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 根据用户权限设置默认标签页
|
||||||
|
watch(() => props.visible, (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
if (userStore.isSuperAdmin) {
|
||||||
|
activeTab.value = 'base'
|
||||||
|
} else if (userStore.isAdmin) {
|
||||||
|
activeTab.value = 'user'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.settings-modal {
|
||||||
|
:deep(.ant-modal-header) {
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-bottom: 1px solid var(--gray-150);
|
||||||
|
|
||||||
|
.ant-modal-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--gray-900);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-modal-body) {
|
||||||
|
padding: 0;
|
||||||
|
height: calc(80vh - 55px); /* Header height approx 55px */
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-modal-content) {
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
max-height: 80vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-container {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar Styles - Matching SettingView.vue style */
|
||||||
|
.settings-sider {
|
||||||
|
width: 140px;
|
||||||
|
height: 100%;
|
||||||
|
// padding: 20px 20px 0 0; /* Matches SettingView .sider */
|
||||||
|
padding-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.sider-item {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 16px; /* Matches SettingView .sider > * */
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.1s; /* Matches SettingView */
|
||||||
|
text-align: left;
|
||||||
|
font-size: 15px; /* Matches SettingView */
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--gray-700);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
font-size: 14px; /* Slightly adjusted to align better, SettingView uses h() icon defaults */
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--gray-50);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.activesec {
|
||||||
|
background: var(--gray-100);
|
||||||
|
color: var(--main-700);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content Area */
|
||||||
|
.settings-content-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
/* background-color: #fff; SettingView seems to use default background */
|
||||||
|
|
||||||
|
.settings-content {
|
||||||
|
padding: 0 20px; /* Matches SettingView .setting padding */
|
||||||
|
margin-bottom: 40px; /* Matches SettingView .setting margin-bottom */
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--gray-900);
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* BasicSettingsSection has its own h3 styles which might conflict slightly but are mostly self-contained */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Styles */
|
||||||
|
.settings-mobile-nav {
|
||||||
|
display: flex;
|
||||||
|
overflow-x: auto;
|
||||||
|
border-bottom: 1px solid var(--gray-150);
|
||||||
|
background: var(--gray-0);
|
||||||
|
padding: 0 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
padding: 12px 16px;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--gray-600);
|
||||||
|
font-weight: 500;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: var(--main-color);
|
||||||
|
border-bottom-color: var(--main-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.settings-container {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-content-wrapper {
|
||||||
|
.settings-content {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.settings-modal {
|
||||||
|
:deep(.ant-modal) {
|
||||||
|
margin: 0;
|
||||||
|
max-width: 100vw;
|
||||||
|
width: 100vw !important;
|
||||||
|
height: 100vh;
|
||||||
|
top: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-modal-content) {
|
||||||
|
height: 100vh;
|
||||||
|
border-radius: 0;
|
||||||
|
max-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-modal-body) {
|
||||||
|
height: calc(100vh - 55px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -158,7 +158,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref, inject } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { useUserStore } from '@/stores/user';
|
import { useUserStore } from '@/stores/user';
|
||||||
//
|
//
|
||||||
@ -173,6 +173,9 @@ const router = useRouter();
|
|||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
|
|
||||||
|
// Inject settings modal methods
|
||||||
|
const { openSettingsModal } = inject('settingsModal', {});
|
||||||
|
|
||||||
// 个人资料弹窗状态
|
// 个人资料弹窗状态
|
||||||
const profileModalVisible = ref(false);
|
const profileModalVisible = ref(false);
|
||||||
const avatarUploading = ref(false);
|
const avatarUploading = ref(false);
|
||||||
@ -245,7 +248,9 @@ const toggleTheme = () => {
|
|||||||
|
|
||||||
// 前往设置页
|
// 前往设置页
|
||||||
const goToSetting = () => {
|
const goToSetting = () => {
|
||||||
router.push('/setting');
|
if (openSettingsModal) {
|
||||||
|
openSettingsModal()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打开个人资料页面
|
// 打开个人资料页面
|
||||||
|
|||||||
@ -470,6 +470,7 @@ onMounted(() => {
|
|||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.user-management {
|
.user-management {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
|
min-height: 50vh;
|
||||||
|
|
||||||
.header-section {
|
.header-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -481,7 +482,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
.description {
|
.description {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: var(--gray-400);
|
color: var(--gray-600);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
@ -505,7 +506,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
.user-cards-grid {
|
.user-cards-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
// padding: 16px;
|
// padding: 16px;
|
||||||
|
|
||||||
@ -670,7 +671,7 @@ onMounted(() => {
|
|||||||
.user-modal {
|
.user-modal {
|
||||||
:deep(.ant-modal-header) {
|
:deep(.ant-modal-header) {
|
||||||
padding: 20px 24px;
|
padding: 20px 24px;
|
||||||
border-bottom: 1px solid var(--gray-800);
|
border-bottom: 1px solid var(--gray-150);
|
||||||
|
|
||||||
.ant-modal-title {
|
.ant-modal-title {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
@ -705,7 +706,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.help-text {
|
.help-text {
|
||||||
color: var(--gray-400);
|
color: var(--gray-600);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted, useTemplateRef, computed } from 'vue'
|
import { ref, reactive, onMounted, useTemplateRef, computed, provide } from 'vue'
|
||||||
import { RouterLink, RouterView, useRoute } from 'vue-router'
|
import { RouterLink, RouterView, useRoute } from 'vue-router'
|
||||||
import {
|
import {
|
||||||
GithubOutlined,
|
GithubOutlined,
|
||||||
@ -15,6 +15,7 @@ import { storeToRefs } from 'pinia'
|
|||||||
import UserInfoComponent from '@/components/UserInfoComponent.vue'
|
import UserInfoComponent from '@/components/UserInfoComponent.vue'
|
||||||
import DebugComponent from '@/components/DebugComponent.vue'
|
import DebugComponent from '@/components/DebugComponent.vue'
|
||||||
import TaskCenterDrawer from '@/components/TaskCenterDrawer.vue'
|
import TaskCenterDrawer from '@/components/TaskCenterDrawer.vue'
|
||||||
|
import SettingsModal from '@/components/SettingsModal.vue'
|
||||||
|
|
||||||
const configStore = useConfigStore()
|
const configStore = useConfigStore()
|
||||||
const databaseStore = useDatabaseStore()
|
const databaseStore = useDatabaseStore()
|
||||||
@ -35,6 +36,14 @@ const isLoadingStars = ref(false)
|
|||||||
const showDebugModal = ref(false)
|
const showDebugModal = ref(false)
|
||||||
const htmlRefHook = useTemplateRef('htmlRefHook')
|
const htmlRefHook = useTemplateRef('htmlRefHook')
|
||||||
|
|
||||||
|
// Add state for settings modal
|
||||||
|
const showSettingsModal = ref(false)
|
||||||
|
|
||||||
|
// Provide settings modal methods to child components
|
||||||
|
const openSettingsModal = () => {
|
||||||
|
showSettingsModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
// Setup long press for debug modal
|
// Setup long press for debug modal
|
||||||
onLongPress(
|
onLongPress(
|
||||||
htmlRefHook,
|
htmlRefHook,
|
||||||
@ -116,6 +125,11 @@ const mainList = [{
|
|||||||
activeIcon: BarChart3,
|
activeIcon: BarChart3,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// Provide settings modal methods to child components
|
||||||
|
provide('settingsModal', {
|
||||||
|
openSettingsModal
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -195,7 +209,6 @@ const mainList = [{
|
|||||||
<div class="header-mobile">
|
<div class="header-mobile">
|
||||||
<RouterLink to="/agent" class="nav-item" active-class="active">对话</RouterLink>
|
<RouterLink to="/agent" class="nav-item" active-class="active">对话</RouterLink>
|
||||||
<RouterLink to="/database" class="nav-item" active-class="active">知识</RouterLink>
|
<RouterLink to="/database" class="nav-item" active-class="active">知识</RouterLink>
|
||||||
<RouterLink to="/setting" class="nav-item" active-class="active">设置</RouterLink>
|
|
||||||
</div>
|
</div>
|
||||||
<router-view v-slot="{ Component, route }" id="app-router-view">
|
<router-view v-slot="{ Component, route }" id="app-router-view">
|
||||||
<keep-alive v-if="route.meta.keepAlive !== false">
|
<keep-alive v-if="route.meta.keepAlive !== false">
|
||||||
@ -218,6 +231,10 @@ const mainList = [{
|
|||||||
<DebugComponent />
|
<DebugComponent />
|
||||||
</a-modal>
|
</a-modal>
|
||||||
<TaskCenterDrawer />
|
<TaskCenterDrawer />
|
||||||
|
<SettingsModal
|
||||||
|
v-model:visible="showSettingsModal"
|
||||||
|
@close="() => showSettingsModal = false"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@ -77,20 +77,7 @@ const router = createRouter({
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/setting',
|
|
||||||
name: 'setting',
|
|
||||||
component: AppLayout,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
name: 'SettingComp',
|
|
||||||
component: () => import('../views/SettingView.vue'),
|
|
||||||
meta: { keepAlive: true, requiresAuth: true, requiresAdmin: true }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/dashboard',
|
path: '/dashboard',
|
||||||
name: 'dashboard',
|
name: 'dashboard',
|
||||||
component: AppLayout,
|
component: AppLayout,
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<a-empty>
|
<a-empty>
|
||||||
<template #description>
|
<template #description>
|
||||||
<span>
|
<span>
|
||||||
前往 <router-link to="/setting" style="color: var(--main-color); font-weight: bold;">设置</router-link> 页面启用知识图谱。
|
点击右上角用户头像中的"系统设置"来启用知识图谱。
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</a-empty>
|
</a-empty>
|
||||||
|
|||||||
@ -16,10 +16,7 @@
|
|||||||
<router-link to="/database" class="nav-link" v-if="userStore.isLoggedIn && userStore.isAdmin">
|
<router-link to="/database" class="nav-link" v-if="userStore.isLoggedIn && userStore.isAdmin">
|
||||||
<span>知识库</span>
|
<span>知识库</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link to="/setting" class="nav-link" v-if="userStore.isLoggedIn && userStore.isAdmin">
|
</nav>
|
||||||
<span>设置</span>
|
|
||||||
</router-link>
|
|
||||||
</nav>
|
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<div class="github-link">
|
<div class="github-link">
|
||||||
<a href="https://github.com/xerrors/Yuxi-Know" target="_blank">
|
<a href="https://github.com/xerrors/Yuxi-Know" target="_blank">
|
||||||
|
|||||||
@ -1,435 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="setting-view">
|
|
||||||
<HeaderComponent title="设置" class="setting-header">
|
|
||||||
</HeaderComponent>
|
|
||||||
<div class="setting-container layout-container">
|
|
||||||
<div class="sider" v-if="state.windowWidth > 520">
|
|
||||||
<a-button type="text" v-if="userStore.isSuperAdmin" :class="{ activesec: state.section === 'base'}" @click="state.section='base'" :icon="h(SettingOutlined)"> 基本设置 </a-button>
|
|
||||||
<a-button type="text" v-if="userStore.isSuperAdmin" :class="{ activesec: state.section === 'model'}" @click="state.section='model'" :icon="h(CodeOutlined)"> 模型配置 </a-button>
|
|
||||||
<a-button type="text" :class="{ activesec: state.section === 'user'}" @click="state.section='user'" :icon="h(UserOutlined)" v-if="userStore.isAdmin"> 用户管理 </a-button>
|
|
||||||
</div>
|
|
||||||
<div class="setting" v-if="(state.windowWidth <= 520 || state.section === 'base') && userStore.isSuperAdmin">
|
|
||||||
<h3>检索配置</h3>
|
|
||||||
<div class="section">
|
|
||||||
<div class="card card-select">
|
|
||||||
<span class="label">{{ items?.default_model?.des || '默认对话模型' }}</span>
|
|
||||||
<ModelSelectorComponent
|
|
||||||
@select-model="handleChatModelSelect"
|
|
||||||
:model_spec="configStore.config?.default_model"
|
|
||||||
placeholder="请选择默认模型"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="card card-select">
|
|
||||||
<span class="label">{{ items?.fast_model.des }}</span>
|
|
||||||
<ModelSelectorComponent
|
|
||||||
@select-model="handleFastModelSelect"
|
|
||||||
:model_spec="configStore.config?.fast_model"
|
|
||||||
placeholder="请选择模型"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="card card-select">
|
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
||||||
<span class="label">{{ items?.embed_model.des }}</span>
|
|
||||||
<!-- <a-button
|
|
||||||
size="small"
|
|
||||||
:loading="state.checkingStatus"
|
|
||||||
@click="checkAllModelStatus"
|
|
||||||
:disabled="state.checkingStatus"
|
|
||||||
>
|
|
||||||
检查状态
|
|
||||||
</a-button> -->
|
|
||||||
</div>
|
|
||||||
<EmbeddingModelSelector
|
|
||||||
:value="configStore.config?.embed_model"
|
|
||||||
@change="handleChange('embed_model', $event)"
|
|
||||||
style="width: 320px"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="card card-select">
|
|
||||||
<span class="label">{{ items?.reranker.des }}</span>
|
|
||||||
<a-select style="width: 320px"
|
|
||||||
:value="configStore.config?.reranker"
|
|
||||||
@change="handleChange('reranker', $event)"
|
|
||||||
>
|
|
||||||
<a-select-option
|
|
||||||
v-for="(name, idx) in rerankerChoices" :key="idx"
|
|
||||||
:value="name">{{ name }}
|
|
||||||
</a-select-option>
|
|
||||||
</a-select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h3>内容审查配置</h3>
|
|
||||||
<div class="section">
|
|
||||||
<!-- 内容审查配置 -->
|
|
||||||
<div class="card">
|
|
||||||
<span class="label">{{ items?.enable_content_guard.des }}</span>
|
|
||||||
<a-switch
|
|
||||||
:checked="configStore.config?.enable_content_guard"
|
|
||||||
@change="handleChange('enable_content_guard', $event)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="card" v-if="configStore.config?.enable_content_guard">
|
|
||||||
<span class="label">{{ items?.enable_content_guard_llm.des }}</span>
|
|
||||||
<a-switch
|
|
||||||
:checked="configStore.config?.enable_content_guard_llm"
|
|
||||||
@change="handleChange('enable_content_guard_llm', $event)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="card card-select" v-if="configStore.config?.enable_content_guard && configStore.config?.enable_content_guard_llm">
|
|
||||||
<span class="label">{{ items?.content_guard_llm_model.des }}</span>
|
|
||||||
<ModelSelectorComponent
|
|
||||||
@select-model="handleContentGuardModelSelect"
|
|
||||||
:model_spec="configStore.config?.content_guard_llm_model"
|
|
||||||
placeholder="请选择模型"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 服务链接部分 -->
|
|
||||||
<div v-if="userStore.isAdmin">
|
|
||||||
<h3>服务链接</h3>
|
|
||||||
<p>快速访问系统相关的外部服务,需要将 localhost 替换为实际的 IP 地址。</p>
|
|
||||||
<div class="services-grid">
|
|
||||||
<div class="service-link-card">
|
|
||||||
<div class="service-info">
|
|
||||||
<h4>Neo4j 浏览器</h4>
|
|
||||||
<p>图数据库管理界面</p>
|
|
||||||
</div>
|
|
||||||
<a-button type="default" @click="openLink('http://localhost:7474/')" :icon="h(GlobalOutlined)">
|
|
||||||
访问
|
|
||||||
</a-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="service-link-card">
|
|
||||||
<div class="service-info">
|
|
||||||
<h4>API 接口文档</h4>
|
|
||||||
<p>系统接口文档和调试工具</p>
|
|
||||||
</div>
|
|
||||||
<a-button type="default" @click="openLink('http://localhost:5050/docs')" :icon="h(GlobalOutlined)">
|
|
||||||
访问
|
|
||||||
</a-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="service-link-card">
|
|
||||||
<div class="service-info">
|
|
||||||
<h4>MinIO 对象存储</h4>
|
|
||||||
<p>文件存储管理控制台</p>
|
|
||||||
</div>
|
|
||||||
<a-button type="default" @click="openLink('http://localhost:9001')" :icon="h(GlobalOutlined)">
|
|
||||||
访问
|
|
||||||
</a-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="service-link-card">
|
|
||||||
<div class="service-info">
|
|
||||||
<h4>Milvus WebUI</h4>
|
|
||||||
<p>向量数据库管理界面</p>
|
|
||||||
</div>
|
|
||||||
<a-button type="default" @click="openLink('http://localhost:9091/webui/')" :icon="h(GlobalOutlined)">
|
|
||||||
访问
|
|
||||||
</a-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="setting" v-if="(state.windowWidth <= 520 || state.section === 'model') && userStore.isSuperAdmin">
|
|
||||||
<h3>模型配置</h3>
|
|
||||||
<p>请在 <code>.env</code> 文件中配置对应的 APIKEY,并重新启动服务</p>
|
|
||||||
<ModelProvidersComponent />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="setting" v-if="(state.windowWidth <= 520 || state.section === 'user') && userStore.isAdmin">
|
|
||||||
<UserManagementComponent />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { message } from 'ant-design-vue';
|
|
||||||
import { computed, reactive, ref, h, watch, onMounted, onUnmounted } from 'vue'
|
|
||||||
import { useConfigStore } from '@/stores/config';
|
|
||||||
import { useUserStore } from '@/stores/user'
|
|
||||||
import {
|
|
||||||
SettingOutlined,
|
|
||||||
CodeOutlined,
|
|
||||||
FolderOutlined,
|
|
||||||
UserOutlined,
|
|
||||||
GlobalOutlined
|
|
||||||
} from '@ant-design/icons-vue';
|
|
||||||
import HeaderComponent from '@/components/HeaderComponent.vue';
|
|
||||||
import ModelProvidersComponent from '@/components/ModelProvidersComponent.vue';
|
|
||||||
import UserManagementComponent from '@/components/UserManagementComponent.vue';
|
|
||||||
import ModelSelectorComponent from '@/components/ModelSelectorComponent.vue';
|
|
||||||
import EmbeddingModelSelector from '@/components/EmbeddingModelSelector.vue';
|
|
||||||
|
|
||||||
const configStore = useConfigStore()
|
|
||||||
const userStore = useUserStore()
|
|
||||||
const items = computed(() => configStore.config._config_items)
|
|
||||||
const state = reactive({
|
|
||||||
loading: false,
|
|
||||||
section: 'base',
|
|
||||||
windowWidth: window?.innerWidth || 0,
|
|
||||||
})
|
|
||||||
|
|
||||||
const rerankerChoices = computed(() => {
|
|
||||||
return Object.keys(configStore?.config?.reranker_names || {}) || []
|
|
||||||
})
|
|
||||||
|
|
||||||
const preHandleChange = (key, e) => {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleChange = (key, e) => {
|
|
||||||
if (!preHandleChange(key, e)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
configStore.setConfigValue(key, e)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleChanges = (items) => {
|
|
||||||
for (const key in items) {
|
|
||||||
if (!preHandleChange(key, items[key])) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
configStore.setConfigValues(items)
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateWindowWidth = () => {
|
|
||||||
state.windowWidth = window?.innerWidth || 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleChatModelSelect = (spec) => {
|
|
||||||
if (typeof spec === 'string' && spec) {
|
|
||||||
configStore.setConfigValue('default_model', spec)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFastModelSelect = (spec) => {
|
|
||||||
if (typeof spec === 'string' && spec) {
|
|
||||||
configStore.setConfigValue('fast_model', spec)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleContentGuardModelSelect = (spec) => {
|
|
||||||
if (typeof spec === 'string' && spec) {
|
|
||||||
configStore.setConfigValue('content_guard_llm_model', spec)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
updateWindowWidth()
|
|
||||||
window.addEventListener('resize', updateWindowWidth)
|
|
||||||
state.section = userStore.isSuperAdmin ? 'base' : 'user'
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener('resize', updateWindowWidth)
|
|
||||||
})
|
|
||||||
|
|
||||||
const openLink = (url) => {
|
|
||||||
window.open(url, '_blank')
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
|
|
||||||
.setting-container {
|
|
||||||
--setting-header-height: 55px;
|
|
||||||
max-width: 1054px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-header {
|
|
||||||
height: var(--setting-header-height);
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-header p {
|
|
||||||
margin: 8px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-container {
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: flex;
|
|
||||||
position: relative;
|
|
||||||
min-height: calc(100vh - var(--setting-header-height));
|
|
||||||
}
|
|
||||||
|
|
||||||
.sider {
|
|
||||||
width: 180px;
|
|
||||||
height: 100%;
|
|
||||||
padding: 0 20px;
|
|
||||||
position: sticky;
|
|
||||||
top: var(--setting-header-height);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding-top: 20px;
|
|
||||||
|
|
||||||
|
|
||||||
& > * {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
padding: 6px 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.1s;
|
|
||||||
text-align: left;
|
|
||||||
font-size: 15px;
|
|
||||||
border-radius: 8px;
|
|
||||||
color: var(--gray-700);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--gray-50);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.activesec {
|
|
||||||
background: var(--gray-100);
|
|
||||||
color: var(--main-700);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting {
|
|
||||||
width: 100%;
|
|
||||||
flex: 1;
|
|
||||||
margin: 0 auto;
|
|
||||||
height: 100%;
|
|
||||||
padding: 0 20px;
|
|
||||||
margin-bottom: 40px;
|
|
||||||
|
|
||||||
h3:not(:first-child) {
|
|
||||||
margin-top: 30px;
|
|
||||||
}
|
|
||||||
h3:first-child {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section {
|
|
||||||
margin-top: 10px;
|
|
||||||
background-color: var(--gray-0);
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-radius: 8px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
border: 1px solid var(--gray-150);
|
|
||||||
|
|
||||||
.content-guard-section {
|
|
||||||
h4 {
|
|
||||||
margin: 0 0 12px 0;
|
|
||||||
color: var(--gray-900);
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
border-bottom: 1px solid var(--gray-150);
|
|
||||||
padding-bottom: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
// padding: 12px 0;
|
|
||||||
|
|
||||||
.label {
|
|
||||||
margin-right: 20px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--gray-800);
|
|
||||||
|
|
||||||
button {
|
|
||||||
margin-left: 10px;
|
|
||||||
height: 24px;
|
|
||||||
padding: 0 8px;
|
|
||||||
font-size: smaller;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.services-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
||||||
gap: 12px;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-link-card {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border: 1px solid var(--gray-150);
|
|
||||||
border-radius: 8px;
|
|
||||||
background: var(--gray-0);
|
|
||||||
transition: all 0.2s;
|
|
||||||
min-height: 60px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
box-shadow: 0 1px 8px var(--gray-200);
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-info {
|
|
||||||
flex: 1;
|
|
||||||
margin-right: 16px;
|
|
||||||
|
|
||||||
h4 {
|
|
||||||
margin: 0 0 4px 0;
|
|
||||||
color: var(--gray-900);
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
color: var(--gray-600);
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@media (max-width: 520px) {
|
|
||||||
.setting-container {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card.card-select {
|
|
||||||
gap: 0.75rem;
|
|
||||||
align-items: flex-start;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.services-grid {
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-link-card {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 12px;
|
|
||||||
min-height: auto;
|
|
||||||
padding: 12px;
|
|
||||||
|
|
||||||
.service-info {
|
|
||||||
text-align: left;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style lang="less">
|
|
||||||
// 添加全局样式以确保滚动功能在dropdown内正常工作
|
|
||||||
.ant-dropdown-menu {
|
|
||||||
&.scrollable-menu {
|
|
||||||
max-height: 300px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
Loading…
Reference in New Issue
Block a user