feat(ui): 添加服务应用切换记忆功能并优化菜单结构

- 在IM、授权、推送、版本管理视图中添加最近使用应用的记忆功能
- 新增serviceApp工具函数用于存储和获取最近使用的应用
- 将系统日志、数据库、操作日志菜单项归类到运维管理子菜单
- 修复实体类索引字段命名不一致问题
- 在安全配置中启用方法级别安全注解支持
这个提交包含在:
XuqmGroup 2026-05-28 10:53:13 +08:00
父节点 db3158d2e4
当前提交 62966dcf20
共有 6 个文件被更改,包括 73 次插入13 次删除

查看文件

@ -0,0 +1,17 @@
const PREFIX = 'xqm_last_service_app_'
export function saveLastServiceApp(service: string, appKey: string) {
try {
localStorage.setItem(`${PREFIX}${service}`, appKey)
} catch {
// ignore
}
}
export function getLastServiceApp(service: string): string | null {
try {
return localStorage.getItem(`${PREFIX}${service}`)
} catch {
return null
}
}

查看文件

@ -820,6 +820,7 @@ import { appApi, type App } from '@/api/app'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search } from '@element-plus/icons-vue'
import { formatTime } from '@/utils/date'
import { getLastServiceApp, saveLastServiceApp } from '@/utils/serviceApp'
import {
imAdminApi,
type GlobalMute,
@ -1346,6 +1347,7 @@ function resetMessageSearch() {
}
function switchApp(val: string) {
saveLastServiceApp('im', val)
router.push(`/services/im/${val}`)
}
@ -2002,7 +2004,14 @@ watch(appKey, (key) => {
onMounted(() => {
if (isServicesPortal.value) {
appApi.list().then(res => { portalApps.value = res.data.data })
appApi.list().then(res => {
portalApps.value = res.data.data
if (!appKey.value && portalApps.value.length) {
const saved = getLastServiceApp('im')
const target = saved && portalApps.value.some(a => a.appKey === saved) ? saved : portalApps.value[0].appKey
switchApp(target)
}
})
if (!appKey.value) return
checkServiceEnabled(appKey.value)
return

查看文件

@ -23,9 +23,12 @@
<el-menu-item index="/services/license"><el-icon><Key /></el-icon><span></span></el-menu-item>
</el-sub-menu>
<el-menu-item index="/security"><el-icon><Lock /></el-icon><span></span></el-menu-item>
<el-sub-menu index="ops">
<template #title><el-icon><Setting /></el-icon><span></span></template>
<el-menu-item v-if="isPrivateDeploy" index="/system-logs"><el-icon><Monitor /></el-icon><span></span></el-menu-item>
<el-menu-item v-if="isPrivateDeploy" index="/database"><el-icon><Coin /></el-icon><span></span></el-menu-item>
<el-menu-item index="/operation-logs"><el-icon><Document /></el-icon><span></span></el-menu-item>
</el-sub-menu>
<el-menu-item v-if="auth.user?.type === 'MAIN'" index="/accounts"><el-icon><User /></el-icon><span></span></el-menu-item>
</el-menu>
</el-aside>
@ -60,9 +63,12 @@
<el-menu-item index="/services/license"><el-icon><Key /></el-icon><span></span></el-menu-item>
</el-sub-menu>
<el-menu-item index="/security"><el-icon><Lock /></el-icon><span></span></el-menu-item>
<el-sub-menu index="ops">
<template #title><el-icon><Setting /></el-icon><span></span></template>
<el-menu-item v-if="isPrivateDeploy" index="/system-logs"><el-icon><Monitor /></el-icon><span></span></el-menu-item>
<el-menu-item v-if="isPrivateDeploy" index="/database"><el-icon><Coin /></el-icon><span></span></el-menu-item>
<el-menu-item index="/operation-logs"><el-icon><Document /></el-icon><span></span></el-menu-item>
</el-sub-menu>
<el-menu-item v-if="auth.user?.type === 'MAIN'" index="/accounts"><el-icon><User /></el-icon><span></span></el-menu-item>
</el-menu>
</el-drawer>
@ -110,7 +116,7 @@
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useAuthStore } from '@/stores/auth'
import { useRoute, useRouter } from 'vue-router'
import { Bell, ChatDotRound, Coin, Document, Grid, Key, List, Lock, Menu, Monitor, Odometer, Upload, User } from '@element-plus/icons-vue'
import { Bell, ChatDotRound, Coin, Document, Grid, Key, List, Lock, Menu, Monitor, Odometer, Setting, Upload, User } from '@element-plus/icons-vue'
import { getDeploymentStatus } from '@/api/system'
const auth = useAuthStore()
@ -120,9 +126,12 @@ const isMobile = ref(false)
const drawerVisible = ref(false)
const isPrivateDeploy = ref(false)
const openedMenus = computed(() =>
route.path.startsWith('/services/') ? ['services'] : [],
)
const openedMenus = computed(() => {
const menus: string[] = []
if (route.path.startsWith('/services/')) menus.push('services')
if (['/system-logs', '/database', '/operation-logs'].includes(route.path)) menus.push('ops')
return menus
})
function updateViewport() {
isMobile.value = window.innerWidth < 768

查看文件

@ -151,6 +151,7 @@ import { ElMessage, ElMessageBox } from 'element-plus'
import { licenseApi, type AppLicense, type LicenseDevice } from '@/api/license'
import { appApi, type App } from '@/api/app'
import { formatTime } from '@/utils/date'
import { getLastServiceApp, saveLastServiceApp } from '@/utils/serviceApp'
import {
connectServiceActivationRealtime,
disconnectServiceActivationRealtime,
@ -304,6 +305,7 @@ async function submitActivation() {
}
function switchApp(key: string) {
saveLastServiceApp('license', key)
router.push(`/services/license/${key}`)
}
@ -342,6 +344,11 @@ onMounted(() => {
appApi.list().then(res => {
portalApps.value = res.data.data || []
currentApp.value = portalApps.value.find(item => item.appKey === appKey.value) ?? currentApp.value
if (!appKey.value && portalApps.value.length) {
const saved = getLastServiceApp('license')
const target = saved && portalApps.value.some(a => a.appKey === saved) ? saved : portalApps.value[0].appKey
switchApp(target)
}
})
if (appKey.value) checkServiceEnabled(appKey.value)
} else {

查看文件

@ -174,6 +174,7 @@ import { ElMessage } from 'element-plus'
import { appApi, type App } from '@/api/app'
import { pushAdminApi, type DeviceLoginLog, type TestPushResult, type UserPushStatus } from '@/api/push'
import { formatTime } from '@/utils/date'
import { getLastServiceApp, saveLastServiceApp } from '@/utils/serviceApp'
import {
connectServiceActivationRealtime,
disconnectServiceActivationRealtime,
@ -243,6 +244,7 @@ const logsTotal = ref(0)
const logsTotalPages = ref(0)
function switchApp(val: string) {
saveLastServiceApp('push', val)
router.push(`/services/push/${val}`)
}
@ -326,7 +328,14 @@ watch(appKey, (key) => {
onMounted(() => {
if (isServicesPortal.value) {
appApi.list().then(res => { portalApps.value = res.data.data })
appApi.list().then(res => {
portalApps.value = res.data.data
if (!appKey.value && portalApps.value.length) {
const saved = getLastServiceApp('push')
const target = saved && portalApps.value.some(a => a.appKey === saved) ? saved : portalApps.value[0].appKey
switchApp(target)
}
})
if (appKey.value) {
checkServiceEnabled()
}

查看文件

@ -939,6 +939,7 @@ import { CircleCheckFilled, Delete, Document, Edit, Loading, UploadFilled, Warni
import { appApi, type App } from '@/api/app'
import { fileApi } from '@/api/file'
import { formatTime } from '@/utils/date'
import { getLastServiceApp, saveLastServiceApp } from '@/utils/serviceApp'
import {
updateAdminApi,
type AppPackageInspectResult,
@ -1359,6 +1360,7 @@ async function loadStoreConfigs() {
}
function switchApp(val: string) {
saveLastServiceApp('update', val)
router.push(`/services/update/${val}`)
}
@ -2488,7 +2490,14 @@ onMounted(() => {
updateViewport()
window.addEventListener('resize', updateViewport)
if (isServicesPortal.value) {
appApi.list().then(res => { portalApps.value = res.data.data })
appApi.list().then(res => {
portalApps.value = res.data.data
if (!appKey.value && portalApps.value.length) {
const saved = getLastServiceApp('update')
const target = saved && portalApps.value.some(a => a.appKey === saved) ? saved : portalApps.value[0].appKey
switchApp(target)
}
})
if (!appKey.value) return
checkServiceEnabled()
}