mirror of
https://github.com/Tencent/WeKnora.git
synced 2026-06-04 13:30:32 +08:00
feat(settings): refactor system settings management and UI enhancements
- Consolidated global system settings into the standard Settings modal, improving accessibility and user experience. - Updated routing to redirect system settings to the new modal structure, ensuring a seamless transition for users. - Enhanced UI components for better responsiveness and visual clarity, including adjustments to layout and styling. - Added visibility controls for system admin-specific settings, ensuring proper access management. - Refactored related comments and documentation for improved clarity on the new settings structure.
This commit is contained in:
@@ -116,11 +116,7 @@
|
||||
including tenant Owners. Real authorisation lives server-side
|
||||
(RequireSystemAdmin middleware); this is UI gating only.
|
||||
-->
|
||||
<div
|
||||
v-if="authStore.isSystemAdmin"
|
||||
class="menu-item"
|
||||
@click="handleSystemAdmin"
|
||||
>
|
||||
<div v-if="authStore.isSystemAdmin" class="menu-item" @click="handleSystemAdmin">
|
||||
<t-icon name="server" class="menu-icon" />
|
||||
<span>系统管理</span>
|
||||
</div>
|
||||
@@ -357,13 +353,13 @@ const handleSettings = () => {
|
||||
router.push('/platform/settings')
|
||||
}
|
||||
|
||||
// Open the platform-wide system administration area. Mirrors the
|
||||
// existing handleSettings flow: dismiss the menu, then route. There is
|
||||
// no UI store flag for system-admin (settings.vue uses one for its
|
||||
// modal-overlay pattern; system area is a regular routed page).
|
||||
// Open platform-wide settings inside the standard Settings modal.
|
||||
// Administrator-table workflows still live under /platform/system/admins,
|
||||
// but global tunables belong with the rest of the configuration surface.
|
||||
const handleSystemAdmin = () => {
|
||||
menuVisible.value = false
|
||||
router.push('/platform/system')
|
||||
uiStore.openSettings('system-global')
|
||||
router.push({ path: '/platform/settings', query: { section: 'system-global' } })
|
||||
}
|
||||
|
||||
// Hover-driven submenu controls. A small hide delay tolerates the pointer
|
||||
|
||||
@@ -148,15 +148,15 @@ const router = createRouter({
|
||||
path: "system",
|
||||
component: () => import("../views/system/SystemLayout.vue"),
|
||||
meta: { requiresInit: true, requiresAuth: true, requiresSystemAdmin: true },
|
||||
redirect: "/platform/system/settings",
|
||||
redirect: "/platform/system/admins",
|
||||
children: [
|
||||
{
|
||||
// P1 default landing page — the SystemAdmin lands here
|
||||
// because most platform-level operations involve tweaking
|
||||
// a setting, not promoting another admin.
|
||||
// Kept as a compatibility URL. Global settings now live in
|
||||
// the standard settings modal rather than a standalone
|
||||
// routed page.
|
||||
path: "settings",
|
||||
name: "systemSettings",
|
||||
component: () => import("../views/system/SystemSettings.vue"),
|
||||
redirect: { path: "/platform/settings", query: { section: "system-global" } },
|
||||
meta: { requiresInit: true, requiresAuth: true, requiresSystemAdmin: true }
|
||||
},
|
||||
{
|
||||
|
||||
@@ -68,7 +68,13 @@
|
||||
|
||||
<!-- 右侧内容区域 -->
|
||||
<div class="settings-content">
|
||||
<div class="content-wrapper" :class="{ 'content-wrapper--wide': currentSection === 'members' }">
|
||||
<div
|
||||
class="content-wrapper"
|
||||
:class="{
|
||||
'content-wrapper--wide': currentSection === 'members',
|
||||
'content-wrapper--full': currentSection === 'system-global',
|
||||
}"
|
||||
>
|
||||
<!-- 角色不允许访问当前 section(deep-link 进来 / 跨租户切换后角色降级)—— 优先于具体 section 渲染。
|
||||
正常导航走 navItems filter 不会到这里,但 watch(navItems) 的 fallback 会在角色降级
|
||||
的瞬间触发;这一段做兜底兼容旧 URL。 -->
|
||||
@@ -130,6 +136,11 @@
|
||||
<SystemInfo />
|
||||
</div>
|
||||
|
||||
<!-- 系统管理员可见的全局运行时设置 -->
|
||||
<div v-if="currentSection === 'system-global'" class="section">
|
||||
<SystemSettings />
|
||||
</div>
|
||||
|
||||
<!-- 用户信息(账户基础信息:ID / 用户名 / 邮箱 / 注册时间)。
|
||||
从 ApiInfo.vue 拆出来,原页面挂的是 owner-only 入口,
|
||||
用户的基本信息不该跟 owner 权限绑定。 -->
|
||||
@@ -187,6 +198,7 @@ import ParserEngineSettings from './ParserEngineSettings.vue'
|
||||
import StorageEngineSettings from './StorageEngineSettings.vue'
|
||||
import WeKnoraCloudSettings from './WeKnoraCloudSettings.vue'
|
||||
import TenantMembers from './TenantMembers.vue'
|
||||
import SystemSettings from '@/views/system/SystemSettings.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -244,7 +256,12 @@ const SECTION_MIN_ROLE: Record<string, RoleKey> = {
|
||||
api: 'owner',
|
||||
}
|
||||
|
||||
const SYSTEM_ADMIN_SECTIONS = new Set(['system-global'])
|
||||
|
||||
const canSeeSection = (key: string): boolean => {
|
||||
if (SYSTEM_ADMIN_SECTIONS.has(key)) {
|
||||
return authStore.isSystemAdmin
|
||||
}
|
||||
const min = SECTION_MIN_ROLE[key] ?? 'viewer'
|
||||
// canAccessAllTenants(superuser)和路由层一样必须 bypass,否则 cross-tenant
|
||||
// 管理员看不到自己有权操作的入口(参考 TenantMembers.vue 的 canManage)。
|
||||
@@ -268,6 +285,7 @@ const navItems = computed(() => {
|
||||
{ key: 'storage', icon: 'cloud', label: t('settings.storageEngine') },
|
||||
{ key: 'mcp', icon: 'tools', label: t('settings.mcpService') },
|
||||
{ key: 'system', icon: 'info-circle', label: t('settings.systemSettings') },
|
||||
{ key: 'system-global', icon: 'server', label: '全局设置' },
|
||||
{ key: 'userprofile', icon: 'user', label: t('userProfile.title') },
|
||||
{ key: 'tenant', icon: 'user-circle', label: t('settings.tenantInfo') },
|
||||
{ key: 'members', icon: 'usergroup', label: t('tenantMember.title') },
|
||||
@@ -310,7 +328,7 @@ const navGroups = computed<NavGroup[]>(() => {
|
||||
{
|
||||
key: 'platform',
|
||||
label: t('settings.navGroups.platform'),
|
||||
items: pickItems(['chathistory', 'system', 'api']),
|
||||
items: pickItems(['chathistory', 'system-global', 'system', 'api']),
|
||||
},
|
||||
].filter((group) => group.items.length > 0)
|
||||
})
|
||||
@@ -358,7 +376,11 @@ const handleClose = () => {
|
||||
uiStore.closeSettings()
|
||||
// 如果当前路由是设置页,返回上一页
|
||||
if (route.path === '/platform/settings') {
|
||||
router.back()
|
||||
if (route.query.section === 'system-global') {
|
||||
router.push('/platform/knowledge-bases')
|
||||
} else {
|
||||
router.back()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,6 +408,16 @@ watch(() => uiStore.settingsInitialSection, (section) => {
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
watch(
|
||||
() => [visible.value, route.query.section],
|
||||
([isVisible, section]) => {
|
||||
if (!isVisible || typeof section !== 'string') return
|
||||
currentSection.value = section
|
||||
currentSubSection.value = ''
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// 切换租户后角色可能变化,原本可见的 admin-only 面板可能消失。
|
||||
// 如果 currentSection 落到了不再显示的 key 上,就回退到第一个可见项。
|
||||
watch(navItems, (items) => {
|
||||
@@ -646,6 +678,13 @@ onUnmounted(() => {
|
||||
padding: 32px 36px 40px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
&--full {
|
||||
max-width: none;
|
||||
width: 100%;
|
||||
padding: 30px 34px 40px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.section {
|
||||
|
||||
@@ -5,10 +5,9 @@
|
||||
router; reaching this component means the caller is an authenticated
|
||||
SystemAdmin.
|
||||
|
||||
Sidebar is intentionally simple for the P0 milestone: only the
|
||||
"Administrators" page is wired. Future P1+ pages (global settings,
|
||||
built-in models, audit log, tenants overview) plug in here as new
|
||||
sidebar items + child routes.
|
||||
Sidebar is intentionally simple: global system settings live inside
|
||||
the standard Settings modal, while this routed area keeps workflows
|
||||
that need a table-oriented management surface.
|
||||
-->
|
||||
<div class="system-layout">
|
||||
<aside class="system-sidebar">
|
||||
@@ -46,7 +45,6 @@
|
||||
// When a new child route is added in router/index.ts under /platform/system,
|
||||
// add a matching entry here.
|
||||
const navItems = [
|
||||
{ name: 'systemSettings', label: '全局设置', icon: 'setting' },
|
||||
{ name: 'systemAdmins', label: '系统管理员', icon: 'user-shield' },
|
||||
]
|
||||
</script>
|
||||
|
||||
@@ -14,15 +14,13 @@
|
||||
<div class="system-settings">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">全局设置</h1>
|
||||
<h2 class="page-title">全局设置</h2>
|
||||
<p class="page-desc">
|
||||
平台级运行时配置。修改保存后立即生效(不需要重启服务)。
|
||||
所有变更会写入审计日志。
|
||||
平台级运行时配置,保存后立即生效。
|
||||
</p>
|
||||
</div>
|
||||
<t-button variant="text" @click="loadSettings" :loading="loading">
|
||||
<t-button shape="square" variant="text" @click="loadSettings" :loading="loading">
|
||||
<template #icon><t-icon name="refresh" /></template>
|
||||
刷新
|
||||
</t-button>
|
||||
</div>
|
||||
|
||||
@@ -35,19 +33,15 @@
|
||||
<div>暂无可配置的系统设置</div>
|
||||
</div>
|
||||
|
||||
<t-card
|
||||
<section
|
||||
v-for="group in groupedSettings"
|
||||
:key="group.category"
|
||||
class="settings-group"
|
||||
:bordered="false"
|
||||
:header-bordered="true"
|
||||
>
|
||||
<template #title>
|
||||
<div class="group-title">
|
||||
<span class="group-title-text">{{ categoryLabel(group.category) }}</span>
|
||||
<span class="group-title-count">{{ group.items.length }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="group-title">
|
||||
<span class="group-title-text">{{ categoryLabel(group.category) }}</span>
|
||||
<span class="group-title-count">{{ group.items.length }}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="item in group.items"
|
||||
@@ -129,16 +123,18 @@
|
||||
|
||||
<t-button
|
||||
theme="primary"
|
||||
size="small"
|
||||
variant="outline"
|
||||
shape="square"
|
||||
:loading="savingKey === item.key"
|
||||
:disabled="!isDirty(item)"
|
||||
@click="saveSetting(item)"
|
||||
title="保存"
|
||||
>
|
||||
保存
|
||||
<template #icon><t-icon name="save" /></template>
|
||||
</t-button>
|
||||
</div>
|
||||
</div>
|
||||
</t-card>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -350,7 +346,7 @@ onMounted(() => {
|
||||
|
||||
<style scoped>
|
||||
.system-settings {
|
||||
max-width: 980px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
@@ -358,12 +354,12 @@ onMounted(() => {
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0 0 6px;
|
||||
font-size: 22px;
|
||||
margin: 0 0 4px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--td-text-color-primary, #000);
|
||||
}
|
||||
@@ -371,7 +367,7 @@ onMounted(() => {
|
||||
.page-desc {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
line-height: 1.5;
|
||||
color: var(--td-text-color-secondary, #666);
|
||||
max-width: 720px;
|
||||
}
|
||||
@@ -388,34 +384,34 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.settings-group {
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.group-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--td-border-level-1-color, #eee);
|
||||
}
|
||||
|
||||
.group-title-text {
|
||||
font-size: 15px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--td-text-color-secondary, #666);
|
||||
}
|
||||
|
||||
.group-title-count {
|
||||
font-size: 12px;
|
||||
font-size: 11px;
|
||||
color: var(--td-text-color-placeholder, #999);
|
||||
background: var(--td-bg-color-component, #f5f5f5);
|
||||
padding: 1px 8px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.setting-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
padding: 14px 0;
|
||||
gap: 20px;
|
||||
padding: 15px 0;
|
||||
border-bottom: 1px solid var(--td-border-level-1-color, #eee);
|
||||
}
|
||||
|
||||
@@ -437,15 +433,15 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.setting-key-text {
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--td-text-color-primary, #000);
|
||||
font-family: var(--td-font-family-mono, monospace);
|
||||
}
|
||||
|
||||
.setting-desc {
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--td-text-color-secondary, #666);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
@@ -461,15 +457,31 @@ onMounted(() => {
|
||||
.setting-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.setting-input {
|
||||
width: 220px;
|
||||
width: 210px;
|
||||
}
|
||||
|
||||
.setting-input--wide {
|
||||
width: 420px;
|
||||
width: 340px;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.setting-row {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.setting-control {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.setting-input,
|
||||
.setting-input--wide {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user