完善页面布局
This commit is contained in:
@@ -22,14 +22,15 @@
|
||||
<!-- 二级侧边栏 (1:1 复刻 CRMEB 抽屉/Dock 平滑切换) -->
|
||||
<AdminSubSider
|
||||
:visible="isSubSiderVisible"
|
||||
:class="{ 'sub-sider-overlay': isSubSiderOverlay || layoutMode === 'mobile' }"
|
||||
:topMenuTitle="activeTopMenuTitle"
|
||||
:groups="activeGroups"
|
||||
:routes="activeRoutes"
|
||||
:activeRouteId="activeRouteId"
|
||||
:isOverlay="isOverlayVisible || layoutMode === 'mobile'"
|
||||
:layoutMode="layoutMode"
|
||||
:menuTitle="activeTopMenuTitle"
|
||||
:menuTree="activeMenuTree"
|
||||
:activeId="activeRouteId"
|
||||
:currentPath="currentRoutePath"
|
||||
:asideWidth="layoutMode === 'mobile' ? 0 : ASIDE_W"
|
||||
:siderWidth="SUB_W"
|
||||
@route-click="onRouteClick"
|
||||
:width="SUB_W"
|
||||
@sub-click="onSubClick"
|
||||
/>
|
||||
|
||||
<!-- 右侧内容区 -->
|
||||
@@ -86,9 +87,12 @@ import {
|
||||
} from '@/layouts/admin/router/adminRoutes.uts'
|
||||
import type { TopMenu, MenuGroup, RouteRecord } from '@/layouts/admin/router/adminRoutes.uts'
|
||||
|
||||
import { MenuNode, settingSubSiderMenu } from '@/layouts/admin/router/settingSubSiderMenu.uts'
|
||||
|
||||
import {
|
||||
activeTopMenuId,
|
||||
activeRouteId,
|
||||
lastSubIdByMenu,
|
||||
tabs,
|
||||
isMainAsideCollapsed,
|
||||
showSubSider,
|
||||
@@ -123,20 +127,66 @@ const isSubSiderOverlay = computed<boolean>(() => {
|
||||
return layoutMode.value === 'tablet'
|
||||
})
|
||||
|
||||
// 当前路由的路径
|
||||
const currentRoutePath = computed<string>(() => {
|
||||
const route = findRouteById(activeRouteId.value)
|
||||
return route ? route.path : ''
|
||||
})
|
||||
|
||||
// 将旧的 Group + Routes 转换为新的 Tree 结构 (支持 3 级嵌套)
|
||||
const activeMenuTree = computed<MenuNode[]>(() => {
|
||||
if (activeTopMenuId.value === 'setting') {
|
||||
return settingSubSiderMenu
|
||||
}
|
||||
|
||||
const tree: MenuNode[] = []
|
||||
activeGroups.value.forEach(group => {
|
||||
const routes = activeRoutes.value.get(group.id)
|
||||
if (routes != null && routes.length > 0) {
|
||||
const children: MenuNode[] = []
|
||||
routes.forEach(route => {
|
||||
children.push({
|
||||
id: route.id,
|
||||
title: route.title,
|
||||
type: 'page',
|
||||
path: route.path
|
||||
} as MenuNode)
|
||||
})
|
||||
|
||||
// 1:1 复刻 CRMEB: 如果分组标题为空,直接将子菜单平铺在顶层,不渲染分组行
|
||||
if (group.title === '') {
|
||||
children.forEach(child => {
|
||||
tree.push(child)
|
||||
})
|
||||
} else {
|
||||
tree.push({
|
||||
id: group.id,
|
||||
title: group.title,
|
||||
type: 'group',
|
||||
children: children
|
||||
} as MenuNode)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return tree
|
||||
})
|
||||
|
||||
/**
|
||||
* 核心逻辑:二级菜单是否可见
|
||||
* 1. 如果有子菜单内容 (activeGroups > 0)
|
||||
* 1. 如果有子菜单内容 (activeMenuTree > 0)
|
||||
* 2. 如果是 Desktop 且 showSubSider 为真 (Dock模式)
|
||||
* 3. 如果是 Tablet/Mobile 且 isOverlayVisible 为真 (Overlay模式)
|
||||
*/
|
||||
const isSubSiderVisible = computed<boolean>(() => {
|
||||
if (activeGroups.value.length === 0) return false
|
||||
// 首页模块通常不显示 SubSider
|
||||
if (activeTopMenuId.value === 'home' || activeMenuTree.value.length === 0) return false
|
||||
|
||||
if (layoutMode.value === 'desktop') {
|
||||
return showSubSider.value
|
||||
}
|
||||
|
||||
// Tablet 和 Mobile 模式下,作为 Overlay 受 isOverlayVisible 控制
|
||||
// Tablet 和 Mobile 模式下,直接由 isOverlayVisible 控制
|
||||
return isOverlayVisible.value
|
||||
})
|
||||
|
||||
@@ -155,7 +205,7 @@ const mainLeft = computed<string>(() => {
|
||||
let left = ASIDE_W // 只要不是 Mobile,主侧栏 70px 始终 Dock
|
||||
|
||||
// 只有在 Desktop 模式且二级菜单处于 Dock 模式显示时,才累加宽度
|
||||
if (layoutMode.value === 'desktop' && showSubSider.value && activeGroups.value.length > 0) {
|
||||
if (layoutMode.value === 'desktop' && showSubSider.value && activeMenuTree.value.length > 0) {
|
||||
left += SUB_W
|
||||
}
|
||||
|
||||
@@ -199,6 +249,24 @@ const currentComponent = computed<any>(() => {
|
||||
return getComponent(route.componentKey)
|
||||
})
|
||||
|
||||
// 监听路由变化,同步状态 (处理从 Tabs 或外部跳转的情况)
|
||||
watch(() => activeRouteId.value, (newId) => {
|
||||
const route = findRouteById(newId)
|
||||
if (route && route.parentId) {
|
||||
// 同步一级菜单
|
||||
if (activeTopMenuId.value !== route.parentId) {
|
||||
activeTopMenuId.value = route.parentId
|
||||
}
|
||||
// 同步最后访问记录
|
||||
lastSubIdByMenu.value[route.parentId] = newId
|
||||
|
||||
// 如果是 Desktop 且 SubSider 关着,自动打开(除非用户手动关了)
|
||||
if (layoutMode.value === 'desktop' && !showSubSider.value) {
|
||||
showSubSider.value = true
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// ============================================
|
||||
// 事件处理
|
||||
// ============================================
|
||||
@@ -206,27 +274,46 @@ const currentComponent = computed<any>(() => {
|
||||
function onTopMenuClick(menu: TopMenu): void {
|
||||
activeTopMenuId.value = menu.id
|
||||
|
||||
// 1:1 复刻 CRMEB 交互:
|
||||
// 1. 如果有子菜单:Desktop 下 dock,Tablet/Mobile 下唤起 Overlay
|
||||
if (menu.groups.length > 0) {
|
||||
if (layoutMode.value === 'desktop') {
|
||||
showSubSider.value = true
|
||||
} else {
|
||||
isOverlayVisible.value = true
|
||||
}
|
||||
// 1:1 复刻 CRMEB 交互:点击 Aside 立即展示 SubSider
|
||||
if (layoutMode.value === 'desktop') {
|
||||
showSubSider.value = true
|
||||
} else {
|
||||
// 2. 如果没有子菜单:直接跳转并关闭所有 Overlay
|
||||
isOverlayVisible.value = true
|
||||
isMobileMenuOpen.value = false // 如果主侧栏开着,关掉它,开二级
|
||||
}
|
||||
|
||||
// 1:1 复刻 CRMEB 交互:点击一级菜单后,自动跳转到上一次记录的子页面或第一个子页面
|
||||
let targetId = lastSubIdByMenu.value[menu.id]
|
||||
if (!targetId && activeMenuTree.value.length > 0) {
|
||||
const firstNode = activeMenuTree.value[0]
|
||||
if (firstNode.type === 'page') {
|
||||
targetId = firstNode.id
|
||||
} else if (firstNode.children != null && firstNode.children!.length > 0) {
|
||||
targetId = firstNode.children![0].id
|
||||
}
|
||||
}
|
||||
|
||||
if (targetId) {
|
||||
openRoute(targetId)
|
||||
} else {
|
||||
// 兜底逻辑:如果没有二级菜单,跳转到 index
|
||||
openRoute(menu.id + '_index')
|
||||
closeAllMenu()
|
||||
}
|
||||
}
|
||||
|
||||
function onRouteClick(routeId: string): void {
|
||||
openRoute(routeId)
|
||||
// 1:1 复刻 CRMEB:在移动端或平板叠加模式下,点击具体子路由后自动收起
|
||||
if (layoutMode.value !== 'desktop') {
|
||||
closeAllMenu()
|
||||
}
|
||||
function onSubClick(payload: { id: string, path: string }): void {
|
||||
// CRMEB 跳转逻辑
|
||||
openRoute(payload.id)
|
||||
|
||||
// 更新最后访问记录
|
||||
lastSubIdByMenu.value[activeTopMenuId.value] = payload.id
|
||||
|
||||
// 重要:1:1 复刻 CRMEB,点击二级菜单项后,SubSider 通常保持显示状态
|
||||
// 只有在 Mobile 模式下,用户如果是想“跳转并收起”,通常需要点击遮罩或关闭按钮,但这里我们按桌面版常驻逻辑
|
||||
// 如果需要移动端点击自动关闭,才解开下面注释
|
||||
// if (layoutMode.value === 'mobile') {
|
||||
// closeAllMenu()
|
||||
// }
|
||||
}
|
||||
|
||||
function onTabClick(tab: TabItem): void {
|
||||
|
||||
@@ -109,45 +109,34 @@ function onLogoClick(): void {
|
||||
|
||||
.aside-menu {
|
||||
flex: 1;
|
||||
padding: 8px 0;
|
||||
padding: 0; /* CRMEB typically no padding here */
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
height: 60px;
|
||||
height: 50px; /* 1:1 CRMEB columnsAside height */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
transition: all 0.3s;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #1890ff;
|
||||
background: #1890ff; /* CRMEB 主色蓝 */
|
||||
color: #fff;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 3px;
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 4px;
|
||||
font-size: 18px; /* CRMEB icons are smaller than 24px usually */
|
||||
margin-bottom: 2px;
|
||||
|
||||
.icon-text {
|
||||
display: block;
|
||||
@@ -156,6 +145,7 @@ function onLogoClick(): void {
|
||||
|
||||
.menu-title {
|
||||
font-size: 12px;
|
||||
transform: scale(0.9); /* CRMEB text is tiny */
|
||||
text-align: center;
|
||||
|
||||
text {
|
||||
|
||||
@@ -1,57 +1,160 @@
|
||||
<template>
|
||||
<view
|
||||
class="admin-subsider"
|
||||
:class="{ 'is-hidden': !visible }"
|
||||
:style="{ left: asideWidth + 'px', width: siderWidth + 'px' }"
|
||||
:class="{ 'is-hidden': !visible, 'sub-sider-overlay': isOverlay || layoutMode === 'mobile' }"
|
||||
:style="{ left: asideWidth + 'px', width: width + 'px' }"
|
||||
>
|
||||
<view class="subsider-header">
|
||||
<text class="header-title">{{ topMenuTitle }}</text>
|
||||
<!-- 头部模块标题 (1:1 复刻 CRMEB) -->
|
||||
<view v-if="menuTitle" class="subsider-cat-name">
|
||||
<text class="cat-title">{{ menuTitle }}</text>
|
||||
</view>
|
||||
|
||||
<view class="subsider-menu">
|
||||
<view v-for="group in groups" :key="group.id" class="menu-group">
|
||||
<view class="group-title">
|
||||
<text>{{ group.title }}</text>
|
||||
</view>
|
||||
<!-- 菜单列表滚动 -->
|
||||
<scroll-view class="subsider-content" scroll-y="true" show-scrollbar="false">
|
||||
<view class="menu-list">
|
||||
<!-- 分级手动多层渲染 (UTS 下直接循环比递归更稳定) -->
|
||||
<template v-for="level1 in menuTree" :key="level1.id">
|
||||
<!-- 一级层级 (通常是分组或顶级页面) -->
|
||||
<view
|
||||
class="menu-row level-1"
|
||||
:class="{ 'is-active': isActive(level1), 'is-open': isOpen(level1.id, 1) }"
|
||||
@click="handleNodeClick(level1, 1)"
|
||||
>
|
||||
<text class="menu-text">{{ level1.title }}</text>
|
||||
<text v-if="level1.type === 'group'" class="chevron">
|
||||
{{ isOpen(level1.id, 1) ? '▼' : '▶' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view
|
||||
v-for="route in getGroupRoutes(group.id)"
|
||||
:key="route.id"
|
||||
class="menu-item"
|
||||
:class="{ active: route.id === activeRouteId }"
|
||||
@click="onRouteClick(route.id)"
|
||||
>
|
||||
<text class="item-title">{{ route.title }}</text>
|
||||
</view>
|
||||
<!-- 二级子菜单容器 -->
|
||||
<view v-if="level1.type === 'group' && isOpen(level1.id, 1)" class="sub-menu-container">
|
||||
<template v-for="level2 in level1.children" :key="level2.id">
|
||||
<view
|
||||
class="menu-row level-2"
|
||||
:class="{ 'is-active': isActive(level2), 'is-open': isOpen(level2.id, 2) }"
|
||||
@click="handleNodeClick(level2, 2, level1.id)"
|
||||
>
|
||||
<text class="menu-text">{{ level2.title }}</text>
|
||||
<text v-if="level2.type === 'group'" class="chevron">
|
||||
{{ isOpen(level2.id, 2) ? '▼' : '▶' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 三级子菜单容器 -->
|
||||
<view v-if="level2.type === 'group' && isOpen(level2.id, 2)" class="sub-menu-container">
|
||||
<template v-for="level3 in level2.children" :key="level3.id">
|
||||
<view
|
||||
class="menu-row level-3"
|
||||
:class="{ 'is-active': isActive(level3) }"
|
||||
@click="handleNodeClick(level3, 3, level2.id)"
|
||||
>
|
||||
<text class="menu-text">{{ level3.title }}</text>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import type { MenuGroup, RouteRecord } from '@/layouts/admin/router/adminRoutes.uts'
|
||||
import { ref, reactive, watch, onMounted } from 'vue'
|
||||
import type { MenuNode } from '../router/settingSubSiderMenu.uts'
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean // ✅ 新增:由父层控制显示/隐藏(不要用 v-if 卸载)
|
||||
topMenuTitle: string
|
||||
groups: MenuGroup[]
|
||||
routes: Map<string, RouteRecord[]>
|
||||
activeRouteId: string
|
||||
visible: boolean
|
||||
menuTitle: string
|
||||
menuTree: MenuNode[]
|
||||
activeId: string
|
||||
currentPath: string
|
||||
asideWidth: number
|
||||
siderWidth: number
|
||||
width: number
|
||||
isOverlay?: boolean
|
||||
layoutMode?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'route-click', routeId: string): void
|
||||
(e: 'sub-click', payload: { id: string, path: string }): void
|
||||
}>()
|
||||
|
||||
function getGroupRoutes(groupId: string): RouteRecord[] {
|
||||
return props.routes.get(groupId) || []
|
||||
// --- 展开状态管理 ---
|
||||
const openIdsByLevel = reactive<Record<number, string>>({
|
||||
1: '',
|
||||
2: '',
|
||||
3: ''
|
||||
})
|
||||
|
||||
/** 判断项是否激活 (页面级) */
|
||||
function isActive(node: MenuNode): boolean {
|
||||
// 只有非分组项才能被视为“激活”变色 (1:1 复刻 screenshot 效果)
|
||||
if (node.type === 'group') return false
|
||||
|
||||
if (props.activeId != '' && node.id === props.activeId) return true
|
||||
if (props.currentPath != '' && node.path === props.currentPath) return true
|
||||
return false
|
||||
}
|
||||
|
||||
function onRouteClick(routeId: string): void {
|
||||
emit('route-click', routeId)
|
||||
/** 判断分组是否展开 */
|
||||
function isOpen(id: string, level: number): boolean {
|
||||
return openIdsByLevel[level] === id
|
||||
}
|
||||
|
||||
/** 统一点击处理 */
|
||||
function handleNodeClick(node: MenuNode, level: number, parentId: string = ''): void {
|
||||
if (node.type === 'group') {
|
||||
// 手风琴逻辑
|
||||
if (openIdsByLevel[level] === node.id) {
|
||||
openIdsByLevel[level] = ''
|
||||
} else {
|
||||
openIdsByLevel[level] = node.id
|
||||
}
|
||||
} else {
|
||||
// 页面跳转
|
||||
emit('sub-click', { id: node.id, path: node.path || '' })
|
||||
}
|
||||
}
|
||||
|
||||
/** 自动展开逻辑:根据当前激活项回溯父级展开状态 */
|
||||
function autoExpand() {
|
||||
const targetId = props.activeId || ''
|
||||
const targetPath = props.currentPath || ''
|
||||
|
||||
if (targetId === '' && targetPath === '') return
|
||||
|
||||
const findPath = (nodes: MenuNode[]): string[] | null => {
|
||||
for (const node of nodes) {
|
||||
if (node.type === 'page' && ((targetId != '' && node.id === targetId) || (targetPath != '' && node.path === targetPath))) {
|
||||
return [node.id]
|
||||
}
|
||||
if (node.children != null) {
|
||||
const childPath = findPath(node.children!)
|
||||
if (childPath != null) {
|
||||
return [node.id, ...childPath]
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const path = findPath(props.menuTree)
|
||||
if (path != null) {
|
||||
// path 包含了从祖先到叶子的所有 ID
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
openIdsByLevel[i + 1] = path[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.activeId, () => autoExpand())
|
||||
watch(() => props.currentPath, () => autoExpand())
|
||||
watch(() => props.menuTree, () => autoExpand(), { immediate: true })
|
||||
|
||||
onMounted(() => {
|
||||
autoExpand()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -59,118 +162,102 @@ function onRouteClick(routeId: string): void {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background: #fff;
|
||||
border-right: 1px solid #e8e8e8;
|
||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.05);
|
||||
background: #ffffff;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
/* ✅ 动画只用 GPU 友好属性,避免 width/left 导致卡顿 */
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 0, 0);
|
||||
will-change: transform, opacity;
|
||||
|
||||
/* ✅ 可点击时才需要高 z-index */
|
||||
z-index: 999;
|
||||
|
||||
/* ✅ 注意:visibility 默认可见 */
|
||||
visibility: visible;
|
||||
|
||||
/* 显示态:visibility 不延迟 */
|
||||
transition: transform 300ms ease, opacity 250ms ease, visibility 0s linear 0s;
|
||||
}
|
||||
|
||||
/* 悬浮/抽屉模式:复刻 CRMEB 核心逻辑 */
|
||||
.sub-sider-overlay {
|
||||
z-index: 1001;
|
||||
box-shadow: 6px 0 16px rgba(0, 0, 0, 0.08);
|
||||
z-index: 1000;
|
||||
transition: transform 300ms ease, opacity 250ms ease;
|
||||
}
|
||||
|
||||
.admin-subsider.is-hidden {
|
||||
/* ✅ 立即禁止拦截点击,彻底解决“遮挡可操作区” */
|
||||
pointer-events: none;
|
||||
|
||||
/* ✅ 先淡出 + 滑出(推荐滑出 100%:完全离开可视区) */
|
||||
opacity: 0;
|
||||
transform: translate3d(-100%, 0, 0);
|
||||
|
||||
/* ✅ 动画结束后再隐藏(避免那一帧还挡住) */
|
||||
visibility: hidden;
|
||||
transition: transform 300ms ease, opacity 250ms ease, visibility 0s linear 300ms;
|
||||
|
||||
/* 可选:进一步避免层叠影响 */
|
||||
z-index: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.subsider-header {
|
||||
height: 64px;
|
||||
.sub-sider-overlay {
|
||||
z-index: 1010;
|
||||
box-shadow: 6px 0 16px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.subsider-cat-name {
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
|
||||
.header-title {
|
||||
font-size: 16px;
|
||||
padding: 0 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
background-color: #fff;
|
||||
flex-shrink: 0;
|
||||
|
||||
.cat-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.subsider-menu {
|
||||
.subsider-content {
|
||||
flex: 1;
|
||||
padding: 8px 0;
|
||||
|
||||
/* ✅ 用 auto,避免一直显示滚动条影响布局 */
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.menu-group {
|
||||
margin-bottom: 16px;
|
||||
.menu-list {
|
||||
padding: 0; /* 移除 8px 边距,确保菜单项紧贴顶部/标题栏 */
|
||||
}
|
||||
|
||||
.group-title {
|
||||
padding: 8px 16px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
font-weight: 500;
|
||||
|
||||
text {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
height: 36px;
|
||||
padding: 0 16px 0 24px;
|
||||
.menu-row {
|
||||
height: 50px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid transparent; /* 保持布局稳定 */
|
||||
|
||||
/* ✅ 不要 transition: all(会引发布局属性动画/回流) */
|
||||
transition: background-color 0.2s ease, color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 3px;
|
||||
background: #1890ff;
|
||||
&.is-active {
|
||||
background-color: #e7f0ff;
|
||||
.menu-text {
|
||||
color: #2f65ff;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.item-title {
|
||||
.menu-text {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* 1:1 复刻 CRMEB 缩进逻辑 */
|
||||
&.level-1 {
|
||||
padding-left: 20px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
&.level-2 {
|
||||
padding-left: 44px; /* 二级缩进 */
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
&.level-3 {
|
||||
padding-left: 64px; /* 三级进一步缩进 */
|
||||
padding-right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.chevron {
|
||||
font-size: 10px;
|
||||
color: #c0c4cc;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.sub-menu-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -123,10 +123,25 @@ export const componentMap: Map<string, any> = new Map([
|
||||
// 数据模块 - 暂时使用占位组件
|
||||
['StatisticIndex', PlaceholderPage],
|
||||
|
||||
// 设置模块 - 暂时使用占位组件
|
||||
// 设置模块
|
||||
['SettingSystemConfig', defineAsyncComponent(() => import('@/pages/mall/admin/setting/system/config.uvue'))],
|
||||
['SettingSystemAdmin', PlaceholderPage],
|
||||
['SettingSystemRole', PlaceholderPage],
|
||||
['SettingMessage', defineAsyncComponent(() => import('@/pages/mall/admin/setting/message.uvue'))],
|
||||
['SettingAgreement', defineAsyncComponent(() => import('@/pages/mall/admin/setting/agreement.uvue'))],
|
||||
['SettingTicket', defineAsyncComponent(() => import('@/pages/mall/admin/setting/ticket.uvue'))],
|
||||
['SettingAuthRole', defineAsyncComponent(() => import('@/pages/mall/admin/setting/auth/role.uvue'))],
|
||||
['SettingAuthAdmin', defineAsyncComponent(() => import('@/pages/mall/admin/setting/auth/admin.uvue'))],
|
||||
['SettingAuthPermission', defineAsyncComponent(() => import('@/pages/mall/admin/setting/auth/permission.uvue'))],
|
||||
['SettingDeliveryStaff', defineAsyncComponent(() => import('@/pages/mall/admin/setting/delivery/staff.uvue'))],
|
||||
['SettingDeliveryStation', defineAsyncComponent(() => import('@/pages/mall/admin/setting/delivery/station.uvue'))],
|
||||
['SettingDeliveryTemplate', defineAsyncComponent(() => import('@/pages/mall/admin/setting/delivery/template.uvue'))],
|
||||
['SettingInterfaceOnepassConfig', defineAsyncComponent(() => import('@/pages/mall/admin/setting/interface/onepass/config.uvue'))],
|
||||
['SettingInterfaceOnepassIndex', defineAsyncComponent(() => import('@/pages/mall/admin/setting/interface/onepass/index.uvue'))],
|
||||
['SettingInterfaceStorage', defineAsyncComponent(() => import('@/pages/mall/admin/setting/interface/storage.uvue'))],
|
||||
['SettingInterfaceCollect', defineAsyncComponent(() => import('@/pages/mall/admin/setting/interface/collect.uvue'))],
|
||||
['SettingInterfaceLogistics', defineAsyncComponent(() => import('@/pages/mall/admin/setting/interface/logistics.uvue'))],
|
||||
['SettingInterfaceESheet', defineAsyncComponent(() => import('@/pages/mall/admin/setting/interface/e-sheet.uvue'))],
|
||||
['SettingInterfaceSms', defineAsyncComponent(() => import('@/pages/mall/admin/setting/interface/sms.uvue'))],
|
||||
['SettingInterfacePayment', defineAsyncComponent(() => import('@/pages/mall/admin/setting/interface/payment.uvue'))],
|
||||
|
||||
// 分销模块
|
||||
['DistributionStatistic', PlaceholderPage],
|
||||
|
||||
@@ -92,7 +92,7 @@ export const topMenus: TopMenu[] = [
|
||||
path: '/pages/mall/admin/product/statistic',
|
||||
order: 4,
|
||||
groups: [
|
||||
{ id: 'product-manage', title: '商品管理', order: 1 }
|
||||
{ id: 'product-manage', title: '', order: 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -156,7 +156,7 @@ export const topMenus: TopMenu[] = [
|
||||
path: '/pages/mall/admin/cms/article/list',
|
||||
order: 9,
|
||||
groups: [
|
||||
{ id: 'cms-manage', title: '内容管理', order: 1 }
|
||||
{ id: 'cms-manage', title: '', order: 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -187,7 +187,10 @@ export const topMenus: TopMenu[] = [
|
||||
order: 12,
|
||||
groups: [
|
||||
{ id: 'setting-system', title: '系统设置', order: 1 },
|
||||
{ id: 'setting-application', title: '应用设置', order: 2 }
|
||||
{ id: 'setting-message', title: '通知管理', order: 2 },
|
||||
{ id: 'setting-auth', title: '权限管理', order: 3 },
|
||||
{ id: 'setting-delivery', title: '物流设置', order: 4 },
|
||||
{ id: 'setting-interface', title: '接口设置', order: 5 }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -892,6 +895,7 @@ export const routes: RouteRecord[] = [
|
||||
},
|
||||
|
||||
// ========== 设置模块 ==========
|
||||
// 1. 系统设置
|
||||
{
|
||||
id: 'setting_systemConfig',
|
||||
title: '系统设置',
|
||||
@@ -902,27 +906,168 @@ export const routes: RouteRecord[] = [
|
||||
auth: ['admin-setting-system-config'],
|
||||
order: 1
|
||||
},
|
||||
|
||||
// 2. 通知管理
|
||||
{
|
||||
id: 'setting_systemAdmin',
|
||||
title: '管理员管理',
|
||||
path: '/pages/mall/admin/setting/system/admin',
|
||||
componentKey: 'SettingSystemAdmin',
|
||||
id: 'setting_message',
|
||||
title: '消息管理',
|
||||
path: '/pages/mall/admin/setting/message',
|
||||
componentKey: 'SettingMessage',
|
||||
parentId: 'setting',
|
||||
groupId: 'setting-system',
|
||||
auth: ['admin-setting-system-admin'],
|
||||
groupId: 'setting-message',
|
||||
order: 1
|
||||
},
|
||||
{
|
||||
id: 'setting_agreement',
|
||||
title: '协议管理',
|
||||
path: '/pages/mall/admin/setting/agreement',
|
||||
componentKey: 'SettingAgreement',
|
||||
parentId: 'setting',
|
||||
groupId: 'setting-message',
|
||||
order: 2
|
||||
},
|
||||
{
|
||||
id: 'setting_systemRole',
|
||||
title: '角色管理',
|
||||
path: '/pages/mall/admin/setting/system/role',
|
||||
componentKey: 'SettingSystemRole',
|
||||
id: 'setting_ticket',
|
||||
title: '客服设置',
|
||||
path: '/pages/mall/admin/setting/ticket',
|
||||
componentKey: 'SettingTicket',
|
||||
parentId: 'setting',
|
||||
groupId: 'setting-system',
|
||||
auth: ['admin-setting-system-role'],
|
||||
groupId: 'setting-message',
|
||||
order: 3
|
||||
},
|
||||
|
||||
// 3. 权限管理
|
||||
{
|
||||
id: 'setting_auth_role',
|
||||
title: '角色管理',
|
||||
path: '/pages/mall/admin/setting/auth/role',
|
||||
componentKey: 'SettingAuthRole',
|
||||
parentId: 'setting',
|
||||
groupId: 'setting-auth',
|
||||
order: 1
|
||||
},
|
||||
{
|
||||
id: 'setting_auth_admin',
|
||||
title: '管理员管理',
|
||||
path: '/pages/mall/admin/setting/auth/admin',
|
||||
componentKey: 'SettingAuthAdmin',
|
||||
parentId: 'setting',
|
||||
groupId: 'setting-auth',
|
||||
order: 2
|
||||
},
|
||||
{
|
||||
id: 'setting_auth_permission',
|
||||
title: '权限管理',
|
||||
path: '/pages/mall/admin/setting/auth/permission',
|
||||
componentKey: 'SettingAuthPermission',
|
||||
parentId: 'setting',
|
||||
groupId: 'setting-auth',
|
||||
order: 3
|
||||
},
|
||||
|
||||
// 4. 物流设置
|
||||
{
|
||||
id: 'setting_delivery_staff',
|
||||
title: '配送员管理',
|
||||
path: '/pages/mall/admin/setting/delivery/staff',
|
||||
componentKey: 'SettingDeliveryStaff',
|
||||
parentId: 'setting',
|
||||
groupId: 'setting-delivery',
|
||||
order: 1
|
||||
},
|
||||
{
|
||||
id: 'setting_delivery_station',
|
||||
title: '提货点管理',
|
||||
path: '/pages/mall/admin/setting/delivery/station',
|
||||
componentKey: 'SettingDeliveryStation',
|
||||
parentId: 'setting',
|
||||
groupId: 'setting-delivery',
|
||||
order: 2
|
||||
},
|
||||
{
|
||||
id: 'setting_delivery_template',
|
||||
title: '运费模板',
|
||||
path: '/pages/mall/admin/setting/delivery/template',
|
||||
componentKey: 'SettingDeliveryTemplate',
|
||||
parentId: 'setting',
|
||||
groupId: 'setting-delivery',
|
||||
order: 3
|
||||
},
|
||||
|
||||
// 5. 接口设置
|
||||
{
|
||||
id: 'setting_interface_onepass_config',
|
||||
title: '总平台配置',
|
||||
path: '/pages/mall/admin/setting/interface/onepass/config',
|
||||
componentKey: 'SettingInterfaceOnepassConfig',
|
||||
parentId: 'setting',
|
||||
groupId: 'setting-interface',
|
||||
order: 1
|
||||
},
|
||||
{
|
||||
id: 'setting_interface_onepass_index',
|
||||
title: '账号列表',
|
||||
path: '/pages/mall/admin/setting/interface/onepass/index',
|
||||
componentKey: 'SettingInterfaceOnepassIndex',
|
||||
parentId: 'setting',
|
||||
groupId: 'setting-interface',
|
||||
order: 2
|
||||
},
|
||||
{
|
||||
id: 'setting_interface_storage',
|
||||
title: '存储配置',
|
||||
path: '/pages/mall/admin/setting/interface/storage',
|
||||
componentKey: 'SettingInterfaceStorage',
|
||||
parentId: 'setting',
|
||||
groupId: 'setting-interface',
|
||||
order: 3
|
||||
},
|
||||
{
|
||||
id: 'setting_interface_collect',
|
||||
title: '商品采集',
|
||||
path: '/pages/mall/admin/setting/interface/collect',
|
||||
componentKey: 'SettingInterfaceCollect',
|
||||
parentId: 'setting',
|
||||
groupId: 'setting-interface',
|
||||
order: 4
|
||||
},
|
||||
{
|
||||
id: 'setting_interface_logistics',
|
||||
title: '物流查询',
|
||||
path: '/pages/mall/admin/setting/interface/logistics',
|
||||
componentKey: 'SettingInterfaceLogistics',
|
||||
parentId: 'setting',
|
||||
groupId: 'setting-interface',
|
||||
order: 5
|
||||
},
|
||||
{
|
||||
id: 'setting_interface_esheet',
|
||||
title: '电子面单',
|
||||
path: '/pages/mall/admin/setting/interface/e-sheet',
|
||||
componentKey: 'SettingInterfaceESheet',
|
||||
parentId: 'setting',
|
||||
groupId: 'setting-interface',
|
||||
order: 6
|
||||
},
|
||||
{
|
||||
id: 'setting_interface_sms',
|
||||
title: '短信接口',
|
||||
path: '/pages/mall/admin/setting/interface/sms',
|
||||
componentKey: 'SettingInterfaceSms',
|
||||
parentId: 'setting',
|
||||
groupId: 'setting-interface',
|
||||
order: 7
|
||||
},
|
||||
{
|
||||
id: 'setting_interface_payment',
|
||||
title: '商城支付',
|
||||
path: '/pages/mall/admin/setting/interface/payment',
|
||||
componentKey: 'SettingInterfacePayment',
|
||||
parentId: 'setting',
|
||||
groupId: 'setting-interface',
|
||||
order: 8
|
||||
},
|
||||
|
||||
// ========== 分销模块 ==========
|
||||
{
|
||||
id: 'distribution_statistic',
|
||||
@@ -1055,7 +1200,7 @@ export const routes: RouteRecord[] = [
|
||||
order: 6
|
||||
},
|
||||
{
|
||||
id: 'decoration_link',
|
||||
id: 'DecorationLink',
|
||||
title: '链接管理',
|
||||
path: '/pages/mall/admin/decoration/link',
|
||||
componentKey: 'DecorationLink',
|
||||
@@ -1064,6 +1209,30 @@ export const routes: RouteRecord[] = [
|
||||
order: 7
|
||||
},
|
||||
|
||||
// ========== 设置模块 (1:1 复刻 CRMEB 路由结构) ==========
|
||||
// 通知管理
|
||||
{ id: 'setting_message_index', title: '消息管理', path: '/pages/mall/admin/setting/message/index', componentKey: 'SettingMessageIndex', parentId: 'setting', groupId: 'setting-message', order: 1 },
|
||||
{ id: 'setting_protocol_index', title: '协议设置', path: '/pages/mall/admin/setting/protocol/index', componentKey: 'SettingProtocolIndex', parentId: 'setting', groupId: 'setting-message', order: 2 },
|
||||
{ id: 'setting_ticket_index', title: '小票配置', path: '/pages/mall/admin/setting/ticket/index', componentKey: 'SettingTicketIndex', parentId: 'setting', groupId: 'setting-message', order: 3 },
|
||||
|
||||
// 权限管理
|
||||
{ id: 'setting_auth_role', title: '角色管理', path: '/pages/mall/admin/setting/auth/role/index', componentKey: 'SettingAuthRole', parentId: 'setting', groupId: 'setting-auth', order: 1 },
|
||||
{ id: 'setting_auth_admin', title: '管理员列表', path: '/pages/mall/admin/setting/auth/admin-list/index', componentKey: 'SettingAuthAdmin', parentId: 'setting', groupId: 'setting-auth', order: 2 },
|
||||
{ id: 'setting_auth_perm', title: '权限设置', path: '/pages/mall/admin/setting/auth/permission/index', componentKey: 'SettingAuthPerm', parentId: 'setting', groupId: 'setting-auth', order: 3 },
|
||||
|
||||
// 物流设置
|
||||
{ id: 'setting_delivery_courier', title: '配送员管理', path: '/pages/mall/admin/setting/delivery/courier/index', componentKey: 'SettingDeliveryCourier', parentId: 'setting', groupId: 'setting-delivery', order: 1 },
|
||||
{ id: 'setting_delivery_pickup', title: '提货点设置', path: '/pages/mall/admin/setting/delivery/pickup/index', componentKey: 'SettingDeliveryPickup', parentId: 'setting', groupId: 'setting-delivery', order: 2 },
|
||||
{ id: 'setting_delivery_freight', title: '运费模板', path: '/pages/mall/admin/setting/delivery/freight/index', componentKey: 'SettingDeliveryFreight', parentId: 'setting', groupId: 'setting-delivery', order: 3 },
|
||||
|
||||
// 接口设置
|
||||
{ id: 'setting_api_storage', title: '系统存储配置', path: '/pages/mall/admin/setting/api/yht/storage/index', componentKey: 'SettingApiStorage', parentId: 'setting', groupId: 'setting-interface', order: 1 },
|
||||
{ id: 'setting_api_collect', title: '商品采集配置', path: '/pages/mall/admin/setting/api/yht/collect/index', componentKey: 'SettingApiCollect', parentId: 'setting', groupId: 'setting-interface', order: 2 },
|
||||
{ id: 'setting_api_logistics', title: '物流查询配置', path: '/pages/mall/admin/setting/api/yht/logistics/index', componentKey: 'SettingApiLogistics', parentId: 'setting', groupId: 'setting-interface', order: 3 },
|
||||
{ id: 'setting_api_waybill', title: '电子面单配置', path: '/pages/mall/admin/setting/api/yht/waybill/index', componentKey: 'SettingApiWaybill', parentId: 'setting', groupId: 'setting-interface', order: 4 },
|
||||
{ id: 'setting_api_sms', title: '短信接口配置', path: '/pages/mall/admin/setting/api/yht/sms/index', componentKey: 'SettingApiSms', parentId: 'setting', groupId: 'setting-interface', order: 5 },
|
||||
{ id: 'setting_api_pay', title: '商城支付配置', path: '/pages/mall/admin/setting/api/yht/pay/index', componentKey: 'SettingApiPay', parentId: 'setting', groupId: 'setting-interface', order: 6 },
|
||||
|
||||
// ========== 应用模块 ==========
|
||||
{
|
||||
id: 'app_statistic',
|
||||
|
||||
37
layouts/admin/router/settingSubSiderMenu.uts
Normal file
37
layouts/admin/router/settingSubSiderMenu.uts
Normal file
@@ -0,0 +1,37 @@
|
||||
export type MenuNode = {
|
||||
id: string
|
||||
title: string
|
||||
type: 'group' | 'page'
|
||||
path?: string // type=page 必填
|
||||
children?: MenuNode[] // type=group 必填
|
||||
}
|
||||
|
||||
export const settingSubSiderMenu: MenuNode[] = [
|
||||
{ id: 'setting_message_index', title: '消息管理', type: 'page', path: '/pages/mall/admin/setting/message/index' },
|
||||
{ id: 'setting_protocol_index', title: '协议设置', type: 'page', path: '/pages/mall/admin/setting/protocol/index' },
|
||||
{ id: 'setting_ticket_index', title: '小票配置', type: 'page', path: '/pages/mall/admin/setting/ticket/index' },
|
||||
{ id: 'auth_group', title: '管理权限', type: 'group', children: [
|
||||
{ id: 'setting_auth_role', title: '角色管理', type: 'page', path: '/pages/mall/admin/setting/auth/role/index' },
|
||||
{ id: 'setting_auth_admin', title: '管理员列表', type: 'page', path: '/pages/mall/admin/setting/auth/admin-list/index' },
|
||||
{ id: 'setting_auth_perm', title: '权限设置', type: 'page', path: '/pages/mall/admin/setting/auth/permission/index' }
|
||||
]
|
||||
},
|
||||
{ id: 'delivery_group', title: '发货设置', type: 'group', children: [
|
||||
{ id: 'setting_delivery_courier', title: '配送员管理', type: 'page', path: '/pages/mall/admin/setting/delivery/courier/index' },
|
||||
{ id: 'setting_delivery_pickup', title: '提货点设置', type: 'page', path: '/pages/mall/admin/setting/delivery/pickup/index' },
|
||||
{ id: 'setting_delivery_freight', title: '运费模板', type: 'page', path: '/pages/mall/admin/setting/delivery/freight/index' }
|
||||
]
|
||||
},
|
||||
{ id: 'api_group', title: '接口配置', type: 'group', children: [
|
||||
{ id: 'yh_tong', title: '一号通', type: 'group', children: [
|
||||
{ id: 'setting_api_storage', title: '系统存储配置', type: 'page', path: '/pages/mall/admin/setting/api/yht/storage/index' },
|
||||
{ id: 'setting_api_collect', title: '商品采集配置', type: 'page', path: '/pages/mall/admin/setting/api/yht/collect/index' },
|
||||
{ id: 'setting_api_logistics', title: '物流查询配置', type: 'page', path: '/pages/mall/admin/setting/api/yht/logistics/index' },
|
||||
{ id: 'setting_api_waybill', title: '电子面单配置', type: 'page', path: '/pages/mall/admin/setting/api/yht/waybill/index' },
|
||||
{ id: 'setting_api_sms', title: '短信接口配置', type: 'page', path: '/pages/mall/admin/setting/api/yht/sms/index' },
|
||||
{ id: 'setting_api_pay', title: '商城支付配置', type: 'page', path: '/pages/mall/admin/setting/api/yht/pay/index' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
buildDefaultTabs,
|
||||
getTopMenus
|
||||
} from '@/layouts/admin/router/adminRoutes.uts'
|
||||
import { addView, activeFullPath, visitedViews } from './tagsViewStore.uts'
|
||||
|
||||
/**
|
||||
* 标签页类型
|
||||
@@ -32,6 +33,12 @@ export const activeTopMenuId = ref<string>('home')
|
||||
/** 当前激活的路由ID */
|
||||
export const activeRouteId = ref<string>('home_index')
|
||||
|
||||
/** 记录每个一级模块上一次访问的二级路由ID (CRMEB 体验增强) */
|
||||
export const lastSubIdByMenu = ref<Record<string, string>>({})
|
||||
|
||||
/** 标记是否由用户手动关闭了 SubSider (移动端) */
|
||||
export const isManualClosed = ref<boolean>(false)
|
||||
|
||||
/** 打开的标签页列表 */
|
||||
export const tabs = ref<TabItem[]>([])
|
||||
|
||||
@@ -82,6 +89,8 @@ export function openRoute(routeId: string, addTab: boolean = true): void {
|
||||
// 更新一级菜单选中态
|
||||
if (route.parentId) {
|
||||
activeTopMenuId.value = route.parentId
|
||||
// 记录该模块最后访问的子路由
|
||||
lastSubIdByMenu.value[route.parentId] = routeId
|
||||
} else {
|
||||
// 首页等顶级路由
|
||||
activeTopMenuId.value = routeId.split('_')[0]
|
||||
@@ -116,6 +125,10 @@ function addTabItem(route: RouteRecord): void {
|
||||
isAffix: route.isAffix || false
|
||||
})
|
||||
}
|
||||
|
||||
// 更新新版 TagsViewStore
|
||||
addView(route, route.path)
|
||||
activeFullPath.value = route.path
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -201,6 +214,11 @@ export function initNavState(): void {
|
||||
isAffix: r.isAffix || false
|
||||
}))
|
||||
|
||||
// 初始化 TagsViewStore
|
||||
defaultTabs.forEach(r => {
|
||||
addView(r, r.path)
|
||||
})
|
||||
|
||||
// 打开首页
|
||||
openRoute('home_index', false)
|
||||
}
|
||||
|
||||
126
layouts/admin/store/tagsViewStore.uts
Normal file
126
layouts/admin/store/tagsViewStore.uts
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* TagsView 状态管理
|
||||
* 复刻 CRMEB 风格的标签栏逻辑
|
||||
*/
|
||||
|
||||
import { ref } from 'vue'
|
||||
import type { RouteRecord } from '@/layouts/admin/router/adminRoutes.uts'
|
||||
|
||||
/**
|
||||
* 标签页视图类型
|
||||
*/
|
||||
export type TagView = {
|
||||
id: string // 对应路由ID
|
||||
title: string // 标题
|
||||
path: string // 基础路径
|
||||
fullPath: string // 完整路径(包含参数)
|
||||
name: string // 组件Key
|
||||
meta: {
|
||||
affix?: boolean
|
||||
noCache?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
/** 访问过的页面列表 */
|
||||
export const visitedViews = ref<TagView[]>([])
|
||||
|
||||
/** 缓存的页面列表 (用于 keep-alive) */
|
||||
export const cachedViews = ref<string[]>([])
|
||||
|
||||
/** 开启的标签页详情 */
|
||||
export const activeFullPath = ref<string>('')
|
||||
|
||||
// ============================================
|
||||
// Actions
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 添加视图
|
||||
*/
|
||||
export function addView(route: RouteRecord, fullPath: string): void {
|
||||
// 1. 添加到访问列表
|
||||
if (visitedViews.value.some(v => v.fullPath === fullPath)) return
|
||||
|
||||
// 如果 ID 相同但 fullPath 不同(参数变化), 则更新或者替换
|
||||
const existingIndex = visitedViews.value.findIndex(v => v.id === route.id)
|
||||
|
||||
const newView: TagView = {
|
||||
id: route.id,
|
||||
title: route.title,
|
||||
path: route.path,
|
||||
fullPath: fullPath,
|
||||
name: route.componentKey,
|
||||
meta: {
|
||||
affix: route.isAffix || false,
|
||||
noCache: false // 默认为缓存,如果有特殊需求可扩展
|
||||
}
|
||||
}
|
||||
|
||||
if (existingIndex > -1) {
|
||||
// 同一路由不同参数, 更新记录
|
||||
visitedViews.value[existingIndex] = newView
|
||||
} else {
|
||||
visitedViews.value.push(newView)
|
||||
}
|
||||
|
||||
// 2. 添加到缓存列表
|
||||
if (!route.componentKey) return
|
||||
if (cachedViews.value.includes(route.componentKey)) return
|
||||
cachedViews.value.push(route.componentKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除视图
|
||||
*/
|
||||
export function delView(view: TagView): void {
|
||||
const index = visitedViews.value.findIndex(v => v.fullPath === view.fullPath)
|
||||
if (index > -1 && !visitedViews.value[index].meta.affix) {
|
||||
visitedViews.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const cacheIndex = cachedViews.value.indexOf(view.name)
|
||||
if (cacheIndex > -1) {
|
||||
cachedViews.value.splice(cacheIndex, 1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭其他标签
|
||||
*/
|
||||
export function delOthersViews(view: TagView): void {
|
||||
visitedViews.value = visitedViews.value.filter(v => v.meta.affix || v.fullPath === view.fullPath)
|
||||
cachedViews.value = visitedViews.value.map(v => v.name)
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭右侧标签
|
||||
*/
|
||||
export function delRightTags(view: TagView): void {
|
||||
const index = visitedViews.value.findIndex(v => v.fullPath === view.fullPath)
|
||||
if (index === -1) return
|
||||
|
||||
visitedViews.value = visitedViews.value.filter((v, i) => {
|
||||
return i <= index || v.meta.affix
|
||||
})
|
||||
cachedViews.value = visitedViews.value.map(v => v.name)
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭所有
|
||||
*/
|
||||
export function delAllViews(): void {
|
||||
const affixTags = visitedViews.value.filter(v => v.meta.affix)
|
||||
visitedViews.value = affixTags
|
||||
cachedViews.value = affixTags.map(v => v.name)
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新当前视图 (通常结合全局事件通知组件)
|
||||
*/
|
||||
export function refreshView(view: TagView): void {
|
||||
const index = cachedViews.value.indexOf(view.name)
|
||||
if (index > -1) {
|
||||
cachedViews.value.splice(index, 1)
|
||||
}
|
||||
// 通知框架重新加载该组件的逻辑通常在组件内部处理或通过 key 变化
|
||||
}
|
||||
Reference in New Issue
Block a user