b70557a077
- 单设备登录:登录成功即 last-login-wins 绑定 users.machine;非超管旧 token 在下一次受保护请求抛 4011 下线,超管豁免;仅认 token 内签名 deviceId,不用请求头。 前端两端接入踢下线跳转(?kicked=1 提示),V118 清空历史 machine - 站内通知:新增 notification 模块(任务失败扫描 / 密钥欠费 / 下游服务探测三类来源), 前后台铃铛组件 + 轮询;后台列表按主管数据范围(UserDataScopeSupport)过滤;V116 建表 均已于 2026-09-13 部署上线,此次补提交源码(此前仅存在于已部署 JAR/构建产物中)
134 lines
4.9 KiB
Vue
134 lines
4.9 KiB
Vue
<script setup lang="ts">
|
|
import { computed, watch } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
import { useAdminSessionStore } from '@/stores/admin-session'
|
|
import { joinAdminPath } from '@/config/app'
|
|
import { pageTitleOf, topbarUserOf } from '@/layout/topbar-model'
|
|
import { groupKeysForActive, toSidebarEntries } from '@/layout/menu-mapper'
|
|
import {
|
|
EMPTY_MENU_DESCRIPTION,
|
|
EMPTY_MENU_TITLE,
|
|
shouldShowEmptyMenu,
|
|
} from '@/layout/empty-state'
|
|
import GlobalErrorContainer from '@/layout/GlobalErrorContainer.vue'
|
|
import NotificationBell from '@/components/NotificationBell.vue'
|
|
import OperationGuide from '@/layout/OperationGuide.vue'
|
|
import { crumbsForActiveRoute, resolveDocumentTitle, updateDocumentTitle } from '@/layout/title-breadcrumb'
|
|
import {
|
|
LOGOUT_CONFIRM_CANCEL,
|
|
LOGOUT_CONFIRM_MESSAGE,
|
|
LOGOUT_CONFIRM_OK,
|
|
LOGOUT_CONFIRM_TITLE,
|
|
runLogout,
|
|
} from '@/layout/logout'
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const session = useAdminSessionStore()
|
|
|
|
const activePath = computed(() => route.path)
|
|
const menuEntries = computed(() => toSidebarEntries(session.menuTree))
|
|
const openedKeys = computed(() => groupKeysForActive(menuEntries.value, route.path))
|
|
const emptyMenu = computed(() => shouldShowEmptyMenu(menuEntries.value.length > 0, session.loading, session.initialized))
|
|
const pageTitle = computed(() => pageTitleOf(route.meta.title))
|
|
const userVm = computed(() => topbarUserOf(session.user))
|
|
const breadcrumbs = computed(() => crumbsForActiveRoute(session.menuTree, route.path))
|
|
const logoUrl = joinAdminPath('assets', 'logo.jpg')
|
|
|
|
watch(
|
|
pageTitle,
|
|
(title) => updateDocumentTitle(resolveDocumentTitle(title), typeof document !== 'undefined' ? document : null),
|
|
{ immediate: true },
|
|
)
|
|
|
|
async function signOut() {
|
|
try {
|
|
await runLogout({
|
|
confirm: () =>
|
|
ElMessageBox.confirm(LOGOUT_CONFIRM_MESSAGE, LOGOUT_CONFIRM_TITLE, {
|
|
confirmButtonText: LOGOUT_CONFIRM_OK,
|
|
cancelButtonText: LOGOUT_CONFIRM_CANCEL,
|
|
type: 'warning',
|
|
}).then(
|
|
() => true,
|
|
() => false,
|
|
),
|
|
signOut: () => session.signOut(),
|
|
})
|
|
} catch (error) {
|
|
ElMessage.error(error instanceof Error ? error.message : '退出登录失败')
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="admin-shell">
|
|
<a class="skip-link" href="#admin-content">跳到主内容</a>
|
|
<aside class="admin-sidebar" role="navigation" aria-label="侧边栏菜单">
|
|
<div class="admin-brand">
|
|
<img :src="logoUrl" alt="数富AI" class="admin-brand-logo" />
|
|
<div class="admin-brand-copy">
|
|
<strong>数富AI</strong>
|
|
<span>电商运营管理后台</span>
|
|
</div>
|
|
</div>
|
|
|
|
<el-scrollbar class="admin-menu-scroll">
|
|
<el-menu :default-active="activePath" :default-openeds="openedKeys" router class="admin-menu">
|
|
<template v-for="entry in menuEntries" :key="entry.key">
|
|
<el-sub-menu v-if="entry.kind === 'group'" :index="entry.key">
|
|
<template #title>
|
|
<span class="menu-group-title">{{ entry.name }}</span>
|
|
</template>
|
|
<el-menu-item v-for="item in entry.children" :key="item.key" :index="item.route">
|
|
{{ item.name }}
|
|
</el-menu-item>
|
|
</el-sub-menu>
|
|
<el-menu-item v-else :key="entry.key" :index="entry.route">
|
|
{{ entry.name }}
|
|
</el-menu-item>
|
|
</template>
|
|
</el-menu>
|
|
<el-empty v-if="emptyMenu" :description="EMPTY_MENU_TITLE" :image-size="72" />
|
|
</el-scrollbar>
|
|
</aside>
|
|
|
|
<section class="admin-main">
|
|
<header class="admin-topbar">
|
|
<div class="admin-topbar-title">
|
|
<span class="admin-kicker">运营管理控制台</span>
|
|
<h1>{{ pageTitle }}</h1>
|
|
</div>
|
|
<div class="admin-user">
|
|
<NotificationBell />
|
|
<div class="admin-user-meta">
|
|
<strong>{{ userVm.username }}</strong>
|
|
</div>
|
|
<span v-if="userVm.role" class="admin-user-role">{{ userVm.role }}</span>
|
|
<el-button class="admin-user-logout" @click="signOut">退出登录</el-button>
|
|
</div>
|
|
</header>
|
|
<main id="admin-content" class="admin-content">
|
|
<el-breadcrumb v-if="breadcrumbs.length > 1" class="admin-breadcrumb" separator="/">
|
|
<el-breadcrumb-item v-for="crumb in breadcrumbs" :key="crumb.key">{{ crumb.name }}</el-breadcrumb-item>
|
|
</el-breadcrumb>
|
|
<OperationGuide />
|
|
<GlobalErrorContainer />
|
|
<el-alert v-if="session.error" :title="session.error" type="error" show-icon :closable="false" />
|
|
<el-empty
|
|
v-if="emptyMenu && route.path === '/'"
|
|
:title="EMPTY_MENU_TITLE"
|
|
:description="EMPTY_MENU_DESCRIPTION"
|
|
:image-size="96"
|
|
/>
|
|
<RouterView v-else />
|
|
</main>
|
|
</section>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.admin-topbar-title { min-width: 0; }
|
|
</style>
|