merge: branch 'huangzhenbao-admin' into comclib-analytics, keeping local RPC integration versions

This commit is contained in:
comlibmb
2026-02-11 17:23:01 +08:00
92 changed files with 5500 additions and 1735 deletions

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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;

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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],

View File

@@ -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',

View 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' }
]
}
]

View File

@@ -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)
}

View 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 变化
}

View 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;
}
}
/* 规则 2768px <= 屏幕宽度 < 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;
}