chore(ui): Update Setting page

This commit is contained in:
wizardchen
2025-09-16 20:18:47 +08:00
committed by lyingbug
parent 4137a63852
commit 66aec78960
10 changed files with 2944 additions and 252 deletions

View File

@@ -19,6 +19,7 @@ export interface InitializationConfig {
modelName: string;
baseUrl: string;
apiKey?: string;
enabled: boolean;
};
multimodal: {
enabled: boolean;

View File

@@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="10" cy="6" r="3" stroke="#07C05F" stroke-width="1.5" fill="none"/>
<path d="M4 16c0-3.314 2.686-6 6-6s6 2.686 6 6" stroke="#07C05F" stroke-width="1.5" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 284 B

View File

@@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="10" cy="6" r="3" stroke="currentColor" stroke-width="1.5" fill="none"/>
<path d="M4 16c0-3.314 2.686-6 6-6s6 2.686 6 6" stroke="currentColor" stroke-width="1.5" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 294 B

View File

@@ -201,24 +201,19 @@ let knowledgeIcon = ref('zhishiku-green.svg');
let prefixIcon = ref('prefixIcon.svg');
let settingIcon = ref('setting.svg');
let logoutIcon = ref('logout.svg');
let tenantIcon = ref('setting.svg'); // 暂时使用setting图标
let tenantIcon = ref('user.svg'); // 使用专门的用户图标
let pathPrefix = ref(route.name)
const getIcon = (path) => {
fileAddIcon.value = path == 'knowledgeBase' ? 'file-add-green.svg' : 'file-add.svg';
knowledgeIcon.value = path == 'knowledgeBase' ? 'zhishiku-green.svg' : 'zhishiku.svg';
prefixIcon.value = path == 'creatChat' ? 'prefixIcon-green.svg' : path == 'knowledgeBase' ? 'prefixIcon-grey.svg' : 'prefixIcon.svg';
settingIcon.value = path == 'settings' ? 'setting-green.svg' : 'setting.svg';
tenantIcon.value = path == 'tenant' ? 'setting-green.svg' : 'setting.svg'; // 暂时使用setting图标
tenantIcon.value = path == 'tenant' ? 'user-green.svg' : 'user.svg'; // 使用专门的用户图标
logoutIcon.value = 'logout.svg';
}
getIcon(route.name)
const gotopage = (path) => {
pathPrefix.value = path;
// 如果是系统设置,跳转到初始化配置页面
if (path === 'settings') {
router.push('/initialization');
return;
}
// 处理退出登录
if (path === 'logout') {
authStore.logout();

View File

@@ -62,9 +62,9 @@ const router = createRouter({
{
path: "settings",
name: "settings",
component: () => import("../views/settings/Settings.vue"),
meta: { requiresInit: true }
},
component: () => import("../views/settings/SystemSettings.vue"),
meta: { requiresInit: true }
},
],
},
],

View File

@@ -2,29 +2,20 @@
<div class="initialization-container">
<!-- 页面标题区域 -->
<div class="initialization-header">
<h1>WeKnora 系统初始化配置</h1>
<p>首次使用需要配置模型和服务信息完成后即可开始使用系统</p>
<div class="header-content">
<h1>WeKnora 系统初始化配置</h1>
<p>首次使用需要配置模型和服务信息完成后即可开始使用系统</p>
</div>
<div class="header-actions">
<t-button size="small" variant="outline" theme="danger" @click="handleLogout">
退出登录
</t-button>
</div>
</div>
<!-- 页面主体两栏布局左侧导航 + 右侧内容 -->
<div class="init-layout">
<!-- 左侧导航 -->
<aside class="sidebar">
<div class="sidebar-card">
<div class="nav-title">配置导航</div>
<ul class="nav-list">
<li v-for="s in sections" :key="s.id" :class="['nav-item', { active: activeSectionId === s.id }]" @click="goToSection(s.id)">
<span class="dot" />{{ s.label }}
</li>
</ul>
<t-divider />
<t-button size="small" variant="outline" theme="danger" block @click="handleLogout">
退出登录
</t-button>
</div>
</aside>
<div class="init-main">
<!-- 顶部公共区域Ollama 服务状态与已安装模型 -->
<!-- 主要内容区域 -->
<div class="init-content">
<!-- 顶部公共区域Ollama 服务状态与已安装模型 -->
<div class="ollama-summary-card" id="section-ollama">
<div class="summary-header">
<span class="title"><t-icon name="server" />Ollama 服务状态</span>
@@ -758,8 +749,7 @@
</div>
</t-form>
</div> <!-- .init-main -->
</div> <!-- .init-layout -->
</div> <!-- .init-content -->
</div>
</template>
@@ -941,26 +931,6 @@ const imageUpload = ref(null);
// Embedding 维度检测状态
const embeddingDimDetecting = ref(false);
// 左侧导航区段
type Section = { id: string; label: string };
const sections: Section[] = [
{ id: 'ollama', label: 'Ollama 服务' },
{ id: 'llm', label: 'LLM 模型' },
{ id: 'embedding', label: 'Embedding 模型' },
{ id: 'rerank', label: 'Rerank 配置' },
{ id: 'multimodal', label: '多模态配置' },
{ id: 'docsplit', label: '文档分割' },
{ id: 'submit', label: '完成配置' },
];
const activeSectionId = ref<string>('ollama');
const goToSection = (id: string) => {
const el = document.getElementById(`section-${id}`);
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
activeSectionId.value = id;
}
};
// 退出登录
const handleLogout = () => {
@@ -968,19 +938,6 @@ const handleLogout = () => {
router.replace('/login');
};
// 监听滚动,高亮当前区块
const onScroll = () => {
const order = ['ollama','llm','embedding','rerank','multimodal','docsplit','submit'];
for (const id of order) {
const el = document.getElementById(`section-${id}`);
if (!el) continue;
const rect = el.getBoundingClientRect();
if (rect.top <= 120 && rect.bottom >= 120) {
activeSectionId.value = id;
break;
}
}
};
// 配置回填
const loadCurrentConfig = async () => {
@@ -1004,8 +961,8 @@ const loadCurrentConfig = async () => {
if (config.rerank) {
Object.assign(formData.rerank, config.rerank);
}
formData.storageType = config.multimodal.storageType;
if (config.multimodal) {
formData.storageType = config.multimodal.storageType || 'minio';
formData.multimodal.enabled = config.multimodal.enabled || false;
if (config.multimodal.vlm) {
Object.assign(formData.multimodal.vlm, config.multimodal.vlm);
@@ -1014,11 +971,14 @@ const loadCurrentConfig = async () => {
formData.multimodal.vlm.interfaceType = 'ollama';
}
}
if (config.multimodal.storageType === 'cos') {
if (config.multimodal.storageType === 'cos' && config.multimodal.cos) {
Object.assign(formData.multimodal.cos, config.multimodal.cos);
} else if (config.multimodal.storageType === 'minio') {
} else if (config.multimodal.storageType === 'minio' && config.multimodal.minio) {
Object.assign(formData.multimodal.minio, config.multimodal.minio);
}
} else {
// 如果没有多模态配置,设置默认存储类型
formData.storageType = 'minio';
}
if (config.documentSplitting) {
Object.assign(formData.documentSplitting, config.documentSplitting);
@@ -1351,8 +1311,6 @@ onUnmounted(() => {
clearTimeout(submitDebounceTimer.value);
submitDebounceTimer.value = null;
}
window.removeEventListener('scroll', onScroll);
});
// 事件处理
@@ -1740,6 +1698,23 @@ const handleSubmit = async () => {
// 调用初始化API
const payload: any = JSON.parse(JSON.stringify(formData));
// 确保存储类型在正确的位置,并只保留选中的存储配置
if (payload.multimodal && payload.storageType) {
payload.multimodal.storageType = payload.storageType;
// 根据选择的存储类型,只保留对应的配置,删除其他存储配置
if (payload.storageType === 'cos') {
// 保留COS配置删除MinIO配置
delete payload.multimodal.minio;
} else if (payload.storageType === 'minio') {
// 保留MinIO配置删除COS配置
delete payload.multimodal.cos;
}
delete payload.storageType; // 删除顶级的storageType字段
}
await initializeSystem(payload);
const successMessage = isUpdateMode.value ? '配置更新成功!' : '系统初始化成功!';
@@ -1777,9 +1752,6 @@ onMounted(async () => {
// 检查已配置模型状态
await checkAllConfiguredModels();
// 绑定滚动监听,用于左侧导航高亮
window.addEventListener('scroll', onScroll, { passive: true });
});
const onRerankChange = () => {
@@ -2347,144 +2319,75 @@ const detectEmbeddingDimension = async () => {
<style lang="less" scoped>
.initialization-container {
padding: 20px 16px;
background: linear-gradient(180deg, #f7faf9 0%, #f9fbfa 60%, #ffffff 100%);
scroll-behavior: smooth;
min-height: 100vh;
background: #f8fafb;
padding: 0;
.initialization-header {
text-align: center;
margin: 10px auto 18px;
background: #fff;
border-bottom: 1px solid #e8f0fe;
padding: 24px 32px;
display: flex;
justify-content: space-between;
align-items: center;
h1 {
margin: 0 0 6px;
font-size: 22px;
font-weight: 700;
color: #0f172a;
.header-content {
h1 {
margin: 0 0 8px;
font-size: 24px;
font-weight: 600;
color: #1f2937;
}
p {
margin: 0;
color: #6b7280;
font-size: 14px;
}
}
p {
margin: 0;
color: #64748b;
font-size: 14px;
.header-actions {
flex-shrink: 0;
}
}
.init-layout {
display: grid;
grid-template-columns: 220px 1fr;
gap: 24px;
.init-content {
max-width: 1200px;
margin: 0 auto;
padding: 24px 32px;
}
.sidebar {
position: sticky;
top: 20px;
height: fit-content;
padding-right: 20px;
}
.sidebar-card {
background: #fff;
border: 1px solid #e8f5e8;
border-radius: 14px;
box-shadow: 0 8px 24px rgba(7, 192, 95, 0.08);
padding: 16px 12px;
}
.nav-title {
font-weight: 600;
color: #2c5234;
padding: 8px 10px 12px;
border-bottom: 2px solid #f0fdf4;
margin-bottom: 12px;
font-size: 15px;
}
.nav-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 6px;
}
.nav-item {
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
color: #6b7280;
padding: 10px 12px;
border-radius: 10px;
transition: all 0.2s ease;
font-size: 14px;
font-weight: 500;
}
.nav-item:hover {
background: #f0fdf4;
color: #166534;
transform: translateX(2px);
}
.nav-item.active {
background: linear-gradient(135deg, #07c05f, #00a651);
color: white;
font-weight: 600;
box-shadow: 0 4px 12px rgba(7, 192, 95, 0.2);
transform: translateX(2px);
}
.nav-item .dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
transition: all 0.2s ease;
}
.nav-item.active .dot {
background: white;
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.3);
}
.init-main {
min-width: 0;
max-width: 960px;
}
/* 统一分区卡片视觉 */
.config-section {
background: #fff;
border: 1px solid #eef4f0;
border: 1px solid #e8f0fe;
border-radius: 12px;
box-shadow: 0 6px 18px rgba(7, 192, 95, 0.04);
padding: 16px;
margin: 14px 0;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
padding: 24px;
margin-bottom: 24px;
h3 {
display: flex;
align-items: center;
gap: 8px;
margin: 0 0 12px;
font-size: 16px;
font-weight: 700;
color: #0f172a;
margin: 0 0 20px;
font-size: 18px;
font-weight: 600;
color: #1f2937;
padding-bottom: 12px;
border-bottom: 1px solid #f3f4f6;
}
.section-icon {
color: #07c05f;
font-size: 18px;
font-size: 20px;
}
}
.ollama-summary-card {
max-width: 100%;
margin: 0 0 16px 0;
background: #ffffff;
border: 1px solid #e9edf5;
background: #fff;
border: 1px solid #e8f0fe;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.05);
padding: 14px 16px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
padding: 20px;
margin-bottom: 24px;
.summary-header {
display: flex;
@@ -2522,57 +2425,6 @@ const detectEmbeddingDimension = async () => {
}
}
}
min-height: 100vh;
/* 更柔和的浅色背景,贴近截图的干净感 */
background: linear-gradient(180deg, #f7fbff 0%, #f8fff9 100%);
padding: 48px 20px;
.initialization-header {
text-align: center;
margin-bottom: 40px;
color: #2c5234;
.logo-container {
margin-bottom: 20px;
.logo {
height: 60px;
width: auto;
max-width: 200px;
object-fit: contain;
filter: drop-shadow(0 4px 8px rgba(44, 82, 52, 0.1));
transition: transform 0.3s ease;
&:hover {
transform: scale(1.05);
}
}
}
h1 {
font-size: 32px;
font-weight: 600;
margin-bottom: 10px;
text-shadow: 0 2px 4px rgba(44, 82, 52, 0.1);
}
p {
font-size: 16px;
opacity: 0.8;
margin: 0;
}
}
:deep(.t-form) {
/* 表单容器卡片化:更宽一点、更浅的阴影与细边框 */
max-width: 100%;
margin: 0;
background: #fff;
padding: 32px 36px;
border-radius: 14px;
border: 1px solid #e9edf5;
box-shadow: 0 12px 32px rgba(15, 23, 42, 0.06);
}
.config-section {
margin-bottom: 32px;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,125 @@
<template>
<div class="system-settings-container">
<!-- 页面标题区域 -->
<div class="settings-header">
<h2>系统设置</h2>
<p class="settings-subtitle">管理和更新系统模型与服务配置</p>
</div>
<!-- 配置内容 -->
<div class="settings-content">
<!-- 直接引用初始化配置组件的内容不使用外层卡片包装 -->
<InitializationContent />
</div>
</div>
</template>
<script setup lang="ts">
import { defineAsyncComponent } from 'vue'
// 异步加载初始化配置组件
const InitializationContent = defineAsyncComponent(() => import('../initialization/InitializationContent.vue'))
</script>
<style lang="less" scoped>
.system-settings-container {
padding: 20px;
background-color: #fff;
margin: 0 20px 0 20px;
height: calc(100vh);
overflow-y: auto;
box-sizing: border-box;
flex: 1;
}
.settings-header {
margin-bottom: 20px;
border-bottom: 1px solid #f0f0f0;
padding-bottom: 16px;
h2 {
font-size: 20px;
font-weight: 600;
color: #000000;
margin: 0 0 8px 0;
}
.settings-subtitle {
font-size: 14px;
color: #666666;
margin: 0;
}
}
.settings-content {
margin-top: 0;
}
/* 响应式设计 */
@media (max-width: 768px) {
.system-settings-container {
padding: 16px;
margin: 10px;
height: calc(100vh - 20px);
}
.settings-header h2 {
font-size: 18px;
}
}
/* 覆盖TDesign组件样式与账户信息页面保持一致 */
:deep(.t-card) {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border: 1px solid #e5e7eb;
}
/* 调整InitializationContent内部样式使每个配置区域显示为独立卡片 */
:deep(.config-section) {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
padding: 20px;
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
h3 {
font-size: 16px;
font-weight: 600;
color: #000000;
margin: 0 0 16px 0;
display: flex;
align-items: center;
padding: 0;
background: none;
border-left: none;
border-radius: 0;
border-bottom: 1px solid #f0f0f0;
padding-bottom: 12px;
.section-icon {
margin-right: 8px;
color: #07c05f;
font-size: 18px;
}
}
}
:deep(.ollama-summary-card) {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
padding: 20px;
margin-bottom: 20px;
}
:deep(.submit-section) {
margin-top: 20px;
text-align: center;
}
</style>

View File

@@ -311,11 +311,11 @@ onMounted(() => {
.tenant-info-container {
padding: 20px;
background-color: #fff;
border-radius: 8px;
margin: 0 20px 20px 20px;
height: calc(100vh - 40px);
margin: 0 20px 0 20px;
height: calc(100vh);
overflow-y: auto;
box-sizing: border-box;
flex: 1;
}
.tenant-header {
@@ -363,7 +363,7 @@ onMounted(() => {
.api-key-content,
.storage-content,
.doc-content {
// padding: 16px 0;
margin-top: 0;
}
.api-key-input {

View File

@@ -83,9 +83,7 @@ func NewInitializationHandler(
// InitializationRequest 初始化请求结构
type InitializationRequest struct {
// 前端传入的存储类型cos 或 minio
StorageType string `json:"storageType"`
LLM struct {
LLM struct {
Source string `json:"source" binding:"required"`
ModelName string `json:"modelName" binding:"required"`
BaseURL string `json:"baseUrl"`
@@ -115,7 +113,8 @@ type InitializationRequest struct {
APIKey string `json:"apiKey"`
InterfaceType string `json:"interfaceType"` // "ollama" or "openai"
} `json:"vlm,omitempty"`
COS *struct {
StorageType string `json:"storageType"`
COS *struct {
SecretID string `json:"secretId"`
SecretKey string `json:"secretKey"`
Region string `json:"region"`
@@ -210,7 +209,7 @@ func (h *InitializationHandler) Initialize(c *gin.Context) {
// 验证多模态配置
if req.Multimodal.Enabled {
storageType := strings.ToLower(req.StorageType)
storageType := strings.ToLower(req.Multimodal.StorageType)
if req.Multimodal.VLM == nil {
logger.Error(ctx, "Multimodal enabled but missing VLM configuration")
c.Error(errors.NewBadRequestError("启用多模态时需要配置VLM信息"))
@@ -502,11 +501,11 @@ func (h *InitializationHandler) Initialize(c *gin.Context) {
InterfaceType: req.Multimodal.VLM.InterfaceType,
},
}
switch req.StorageType {
switch req.Multimodal.StorageType {
case "cos":
if req.Multimodal.COS != nil {
kb.StorageConfig = types.StorageConfig{
Provider: req.StorageType,
Provider: req.Multimodal.StorageType,
BucketName: req.Multimodal.COS.BucketName,
AppID: req.Multimodal.COS.AppID,
PathPrefix: req.Multimodal.COS.PathPrefix,
@@ -518,7 +517,7 @@ func (h *InitializationHandler) Initialize(c *gin.Context) {
case "minio":
if req.Multimodal.Minio != nil {
kb.StorageConfig = types.StorageConfig{
Provider: req.StorageType,
Provider: req.Multimodal.StorageType,
BucketName: req.Multimodal.Minio.BucketName,
PathPrefix: req.Multimodal.Minio.PathPrefix,
SecretID: os.Getenv("MINIO_ACCESS_KEY_ID"),
@@ -560,11 +559,11 @@ func (h *InitializationHandler) Initialize(c *gin.Context) {
APIKey: req.Multimodal.VLM.APIKey,
InterfaceType: req.Multimodal.VLM.InterfaceType,
}
switch req.StorageType {
switch req.Multimodal.StorageType {
case "cos":
if req.Multimodal.COS != nil {
kb.StorageConfig = types.StorageConfig{
Provider: req.StorageType,
Provider: req.Multimodal.StorageType,
SecretID: req.Multimodal.COS.SecretID,
SecretKey: req.Multimodal.COS.SecretKey,
Region: req.Multimodal.COS.Region,
@@ -576,7 +575,7 @@ func (h *InitializationHandler) Initialize(c *gin.Context) {
case "minio":
if req.Multimodal.Minio != nil {
kb.StorageConfig = types.StorageConfig{
Provider: req.StorageType,
Provider: req.Multimodal.StorageType,
BucketName: req.Multimodal.Minio.BucketName,
PathPrefix: req.Multimodal.Minio.PathPrefix,
SecretID: os.Getenv("MINIO_ACCESS_KEY_ID"),