继续完善页面

This commit is contained in:
2026-02-02 21:45:59 +08:00
parent f4af5dded9
commit 93b42a277a
19 changed files with 2785 additions and 672 deletions

View File

@@ -47,12 +47,12 @@
/>
<!-- 内容展示区 (内部路由渲染) -->
<scroll-view class="content" scroll-y="true">
<view class="content-scroll">
<view class="content-inner">
<component :is="currentComponent" />
</view>
<AdminFooter />
</scroll-view>
</view>
</view>
</view>
</template>
@@ -215,8 +215,9 @@ onMounted(() => {
background: #f0f2f5;
}
.content {
.content-scroll {
flex: 1;
overflow-y: scroll;
background: #f0f2f5;
}

View File

@@ -4,7 +4,7 @@
<text class="logo-text">{{ collapsed ? 'M' : 'MALL' }}</text>
</view>
<scroll-view class="aside-menu" scroll-y="true">
<view class="aside-menu">
<view
v-for="menu in topMenus"
:key="menu.id"
@@ -19,7 +19,7 @@
<text>{{ menu.title }}</text>
</view>
</view>
</scroll-view>
</view>
<view class="aside-footer" @click="onToggle">
<view class="toggle-btn">
@@ -46,15 +46,16 @@ const emit = defineEmits<{
function getIconText(icon: string): string {
const iconMap: Record<string, string> = {
'home': 'H',
'user': 'U',
'product': 'P',
'order': 'O',
'marketing': 'M',
'content': 'C',
'finance': 'F',
'statistic': 'S',
'setting': 'T'
'home': '🏠',
'user': '👥',
'product': '📦',
'order': '📜',
'marketing': '📉',
'content': '📝',
'finance': '💰',
'statistic': '📊',
'setting': '⚙️',
'maintenance': '🛠️'
}
return iconMap[icon] || icon.charAt(0).toUpperCase()
}
@@ -107,6 +108,7 @@ function onLogoClick(): void {
.aside-menu {
flex: 1;
padding: 8px 0;
overflow-y: scroll;
}
.menu-item {

View File

@@ -1,89 +0,0 @@
<template>
<view
class="aside"
:style="{ width: asideWidth + 'px' }"
>
<view class="aside-header">
<view class="logo">
<text class="logo-text"> 商城后台</text>
</view>
</view>
<view class="aside-menu">
<view
v-for="m in menuList"
:key="m.id"
class="aside-item"
:class="{ active: activeMenuId === m.id }"
@click="$emit('menu-click', m.id)"
>
<image class="aside-icon" :src="m.icon" mode="aspectFit" />
<text class="aside-title" v-if="!collapsed">{{ m.title }}</text>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import type { MenuItem } from '../types.uts'
defineProps<{
collapsed: boolean
menuList: MenuItem[]
activeMenuId: string
asideWidth:number
}>()
defineEmits<{
(e:'menu-click', menuId: string): void
}>()
</script>
<style>
.aside{
background: #1f2a37;
height: 100vh;
position: fixed;
left: 0;
top: 0;
bottom: 0;
display:flex;
flex-direction: column;
z-index: 1000;
}
.aside-header{
height: 56px;
display:flex;
flex-direction:row;
align-items:center;
justify-content: space-between;
padding: 0 12px;
border-bottom: 1px solid rgba(255,255,255,0.08);
}
.logo-text{
color:#fff;
font-size:20px;
font-weight:600;
display:flex;
justify-content:center;
align-items:center;
text-align:center;
}
.collapse-btn{ width:28px; height:28px; display:flex; align-items:center; justify-content:center; }
.collapse-text{ color:rgba(255,255,255,0.7); }
.aside-menu{ padding: 8px 0; }
.aside-item{
height: 54px;
display:flex;
flex-direction: column;
align-items:center;
justify-content:center;
gap: 6px;
margin: 6px 10px;
border-radius: 8px;
}
.aside-item.active{ background:#1677ff; }
.aside-icon{ width:22px; height:22px; }
.aside-title{ color:#fff; font-size:12px; }
</style>

View File

@@ -4,7 +4,7 @@
<text class="header-title">{{ topMenuTitle }}</text>
</view>
<scroll-view class="subsider-menu" scroll-y="true">
<view class="subsider-menu">
<view v-for="group in groups" :key="group.id" class="menu-group">
<view class="group-title">
<text>{{ group.title }}</text>
@@ -20,7 +20,7 @@
<text class="item-title">{{ route.title }}</text>
</view>
</view>
</scroll-view>
</view>
</view>
</template>
@@ -79,6 +79,7 @@ function onRouteClick(routeId: string): void {
.subsider-menu {
flex: 1;
padding: 8px 0;
overflow-y: scroll;
}
.menu-group {

View File

@@ -1,396 +0,0 @@
<template>
<view
class="sub-sider"
:style="{ left: props.asideWidth + 'px', width: props.siderWidth + 'px' }"
>
<view class="sub-header">
<text class="sub-title">{{ activeMenuTitle }}</text>
</view>
<scroll-view class="sub-body" scroll-y="true">
<view
v-for="g in groups"
:key="groupKey(g)"
class="group"
>
<!-- ✅ 改:点击标题时,如果该组没有 children 但有 path则当成“叶子菜单”直接跳 -->
<view class="group-title" @click="handleGroupTitleClick(g)">
<text class="group-title-text">{{ g.title }}</text>
<!-- ✅ 改:只有有 children 才显示箭头 -->
<text
v-if="groupChildren(g).length > 0"
class="group-arrow"
:class="{ open: isGroupOpen(groupKey(g)) }"
>v</text>
</view>
<!-- ✅ 改v-show 的条件不变,但 children 的遍历必须兜底 -->
<view v-show="isGroupOpen(groupKey(g))" class="group-children">
<view
v-for="c in groupChildren(g)"
:key="c.id"
class="sub-item"
:class="{ active: resolvedActiveId === c.id }"
@click.stop="handleClick(c)"
>
<text class="sub-item-text" :class="{ activeText: resolvedActiveId === c.id }">
{{ c.title }}
</text>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup lang="uts">
import { ref, computed, watch, withDefaults, onMounted } from 'vue'
import type { MenuGroup, MenuChild } from '../types.uts'
const props = withDefaults(defineProps<{
activeMenuTitle: string
groups: MenuGroup[]
activeSubId: string
activeMenuId?: string | null
asideWidth: number
siderWidth: number
}>(), {
activeMenuId: 'home',
})
const emit = defineEmits<{
(e: 'sub-click', child: MenuChild): void
}>()
/** 只展开一个分组(更像 CRMEB */
const openGroupKey = ref<string>('')
/** 给 group 一个稳定 key优先用 id如果你 types 里没有 id就用 title */
const groupKey = (g: MenuGroup): string => {
// @ts-ignore
const anyG: any = g as any
return (anyG.id && (anyG.id as string)) ? (anyG.id as string) : g.title
}
/** ✅ 核心group.children 兜底成 [](因为 children 现在可选/可空) */
const groupChildren = (g: MenuGroup): MenuChild[] => {
// @ts-ignore
const anyG: any = g as any
// @ts-ignore
const list = anyG.children as (MenuChild[] | null | undefined)
return list && list.length > 0 ? list : []
}
/** ✅ 兼容四级child.children 兜底成 [] */
const childChildren = (c: MenuChild): MenuChild[] => {
// @ts-ignore
const anyC: any = c as any
// @ts-ignore
const list = anyC.children as (MenuChild[] | null | undefined)
return list && list.length > 0 ? list : []
}
/** ✅ 叶子 group无 children如果有 path则构造一个“伪 MenuChild”用于 emit 跳转 */
const groupAsChild = (g: MenuGroup): MenuChild | null => {
// @ts-ignore
const anyG: any = g as any
// @ts-ignore
const p = anyG.path as (string | null | undefined)
if (!p) return null
return { id: groupKey(g), title: g.title, path: p } as MenuChild
}
const normalizePath = (p: string): string => {
if (!p) return ''
const s = p.startsWith('/') ? p.slice(1) : p
const q = s.indexOf('?')
return q >= 0 ? s.slice(0, q) : s
}
/** 扁平查找id -> child✅ 改:遍历时用 groupChildren/childChildren */
const findChildById = (id: string): MenuChild | null => {
if (!id) return null
for (const g of props.groups) {
// ✅ 叶子 group 也能命中
const gLeaf = groupAsChild(g)
if (gLeaf && gLeaf.id === id) return gLeaf
for (const c of groupChildren(g)) {
if (c.id === id) return c
const deep = childChildren(c)
if (deep.length > 0) {
const hit = findFirstMatchInChildren(id, deep)
if (hit) return hit
}
}
}
return null
}
const findFirstMatchInChildren = (id: string, list: MenuChild[]): MenuChild | null => {
for (const n of list) {
if (n.id === id) return n
const deep = childChildren(n)
if (deep.length > 0) {
const hit = findFirstMatchInChildren(id, deep)
if (hit) return hit
}
}
return null
}
const findChildByPath = (path: string): MenuChild | null => {
const target = normalizePath(path)
if (!target) return null
for (const g of props.groups) {
// ✅ 叶子 group 也能按 path 命中
const gLeaf = groupAsChild(g)
if (gLeaf && normalizePath(gLeaf.path) === target) return gLeaf
for (const c of groupChildren(g)) {
if (normalizePath(c.path) === target) return c
const deep = childChildren(c)
if (deep.length > 0) {
const hit = findFirstMatchByPath(target, deep)
if (hit) return hit
}
}
}
return null
}
const findFirstMatchByPath = (targetNorm: string, list: MenuChild[]): MenuChild | null => {
for (const n of list) {
if (normalizePath(n.path) === targetNorm) return n
const deep = childChildren(n)
if (deep.length > 0) {
const hit = findFirstMatchByPath(targetNorm, deep)
if (hit) return hit
}
}
return null
}
/** 找到某个 child 属于哪个 group用于自动展开该组✅ 改:遍历兜底) */
const findGroupKeyByChildId = (id: string): string => {
for (const g of props.groups) {
const gLeaf = groupAsChild(g)
if (gLeaf && gLeaf.id === id) return groupKey(g)
for (const c of groupChildren(g)) {
if (c.id === id) return groupKey(g)
const deep = childChildren(c)
if (deep.length > 0) {
const hit = findFirstMatchInChildren(id, deep)
if (hit) return groupKey(g)
}
}
}
return props.groups.length > 0 ? groupKey(props.groups[0]) : ''
}
/** 递归取第一个 leaf 菜单(✅ 改children 兜底) */
const firstLeaf = (): MenuChild | null => {
if (!props.groups || props.groups.length === 0) return null
const g0 = props.groups[0]
const g0Children = groupChildren(g0)
// ✅ 如果第一个 group 是叶子(无 children 但有 path也能返回
if (g0Children.length === 0) {
return groupAsChild(g0)
}
const c0 = g0Children[0]
const deep = childChildren(c0)
if (deep.length > 0) return findFirstLeafInChildren(deep)
return c0
}
const findFirstLeafInChildren = (list: MenuChild[]): MenuChild | null => {
if (!list || list.length === 0) return null
const n = list[0]
const deep = childChildren(n)
if (deep.length > 0) return findFirstLeafInChildren(deep)
return n
}
/** 高亮兜底:按 activeSubId -> route/path 再匹配一次 */
const resolvedActiveId = computed((): string => {
const byId = findChildById(props.activeSubId)
if (byId) return byId.id
try {
const pages = getCurrentPages()
if (pages && pages.length > 0) {
// @ts-ignore
const cur: any = pages[pages.length - 1]
// @ts-ignore
const route: string = cur.route ? (cur.route as string) : ''
const hit = findChildByPath(route)
if (hit) return hit.id
}
} catch (e) {}
return props.activeSubId || ''
})
const isGroupOpen = (key: string): boolean => {
if (!openGroupKey.value) {
return props.groups && props.groups.length > 0 ? groupKey(props.groups[0]) === key : false
}
return openGroupKey.value === key
}
const toggleGroup = (key: string) => {
openGroupKey.value = (openGroupKey.value === key) ? '' : key
}
/** ✅ 新增:点 group 标题时的行为 */
const handleGroupTitleClick = (g: MenuGroup) => {
const list = groupChildren(g)
// 有 children正常展开/收起
if (list.length > 0) {
toggleGroup(groupKey(g))
return
}
// 无 children如果有 path当成叶子菜单直接跳
const leaf = groupAsChild(g)
if (leaf) {
openGroupKey.value = groupKey(g)
emit('sub-click', leaf)
}
}
const handleClick = (c: MenuChild) => {
openGroupKey.value = findGroupKeyByChildId(c.id)
emit('sub-click', c)
}
/** 自动groups 变更/activeSubId 无效时,只做状态同步(展开对应 group不触发导航 */
const ensureDefault = () => {
if (!props.groups || props.groups.length === 0) return
const hit = findChildById(props.activeSubId)
if (hit) { openGroupKey.value = findGroupKeyByChildId(hit.id); return }
// ✅ 再按当前 route 找一次
try {
const pages = getCurrentPages()
// @ts-ignore
const cur: any = pages[pages.length - 1]
const route: string = cur?.route ?? ''
const byRoute = findChildByPath(route)
if (byRoute) { openGroupKey.value = findGroupKeyByChildId(byRoute.id); return }
} catch(e) {}
const first = firstLeaf()
if (first) openGroupKey.value = findGroupKeyByChildId(first.id)
}
// ✅ 移除 watch(immediate: true) 中的自动 emit仅在 groups/activeSubId 变更时同步状态
watch(
() => props.groups,
() => { ensureDefault() },
{ immediate: false, deep: true }
)
watch(
() => props.activeSubId,
() => { ensureDefault() },
{ immediate: false }
)
// ✅ 初始化时只做一次状态同步(不通过 watch immediate
onMounted(() => {
ensureDefault()
})
</script>
<style>
/* 原样保留你的样式 */
.sub-sider{
background:#ffffff;
border-right: 1px solid #e5e7eb;
height: 100vh;
position: fixed;
top: 0;
bottom: 0;
z-index: 900;
}
.sub-header{
height: 56px;
display:flex;
align-items:center;
border-bottom: 1px solid #eef2f7;
}
.sub-title{
font-size:16px;
font-weight:700;
color:#111827;
display:flex;
justify-content:center;
align-items:center;
}
.sub-body{
height: calc(100vh - 56px);
}
.group{ padding: 8px 0; }
.group-title{
height: 42px;
margin: 8px 12px 6px 12px;
padding: 0 14px;
display:flex;
align-items:center;
justify-content: space-between;
background: #f6f7fb;
border-radius: 8px;
}
.group-title-text{
font-size: 14px;
font-weight: 700;
color:#111827;
}
.group-arrow{
font-size: 12px;
color: #6b7280;
transform: rotate(0deg);
}
.group-arrow.open{
transform: rotate(180deg);
}
.group-children{
padding: 0 12px 6px 12px;
}
.sub-item{
height: 34px;
margin: 4px 0;
padding: 0 14px 0 28px;
display:flex;
align-items:center;
border-radius: 8px;
}
.sub-item.active{
background: #eaf2ff;
}
.sub-item-text{
font-size: 13px;
color:#374151;
}
.sub-item-text.activeText{
color:#1677ff;
font-weight: 700;
}
</style>

View File

@@ -18,15 +18,18 @@ import PlaceholderPage from '@/layouts/admin/components/PlaceholderPage.uvue'
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'
// 其他用户模块组件暂时使用 PlaceholderPage
// import UserGradeType from '@/pages/mall/admin/user/grade/type.uvue'
// import UserGradeCard from '@/pages/mall/admin/user/grade/card.uvue'
// import UserGradeRecord from '@/pages/mall/admin/user/grade/record.uvue'
// import UserGradeRight from '@/pages/mall/admin/user/grade/right.uvue'
// import UserMemberConfig from '@/pages/mall/admin/user/MemberConfig.uvue'
// 导入商品模块(纯组件,不包含 AdminLayout
import ProductList from '@/pages/mall/admin/product/list.uvue'
@@ -39,6 +42,11 @@ 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'
// 营销、内容、财务、数据、设置模块暂时使用 PlaceholderPage
// 避免循环依赖问题
@@ -63,10 +71,12 @@ export const componentMap: Map<string, any> = new Map([
['HomeIndex', HomeIndex],
// 用户模块
['UserStatistic', UserStatistic],
['UserList', UserList],
['UserLevel', UserLevel],
['UserGroup', UserGroup],
['UserLabel', UserLabel],
['UserMemberConfig', MemberConfig],
['UserGradeType', PlaceholderPage], // 暂时使用占位组件
['UserGradeCard', PlaceholderPage],
['UserGradeRecord', PlaceholderPage],
@@ -83,6 +93,11 @@ export const componentMap: Map<string, any> = new Map([
// 订单模块
['OrderList', OrderList],
['OrderStatistic', OrderStatistic],
['OrderRefund', OrderRefund],
['OrderCashier', OrderCashier],
['OrderVerify', OrderVerify],
['OrderConfig', OrderConfig],
// 营销模块 - 暂时使用占位组件
['MarketingCoupon', PlaceholderPage],

View File

@@ -72,8 +72,8 @@ export const topMenus: TopMenu[] = [
path: '/pages/mall/admin/user/list',
order: 2,
groups: [
{ id: 'user-manage', title: '用户管理', order: 1 },
{ id: 'user-grade', title: '会员管理', order: 2 }
{ id: 'user-manage', title: '', order: 1 },
{ id: 'member-manage', title: '会员管理', order: 2 }
]
},
{
@@ -93,7 +93,7 @@ export const topMenus: TopMenu[] = [
path: '/pages/mall/admin/order/list',
order: 4,
groups: [
{ id: 'order-manage', title: '订单管理', order: 1 }
{ id: 'order-manage', title: '', order: 1 }
]
},
{
@@ -168,6 +168,16 @@ export const routes: RouteRecord[] = [
},
// ========== 用户模块 ==========
{
id: 'user_statistic',
title: '用户统计',
path: '/pages/mall/admin/user/Statistic',
componentKey: 'UserStatistic',
parentId: 'user',
groupId: 'user-manage',
auth: ['admin-user-statistic-index'],
order: 1
},
{
id: 'user_list',
title: '用户管理',
@@ -176,16 +186,6 @@ export const routes: RouteRecord[] = [
parentId: 'user',
groupId: 'user-manage',
auth: ['admin-user-user-index'],
order: 1
},
{
id: 'user_level',
title: '用户等级',
path: '/pages/mall/admin/user/level',
componentKey: 'UserLevel',
parentId: 'user',
groupId: 'user-manage',
auth: ['user-user-level'],
order: 2
},
{
@@ -208,15 +208,35 @@ export const routes: RouteRecord[] = [
auth: ['user-user-label'],
order: 4
},
{
id: 'user_level',
title: '用户等级',
path: '/pages/mall/admin/user/level',
componentKey: 'UserLevel',
parentId: 'user',
groupId: 'user-manage',
auth: ['user-user-level'],
order: 5
},
{
id: 'user_member_config',
title: '用户配置',
path: '/pages/mall/admin/user/MemberConfig',
componentKey: 'UserMemberConfig',
parentId: 'user',
groupId: 'user-manage',
auth: ['admin-user-member-config'],
order: 6
},
{
id: 'user_type',
title: '会员类型',
path: '/pages/mall/admin/user/grade/type',
componentKey: 'UserGradeType',
parentId: 'user',
groupId: 'user-grade',
groupId: 'member-manage',
auth: ['admin-user-member-type'],
order: 5
order: 1
},
{
id: 'user_card',
@@ -224,9 +244,9 @@ export const routes: RouteRecord[] = [
path: '/pages/mall/admin/user/grade/card',
componentKey: 'UserGradeCard',
parentId: 'user',
groupId: 'user-grade',
groupId: 'member-manage',
auth: ['admin-user-grade-card'],
order: 6
order: 2
},
{
id: 'user_record',
@@ -234,9 +254,9 @@ export const routes: RouteRecord[] = [
path: '/pages/mall/admin/user/grade/record',
componentKey: 'UserGradeRecord',
parentId: 'user',
groupId: 'user-grade',
groupId: 'member-manage',
auth: ['admin-user-grade-record'],
order: 7
order: 3
},
{
id: 'user_right',
@@ -244,9 +264,9 @@ export const routes: RouteRecord[] = [
path: '/pages/mall/admin/user/grade/right',
componentKey: 'UserGradeRight',
parentId: 'user',
groupId: 'user-grade',
groupId: 'member-manage',
auth: ['admin-user-grade-right'],
order: 8
order: 4
},
// ========== 商品模块 ==========
@@ -323,6 +343,16 @@ export const routes: RouteRecord[] = [
},
// ========== 订单模块 ==========
{
id: 'order_statistic',
title: '订单统计',
path: '/pages/mall/admin/order/statistic',
componentKey: 'OrderStatistic',
parentId: 'order',
groupId: 'order-manage',
auth: ['admin-order-statistic-index'],
order: 1
},
{
id: 'order_list',
title: '订单管理',
@@ -332,7 +362,47 @@ export const routes: RouteRecord[] = [
groupId: 'order-manage',
auth: ['admin-order-storeOrder-index'],
keepAlive: true,
order: 1
order: 2
},
{
id: 'order_refund',
title: '售后订单',
path: '/pages/mall/admin/order/refund',
componentKey: 'OrderRefund',
parentId: 'order',
groupId: 'order-manage',
auth: ['admin-order-refund-index'],
order: 3
},
{
id: 'order_cashier',
title: '收银订单',
path: '/pages/mall/admin/order/cashier',
componentKey: 'OrderCashier',
parentId: 'order',
groupId: 'order-manage',
auth: ['admin-order-cashier-index'],
order: 4
},
{
id: 'order_verify',
title: '核销记录',
path: '/pages/mall/admin/order/verify',
componentKey: 'OrderVerify',
parentId: 'order',
groupId: 'order-manage',
auth: ['admin-order-verify-index'],
order: 5
},
{
id: 'order_config',
title: '订单配置',
path: '/pages/mall/admin/order/config',
componentKey: 'OrderConfig',
parentId: 'order',
groupId: 'order-manage',
auth: ['admin-order-config-index'],
order: 6
},
// ========== 营销模块 ==========