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 historyConvs = MessageProcessor.convertServerHistoryToMessages(currentThreadMessages.value);
|
||||
const threadState = currentThreadState.value;
|
||||
const historyConvs = historyConversations.value;
|
||||
|
||||
// 如果有进行中的消息且线程状态显示正在流式处理,添加进行中的对话
|
||||
if (onGoingConvMessages.value.length > 0) {
|
||||
@ -369,7 +372,6 @@ const isMediumContainer = computed(() => localUIState.containerWidth <= 768);
|
||||
|
||||
// ==================== SCROLL & RESIZE HANDLING ====================
|
||||
const chatContainerRef = ref(null);
|
||||
const messageInputRef = ref(null);
|
||||
const scrollController = new ScrollController('.chat');
|
||||
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">
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
class="expand-button"
|
||||
@click.stop="openProviderConfig(item)"
|
||||
title="配置模型"
|
||||
>
|
||||
<SettingOutlined />
|
||||
</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 class="card-body-wrapper" :class="{'expanded': expandedModels[item]}">
|
||||
@ -784,20 +773,6 @@ const testCustomProvider = async (providerId, modelName) => {
|
||||
.custom-providers-section {
|
||||
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 {
|
||||
margin: 0 0 16px 0;
|
||||
color: var(--gray-600);
|
||||
@ -850,13 +825,7 @@ const testCustomProvider = async (providerId, modelName) => {
|
||||
|
||||
.provider-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
.ant-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
gap: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
.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 {
|
||||
display: flex;
|
||||
@ -923,13 +896,13 @@ const testCustomProvider = async (providerId, modelName) => {
|
||||
font-size: 12px;
|
||||
|
||||
&.available {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
color: #16a34a;
|
||||
background: var(--color-success-50);
|
||||
color: var(--color-success-700);
|
||||
}
|
||||
|
||||
&.unavailable {
|
||||
background: rgba(249, 115, 22, 0.1);
|
||||
color: #ea580c;
|
||||
background: var(--color-warning-50);
|
||||
color: var(--color-warning-700);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1007,13 +980,13 @@ const testCustomProvider = async (providerId, modelName) => {
|
||||
& > span {
|
||||
margin-left: 6px;
|
||||
user-select: all;
|
||||
background-color: rgba(251, 146, 60, 0.15);
|
||||
color: #d97706;
|
||||
background-color: var(--color-warning-50);
|
||||
color: var(--color-warning-700);
|
||||
padding: 3px 8px;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
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);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--gray-50);
|
||||
}
|
||||
|
||||
.model-title-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
|
||||
h3 {
|
||||
@ -1056,8 +1024,8 @@ const testCustomProvider = async (providerId, modelName) => {
|
||||
}
|
||||
|
||||
.provider-id {
|
||||
background: var(--gray-100);
|
||||
color: var(--gray-600);
|
||||
background: var(--gray-50);
|
||||
color: var(--gray-900);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
@ -1095,7 +1063,7 @@ const testCustomProvider = async (providerId, modelName) => {
|
||||
filter: grayscale(100%);
|
||||
transition: filter 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
background-color: white;
|
||||
background-color: white; /* 确保图标背景为白色 */
|
||||
border: 1px solid var(--gray-200);
|
||||
|
||||
img {
|
||||
@ -1161,13 +1129,13 @@ const testCustomProvider = async (providerId, modelName) => {
|
||||
& > span {
|
||||
margin-left: 6px;
|
||||
user-select: all;
|
||||
background-color: rgba(251, 146, 60, 0.15);
|
||||
color: #d97706;
|
||||
background-color: var(--color-warning-50);
|
||||
color: var(--color-warning-700);
|
||||
padding: 3px 8px;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
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));
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: var(--gray-10);
|
||||
|
||||
// 普通模型卡片样式
|
||||
.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>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, ref, inject } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
//
|
||||
@ -173,6 +173,9 @@ const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
// Inject settings modal methods
|
||||
const { openSettingsModal } = inject('settingsModal', {});
|
||||
|
||||
// 个人资料弹窗状态
|
||||
const profileModalVisible = ref(false);
|
||||
const avatarUploading = ref(false);
|
||||
@ -245,7 +248,9 @@ const toggleTheme = () => {
|
||||
|
||||
// 前往设置页
|
||||
const goToSetting = () => {
|
||||
router.push('/setting');
|
||||
if (openSettingsModal) {
|
||||
openSettingsModal()
|
||||
}
|
||||
}
|
||||
|
||||
// 打开个人资料页面
|
||||
|
||||
@ -470,6 +470,7 @@ onMounted(() => {
|
||||
<style lang="less" scoped>
|
||||
.user-management {
|
||||
margin-top: 20px;
|
||||
min-height: 50vh;
|
||||
|
||||
.header-section {
|
||||
display: flex;
|
||||
@ -481,7 +482,7 @@ onMounted(() => {
|
||||
|
||||
.description {
|
||||
font-size: 14px;
|
||||
color: var(--gray-400);
|
||||
color: var(--gray-600);
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 16px;
|
||||
@ -505,7 +506,7 @@ onMounted(() => {
|
||||
|
||||
.user-cards-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 16px;
|
||||
// padding: 16px;
|
||||
|
||||
@ -670,7 +671,7 @@ onMounted(() => {
|
||||
.user-modal {
|
||||
:deep(.ant-modal-header) {
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid var(--gray-800);
|
||||
border-bottom: 1px solid var(--gray-150);
|
||||
|
||||
.ant-modal-title {
|
||||
font-size: 16px;
|
||||
@ -705,7 +706,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.help-text {
|
||||
color: var(--gray-400);
|
||||
color: var(--gray-600);
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
line-height: 1.3;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<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 {
|
||||
GithubOutlined,
|
||||
@ -15,6 +15,7 @@ import { storeToRefs } from 'pinia'
|
||||
import UserInfoComponent from '@/components/UserInfoComponent.vue'
|
||||
import DebugComponent from '@/components/DebugComponent.vue'
|
||||
import TaskCenterDrawer from '@/components/TaskCenterDrawer.vue'
|
||||
import SettingsModal from '@/components/SettingsModal.vue'
|
||||
|
||||
const configStore = useConfigStore()
|
||||
const databaseStore = useDatabaseStore()
|
||||
@ -35,6 +36,14 @@ const isLoadingStars = ref(false)
|
||||
const showDebugModal = ref(false)
|
||||
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
|
||||
onLongPress(
|
||||
htmlRefHook,
|
||||
@ -116,6 +125,11 @@ const mainList = [{
|
||||
activeIcon: BarChart3,
|
||||
}
|
||||
]
|
||||
|
||||
// Provide settings modal methods to child components
|
||||
provide('settingsModal', {
|
||||
openSettingsModal
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -195,7 +209,6 @@ const mainList = [{
|
||||
<div class="header-mobile">
|
||||
<RouterLink to="/agent" 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>
|
||||
<router-view v-slot="{ Component, route }" id="app-router-view">
|
||||
<keep-alive v-if="route.meta.keepAlive !== false">
|
||||
@ -218,6 +231,10 @@ const mainList = [{
|
||||
<DebugComponent />
|
||||
</a-modal>
|
||||
<TaskCenterDrawer />
|
||||
<SettingsModal
|
||||
v-model:visible="showSettingsModal"
|
||||
@close="() => showSettingsModal = false"
|
||||
/>
|
||||
</div>
|
||||
</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',
|
||||
name: 'dashboard',
|
||||
component: AppLayout,
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<a-empty>
|
||||
<template #description>
|
||||
<span>
|
||||
前往 <router-link to="/setting" style="color: var(--main-color); font-weight: bold;">设置</router-link> 页面启用知识图谱。
|
||||
点击右上角用户头像中的"系统设置"来启用知识图谱。
|
||||
</span>
|
||||
</template>
|
||||
</a-empty>
|
||||
|
||||
@ -16,10 +16,7 @@
|
||||
<router-link to="/database" class="nav-link" v-if="userStore.isLoggedIn && userStore.isAdmin">
|
||||
<span>知识库</span>
|
||||
</router-link>
|
||||
<router-link to="/setting" class="nav-link" v-if="userStore.isLoggedIn && userStore.isAdmin">
|
||||
<span>设置</span>
|
||||
</router-link>
|
||||
</nav>
|
||||
</nav>
|
||||
<div class="header-actions">
|
||||
<div class="github-link">
|
||||
<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