diff --git a/fe/src/views/components/HomeHeader/HomeHeader.js b/fe/src/views/components/HomeHeader/HomeHeader.js index cd8f5dd..c22ff3a 100644 --- a/fe/src/views/components/HomeHeader/HomeHeader.js +++ b/fe/src/views/components/HomeHeader/HomeHeader.js @@ -41,6 +41,16 @@ export function usePage() { window.removeEventListener('resize', handleResize); }); + const settingsMenu = computed(() => [ + { key: 'security', label: lang.security, component: SecuritySettings, capability: 'password.change_own' }, + { key: 'third-party', label: '第三方授权', component: ThirdPartyAuthorization, capability: 'plugin.write_own' }, + { key: 'config', label: '配置管理', component: ConfigSettings, capability: 'config.read' }, + { key: 'group', label: lang.group_settings, component: GroupSettings, capability: 'user.read' }, + { key: 'rule', label: lang.rule_setting, component: RuleSettings, capability: 'user.read' }, + { key: 'users', label: lang.user_management, component: UserManagement, capability: 'user.manage' }, + { key: 'plugins', label: lang.plugin_settings, component: PluginSettings, capability: 'plugin.read' }, + ].filter(item => globalStatus.hasCapability(item.capability))) + return { EpMenu, ref, @@ -60,9 +70,7 @@ export function usePage() { userInfos, isMobile, handleResize, - canReadConfig: computed(() => globalStatus.hasCapability("config.read")), - canManageUsers: computed(() => globalStatus.hasCapability("user.manage")), - canReadPlugins: computed(() => globalStatus.hasCapability("plugin.read")) + settingsMenu } } diff --git a/fe/src/views/components/HomeHeader/HomeHeader.vue b/fe/src/views/components/HomeHeader/HomeHeader.vue index 00fed78..ce5108e 100644 --- a/fe/src/views/components/HomeHeader/HomeHeader.vue +++ b/fe/src/views/components/HomeHeader/HomeHeader.vue @@ -20,26 +20,8 @@ - - - - - - - - - - - - - - - - - - - - + + @@ -63,9 +45,7 @@ const { globalStatus, isMobile, lang, - canReadConfig, - canManageUsers, - canReadPlugins + settingsMenu } = usePage() diff --git a/fe/src/views/components/SecuritySettings/SecuritySettings.css b/fe/src/views/components/SecuritySettings/SecuritySettings.css index 9cf7058..1985d46 100644 --- a/fe/src/views/components/SecuritySettings/SecuritySettings.css +++ b/fe/src/views/components/SecuritySettings/SecuritySettings.css @@ -1,4 +1,39 @@ .settings-card { padding: 0; } +.security-workspace { + display: grid; + grid-template-columns: 176px minmax(0, 1fr); + align-items: start; + gap: 20px; +} +.security-submenu { + position: sticky; + top: 0; + display: grid; + gap: 6px; + padding: 8px; + border: 1px solid var(--pm-border-color); + border-radius: var(--pm-radius-lg); + background: var(--pm-bg-secondary); +} +.security-submenu-item { + width: 100%; + padding: 12px 14px; + color: var(--pm-text-secondary); + text-align: left; + font: inherit; + border: 0; + border-radius: var(--pm-radius-sm); + background: transparent; + cursor: pointer; + transition: color .2s ease, background-color .2s ease; +} +.security-submenu-item:hover, +.security-submenu-item.active { + color: var(--pm-primary-color); + background: var(--el-color-primary-light-9); +} +.security-submenu-item.active { font-weight: 600; } +.security-main { min-width: 0; } .settings-header { margin-bottom: 24px; } .settings-header h3 { font-size: 18px; font-weight: 600; color: var(--pm-text-primary); margin: 0 0 8px; } .settings-desc { font-size: 14px; color: var(--pm-text-secondary); margin: 0; } @@ -20,6 +55,19 @@ .credential-warning { width: 100%; color: #b45309 !important; font-size: 13px; } .credential-actions { display: flex; flex: none; gap: 8px; } @media (max-width: 640px) { + .security-workspace { grid-template-columns: 1fr; gap: 16px; } + .security-submenu { + position: static; + display: flex; + gap: 6px; + overflow-x: auto; + padding: 6px; + } + .security-submenu-item { + flex: 0 0 auto; + width: auto; + white-space: nowrap; + } .security-key-header, .credential-item { flex-direction: column; align-items: stretch; } .credential-actions { width: 100%; } .credential-actions .el-button { flex: 1; } diff --git a/fe/src/views/components/SecuritySettings/SecuritySettings.js b/fe/src/views/components/SecuritySettings/SecuritySettings.js index 492bc03..9488547 100644 --- a/fe/src/views/components/SecuritySettings/SecuritySettings.js +++ b/fe/src/views/components/SecuritySettings/SecuritySettings.js @@ -1,4 +1,4 @@ -import { computed, onMounted, reactive, ref } from 'vue' +import { computed, onMounted, reactive, ref, watch } from 'vue' import { ElMessageBox, ElNotification } from 'element-plus' @@ -11,6 +11,33 @@ export function usePage() { const globalStatus = useGlobalStatusStore() const isSuperAdmin = computed(() => globalStatus.role === 'super_admin') const supported = webAuthnSupported() + const sectionOptions = computed(() => { + const options = [] + if (!isSuperAdmin.value) { + options.push({ value: 'password', label: lang.modify_pwd }) + } + options.push({ value: 'question', label: '密保问题' }) + if (!isSuperAdmin.value) { + options.push({ value: 'recovery', label: '离线恢复码' }) + } + options.push({ value: 'external-email', label: '外部安全邮箱' }) + options.push({ value: 'webauthn', label: '安全密钥' }) + options.push({ value: 'logout', label: lang.logout }) + return options + }) + const selectedSection = ref('question') + + const selectSection = value => { + if (sectionOptions.value.some(item => item.value === value)) { + selectedSection.value = value + } + } + + watch(sectionOptions, options => { + if (!options.some(item => item.value === selectedSection.value)) { + selectedSection.value = options[0]?.value || '' + } + }, { immediate: true }) const registering = ref(false) const credentialsLoading = ref(false) @@ -233,7 +260,8 @@ export function usePage() { }) return { - lang, isSuperAdmin, supported, registering, credentialsLoading, credentials, securityLoading, + lang, isSuperAdmin, supported, sectionOptions, selectedSection, selectSection, + registering, credentialsLoading, credentials, securityLoading, securityQuestion, questionForm, recoveryConfigured, generatedCodes, externalEmails, externalLoading, bindForm, ruleForm, rules, submit, registerCredential, renameCredential, revokeCredential, saveSecurityQuestion, generateRecoveryCodes, revokeRecoveryCodes, bindExternalEmail, diff --git a/fe/src/views/components/SecuritySettings/SecuritySettings.vue b/fe/src/views/components/SecuritySettings/SecuritySettings.vue index 590f4d0..dcacfc4 100644 --- a/fe/src/views/components/SecuritySettings/SecuritySettings.vue +++ b/fe/src/views/components/SecuritySettings/SecuritySettings.vue @@ -1,6 +1,22 @@ @@ -77,7 +95,8 @@ import { usePage } from './SecuritySettings.js' const { - lang, isSuperAdmin, supported, registering, credentialsLoading, credentials, securityLoading, + lang, isSuperAdmin, supported, sectionOptions, selectedSection, selectSection, + registering, credentialsLoading, credentials, securityLoading, securityQuestion, questionForm, recoveryConfigured, generatedCodes, externalEmails, externalLoading, bindForm, ruleForm, rules, submit, registerCredential, renameCredential, revokeCredential, saveSecurityQuestion, generateRecoveryCodes, revokeRecoveryCodes, bindExternalEmail, diff --git a/server/config/config.json b/server/config/config.json index d2592a0..f963de0 100644 --- a/server/config/config.json +++ b/server/config/config.json @@ -1,8 +1,8 @@ { "logLevel": "debug", - "domain": "test.domain", + "domain": "xuans.com", "domains": null, - "webDomain": "mail.test.domain", + "webDomain": "xuans.com", "dkimPrivateKeyPath": "config/dkim/dkim.priv", "sslType": "1", "SSLPrivateKeyPath": "./config/ssl/private.key", @@ -14,4 +14,4 @@ "httpPort": 80, "httpsPort": 443, "isInit": true -} \ No newline at end of file +}