merge: branch 'huangzhenbao-admin' into comclib-analytics, keeping local RPC integration versions
This commit is contained in:
@@ -1,8 +1,17 @@
|
||||
<template>
|
||||
<view class="layout-root">
|
||||
<!-- 主侧边栏 (CRMEB风格) -->
|
||||
<!-- 统一遮罩层 (复刻 CRMEB: 用于所有 Overlay 状态) -->
|
||||
<view
|
||||
class="mobile-mask"
|
||||
:class="{ 'mask-show': isOverlayVisible || (isMobile && isMobileMenuOpen) }"
|
||||
@click="closeAllMenu"
|
||||
></view>
|
||||
|
||||
<!-- 主侧边栏 (CRMEB风格: 70px) -->
|
||||
<AdminAside
|
||||
:collapsed="isMainAsideCollapsed"
|
||||
class="admin-sidebar"
|
||||
:class="{ 'mobile-aside-open': isMobileMenuOpen }"
|
||||
:collapsed="false"
|
||||
:topMenus="topMenus"
|
||||
:activeTopMenuId="activeTopMenuId"
|
||||
@toggle="toggleMainAsideCollapse"
|
||||
@@ -10,46 +19,52 @@
|
||||
:asideWidth="ASIDE_W"
|
||||
/>
|
||||
|
||||
<!-- 二级侧边栏 (CRMEB风格 - 内容区左侧) -->
|
||||
<!-- 二级侧边栏 (1:1 复刻 CRMEB 抽屉/Dock 平滑切换) -->
|
||||
<AdminSubSider
|
||||
v-if="showSubSider"
|
||||
:topMenuTitle="activeTopMenuTitle"
|
||||
:groups="activeGroups"
|
||||
:routes="activeRoutes"
|
||||
:activeRouteId="activeRouteId"
|
||||
:asideWidth="ASIDE_W"
|
||||
:siderWidth="SUB_W"
|
||||
@route-click="onRouteClick"
|
||||
:visible="isSubSiderVisible"
|
||||
:isOverlay="isOverlayVisible || layoutMode === 'mobile'"
|
||||
:layoutMode="layoutMode"
|
||||
:menuTitle="activeTopMenuTitle"
|
||||
:menuTree="activeMenuTree"
|
||||
:activeId="activeRouteId"
|
||||
:currentPath="currentRoutePath"
|
||||
:asideWidth="layoutMode === 'mobile' ? 0 : ASIDE_W"
|
||||
:width="SUB_W"
|
||||
@sub-click="onSubClick"
|
||||
/>
|
||||
|
||||
<!-- 右侧内容区 -->
|
||||
<view
|
||||
class="main"
|
||||
:style="{ marginLeft: mainLeft }"
|
||||
:style="{ marginLeft: isMobile ? '0' : mainLeft }"
|
||||
>
|
||||
<!-- 顶部导航栏 -->
|
||||
<AdminHeader
|
||||
:breadcrumb="breadcrumb"
|
||||
:hasNotification="hasNotification"
|
||||
:isMobile="isMobile"
|
||||
@search="onSearch"
|
||||
@refresh="onRefresh"
|
||||
@notify="onNotify"
|
||||
/>
|
||||
|
||||
<!-- 标签页 (CRMEB风格) -->
|
||||
<!-- 标签页 (CRMEB风格) - 移动端可以隐藏或滚动 -->
|
||||
<AdminTagsView
|
||||
v-if="!isMobile"
|
||||
:tabs="tabs"
|
||||
:activeTabId="activeRouteId"
|
||||
@tab-click="onTabClick"
|
||||
@tab-close="onTabClose"
|
||||
@close-other="onCloseOther"
|
||||
@close-all="onCloseAll"
|
||||
@refresh="onRefresh"
|
||||
/>
|
||||
|
||||
<!-- 内容展示区 (内部路由渲染) -->
|
||||
<view class="content-scroll">
|
||||
<view class="content-inner">
|
||||
<component :is="currentComponent" />
|
||||
<view class="content-inner" :class="{ 'is-mobile': isMobile }">
|
||||
<component :is="currentComponent" v-if="!isPageLoading" />
|
||||
<AdminPageLoading v-if="isPageLoading" />
|
||||
</view>
|
||||
<AdminFooter />
|
||||
</view>
|
||||
@@ -66,6 +81,7 @@ import AdminSubSider from '@/layouts/admin/components/AdminSubSider.uvue'
|
||||
import AdminHeader from '@/layouts/admin/components/AdminHeader.uvue'
|
||||
import AdminTagsView from '@/layouts/admin/components/AdminTagsView.uvue'
|
||||
import AdminFooter from '@/layouts/admin/components/AdminFooter.uvue'
|
||||
import AdminPageLoading from '@/layouts/admin/components/AdminPageLoading.uvue'
|
||||
|
||||
import {
|
||||
getTopMenus,
|
||||
@@ -76,34 +92,132 @@ 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,
|
||||
windowWidth,
|
||||
layoutMode,
|
||||
isMobile,
|
||||
isMobileMenuOpen,
|
||||
isOverlayVisible,
|
||||
openRoute,
|
||||
closeTab,
|
||||
closeOtherTabs,
|
||||
closeAllTabs,
|
||||
toggleMainAsideCollapse as storeToggleCollapse,
|
||||
toggleSubSider as storeToggleSubSider,
|
||||
initNavState
|
||||
} from '@/layouts/admin/store/adminNavStore.uts'
|
||||
import type { TabItem } from '@/layouts/admin/store/adminNavStore.uts'
|
||||
|
||||
import { getComponent } from '@/layouts/admin/router/adminComponentMap.uts'
|
||||
|
||||
// 侧边栏宽度配置
|
||||
const ASIDE_W = 96 // 主侧边栏宽度
|
||||
const SUB_W = 180 // 二级侧边栏宽度
|
||||
// 侧边栏宽度配置 (CRMEB 1:1)
|
||||
const ASIDE_W = 70
|
||||
const SUB_W = 200
|
||||
|
||||
// 页面加载状态
|
||||
const isPageLoading = ref(false)
|
||||
|
||||
const hasNotification = ref<boolean>(false)
|
||||
|
||||
// 计算主内容区左边距
|
||||
/**
|
||||
* 核心逻辑:计算二级菜单是否应该以 Overlay (抽屉) 模式展示
|
||||
* CRMEB 规则:Tablet 屏 (768~1199) 为 Overlay
|
||||
*/
|
||||
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. 如果有子菜单内容 (activeMenuTree > 0)
|
||||
* 2. 如果是 Desktop 且 showSubSider 为真 (Dock模式)
|
||||
* 3. 如果是 Tablet/Mobile 且 isOverlayVisible 为真 (Overlay模式)
|
||||
*/
|
||||
const isSubSiderVisible = computed<boolean>(() => {
|
||||
// 首页模块通常不显示 SubSider
|
||||
if (activeTopMenuId.value === 'home' || activeMenuTree.value.length === 0) return false
|
||||
|
||||
if (layoutMode.value === 'desktop') {
|
||||
return showSubSider.value
|
||||
}
|
||||
|
||||
// Tablet 和 Mobile 模式下,直接由 isOverlayVisible 控制
|
||||
return isOverlayVisible.value
|
||||
})
|
||||
|
||||
/**
|
||||
* 核心逻辑:计算主内容区左偏移量 (mainLeft)
|
||||
* 严格按照断点策略计算,防止遮挡
|
||||
* 1. Desktop: ASIDE_W + (SUB_W if dock)
|
||||
* 2. Tablet: ASIDE_W (sub-sider 是 overlay,不占位)
|
||||
* 3. Mobile: 0
|
||||
*/
|
||||
const mainLeft = computed<string>(() => {
|
||||
const asideWidth = isMainAsideCollapsed.value ? 0 : ASIDE_W
|
||||
const subWidth = showSubSider.value ? SUB_W : 0
|
||||
return (asideWidth + subWidth) + 'px'
|
||||
if (layoutMode.value === 'mobile') {
|
||||
return '0px'
|
||||
}
|
||||
|
||||
let left = ASIDE_W // 只要不是 Mobile,主侧栏 70px 始终 Dock
|
||||
|
||||
// 只有在 Desktop 模式且二级菜单处于 Dock 模式显示时,才累加宽度
|
||||
if (layoutMode.value === 'desktop' && showSubSider.value && activeMenuTree.value.length > 0) {
|
||||
left += SUB_W
|
||||
}
|
||||
|
||||
return left + 'px'
|
||||
})
|
||||
|
||||
// 获取一级菜单列表
|
||||
@@ -143,19 +257,77 @@ const currentComponent = computed<any>(() => {
|
||||
return getComponent(route.componentKey)
|
||||
})
|
||||
|
||||
// 监听路由变化,同步状态 (处理从 Tabs 或外部跳转的情况)
|
||||
watch(() => activeRouteId.value, (newId) => {
|
||||
// 触发页面加载动画
|
||||
isPageLoading.value = true
|
||||
setTimeout(() => {
|
||||
isPageLoading.value = false
|
||||
}, 400) // 给予足够的时间让异步组件加载
|
||||
|
||||
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 })
|
||||
|
||||
// ============================================
|
||||
// 事件处理
|
||||
// ============================================
|
||||
|
||||
function onTopMenuClick(menu: TopMenu): void {
|
||||
activeTopMenuId.value = menu.id
|
||||
if (menu.groups.length === 0) {
|
||||
|
||||
// 1:1 复刻 CRMEB 交互:点击 Aside 立即展示 SubSider
|
||||
if (layoutMode.value === 'desktop') {
|
||||
showSubSider.value = true
|
||||
} else {
|
||||
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')
|
||||
}
|
||||
}
|
||||
|
||||
function onRouteClick(routeId: string): void {
|
||||
openRoute(routeId)
|
||||
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 {
|
||||
@@ -178,6 +350,12 @@ function toggleMainAsideCollapse(): void {
|
||||
storeToggleCollapse()
|
||||
}
|
||||
|
||||
function closeAllMenu(): void {
|
||||
isMobileMenuOpen.value = false
|
||||
isOverlayVisible.value = false
|
||||
// 注意:在 Desktop 模式下,closeAllMenu 通常不隐藏 showSubSider,除非用户手动点汉堡按钮
|
||||
}
|
||||
|
||||
function onSearch(): void {
|
||||
uni.showToast({ title: '搜索', icon: 'none' })
|
||||
}
|
||||
@@ -194,6 +372,8 @@ function onNotify(): void {
|
||||
// 生命周期
|
||||
// ============================================
|
||||
|
||||
let resizeTid: any = null
|
||||
|
||||
onMounted(async () => {
|
||||
if (!ensureAnalyticsLogin({ toastTitle: '请先登录以访问管理后台' })) return
|
||||
|
||||
@@ -208,16 +388,84 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
initNavState()
|
||||
|
||||
// 初始化窗口宽度
|
||||
windowWidth.value = uni.getWindowInfo().windowWidth
|
||||
|
||||
// 监听窗口变化 (增加节流保护与跨断点状态重置)
|
||||
uni.onWindowResize((res) => {
|
||||
if (resizeTid != null) {
|
||||
cancelAnimationFrame(resizeTid)
|
||||
}
|
||||
|
||||
resizeTid = requestAnimationFrame(() => {
|
||||
const oldMode = layoutMode.value
|
||||
windowWidth.value = res.size.windowWidth
|
||||
const newMode = layoutMode.value
|
||||
|
||||
// 跨断点自动关闭所有 Overlay,防止状态残留遮挡内容
|
||||
if (oldMode != newMode) {
|
||||
isOverlayVisible.value = false
|
||||
isMobileMenuOpen.value = false
|
||||
// 如果切到桌面端,默认展开二级侧栏以符合 CRMEB 习惯
|
||||
if (newMode === 'desktop') {
|
||||
showSubSider.value = true
|
||||
}
|
||||
}
|
||||
|
||||
resizeTid = null
|
||||
})
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.layout-root {
|
||||
--admin-page-padding-desktop: 12px;
|
||||
--admin-page-padding-mobile: 8px;
|
||||
--admin-section-gap: 12px;
|
||||
--admin-card-padding: 16px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
background: #f0f2f5;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 移动端侧边栏样式 */
|
||||
.mobile-aside {
|
||||
position: absolute;
|
||||
left: -70px; /* 隐藏在左侧,匹配 ASIDE_W: 70 */
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 1001;
|
||||
transition: transform 0.3s ease;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.mobile-aside-open {
|
||||
transform: translateX(70px); /* 移入视图 */
|
||||
}
|
||||
|
||||
.mobile-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 300ms ease, visibility 0s linear 300ms;
|
||||
}
|
||||
|
||||
.mask-show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transition: opacity 300ms ease, visibility 0s linear 0s;
|
||||
}
|
||||
|
||||
.main {
|
||||
@@ -225,18 +473,49 @@ onMounted(async () => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
transition: margin-left 0.3s ease;
|
||||
transition: margin-left 300ms ease;
|
||||
background: #f0f2f5;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 响应式强制覆盖 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.main {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
/* 强行改变侧边栏布局模式 */
|
||||
.admin-sidebar {
|
||||
position: absolute !important;
|
||||
left: -70px !important; /* 隐藏在左侧 */
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 1001;
|
||||
transition: transform 0.3s ease !important;
|
||||
}
|
||||
|
||||
/* 展开时的状态 */
|
||||
.mobile-aside-open {
|
||||
transform: translateX(70px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.content-scroll {
|
||||
flex: 1;
|
||||
overflow-y: scroll;
|
||||
overflow-x: auto; /* 允许横向滚动,兼容极端窄屏 */
|
||||
background: #f0f2f5;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.content-inner {
|
||||
min-height: calc(100vh - 120px);
|
||||
padding: 16px;
|
||||
padding: var(--admin-page-padding-desktop);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content-inner.is-mobile {
|
||||
padding: var(--admin-page-padding-mobile);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -21,16 +21,18 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="aside-footer" @click="onToggle">
|
||||
<!-- 1:1 复刻 CRMEB: 一级侧边栏通常不单独折叠,由顶部汉堡菜单控制整体 -->
|
||||
<!-- <view class="aside-footer" @click="onToggle">
|
||||
<view class="toggle-btn">
|
||||
<text class="toggle-icon">{{ collapsed ? '>' : '<' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view> -->
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import type { TopMenu } from '@/layouts/admin/router/adminRoutes.uts'
|
||||
import { isMobile } from '@/layouts/admin/store/adminNavStore.uts'
|
||||
|
||||
const props = defineProps<{
|
||||
collapsed: boolean
|
||||
@@ -86,7 +88,7 @@ function onLogoClick(): void {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: width 0.3s ease;
|
||||
z-index: 1000;
|
||||
z-index: 1002; /* 确保在遮罩层之上 */
|
||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
@@ -107,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;
|
||||
@@ -154,6 +145,7 @@ function onLogoClick(): void {
|
||||
|
||||
.menu-title {
|
||||
font-size: 12px;
|
||||
transform: scale(0.9); /* CRMEB text is tiny */
|
||||
text-align: center;
|
||||
|
||||
text {
|
||||
|
||||
@@ -1,15 +1,30 @@
|
||||
<template>
|
||||
<view class="header">
|
||||
<view class="header-left">
|
||||
<text class="crumb" v-for="(item, index) in breadcrumb" :key="item.id">
|
||||
{{ item.title }}
|
||||
<text v-if="index < breadcrumb.length - 1" class="separator"> / </text>
|
||||
</text>
|
||||
<!-- 移动端菜单切换按钮 (CSS 控制显隐) -->
|
||||
<view class="menu-toggle mobile-only" @click="onToggleSubSider">
|
||||
<text class="menu-icon">☰</text>
|
||||
</view>
|
||||
|
||||
<!-- Desktop/Tablet Hamburger (1:1 复刻 CRMEB 切换二级侧边栏) -->
|
||||
<view class="menu-toggle desktop-only" @click="onToggleSubSider">
|
||||
<text class="menu-icon">☰</text>
|
||||
</view>
|
||||
|
||||
<view class="breadcrumb-container desktop-only">
|
||||
<text class="crumb" v-for="(item, index) in breadcrumb" :key="item.id">
|
||||
{{ item.title }}
|
||||
<text v-if="index < breadcrumb.length - 1" class="separator"> / </text>
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 移动端简单标题 (CSS 控制显隐) -->
|
||||
<text class="mobile-title mobile-only">{{ currentTitle }}</text>
|
||||
</view>
|
||||
|
||||
<view class="header-right">
|
||||
<view class="hbtn" @click="$emit('search')"><text>🔍</text></view>
|
||||
<view class="hbtn" @click="$emit('refresh')"><text>⟳</text></view>
|
||||
<view v-if="!isMobile" class="hbtn" @click="$emit('refresh')"><text>⟳</text></view>
|
||||
<view class="hbtn" @click="$emit('notify')">
|
||||
<text>🔔</text>
|
||||
<view class="dot" v-if="hasNotification"></view>
|
||||
@@ -19,16 +34,50 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
defineProps<{
|
||||
import { computed } from 'vue'
|
||||
import {
|
||||
toggleSubSider,
|
||||
showSubSider,
|
||||
layoutMode,
|
||||
isOverlayVisible,
|
||||
isMobileMenuOpen
|
||||
} from '@/layouts/admin/store/adminNavStore.uts'
|
||||
|
||||
const props = defineProps<{
|
||||
breadcrumb: Array<{id: string, title: string}>
|
||||
hasNotification: boolean
|
||||
isMobile: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(e:'search'): void
|
||||
(e:'refresh'): void
|
||||
(e:'notify'): void
|
||||
(e:'toggle-mobile-menu'): void
|
||||
}>()
|
||||
|
||||
/**
|
||||
* 核心切换逻辑:
|
||||
* 1. Desktop: 切换 showSubSider (Dock状态)
|
||||
* 2. Tablet: 切换 isOverlayVisible (Overlay状态)
|
||||
* 3. Mobile: 切换 isMobileMenuOpen (Mobile Aside)
|
||||
*/
|
||||
function onToggleSubSider(): void {
|
||||
if (layoutMode.value === 'desktop') {
|
||||
toggleSubSider()
|
||||
} else if (layoutMode.value === 'tablet') {
|
||||
isOverlayVisible.value = !isOverlayVisible.value
|
||||
} else {
|
||||
isMobileMenuOpen.value = !isMobileMenuOpen.value
|
||||
}
|
||||
}
|
||||
|
||||
const currentTitle = computed((): string => {
|
||||
if (props.breadcrumb.length > 0) {
|
||||
return props.breadcrumb[props.breadcrumb.length - 1].title
|
||||
}
|
||||
return '管理后台'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -49,11 +98,54 @@ defineEmits<{
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
margin-right: 12px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
font-size: 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.mobile-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.breadcrumb-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.crumb {
|
||||
color: #374151;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 响应式控制 */
|
||||
.mobile-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.desktop-only {
|
||||
display: none !important;
|
||||
}
|
||||
.mobile-only {
|
||||
display: flex !important;
|
||||
}
|
||||
.header-right {
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.separator {
|
||||
color: #d1d5db;
|
||||
margin: 0 8px;
|
||||
|
||||
53
layouts/admin/components/AdminPageLoading.uvue
Normal file
53
layouts/admin/components/AdminPageLoading.uvue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<view class="loading-overlay">
|
||||
<view class="loading-content">
|
||||
<view class="spinner"></view>
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
// 管理后台统一加载动画组件
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #2d8cf0;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
@@ -1,52 +1,160 @@
|
||||
<template>
|
||||
<view class="admin-subsider" :style="{ left: asideWidth + 'px', width: siderWidth + 'px' }">
|
||||
<view class="subsider-header">
|
||||
<text class="header-title">{{ topMenuTitle }}</text>
|
||||
<view
|
||||
class="admin-subsider"
|
||||
:class="{ 'is-hidden': !visible, 'sub-sider-overlay': isOverlay || layoutMode === 'mobile' }"
|
||||
:style="{ left: asideWidth + 'px', width: width + 'px' }"
|
||||
>
|
||||
<!-- 头部模块标题 (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">▶</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>
|
||||
<!-- 二级子菜单容器 -->
|
||||
<transition name="expand">
|
||||
<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">▶</text>
|
||||
</view>
|
||||
|
||||
<!-- 三级子菜单容器 -->
|
||||
<transition name="expand">
|
||||
<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>
|
||||
</transition>
|
||||
</template>
|
||||
</view>
|
||||
</transition>
|
||||
</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<{
|
||||
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">
|
||||
@@ -54,79 +162,122 @@ 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.06);
|
||||
background: #ffffff;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 999;
|
||||
z-index: 1000;
|
||||
transition: transform 300ms ease, opacity 250ms ease;
|
||||
}
|
||||
|
||||
.subsider-header {
|
||||
height: 64px;
|
||||
.admin-subsider.is-hidden {
|
||||
transform: translate3d(-100%, 0, 0);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.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;
|
||||
padding: 0 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
background-color: #fff;
|
||||
flex-shrink: 0;
|
||||
|
||||
.header-title {
|
||||
font-size: 16px;
|
||||
.cat-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.subsider-menu {
|
||||
.subsider-content {
|
||||
flex: 1;
|
||||
padding: 8px 0;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.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;
|
||||
|
||||
&:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 3px;
|
||||
background: #1890ff;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid transparent; /* 保持布局稳定 */
|
||||
|
||||
&.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;
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* 旋转动画 */
|
||||
.is-open .chevron {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.sub-menu-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 展开收起动画 */
|
||||
.expand-enter-active,
|
||||
.expand-leave-active {
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
max-height: 1000px; /* 足够大的预设值 */
|
||||
}
|
||||
|
||||
.expand-enter-from,
|
||||
.expand-leave-to {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,36 +1,122 @@
|
||||
<template>
|
||||
<view class="tags">
|
||||
<scroll-view class="tags-scroll" scroll-x="true" show-scrollbar="false">
|
||||
<view class="tags-row">
|
||||
<transition-group name="tag-list" tag="view" class="tags-row">
|
||||
<view
|
||||
v-for="t in tabs"
|
||||
:key="t.id"
|
||||
class="tag"
|
||||
:class="{ active: activeTabId === t.id }"
|
||||
@click="$emit('tab-click', t)"
|
||||
@click="onTabClick(t)"
|
||||
@contextmenu.prevent="openContextMenu($event, t)"
|
||||
>
|
||||
<text class="tag-text">{{ t.title }}</text>
|
||||
<view class="tag-close" @click.stop="$emit('tab-close', t.id)">
|
||||
<view v-if="!t.isAffix" class="tag-close" @click.stop="$emit('tab-close', t.id)">
|
||||
<text class="tag-close-text">×</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</transition-group>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 右键菜单 (带动画) -->
|
||||
<transition name="menu-fade">
|
||||
<view
|
||||
v-if="menuVisible"
|
||||
class="context-menu"
|
||||
:style="{ top: menuY + 'px', left: menuX + 'px' }"
|
||||
@click.stop=""
|
||||
>
|
||||
<view class="menu-item" @click="handleAction('refresh')">
|
||||
<text class="menu-icon">↻</text>
|
||||
<text class="menu-text">刷新</text>
|
||||
</view>
|
||||
<view class="menu-item" v-if="!selectedTab?.isAffix" @click="handleAction('close')">
|
||||
<text class="menu-icon">×</text>
|
||||
<text class="menu-text">关闭</text>
|
||||
</view>
|
||||
<view class="menu-item" @click="handleAction('close-other')">
|
||||
<text class="menu-icon">↸</text>
|
||||
<text class="menu-text">关闭其他</text>
|
||||
</view>
|
||||
<view class="menu-item" @click="handleAction('close-all')">
|
||||
<text class="menu-icon">⊘</text>
|
||||
<text class="menu-text">全部关闭</text>
|
||||
</view>
|
||||
</view>
|
||||
</transition>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import type { TabItem } from '../types.uts'
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import type { TabItem } from '../store/adminNavStore.uts'
|
||||
|
||||
defineProps<{
|
||||
tabs: TabItem[]
|
||||
activeTabId: string
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
const emit = defineEmits<{
|
||||
(e:'tab-click', tab: TabItem): void
|
||||
(e:'tab-close', tabId: string): void
|
||||
(e:'close-other', tabId: string): void
|
||||
(e:'close-all'): void
|
||||
(e:'refresh'): void
|
||||
}>()
|
||||
|
||||
// 右键菜单状态
|
||||
const menuVisible = ref(false)
|
||||
const menuX = ref(0)
|
||||
const menuY = ref(0)
|
||||
const selectedTab = ref<TabItem | null>(null)
|
||||
|
||||
function onTabClick(tab: TabItem) {
|
||||
closeMenu()
|
||||
emit('tab-click', tab)
|
||||
}
|
||||
|
||||
function openContextMenu(e: MouseEvent, tab: TabItem) {
|
||||
selectedTab.value = tab
|
||||
menuX.value = e.clientX
|
||||
menuY.value = e.clientY
|
||||
|
||||
// 边缘检测
|
||||
if (menuX.value + 100 > window.innerWidth) {
|
||||
menuX.value = window.innerWidth - 110
|
||||
}
|
||||
|
||||
menuVisible.value = true
|
||||
}
|
||||
|
||||
function closeMenu() {
|
||||
menuVisible.value = false
|
||||
}
|
||||
|
||||
function handleAction(type: string) {
|
||||
if (!selectedTab.value) return
|
||||
|
||||
const id = selectedTab.value!.id
|
||||
|
||||
if (type === 'refresh') {
|
||||
emit('refresh')
|
||||
} else if (type === 'close') {
|
||||
emit('tab-close', id)
|
||||
} else if (type === 'close-other') {
|
||||
emit('close-other', id)
|
||||
} else if (type === 'close-all') {
|
||||
emit('close-all')
|
||||
}
|
||||
|
||||
closeMenu()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('click', closeMenu)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('click', closeMenu)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -41,6 +127,7 @@ defineEmits<{
|
||||
display:flex;
|
||||
flex-direction:row;
|
||||
align-items:center;
|
||||
position: relative;
|
||||
}
|
||||
.tags-scroll{ width: 100%; height: 44px; }
|
||||
.tags-row{
|
||||
@@ -63,6 +150,11 @@ defineEmits<{
|
||||
flex-direction: row;
|
||||
align-items:center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.tag:hover {
|
||||
background: #f9fafb;
|
||||
}
|
||||
.tag.active{
|
||||
border-color:#1677ff;
|
||||
@@ -78,5 +170,80 @@ defineEmits<{
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
}
|
||||
.tag-close:hover {
|
||||
background: rgba(0,0,0,0.1);
|
||||
}
|
||||
.tag-close-text{ font-size:14px; color:#6b7280; line-height:14px; }
|
||||
|
||||
/* 右键菜单样式 */
|
||||
.context-menu {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
|
||||
padding: 5px 0;
|
||||
border-radius: 4px;
|
||||
min-width: 100px;
|
||||
border: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
font-size: 14px;
|
||||
margin-right: 8px;
|
||||
color: #606266;
|
||||
width: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.menu-text {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
/* 标签列表动画 - 平滑平移和缩放 */
|
||||
.tag-list-enter-active,
|
||||
.tag-list-leave-active {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.tag-list-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px) scale(0.9);
|
||||
}
|
||||
|
||||
.tag-list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(10px) scale(0.8);
|
||||
}
|
||||
|
||||
/* 确保列表项在移动过程中位置平滑切换 */
|
||||
.tag-list-move {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
/* 右键菜单动画 - 类似 CRMEB 的缩放渐入 */
|
||||
.menu-fade-enter-active,
|
||||
.menu-fade-leave-active {
|
||||
transition: all 0.2s cubic-bezier(0.25, 0.8, 0.5, 1);
|
||||
transform-origin: top left;
|
||||
}
|
||||
|
||||
.menu-fade-enter-from,
|
||||
.menu-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.8) translateY(-10px);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,74 +1,54 @@
|
||||
<template>
|
||||
<view class="home-page">
|
||||
<!-- 数据统计卡片行 -->
|
||||
<view class="stats-row">
|
||||
<!-- 销售额卡片 -->
|
||||
<view class="stat-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">销售额</text>
|
||||
<view class="tag today">今日</view>
|
||||
</view>
|
||||
<view class="card-value">91.1</view>
|
||||
<view class="card-meta">
|
||||
<text class="meta-text">昨日 2740</text>
|
||||
<text class="meta-trend down">日环比 -96.67% ▼</text>
|
||||
</view>
|
||||
<view class="card-footer">
|
||||
<text class="footer-label">本月销售额</text>
|
||||
<text class="footer-value">2831.1元</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 数据统计卡片行 (使用统一响应式网格) -->
|
||||
<view class="kpi-grid">
|
||||
<KpiMiniCard
|
||||
class="stat-card"
|
||||
title="销售额"
|
||||
tagText="今日"
|
||||
:valueText="statsData.sales.today.toString()"
|
||||
:metaLeft="'昨日 ' + statsData.sales.yesterday"
|
||||
:metaRight="'日环比 ' + statsData.sales.trend + '%'"
|
||||
:trend="statsData.sales.trend > 0 ? 'up' : 'down'"
|
||||
footerLeftText="本月销售额"
|
||||
:footerRightText="statsData.sales.monthTotal + '元'"
|
||||
/>
|
||||
|
||||
<!-- 用户访问量卡片 -->
|
||||
<view class="stat-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">用户访问量</text>
|
||||
<view class="tag today">今日</view>
|
||||
</view>
|
||||
<view class="card-value">224</view>
|
||||
<view class="card-meta">
|
||||
<text class="meta-text">昨日 136</text>
|
||||
<text class="meta-trend up">日环比 64.7% ▲</text>
|
||||
</view>
|
||||
<view class="card-footer">
|
||||
<text class="footer-label">本月访问量</text>
|
||||
<text class="footer-value">360Pv</text>
|
||||
</view>
|
||||
</view>
|
||||
<KpiMiniCard
|
||||
class="stat-card"
|
||||
title="用户访问量"
|
||||
tagText="今日"
|
||||
:valueText="statsData.visits.today.toString()"
|
||||
:metaLeft="'昨日 ' + statsData.visits.yesterday"
|
||||
:metaRight="'日环比 ' + statsData.visits.trend + '%'"
|
||||
:trend="statsData.visits.trend > 0 ? 'up' : 'down'"
|
||||
footerLeftText="本月访问量"
|
||||
:footerRightText="statsData.visits.monthTotal + 'Pv'"
|
||||
/>
|
||||
|
||||
<!-- 订单量卡片 -->
|
||||
<view class="stat-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">订单量</text>
|
||||
<view class="tag today">今日</view>
|
||||
</view>
|
||||
<view class="card-value">4</view>
|
||||
<view class="card-meta">
|
||||
<text class="meta-text">昨日 8</text>
|
||||
<text class="meta-trend down">日环比 -50% ▼</text>
|
||||
</view>
|
||||
<view class="card-footer">
|
||||
<text class="footer-label">本月订单量</text>
|
||||
<text class="footer-value">12单</text>
|
||||
</view>
|
||||
</view>
|
||||
<KpiMiniCard
|
||||
class="stat-card"
|
||||
title="订单量"
|
||||
tagText="今日"
|
||||
:valueText="statsData.orders.today.toString()"
|
||||
:metaLeft="'昨日 ' + statsData.orders.yesterday"
|
||||
:metaRight="'日环比 ' + statsData.orders.trend + '%'"
|
||||
:trend="statsData.orders.trend > 0 ? 'up' : 'down'"
|
||||
footerLeftText="本月订单量"
|
||||
:footerRightText="statsData.orders.monthTotal + '单'"
|
||||
/>
|
||||
|
||||
<!-- 新增用户卡片 -->
|
||||
<view class="stat-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">新增用户</text>
|
||||
<view class="tag today">今日</view>
|
||||
</view>
|
||||
<view class="card-value">21</view>
|
||||
<view class="card-meta">
|
||||
<text class="meta-text">昨日 6</text>
|
||||
<text class="meta-trend up">日环比 250% ▲</text>
|
||||
</view>
|
||||
<view class="card-footer">
|
||||
<text class="footer-label">本月新增用户</text>
|
||||
<text class="footer-value">27人</text>
|
||||
</view>
|
||||
</view>
|
||||
<KpiMiniCard
|
||||
class="stat-card"
|
||||
title="新增用户"
|
||||
tagText="今日"
|
||||
:valueText="statsData.users.today.toString()"
|
||||
:metaLeft="'昨日 ' + statsData.users.yesterday"
|
||||
:metaRight="'日环比 ' + statsData.users.trend + '%'"
|
||||
:trend="statsData.users.trend > 0 ? 'up' : 'down'"
|
||||
footerLeftText="本月新增用户"
|
||||
:footerRightText="statsData.users.monthTotal + '人'"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 订单趋势图表区 -->
|
||||
@@ -156,6 +136,7 @@ import { ref, computed, onMounted } from 'vue'
|
||||
import AnalyticsComboChart from '@/components/analytics/AnalyticsComboChart.uvue'
|
||||
import AnalyticsAreaChart from '@/components/analytics/AnalyticsAreaChart.uvue'
|
||||
import AnalyticsPieChart from '@/components/analytics/AnalyticsPieChart.uvue'
|
||||
import KpiMiniCard from '@/pages/mall/admin/homePage/components/KpiMiniCard.uvue'
|
||||
|
||||
// Filter periods
|
||||
const periods = [
|
||||
@@ -244,103 +225,22 @@ const statsData = ref({
|
||||
|
||||
<style scoped>
|
||||
.home-page {
|
||||
padding: 16px;
|
||||
padding: 0;
|
||||
background-color: #f0f2f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* 统计卡片 */
|
||||
/* 兼容旧布局标识,样式逻辑已由 .kpi-grid 接管 */
|
||||
.stat-card {
|
||||
flex: 1;
|
||||
min-width: 280px;
|
||||
background-color: #ffffff;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||
transition: box-shadow 0.3s;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
/* 卡片头部 */
|
||||
.card-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.tag {
|
||||
padding: 2px 8px;
|
||||
border-radius: 2px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tag.today {
|
||||
background-color: #e8f4ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
/* 卡片主值 */
|
||||
.card-value {
|
||||
font-size: 32px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* 卡片元数据 */
|
||||
.card-meta {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.meta-text {
|
||||
font-size: 13px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.meta-trend {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.meta-trend.up {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.meta-trend.down {
|
||||
color: #52c41a;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
/* 图表区样式 */
|
||||
.chart-section {
|
||||
margin-top: 16px;
|
||||
margin-top: 12px;
|
||||
background-color: #ffffff;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
@@ -349,7 +249,7 @@ const statsData = ref({
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
@@ -420,10 +320,10 @@ const statsData = ref({
|
||||
}
|
||||
|
||||
.bottom-charts {
|
||||
margin-top: 16px;
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.half-width {
|
||||
@@ -453,14 +353,11 @@ const statsData = ref({
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media screen and (max-width: 1400px) {
|
||||
.stat-card {
|
||||
min-width: calc(50% - 8px);
|
||||
@media screen and (max-width: 768px) {
|
||||
.home-page {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1024px) {
|
||||
|
||||
.bottom-charts {
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -468,24 +365,25 @@ const statsData = ref({
|
||||
.half-width {
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.home-page {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
/* 调整图表头部在移动端的展示 */
|
||||
.chart-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
min-width: 100%;
|
||||
padding: 16px;
|
||||
.header-right {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 28px;
|
||||
.period-tabs {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.period-tab {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,122 +6,25 @@
|
||||
* value: 组件引用
|
||||
*
|
||||
* 注意:
|
||||
* 1. 所有组件必须静态导入,确保打包可分析
|
||||
* 1. 组件已切换为 defineAsyncComponent 异步导入,优化 H5 环境下的加载性能与包体积
|
||||
* 2. 组件路径使用 @ 别名
|
||||
* 3. 占位组件统一使用 PlaceholderPage
|
||||
*/
|
||||
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
// 导入占位组件
|
||||
import PlaceholderPage from '@/layouts/admin/components/PlaceholderPage.uvue'
|
||||
|
||||
// 导入首页(内部组件,不包含 AdminLayout)
|
||||
import HomeIndex from '@/layouts/admin/pages/HomeIndex.uvue'
|
||||
|
||||
// 导入用户模块(纯组件,不包含 AdminLayout)
|
||||
import UserStatistic from '@/pages/mall/admin/user/Statistic.uvue'
|
||||
import UserList from '@/pages/mall/admin/user/list.uvue'
|
||||
import UserLevel from '@/pages/mall/admin/user/level.uvue'
|
||||
import UserGroup from '@/pages/mall/admin/user/group.uvue'
|
||||
import UserLabel from '@/pages/mall/admin/user/label.uvue'
|
||||
import MemberConfig from '@/pages/mall/admin/user/MemberConfig.uvue'
|
||||
|
||||
// 导入商品模块(纯组件,不包含 AdminLayout)
|
||||
import ProductStatistic from '@/pages/mall/admin/product/product-statistics/index.uvue'
|
||||
import ProductList from '@/pages/mall/admin/product/product-management/index.uvue'
|
||||
import ProductEdit from '@/pages/mall/admin/product/product-management/edit.uvue'
|
||||
import ProductMemberPrice from '@/pages/mall/admin/product/product-management/member-price.uvue'
|
||||
import ProductClassify from '@/pages/mall/admin/product/classify.uvue'
|
||||
import ProductReply from '@/pages/mall/admin/product/reply.uvue'
|
||||
import ProductAttr from '@/pages/mall/admin/product/attr.uvue'
|
||||
import ProductParam from '@/pages/mall/admin/product/param.uvue'
|
||||
import ProductLabel from '@/pages/mall/admin/product/label.uvue'
|
||||
import ProductProtection from '@/pages/mall/admin/product/protection.uvue'
|
||||
|
||||
// 导入订单模块(纯组件,不包含 AdminLayout)
|
||||
import OrderList from '@/pages/mall/admin/order/list.uvue'
|
||||
import OrderStatistic from '@/pages/mall/admin/order/order-statistics/index.uvue'
|
||||
import OrderRefund from '@/pages/mall/admin/order/aftersales-order/index.uvue'
|
||||
import OrderCashier from '@/pages/mall/admin/order/cashier-order/index.uvue'
|
||||
import OrderVerify from '@/pages/mall/admin/order/write-off-records/index.uvue'
|
||||
import OrderConfig from '@/pages/mall/admin/order/order-configuration/index.uvue'
|
||||
// 用户、商品、订单模块已改为 defineAsyncComponent 异步加载,移除静态导入以优化 H5 加载性能
|
||||
|
||||
// 营销设置模块暂时使用 PlaceholderPage
|
||||
// 避免循环依赖问题
|
||||
import CmsArticle from '@/pages/mall/admin/cms/article/list.uvue'
|
||||
import CmsCategory from '@/pages/mall/admin/cms/category/list.uvue'
|
||||
import MarketingCouponList from '@/pages/mall/admin/marketing/coupon/list.uvue'
|
||||
import MarketingCouponUser from '@/pages/mall/admin/marketing/coupon/user.uvue'
|
||||
import MarketingIntegralStatistic from '@/pages/mall/admin/marketing/integral/statistic.uvue'
|
||||
import MarketingIntegralProduct from '@/pages/mall/admin/marketing/integral/list.uvue'
|
||||
import MarketingIntegralOrder from '@/pages/mall/admin/marketing/integral/order.uvue'
|
||||
import MarketingIntegralRecord from '@/pages/mall/admin/marketing/integral/record.uvue'
|
||||
import MarketingIntegralConfig from '@/pages/mall/admin/marketing/integral/config.uvue'
|
||||
import MarketingLotteryList from '@/pages/mall/admin/marketing/lottery/list.uvue'
|
||||
import MarketingLotteryConfig from '@/pages/mall/admin/marketing/lottery/config.uvue'
|
||||
import MarketingCombinationProduct from '@/pages/mall/admin/marketing/combination/product.uvue'
|
||||
import MarketingCombinationList from '@/pages/mall/admin/marketing/combination/list.uvue'
|
||||
import MarketingCombinationCreate from '@/pages/mall/admin/marketing/combination/create.uvue'
|
||||
import MarketingSeckillList from '@/pages/mall/admin/marketing/seckill/list.uvue'
|
||||
import MarketingSeckillProduct from '@/pages/mall/admin/marketing/seckill/product.uvue'
|
||||
import MarketingSeckillConfig from '@/pages/mall/admin/marketing/seckill/config.uvue'
|
||||
|
||||
// 导入财务模块(纯组件)
|
||||
import FinanceTransactionStats from '@/pages/mall/admin/finance/transaction_stats.uvue'
|
||||
import FinanceWithdrawal from '@/pages/mall/admin/finance/withdrawal.uvue'
|
||||
import FinanceInvoice from '@/pages/mall/admin/finance/invoice.uvue'
|
||||
import FinanceRecharge from '@/pages/mall/admin/finance/recharge.uvue'
|
||||
import FinanceCapitalFlow from '@/pages/mall/admin/finance/capital_flow.uvue'
|
||||
import FinanceBill from '@/pages/mall/admin/finance/bill.uvue'
|
||||
import FinanceCommission from '@/pages/mall/admin/finance/commission.uvue'
|
||||
import FinanceBalanceStats from '@/pages/mall/admin/finance/balance_stats.uvue'
|
||||
import FinanceBalanceRecord from '@/pages/mall/admin/finance/balance_record.uvue'
|
||||
|
||||
// 导入客服模块
|
||||
import KefuList from '@/pages/mall/admin/kefu/list.uvue'
|
||||
import KefuWords from '@/pages/mall/admin/kefu/words.uvue'
|
||||
import KefuFeedback from '@/pages/mall/admin/kefu/feedback.uvue'
|
||||
import KefuAutoReply from '@/pages/mall/admin/kefu/auto_reply.uvue'
|
||||
import KefuConfig from '@/pages/mall/admin/kefu/config.uvue'
|
||||
|
||||
// 导入装修模块
|
||||
import DecorationHome from '@/pages/mall/admin/decoration/home.uvue'
|
||||
import DecorationCategory from '@/pages/mall/admin/decoration/category.uvue'
|
||||
import DecorationUser from '@/pages/mall/admin/decoration/user.uvue'
|
||||
import DecorationData from '@/pages/mall/admin/decoration/data-config.uvue'
|
||||
import DecorationStyle from '@/pages/mall/admin/design/theme-style.uvue'
|
||||
import DecorationMaterial from '@/pages/mall/admin/design/material.uvue'
|
||||
import DecorationLink from '@/pages/mall/admin/design/link-management.uvue'
|
||||
|
||||
// 导入直播管理
|
||||
import MarketingLiveRoom from '@/pages/mall/admin/marketing/live/room.uvue'
|
||||
import MarketingLiveProduct from '@/pages/mall/admin/marketing/live/product.uvue'
|
||||
import MarketingLiveAnchor from '@/pages/mall/admin/marketing/live/anchor.uvue'
|
||||
|
||||
// 导入用户充值
|
||||
import MarketingRechargeQuota from '@/pages/mall/admin/marketing/recharge/quota.uvue'
|
||||
import MarketingRechargeConfig from '@/pages/mall/admin/marketing/recharge/config.uvue'
|
||||
|
||||
// 导入每日签到
|
||||
import MarketingCheckinConfig from '@/pages/mall/admin/marketing/checkin/config.uvue'
|
||||
import MarketingCheckinReward from '@/pages/mall/admin/marketing/checkin/reward.uvue'
|
||||
|
||||
// 导入新人礼
|
||||
import MarketingNewcomerGift from '@/pages/mall/admin/marketing/newcomer/index.uvue'
|
||||
|
||||
// 导入付费会员
|
||||
import MarketingMemberType from '@/pages/mall/admin/marketing/member/type.uvue'
|
||||
import MarketingMemberRight from '@/pages/mall/admin/marketing/member/right.uvue'
|
||||
import MarketingMemberCard from '@/pages/mall/admin/marketing/member/card.uvue'
|
||||
import MarketingMemberRecord from '@/pages/mall/admin/marketing/member/record.uvue'
|
||||
import MarketingMemberConfig from '@/pages/mall/admin/marketing/member/config.uvue'
|
||||
|
||||
// 导入维护模块
|
||||
import MaintainDevConfig from '@/pages/mall/admin/maintain/dev/config.uvue'
|
||||
|
||||
// import StatisticIndex from '@/pages/mall/admin/statistic/index.uvue'
|
||||
// import SettingSystemConfig from '@/pages/mall/admin/setting/system/config.uvue'
|
||||
// import SettingSystemAdmin from '@/pages/mall/admin/setting/system/admin.uvue'
|
||||
// import SettingSystemRole from '@/pages/mall/admin/setting/system/role.uvue'
|
||||
// 营销、内容、财务、客服、装修等模块已改为 defineAsyncComponent 异步加载,移除静态导入以优化 H5 加载性能
|
||||
|
||||
/**
|
||||
* 组件映射表
|
||||
@@ -131,127 +34,146 @@ export const componentMap: Map<string, any> = new Map([
|
||||
['HomeIndex', HomeIndex],
|
||||
|
||||
// 用户模块
|
||||
['UserStatistic', UserStatistic],
|
||||
['UserList', UserList],
|
||||
['UserLevel', UserLevel],
|
||||
['UserGroup', UserGroup],
|
||||
['UserLabel', UserLabel],
|
||||
['UserMemberConfig', MemberConfig],
|
||||
['UserStatistic', defineAsyncComponent(() => import('@/pages/mall/admin/user/statistics/index.uvue'))],
|
||||
['UserList', defineAsyncComponent(() => import('@/pages/mall/admin/user/management/index.uvue'))],
|
||||
['UserLevel', defineAsyncComponent(() => import('@/pages/mall/admin/user/level/index.uvue'))],
|
||||
['UserGroup', defineAsyncComponent(() => import('@/pages/mall/admin/user/grouping/index.uvue'))],
|
||||
['UserLabel', defineAsyncComponent(() => import('@/pages/mall/admin/user/label/index.uvue'))],
|
||||
['UserMemberConfig', defineAsyncComponent(() => import('@/pages/mall/admin/user/configuration/index.uvue'))],
|
||||
|
||||
// 商品模块
|
||||
['ProductStatistic', ProductStatistic],
|
||||
['ProductList', ProductList],
|
||||
['ProductEdit', ProductEdit],
|
||||
['ProductMemberPrice', ProductMemberPrice],
|
||||
['ProductClassify', ProductClassify],
|
||||
['ProductReply', ProductReply],
|
||||
['ProductAttr', ProductAttr],
|
||||
['ProductParam', ProductParam],
|
||||
['ProductLabel', ProductLabel],
|
||||
['ProductProtection', ProductProtection],
|
||||
['ProductStatistic', defineAsyncComponent(() => import('@/pages/mall/admin/product/product-statistics/index.uvue'))],
|
||||
['ProductList', defineAsyncComponent(() => import('@/pages/mall/admin/product/product-management/index.uvue'))],
|
||||
['ProductEdit', defineAsyncComponent(() => import('@/pages/mall/admin/product/product-management/edit.uvue'))],
|
||||
['ProductMemberPrice', defineAsyncComponent(() => import('@/pages/mall/admin/product/product-management/member-price.uvue'))],
|
||||
['ProductClassify', defineAsyncComponent(() => import('@/pages/mall/admin/product/classification/index.uvue'))],
|
||||
['ProductReply', defineAsyncComponent(() => import('@/pages/mall/admin/product/reviews/index.uvue'))],
|
||||
['ProductAttr', defineAsyncComponent(() => import('@/pages/mall/admin/product/specifications/index.uvue'))],
|
||||
['ProductParam', defineAsyncComponent(() => import('@/pages/mall/admin/product/parameters/index.uvue'))],
|
||||
['ProductLabel', defineAsyncComponent(() => import('@/pages/mall/admin/product/labels/index.uvue'))],
|
||||
['ProductProtection', defineAsyncComponent(() => import('@/pages/mall/admin/product/protection/index.uvue'))],
|
||||
|
||||
// 订单模块
|
||||
['OrderList', OrderList],
|
||||
['OrderStatistic', OrderStatistic],
|
||||
['OrderRefund', OrderRefund],
|
||||
['OrderCashier', OrderCashier],
|
||||
['OrderVerify', OrderVerify],
|
||||
['OrderConfig', OrderConfig],
|
||||
['OrderList', defineAsyncComponent(() => import('@/pages/mall/admin/order/list.uvue'))],
|
||||
['OrderStatistic', defineAsyncComponent(() => import('@/pages/mall/admin/order/order-statistics/index.uvue'))],
|
||||
['OrderRefund', defineAsyncComponent(() => import('@/pages/mall/admin/order/aftersales-order/index.uvue'))],
|
||||
['OrderCashier', defineAsyncComponent(() => import('@/pages/mall/admin/order/cashier-order/index.uvue'))],
|
||||
['OrderVerify', defineAsyncComponent(() => import('@/pages/mall/admin/order/write-off-records/index.uvue'))],
|
||||
['OrderConfig', defineAsyncComponent(() => import('@/pages/mall/admin/order/order-configuration/index.uvue'))],
|
||||
|
||||
// 营销模块
|
||||
// 营销模块已改为异步加载
|
||||
// 1. 优惠券
|
||||
['MarketingCouponList', MarketingCouponList],
|
||||
['MarketingCouponUser', MarketingCouponUser],
|
||||
['MarketingCouponList', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/coupon/list.uvue'))],
|
||||
['MarketingCouponUser', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/coupon/user.uvue'))],
|
||||
// 2. 积分管理
|
||||
['MarketingIntegralStatistic', MarketingIntegralStatistic],
|
||||
['MarketingIntegralProduct', MarketingIntegralProduct],
|
||||
['MarketingIntegralOrder', MarketingIntegralOrder],
|
||||
['MarketingIntegralRecord', MarketingIntegralRecord],
|
||||
['MarketingIntegralConfig', MarketingIntegralConfig],
|
||||
['MarketingIntegralStatistic', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/integral/statistic.uvue'))],
|
||||
['MarketingIntegralProduct', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/integral/list.uvue'))],
|
||||
['MarketingIntegralOrder', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/integral/order.uvue'))],
|
||||
['MarketingIntegralRecord', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/integral/record.uvue'))],
|
||||
['MarketingIntegralConfig', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/integral/config.uvue'))],
|
||||
// 3. 抽奖管理
|
||||
['MarketingLotteryList', MarketingLotteryList],
|
||||
['MarketingLotteryConfig', MarketingLotteryConfig],
|
||||
['MarketingLotteryList', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/lottery/list.uvue'))],
|
||||
['MarketingLotteryConfig', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/lottery/config.uvue'))],
|
||||
// 4. 砍价管理
|
||||
['MarketingBargainProduct', PlaceholderPage],
|
||||
['MarketingBargainList', PlaceholderPage],
|
||||
// 5. 拼团管理
|
||||
['MarketingCombinationProduct', MarketingCombinationProduct],
|
||||
['MarketingCombinationList', MarketingCombinationList],
|
||||
['MarketingCombinationCreate', MarketingCombinationCreate],
|
||||
['MarketingCombinationProduct', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/combination/product.uvue'))],
|
||||
['MarketingCombinationList', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/combination/list.uvue'))],
|
||||
['MarketingCombinationCreate', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/combination/create.uvue'))],
|
||||
// 6. 秒杀管理
|
||||
['MarketingSeckillList', MarketingSeckillList],
|
||||
['MarketingSeckillProduct', MarketingSeckillProduct],
|
||||
['MarketingSeckillConfig', MarketingSeckillConfig],
|
||||
['MarketingSeckillList', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/seckill/list.uvue'))],
|
||||
['MarketingSeckillProduct', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/seckill/product.uvue'))],
|
||||
['MarketingSeckillConfig', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/seckill/config.uvue'))],
|
||||
// 7. 付费会员
|
||||
['MarketingMemberType', MarketingMemberType],
|
||||
['MarketingMemberRight', MarketingMemberRight],
|
||||
['MarketingMemberCard', MarketingMemberCard],
|
||||
['MarketingMemberRecord', MarketingMemberRecord],
|
||||
['MarketingMemberConfig', MarketingMemberConfig],
|
||||
['MarketingMemberType', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/member/type.uvue'))],
|
||||
['MarketingMemberRight', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/member/right.uvue'))],
|
||||
['MarketingMemberCard', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/member/card.uvue'))],
|
||||
['MarketingMemberRecord', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/member/record.uvue'))],
|
||||
['MarketingMemberConfig', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/member/config.uvue'))],
|
||||
// 8. 直播管理
|
||||
['MarketingLiveRoom', MarketingLiveRoom],
|
||||
['MarketingLiveProduct', MarketingLiveProduct],
|
||||
['MarketingLiveAnchor', MarketingLiveAnchor],
|
||||
['MarketingLiveRoom', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/live/room.uvue'))],
|
||||
['MarketingLiveProduct', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/live/product.uvue'))],
|
||||
['MarketingLiveAnchor', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/live/anchor.uvue'))],
|
||||
// 9. 用户充值
|
||||
['MarketingRechargeQuota', MarketingRechargeQuota],
|
||||
['MarketingRechargeConfig', MarketingRechargeConfig],
|
||||
['MarketingRechargeQuota', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/recharge/quota.uvue'))],
|
||||
['MarketingRechargeConfig', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/recharge/config.uvue'))],
|
||||
// 10. 每日签到
|
||||
['MarketingCheckinConfig', MarketingCheckinConfig],
|
||||
['MarketingCheckinReward', MarketingCheckinReward],
|
||||
['MarketingCheckinConfig', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/checkin/config.uvue'))],
|
||||
['MarketingCheckinReward', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/checkin/reward.uvue'))],
|
||||
// 11. 渠道码 & 新人礼
|
||||
['MarketingChannelList', PlaceholderPage],
|
||||
['MarketingNewcomerGift', MarketingNewcomerGift],
|
||||
['MarketingNewcomerGift', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/newcomer/index.uvue'))],
|
||||
|
||||
// 内容模块
|
||||
['CmsArticle', CmsArticle],
|
||||
['CmsCategory', CmsCategory],
|
||||
['CmsArticle', defineAsyncComponent(() => import('@/pages/mall/admin/cms/article/list.uvue'))],
|
||||
['CmsCategory', defineAsyncComponent(() => import('@/pages/mall/admin/cms/category/list.uvue'))],
|
||||
|
||||
// 财务模块
|
||||
['FinanceTransactionStats', FinanceTransactionStats],
|
||||
['FinanceWithdrawal', FinanceWithdrawal],
|
||||
['FinanceInvoice', FinanceInvoice],
|
||||
['FinanceRecharge', FinanceRecharge],
|
||||
['FinanceCapitalFlow', FinanceCapitalFlow],
|
||||
['FinanceBill', FinanceBill],
|
||||
['FinanceCommission', FinanceCommission],
|
||||
['FinanceBalanceStats', FinanceBalanceStats],
|
||||
['FinanceBalanceRecord', FinanceBalanceRecord],
|
||||
['FinanceTransactionStats', defineAsyncComponent(() => import('@/pages/mall/admin/finance/transaction_stats.uvue'))],
|
||||
['FinanceWithdrawal', defineAsyncComponent(() => import('@/pages/mall/admin/finance/withdrawal.uvue'))],
|
||||
['FinanceInvoice', defineAsyncComponent(() => import('@/pages/mall/admin/finance/invoice.uvue'))],
|
||||
['FinanceRecharge', defineAsyncComponent(() => import('@/pages/mall/admin/finance/recharge.uvue'))],
|
||||
['FinanceCapitalFlow', defineAsyncComponent(() => import('@/pages/mall/admin/finance/capital_flow.uvue'))],
|
||||
['FinanceBill', defineAsyncComponent(() => import('@/pages/mall/admin/finance/bill.uvue'))],
|
||||
['FinanceCommission', defineAsyncComponent(() => import('@/pages/mall/admin/finance/commission.uvue'))],
|
||||
['FinanceBalanceStats', defineAsyncComponent(() => import('@/pages/mall/admin/finance/balance_stats.uvue'))],
|
||||
['FinanceBalanceRecord', defineAsyncComponent(() => import('@/pages/mall/admin/finance/balance_record.uvue'))],
|
||||
|
||||
// 数据模块 - 暂时使用占位组件
|
||||
['StatisticIndex', PlaceholderPage],
|
||||
|
||||
// 设置模块 - 暂时使用占位组件
|
||||
['SettingSystemConfig', PlaceholderPage],
|
||||
['SettingSystemAdmin', PlaceholderPage],
|
||||
['SettingSystemRole', PlaceholderPage],
|
||||
// 设置模块
|
||||
['SettingSystemConfig', defineAsyncComponent(() => import('@/pages/mall/admin/setting/system/config.uvue'))],
|
||||
['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],
|
||||
['DistributionList', PlaceholderPage],
|
||||
['DistributionConfig', PlaceholderPage],
|
||||
['DistributionPromoter', defineAsyncComponent(() => import('@/pages/mall/admin/distribution/promoter/index.uvue'))],
|
||||
['DistributionLevel', defineAsyncComponent(() => import('@/pages/mall/admin/distribution/level/index.uvue'))],
|
||||
['DistributionSetting', defineAsyncComponent(() => import('@/pages/mall/admin/distribution/setting/index.uvue'))],
|
||||
['DivisionList', defineAsyncComponent(() => import('@/pages/mall/admin/distribution/division/list.uvue'))],
|
||||
['DivisionAgent', defineAsyncComponent(() => import('@/pages/mall/admin/distribution/division/agent.uvue'))],
|
||||
['DivisionApply', defineAsyncComponent(() => import('@/pages/mall/admin/distribution/division/apply.uvue'))],
|
||||
|
||||
// 客服模块
|
||||
['KefuList', KefuList],
|
||||
['KefuWords', KefuWords],
|
||||
['KefuFeedback', KefuFeedback],
|
||||
['KefuAutoReply', KefuAutoReply],
|
||||
['KefuConfig', KefuConfig],
|
||||
['KefuList', defineAsyncComponent(() => import('@/pages/mall/admin/kefu/list.uvue'))],
|
||||
['KefuWords', defineAsyncComponent(() => import('@/pages/mall/admin/kefu/words.uvue'))],
|
||||
['KefuFeedback', defineAsyncComponent(() => import('@/pages/mall/admin/kefu/feedback.uvue'))],
|
||||
['KefuAutoReply', defineAsyncComponent(() => import('@/pages/mall/admin/kefu/auto_reply.uvue'))],
|
||||
['KefuConfig', defineAsyncComponent(() => import('@/pages/mall/admin/kefu/config.uvue'))],
|
||||
|
||||
// 装修模块
|
||||
['DecorationHome', DecorationHome],
|
||||
['DecorationCategory', DecorationCategory],
|
||||
['DecorationUser', DecorationUser],
|
||||
['DecorationData', DecorationData],
|
||||
['DecorationStyle', DecorationStyle],
|
||||
['DecorationMaterial', DecorationMaterial],
|
||||
['DecorationLink', DecorationLink],
|
||||
['DecorationHome', defineAsyncComponent(() => import('@/pages/mall/admin/decoration/home.uvue'))],
|
||||
['DecorationCategory', defineAsyncComponent(() => import('@/pages/mall/admin/decoration/category.uvue'))],
|
||||
['DecorationUser', defineAsyncComponent(() => import('@/pages/mall/admin/decoration/user.uvue'))],
|
||||
['DecorationData', defineAsyncComponent(() => import('@/pages/mall/admin/decoration/data-config.uvue'))],
|
||||
['DecorationStyle', defineAsyncComponent(() => import('@/pages/mall/admin/design/theme-style.uvue'))],
|
||||
['DecorationMaterial', defineAsyncComponent(() => import('@/pages/mall/admin/design/material.uvue'))],
|
||||
['DecorationLink', defineAsyncComponent(() => import('@/pages/mall/admin/design/link-management.uvue'))],
|
||||
|
||||
// 应用模块
|
||||
['AppStatistic', PlaceholderPage],
|
||||
['AppList', PlaceholderPage],
|
||||
|
||||
// 维护模块
|
||||
['MaintainDevConfig', MaintainDevConfig],
|
||||
['MaintainDevConfig', defineAsyncComponent(() => import('@/pages/mall/admin/maintain/dev/config.uvue'))],
|
||||
['MaintainDevData', PlaceholderPage],
|
||||
['MaintainDevTask', PlaceholderPage],
|
||||
['MaintainDevAuth', PlaceholderPage],
|
||||
|
||||
@@ -69,7 +69,7 @@ export const topMenus: TopMenu[] = [
|
||||
id: 'user',
|
||||
title: '用户',
|
||||
icon: 'user',
|
||||
path: '/pages/mall/admin/user/list',
|
||||
path: '/pages/mall/admin/user/management/index',
|
||||
order: 2,
|
||||
groups: [
|
||||
{ id: 'user-manage', title: '', order: 1 }
|
||||
@@ -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 }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -228,7 +231,7 @@ export const routes: RouteRecord[] = [
|
||||
{
|
||||
id: 'user_statistic',
|
||||
title: '用户统计',
|
||||
path: '/pages/mall/admin/user/Statistic',
|
||||
path: '/pages/mall/admin/user/statistics/index',
|
||||
componentKey: 'UserStatistic',
|
||||
parentId: 'user',
|
||||
groupId: 'user-manage',
|
||||
@@ -238,7 +241,7 @@ export const routes: RouteRecord[] = [
|
||||
{
|
||||
id: 'user_list',
|
||||
title: '用户管理',
|
||||
path: '/pages/mall/admin/user/list',
|
||||
path: '/pages/mall/admin/user/management/index',
|
||||
componentKey: 'UserList',
|
||||
parentId: 'user',
|
||||
groupId: 'user-manage',
|
||||
@@ -248,7 +251,7 @@ export const routes: RouteRecord[] = [
|
||||
{
|
||||
id: 'user_group',
|
||||
title: '用户分组',
|
||||
path: '/pages/mall/admin/user/group',
|
||||
path: '/pages/mall/admin/user/grouping/index',
|
||||
componentKey: 'UserGroup',
|
||||
parentId: 'user',
|
||||
groupId: 'user-manage',
|
||||
@@ -258,7 +261,7 @@ export const routes: RouteRecord[] = [
|
||||
{
|
||||
id: 'user_label',
|
||||
title: '用户标签',
|
||||
path: '/pages/mall/admin/user/label',
|
||||
path: '/pages/mall/admin/user/label/index',
|
||||
componentKey: 'UserLabel',
|
||||
parentId: 'user',
|
||||
groupId: 'user-manage',
|
||||
@@ -268,7 +271,7 @@ export const routes: RouteRecord[] = [
|
||||
{
|
||||
id: 'user_level',
|
||||
title: '用户等级',
|
||||
path: '/pages/mall/admin/user/level',
|
||||
path: '/pages/mall/admin/user/level/index',
|
||||
componentKey: 'UserLevel',
|
||||
parentId: 'user',
|
||||
groupId: 'user-manage',
|
||||
@@ -301,7 +304,7 @@ export const routes: RouteRecord[] = [
|
||||
{
|
||||
id: 'product_productClassify',
|
||||
title: '商品分类',
|
||||
path: '/pages/mall/admin/product/classify',
|
||||
path: '/pages/mall/admin/product/classification/index',
|
||||
componentKey: 'ProductClassify',
|
||||
parentId: 'product',
|
||||
groupId: 'product-manage',
|
||||
@@ -311,7 +314,7 @@ export const routes: RouteRecord[] = [
|
||||
{
|
||||
id: 'product_productAttr',
|
||||
title: '商品规格',
|
||||
path: '/pages/mall/admin/product/attr',
|
||||
path: '/pages/mall/admin/product/specifications/index',
|
||||
componentKey: 'ProductAttr',
|
||||
parentId: 'product',
|
||||
groupId: 'product-manage',
|
||||
@@ -321,7 +324,7 @@ export const routes: RouteRecord[] = [
|
||||
{
|
||||
id: 'product_paramList',
|
||||
title: '商品参数',
|
||||
path: '/pages/mall/admin/product/param',
|
||||
path: '/pages/mall/admin/product/parameters/index',
|
||||
componentKey: 'ProductParam',
|
||||
parentId: 'product',
|
||||
groupId: 'product-manage',
|
||||
@@ -331,7 +334,7 @@ export const routes: RouteRecord[] = [
|
||||
{
|
||||
id: 'product_labelList',
|
||||
title: '商品标签',
|
||||
path: '/pages/mall/admin/product/label',
|
||||
path: '/pages/mall/admin/product/labels/index',
|
||||
componentKey: 'ProductLabel',
|
||||
parentId: 'product',
|
||||
groupId: 'product-manage',
|
||||
@@ -341,7 +344,7 @@ export const routes: RouteRecord[] = [
|
||||
{
|
||||
id: 'product_protectionList',
|
||||
title: '商品保障',
|
||||
path: '/pages/mall/admin/product/protection',
|
||||
path: '/pages/mall/admin/product/protection/index',
|
||||
componentKey: 'ProductProtection',
|
||||
parentId: 'product',
|
||||
groupId: 'product-manage',
|
||||
@@ -351,7 +354,7 @@ export const routes: RouteRecord[] = [
|
||||
{
|
||||
id: 'product_productEvaluate',
|
||||
title: '商品评论',
|
||||
path: '/pages/mall/admin/product/reply',
|
||||
path: '/pages/mall/admin/product/reviews/index',
|
||||
componentKey: 'ProductReply',
|
||||
parentId: 'product',
|
||||
groupId: 'product-manage',
|
||||
@@ -892,9 +895,10 @@ export const routes: RouteRecord[] = [
|
||||
},
|
||||
|
||||
// ========== 设置模块 ==========
|
||||
// 1. 系统设置
|
||||
{
|
||||
id: 'setting_systemConfig',
|
||||
title: '系统配置',
|
||||
title: '系统设置',
|
||||
path: '/pages/mall/admin/setting/system/config',
|
||||
componentKey: 'SettingSystemConfig',
|
||||
parentId: 'setting',
|
||||
@@ -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',
|
||||
@@ -934,23 +1079,59 @@ export const routes: RouteRecord[] = [
|
||||
order: 1
|
||||
},
|
||||
{
|
||||
id: 'distribution_list',
|
||||
title: '分销员列表',
|
||||
path: '/pages/mall/admin/distribution/list',
|
||||
componentKey: 'DistributionList',
|
||||
id: 'distribution_promoter',
|
||||
title: '分销员管理',
|
||||
path: '/pages/mall/admin/distribution/promoter/index',
|
||||
componentKey: 'DistributionPromoter',
|
||||
parentId: 'distribution',
|
||||
groupId: 'distribution-manage',
|
||||
order: 2
|
||||
},
|
||||
{
|
||||
id: 'distribution_config',
|
||||
title: '分销设置',
|
||||
path: '/pages/mall/admin/distribution/config',
|
||||
componentKey: 'DistributionConfig',
|
||||
id: 'distribution_level',
|
||||
title: '分销等级',
|
||||
path: '/pages/mall/admin/distribution/level/index',
|
||||
componentKey: 'DistributionLevel',
|
||||
parentId: 'distribution',
|
||||
groupId: 'distribution-manage',
|
||||
order: 3
|
||||
},
|
||||
{
|
||||
id: 'distribution_setting',
|
||||
title: '分销设置',
|
||||
path: '/pages/mall/admin/distribution/setting/index',
|
||||
componentKey: 'DistributionSetting',
|
||||
parentId: 'distribution',
|
||||
groupId: 'distribution-manage',
|
||||
order: 4
|
||||
},
|
||||
{
|
||||
id: 'division_list',
|
||||
title: '事业部管理',
|
||||
path: '/pages/mall/admin/distribution/division/list',
|
||||
componentKey: 'DivisionList',
|
||||
parentId: 'distribution',
|
||||
groupId: 'distribution-manage',
|
||||
order: 5
|
||||
},
|
||||
{
|
||||
id: 'division_agent',
|
||||
title: '代理商管理',
|
||||
path: '/pages/mall/admin/distribution/division/agent',
|
||||
componentKey: 'DivisionAgent',
|
||||
parentId: 'distribution',
|
||||
groupId: 'distribution-manage',
|
||||
order: 6
|
||||
},
|
||||
{
|
||||
id: 'division_apply',
|
||||
title: '事业部申请',
|
||||
path: '/pages/mall/admin/distribution/division/apply',
|
||||
componentKey: 'DivisionApply',
|
||||
parentId: 'distribution',
|
||||
groupId: 'distribution-manage',
|
||||
order: 7
|
||||
},
|
||||
|
||||
// ========== 客服模块 ==========
|
||||
{
|
||||
@@ -1055,7 +1236,7 @@ export const routes: RouteRecord[] = [
|
||||
order: 6
|
||||
},
|
||||
{
|
||||
id: 'decoration_link',
|
||||
id: 'DecorationLink',
|
||||
title: '链接管理',
|
||||
path: '/pages/mall/admin/decoration/link',
|
||||
componentKey: 'DecorationLink',
|
||||
@@ -1064,6 +1245,33 @@ export const routes: RouteRecord[] = [
|
||||
order: 7
|
||||
},
|
||||
|
||||
// ========== 设置模块 (1:1 复刻 CRMEB 路由结构) ==========
|
||||
// 通知管理
|
||||
{ id: 'setting_message_index', title: '消息管理', path: '/pages/mall/admin/setting/message', componentKey: 'SettingMessageIndex', parentId: 'setting', groupId: 'setting-message', order: 1 },
|
||||
{ id: 'setting_protocol_index', title: '协议设置', path: '/pages/mall/admin/setting/agreement', componentKey: 'SettingProtocolIndex', parentId: 'setting', groupId: 'setting-message', order: 2 },
|
||||
{ id: 'setting_ticket_index', title: '小票配置', path: '/pages/mall/admin/setting/ticket', componentKey: 'SettingTicketIndex', parentId: 'setting', groupId: 'setting-message', order: 3 },
|
||||
|
||||
// 权限管理
|
||||
{ id: 'setting_auth_admin', title: '管理员管理', path: '/pages/mall/admin/setting/auth/admin', componentKey: 'SettingAuthAdmin', parentId: 'setting', groupId: 'setting-auth', order: 1 },
|
||||
{ id: 'setting_auth_role', title: '角色管理', path: '/pages/mall/admin/setting/auth/role', componentKey: 'SettingAuthRole', parentId: 'setting', groupId: 'setting-auth', order: 2 },
|
||||
{ id: 'setting_auth_menu', title: '菜单管理', path: '/pages/mall/admin/setting/auth/permission', componentKey: 'SettingAuthMenu', parentId: 'setting', groupId: 'setting-auth', order: 3 },
|
||||
|
||||
// 物流设置
|
||||
{ id: 'setting_delivery_courier', title: '配送员管理', path: '/pages/mall/admin/setting/delivery/staff', componentKey: 'SettingDeliveryCourier', parentId: 'setting', groupId: 'setting-delivery', order: 1 },
|
||||
{ id: 'setting_delivery_pickup_list', title: '提货点', path: '/pages/mall/admin/setting/delivery/station', componentKey: 'SettingDeliveryPickupList', parentId: 'setting', groupId: 'setting-delivery', order: 2 },
|
||||
{ id: 'setting_delivery_verifier', title: '核销员', path: '/pages/mall/admin/setting/delivery/station', componentKey: 'SettingDeliveryVerifier', parentId: 'setting', groupId: 'setting-delivery', order: 3 },
|
||||
{ id: 'setting_delivery_freight', title: '运费模板', path: '/pages/mall/admin/setting/delivery/template', componentKey: 'SettingDeliveryFreight', parentId: 'setting', groupId: 'setting-delivery', order: 4 },
|
||||
|
||||
// 接口设置
|
||||
{ id: 'setting_api_yht_page', title: '一号通页面', path: '/pages/mall/admin/setting/interface/onepass/index', componentKey: 'SettingApiYhtPage', parentId: 'setting', groupId: 'setting-interface', order: 1 },
|
||||
{ id: 'setting_api_yht_config', title: '一号通配置', path: '/pages/mall/admin/setting/interface/onepass/config', componentKey: 'SettingApiYhtConfig', parentId: 'setting', groupId: 'setting-interface', order: 2 },
|
||||
{ id: 'setting_api_storage', title: '系统存储配置', path: '/pages/mall/admin/setting/interface/storage', componentKey: 'SettingApiStorage', parentId: 'setting', groupId: 'setting-interface', order: 3 },
|
||||
{ id: 'setting_api_collect', title: '商品采集配置', path: '/pages/mall/admin/setting/interface/collect', componentKey: 'SettingApiCollect', parentId: 'setting', groupId: 'setting-interface', order: 4 },
|
||||
{ id: 'setting_api_logistics', title: '物流查询配置', path: '/pages/mall/admin/setting/interface/logistics', componentKey: 'SettingApiLogistics', parentId: 'setting', groupId: 'setting-interface', order: 5 },
|
||||
{ id: 'setting_api_waybill', title: '电子面单配置', path: '/pages/mall/admin/setting/interface/e-sheet', componentKey: 'SettingApiWaybill', parentId: 'setting', groupId: 'setting-interface', order: 6 },
|
||||
{ id: 'setting_api_sms', title: '短信接口配置', path: '/pages/mall/admin/setting/interface/sms', componentKey: 'SettingApiSms', parentId: 'setting', groupId: 'setting-interface', order: 7 },
|
||||
{ id: 'setting_api_pay', title: '商城支付配置', path: '/pages/mall/admin/setting/interface/payment', componentKey: 'SettingApiPay', parentId: 'setting', groupId: 'setting-interface', order: 8 },
|
||||
|
||||
// ========== 应用模块 ==========
|
||||
{
|
||||
id: 'app_statistic',
|
||||
|
||||
42
layouts/admin/router/settingSubSiderMenu.uts
Normal file
42
layouts/admin/router/settingSubSiderMenu.uts
Normal file
@@ -0,0 +1,42 @@
|
||||
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' },
|
||||
{ id: 'setting_protocol_index', title: '协议设置', type: 'page', path: '/pages/mall/admin/setting/agreement' },
|
||||
{ id: 'setting_ticket_index', title: '小票配置', type: 'page', path: '/pages/mall/admin/setting/ticket' },
|
||||
{ id: 'auth_group', title: '管理权限', type: 'group', children: [
|
||||
{ id: 'setting_auth_admin', title: '管理员管理', type: 'page', path: '/pages/mall/admin/setting/auth/admin' },
|
||||
{ id: 'setting_auth_role', title: '角色管理', type: 'page', path: '/pages/mall/admin/setting/auth/role' },
|
||||
{ id: 'setting_auth_menu', title: '菜单管理', type: 'page', path: '/pages/mall/admin/setting/auth/permission' }
|
||||
]
|
||||
},
|
||||
{ id: 'delivery_group', title: '发货设置', type: 'group', children: [
|
||||
{ id: 'setting_delivery_courier', title: '配送员管理', type: 'page', path: '/pages/mall/admin/setting/delivery/staff' },
|
||||
{ id: 'pickup_order_group', title: '提货点设置', type: 'group', children: [
|
||||
{ id: 'setting_delivery_pickup_list', title: '提货点', type: 'page', path: '/pages/mall/admin/setting/delivery/station' },
|
||||
{ id: 'setting_delivery_verifier', title: '核销员', type: 'page', path: '/pages/mall/admin/setting/delivery/station' },
|
||||
{ id: 'setting_delivery_freight', title: '运费模板', type: 'page', path: '/pages/mall/admin/setting/delivery/template' }
|
||||
]}
|
||||
]
|
||||
},
|
||||
{ id: 'api_group', title: '接口配置', type: 'group', children: [
|
||||
{ id: 'yh_tong_group', title: '一号通', type: 'group', children: [
|
||||
{ id: 'setting_api_yht_page', title: '一号通页面', type: 'page', path: '/pages/mall/admin/setting/interface/onepass/index' },
|
||||
{ id: 'setting_api_yht_config', title: '一号通配置', type: 'page', path: '/pages/mall/admin/setting/interface/onepass/config' }
|
||||
]
|
||||
},
|
||||
{ id: 'setting_api_storage', title: '系统存储配置', type: 'page', path: '/pages/mall/admin/setting/interface/storage' },
|
||||
{ id: 'setting_api_collect', title: '商品采集配置', type: 'page', path: '/pages/mall/admin/setting/interface/collect' },
|
||||
{ id: 'setting_api_logistics', title: '物流查询配置', type: 'page', path: '/pages/mall/admin/setting/interface/logistics' },
|
||||
{ id: 'setting_api_waybill', title: '电子面单配置', type: 'page', path: '/pages/mall/admin/setting/interface/e-sheet' },
|
||||
{ id: 'setting_api_sms', title: '短信接口配置', type: 'page', path: '/pages/mall/admin/setting/interface/sms' },
|
||||
{ id: 'setting_api_pay', title: '商城支付配置', type: 'page', path: '/pages/mall/admin/setting/interface/payment' }
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
buildDefaultTabs,
|
||||
getTopMenus
|
||||
} from '@/layouts/admin/router/adminRoutes.uts'
|
||||
import { addView, activeFullPath, visitedViews } from './tagsViewStore.uts'
|
||||
|
||||
/**
|
||||
* 标签页类型
|
||||
@@ -32,19 +33,40 @@ 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[]>([])
|
||||
|
||||
/** 是否折叠主侧边栏 */
|
||||
/** 是否折叠主侧边栏 (CRMEB: 70px) */
|
||||
export const isMainAsideCollapsed = ref<boolean>(false)
|
||||
|
||||
/** 是否显示二级侧边栏 */
|
||||
export const showSubSider = computed<boolean>(() => {
|
||||
const topMenus = getTopMenus()
|
||||
const activeMenu = topMenus.find(m => m.id === activeTopMenuId.value)
|
||||
return activeMenu ? activeMenu.groups.length > 0 : false
|
||||
/** 是否显示二级侧边栏状态控制 */
|
||||
export const showSubSider = ref<boolean>(true)
|
||||
|
||||
/** 屏幕宽度 */
|
||||
export const windowWidth = ref<number>(1024)
|
||||
|
||||
/** 布局模式:desktop | tablet | mobile */
|
||||
export const layoutMode = computed<string>(() => {
|
||||
if (windowWidth.value < 768) return 'mobile'
|
||||
if (windowWidth.value < 1200) return 'tablet'
|
||||
return 'desktop'
|
||||
})
|
||||
|
||||
/** 是否为移动端简易判断 */
|
||||
export const isMobile = computed<boolean>(() => layoutMode.value === 'mobile')
|
||||
|
||||
/** 移动端开关 */
|
||||
export const isMobileMenuOpen = ref<boolean>(false)
|
||||
|
||||
/** 遮罩层开关 (用于 tablet 的 subSider overlay 和 mobile 的 aside overlay) */
|
||||
export const isOverlayVisible = ref<boolean>(false)
|
||||
|
||||
// ============================================
|
||||
// Actions
|
||||
// ============================================
|
||||
@@ -67,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]
|
||||
@@ -101,6 +125,10 @@ function addTabItem(route: RouteRecord): void {
|
||||
isAffix: route.isAffix || false
|
||||
})
|
||||
}
|
||||
|
||||
// 更新新版 TagsViewStore
|
||||
addView(route, route.path)
|
||||
activeFullPath.value = route.path
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,6 +193,13 @@ export function toggleMainAsideCollapse(): void {
|
||||
isMainAsideCollapsed.value = !isMainAsideCollapsed.value
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换二级侧边栏显示状态 (Desktop 模式)
|
||||
*/
|
||||
export function toggleSubSider(): void {
|
||||
showSubSider.value = !showSubSider.value
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化导航状态
|
||||
* 在 AdminLayout 组件 onMounted 时调用
|
||||
@@ -179,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 变化
|
||||
}
|
||||
96
layouts/admin/styles/admin-responsive.css
Normal file
96
layouts/admin/styles/admin-responsive.css
Normal file
@@ -0,0 +1,96 @@
|
||||
/* 统一 KPI 统计网格响应式规范 */
|
||||
|
||||
.kpi-grid {
|
||||
display: grid !important;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 规则 1:屏幕宽度 >= 1200px -> 固定 4 列 */
|
||||
@media (min-width: 1200px) {
|
||||
.kpi-grid {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 规则 2:768px <= 屏幕宽度 < 1200px -> 固定 2 列 */
|
||||
@media (min-width: 768px) and (max-width: 1199.98px) {
|
||||
.kpi-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 规则 3:屏幕宽度 < 768px -> 固定 1 列 */
|
||||
@media (max-width: 767.98px) {
|
||||
.kpi-grid {
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 强制子项允许收缩,防止内部长文本撑爆网格 */
|
||||
.kpi-grid > *,
|
||||
.kpi-grid-6 > * {
|
||||
min-width: 0 !important;
|
||||
flex: none !important; /* 覆盖旧的 flex 逻辑 */
|
||||
width: auto !important; /* 让 grid 控制宽度 */
|
||||
}
|
||||
|
||||
/* 6-2-1 网格规范 (对标 CRMEB 统计概况) */
|
||||
.kpi-grid-6 {
|
||||
display: grid !important;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.kpi-grid-6 {
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) and (max-width: 1199.98px) {
|
||||
.kpi-grid-6 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.kpi-grid-6 {
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 后台通用紧凑布局规范 (CRMEB 1:1) ===== */
|
||||
|
||||
/* 页面根容器: 去除各页面自带的 padding,由 Layout 统一管理 */
|
||||
.admin-page {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
/* 垂直区块容器: 统一控制卡片、组件之间的间距 */
|
||||
.admin-sections {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--admin-section-gap);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 通用网格: 用于图表布局等 */
|
||||
.admin-grid {
|
||||
display: grid;
|
||||
gap: var(--admin-section-gap);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 统一卡片内部间距: 替代业务组件或页面内硬编码的 padding */
|
||||
.admin-card {
|
||||
padding: var(--admin-card-padding) !important;
|
||||
background: #ffffff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
Reference in New Issue
Block a user