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:
wizardchen
2026-05-24 22:07:50 +08:00
committed by lyingbug
parent d074dc067a
commit 554ba9ef74
5 changed files with 104 additions and 59 deletions

View File

@@ -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

View File

@@ -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 }
},
{

View File

@@ -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',
}"
>
<!-- 角色不允许访问当前 sectiondeep-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'
// canAccessAllTenantssuperuser和路由层一样必须 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 {

View File

@@ -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>

View File

@@ -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>