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

@@ -9,6 +9,9 @@
</script> </script>
<style> <style>
/* 引入管理后台通用响应式样式 */
@import "@/layouts/admin/styles/admin-responsive.css";
/* ===== 全局重置样式 ===== */ /* ===== 全局重置样式 ===== */
* { * {
margin: 0; margin: 0;

25
check_tags.py Normal file
View File

@@ -0,0 +1,25 @@
import os
def check_tags(file_path):
print(f"Checking {file_path}")
try:
content = open(file_path, 'r', encoding='utf-8').read()
tags = ['template', 'script', 'style', 'view', 'text', 'image', 'scroll-view']
for tag in tags:
o = content.count(f'<{tag}')
c = content.count(f'</{tag}>')
if o != c:
print(f" [ERROR] {tag}: open={o}, close={c}")
else:
print(f" [OK] {tag}: {o}")
except Exception as e:
print(f" [ERROR] Failed to read: {e}")
files = [
r'd:\骅锋\mall\pages\mall\admin\order\order-statistics\index.uvue',
r'd:\骅锋\mall\pages\mall\admin\order\list.uvue',
r'd:\骅锋\mall\pages\mall\admin\order\order-configuration\index.uvue'
]
for f in files:
check_tags(f)

View File

@@ -11,6 +11,114 @@
- **解决方案**: 确保替换操作覆盖文件的完整生命周期,或者在发现 500 错误时检查文件末尾是否有残留的旧标签。 - **解决方案**: 确保替换操作覆盖文件的完整生命周期,或者在发现 500 错误时检查文件末尾是否有残留的旧标签。
- **预防**: 优先使用 `create_file` 或子代理重写整个文件,而非局部替换复杂的 SFC 结构。 - **预防**: 优先使用 `create_file` 或子代理重写整个文件,而非局部替换复杂的 SFC 结构。
#### **原因十二KPI 统计网格响应式不一致 (用户体验红线)**
- **现象**: 某些宽度下出现一行 3 个卡片,导致视觉不平衡或数据展示拥挤。
- **原因**: 使用了 `repeat(auto-fit/auto-fill, ...)` 或基于 `min-width` 的 flex 自动布局。
- **解决方案**:
1. 使用全局统一类 `.kpi-grid`
2. 严禁使用 `auto-fit/auto-fill`
3. 必须显式使用视图断点拦截:
- `>= 1200px`: 固定 4 列 (`grid-template-columns: repeat(4, minmax(0, 1fr))`)。
- `768px - 1199px`: 固定 2 列 (`grid-template-columns: repeat(2, minmax(0, 1fr))`)。
- `< 768px`: 固定 1 列 (`grid-template-columns: repeat(1, minmax(0, 1fr))`)。
4. 使用 `minmax(0, 1fr)` 配分子项 `min-width: 0` 确保在任何容器宽度下网格不被撑爆。
- **强制规则**: 任何页面都不允许出现一行 3 个卡片的情况。
#### **原因十三:侧边栏响应式断点与 Overlay 冲突 (严重体验红线)**
- **现象**: 在 770px~1005px 宽度下,侧边栏遮挡内容,或内容区没有正确让出空间。
- **原因**:
1. 断点逻辑不统一:`main` 布局计算与组件显隐逻辑使用了不同的宽度阈值。
2. 动画不匹配:内容区 `margin-left` 动画时长与侧栏 `transform` 时长不一致。
3. 状态残留:跨断点时没有强制重置 Overlay 状态。
- **1:1 复刻 CRMEB 解决方案**:
1. **明确三段断点策略**:
- **Desktop (>=1200px)**: `aside=dock`, `subSider=dock` (如果开启)。`mainLeft = 270px`
- **Tablet (768px-1199px)**: `aside=dock`, `subSider=overlay` (带 mask)。`mainLeft = 70px`
- **Mobile (<768px)**: `aside=overlay`, `subSider=overlay``mainLeft = 0px`
2. **统一状态机**: 使用 `layoutMode` (desktop/tablet/mobile) 驱动所有组件渲染,而非散乱的媒体查询。
3. **计算属性驱动布局**: `mainLeft` 必须严格根据 `layoutMode``subSider` 的 Dock/Overlay 属性动态计算。
4. **跨断点清理**: 在 `onWindowResize` 监听到模式切换时,立即强制关闭所有 Overlay (mask=false),防止残影遮挡。
5. **指针事件隔离**: 隐藏态侧栏必须显式设置 `pointer-events: none``visibility: hidden`
#### **原因十四KPI 统计概况列数不一致 (CRMEB 像素级规范)**
- **现象**: 统计概况在大屏下显示 4 列或 5 列,导致无法一行平铺 6 个核心指标。
- **解决方案 (6-2-1 规则)**:
1. 使用全局类 `.kpi-grid-6` 实现专用的统计概况布局。
2. **强制断点**:
- `> 1200px`: 固定 6 列 (`repeat(6, minmax(0, 1fr))`)。
- `768px - 1199.98px`: 固定 2 列。
- `< 768px`: 固定 1 列。
3. **侧栏联动**: 当 `viewport < 768px` 时,布局容器必须切换到移动端模式(主侧栏变为 OverlaymainLeft 归零),确保 1 列布局拥有最大水平空间。
#### **原因十五ECharts 图表响应式裁切与视觉偏位**
- **现象**: 窗口缩小时饼图被砍掉一半,或在大屏下视觉不居中、底部不对齐。
- **原因**:
1. 使用了固定高度(如 `height: 521px`)而没有弹性容器。
2. ECharts 的 `center``radius` 使用了静态百分比或固定像素,无法适配极端宽高比。
3. 仅依赖 `window.resize` 而没有监听“容器级”尺寸变化(如侧边栏折叠导致的局部宽度变化)。
- **解决方案**:
1. **容器加固**: 移除根组件固定高度,改用 `min-height`;卡片内容区设置 `overflow: visible` 防止裁切。
2. **像素级算法**: 禁止在 `option` 中直接硬编码 `['50%', '60%']`,应计算:`outerRadius = min(w, h) * 0.38``centerY = legendSpace + (h - legendSpace) / 2`
3. **双重自适应**:
- **底层**: 使用 `ResizeObserver` 监听容器级 DOM 变化。
- **上层**: 监听 `store` 中的侧边栏状态,在动画结束后强制触发 `refreshSize()`
4. **文字同步**: 中间文字total-value必须通过 `computed` 样式与饼图中心点像素级同步。
5. **架构建议**: 在 UTS 环境下,涉及 ECharts 等需要向 RenderJS 传递复杂 Object 的组件,**优先使用 Options API**。Options API 在处理 `toPlainObject` 转换及 Prop 传递时具备更稳定的兼容性,可避免 `script setup` 下可能出现的对象元数据干扰。
#### **原因十六ECharts 响应式失效与百分比布局规范**
- **现象**: 侧边栏折叠时图表不缩小导致溢出,或在不同宽高比下圆环变形/裁切。
- **原因**:
1. 使用了固定的像素值定义 `radius``center`
2. 容器高度依赖父级 `flex` 且未设置 `min-height`,导致某些极端高度下被折叠。
3. 缺乏对动画中间态的捕获。
- **强制解决方案 (Web/uni-app-x)**:
1. **百分比优先**: `radius` 必须使用百分比(如 `['55%', '75%']``center` 必须使用百分比。
2. **高度钳制**: 容器使用 `height: clamp(...)` 或固定高度(如 `520px`+ `min-height`
3. **全链路 Resize**:
- `ResizeObserver` 监听 DOM 容器变化。
- `transitionend` 监听侧边栏动画结束。
- `watch` 监听全局布局状态(`layoutMode` / `collapsed`)。
4. **裁切隔离**: 所有祖先容器、`ec-wrap``ec-canvas` 必须显式设置 `overflow: visible !important`
#### **原因十七ECharts 画布溢出与容器约束 (Containing Block 丢失)**
- **现象**: `ec-canvas` (uni-view) 拥有极大的 `width/height` (如 948px) 且 `position: absolute`,但脱离了父级卡片,溢出到整个页面,且图表内容消失。
- **原因**:
1. **层叠上下文丢失**: `ec-canvas` 的父组件或 `EChartsView` 根节点未设置 `position: relative`,导致 absolute 元素相对于 `body` 定位。
2. **尺寸初始化瓶颈**: 在父容器高度为 0 (如 `flex` 自动压缩) 或动画中间态初始化 ECharts导致内部 `canvas` 宽高计算错误。
- **强制解决方案**:
1. **修正 Containing Block (Step 1)**:
- 容器(`chart-wrap`)必须显式设置 `position: relative !important`
- 使用 `:deep()` 强制约束子组件:`.ec-wrap { position: relative; } .ec-canvas { position: absolute; inset: 0; width: 100% !important; height: 100% !important; }`
2. **确定性高度策略 (Step 2)**:
- 图表容器禁止高度塌陷。使用 `height: clamp(min, preferred, max)` 或固定高度。
- 示例:`height: clamp(280px, 40vh, 450px); min-height: 280px;`
3. **Resize 闭环链路 (Step 3)**:
- 禁止使用 `setTimeout` 盲猜。
- 必须使用 `ResizeObserver` 监听 `chart-wrap` 尺寸变化。
- 必须监听 `sidebar``transitionend` 事件,确保侧边栏动画结束后的布局稳定。
- 统一使用 `requestAnimationFrame(() => chart.resize())` 确保在下一次重绘前完成布局对齐。
4. **百分比布局 (Step 4)**:
- `series.pie``radius``center` 必须使用百分比形式,杜绝 px 导致的自适应失败。
#### **原因十八CRMEB 响应式断点与图表重构规范 (1:1 复刻)**
- **现象**: 在 1200px 断点切换时布局生硬,图表在单列模式下比例过小或中心偏移。
- **解决方案**:
1. **Grid 布局容器**: 容器页面必须使用 CSS Grid 定义 `grid-template-columns: 2fr 1fr` (>=1200) 和 `1fr` (<1200),避免 flex 宽度计算误差。
2. **外部图例隔离**: 为保证 ECharts 渲染空间的确定性,禁止使用内置 `legend`。改为 `flex-direction: row` 的外部 `legend-col`,确保左上角图例与右侧饼图互不干扰。
3. **确定性中心同步**:
- ECharts `center` 设置为 `['50%', '50%']`
- 中心文字组件使用绝对定位 `top:50%; left:50%; transform:translate(-50%,-50%)` 实现物理像素级对齐。
4. **两档响应式高度**:
- `chart-col` 在桌面端 (>=1200) 使用中等高度 (约 320-360px)。
- 在移动端/窄屏 (<1200) 自动扩展为大高度 (约 500-600px),以匹配全宽展示的视觉张力。
## 🛠️ 完整修复流程 ## 🛠️ 完整修复流程
``` ```

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

27
find_tags.py Normal file
View File

@@ -0,0 +1,27 @@
import re
def find_mismatched_text_tags(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
stack = []
print(f"Scanning {file_path}...")
for i, line in enumerate(lines):
# Use a better regex to find opening and closing tags separately
open_tags = re.findall(r'<text\b', line)
close_tags = re.findall(r'</text>', line)
for _ in open_tags:
stack.append(i + 1)
for _ in close_tags:
if stack:
stack.pop()
else:
print(f" Extra closing tag </text> at line {i+1}")
for line_num in stack:
print(f" Unclosed <text> tag at line {line_num}")
find_mismatched_text_tags(r'd:\骅锋\mall\pages\mall\admin\order\order-configuration\index.uvue')
find_mismatched_text_tags(r'd:\骅锋\mall\pages\mall\admin\order\order-statistics\index.uvue')
find_mismatched_text_tags(r'd:\骅锋\mall\pages\mall\admin\order\list.uvue')

View File

@@ -1,8 +1,17 @@
<template> <template>
<view class="layout-root"> <view class="layout-root">
<!-- 主侧边栏 (CRMEB风格) --> <!-- 统一遮罩层 (复刻 CRMEB: 用于所有 Overlay 状态) -->
<view
class="mobile-mask"
:class="{ 'mask-show': isOverlayVisible || (isMobile && isMobileMenuOpen) }"
@click="closeAllMenu"
></view>
<!-- 主侧边栏 (CRMEB风格: 70px) -->
<AdminAside <AdminAside
:collapsed="isMainAsideCollapsed" class="admin-sidebar"
:class="{ 'mobile-aside-open': isMobileMenuOpen }"
:collapsed="false"
:topMenus="topMenus" :topMenus="topMenus"
:activeTopMenuId="activeTopMenuId" :activeTopMenuId="activeTopMenuId"
@toggle="toggleMainAsideCollapse" @toggle="toggleMainAsideCollapse"
@@ -10,46 +19,52 @@
:asideWidth="ASIDE_W" :asideWidth="ASIDE_W"
/> />
<!-- 二级侧边栏 (CRMEB风格 - 内容区左侧) --> <!-- 二级侧边栏 (1:1 复刻 CRMEB 抽屉/Dock 平滑切换) -->
<AdminSubSider <AdminSubSider
v-if="showSubSider" :visible="isSubSiderVisible"
:topMenuTitle="activeTopMenuTitle" :isOverlay="isOverlayVisible || layoutMode === 'mobile'"
:groups="activeGroups" :layoutMode="layoutMode"
:routes="activeRoutes" :menuTitle="activeTopMenuTitle"
:activeRouteId="activeRouteId" :menuTree="activeMenuTree"
:asideWidth="ASIDE_W" :activeId="activeRouteId"
:siderWidth="SUB_W" :currentPath="currentRoutePath"
@route-click="onRouteClick" :asideWidth="layoutMode === 'mobile' ? 0 : ASIDE_W"
:width="SUB_W"
@sub-click="onSubClick"
/> />
<!-- 右侧内容区 --> <!-- 右侧内容区 -->
<view <view
class="main" class="main"
:style="{ marginLeft: mainLeft }" :style="{ marginLeft: isMobile ? '0' : mainLeft }"
> >
<!-- 顶部导航栏 --> <!-- 顶部导航栏 -->
<AdminHeader <AdminHeader
:breadcrumb="breadcrumb" :breadcrumb="breadcrumb"
:hasNotification="hasNotification" :hasNotification="hasNotification"
:isMobile="isMobile"
@search="onSearch" @search="onSearch"
@refresh="onRefresh" @refresh="onRefresh"
@notify="onNotify" @notify="onNotify"
/> />
<!-- 标签页 (CRMEB风格) --> <!-- 标签页 (CRMEB风格) - 移动端可以隐藏或滚动 -->
<AdminTagsView <AdminTagsView
v-if="!isMobile"
:tabs="tabs" :tabs="tabs"
:activeTabId="activeRouteId" :activeTabId="activeRouteId"
@tab-click="onTabClick" @tab-click="onTabClick"
@tab-close="onTabClose" @tab-close="onTabClose"
@close-other="onCloseOther" @close-other="onCloseOther"
@close-all="onCloseAll" @close-all="onCloseAll"
@refresh="onRefresh"
/> />
<!-- 内容展示区 (内部路由渲染) --> <!-- 内容展示区 (内部路由渲染) -->
<view class="content-scroll"> <view class="content-scroll">
<view class="content-inner"> <view class="content-inner" :class="{ 'is-mobile': isMobile }">
<component :is="currentComponent" /> <component :is="currentComponent" v-if="!isPageLoading" />
<AdminPageLoading v-if="isPageLoading" />
</view> </view>
<AdminFooter /> <AdminFooter />
</view> </view>
@@ -66,6 +81,7 @@ import AdminSubSider from '@/layouts/admin/components/AdminSubSider.uvue'
import AdminHeader from '@/layouts/admin/components/AdminHeader.uvue' import AdminHeader from '@/layouts/admin/components/AdminHeader.uvue'
import AdminTagsView from '@/layouts/admin/components/AdminTagsView.uvue' import AdminTagsView from '@/layouts/admin/components/AdminTagsView.uvue'
import AdminFooter from '@/layouts/admin/components/AdminFooter.uvue' import AdminFooter from '@/layouts/admin/components/AdminFooter.uvue'
import AdminPageLoading from '@/layouts/admin/components/AdminPageLoading.uvue'
import { import {
getTopMenus, getTopMenus,
@@ -76,34 +92,132 @@ import {
} from '@/layouts/admin/router/adminRoutes.uts' } from '@/layouts/admin/router/adminRoutes.uts'
import type { TopMenu, MenuGroup, RouteRecord } 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 { import {
activeTopMenuId, activeTopMenuId,
activeRouteId, activeRouteId,
lastSubIdByMenu,
tabs, tabs,
isMainAsideCollapsed, isMainAsideCollapsed,
showSubSider, showSubSider,
windowWidth,
layoutMode,
isMobile,
isMobileMenuOpen,
isOverlayVisible,
openRoute, openRoute,
closeTab, closeTab,
closeOtherTabs, closeOtherTabs,
closeAllTabs, closeAllTabs,
toggleMainAsideCollapse as storeToggleCollapse, toggleMainAsideCollapse as storeToggleCollapse,
toggleSubSider as storeToggleSubSider,
initNavState initNavState
} from '@/layouts/admin/store/adminNavStore.uts' } from '@/layouts/admin/store/adminNavStore.uts'
import type { TabItem } from '@/layouts/admin/store/adminNavStore.uts' import type { TabItem } from '@/layouts/admin/store/adminNavStore.uts'
import { getComponent } from '@/layouts/admin/router/adminComponentMap.uts' import { getComponent } from '@/layouts/admin/router/adminComponentMap.uts'
// 侧边栏宽度配置 // 侧边栏宽度配置 (CRMEB 1:1)
const ASIDE_W = 96 // 主侧边栏宽度 const ASIDE_W = 70
const SUB_W = 180 // 二级侧边栏宽度 const SUB_W = 200
// 页面加载状态
const isPageLoading = ref(false)
const hasNotification = ref<boolean>(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 mainLeft = computed<string>(() => {
const asideWidth = isMainAsideCollapsed.value ? 0 : ASIDE_W if (layoutMode.value === 'mobile') {
const subWidth = showSubSider.value ? SUB_W : 0 return '0px'
return (asideWidth + subWidth) + 'px' }
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) 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 { function onTopMenuClick(menu: TopMenu): void {
activeTopMenuId.value = menu.id 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') openRoute(menu.id + '_index')
} }
} }
function onRouteClick(routeId: string): void { function onSubClick(payload: { id: string, path: string }): void {
openRoute(routeId) // 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 { function onTabClick(tab: TabItem): void {
@@ -178,6 +350,12 @@ function toggleMainAsideCollapse(): void {
storeToggleCollapse() storeToggleCollapse()
} }
function closeAllMenu(): void {
isMobileMenuOpen.value = false
isOverlayVisible.value = false
// 注意:在 Desktop 模式下closeAllMenu 通常不隐藏 showSubSider除非用户手动点汉堡按钮
}
function onSearch(): void { function onSearch(): void {
uni.showToast({ title: '搜索', icon: 'none' }) uni.showToast({ title: '搜索', icon: 'none' })
} }
@@ -194,6 +372,8 @@ function onNotify(): void {
// 生命周期 // 生命周期
// ============================================ // ============================================
let resizeTid: any = null
onMounted(async () => { onMounted(async () => {
if (!ensureAnalyticsLogin({ toastTitle: '请先登录以访问管理后台' })) return if (!ensureAnalyticsLogin({ toastTitle: '请先登录以访问管理后台' })) return
@@ -208,16 +388,84 @@ onMounted(async () => {
} }
initNavState() 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> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.layout-root { .layout-root {
--admin-page-padding-desktop: 12px;
--admin-page-padding-mobile: 8px;
--admin-section-gap: 12px;
--admin-card-padding: 16px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
width: 100%; width: 100%;
min-height: 100vh; min-height: 100vh;
background: #f0f2f5; 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 { .main {
@@ -225,18 +473,49 @@ onMounted(async () => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 100vh; min-height: 100vh;
transition: margin-left 0.3s ease; transition: margin-left 300ms ease;
background: #f0f2f5; 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 { .content-scroll {
flex: 1; flex: 1;
overflow-y: scroll; overflow-y: scroll;
overflow-x: auto; /* 允许横向滚动,兼容极端窄屏 */
background: #f0f2f5; background: #f0f2f5;
padding: 0;
} }
.content-inner { .content-inner {
min-height: calc(100vh - 120px); 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> </style>

View File

@@ -21,16 +21,18 @@
</view> </view>
</view> </view>
<view class="aside-footer" @click="onToggle"> <!-- 1:1 复刻 CRMEB: 一级侧边栏通常不单独折叠,由顶部汉堡菜单控制整体 -->
<!-- <view class="aside-footer" @click="onToggle">
<view class="toggle-btn"> <view class="toggle-btn">
<text class="toggle-icon">{{ collapsed ? '>' : '<' }}</text> <text class="toggle-icon">{{ collapsed ? '>' : '<' }}</text>
</view> </view>
</view> </view> -->
</view> </view>
</template> </template>
<script setup lang="uts"> <script setup lang="uts">
import type { TopMenu } from '@/layouts/admin/router/adminRoutes.uts' import type { TopMenu } from '@/layouts/admin/router/adminRoutes.uts'
import { isMobile } from '@/layouts/admin/store/adminNavStore.uts'
const props = defineProps<{ const props = defineProps<{
collapsed: boolean collapsed: boolean
@@ -86,7 +88,7 @@ function onLogoClick(): void {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
transition: width 0.3s ease; transition: width 0.3s ease;
z-index: 1000; z-index: 1002; /* 确保在遮罩层之上 */
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15); box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
} }
@@ -107,45 +109,34 @@ function onLogoClick(): void {
.aside-menu { .aside-menu {
flex: 1; flex: 1;
padding: 8px 0; padding: 0; /* CRMEB typically no padding here */
overflow-y: scroll; overflow-y: scroll;
} }
.menu-item { .menu-item {
height: 60px; height: 50px; /* 1:1 CRMEB columnsAside height */
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
color: rgba(255, 255, 255, 0.65); color: rgba(255, 255, 255, 0.7);
transition: all 0.3s; transition: all 0.3s;
position: relative; position: relative;
&:hover { &:hover {
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.05);
color: #fff;
} }
&.active { &.active {
background: #1890ff; background: #1890ff; /* CRMEB 主色蓝 */
color: #fff; color: #fff;
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 3px;
background: #fff;
}
} }
} }
.menu-icon { .menu-icon {
font-size: 24px; font-size: 18px; /* CRMEB icons are smaller than 24px usually */
margin-bottom: 4px; margin-bottom: 2px;
.icon-text { .icon-text {
display: block; display: block;
@@ -154,6 +145,7 @@ function onLogoClick(): void {
.menu-title { .menu-title {
font-size: 12px; font-size: 12px;
transform: scale(0.9); /* CRMEB text is tiny */
text-align: center; text-align: center;
text { text {

View File

@@ -1,15 +1,30 @@
<template> <template>
<view class="header"> <view class="header">
<view class="header-left"> <view class="header-left">
<text class="crumb" v-for="(item, index) in breadcrumb" :key="item.id"> <!-- 移动端菜单切换按钮 (CSS 控制显隐) -->
{{ item.title }} <view class="menu-toggle mobile-only" @click="onToggleSubSider">
<text v-if="index < breadcrumb.length - 1" class="separator"> / </text> <text class="menu-icon">☰</text>
</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>
<view class="header-right"> <view class="header-right">
<view class="hbtn" @click="$emit('search')"><text>🔍</text></view> <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')"> <view class="hbtn" @click="$emit('notify')">
<text>🔔</text> <text>🔔</text>
<view class="dot" v-if="hasNotification"></view> <view class="dot" v-if="hasNotification"></view>
@@ -19,16 +34,50 @@
</template> </template>
<script setup lang="uts"> <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}> breadcrumb: Array<{id: string, title: string}>
hasNotification: boolean hasNotification: boolean
isMobile: boolean
}>() }>()
defineEmits<{ defineEmits<{
(e:'search'): void (e:'search'): void
(e:'refresh'): void (e:'refresh'): void
(e:'notify'): 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> </script>
<style> <style>
@@ -49,11 +98,54 @@ defineEmits<{
align-items: center; 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 { .crumb {
color: #374151; color: #374151;
font-size: 14px; 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 { .separator {
color: #d1d5db; color: #d1d5db;
margin: 0 8px; 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> <template>
<view class="admin-subsider" :style="{ left: asideWidth + 'px', width: siderWidth + 'px' }"> <view
<view class="subsider-header"> class="admin-subsider"
<text class="header-title">{{ topMenuTitle }}</text> :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>
<view class="subsider-menu"> <!-- 菜单列表滚动 -->
<view v-for="group in groups" :key="group.id" class="menu-group"> <scroll-view class="subsider-content" scroll-y="true" show-scrollbar="false">
<view class="group-title"> <view class="menu-list">
<text>{{ group.title }}</text> <!-- 分级手动多层渲染 (UTS 下直接循环比递归更稳定) -->
</view> <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)" <transition name="expand">
:key="route.id" <view v-if="level1.type === 'group' && isOpen(level1.id, 1)" class="sub-menu-container">
class="menu-item" <template v-for="level2 in level1.children" :key="level2.id">
:class="{ active: route.id === activeRouteId }" <view
@click="onRouteClick(route.id)" class="menu-row level-2"
> :class="{ 'is-active': isActive(level2), 'is-open': isOpen(level2.id, 2) }"
<text class="item-title">{{ route.title }}</text> @click="handleNodeClick(level2, 2, level1.id)"
</view> >
<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>
</view> </scroll-view>
</view> </view>
</template> </template>
<script setup lang="uts"> <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<{ const props = defineProps<{
topMenuTitle: string visible: boolean
groups: MenuGroup[] menuTitle: string
routes: Map<string, RouteRecord[]> menuTree: MenuNode[]
activeRouteId: string activeId: string
currentPath: string
asideWidth: number asideWidth: number
siderWidth: number width: number
isOverlay?: boolean
layoutMode?: string
}>() }>()
const emit = defineEmits<{ 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> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@@ -54,79 +162,122 @@ function onRouteClick(routeId: string): void {
position: fixed; position: fixed;
top: 0; top: 0;
bottom: 0; bottom: 0;
background: #fff; background: #ffffff;
border-right: 1px solid #e8e8e8; border-right: 1px solid #f0f0f0;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.06);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
z-index: 999; z-index: 1000;
transition: transform 300ms ease, opacity 250ms ease;
} }
.subsider-header { .admin-subsider.is-hidden {
height: 64px; 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; display: flex;
align-items: center; align-items: center;
padding: 0 16px; padding: 0 20px;
border-bottom: 1px solid #e8e8e8; border-bottom: 1px solid #f0f0f0;
background-color: #fff;
flex-shrink: 0;
.header-title { .cat-title {
font-size: 16px; font-size: 15px;
font-weight: 600; font-weight: 600;
color: #333; color: #333;
} }
} }
.subsider-menu { .subsider-content {
flex: 1; flex: 1;
padding: 8px 0;
overflow-y: scroll;
} }
.menu-group { .menu-list {
margin-bottom: 16px; padding: 0; /* 移除 8px 边距,确保菜单项紧贴顶部/标题栏 */
} }
.group-title { .menu-row {
padding: 8px 16px; height: 50px;
font-size: 12px;
color: #999;
font-weight: 500;
text {
display: block;
}
}
.menu-item {
height: 36px;
padding: 0 16px 0 24px;
display: flex; display: flex;
flex-direction: row;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
position: relative; position: relative;
width: 100%;
&:hover { border-bottom: 1px solid transparent; /* 保持布局稳定 */
background: #f5f5f5;
} &.is-active {
background-color: #e7f0ff;
&.active { .menu-text {
background: #e6f7ff; color: #2f65ff;
color: #1890ff; font-weight: 500;
&::after {
content: '';
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 3px;
background: #1890ff;
} }
} }
.item-title { .menu-text {
font-size: 14px; 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> </style>

View File

@@ -1,36 +1,122 @@
<template> <template>
<view class="tags"> <view class="tags">
<scroll-view class="tags-scroll" scroll-x="true" show-scrollbar="false"> <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 <view
v-for="t in tabs" v-for="t in tabs"
:key="t.id" :key="t.id"
class="tag" class="tag"
:class="{ active: activeTabId === t.id }" :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> <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> <text class="tag-close-text">×</text>
</view> </view>
</view> </view>
</view> </transition-group>
</scroll-view> </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> </view>
</template> </template>
<script setup lang="uts"> <script setup lang="uts">
import type { TabItem } from '../types.uts' import { ref, onMounted, onUnmounted } from 'vue'
import type { TabItem } from '../store/adminNavStore.uts'
defineProps<{ defineProps<{
tabs: TabItem[] tabs: TabItem[]
activeTabId: string activeTabId: string
}>() }>()
defineEmits<{ const emit = defineEmits<{
(e:'tab-click', tab: TabItem): void (e:'tab-click', tab: TabItem): void
(e:'tab-close', tabId: string): 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> </script>
<style> <style>
@@ -41,6 +127,7 @@ defineEmits<{
display:flex; display:flex;
flex-direction:row; flex-direction:row;
align-items:center; align-items:center;
position: relative;
} }
.tags-scroll{ width: 100%; height: 44px; } .tags-scroll{ width: 100%; height: 44px; }
.tags-row{ .tags-row{
@@ -63,6 +150,11 @@ defineEmits<{
flex-direction: row; flex-direction: row;
align-items:center; align-items:center;
gap: 8px; gap: 8px;
cursor: pointer;
transition: all 0.2s;
}
.tag:hover {
background: #f9fafb;
} }
.tag.active{ .tag.active{
border-color:#1677ff; border-color:#1677ff;
@@ -78,5 +170,80 @@ defineEmits<{
align-items:center; align-items:center;
justify-content: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; } .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> </style>

View File

@@ -1,74 +1,54 @@
<template> <template>
<view class="home-page"> <view class="home-page">
<!-- 数据统计卡片行 --> <!-- 数据统计卡片行 (使用统一响应式网格) -->
<view class="stats-row"> <view class="kpi-grid">
<!-- 销售额卡片 --> <KpiMiniCard
<view class="stat-card"> class="stat-card"
<view class="card-header"> title="销售额"
<text class="card-title">销售额</text> tagText="今日"
<view class="tag today">今日</view> :valueText="statsData.sales.today.toString()"
</view> :metaLeft="'昨日 ' + statsData.sales.yesterday"
<view class="card-value">91.1</view> :metaRight="'日环比 ' + statsData.sales.trend + '%'"
<view class="card-meta"> :trend="statsData.sales.trend > 0 ? 'up' : 'down'"
<text class="meta-text">昨日 2740</text> footerLeftText="本月销售额"
<text class="meta-trend down">日环比 -96.67% ▼</text> :footerRightText="statsData.sales.monthTotal + '元'"
</view> />
<view class="card-footer">
<text class="footer-label">本月销售额</text>
<text class="footer-value">2831.1元</text>
</view>
</view>
<!-- 用户访问量卡片 --> <KpiMiniCard
<view class="stat-card"> class="stat-card"
<view class="card-header"> title="用户访问量"
<text class="card-title">用户访问量</text> tagText="今日"
<view class="tag today">今日</view> :valueText="statsData.visits.today.toString()"
</view> :metaLeft="'昨日 ' + statsData.visits.yesterday"
<view class="card-value">224</view> :metaRight="'日环比 ' + statsData.visits.trend + '%'"
<view class="card-meta"> :trend="statsData.visits.trend > 0 ? 'up' : 'down'"
<text class="meta-text">昨日 136</text> footerLeftText="本月访问量"
<text class="meta-trend up">日环比 64.7% ▲</text> :footerRightText="statsData.visits.monthTotal + 'Pv'"
</view> />
<view class="card-footer">
<text class="footer-label">本月访问量</text>
<text class="footer-value">360Pv</text>
</view>
</view>
<!-- 订单量卡片 --> <KpiMiniCard
<view class="stat-card"> class="stat-card"
<view class="card-header"> title="订单量"
<text class="card-title">订单量</text> tagText="今日"
<view class="tag today">今日</view> :valueText="statsData.orders.today.toString()"
</view> :metaLeft="'昨日 ' + statsData.orders.yesterday"
<view class="card-value">4</view> :metaRight="'日环比 ' + statsData.orders.trend + '%'"
<view class="card-meta"> :trend="statsData.orders.trend > 0 ? 'up' : 'down'"
<text class="meta-text">昨日 8</text> footerLeftText="本月订单量"
<text class="meta-trend down">日环比 -50% ▼</text> :footerRightText="statsData.orders.monthTotal + '单'"
</view> />
<view class="card-footer">
<text class="footer-label">本月订单量</text>
<text class="footer-value">12单</text>
</view>
</view>
<!-- 新增用户卡片 --> <KpiMiniCard
<view class="stat-card"> class="stat-card"
<view class="card-header"> title="新增用户"
<text class="card-title">新增用户</text> tagText="今日"
<view class="tag today">今日</view> :valueText="statsData.users.today.toString()"
</view> :metaLeft="'昨日 ' + statsData.users.yesterday"
<view class="card-value">21</view> :metaRight="'日环比 ' + statsData.users.trend + '%'"
<view class="card-meta"> :trend="statsData.users.trend > 0 ? 'up' : 'down'"
<text class="meta-text">昨日 6</text> footerLeftText="本月新增用户"
<text class="meta-trend up">日环比 250% ▲</text> :footerRightText="statsData.users.monthTotal + '人'"
</view> />
<view class="card-footer">
<text class="footer-label">本月新增用户</text>
<text class="footer-value">27人</text>
</view>
</view>
</view> </view>
<!-- 订单趋势图表区 --> <!-- 订单趋势图表区 -->
@@ -156,6 +136,7 @@ import { ref, computed, onMounted } from 'vue'
import AnalyticsComboChart from '@/components/analytics/AnalyticsComboChart.uvue' import AnalyticsComboChart from '@/components/analytics/AnalyticsComboChart.uvue'
import AnalyticsAreaChart from '@/components/analytics/AnalyticsAreaChart.uvue' import AnalyticsAreaChart from '@/components/analytics/AnalyticsAreaChart.uvue'
import AnalyticsPieChart from '@/components/analytics/AnalyticsPieChart.uvue' import AnalyticsPieChart from '@/components/analytics/AnalyticsPieChart.uvue'
import KpiMiniCard from '@/pages/mall/admin/homePage/components/KpiMiniCard.uvue'
// Filter periods // Filter periods
const periods = [ const periods = [
@@ -244,103 +225,22 @@ const statsData = ref({
<style scoped> <style scoped>
.home-page { .home-page {
padding: 16px; padding: 0;
background-color: #f0f2f5; background-color: #f0f2f5;
min-height: 100vh; min-height: 100vh;
} }
.stats-row { /* 兼容旧布局标识,样式逻辑已由 .kpi-grid 接管 */
display: flex;
flex-direction: row;
gap: 16px;
flex-wrap: wrap;
}
/* 统计卡片 */
.stat-card { .stat-card {
flex: 1; margin-bottom: 0px;
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;
} }
/* 图表区样式 */ /* 图表区样式 */
.chart-section { .chart-section {
margin-top: 16px; margin-top: 12px;
background-color: #ffffff; background-color: #ffffff;
border-radius: 4px; border-radius: 4px;
padding: 20px; padding: 16px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08); box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
} }
@@ -349,7 +249,7 @@ const statsData = ref({
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
margin-bottom: 24px; margin-bottom: 16px;
} }
.header-left { .header-left {
@@ -420,10 +320,10 @@ const statsData = ref({
} }
.bottom-charts { .bottom-charts {
margin-top: 16px; margin-top: 12px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 16px; gap: 12px;
} }
.half-width { .half-width {
@@ -453,14 +353,11 @@ const statsData = ref({
font-weight: 500; font-weight: 500;
} }
/* 响应式 */ @media screen and (max-width: 768px) {
@media screen and (max-width: 1400px) { .home-page {
.stat-card { padding: 0;
min-width: calc(50% - 8px);
} }
}
@media screen and (max-width: 1024px) {
.bottom-charts { .bottom-charts {
flex-direction: column; flex-direction: column;
} }
@@ -468,24 +365,25 @@ const statsData = ref({
.half-width { .half-width {
min-width: 100%; min-width: 100%;
} }
}
@media screen and (max-width: 768px) { /* 调整图表头部在移动端的展示 */
.home-page { .chart-header {
padding: 12px; flex-direction: column;
} align-items: flex-start;
.stats-row {
gap: 12px; gap: 12px;
} }
.stat-card { .header-right {
min-width: 100%; width: 100%;
padding: 16px;
} }
.card-value { .period-tabs {
font-size: 28px; width: 100%;
justify-content: space-between;
}
.period-tab {
flex: 1;
} }
} }
</style> </style>

View File

@@ -6,122 +6,25 @@
* value: 组件引用 * value: 组件引用
* *
* 注意: * 注意:
* 1. 所有组件必须静态导入,确保打包可分析 * 1. 组件已切换为 defineAsyncComponent 异步导入,优化 H5 环境下的加载性能与包体积
* 2. 组件路径使用 @ 别名 * 2. 组件路径使用 @ 别名
* 3. 占位组件统一使用 PlaceholderPage * 3. 占位组件统一使用 PlaceholderPage
*/ */
import { defineAsyncComponent } from 'vue'
// 导入占位组件 // 导入占位组件
import PlaceholderPage from '@/layouts/admin/components/PlaceholderPage.uvue' import PlaceholderPage from '@/layouts/admin/components/PlaceholderPage.uvue'
// 导入首页(内部组件,不包含 AdminLayout // 导入首页(内部组件,不包含 AdminLayout
import HomeIndex from '@/layouts/admin/pages/HomeIndex.uvue' import HomeIndex from '@/layouts/admin/pages/HomeIndex.uvue'
// 导入用户模块(纯组件,不包含 AdminLayout // 用户、商品、订单模块已改为 defineAsyncComponent 异步加载,移除静态导入以优化 H5 加载性能
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'
// 营销设置模块暂时使用 PlaceholderPage // 营销设置模块暂时使用 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'
// 导入财务模块(纯组件) // 营销、内容、财务、客服、装修等模块已改为 defineAsyncComponent 异步加载,移除静态导入以优化 H5 加载性能
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'
/** /**
* 组件映射表 * 组件映射表
@@ -131,127 +34,146 @@ export const componentMap: Map<string, any> = new Map([
['HomeIndex', HomeIndex], ['HomeIndex', HomeIndex],
// 用户模块 // 用户模块
['UserStatistic', UserStatistic], ['UserStatistic', defineAsyncComponent(() => import('@/pages/mall/admin/user/statistics/index.uvue'))],
['UserList', UserList], ['UserList', defineAsyncComponent(() => import('@/pages/mall/admin/user/management/index.uvue'))],
['UserLevel', UserLevel], ['UserLevel', defineAsyncComponent(() => import('@/pages/mall/admin/user/level/index.uvue'))],
['UserGroup', UserGroup], ['UserGroup', defineAsyncComponent(() => import('@/pages/mall/admin/user/grouping/index.uvue'))],
['UserLabel', UserLabel], ['UserLabel', defineAsyncComponent(() => import('@/pages/mall/admin/user/label/index.uvue'))],
['UserMemberConfig', MemberConfig], ['UserMemberConfig', defineAsyncComponent(() => import('@/pages/mall/admin/user/configuration/index.uvue'))],
// 商品模块 // 商品模块
['ProductStatistic', ProductStatistic], ['ProductStatistic', defineAsyncComponent(() => import('@/pages/mall/admin/product/product-statistics/index.uvue'))],
['ProductList', ProductList], ['ProductList', defineAsyncComponent(() => import('@/pages/mall/admin/product/product-management/index.uvue'))],
['ProductEdit', ProductEdit], ['ProductEdit', defineAsyncComponent(() => import('@/pages/mall/admin/product/product-management/edit.uvue'))],
['ProductMemberPrice', ProductMemberPrice], ['ProductMemberPrice', defineAsyncComponent(() => import('@/pages/mall/admin/product/product-management/member-price.uvue'))],
['ProductClassify', ProductClassify], ['ProductClassify', defineAsyncComponent(() => import('@/pages/mall/admin/product/classification/index.uvue'))],
['ProductReply', ProductReply], ['ProductReply', defineAsyncComponent(() => import('@/pages/mall/admin/product/reviews/index.uvue'))],
['ProductAttr', ProductAttr], ['ProductAttr', defineAsyncComponent(() => import('@/pages/mall/admin/product/specifications/index.uvue'))],
['ProductParam', ProductParam], ['ProductParam', defineAsyncComponent(() => import('@/pages/mall/admin/product/parameters/index.uvue'))],
['ProductLabel', ProductLabel], ['ProductLabel', defineAsyncComponent(() => import('@/pages/mall/admin/product/labels/index.uvue'))],
['ProductProtection', ProductProtection], ['ProductProtection', defineAsyncComponent(() => import('@/pages/mall/admin/product/protection/index.uvue'))],
// 订单模块 // 订单模块
['OrderList', OrderList], ['OrderList', defineAsyncComponent(() => import('@/pages/mall/admin/order/list.uvue'))],
['OrderStatistic', OrderStatistic], ['OrderStatistic', defineAsyncComponent(() => import('@/pages/mall/admin/order/order-statistics/index.uvue'))],
['OrderRefund', OrderRefund], ['OrderRefund', defineAsyncComponent(() => import('@/pages/mall/admin/order/aftersales-order/index.uvue'))],
['OrderCashier', OrderCashier], ['OrderCashier', defineAsyncComponent(() => import('@/pages/mall/admin/order/cashier-order/index.uvue'))],
['OrderVerify', OrderVerify], ['OrderVerify', defineAsyncComponent(() => import('@/pages/mall/admin/order/write-off-records/index.uvue'))],
['OrderConfig', OrderConfig], ['OrderConfig', defineAsyncComponent(() => import('@/pages/mall/admin/order/order-configuration/index.uvue'))],
// 营销模块 // 营销模块已改为异步加载
// 1. 优惠券 // 1. 优惠券
['MarketingCouponList', MarketingCouponList], ['MarketingCouponList', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/coupon/list.uvue'))],
['MarketingCouponUser', MarketingCouponUser], ['MarketingCouponUser', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/coupon/user.uvue'))],
// 2. 积分管理 // 2. 积分管理
['MarketingIntegralStatistic', MarketingIntegralStatistic], ['MarketingIntegralStatistic', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/integral/statistic.uvue'))],
['MarketingIntegralProduct', MarketingIntegralProduct], ['MarketingIntegralProduct', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/integral/list.uvue'))],
['MarketingIntegralOrder', MarketingIntegralOrder], ['MarketingIntegralOrder', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/integral/order.uvue'))],
['MarketingIntegralRecord', MarketingIntegralRecord], ['MarketingIntegralRecord', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/integral/record.uvue'))],
['MarketingIntegralConfig', MarketingIntegralConfig], ['MarketingIntegralConfig', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/integral/config.uvue'))],
// 3. 抽奖管理 // 3. 抽奖管理
['MarketingLotteryList', MarketingLotteryList], ['MarketingLotteryList', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/lottery/list.uvue'))],
['MarketingLotteryConfig', MarketingLotteryConfig], ['MarketingLotteryConfig', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/lottery/config.uvue'))],
// 4. 砍价管理 // 4. 砍价管理
['MarketingBargainProduct', PlaceholderPage], ['MarketingBargainProduct', PlaceholderPage],
['MarketingBargainList', PlaceholderPage], ['MarketingBargainList', PlaceholderPage],
// 5. 拼团管理 // 5. 拼团管理
['MarketingCombinationProduct', MarketingCombinationProduct], ['MarketingCombinationProduct', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/combination/product.uvue'))],
['MarketingCombinationList', MarketingCombinationList], ['MarketingCombinationList', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/combination/list.uvue'))],
['MarketingCombinationCreate', MarketingCombinationCreate], ['MarketingCombinationCreate', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/combination/create.uvue'))],
// 6. 秒杀管理 // 6. 秒杀管理
['MarketingSeckillList', MarketingSeckillList], ['MarketingSeckillList', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/seckill/list.uvue'))],
['MarketingSeckillProduct', MarketingSeckillProduct], ['MarketingSeckillProduct', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/seckill/product.uvue'))],
['MarketingSeckillConfig', MarketingSeckillConfig], ['MarketingSeckillConfig', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/seckill/config.uvue'))],
// 7. 付费会员 // 7. 付费会员
['MarketingMemberType', MarketingMemberType], ['MarketingMemberType', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/member/type.uvue'))],
['MarketingMemberRight', MarketingMemberRight], ['MarketingMemberRight', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/member/right.uvue'))],
['MarketingMemberCard', MarketingMemberCard], ['MarketingMemberCard', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/member/card.uvue'))],
['MarketingMemberRecord', MarketingMemberRecord], ['MarketingMemberRecord', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/member/record.uvue'))],
['MarketingMemberConfig', MarketingMemberConfig], ['MarketingMemberConfig', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/member/config.uvue'))],
// 8. 直播管理 // 8. 直播管理
['MarketingLiveRoom', MarketingLiveRoom], ['MarketingLiveRoom', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/live/room.uvue'))],
['MarketingLiveProduct', MarketingLiveProduct], ['MarketingLiveProduct', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/live/product.uvue'))],
['MarketingLiveAnchor', MarketingLiveAnchor], ['MarketingLiveAnchor', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/live/anchor.uvue'))],
// 9. 用户充值 // 9. 用户充值
['MarketingRechargeQuota', MarketingRechargeQuota], ['MarketingRechargeQuota', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/recharge/quota.uvue'))],
['MarketingRechargeConfig', MarketingRechargeConfig], ['MarketingRechargeConfig', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/recharge/config.uvue'))],
// 10. 每日签到 // 10. 每日签到
['MarketingCheckinConfig', MarketingCheckinConfig], ['MarketingCheckinConfig', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/checkin/config.uvue'))],
['MarketingCheckinReward', MarketingCheckinReward], ['MarketingCheckinReward', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/checkin/reward.uvue'))],
// 11. 渠道码 & 新人礼 // 11. 渠道码 & 新人礼
['MarketingChannelList', PlaceholderPage], ['MarketingChannelList', PlaceholderPage],
['MarketingNewcomerGift', MarketingNewcomerGift], ['MarketingNewcomerGift', defineAsyncComponent(() => import('@/pages/mall/admin/marketing/newcomer/index.uvue'))],
// 内容模块 // 内容模块
['CmsArticle', CmsArticle], ['CmsArticle', defineAsyncComponent(() => import('@/pages/mall/admin/cms/article/list.uvue'))],
['CmsCategory', CmsCategory], ['CmsCategory', defineAsyncComponent(() => import('@/pages/mall/admin/cms/category/list.uvue'))],
// 财务模块 // 财务模块
['FinanceTransactionStats', FinanceTransactionStats], ['FinanceTransactionStats', defineAsyncComponent(() => import('@/pages/mall/admin/finance/transaction_stats.uvue'))],
['FinanceWithdrawal', FinanceWithdrawal], ['FinanceWithdrawal', defineAsyncComponent(() => import('@/pages/mall/admin/finance/withdrawal.uvue'))],
['FinanceInvoice', FinanceInvoice], ['FinanceInvoice', defineAsyncComponent(() => import('@/pages/mall/admin/finance/invoice.uvue'))],
['FinanceRecharge', FinanceRecharge], ['FinanceRecharge', defineAsyncComponent(() => import('@/pages/mall/admin/finance/recharge.uvue'))],
['FinanceCapitalFlow', FinanceCapitalFlow], ['FinanceCapitalFlow', defineAsyncComponent(() => import('@/pages/mall/admin/finance/capital_flow.uvue'))],
['FinanceBill', FinanceBill], ['FinanceBill', defineAsyncComponent(() => import('@/pages/mall/admin/finance/bill.uvue'))],
['FinanceCommission', FinanceCommission], ['FinanceCommission', defineAsyncComponent(() => import('@/pages/mall/admin/finance/commission.uvue'))],
['FinanceBalanceStats', FinanceBalanceStats], ['FinanceBalanceStats', defineAsyncComponent(() => import('@/pages/mall/admin/finance/balance_stats.uvue'))],
['FinanceBalanceRecord', FinanceBalanceRecord], ['FinanceBalanceRecord', defineAsyncComponent(() => import('@/pages/mall/admin/finance/balance_record.uvue'))],
// 数据模块 - 暂时使用占位组件 // 数据模块 - 暂时使用占位组件
['StatisticIndex', PlaceholderPage], ['StatisticIndex', PlaceholderPage],
// 设置模块 - 暂时使用占位组件 // 设置模块
['SettingSystemConfig', PlaceholderPage], ['SettingSystemConfig', defineAsyncComponent(() => import('@/pages/mall/admin/setting/system/config.uvue'))],
['SettingSystemAdmin', PlaceholderPage], ['SettingMessage', defineAsyncComponent(() => import('@/pages/mall/admin/setting/message.uvue'))],
['SettingSystemRole', PlaceholderPage], ['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], ['DistributionStatistic', PlaceholderPage],
['DistributionList', PlaceholderPage], ['DistributionPromoter', defineAsyncComponent(() => import('@/pages/mall/admin/distribution/promoter/index.uvue'))],
['DistributionConfig', PlaceholderPage], ['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], ['KefuList', defineAsyncComponent(() => import('@/pages/mall/admin/kefu/list.uvue'))],
['KefuWords', KefuWords], ['KefuWords', defineAsyncComponent(() => import('@/pages/mall/admin/kefu/words.uvue'))],
['KefuFeedback', KefuFeedback], ['KefuFeedback', defineAsyncComponent(() => import('@/pages/mall/admin/kefu/feedback.uvue'))],
['KefuAutoReply', KefuAutoReply], ['KefuAutoReply', defineAsyncComponent(() => import('@/pages/mall/admin/kefu/auto_reply.uvue'))],
['KefuConfig', KefuConfig], ['KefuConfig', defineAsyncComponent(() => import('@/pages/mall/admin/kefu/config.uvue'))],
// 装修模块 // 装修模块
['DecorationHome', DecorationHome], ['DecorationHome', defineAsyncComponent(() => import('@/pages/mall/admin/decoration/home.uvue'))],
['DecorationCategory', DecorationCategory], ['DecorationCategory', defineAsyncComponent(() => import('@/pages/mall/admin/decoration/category.uvue'))],
['DecorationUser', DecorationUser], ['DecorationUser', defineAsyncComponent(() => import('@/pages/mall/admin/decoration/user.uvue'))],
['DecorationData', DecorationData], ['DecorationData', defineAsyncComponent(() => import('@/pages/mall/admin/decoration/data-config.uvue'))],
['DecorationStyle', DecorationStyle], ['DecorationStyle', defineAsyncComponent(() => import('@/pages/mall/admin/design/theme-style.uvue'))],
['DecorationMaterial', DecorationMaterial], ['DecorationMaterial', defineAsyncComponent(() => import('@/pages/mall/admin/design/material.uvue'))],
['DecorationLink', DecorationLink], ['DecorationLink', defineAsyncComponent(() => import('@/pages/mall/admin/design/link-management.uvue'))],
// 应用模块 // 应用模块
['AppStatistic', PlaceholderPage], ['AppStatistic', PlaceholderPage],
['AppList', PlaceholderPage], ['AppList', PlaceholderPage],
// 维护模块 // 维护模块
['MaintainDevConfig', MaintainDevConfig], ['MaintainDevConfig', defineAsyncComponent(() => import('@/pages/mall/admin/maintain/dev/config.uvue'))],
['MaintainDevData', PlaceholderPage], ['MaintainDevData', PlaceholderPage],
['MaintainDevTask', PlaceholderPage], ['MaintainDevTask', PlaceholderPage],
['MaintainDevAuth', PlaceholderPage], ['MaintainDevAuth', PlaceholderPage],

View File

@@ -69,7 +69,7 @@ export const topMenus: TopMenu[] = [
id: 'user', id: 'user',
title: '用户', title: '用户',
icon: 'user', icon: 'user',
path: '/pages/mall/admin/user/list', path: '/pages/mall/admin/user/management/index',
order: 2, order: 2,
groups: [ groups: [
{ id: 'user-manage', title: '', order: 1 } { id: 'user-manage', title: '', order: 1 }
@@ -92,7 +92,7 @@ export const topMenus: TopMenu[] = [
path: '/pages/mall/admin/product/statistic', path: '/pages/mall/admin/product/statistic',
order: 4, order: 4,
groups: [ 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', path: '/pages/mall/admin/cms/article/list',
order: 9, order: 9,
groups: [ groups: [
{ id: 'cms-manage', title: '内容管理', order: 1 } { id: 'cms-manage', title: '', order: 1 }
] ]
}, },
{ {
@@ -187,7 +187,10 @@ export const topMenus: TopMenu[] = [
order: 12, order: 12,
groups: [ groups: [
{ id: 'setting-system', title: '系统设置', order: 1 }, { 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', id: 'user_statistic',
title: '用户统计', title: '用户统计',
path: '/pages/mall/admin/user/Statistic', path: '/pages/mall/admin/user/statistics/index',
componentKey: 'UserStatistic', componentKey: 'UserStatistic',
parentId: 'user', parentId: 'user',
groupId: 'user-manage', groupId: 'user-manage',
@@ -238,7 +241,7 @@ export const routes: RouteRecord[] = [
{ {
id: 'user_list', id: 'user_list',
title: '用户管理', title: '用户管理',
path: '/pages/mall/admin/user/list', path: '/pages/mall/admin/user/management/index',
componentKey: 'UserList', componentKey: 'UserList',
parentId: 'user', parentId: 'user',
groupId: 'user-manage', groupId: 'user-manage',
@@ -248,7 +251,7 @@ export const routes: RouteRecord[] = [
{ {
id: 'user_group', id: 'user_group',
title: '用户分组', title: '用户分组',
path: '/pages/mall/admin/user/group', path: '/pages/mall/admin/user/grouping/index',
componentKey: 'UserGroup', componentKey: 'UserGroup',
parentId: 'user', parentId: 'user',
groupId: 'user-manage', groupId: 'user-manage',
@@ -258,7 +261,7 @@ export const routes: RouteRecord[] = [
{ {
id: 'user_label', id: 'user_label',
title: '用户标签', title: '用户标签',
path: '/pages/mall/admin/user/label', path: '/pages/mall/admin/user/label/index',
componentKey: 'UserLabel', componentKey: 'UserLabel',
parentId: 'user', parentId: 'user',
groupId: 'user-manage', groupId: 'user-manage',
@@ -268,7 +271,7 @@ export const routes: RouteRecord[] = [
{ {
id: 'user_level', id: 'user_level',
title: '用户等级', title: '用户等级',
path: '/pages/mall/admin/user/level', path: '/pages/mall/admin/user/level/index',
componentKey: 'UserLevel', componentKey: 'UserLevel',
parentId: 'user', parentId: 'user',
groupId: 'user-manage', groupId: 'user-manage',
@@ -301,7 +304,7 @@ export const routes: RouteRecord[] = [
{ {
id: 'product_productClassify', id: 'product_productClassify',
title: '商品分类', title: '商品分类',
path: '/pages/mall/admin/product/classify', path: '/pages/mall/admin/product/classification/index',
componentKey: 'ProductClassify', componentKey: 'ProductClassify',
parentId: 'product', parentId: 'product',
groupId: 'product-manage', groupId: 'product-manage',
@@ -311,7 +314,7 @@ export const routes: RouteRecord[] = [
{ {
id: 'product_productAttr', id: 'product_productAttr',
title: '商品规格', title: '商品规格',
path: '/pages/mall/admin/product/attr', path: '/pages/mall/admin/product/specifications/index',
componentKey: 'ProductAttr', componentKey: 'ProductAttr',
parentId: 'product', parentId: 'product',
groupId: 'product-manage', groupId: 'product-manage',
@@ -321,7 +324,7 @@ export const routes: RouteRecord[] = [
{ {
id: 'product_paramList', id: 'product_paramList',
title: '商品参数', title: '商品参数',
path: '/pages/mall/admin/product/param', path: '/pages/mall/admin/product/parameters/index',
componentKey: 'ProductParam', componentKey: 'ProductParam',
parentId: 'product', parentId: 'product',
groupId: 'product-manage', groupId: 'product-manage',
@@ -331,7 +334,7 @@ export const routes: RouteRecord[] = [
{ {
id: 'product_labelList', id: 'product_labelList',
title: '商品标签', title: '商品标签',
path: '/pages/mall/admin/product/label', path: '/pages/mall/admin/product/labels/index',
componentKey: 'ProductLabel', componentKey: 'ProductLabel',
parentId: 'product', parentId: 'product',
groupId: 'product-manage', groupId: 'product-manage',
@@ -341,7 +344,7 @@ export const routes: RouteRecord[] = [
{ {
id: 'product_protectionList', id: 'product_protectionList',
title: '商品保障', title: '商品保障',
path: '/pages/mall/admin/product/protection', path: '/pages/mall/admin/product/protection/index',
componentKey: 'ProductProtection', componentKey: 'ProductProtection',
parentId: 'product', parentId: 'product',
groupId: 'product-manage', groupId: 'product-manage',
@@ -351,7 +354,7 @@ export const routes: RouteRecord[] = [
{ {
id: 'product_productEvaluate', id: 'product_productEvaluate',
title: '商品评论', title: '商品评论',
path: '/pages/mall/admin/product/reply', path: '/pages/mall/admin/product/reviews/index',
componentKey: 'ProductReply', componentKey: 'ProductReply',
parentId: 'product', parentId: 'product',
groupId: 'product-manage', groupId: 'product-manage',
@@ -892,9 +895,10 @@ export const routes: RouteRecord[] = [
}, },
// ========== 设置模块 ========== // ========== 设置模块 ==========
// 1. 系统设置
{ {
id: 'setting_systemConfig', id: 'setting_systemConfig',
title: '系统置', title: '系统置',
path: '/pages/mall/admin/setting/system/config', path: '/pages/mall/admin/setting/system/config',
componentKey: 'SettingSystemConfig', componentKey: 'SettingSystemConfig',
parentId: 'setting', parentId: 'setting',
@@ -902,27 +906,168 @@ export const routes: RouteRecord[] = [
auth: ['admin-setting-system-config'], auth: ['admin-setting-system-config'],
order: 1 order: 1
}, },
// 2. 通知管理
{ {
id: 'setting_systemAdmin', id: 'setting_message',
title: '管理员管理', title: '消息管理',
path: '/pages/mall/admin/setting/system/admin', path: '/pages/mall/admin/setting/message',
componentKey: 'SettingSystemAdmin', componentKey: 'SettingMessage',
parentId: 'setting', parentId: 'setting',
groupId: 'setting-system', groupId: 'setting-message',
auth: ['admin-setting-system-admin'], order: 1
},
{
id: 'setting_agreement',
title: '协议管理',
path: '/pages/mall/admin/setting/agreement',
componentKey: 'SettingAgreement',
parentId: 'setting',
groupId: 'setting-message',
order: 2 order: 2
}, },
{ {
id: 'setting_systemRole', id: 'setting_ticket',
title: '角色管理', title: '客服设置',
path: '/pages/mall/admin/setting/system/role', path: '/pages/mall/admin/setting/ticket',
componentKey: 'SettingSystemRole', componentKey: 'SettingTicket',
parentId: 'setting', parentId: 'setting',
groupId: 'setting-system', groupId: 'setting-message',
auth: ['admin-setting-system-role'],
order: 3 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', id: 'distribution_statistic',
@@ -934,23 +1079,59 @@ export const routes: RouteRecord[] = [
order: 1 order: 1
}, },
{ {
id: 'distribution_list', id: 'distribution_promoter',
title: '分销员列表', title: '分销员管理',
path: '/pages/mall/admin/distribution/list', path: '/pages/mall/admin/distribution/promoter/index',
componentKey: 'DistributionList', componentKey: 'DistributionPromoter',
parentId: 'distribution', parentId: 'distribution',
groupId: 'distribution-manage', groupId: 'distribution-manage',
order: 2 order: 2
}, },
{ {
id: 'distribution_config', id: 'distribution_level',
title: '分销设置', title: '分销等级',
path: '/pages/mall/admin/distribution/config', path: '/pages/mall/admin/distribution/level/index',
componentKey: 'DistributionConfig', componentKey: 'DistributionLevel',
parentId: 'distribution', parentId: 'distribution',
groupId: 'distribution-manage', groupId: 'distribution-manage',
order: 3 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 order: 6
}, },
{ {
id: 'decoration_link', id: 'DecorationLink',
title: '链接管理', title: '链接管理',
path: '/pages/mall/admin/decoration/link', path: '/pages/mall/admin/decoration/link',
componentKey: 'DecorationLink', componentKey: 'DecorationLink',
@@ -1064,6 +1245,33 @@ export const routes: RouteRecord[] = [
order: 7 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', 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, buildDefaultTabs,
getTopMenus getTopMenus
} from '@/layouts/admin/router/adminRoutes.uts' } from '@/layouts/admin/router/adminRoutes.uts'
import { addView, activeFullPath, visitedViews } from './tagsViewStore.uts'
/** /**
* 标签页类型 * 标签页类型
@@ -32,19 +33,40 @@ export const activeTopMenuId = ref<string>('home')
/** 当前激活的路由ID */ /** 当前激活的路由ID */
export const activeRouteId = ref<string>('home_index') 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[]>([]) export const tabs = ref<TabItem[]>([])
/** 是否折叠主侧边栏 */ /** 是否折叠主侧边栏 (CRMEB: 70px) */
export const isMainAsideCollapsed = ref<boolean>(false) export const isMainAsideCollapsed = ref<boolean>(false)
/** 是否显示二级侧边栏 */ /** 是否显示二级侧边栏状态控制 */
export const showSubSider = computed<boolean>(() => { export const showSubSider = ref<boolean>(true)
const topMenus = getTopMenus()
const activeMenu = topMenus.find(m => m.id === activeTopMenuId.value) /** 屏幕宽度 */
return activeMenu ? activeMenu.groups.length > 0 : false 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 // Actions
// ============================================ // ============================================
@@ -67,6 +89,8 @@ export function openRoute(routeId: string, addTab: boolean = true): void {
// 更新一级菜单选中态 // 更新一级菜单选中态
if (route.parentId) { if (route.parentId) {
activeTopMenuId.value = route.parentId activeTopMenuId.value = route.parentId
// 记录该模块最后访问的子路由
lastSubIdByMenu.value[route.parentId] = routeId
} else { } else {
// 首页等顶级路由 // 首页等顶级路由
activeTopMenuId.value = routeId.split('_')[0] activeTopMenuId.value = routeId.split('_')[0]
@@ -101,6 +125,10 @@ function addTabItem(route: RouteRecord): void {
isAffix: route.isAffix || false 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 isMainAsideCollapsed.value = !isMainAsideCollapsed.value
} }
/**
* 切换二级侧边栏显示状态 (Desktop 模式)
*/
export function toggleSubSider(): void {
showSubSider.value = !showSubSider.value
}
/** /**
* 初始化导航状态 * 初始化导航状态
* 在 AdminLayout 组件 onMounted 时调用 * 在 AdminLayout 组件 onMounted 时调用
@@ -179,6 +214,11 @@ export function initNavState(): void {
isAffix: r.isAffix || false isAffix: r.isAffix || false
})) }))
// 初始化 TagsViewStore
defaultTabs.forEach(r => {
addView(r, r.path)
})
// 打开首页 // 打开首页
openRoute('home_index', false) 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;
}

View File

@@ -7,11 +7,9 @@ export function createApp() {
// 注册 i18n 全局属性,使组件可以使用 $t 方法 // 注册 i18n 全局属性,使组件可以使用 $t 方法
app.config.globalProperties.$t = (key: string, values?: any, locale?: string): string => { app.config.globalProperties.$t = (key: string, values?: any, locale?: string): string => {
if (!i18n.global) { // 临时方案:移除对未定义 i18n 的引用,直接返回 Key
console.error('i18n is not initialized') // 如果之后需要 i18n应正确导入并初始化
return key return key
}
return i18n.global.t(key, values, locale) || key
} }
return { app } return { app }

1363
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -453,19 +453,54 @@
"root": "pages/mall/admin", "root": "pages/mall/admin",
"pages": [ "pages": [
{ {
"path": "user-management", "path": "user/management/index",
"style": { "style": {
"navigationBarTitleText": "用户管理", "navigationBarTitleText": "用户管理",
"navigationStyle": "custom" "navigationStyle": "custom"
} }
}, },
{ {
"path": "product-management", "path": "product/product-management/index",
"style": { "style": {
"navigationBarTitleText": "商品管理", "navigationBarTitleText": "商品管理",
"navigationStyle": "custom" "navigationStyle": "custom"
} }
}, },
{
"path": "order/list",
"style": {
"navigationBarTitleText": "订单列表",
"navigationStyle": "custom"
}
},
{
"path": "order/order-statistics/index",
"style": {
"navigationBarTitleText": "订单统计",
"navigationStyle": "custom"
}
},
{
"path": "order/order-configuration/index",
"style": {
"navigationBarTitleText": "订单配置",
"navigationStyle": "custom"
}
},
{
"path": "order/aftersales-order/index",
"style": {
"navigationBarTitleText": "售后订单",
"navigationStyle": "custom"
}
},
{
"path": "order/write-off-records/index",
"style": {
"navigationBarTitleText": "核销记录",
"navigationStyle": "custom"
}
},
{ {
"path": "order-management", "path": "order-management",
"style": { "style": {
@@ -481,19 +516,12 @@
} }
}, },
{ {
"path": "user-statistics", "path": "user/statistics/index",
"style": { "style": {
"navigationBarTitleText": "用户统计", "navigationBarTitleText": "用户统计",
"navigationStyle": "custom" "navigationStyle": "custom"
} }
}, },
{
"path": "system-settings",
"style": {
"navigationBarTitleText": "系统设置",
"navigationStyle": "custom"
}
},
{ {
"path": "subscription/plan-management", "path": "subscription/plan-management",
"style": { "style": {
@@ -506,6 +534,48 @@
"navigationBarTitleText": "用户订阅管理" "navigationBarTitleText": "用户订阅管理"
} }
}, },
{
"path": "distribution/setting/index",
"style": {
"navigationBarTitleText": "分销设置",
"navigationStyle": "custom"
}
},
{
"path": "distribution/promoter/index",
"style": {
"navigationBarTitleText": "分销员管理",
"navigationStyle": "custom"
}
},
{
"path": "distribution/level/index",
"style": {
"navigationBarTitleText": "分销等级管理",
"navigationStyle": "custom"
}
},
{
"path": "distribution/division/list",
"style": {
"navigationBarTitleText": "事业部管理",
"navigationStyle": "custom"
}
},
{
"path": "distribution/division/agent",
"style": {
"navigationBarTitleText": "代理商列表",
"navigationStyle": "custom"
}
},
{
"path": "distribution/division/apply",
"style": {
"navigationBarTitleText": "事业部申请",
"navigationStyle": "custom"
}
},
{ {
"path": "marketing/coupon/list", "path": "marketing/coupon/list",
"style": { "style": {
@@ -633,4 +703,4 @@
"navigationBarBackgroundColor": "#FFFFFF", "navigationBarBackgroundColor": "#FFFFFF",
"backgroundColor": "#F8F8F8" "backgroundColor": "#F8F8F8"
} }
} }

View File

@@ -0,0 +1,83 @@
<template>
<view class="admin-page">
<view class="filter-card">
<view class="filter-row">
<view class="filter-item">
<text class="label">代理商查询:</text>
<input class="filter-input" placeholder="请输入姓名、手机号或UID" />
</view>
<view class="filter-btns">
<button class="btn primary" @click="onSearch">查询</button>
</view>
</view>
</view>
<view class="content-card">
<view class="action-bar">
<button class="btn primary small" @click="onAdd">添加代理商</button>
</view>
<view class="table-container">
<view class="table-header">
<view class="col col-uid"><text>用户UID</text></view>
<view class="col col-avatar"><text>头像</text></view>
<view class="col col-name"><text>名称</text></view>
<view class="col col-ratio"><text>分佣比例</text></view>
<view class="col col-count"><text>员工数量</text></view>
<view class="col col-time"><text>过期时间</text></view>
<view class="col col-status"><text>状态</text></view>
<view class="col col-ops"><text>操作</text></view>
</view>
<view class="table-body">
<view v-for="item in agentList" :key="item.uid" class="table-row">
<view class="col col-uid"><text>{{ item.uid }}</text></view>
<view class="col col-avatar">
<image class="avatar-img" src="/static/logo.png" mode="aspectFill" />
</view>
<view class="col col-name"><text>{{ item.name }}</text></view>
<view class="col col-ratio"><text>{{ item.ratio }}%</text></view>
<view class="col col-count"><text>{{ item.staffCount }}</text></view>
<view class="col col-time"><text>{{ item.endTime }}</text></view>
<view class="col col-status">
<switch :checked="item.status" color="#1890ff" scale="0.8" />
</view>
<view class="col col-ops">
<text class="op-link">编辑</text>
<text class="op-divider">|</text>
<text class="op-link">查看</text>
<text class="op-divider">|</text>
<text class="op-link">员工</text>
<text class="op-divider">|</text>
<text class="op-link">删除</text>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref } from 'vue'
const agentList = ref([
{ uid: '60569', name: 'cs2020', ratio: 50, staffCount: 1, endTime: '2026-01-01', status: true },
])
function onSearch() { uni.showToast({ title: '查询中...', icon: 'none' }) }
function onAdd() { uni.showToast({ title: '添加中...', icon: 'none' }) }
</script>
<style scoped lang="scss">
.admin-page { padding: 0; }
.filter-card { background: #fff; padding: 24px; margin-bottom: 16px; border-radius: 4px; }
.filter-row { display: flex; flex-direction: row; align-items: center; gap: 24px; }
.label { font-size: 14px; color: #333; }
.filter-input { border: 1px solid #d9d9d9; height: 32px; width: 220px; padding: 0 12px; font-size: 14px; }
.btn { height: 32px; padding: 0 16px; font-size: 14px; border: 1px solid #d9d9d9; background: #fff; display: flex; align-items: center; justify-content: center; cursor: pointer; }
.btn.primary { background: #1890ff; border-color: #1890ff; color: #fff; }
.content-card { background: #fff; border-radius: 4px; }
.action-bar { padding: 16px 24px; }
.table-container { padding: 0 24px 24px; }
.table-header { display: flex; flex-direction: row; background: #f8faff; border-bottom: 1px solid #f0f0f0; padding: 12px 0; }
.table-row { display: flex; flex-direction: row; border-bottom: 1px solid #f0f0f0; padding: 12px 0; align-items: center; &:hover { background: #fafafa; } }
.col { padding: 0 8px; font-size: 14px; color: #333; display: flex; align-items: center; }
.col-uid { width: 80px; } .col-avatar { width: 80px; justify-content: center; } .col-name { width: 120px; } .col-ratio { width: 100px; justify-content: center; } .col-count { width: 100px; justify-content: center; } .col-time { width: 120px; justify-content: center; } .col-status { width: 80px; justify-content: center; } .col-ops { flex: 1; justify-content: flex-end; }
.avatar-img { width: 32px; height: 32px; border-radius: 2px; }
.op-link { color: #1890ff; cursor: pointer; }
.op-divider { color: #e8e8e8; margin: 0 8px; }
</style>

View File

@@ -0,0 +1,89 @@
<template>
<view class="admin-page">
<view class="filter-card">
<view class="filter-row">
<view class="filter-item">
<text class="label">搜索:</text>
<input class="filter-input" placeholder="请输入姓名、UID" />
</view>
<view class="filter-btns">
<button class="btn primary" @click="onSearch">查询</button>
</view>
</view>
</view>
<view class="content-card">
<view class="tabs-row">
<view v-for="(tab, index) in tabs" :key="index" class="tab-item" :class="{ active: activeTab === index }" @click="activeTab = index">
<text>{{ tab }}</text>
</view>
</view>
<view class="table-container">
<view class="table-header">
<view class="col col-uid"><text>用户UID</text></view>
<view class="col col-name"><text>代理商名称</text></view>
<view class="col col-phone"><text>代理商电话</text></view>
<view class="col col-dept"><text>事业部名称</text></view>
<view class="col col-img"><text>申请图片</text></view>
<view class="col col-time"><text>申请时间</text></view>
<view class="col col-status"><text>申请状态</text></view>
<view class="col col-code"><text>邀请码</text></view>
<view class="col col-ops"><text>操作</text></view>
</view>
<view class="table-body">
<view v-for="item in applyList" :key="item.uid" class="table-row">
<view class="col col-uid"><text>{{ item.uid }}</text></view>
<view class="col col-name"><text>{{ item.name }}</text></view>
<view class="col col-phone"><text>{{ item.phone }}</text></view>
<view class="col col-dept"><text>{{ item.deptName }}</text></view>
<view class="col col-img">
<image class="table-img" src="/static/logo.png" mode="aspectFill" />
</view>
<view class="col col-time"><text>{{ item.time }}</text></view>
<view class="col col-status">
<view class="status-tag"><text>{{ item.statusText }}</text></view>
</view>
<view class="col col-code"><view class="code-box"><text>{{ item.code }}</text></view></view>
<view class="col col-ops">
<text class="op-link">同意</text>
<text class="op-divider">|</text>
<text class="op-link">拒绝</text>
<text class="op-divider">|</text>
<text class="op-link">删除</text>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref } from 'vue'
const activeTab = ref(0)
const tabs = ['全部', '申请中', '已同意', '已拒绝']
const applyList = ref([
{ uid: '81806', name: '测试测试', phone: '19910205954', deptName: '26991', time: '2026-01-08 15:30:39', statusText: '申请中', code: '70623142' },
])
function onSearch() { uni.showToast({ title: '查询中...', icon: 'none' }) }
</script>
<style scoped lang="scss">
.admin-page { padding: 0; }
.filter-card { background: #fff; padding: 24px; margin-bottom: 16px; border-radius: 4px; }
.filter-row { display: flex; flex-direction: row; align-items: center; gap: 24px; }
.label { font-size: 14px; color: #333; }
.filter-input { border: 1px solid #d9d9d9; height: 32px; width: 220px; padding: 0 12px; font-size: 14px; }
.btn { height: 32px; padding: 0 16px; font-size: 14px; border: 1px solid #d9d9d9; background: #fff; display: flex; align-items: center; justify-content: center; cursor: pointer; }
.btn.primary { background: #1890ff; border-color: #1890ff; color: #fff; }
.content-card { background: #fff; border-radius: 4px; }
.tabs-row { display: flex; flex-direction: row; padding: 0 24px; border-bottom: 1px solid #f0f0f0; }
.tab-item { padding: 16px 20px; cursor: pointer; position: relative; text { font-size: 15px; color: #666; } &.active { text { color: #1890ff; font-weight: 500; } &::after { content: ''; position: absolute; bottom: 0; left: 0; right: 0; height: 2px; background: #1890ff; } } }
.table-container { padding: 24px; }
.table-header { display: flex; flex-direction: row; background: #f8faff; border-bottom: 1px solid #f0f0f0; padding: 12px 0; }
.table-row { display: flex; flex-direction: row; border-bottom: 1px solid #f0f0f0; padding: 12px 0; align-items: center; &:hover { background: #fafafa; } }
.col { padding: 0 8px; font-size: 14px; color: #333; display: flex; align-items: center; }
.col-uid { width: 80px; } .col-name { width: 120px; } .col-phone { width: 120px; } .col-dept { width: 120px; } .col-img { width: 80px; justify-content: center; } .col-time { width: 160px; justify-content: center; } .col-status { width: 100px; justify-content: center; } .col-code { width: 100px; justify-content: center; } .col-ops { flex: 1; justify-content: flex-end; }
.table-img { width: 32px; height: 32px; border-radius: 2px; }
.status-tag { border: 1px solid #1890ff; color: #1890ff; padding: 2px 8px; border-radius: 4px; font-size: 12px; }
.code-box { border: 1px solid #d9d9d9; padding: 2px 8px; border-radius: 4px; font-size: 12px; background: #fafafa; }
.op-link { color: #1890ff; cursor: pointer; }
.op-divider { color: #e8e8e8; margin: 0 8px; }
</style>

View File

@@ -0,0 +1,83 @@
<template>
<view class="admin-page">
<view class="filter-card">
<view class="filter-row">
<view class="filter-item">
<text class="label">搜索:</text>
<input class="filter-input" placeholder="请输入姓名、UID" />
</view>
<view class="filter-btns">
<button class="btn primary" @click="onSearch">查询</button>
</view>
</view>
</view>
<view class="content-card">
<view class="action-bar">
<button class="btn primary small" @click="onAdd">添加事业部</button>
</view>
<view class="table-container">
<view class="table-header">
<view class="col col-uid"><text>用户UID</text></view>
<view class="col col-avatar"><text>头像</text></view>
<view class="col col-name"><text>名称</text></view>
<view class="col col-code"><text>邀请码</text></view>
<view class="col col-ratio"><text>分销比例</text></view>
<view class="col col-count"><text>代理商数量</text></view>
<view class="col col-time"><text>截止时间</text></view>
<view class="col col-status"><text>状态</text></view>
<view class="col col-ops"><text>操作</text></view>
</view>
<view class="table-body">
<view v-for="item in divisionList" :key="item.uid" class="table-row">
<view class="col col-uid"><text>{{ item.uid }}</text></view>
<view class="col col-avatar">
<image class="avatar-img" src="/static/logo.png" mode="aspectFill" />
</view>
<view class="col col-name"><text>{{ item.name }}</text></view>
<view class="col col-code"><text>{{ item.code }}</text></view>
<view class="col col-ratio"><text>{{ item.ratio }}%</text></view>
<view class="col col-count"><text>{{ item.agentCount }}</text></view>
<view class="col col-time"><text>{{ item.endTime }}</text></view>
<view class="col col-status">
<switch :checked="item.status" color="#1890ff" scale="0.8" />
</view>
<view class="col col-ops">
<text class="op-link">查看代理商</text>
<text class="op-divider">|</text>
<text class="op-link">编辑</text>
<text class="op-divider">|</text>
<text class="op-link">删除</text>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref } from 'vue'
const divisionList = ref([
{ uid: '26991', name: '26991', code: '70623142', ratio: 40, agentCount: 1, endTime: '2026-12-31', status: true },
])
function onSearch() { uni.showToast({ title: '查询中...', icon: 'none' }) }
function onAdd() { uni.showToast({ title: '添加中...', icon: 'none' }) }
</script>
<style scoped lang="scss">
.admin-page { padding: 0; }
.filter-card { background: #fff; padding: 24px; margin-bottom: 16px; border-radius: 4px; }
.filter-row { display: flex; flex-direction: row; align-items: center; gap: 24px; }
.label { font-size: 14px; color: #333; }
.filter-input { border: 1px solid #d9d9d9; height: 32px; width: 220px; padding: 0 12px; font-size: 14px; }
.btn { height: 32px; padding: 0 16px; font-size: 14px; border: 1px solid #d9d9d9; background: #fff; display: flex; align-items: center; justify-content: center; cursor: pointer; }
.btn.primary { background: #1890ff; border-color: #1890ff; color: #fff; }
.content-card { background: #fff; border-radius: 4px; }
.action-bar { padding: 16px 24px; }
.table-container { padding: 0 24px 24px; }
.table-header { display: flex; flex-direction: row; background: #f8faff; border-bottom: 1px solid #f0f0f0; padding: 12px 0; }
.table-row { display: flex; flex-direction: row; border-bottom: 1px solid #f0f0f0; padding: 12px 0; align-items: center; &:hover { background: #fafafa; } }
.col { padding: 0 8px; font-size: 14px; color: #333; display: flex; align-items: center; }
.col-uid { width: 80px; } .col-avatar { width: 80px; justify-content: center; } .col-name { width: 120px; } .col-code { width: 100px; justify-content: center; } .col-ratio { width: 100px; justify-content: center; } .col-count { width: 100px; justify-content: center; } .col-time { width: 120px; justify-content: center; } .col-status { width: 80px; justify-content: center; } .col-ops { flex: 1; justify-content: flex-end; }
.avatar-img { width: 32px; height: 32px; border-radius: 2px; }
.op-link { color: #1890ff; cursor: pointer; }
.op-divider { color: #e8e8e8; margin: 0 8px; }
</style>

View File

@@ -0,0 +1,95 @@
<template>
<view class="admin-page">
<view class="filter-card">
<view class="filter-row">
<view class="filter-item">
<text class="label">是否显示:</text>
<view class="select-mock"><text>全部</text><text class="arrow">▼</text></view>
</view>
<view class="filter-item">
<text class="label">等级名称:</text>
<input class="filter-input" placeholder="请输入等级名称" />
</view>
<view class="filter-btns">
<button class="btn primary" @click="onSearch">查询</button>
</view>
</view>
</view>
<view class="content-card">
<view class="action-bar">
<button class="btn primary small" @click="onAdd">添加等级</button>
</view>
<view class="table-container">
<view class="table-header">
<view class="col col-id"><text>ID</text></view>
<view class="col col-img"><text>商品图片</text></view>
<view class="col col-name"><text>名称</text></view>
<view class="col col-level"><text>等级</text></view>
<view class="col col-percent"><text>一级分佣比例</text></view>
<view class="col col-percent"><text>二级分佣比例</text></view>
<view class="col col-stat"><text>任务总数</text></view>
<view class="col col-stat"><text>需完成数量</text></view>
<view class="col col-status"><text>是否显示</text></view>
<view class="col col-ops"><text>操作</text></view>
</view>
<view class="table-body">
<view v-for="item in levelList" :key="item.id" class="table-row">
<view class="col col-id"><text>{{ item.id }}</text></view>
<view class="col col-img">
<image class="table-img" src="/static/logo.png" mode="aspectFill" />
</view>
<view class="col col-name"><text>{{ item.name }}</text></view>
<view class="col col-level"><text>{{ item.level }}</text></view>
<view class="col col-percent"><text>{{ item.percent1 }}%</text></view>
<view class="col col-percent"><text>{{ item.percent2 }}%</text></view>
<view class="col col-stat"><text>{{ item.taskTotal }}</text></view>
<view class="col col-stat"><text>{{ item.taskFinish }}</text></view>
<view class="col col-status">
<switch :checked="item.show" color="#1890ff" scale="0.8" />
</view>
<view class="col col-ops">
<text class="op-link">等级任务</text>
<text class="op-divider">|</text>
<text class="op-link">编辑</text>
<text class="op-divider">|</text>
<text class="op-link">删除</text>
</view>
</view>
</view>
</view>
<view class="pagination">
<text class="page-info">共 {{ levelList.length }} 条</text>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref } from 'vue'
const levelList = ref([
{ id: '1', name: '一级分销员', level: 1, percent1: 0.00, percent2: 0.00, taskTotal: 0, taskFinish: 0, show: true },
])
function onSearch() { uni.showToast({ title: '查询中...', icon: 'none' }) }
function onAdd() { uni.showToast({ title: '添加中...', icon: 'none' }) }
</script>
<style scoped lang="scss">
.admin-page { padding: 0; }
.filter-card { background: #fff; padding: 24px; margin-bottom: 16px; border-radius: 4px; }
.filter-row { display: flex; flex-direction: row; align-items: center; gap: 24px; }
.label { font-size: 14px; color: #333; }
.select-mock { display: flex; flex-direction: row; align-items: center; justify-content: space-between; border: 1px solid #d9d9d9; border-radius: 2px; height: 32px; width: 160px; padding: 0 12px; background: #fff; text { font-size: 14px; color: #666; } .arrow { font-size: 10px; color: #bfbfbf; } }
.filter-input { border: 1px solid #d9d9d9; height: 32px; width: 220px; padding: 0 12px; font-size: 14px; }
.btn { height: 32px; padding: 0 16px; font-size: 14px; border-radius: 2px; border: 1px solid #d9d9d9; background: #fff; display: flex; align-items: center; justify-content: center; cursor: pointer; }
.btn.primary { background: #1890ff; border-color: #1890ff; color: #fff; }
.content-card { background: #fff; border-radius: 4px; }
.action-bar { padding: 16px 24px; }
.table-container { padding: 0 24px 24px; }
.table-header { display: flex; flex-direction: row; background: #f8faff; border-bottom: 1px solid #f0f0f0; padding: 12px 0; }
.table-row { display: flex; flex-direction: row; border-bottom: 1px solid #f0f0f0; padding: 12px 0; align-items: center; &:hover { background: #fafafa; } }
.col { padding: 0 8px; display: flex; align-items: center; font-size: 14px; color: #333; }
.col-id { width: 50px; } .col-img { width: 80px; justify-content: center; } .col-name { width: 120px; } .col-level { width: 80px; justify-content: center; } .col-percent { width: 120px; justify-content: center; } .col-stat { width: 100px; justify-content: center; } .col-status { width: 100px; justify-content: center; } .col-ops { flex: 1; justify-content: flex-end; padding-right: 16px; }
.table-img { width: 32px; height: 32px; border-radius: 2px; }
.op-link { color: #1890ff; cursor: pointer; }
.op-divider { color: #e8e8e8; margin: 0 8px; }
.pagination { padding: 16px 24px; border-top: 1px solid #f0f0f0; }
.page-info { font-size: 14px; color: #999; }
</style>

View File

@@ -0,0 +1,114 @@
<template>
<view class="admin-page">
<view class="filter-card">
<view class="filter-row">
<view class="filter-item">
<text class="label">时间选择:</text>
<view class="date-picker-mock">
<text class="placeholder">开始日期 - 结束日期</text>
<text class="icon-calendar">📅</text>
</view>
</view>
<view class="filter-item">
<text class="label">搜索:</text>
<input class="filter-input" placeholder="请输入姓名、电话、UID" />
</view>
<view class="filter-btns">
<button class="btn primary" @click="onSearch">查询</button>
</view>
</view>
</view>
<view class="content-card">
<view class="action-bar">
<button class="btn ghost small" @click="onExport">导出</button>
</view>
<view class="table-container">
<view class="table-header">
<view class="col col-id"><text>ID</text></view>
<view class="col col-img"><text>头像</text></view>
<view class="col col-info"><text>用户信息</text></view>
<view class="col col-level"><text>分销等级</text></view>
<view class="col col-stat"><text>推广用户数量</text></view>
<view class="col col-stat"><text>推广订单数量</text></view>
<view class="col col-stat"><text>推广订单金额</text></view>
<view class="col col-stat"><text>佣金总金额</text></view>
<view class="col col-stat"><text>已提现金额</text></view>
<view class="col col-stat"><text>提现次数</text></view>
<view class="col col-stat"><text>未提现金额</text></view>
<view class="col col-ops"><text>操作</text></view>
</view>
<view class="table-body">
<view v-for="item in promoterList" :key="item.id" class="table-row">
<view class="col col-id"><text>{{ item.id }}</text></view>
<view class="col col-img">
<image class="table-img" src="/static/logo.png" mode="aspectFill" />
</view>
<view class="col col-info">
<view class="user-info-box">
<text class="info-text">昵称:{{ item.nickname }}</text>
<text class="info-text">姓名:{{ item.name }}</text>
<text class="info-text">电话:{{ item.phone }}</text>
</view>
</view>
<view class="col col-level"><text>{{ item.level }}</text></view>
<view class="col col-stat"><text>{{ item.userCount }}</text></view>
<view class="col col-stat"><text>{{ item.orderCount }}</text></view>
<view class="col col-stat"><text>{{ item.orderAmount }}</text></view>
<view class="col col-stat"><text>{{ item.commissionTotal }}</text></view>
<view class="col col-stat"><text>{{ item.withdrawnAmount }}</text></view>
<view class="col col-stat"><text>{{ item.withdrawCount }}</text></view>
<view class="col col-stat"><text>{{ item.unwithdrawnAmount }}</text></view>
<view class="col col-ops">
<text class="op-link" @click="onPromoter(item)">推广人</text>
<text class="op-divider">|</text>
<text class="op-link" @click="onMore(item)">更多</text>
<text class="arrow-down">▼</text>
</view>
</view>
</view>
</view>
<view class="pagination">
<text class="page-info">共 {{ promoterList.length }} 条</text>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref } from 'vue'
const promoterList = ref([
{ id: '82764', nickname: '183****5762', name: '-', phone: '183****5762', level: '--', userCount: 0, orderCount: 0, orderAmount: '0.00', commissionTotal: '0.00', withdrawnAmount: 0, withdrawCount: 0, unwithdrawnAmount: 0 },
])
function onSearch() { uni.showToast({ title: '查询中...', icon: 'none' }) }
function onExport() { uni.showToast({ title: '开始导出', icon: 'none' }) }
function onPromoter(item: any) { uni.showToast({ title: '推广人: ' + item.id, icon: 'none' }) }
function onMore(item: any) { uni.showToast({ title: '更多: ' + item.id, icon: 'none' }) }
</script>
<style scoped lang="scss">
.admin-page { padding: 0; }
.filter-card { background: #fff; padding: 24px; margin-bottom: 16px; border-radius: 4px; }
.filter-row { display: flex; flex-direction: row; align-items: center; gap: 24px; }
.label { font-size: 14px; color: #333; }
.date-picker-mock { display: flex; flex-direction: row; align-items: center; justify-content: space-between; border: 1px solid #d9d9d9; border-radius: 2px; height: 32px; width: 260px; padding: 0 12px; background: #fff; .placeholder { font-size: 14px; color: #bfbfbf; } .icon-calendar { font-size: 14px; color: #bfbfbf; } }
.filter-input { border: 1px solid #d9d9d9; height: 32px; width: 220px; padding: 0 12px; font-size: 14px; }
.btn { height: 32px; padding: 0 16px; font-size: 14px; border-radius: 2px; border: 1px solid #d9d9d9; background: #fff; display: flex; align-items: center; justify-content: center; cursor: pointer; }
.btn.primary { background: #1890ff; border-color: #1890ff; color: #fff; }
.btn.ghost { color: #666; background: #fff; }
.btn.small { height: 28px; padding: 0 12px; font-size: 13px; }
.content-card { background: #fff; border-radius: 4px; }
.action-bar { padding: 16px 24px; }
.table-container { padding: 0 24px 24px; }
.table-header { display: flex; flex-direction: row; background: #f8faff; border-bottom: 1px solid #f0f0f0; padding: 12px 0; }
.table-row { display: flex; flex-direction: row; border-bottom: 1px solid #f0f0f0; padding: 12px 0; align-items: center; &:hover { background: #fafafa; } }
.col { padding: 0 8px; display: flex; align-items: center; font-size: 14px; color: #333; }
.col-id { width: 60px; } .col-img { width: 80px; justify-content: center; } .col-info { width: 180px; } .col-level { width: 100px; justify-content: center; } .col-stat { width: 110px; justify-content: center; } .col-ops { flex: 1; justify-content: flex-end; padding-right: 16px; }
.table-img { width: 40px; height: 40px; border-radius: 4px; }
.user-info-box { display: flex; flex-direction: column; }
.info-text { font-size: 12px; color: #666; margin-bottom: 2px; }
.op-link { color: #1890ff; cursor: pointer; }
.op-divider { color: #e8e8e8; margin: 0 8px; }
.arrow-down { font-size: 10px; color: #1890ff; margin-left: 4px; }
.pagination { padding: 16px 24px; border-top: 1px solid #f0f0f0; }
.page-info { font-size: 14px; color: #999; }
</style>

View File

@@ -0,0 +1,508 @@
<template>
<view class="admin-page">
<view class="content-card">
<view class="tabs-row">
<view
v-for="(tab, index) in tabs"
:key="index"
class="tab-item"
:class="{ active: activeTab === index }"
@click="activeTab = index"
>
<text>{{ tab }}</text>
</view>
</view>
<view class="form-container">
<!-- 分销模式 -->
<view v-if="activeTab === 0" class="form-content">
<view class="form-item">
<text class="label">分销启用:</text>
<radio-group class="radio-group" @change="(e: UniRadioGroupChangeEvent) => form.statue = e.detail.value">
<label class="radio-label">
<radio value="1" :checked="form.statue === '1'" color="#1890ff" /> 开启
</label>
<label class="radio-label">
<radio value="0" :checked="form.statue === '0'" color="#1890ff" /> 关闭
</label>
</radio-group>
<text class="hint">商城分销功能开启关闭</text>
</view>
<view class="form-item">
<text class="label">分销模式:</text>
<radio-group class="radio-group" @change="(e: UniRadioGroupChangeEvent) => form.extract_type = e.detail.value">
<label class="radio-label">
<radio value="1" :checked="form.extract_type === '1'" color="#1890ff" /> 指定分销
</label>
<label class="radio-label">
<radio value="2" :checked="form.extract_type === '2'" color="#1890ff" /> 人人分销
</label>
<label class="radio-label">
<radio value="3" :checked="form.extract_type === '3'" color="#1890ff" /> 满额分销
</label>
</radio-group>
<text class="hint">人人分销"默认每个人都可以分销,“指定分销”仅后台手动设置推广员,“满额分销”指用户购买商品满足消费金额后自动开启分销</text>
</view>
<view class="form-item">
<text class="label">分销关系绑定:</text>
<radio-group class="radio-group" @change="(e: UniRadioGroupChangeEvent) => form.bind_type = e.detail.value">
<label class="radio-label">
<radio value="1" :checked="form.bind_type === '1'" color="#1890ff" /> 所有用户
</label>
<label class="radio-label">
<radio value="2" :checked="form.bind_type === '2'" color="#1890ff" /> 新用户
</label>
</radio-group>
<text class="hint">所有用户”指所有没有上级推广人的用户点击或扫码即绑定分销关系,“新用户”指新注册的用户或首次进入系统的用户才会绑定推广关系</text>
</view>
<view class="form-item">
<text class="label">绑定模式:</text>
<radio-group class="radio-group" @change="(e: UniRadioGroupChangeEvent) => form.store_brokerage_binding_status = e.detail.value">
<label class="radio-label">
<radio value="1" :checked="form.store_brokerage_binding_status === '1'" color="#1890ff" /> 永久
</label>
<label class="radio-label">
<radio value="2" :checked="form.store_brokerage_binding_status === '2'" color="#1890ff" /> 有效期
</label>
<label class="radio-label">
<radio value="3" :checked="form.store_brokerage_binding_status === '3'" color="#1890ff" /> 临时
</label>
</radio-group>
<text class="hint">永久”一次绑定永久有效,“有效期”绑定后一段时间内有效,“临时”临时有效</text>
</view>
<view class="form-item">
<text class="label">分销海报图:</text>
<view class="poster-upload">
<image class="poster-preview" v-if="form.brokerage_poster_status" :src="form.brokerage_poster_status" mode="aspectFill"></image>
<view v-else class="upload-btn" @click="onUploadPoster">
<text class="plus">+</text>
</view>
</view>
<text class="hint">个人中心分销海报图片建议尺寸600x1000</text>
</view>
<view class="form-item">
<text class="label">分销层级:</text>
<radio-group class="radio-group" @change="(e: UniRadioGroupChangeEvent) => form.brokerage_level = e.detail.value">
<label class="radio-label">
<radio value="1" :checked="form.brokerage_level === '1'" color="#1890ff" /> 一级分销
</label>
<label class="radio-label">
<radio value="2" :checked="form.brokerage_level === '2'" color="#1890ff" /> 二级分销
</label>
</radio-group>
<text class="hint">分销层级,一级是只返上一层的佣金,二级是返上级 and 上上级的佣金</text>
</view>
<view class="form-item">
<text class="label">事业部开关:</text>
<radio-group class="radio-group" @change="(e: UniRadioGroupChangeEvent) => form.is_area_manager = e.detail.value">
<label class="radio-label">
<radio value="1" :checked="form.is_area_manager === '1'" color="#1890ff" /> 开启
</label>
<label class="radio-label">
<radio value="0" :checked="form.is_area_manager === '0'" color="#1890ff" /> 关闭
</label>
</radio-group>
<text class="hint">事业部开关,关闭后不计算事业部佣金</text>
</view>
<view class="form-item">
<text class="label">代理商申请开关:</text>
<radio-group class="radio-group" @change="(e: UniRadioGroupChangeEvent) => form.is_agent_apply = e.detail.value">
<label class="radio-label">
<radio value="1" :checked="form.is_agent_apply === '1'" color="#1890ff" /> 开启
</label>
<label class="radio-label">
<radio value="0" :checked="form.is_agent_apply === '0'" color="#1890ff" /> 关闭
</label>
</radio-group>
<text class="hint">控制移动端我的推广页面的代理商申请按钮是否显示</text>
</view>
<view class="form-item">
<text class="label">佣金悬浮窗开关:</text>
<radio-group class="radio-group" @change="(e: UniRadioGroupChangeEvent) => form.is_commission_window = e.detail.value">
<label class="radio-label">
<radio value="1" :checked="form.is_commission_window === '1'" color="#1890ff" /> 开启
</label>
<label class="radio-label">
<radio value="0" :checked="form.is_commission_window === '0'" color="#1890ff" /> 关闭
</label>
</radio-group>
<text class="hint">佣金悬浮窗开关,关闭之后,商品详情不显示佣金悬浮窗</text>
</view>
<button class="submit-btn" @click="onSubmit">提交</button>
</view>
<!-- 返佣设置 -->
<view v-if="activeTab === 1" class="form-content">
<view class="form-item">
<text class="label">自购返佣:</text>
<radio-group class="radio-group" @change="(e: UniRadioGroupChangeEvent) => form.is_self_brokerage = e.detail.value">
<label class="radio-label">
<radio value="1" :checked="form.is_self_brokerage === '1'" color="#1890ff" /> 开启
</label>
<label class="radio-label">
<radio value="0" :checked="form.is_self_brokerage === '0'" color="#1890ff" /> 关闭
</label>
</radio-group>
<text class="hint">是否开启自购返佣(开启:分销员自己购买商品,享受一级返佣,上级享受二级返佣;关闭:分销员自己购买商品没有返佣)</text>
</view>
<view class="form-item">
<text class="label">购买付费会员返佣:</text>
<radio-group class="radio-group" @change="(e: UniRadioGroupChangeEvent) => form.is_member_brokerage = e.detail.value">
<label class="radio-label">
<radio value="1" :checked="form.is_member_brokerage === '1'" color="#1890ff" /> 开启
</label>
<label class="radio-label">
<radio value="0" :checked="form.is_member_brokerage === '0'" color="#1890ff" /> 关闭
</label>
</radio-group>
<text class="hint">购买付费会员是否按照设置的佣金比例进行返佣</text>
</view>
<view class="form-item">
<text class="label">返佣类型:</text>
<radio-group class="radio-group" @change="(e: UniRadioGroupChangeEvent) => form.brokerage_type = e.detail.value">
<label class="radio-label">
<radio value="1" :checked="form.brokerage_type === '1'" color="#1890ff" /> 按照商品价格返佣
</label>
<label class="radio-label">
<radio value="2" :checked="form.brokerage_type === '2'" color="#1890ff" /> 按照实际支付价格返佣
</label>
</radio-group>
<text class="hint">选择返佣类型,按照商品价格返佣(按照商品售价计算返佣金额)以及按照实际支付价格返佣(按照商品的实际支付价格计算返佣)</text>
</view>
<view class="form-item">
<text class="label">推广用户返佣:</text>
<radio-group class="radio-group" @change="(e: UniRadioGroupChangeEvent) => form.is_promoter_brokerage = e.detail.value">
<label class="radio-label">
<radio value="1" :checked="form.is_promoter_brokerage === '1'" color="#1890ff" /> 开启
</label>
<label class="radio-label">
<radio value="0" :checked="form.is_promoter_brokerage === '0'" color="#1890ff" /> 关闭
</label>
</radio-group>
<text class="hint">分销推广用户获取佣金</text>
</view>
<view class="form-item">
<text class="label">推广佣金单价:</text>
<view class="input-with-unit">
<input class="form-input" :value="form.promoter_brokerage_price" @input="(e: UniInputEvent) => form.promoter_brokerage_price = e.detail.value" />
</view>
<text class="hint">分销推广佣金单价(每推广一个用户)</text>
</view>
<view class="form-item">
<text class="label">每日推广佣金上限:</text>
<view class="input-with-unit">
<input class="form-input" :value="form.promoter_brokerage_day_max" @input="(e: UniInputEvent) => form.promoter_brokerage_day_max = e.detail.value" />
</view>
<text class="hint">每日推广佣金上限0:不发佣金;-1:不限制;最好是推广佣金单价的整数倍)</text>
</view>
<view class="form-item">
<text class="label">一级返佣比例:</text>
<view class="input-with-unit">
<input class="form-input" :value="form.store_brokerage_ratio" @input="(e: UniInputEvent) => form.store_brokerage_ratio = e.detail.value" />
</view>
<text class="hint">订单交易成功后给上级返佣的比例0 - 100,例:5 = 反订单商品金额的5%</text>
</view>
<view class="form-item">
<text class="label">二级返佣比例:</text>
<view class="input-with-unit">
<input class="form-input" :value="form.store_brokerage_two_ratio" @input="(e: UniInputEvent) => form.store_brokerage_two_ratio = e.detail.value" />
</view>
<text class="hint">订单交易成功后给上上级返佣的比例0 - 100,例:5 = 反订单商品金额 of 5%</text>
</view>
<view class="form-item">
<text class="label">冻结时间:</text>
<view class="input-with-unit">
<input class="form-input" :value="form.extract_frozen_time" @input="(e: UniInputEvent) => form.extract_frozen_time = e.detail.value" />
</view>
<text class="hint">防止用户退款,佣金被提现了,所以需要设置佣金冻结时间(天)</text>
</view>
<button class="submit-btn" @click="onSubmit">提交</button>
</view>
<!-- 提现设置 -->
<view v-if="activeTab === 2" class="form-content">
<view class="form-item">
<text class="label">提现最低金额:</text>
<input class="form-input" :value="form.user_extract_min_price" @input="(e: UniInputEvent) => form.user_extract_min_price = e.detail.value" />
<text class="hint">用户提现最低金额限制</text>
</view>
<view class="form-item">
<text class="label">提现银行卡:</text>
<textarea class="form-textarea" :value="form.extract_bank_list" @input="(e: UniInputEvent) => form.extract_bank_list = e.detail.value" placeholder="配置提现银行卡类型,每个银行换行"></textarea>
<text class="hint">配置提现银行卡类型,每个银行换行</text>
</view>
<view class="form-item">
<text class="label">提现方式:</text>
<checkbox-group class="checkbox-group" @change="(e: UniCheckboxGroupChangeEvent) => form.extract_type_list = e.detail.value">
<label class="checkbox-label"><checkbox value="bank" :checked="form.extract_type_list.includes('bank')" color="#1890ff" /> 银行卡</label>
<label class="checkbox-label"><checkbox value="wechat" :checked="form.extract_type_list.includes('wechat')" color="#1890ff" /> 微信</label>
<label class="checkbox-label"><checkbox value="alipay" :checked="form.extract_type_list.includes('alipay')" color="#1890ff" /> 支付宝</label>
</checkbox-group>
<text class="hint">开启后用户可以选择该提现方式</text>
</view>
<view class="form-item">
<text class="label">微信提现:</text>
<radio-group class="radio-group" @change="(e: UniRadioGroupChangeEvent) => form.wechat_extract_type = e.detail.value">
<label class="radio-label">
<radio value="1" :checked="form.wechat_extract_type === '1'" color="#1890ff" /> 手动线下转账
</label>
<label class="radio-label">
<radio value="2" :checked="form.wechat_extract_type === '2'" color="#1890ff" /> 自动转账到零钱
</label>
</radio-group>
<text class="hint">微信提现方式:手动线下转账,自动转账到零钱(需开通商家转账到零钱)</text>
</view>
<view class="form-item">
<text class="label">支付宝提现:</text>
<radio-group class="radio-group" @change="(e: UniRadioGroupChangeEvent) => form.alipay_extract_type = e.detail.value">
<label class="radio-label">
<radio value="1" :checked="form.alipay_extract_type === '1'" color="#1890ff" /> 手动线下转账
</label>
<label class="radio-label">
<radio value="2" :checked="form.alipay_extract_type === '2'" color="#1890ff" /> 自动转账到余额
</label>
</radio-group>
<text class="hint">支付宝提现方式:手动线下转账,自动转账到余额(需开通支付宝转账)</text>
</view>
<view class="form-item">
<text class="label">提现手续费:</text>
<input class="form-input" :value="form.user_extract_fee" @input="(e: UniInputEvent) => form.user_extract_fee = e.detail.value" />
<text class="hint">提现手续费百分比范围0-1000为无提现手续费设置10即收取10%手续费提现100元到账90元10元手续费</text>
</view>
<button class="submit-btn" @click="onSubmit">提交</button>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref } from 'vue'
const activeTab = ref(0)
const tabs = ['分销模式', '返佣设置', '提现设置']
const form = ref({
// 分销模式
statue: '1',
extract_type: '2',
bind_type: '2',
store_brokerage_binding_status: '1',
brokerage_poster_status: '',
brokerage_level: '2',
is_area_manager: '1',
is_agent_apply: '1',
is_commission_window: '1',
// 返佣设置
is_self_brokerage: '1',
is_member_brokerage: '0',
brokerage_type: '1',
is_promoter_brokerage: '1',
promoter_brokerage_price: '2',
promoter_brokerage_day_max: '-1',
store_brokerage_ratio: '20',
store_brokerage_two_ratio: '2',
extract_frozen_time: '1',
// 提现设置
user_extract_min_price: '1',
extract_bank_list: '中国银行',
extract_type_list: ['bank', 'wechat', 'alipay'],
wechat_extract_type: '1',
alipay_extract_type: '1',
user_extract_fee: '0'
})
function onUploadPoster() {
uni.chooseImage({
count: 1,
success: (res) => {
form.value.brokerage_poster_status = res.tempFilePaths[0]
}
})
}
function onSubmit() {
uni.showToast({ title: '保存成功', icon: 'success' })
}
</script>
<style scoped lang="scss">
.admin-page {
display: flex;
flex-direction: column;
}
.content-card {
background: #fff;
border-radius: 4px;
}
.tabs-row {
display: flex;
flex-direction: row;
padding: 0 24px;
border-bottom: 1px solid #f0f0f0;
}
.tab-item {
padding: 16px 20px;
cursor: pointer;
position: relative;
text { font-size: 15px; color: #666; }
&.active {
text { color: #1890ff; font-weight: 500; }
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background: #1890ff;
}
}
}
.form-container {
padding: 32px 24px;
}
.form-item {
margin-bottom: 24px;
display: flex;
flex-direction: column;
}
.label {
font-size: 14px;
color: #333;
margin-bottom: 8px;
font-weight: 500;
}
.radio-group, .checkbox-group {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 16px;
}
.radio-label, .checkbox-label {
display: flex;
flex-direction: row;
align-items: center;
font-size: 14px;
color: #666;
}
.hint {
font-size: 12px;
color: #999;
margin-top: 8px;
line-height: 1.5;
max-width: 800px;
}
.form-input {
border: 1px solid #d9d9d9;
border-radius: 4px;
height: 36px;
padding: 0 12px;
font-size: 14px;
width: 100%;
max-width: 400px;
}
.form-textarea {
border: 1px solid #d9d9d9;
border-radius: 4px;
padding: 8px 12px;
font-size: 14px;
width: 100%;
max-width: 600px;
height: 120px;
}
.poster-upload {
width: 80px;
height: 80px;
border: 1px dashed #d9d9d9;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
background-color: #fafafa;
&:hover {
border-color: #1890ff;
}
}
.poster-preview {
width: 100%;
height: 100%;
border-radius: 4px;
}
.upload-btn {
display: flex;
align-items: center;
justify-content: center;
.plus {
font-size: 24px;
color: #999;
}
}
.input-with-unit {
display: flex;
flex-direction: row;
align-items: center;
}
.submit-btn {
margin-top: 24px;
width: 80px;
height: 36px;
background: #1890ff;
color: #fff;
border-radius: 4px;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&:active {
opacity: 0.8;
}
}
</style>

View File

@@ -0,0 +1,85 @@
# 管理后台响应式布局实现指南
本文档总结了商城管理后台从固定布局到响应式布局的改造过程及核心技术点。
## 1. 核心目标
- **多终端适配**:确保后台在桌面端(宽屏)、平板端(中等屏幕)和移动端(窄屏)均能良好展示。
- **自动适配状态管理**:系统能自动感知屏幕宽度并调整侧边栏显隐逻辑。
- **布局平滑过渡**:侧边栏的收起、拉出及内容区的重排应具有良好的动画效果。
## 2. 状态管理 (Store) 扩展
在 [adminNavStore.uts](layouts/admin/store/adminNavStore.uts) 中引入了响应式状态:
- `windowWidth`: 实时存储当前窗口宽度。
- `isMobile`: 计算属性,当宽度小于 768px 时判定为移动端。
- `isMobileMenuOpen`: 控制移动端模式下侧边栏的展开状态。
```typescript
export const windowWidth = ref<number>(1024);
export const isMobile = computed<boolean>(() => windowWidth.value < 768);
export const isMobileMenuOpen = ref<boolean>(false);
```
## 3. 布局架构调整 (AdminLayout)
[AdminLayout.uvue](layouts/admin/AdminLayout.uvue) 负责整体结构的响应式策略:
### 3.1 监听并初始化
`onMounted` 中初始化宽度并监听 `uni.onWindowResize`
```typescript
onMounted(() => {
windowWidth.value = uni.getWindowInfo().windowWidth;
uni.onWindowResize((res) => {
windowWidth.value = res.size.windowWidth;
});
});
```
### 3.2 侧边栏处理
- **桌面端**:侧边栏常规展示,通过 `marginLeft` 为内容区腾出空间。
- **移动端**:侧边栏通过 `position: absolute` 隐藏在屏幕外,通过 `translateX` 动画滑入。同时禁用二级侧边栏的常驻显示。
### 3.3 移动端遮罩与切换
- 引入 `mobile-mask` 遮罩层,点击遮罩自动关闭菜单。
- [AdminHeader](layouts/admin/components/AdminHeader.uvue) 在移动端会显示 “☰” 切换按钮。
## 4. 页面内容重排 (HomeIndex)
[HomeIndex.uvue](layouts/admin/pages/HomeIndex.uvue) 利用 CSS 媒体查询实现内容区域的自适应:
### 4.1 KPI 卡片流式布局
使用 `flex-wrap: wrap` 配合 `min-width`
- **布局策略**:设置 `min-width: 250px` 作为安全边界,确保在 1200px 分辨率下依然能并排展示 4 个卡片。
- **动态列数**
- **宽屏 (>1200px)**: 4个卡片并排。
- **中屏 (768px - 1200px)**: 强制 2 个并排。
- **窄屏 (<768px)**: 1个卡片占满整行。
- **高度控制**:统一固定为 `200px`
### 4.2 图表响应式
- **垂直排列**:底部两个并排的图表在小屏下自动切换为垂直排列(`flex-direction: column`)。
- **组件自适应**:图表组件内部利用父容器宽度自动伸缩。
- **头部压缩**:移动端下,图表的配置项(如日期切换标签)由横向改为纵向,避免溢出。
## 5. 组件化实践 (KpiMiniCard)
统一使用了 [KpiMiniCard](pages/mall/admin/homePage/components/KpiMiniCard.uvue) 组件,确保:
- 样式一致性。
- 代码复用性。
- 内部样式的可维护性。
## 6. 使用建议
- 后续开发新页面时,请优先使用 `stats-row``stat-card` 进行布局。
- 对于复杂的表格页面,建议在移动端隐藏非核心列,或使用横向滚动条展示。
- 所有的宽度判定建议遵循 768px 这一标准断点。

View File

@@ -81,7 +81,7 @@ const props = withDefaults(defineProps<{
const trendArrow = computed((): string => { const trendArrow = computed((): string => {
if (props.trend === 'up') return '▲' if (props.trend === 'up') return '▲'
if (props.trend === 'down') return '▼' if (props.trend === 'down') return '▼'
return '' return ''
}) })
const trendClass = computed((): string => { const trendClass = computed((): string => {
@@ -94,33 +94,43 @@ const trendClass = computed((): string => {
<style> <style>
.kpi-card{ .kpi-card{
background-color:#ffffff; background-color:#ffffff;
border:1px solid #ebeef5; border-radius:4px;
border-radius:6px;
padding:16px; padding:16px;
box-shadow:0 2px 12px rgba(0,0,0,0.04); box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
transition: box-shadow 0.3s;
height: 200px; /* 固定高度 */
min-width: 0; /* 允许由父级 grid 容器决定宽度,防止在 4 列布局时撑爆容器 */
display: flex;
flex-direction: column;
overflow: hidden;
}
.kpi-card:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
} }
/* Header */ /* Header */
.kpi-header{ .kpi-header{
display:flex; display:flex;
flex-direction: row;
align-items:center; align-items:center;
justify-content:space-between; justify-content:space-between;
gap:12px; margin-bottom: 8px;
flex-shrink: 0;
.kpi-title{ .kpi-title{
font-size:14px; font-size:14px;
color:#303133; color:#666666;
font-weight:600; font-weight:400;
} }
.kpi-tag{ .kpi-tag{
padding:2px 8px; padding:1px 6px;
border-radius:4px; border-radius:2px;
border:1px solid #e1f3d8; background-color: #e8f4ff;
background:#f0f9eb;
} }
.kpi-tag-text{ .kpi-tag-text{
font-size:12px; font-size:12px;
color:#67c23a; color:#1890ff;
} }
} }
@@ -128,60 +138,82 @@ const trendClass = computed((): string => {
/* Body */ /* Body */
.kpi-body{ .kpi-body{
margin-top:10px; flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
.kpi-main-value{ .kpi-main-value{
font-size:32px; font-size:30px;
font-weight:600; font-weight:500;
color:#303133; color:#262626;
line-height:40px; line-height:1.2;
margin-bottom: 4px;
} }
/* “昨日 / 日环比” */ /* “昨日 / 日环比” */
.kpi-meta{ .kpi-meta{
margin-top:8px;
display:flex; display:flex;
flex-direction: row;
align-items:center; align-items:center;
justify-content:flex-start; gap:8px;
gap:12px; padding-bottom:12px;
flex-wrap:wrap; border-bottom:1px solid #f0f0f0;
margin-bottom: auto; /* 将 footer 顶到底部 */
flex-wrap: nowrap; /* 不允许换行,依靠父容器 min-width 保证空间 */
} }
.kpi-meta-text{ .kpi-meta-text{
font-size:12px; font-size:12px;
color:#909399; color:#8c8c8c;
flex-shrink: 0;
} }
.kpi-meta-right{ .kpi-meta-right{
display:flex; display:flex;
flex-direction: row;
align-items:center; align-items:center;
gap:6px; gap:4px;
flex-shrink: 0;
} }
.kpi-trend-arrow{ .kpi-trend-arrow{
font-size:12px; font-size:12px;
font-weight: 500;
} }
.kpi-trend-arrow.is-up{ color:#f56c6c; } .kpi-trend-arrow.is-up{ color:#ff4d4f; }
.kpi-trend-arrow.is-down{ color:#67c23a; } .kpi-trend-arrow.is-down{ color:#52c41a; }
.kpi-trend-arrow.is-flat{ color:#909399; } .kpi-trend-arrow.is-flat{ color:#8c8c8c; }
.kpi-divider{ .kpi-divider{
height:1px; display: none; /* 已整合到 meta 的 border-bottom */
background:#ebeef5;
margin:12px 0;
} }
/* Footer */ /* Footer */
.kpi-footer{ .kpi-footer{
display:flex; display:flex;
flex-direction: row;
align-items:center; align-items:center;
justify-content:space-between; justify-content:space-between;
gap:12px; flex-shrink: 0;
} }
.kpi-footer-left{ .kpi-footer-left{
font-size:12px; font-size:12px;
color:#909399; color:#8c8c8c;
white-space: nowrap;
} }
.kpi-footer-right{ .kpi-footer-right{
font-size:12px; font-size:12px;
color:#909399; color:#262626;
font-weight:500;
white-space: nowrap;
} }
} }
/* 响应式微调 */
@media screen and (max-width: 480px) {
.kpi-main-value {
font-size: 26px !important;
}
.kpi-card {
padding: 12px !important;
}
}
</style> </style>

View File

@@ -1,24 +1,43 @@
<template> <template>
<!-- 管理后台入口:直接加载 AdminLayout使用 CRMEB 内部路由系统 --> <view class="admin-home-page">
<AdminLayout /> <AdminLayout>
<view class="home-content">
<text class="welcome-text">管理后台首页</text>
</view>
</AdminLayout>
</view>
</template> </template>
<script setup lang="uts"> <script setup lang="uts">
/** import { ref } from 'vue'
* 管理后台入口页面 import AdminLayout from '@/layouts/admin/AdminLayout'
*
* 架构说明: const title = ref<string>('管理后台首页')
* 1. 此页面是 pages.json 中配置的主入口
* 2. 直接加载 AdminLayout 组件作为容器 onLoad((options: OnLoadOptions) => {
* 3. AdminLayout 内部使用 CRMEB 路由系统管理所有子页面 console.log('首页加载完成')
* 4. 不需要额外的业务逻辑,保持简洁 })
*
* 路由流程: onShow(() => {
* pages.json → homePage/index.uvue → AdminLayout → 内部路由切换 console.log('首页显示')
*/ })
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
</script> </script>
<style scoped> <style scoped lang="scss">
/* 无需额外样式,完全由 AdminLayout 控制布局 */ .admin-home-page {
width: 100%;
min-height: 100vh;
}
.home-content {
padding: 20px;
background-color: #f5f7fa;
}
.welcome-text {
font-size: 20px;
font-weight: 600;
color: #1a1a1a;
}
</style> </style>

View File

@@ -1,14 +1,16 @@
<template> <template>
<AdminLayout currentPage="marketing"> <AdminLayout currentPage="marketing">
<view class="Page"> <view class="admin-page">
<view class="Header"> <view class="admin-sections">
<text class="Title">营销管理</text> <view class="admin-card Header">
<text class="SubTitle">marketing-management</text> <text class="Title">营销管理</text>
</view> <text class="SubTitle">marketing-management</text>
</view>
<view class="Card"> <view class="admin-card Card">
<text class="Label">页面参数query</text> <text class="Label">页面参数query</text>
<text class="Mono">{{ params }}</text> <text class="Mono">{{ params }}</text>
</view>
</view> </view>
</view> </view>
</AdminLayout> </AdminLayout>
@@ -27,13 +29,7 @@ onLoad((options) => {
</script> </script>
<style> <style>
.Page {
padding: 24rpx;
}
.Header { .Header {
padding: 24rpx;
border-radius: 16rpx;
background: #ffffff;
} }
.Title { .Title {
font-size: 36rpx; font-size: 36rpx;
@@ -45,10 +41,6 @@ onLoad((options) => {
opacity: 0.7; opacity: 0.7;
} }
.Card { .Card {
margin-top: 24rpx;
padding: 24rpx;
border-radius: 16rpx;
background: #ffffff;
} }
.Label { .Label {
font-size: 26rpx; font-size: 26rpx;

View File

@@ -182,6 +182,10 @@ export default {
} }
}, },
methods: { methods: {
getStatusLabel(val : number) : string {
const item = this.statusOptions.find((opt: any) => opt.value === val)
return item ? item.label : '全部'
},
getStatusText(status : number) : string { getStatusText(status : number) : string {
switch(status) { switch(status) {
case 0: return '未发货'; case 0: return '未发货';

View File

@@ -7,7 +7,7 @@
<text class="label">时间选择:</text> <text class="label">时间选择:</text>
<view class="date-range-mock"> <view class="date-range-mock">
<text class="date-text">全部</text> <text class="date-text">全部</text>
<uni-icons type="calendar" size="16" color="#c0c4cc"></uni-icons> <text class="iconfont icon-calendar" style="font-size: 16px; color: #c0c4cc;"></text>
</view> </view>
</view> </view>
<view class="filter-item"> <view class="filter-item">
@@ -15,7 +15,7 @@
<picker mode="selector" :range="typeOptions" range-key="label" @change="typeChange"> <picker mode="selector" :range="typeOptions" range-key="label" @change="typeChange">
<view class="admin-input-picker"> <view class="admin-input-picker">
<text>{{ typeLabel }}</text> <text>{{ typeLabel }}</text>
<uni-icons type="arrowdown" size="14" color="#c0c4cc"></uni-icons> <text class="iconfont icon-arrow-down" style="font-size: 14px; color: #c0c4cc;"></text>
</view> </view>
</picker> </picker>
</view> </view>
@@ -60,7 +60,11 @@
<!-- 分页 --> <!-- 分页 -->
<view class="table-pagination"> <view class="table-pagination">
<text class="total-text">共 {{ total }} 条</text> <text class="total-text">共 {{ total }} 条</text>
<uni-pagination :total="total" :pageSize="10" :current="1"></uni-pagination> <view class="page-ops">
<button class="page-btn" disabled>上一页</button>
<text class="current-page">1</text>
<button class="page-btn">下一页</button>
</view>
</view> </view>
</view> </view>
</view> </view>
@@ -272,6 +276,27 @@ export default {
justify-content: flex-end; justify-content: flex-end;
} }
.total-text { font-size: 14px; color: #515a6e; margin-right: 15px; } .total-text { font-size: 14px; color: #515a6e; margin-right: 15px; }
.page-ops {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.page-btn {
height: 32px;
padding: 0 12px;
font-size: 14px;
border-radius: 2px;
border: 1px solid #d9d9d9;
background: #fff;
margin: 0;
}
.current-page {
padding: 0 12px;
font-size: 14px;
color: #2d8cf0;
}
</style> </style>

View File

@@ -1,14 +1,16 @@
<template> <template>
<AdminLayout :currentPage="currentPage"> <AdminLayout :currentPage="currentPage">
<view class="Page"> <view class="admin-page">
<view class="Header"> <view class="admin-sections">
<text class="Title">订单</text> <view class="admin-card Header">
<text class="SubTitle">order-management</text> <text class="Title">订单</text>
</view> <text class="SubTitle">order-management</text>
</view>
<view class="Card"> <view class="admin-card Card">
<text class="Label">页面参数query</text> <text class="Label">页面参数query</text>
<text class="Mono">{{ params }}</text> <text class="Mono">{{ params }}</text>
</view>
</view> </view>
</view> </view>
</AdminLayout> </AdminLayout>
@@ -33,13 +35,7 @@ onLoad((options: Record<string, string>) => {
</script> </script>
<style> <style>
.Page {
padding: 24rpx;
}
.Header { .Header {
padding: 24rpx;
border-radius: 16rpx;
background: #ffffff;
} }
.Title { .Title {
font-size: 36rpx; font-size: 36rpx;
@@ -51,10 +47,6 @@ onLoad((options: Record<string, string>) => {
opacity: 0.7; opacity: 0.7;
} }
.Card { .Card {
margin-top: 24rpx;
padding: 24rpx;
border-radius: 16rpx;
background: #ffffff;
} }
.Label { .Label {
font-size: 26rpx; font-size: 26rpx;

View File

@@ -97,12 +97,12 @@
</view> </view>
<view class="modal-body"> <view class="modal-body">
<view class="qr-item"> <view class="qr-item">
<image class="qr-img" src="https://p.demo.crmeb.net/uploads/attach/2024/09/20240905/66d87e35b7e9b.jpg" mode="aspectFit"></image> <image class="qr-img" src="/static/logo.png" mode="aspectFit"></image>
<text class="qr-label">公众号二维码</text> <text class="qr-label">公众号二维码</text>
</view> </view>
<view class="qr-item"> <view class="qr-item">
<view class="qr-placeholder-mp"> <view class="qr-placeholder-mp">
<image class="mp-qr-mock" src="https://p.demo.crmeb.net/uploads/attach/2024/09/20240905/66d87e35b7e9b.jpg" mode="aspectFit"></image> <image class="mp-qr-mock" src="/static/logo.png" mode="aspectFit"></image>
</view> </view>
<text class="qr-label">小程序二维码</text> <text class="qr-label">小程序二维码</text>
</view> </view>

View File

@@ -1,64 +0,0 @@
<template>
<AdminLayout currentPage="product-classification">
<view class="Page">
<view class="Header">
<text class="Title">商品分类</text>
<text class="SubTitle">product-classification</text>
</view>
<view class="Card">
<text class="Label">页面参数query</text>
<text class="Mono">{{ params }}</text>
</view>
</view>
</AdminLayout>
</template>
<script setup lang="uts">
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
const params = ref('')
onLoad((options) => {
// options: Record<string, any>
params.value = JSON.stringify(options ?? {})
})
</script>
<style>
.Page {
padding: 24rpx;
}
.Header {
padding: 24rpx;
border-radius: 16rpx;
background: #ffffff;
}
.Title {
font-size: 36rpx;
font-weight: 700;
}
.SubTitle {
margin-top: 8rpx;
font-size: 24rpx;
opacity: 0.7;
}
.Card {
margin-top: 24rpx;
padding: 24rpx;
border-radius: 16rpx;
background: #ffffff;
}
.Label {
font-size: 26rpx;
font-weight: 600;
margin-bottom: 12rpx;
}
.Mono {
font-size: 24rpx;
font-family: monospace;
line-height: 36rpx;
word-break: break-all;
}
</style>

View File

@@ -1,64 +0,0 @@
<template>
<AdminLayout currentPage="product-labels">
<view class="Page">
<view class="Header">
<text class="Title">商品标签</text>
<text class="SubTitle">product-labels</text>
</view>
<view class="Card">
<text class="Label">页面参数query</text>
<text class="Mono">{{ params }}</text>
</view>
</view>
</AdminLayout>
</template>
<script setup lang="uts">
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
const params = ref('')
onLoad((options) => {
// options: Record<string, any>
params.value = JSON.stringify(options ?? {})
})
</script>
<style>
.Page {
padding: 24rpx;
}
.Header {
padding: 24rpx;
border-radius: 16rpx;
background: #ffffff;
}
.Title {
font-size: 36rpx;
font-weight: 700;
}
.SubTitle {
margin-top: 8rpx;
font-size: 24rpx;
opacity: 0.7;
}
.Card {
margin-top: 24rpx;
padding: 24rpx;
border-radius: 16rpx;
background: #ffffff;
}
.Label {
font-size: 26rpx;
font-weight: 600;
margin-bottom: 12rpx;
}
.Mono {
font-size: 24rpx;
font-family: monospace;
line-height: 36rpx;
word-break: break-all;
}
</style>

View File

@@ -1,64 +0,0 @@
<template>
<AdminLayout currentPage="product-management">
<view class="Page">
<view class="Header">
<text class="Title">商品</text>
<text class="SubTitle">product-management</text>
</view>
<view class="Card">
<text class="Label">页面参数query</text>
<text class="Mono">{{ params }}</text>
</view>
</view>
</AdminLayout>
</template>
<script setup lang="uts">
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
const params = ref('')
onLoad((options) => {
// options: Record<string, any>
params.value = JSON.stringify(options ?? {})
})
</script>
<style>
.Page {
padding: 24rpx;
}
.Header {
padding: 24rpx;
border-radius: 16rpx;
background: #ffffff;
}
.Title {
font-size: 36rpx;
font-weight: 700;
}
.SubTitle {
margin-top: 8rpx;
font-size: 24rpx;
opacity: 0.7;
}
.Card {
margin-top: 24rpx;
padding: 24rpx;
border-radius: 16rpx;
background: #ffffff;
}
.Label {
font-size: 26rpx;
font-weight: 600;
margin-bottom: 12rpx;
}
.Mono {
font-size: 24rpx;
font-family: monospace;
line-height: 36rpx;
word-break: break-all;
}
</style>

View File

@@ -1,64 +0,0 @@
<template>
<AdminLayout currentPage="product-parameters">
<view class="Page">
<view class="Header">
<text class="Title">商品参数</text>
<text class="SubTitle">product-parameters</text>
</view>
<view class="Card">
<text class="Label">页面参数query</text>
<text class="Mono">{{ params }}</text>
</view>
</view>
</AdminLayout>
</template>
<script setup lang="uts">
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
const params = ref('')
onLoad((options) => {
// options: Record<string, any>
params.value = JSON.stringify(options ?? {})
})
</script>
<style>
.Page {
padding: 24rpx;
}
.Header {
padding: 24rpx;
border-radius: 16rpx;
background: #ffffff;
}
.Title {
font-size: 36rpx;
font-weight: 700;
}
.SubTitle {
margin-top: 8rpx;
font-size: 24rpx;
opacity: 0.7;
}
.Card {
margin-top: 24rpx;
padding: 24rpx;
border-radius: 16rpx;
background: #ffffff;
}
.Label {
font-size: 26rpx;
font-weight: 600;
margin-bottom: 12rpx;
}
.Mono {
font-size: 24rpx;
font-family: monospace;
line-height: 36rpx;
word-break: break-all;
}
</style>

View File

@@ -1,64 +0,0 @@
<template>
<AdminLayout currentPage="product-protection">
<view class="Page">
<view class="Header">
<text class="Title">商品保障</text>
<text class="SubTitle">product-protection</text>
</view>
<view class="Card">
<text class="Label">页面参数query</text>
<text class="Mono">{{ params }}</text>
</view>
</view>
</AdminLayout>
</template>
<script setup lang="uts">
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
const params = ref('')
onLoad((options) => {
// options: Record<string, any>
params.value = JSON.stringify(options ?? {})
})
</script>
<style>
.Page {
padding: 24rpx;
}
.Header {
padding: 24rpx;
border-radius: 16rpx;
background: #ffffff;
}
.Title {
font-size: 36rpx;
font-weight: 700;
}
.SubTitle {
margin-top: 8rpx;
font-size: 24rpx;
opacity: 0.7;
}
.Card {
margin-top: 24rpx;
padding: 24rpx;
border-radius: 16rpx;
background: #ffffff;
}
.Label {
font-size: 26rpx;
font-weight: 600;
margin-bottom: 12rpx;
}
.Mono {
font-size: 24rpx;
font-family: monospace;
line-height: 36rpx;
word-break: break-all;
}
</style>

View File

@@ -1,65 +0,0 @@
<template>
<AdminLayout currentPage="product-reviews">
<view class="Page">
<view class="Header">
<text class="Title">商品评论</text>
<text class="SubTitle">product-reviews</text>
</view>
<view class="Card">
<text class="Label">页面参数query</text>
<text class="Mono">{{ params }}</text>
</view>
</view>
</AdminLayout>
</template>
<script setup lang="uts">
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
const params = ref('')
onLoad((options) => {
// options: Record<string, any>
params.value = JSON.stringify(options ?? {})
})
</script>
<style>
.Page {
padding: 24rpx;
}
.Header {
padding: 24rpx;
border-radius: 16rpx;
background: #ffffff;
}
.Title {
font-size: 36rpx;
font-weight: 700;
}
.SubTitle {
margin-top: 8rpx;
font-size: 24rpx;
opacity: 0.7;
}
.Card {
margin-top: 24rpx;
padding: 24rpx;
border-radius: 16rpx;
background: #ffffff;
}
.Label {
font-size: 26rpx;
font-weight: 600;
margin-bottom: 12rpx;
}
.Mono {
font-size: 24rpx;
font-family: monospace;
line-height: 36rpx;
word-break: break-all;
}
</style>

View File

@@ -1,64 +0,0 @@
<template>
<AdminLayout currentPage="product-specifications">
<view class="Page">
<view class="Header">
<text class="Title">商品规格</text>
<text class="SubTitle">product-specifications</text>
</view>
<view class="Card">
<text class="Label">页面参数query</text>
<text class="Mono">{{ params }}</text>
</view>
</view>
</AdminLayout>
</template>
<script setup lang="uts">
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
const params = ref('')
onLoad((options) => {
// options: Record<string, any>
params.value = JSON.stringify(options ?? {})
})
</script>
<style>
.Page {
padding: 24rpx;
}
.Header {
padding: 24rpx;
border-radius: 16rpx;
background: #ffffff;
}
.Title {
font-size: 36rpx;
font-weight: 700;
}
.SubTitle {
margin-top: 8rpx;
font-size: 24rpx;
opacity: 0.7;
}
.Card {
margin-top: 24rpx;
padding: 24rpx;
border-radius: 16rpx;
background: #ffffff;
}
.Label {
font-size: 26rpx;
font-weight: 600;
margin-bottom: 12rpx;
}
.Mono {
font-size: 24rpx;
font-family: monospace;
line-height: 36rpx;
word-break: break-all;
}
</style>

View File

@@ -1,25 +0,0 @@
<template>
<AdminLayout :currentPage="currentPage">
<view class="page">
<view class="header">
<text class="title">{{ title }}</text>
<text class="sub-title">页面已修复 (UTF-8)</text>
</view>
</view>
</AdminLayout>
</template>
<script setup lang="uts">
import { ref } from 'vue'
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
const currentPage = ref<string>('index')
const title = ref<string>('index')
</script>
<style scoped lang="scss">
@import '@/uni.scss';
.page { padding: $space-lg; }
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
</style>

View File

@@ -1,25 +0,0 @@
<template>
<AdminLayout :currentPage="currentPage">
<view class="page">
<view class="header">
<text class="title">{{ title }}</text>
<text class="sub-title">页面已修复 (UTF-8)</text>
</view>
</view>
</AdminLayout>
</template>
<script setup lang="uts">
import { ref } from 'vue'
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
const currentPage = ref<string>('index')
const title = ref<string>('index')
</script>
<style scoped lang="scss">
@import '@/uni.scss';
.page { padding: $space-lg; }
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
</style>

View File

@@ -1,25 +0,0 @@
<template>
<AdminLayout :currentPage="currentPage">
<view class="page">
<view class="header">
<text class="title">{{ title }}</text>
<text class="sub-title">页面已修复 (UTF-8)</text>
</view>
</view>
</AdminLayout>
</template>
<script setup lang="uts">
import { ref } from 'vue'
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
const currentPage = ref<string>('index')
const title = ref<string>('index')
</script>
<style scoped lang="scss">
@import '@/uni.scss';
.page { padding: $space-lg; }
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
</style>

View File

@@ -1,26 +0,0 @@
<template>
<AdminLayout :currentPage="currentPage">
<view class="page">
<view class="header">
<text class="title">{{ title }}</text>
<text class="sub-title">商品保障条款℃</text>
</view>
</view>
</AdminLayout>
</template>
<script setup lang="uts">
import { ref } from 'vue'
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
const currentPage = ref<string>('product-protection')
const title = ref<string>('商品保障')
</script>
<style scoped lang="scss">
@import '@/uni.scss';
.page { padding: $space-lg; }
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
</style>

View File

@@ -1,25 +0,0 @@
<template>
<AdminLayout :currentPage="currentPage">
<view class="page">
<view class="header">
<text class="title">{{ title }}</text>
<text class="sub-title">页面已修复 (UTF-8)</text>
</view>
</view>
</AdminLayout>
</template>
<script setup lang="uts">
import { ref } from 'vue'
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
const currentPage = ref<string>('index')
const title = ref<string>('index')
</script>
<style scoped lang="scss">
@import '@/uni.scss';
.page { padding: $space-lg; }
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
</style>

View File

@@ -1,26 +0,0 @@
<template>
<AdminLayout :currentPage="currentPage">
<view class="page">
<view class="header">
<text class="title">{{ title }}</text>
<text class="sub-title">商品规格配置</text>
</view>
</view>
</AdminLayout>
</template>
<script setup lang="uts">
import { ref } from 'vue'
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
const currentPage = ref<string>('product-specifications')
const title = ref<string>('商品规格')
</script>
<style scoped lang="scss">
@import '@/uni.scss';
.page { padding: $space-lg; }
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
</style>

View File

@@ -1,7 +1,8 @@
<template> <template>
<view class="product-statistic-page"> <view class="admin-page">
<!-- 商品概况头部 --> <view class="admin-sections">
<view class="page-header-row"> <!-- 商品概况头部 -->
<view class="admin-card page-header-row">
<view class="title-wrap"> <view class="title-wrap">
<text class="page-title">商品概况</text> <text class="page-title">商品概况</text>
<view class="info-icon">?</view> <view class="info-icon">?</view>
@@ -16,9 +17,9 @@
</view> </view>
</view> </view>
<!-- 统计指标网格 --> <!-- 统计指标网格 (使用统一响应式网格) -->
<view class="stat-grid"> <view class="kpi-grid">
<view v-for="(item, index) in statItems" :key="index" class="stat-card"> <view v-for="(item, index) in statItems" :key="index" class="admin-card stat-card">
<view class="stat-main"> <view class="stat-main">
<view class="icon-box" :style="{ backgroundColor: item.bgColor }"> <view class="icon-box" :style="{ backgroundColor: item.bgColor }">
<text class="stat-emoji">{{ item.emoji }}</text> <text class="stat-emoji">{{ item.emoji }}</text>
@@ -41,7 +42,7 @@
</view> </view>
<!-- 图表卡片 --> <!-- 图表卡片 -->
<view class="chart-card"> <view class="admin-card chart-card">
<view class="chart-header"> <view class="chart-header">
<view class="legend-wrap"> <view class="legend-wrap">
<view class="legend-item"><view class="dot purple"></view><text>商品浏览量</text></view> <view class="legend-item"><view class="dot purple"></view><text>商品浏览量</text></view>
@@ -59,7 +60,7 @@
</view> </view>
<!-- 商品排行 --> <!-- 商品排行 -->
<view class="ranking-card"> <view class="admin-card ranking-card">
<view class="ranking-header"> <view class="ranking-header">
<text class="ranking-title">商品排行</text> <text class="ranking-title">商品排行</text>
<view class="ranking-filters"> <view class="ranking-filters">
@@ -114,6 +115,7 @@
</view> </view>
</view> </view>
</view> </view>
</view>
</view> </view>
</template> </template>
@@ -274,18 +276,11 @@ function initChart(data: any[]) {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.product-statistic-page {
padding: 16px;
background-color: #f0f2f5;
min-height: 100vh;
}
.page-header-row { .page-header-row {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 20px;
} }
.title-wrap { .title-wrap {
@@ -327,19 +322,9 @@ function initChart(data: any[]) {
.btn-query { background: #1890ff; color: #fff; font-size: 14px; height: 32px; padding: 0 15px; border-radius: 4px; border: none; } .btn-query { background: #1890ff; color: #fff; font-size: 14px; height: 32px; padding: 0 15px; border-radius: 4px; border: none; }
.btn-export { background: #1890ff; color: #fff; font-size: 14px; height: 32px; padding: 0 15px; border-radius: 4px; border: none; } .btn-export { background: #1890ff; color: #fff; font-size: 14px; height: 32px; padding: 0 15px; border-radius: 4px; border: none; }
.stat-grid { /* stat-grid 已废弃,由全局 kpi-grid 接管 */
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 16px;
margin-bottom: 16px;
}
.stat-card { .stat-card {
width: calc(33.33% - 11px); min-width: 0;
background: #fff;
border-radius: 8px;
padding: 20px;
} }
.stat-main { .stat-main {
@@ -379,9 +364,6 @@ function initChart(data: any[]) {
.arrow { font-size: 10px; margin-left: 2px; } .arrow { font-size: 10px; margin-left: 2px; }
.chart-card { .chart-card {
background: #fff;
border-radius: 8px;
padding: 24px;
} }
.chart-header { .chart-header {

View File

@@ -0,0 +1,23 @@
<template>
<view class="admin-page-container">
<view class="page-card">
<view class="page-header">
<text class="page-title">协议管理</text>
</view>
<view class="page-content">
<text class="placeholder-text">协议管理 页面开发中...</text>
</view>
</view>
</view>
</template>
<script setup lang="uts">
</script>
<style scoped>
.admin-page-container { padding: 20px; background-color: #f5f7f9; min-height: 100vh; }
.page-card { background-color: #fff; border-radius: 4px; padding: 20px; box-shadow: 0 1px 4px rgba(0,21,41,0.08); }
.page-header { margin-bottom: 20px; border-bottom: 1px solid #f0f0f0; padding-bottom: 15px; }
.page-title { font-size: 16px; font-weight: bold; color: #303133; }
.placeholder-text { font-size: 14px; color: #909399; }
</style>

View File

@@ -0,0 +1,23 @@
<template>
<view class="admin-page-container">
<view class="page-card">
<view class="page-header">
<text class="page-title">管理员列表</text>
</view>
<view class="page-content">
<text class="placeholder-text">管理员列表 页面开发中...</text>
</view>
</view>
</view>
</template>
<script setup lang="uts">
</script>
<style scoped>
.admin-page-container { padding: 20px; background-color: #f5f7f9; min-height: 100vh; }
.page-card { background-color: #fff; border-radius: 4px; padding: 20px; box-shadow: 0 1px 4px rgba(0,21,41,0.08); }
.page-header { margin-bottom: 20px; border-bottom: 1px solid #f0f0f0; padding-bottom: 15px; }
.page-title { font-size: 16px; font-weight: bold; color: #303133; }
.placeholder-text { font-size: 14px; color: #909399; }
</style>

View File

@@ -0,0 +1,23 @@
<template>
<view class="admin-page-container">
<view class="page-card">
<view class="page-header">
<text class="page-title">权限设置</text>
</view>
<view class="page-content">
<text class="placeholder-text">权限设置 页面开发中...</text>
</view>
</view>
</view>
</template>
<script setup lang="uts">
</script>
<style scoped>
.admin-page-container { padding: 20px; background-color: #f5f7f9; min-height: 100vh; }
.page-card { background-color: #fff; border-radius: 4px; padding: 20px; box-shadow: 0 1px 4px rgba(0,21,41,0.08); }
.page-header { margin-bottom: 20px; border-bottom: 1px solid #f0f0f0; padding-bottom: 15px; }
.page-title { font-size: 16px; font-weight: bold; color: #303133; }
.placeholder-text { font-size: 14px; color: #909399; }
</style>

View File

@@ -0,0 +1,23 @@
<template>
<view class="admin-page-container">
<view class="page-card">
<view class="page-header">
<text class="page-title">角色管理</text>
</view>
<view class="page-content">
<text class="placeholder-text">角色管理 页面开发中...</text>
</view>
</view>
</view>
</template>
<script setup lang="uts">
</script>
<style scoped>
.admin-page-container { padding: 20px; background-color: #f5f7f9; min-height: 100vh; }
.page-card { background-color: #fff; border-radius: 4px; padding: 20px; box-shadow: 0 1px 4px rgba(0,21,41,0.08); }
.page-header { margin-bottom: 20px; border-bottom: 1px solid #f0f0f0; padding-bottom: 15px; }
.page-title { font-size: 16px; font-weight: bold; color: #303133; }
.placeholder-text { font-size: 14px; color: #909399; }
</style>

View File

@@ -0,0 +1,23 @@
<template>
<view class="admin-page-container">
<view class="page-card">
<view class="page-header">
<text class="page-title">配送员管理</text>
</view>
<view class="page-content">
<text class="placeholder-text">配送员管理 页面开发中...</text>
</view>
</view>
</view>
</template>
<script setup lang="uts">
</script>
<style scoped>
.admin-page-container { padding: 20px; background-color: #f5f7f9; min-height: 100vh; }
.page-card { background-color: #fff; border-radius: 4px; padding: 20px; box-shadow: 0 1px 4px rgba(0,21,41,0.08); }
.page-header { margin-bottom: 20px; border-bottom: 1px solid #f0f0f0; padding-bottom: 15px; }
.page-title { font-size: 16px; font-weight: bold; color: #303133; }
.placeholder-text { font-size: 14px; color: #909399; }
</style>

View File

@@ -0,0 +1,23 @@
<template>
<view class="admin-page-container">
<view class="page-card">
<view class="page-header">
<text class="page-title">提货点设置</text>
</view>
<view class="page-content">
<text class="placeholder-text">提货点设置 页面开发中...</text>
</view>
</view>
</view>
</template>
<script setup lang="uts">
</script>
<style scoped>
.admin-page-container { padding: 20px; background-color: #f5f7f9; min-height: 100vh; }
.page-card { background-color: #fff; border-radius: 4px; padding: 20px; box-shadow: 0 1px 4px rgba(0,21,41,0.08); }
.page-header { margin-bottom: 20px; border-bottom: 1px solid #f0f0f0; padding-bottom: 15px; }
.page-title { font-size: 16px; font-weight: bold; color: #303133; }
.placeholder-text { font-size: 14px; color: #909399; }
</style>

View File

@@ -0,0 +1,23 @@
<template>
<view class="admin-page-container">
<view class="page-card">
<view class="page-header">
<text class="page-title">运费模板</text>
</view>
<view class="page-content">
<text class="placeholder-text">运费模板 页面开发中...</text>
</view>
</view>
</view>
</template>
<script setup lang="uts">
</script>
<style scoped>
.admin-page-container { padding: 20px; background-color: #f5f7f9; min-height: 100vh; }
.page-card { background-color: #fff; border-radius: 4px; padding: 20px; box-shadow: 0 1px 4px rgba(0,21,41,0.08); }
.page-header { margin-bottom: 20px; border-bottom: 1px solid #f0f0f0; padding-bottom: 15px; }
.page-title { font-size: 16px; font-weight: bold; color: #303133; }
.placeholder-text { font-size: 14px; color: #909399; }
</style>

View File

@@ -0,0 +1,23 @@
<template>
<view class="admin-page-container">
<view class="page-card">
<view class="page-header">
<text class="page-title">商品采集配置</text>
</view>
<view class="page-content">
<text class="placeholder-text">商品采集配置 页面开发中...</text>
</view>
</view>
</view>
</template>
<script setup lang="uts">
</script>
<style scoped>
.admin-page-container { padding: 20px; background-color: #f5f7f9; min-height: 100vh; }
.page-card { background-color: #fff; border-radius: 4px; padding: 20px; box-shadow: 0 1px 4px rgba(0,21,41,0.08); }
.page-header { margin-bottom: 20px; border-bottom: 1px solid #f0f0f0; padding-bottom: 15px; }
.page-title { font-size: 16px; font-weight: bold; color: #303133; }
.placeholder-text { font-size: 14px; color: #909399; }
</style>

View File

@@ -0,0 +1,23 @@
<template>
<view class="admin-page-container">
<view class="page-card">
<view class="page-header">
<text class="page-title">电子面单配置</text>
</view>
<view class="page-content">
<text class="placeholder-text">电子面单配置 页面开发中...</text>
</view>
</view>
</view>
</template>
<script setup lang="uts">
</script>
<style scoped>
.admin-page-container { padding: 20px; background-color: #f5f7f9; min-height: 100vh; }
.page-card { background-color: #fff; border-radius: 4px; padding: 20px; box-shadow: 0 1px 4px rgba(0,21,41,0.08); }
.page-header { margin-bottom: 20px; border-bottom: 1px solid #f0f0f0; padding-bottom: 15px; }
.page-title { font-size: 16px; font-weight: bold; color: #303133; }
.placeholder-text { font-size: 14px; color: #909399; }
</style>

View File

@@ -0,0 +1,23 @@
<template>
<view class="admin-page-container">
<view class="page-card">
<view class="page-header">
<text class="page-title">物流查询配置</text>
</view>
<view class="page-content">
<text class="placeholder-text">物流查询配置 页面开发中...</text>
</view>
</view>
</view>
</template>
<script setup lang="uts">
</script>
<style scoped>
.admin-page-container { padding: 20px; background-color: #f5f7f9; min-height: 100vh; }
.page-card { background-color: #fff; border-radius: 4px; padding: 20px; box-shadow: 0 1px 4px rgba(0,21,41,0.08); }
.page-header { margin-bottom: 20px; border-bottom: 1px solid #f0f0f0; padding-bottom: 15px; }
.page-title { font-size: 16px; font-weight: bold; color: #303133; }
.placeholder-text { font-size: 14px; color: #909399; }
</style>

View File

@@ -0,0 +1,23 @@
<template>
<view class="admin-page-container">
<view class="page-card">
<view class="page-header">
<text class="page-title">一号通配置</text>
</view>
<view class="page-content">
<text class="placeholder-text">一号通配置 页面开发中...</text>
</view>
</view>
</view>
</template>
<script setup lang="uts">
</script>
<style scoped>
.admin-page-container { padding: 20px; background-color: #f5f7f9; min-height: 100vh; }
.page-card { background-color: #fff; border-radius: 4px; padding: 20px; box-shadow: 0 1px 4px rgba(0,21,41,0.08); }
.page-header { margin-bottom: 20px; border-bottom: 1px solid #f0f0f0; padding-bottom: 15px; }
.page-title { font-size: 16px; font-weight: bold; color: #303133; }
.placeholder-text { font-size: 14px; color: #909399; }
</style>

View File

@@ -0,0 +1,23 @@
<template>
<view class="admin-page-container">
<view class="page-card">
<view class="page-header">
<text class="page-title">一号通页面</text>
</view>
<view class="page-content">
<text class="placeholder-text">一号通页面 页面开发中...</text>
</view>
</view>
</view>
</template>
<script setup lang="uts">
</script>
<style scoped>
.admin-page-container { padding: 20px; background-color: #f5f7f9; min-height: 100vh; }
.page-card { background-color: #fff; border-radius: 4px; padding: 20px; box-shadow: 0 1px 4px rgba(0,21,41,0.08); }
.page-header { margin-bottom: 20px; border-bottom: 1px solid #f0f0f0; padding-bottom: 15px; }
.page-title { font-size: 16px; font-weight: bold; color: #303133; }
.placeholder-text { font-size: 14px; color: #909399; }
</style>

View File

@@ -0,0 +1,23 @@
<template>
<view class="admin-page-container">
<view class="page-card">
<view class="page-header">
<text class="page-title">商城支付配置</text>
</view>
<view class="page-content">
<text class="placeholder-text">商城支付配置 页面开发中...</text>
</view>
</view>
</view>
</template>
<script setup lang="uts">
</script>
<style scoped>
.admin-page-container { padding: 20px; background-color: #f5f7f9; min-height: 100vh; }
.page-card { background-color: #fff; border-radius: 4px; padding: 20px; box-shadow: 0 1px 4px rgba(0,21,41,0.08); }
.page-header { margin-bottom: 20px; border-bottom: 1px solid #f0f0f0; padding-bottom: 15px; }
.page-title { font-size: 16px; font-weight: bold; color: #303133; }
.placeholder-text { font-size: 14px; color: #909399; }
</style>

View File

@@ -0,0 +1,23 @@
<template>
<view class="admin-page-container">
<view class="page-card">
<view class="page-header">
<text class="page-title">短信接口配置</text>
</view>
<view class="page-content">
<text class="placeholder-text">短信接口配置 页面开发中...</text>
</view>
</view>
</view>
</template>
<script setup lang="uts">
</script>
<style scoped>
.admin-page-container { padding: 20px; background-color: #f5f7f9; min-height: 100vh; }
.page-card { background-color: #fff; border-radius: 4px; padding: 20px; box-shadow: 0 1px 4px rgba(0,21,41,0.08); }
.page-header { margin-bottom: 20px; border-bottom: 1px solid #f0f0f0; padding-bottom: 15px; }
.page-title { font-size: 16px; font-weight: bold; color: #303133; }
.placeholder-text { font-size: 14px; color: #909399; }
</style>

View File

@@ -0,0 +1,23 @@
<template>
<view class="admin-page-container">
<view class="page-card">
<view class="page-header">
<text class="page-title">系统存储配置</text>
</view>
<view class="page-content">
<text class="placeholder-text">系统存储配置 页面开发中...</text>
</view>
</view>
</view>
</template>
<script setup lang="uts">
</script>
<style scoped>
.admin-page-container { padding: 20px; background-color: #f5f7f9; min-height: 100vh; }
.page-card { background-color: #fff; border-radius: 4px; padding: 20px; box-shadow: 0 1px 4px rgba(0,21,41,0.08); }
.page-header { margin-bottom: 20px; border-bottom: 1px solid #f0f0f0; padding-bottom: 15px; }
.page-title { font-size: 16px; font-weight: bold; color: #303133; }
.placeholder-text { font-size: 14px; color: #909399; }
</style>

View File

@@ -0,0 +1,23 @@
<template>
<view class="admin-page-container">
<view class="page-card">
<view class="page-header">
<text class="page-title">消息管理</text>
</view>
<view class="page-content">
<text class="placeholder-text">消息管理 页面开发中...</text>
</view>
</view>
</view>
</template>
<script setup lang="uts">
</script>
<style scoped>
.admin-page-container { padding: 20px; background-color: #f5f7f9; min-height: 100vh; }
.page-card { background-color: #fff; border-radius: 4px; padding: 20px; box-shadow: 0 1px 4px rgba(0,21,41,0.08); }
.page-header { margin-bottom: 20px; border-bottom: 1px solid #f0f0f0; padding-bottom: 15px; }
.page-title { font-size: 16px; font-weight: bold; color: #303133; }
.placeholder-text { font-size: 14px; color: #909399; }
</style>

View File

@@ -0,0 +1,23 @@
<template>
<view class="admin-page-container">
<view class="page-card">
<view class="page-header">
<text class="page-title">协议设置</text>
</view>
<view class="page-content">
<text class="placeholder-text">协议设置 页面开发中...</text>
</view>
</view>
</view>
</template>
<script setup lang="uts">
</script>
<style scoped>
.admin-page-container { padding: 20px; background-color: #f5f7f9; min-height: 100vh; }
.page-card { background-color: #fff; border-radius: 4px; padding: 20px; box-shadow: 0 1px 4px rgba(0,21,41,0.08); }
.page-header { margin-bottom: 20px; border-bottom: 1px solid #f0f0f0; padding-bottom: 15px; }
.page-title { font-size: 16px; font-weight: bold; color: #303133; }
.placeholder-text { font-size: 14px; color: #909399; }
</style>

View File

@@ -1,81 +1,333 @@
<template> <template>
<view class="page-container"> <view class="admin-page">
<view class="page-header"> <view class="admin-sections">
<text class="page-title">系统配置</text> <view class="admin-card settings-card">
<text class="page-subtitle">Component: SettingSystemConfig</text> <!-- 顶部导航标签 (1:1 复刻 CRMEB: 横向排列) -->
</view> <view class="tabs-container">
<scroll-view class="tabs-scroll" scroll-x="true" show-scrollbar="false" :enable-flex="true">
<view class="page-content"> <view class="tabs-bar">
<view class="placeholder-card"> <view
<text class="placeholder-title">页面占位</text> v-for="(tab, index) in tabs"
<text class="placeholder-desc">该功能模块正在开发中</text> :key="index"
<text class="placeholder-info">当前采用 CRMEB 路由体系 1:1 映射</text> class="tab-item"
:class="{ active: currentTab === index }"
@click="currentTab = index"
>
<text class="tab-text">{{ tab.name }}</text>
<view class="tab-line" v-if="currentTab === index"></view>
</view>
</view>
</scroll-view>
</view> </view>
<!-- 表单区域 -->
<view class="form-container">
<!-- 1. 基础配置 -->
<view v-if="currentTab === 0" class="form-content">
<view class="form-item">
<view class="form-label">站点开启:</view>
<view class="form-right">
<radio-group class="radio-group" @change="formData.site_open = parseInt(($event.detail.value as string))">
<label class="radio-label"><radio value="1" :checked="formData.site_open == 1" color="#1890ff" />开启</label>
<label class="radio-label"><radio value="0" :checked="formData.site_open == 0" color="#1890ff" />关闭</label>
</radio-group>
<view class="form-tip">站点开启/关闭(用于升级等临时关闭),关闭后前端会弹窗显示站点升级中,请稍后访问</view>
</view>
</view>
<view class="form-item">
<view class="form-label">网站名称:</view>
<view class="form-right">
<input class="form-input" v-model="formData.site_name" placeholder="请输入网站名称" />
<view class="form-tip">网站名称很多地方会显示的,建议认真填写</view>
</view>
</view>
<view class="form-item">
<view class="form-label">网站地址:</view>
<view class="form-right">
<input class="form-input" v-model="formData.site_url" placeholder="请输入网站地址" />
<view class="form-tip">安装自动配置,不要轻易修改,更换后会影响网站访问、接口请求、本地文件储存、支付回调、微信授权、支付、小程序图片访问、部分二维码、官方授权等</view>
</view>
</view>
<view class="form-item">
<view class="form-label">消息队列:</view>
<view class="form-right">
<radio-group class="radio-group" @change="formData.msg_queue = parseInt(($event.detail.value as string))">
<label class="radio-label"><radio value="1" :checked="formData.msg_queue == 1" color="#1890ff" />开启</label>
<label class="radio-label"><radio value="0" :checked="formData.msg_queue == 0" color="#1890ff" />关闭</label>
</radio-group>
<view class="form-tip">是否启用消息队列启用后提升程序运行速度启用前必须配置Redis缓存文档地址https://doc.crmeb.com/single/crmeb_v4/7217</view>
</view>
</view>
<view class="form-item">
<view class="form-label">联系电话:</view>
<view class="form-right">
<input class="form-input" v-model="formData.contact_phone" placeholder="请输入联系电话" />
<view class="form-tip">联系电话</view>
</view>
</view>
<view class="form-item">
<view class="form-label">授权密钥:</view>
<view class="form-right">
<input class="form-input" v-model="formData.auth_key" placeholder="请输入授权密钥" />
</view>
</view>
</view>
<!-- 2. 分享配置 -->
<view v-else-if="currentTab === 1" class="form-content">
<view class="form-item">
<view class="form-label">分享图片:</view>
<view class="form-right">
<view class="upload-placeholder" @click="handleUpload('share_img')">上传图片</view>
<view class="form-tip">分享图片比例5:4建议小于50KB</view>
</view>
</view>
<view class="form-item">
<view class="form-label">分享标题:</view>
<view class="form-right">
<input class="form-input" v-model="formData.share_title" />
</view>
</view>
<view class="form-item">
<view class="form-label">分享简介:</view>
<view class="form-right">
<textarea class="form-textarea" v-model="formData.share_desc" />
</view>
</view>
</view>
<!-- 3. LOGO配置 -->
<view v-else-if="currentTab === 2" class="form-content">
<view class="form-item">
<view class="form-label">后台登录LOGO:</view>
<view class="form-right">
<view class="upload-placeholder" @click="handleUpload('login_logo')">上传截图</view>
<view class="form-tip">建议尺寸270*75</view>
</view>
</view>
<view class="form-item">
<view class="form-label">后台小LOGO:</view>
<view class="form-right">
<view class="upload-placeholder" @click="handleUpload('small_logo')">上传图片</view>
<view class="form-tip">建议尺寸180*180</view>
</view>
</view>
<view class="form-item">
<view class="form-label">后台大LOGO:</view>
<view class="form-right">
<view class="upload-placeholder" @click="handleUpload('big_logo')">上传图片</view>
<view class="form-tip">建议尺寸170*50</view>
</view>
</view>
</view>
<!-- 4. 自定义JS -->
<view v-else-if="currentTab === 3" class="form-content">
<view class="form-item">
<view class="form-label">移动端JS:</view>
<view class="form-right">
<textarea class="form-textarea code-bg" v-model="formData.mobile_js" />
</view>
</view>
<view class="form-item">
<view class="form-label">管理端JS:</view>
<view class="form-right">
<textarea class="form-textarea code-bg" v-model="formData.admin_js" />
</view>
</view>
<view class="form-item">
<view class="form-label">PC端JS:</view>
<view class="form-right">
<textarea class="form-textarea code-bg" v-model="formData.pc_js" />
</view>
</view>
</view>
<!-- 5. 地图配置 -->
<view v-else-if="currentTab === 4" class="form-content">
<view class="form-item">
<view class="form-label">腾讯地图KEY:</view>
<view class="form-right">
<input class="form-input" v-model="formData.tencent_map_key" />
<view class="form-tip">申请地址https://lbs.qq.com</view>
</view>
</view>
</view>
<!-- 6. 备案配置 -->
<view v-else-if="currentTab === 5" class="form-content">
<view class="form-item">
<view class="form-label">备案号:</view>
<view class="form-right">
<input class="form-input" v-model="formData.filing_no" />
</view>
</view>
<view class="form-item">
<view class="form-label">ICP链接:</view>
<view class="form-right">
<input class="form-input" v-model="formData.icp_link" />
</view>
</view>
</view>
<!-- 7. 模块配置 -->
<view v-else-if="currentTab === 6" class="form-content">
<view class="form-item">
<view class="form-label">功能开启:</view>
<view class="form-right">
<checkbox-group class="checkbox-group" @change="formData.module_config = ($event.detail.value as string[])">
<label class="checkbox-label"><checkbox value="秒杀" :checked="formData.module_config.includes('秒杀')" color="#1890ff" />秒杀</label>
<label class="checkbox-label"><checkbox value="砍价" :checked="formData.module_config.includes('砍价')" color="#1890ff" />砍价</label>
<label class="checkbox-label"><checkbox value="拼团" :checked="formData.module_config.includes('拼团')" color="#1890ff" />拼团</label>
</checkbox-group>
</view>
</view>
</view>
<!-- 8. 远程登录 -->
<view v-else-if="currentTab === 7" class="form-content">
<view class="form-item">
<view class="form-label">远程登录地址:</view>
<view class="form-right">
<input class="form-input" v-model="formData.remote_login_url" />
</view>
</view>
</view>
<!-- 9. WAF配置 -->
<view v-else-if="currentTab === 8" class="form-content">
<view class="form-item">
<view class="form-label">WAF类型:</view>
<view class="form-right">
<radio-group class="radio-group" @change="formData.waf_type = parseInt(($event.detail.value as string))">
<label class="radio-label"><radio value="0" :checked="formData.waf_type == 0" color="#1890ff" />关闭</label>
<label class="radio-label"><radio value="1" :checked="formData.waf_type == 1" color="#1890ff" />拦截</label>
<label class="radio-label"><radio value="2" :checked="formData.waf_type == 2" color="#1890ff" />过滤</label>
</radio-group>
<view class="form-tip">WAF类型关闭所有参数都能正常请求拦截匹配到WAF配置的参数阻断接口请求过滤匹配到WAF配置的参数过滤参数正常请求接口</view>
</view>
</view>
<view class="form-item">
<view class="form-label">WAF配置:</view>
<view class="form-right">
<textarea class="form-textarea code-bg waf-textarea" v-model="formData.waf_config" />
<view class="form-tip">WAF配置验证参数过滤掉不需要的参数或拦截请求多个参数用回车换行分隔</view>
</view>
</view>
</view>
<!-- 提交按钮 -->
<view class="submit-section">
<view class="form-label"></view> <!-- 占位用于对齐 -->
<view class="form-right">
<button class="btn-submit" @click="handleSubmit">提交</button>
</view>
</view>
</view>
</view>
</view> </view>
</view> </view>
</template> </template>
<script setup lang="uts"> <script setup lang="uts">
import { ref } from 'vue' import { ref } from "vue"
// TODO: 实现 系统配置 的具体功能 const currentTab = ref(0)
const loading = ref<boolean>(false) const tabs = [
{ name: "基础配置" }, { name: "分享配置" }, { name: "LOGO配置" },
{ name: "自定义JS" }, { name: "地图配置" }, { name: "备案配置" },
{ name: "模块配置" }, { name: "远程登录配置" }, { name: "WAF配置" }
]
const formData = ref({
site_open: 1,
site_name: "CRMEB标准版",
site_url: "https://v5.crmeb.net",
msg_queue: 0,
contact_phone: "",
auth_key: "AO9azvBW9vEcOH7swklTM0RYRb6EB4RLWMSD88MnKTi8Vd6cjXVd",
share_img: "",
share_title: "CRMEB v5标准版",
share_desc: "完善的文档 全心而来!",
login_logo: "",
small_logo: "",
big_logo: "",
mobile_js: "",
admin_js: "",
pc_js: "",
tencent_map_key: "SMJBZ-WCHK4-ZPZUA-DSIXI-XDDVQ-XWFX7",
filing_no: "陕ICP备14011498号-3",
icp_link: "https://beian.miit.gov.cn/",
module_config: ["秒杀", "砍价", "拼团"] as string[],
remote_login_url: "",
waf_type: 2,
waf_config: "/\\.\\.\\//\n/\\<\\?/\n/\\bor\\b.*=\\s*\\*/i\n/(select[\\s\\S]*?)(from|limit)/i\n/(union[\\s\\S]*?select)/i\n/(having\\s+updatexml|extractvalue)/i"
})
const handleUpload = (field: string) => {
uni.showToast({ title: "选择文件: " + field, icon: "none" })
}
const handleSubmit = () => {
uni.showLoading({ title: "保存中..." })
setTimeout(() => {
uni.hideLoading()
uni.showToast({ title: "保存成功", icon: "success" })
}, 800)
}
</script> </script>
<style scoped lang="scss"> <style scoped>
.page-container { .settings-card {
padding: 20px; box-shadow: 0 1px 4px rgba(0,21,41,0.08);
min-height: 100vh;
background: #f5f5f5;
} }
.page-header { /* 核心修复:确保 Tabs 横向排列并且可以滑动 */
margin-bottom: 20px; .tabs-container { margin-bottom: 30px; border-bottom: 1px solid #e8eaec; width: 100%; overflow: hidden; }
} .tabs-scroll { width: 100%; white-space: nowrap; }
.tabs-bar { display: inline-flex; flex-direction: row; min-width: 100%; }
.page-title { .tab-item {
display: block; display: inline-flex;
font-size: 24px; flex-direction: row;
font-weight: 600; align-items: center;
color: #333; padding: 12px 24px;
margin-bottom: 8px; font-size: 14px;
color: #515a6e;
position: relative;
cursor: pointer;
flex-shrink: 0;
} }
.tab-item.active { color: #1890ff; font-weight: bold; }
.tab-line { position: absolute; bottom: 0; left: 0; right: 0; height: 2px; background-color: #1890ff; }
.page-subtitle { .form-container { padding-left: 20px; }
display: block;
font-size: 14px;
color: #999;
}
.page-content { /* 核心修复:确保表单项横向排列 (Label left, Input right) */
background: #fff; .form-item { display: flex; flex-direction: row; margin-bottom: 25px; align-items: flex-start; }
border-radius: 4px;
padding: 24px;
}
.placeholder-card { .form-label { width: 140px; font-size: 14px; color: #303133; text-align: right; padding-right: 20px; padding-top: 8px; flex-shrink: 0; }
text-align: center; .form-right { flex: 1; display: flex; flex-direction: column; width: 100%; }
padding: 60px 20px;
}
.placeholder-title { .form-input { border: 1px solid #dcdfe6; width: 450px; height: 32px; padding: 0 15px; border-radius: 4px; font-size: 14px; color: #606266; outline: none; transition: border-color .2s; }
display: block; .form-input:focus { border-color: #409eff; }
font-size: 18px;
font-weight: 600;
color: #666;
margin-bottom: 12px;
}
.placeholder-desc { .form-textarea { border: 1px solid #dcdfe6; width: 550px; height: 120px; padding: 10px 15px; border-radius: 4px; font-size: 14px; color: #606266; line-height: 1.6; outline: none; }
display: block; .form-textarea:focus { border-color: #409eff; }
font-size: 14px;
color: #999;
margin-bottom: 8px;
}
.placeholder-info { .waf-textarea { height: 200px; }
display: block; .code-bg { background-color: #f5f7fa; font-family: "Lucida Console", Monaco, monospace; }
font-size: 12px;
color: #1890ff; .form-tip { font-size: 12px; color: #c0c4cc; margin-top: 8px; line-height: 1.5; width: 550px; }
}
</style> .radio-group, .checkbox-group { display: flex; flex-direction: row; align-items: center; min-height: 32px; }
.radio-label, .checkbox-label { display: flex; flex-direction: row; align-items: center; margin-right: 30px; font-size: 14px; color: #606266; cursor: pointer; }
.upload-placeholder { width: 80px; height: 80px; border: 1px dashed #dcdfe6; border-radius: 4px; display: flex; align-items: center; justify-content: center; font-size: 12px; color: #909399; cursor: pointer; transition: all .2s; }
.upload-placeholder:hover { border-color: #409eff; color: #409eff; }
.submit-section { display: flex; flex-direction: row; margin-top: 20px; padding-top: 10px; }
.btn-submit { background-color: #1890ff; color: #fff; width: 65px; height: 32px; line-height: 32px; font-size: 14px; border-radius: 4px; margin: 0; border: none; cursor: pointer; text-align: center; }
.btn-submit:active { background-color: #096dd9; }
</style>

View File

@@ -0,0 +1,23 @@
<template>
<view class="admin-page-container">
<view class="page-card">
<view class="page-header">
<text class="page-title">消息管理</text>
</view>
<view class="page-content">
<text class="placeholder-text">消息管理 页面开发中...</text>
</view>
</view>
</view>
</template>
<script setup lang="uts">
</script>
<style scoped>
.admin-page-container { padding: 20px; background-color: #f5f7f9; min-height: 100vh; }
.page-card { background-color: #fff; border-radius: 4px; padding: 20px; box-shadow: 0 1px 4px rgba(0,21,41,0.08); }
.page-header { margin-bottom: 20px; border-bottom: 1px solid #f0f0f0; padding-bottom: 15px; }
.page-title { font-size: 16px; font-weight: bold; color: #303133; }
.placeholder-text { font-size: 14px; color: #909399; }
</style>

View File

@@ -0,0 +1,23 @@
<template>
<view class="admin-page-container">
<view class="page-card">
<view class="page-header">
<text class="page-title">小票配置</text>
</view>
<view class="page-content">
<text class="placeholder-text">小票配置 页面开发中...</text>
</view>
</view>
</view>
</template>
<script setup lang="uts">
</script>
<style scoped>
.admin-page-container { padding: 20px; background-color: #f5f7f9; min-height: 100vh; }
.page-card { background-color: #fff; border-radius: 4px; padding: 20px; box-shadow: 0 1px 4px rgba(0,21,41,0.08); }
.page-header { margin-bottom: 20px; border-bottom: 1px solid #f0f0f0; padding-bottom: 15px; }
.page-title { font-size: 16px; font-weight: bold; color: #303133; }
.placeholder-text { font-size: 14px; color: #909399; }
</style>

View File

@@ -0,0 +1,23 @@
<template>
<view class="admin-page-container">
<view class="page-card">
<view class="page-header">
<text class="page-title">客服设置</text>
</view>
<view class="page-content">
<text class="placeholder-text">客服设置 页面开发中...</text>
</view>
</view>
</view>
</template>
<script setup lang="uts">
</script>
<style scoped>
.admin-page-container { padding: 20px; background-color: #f5f7f9; min-height: 100vh; }
.page-card { background-color: #fff; border-radius: 4px; padding: 20px; box-shadow: 0 1px 4px rgba(0,21,41,0.08); }
.page-header { margin-bottom: 20px; border-bottom: 1px solid #f0f0f0; padding-bottom: 15px; }
.page-title { font-size: 16px; font-weight: bold; color: #303133; }
.placeholder-text { font-size: 14px; color: #909399; }
</style>

View File

@@ -1,65 +0,0 @@
<template>
<AdminLayout currentPage="sys-basic">
<view class="Page">
<view class="Header">
<text class="Title">设置</text>
<text class="SubTitle">system-settings</text>
</view>
<view class="Card">
<text class="Label">页面参数query</text>
<text class="Mono">{{ params }}</text>
</view>
</view>
</AdminLayout>
</template>
<script setup lang="uts">
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
const params = ref('')
onLoad((options) => {
// options: Record<string, any>
params.value = JSON.stringify(options ?? {})
})
</script>
<style>
.Page {
padding: 24rpx;
}
.Header {
padding: 24rpx;
border-radius: 16rpx;
background: #ffffff;
}
.Title {
font-size: 36rpx;
font-weight: 700;
}
.SubTitle {
margin-top: 8rpx;
font-size: 24rpx;
opacity: 0.7;
}
.Card {
margin-top: 24rpx;
padding: 24rpx;
border-radius: 16rpx;
background: #ffffff;
}
.Label {
font-size: 26rpx;
font-weight: 600;
margin-bottom: 12rpx;
}
.Mono {
font-size: 24rpx;
font-family: monospace;
line-height: 36rpx;
word-break: break-all;
}
</style>

View File

@@ -1,70 +0,0 @@
<template>
<AdminLayout :currentPage="currentPage">
<view class="Page">
<view class="Header">
<text class="Title">用户</text>
<text class="SubTitle">user-management</text>
</view>
<view class="Card">
<text class="Label">页面参数query</text>
<text class="Mono">{{ params }}</text>
</view>
</view>
</AdminLayout>
</template>
<script setup lang="uts">
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
const currentPage = ref('user-list') // 默认
onLoad((opts: Record<string, string>) => {
const action = opts['action'] || ''
if (action == 'group') currentPage.value = 'user-group'
else if (action == 'tag') currentPage.value = 'user-tag'
else if (action == 'level') currentPage.value = 'user-level'
else if (action == 'config') currentPage.value = 'user-config'
else currentPage.value = 'user-list'
})
</script>
<style>
.Page {
padding: 24rpx;
}
.Header {
padding: 24rpx;
border-radius: 16rpx;
background: #ffffff;
}
.Title {
font-size: 36rpx;
font-weight: 700;
}
.SubTitle {
margin-top: 8rpx;
font-size: 24rpx;
opacity: 0.7;
}
.Card {
margin-top: 24rpx;
padding: 24rpx;
border-radius: 16rpx;
background: #ffffff;
}
.Label {
font-size: 26rpx;
font-weight: 600;
margin-bottom: 12rpx;
}
.Mono {
font-size: 24rpx;
font-family: monospace;
line-height: 36rpx;
word-break: break-all;
}
</style>

View File

@@ -1,65 +0,0 @@
<template>
<AdminLayout currentPage="user">
<view class="Page">
<view class="Header">
<text class="Title">用户统计</text>
<text class="SubTitle">user-statistics</text>
</view>
<view class="Card">
<text class="Label">页面参数query</text>
<text class="Mono">{{ params }}</text>
</view>
</view>
</AdminLayout>
</template>
<script setup lang="uts">
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
const params = ref('')
onLoad((options) => {
// options: Record<string, any>
params.value = JSON.stringify(options ?? {})
})
</script>
<style>
.Page {
padding: 24rpx;
}
.Header {
padding: 24rpx;
border-radius: 16rpx;
background: #ffffff;
}
.Title {
font-size: 36rpx;
font-weight: 700;
}
.SubTitle {
margin-top: 8rpx;
font-size: 24rpx;
opacity: 0.7;
}
.Card {
margin-top: 24rpx;
padding: 24rpx;
border-radius: 16rpx;
background: #ffffff;
}
.Label {
font-size: 26rpx;
font-weight: 600;
margin-bottom: 12rpx;
}
.Mono {
font-size: 24rpx;
font-family: monospace;
line-height: 36rpx;
word-break: break-all;
}
</style>

View File

@@ -74,7 +74,7 @@
</view> </view>
<view class="item-content"> <view class="item-content">
<view class="avatar-upload"> <view class="avatar-upload">
<image class="avatar-preview" src="https://img.crmeb.com/crmeb_demo/75211.png" mode="aspectFill" /> <image class="avatar-preview" src="/static/logo.png" mode="aspectFill" />
<view class="upload-mask"> <view class="upload-mask">
<text class="upload-icon">+</text> <text class="upload-icon">+</text>
</view> </view>

View File

@@ -1,8 +1,9 @@
<template> <template>
<view class="user-list-page"> <view class="admin-page">
<!-- 筛选面板 --> <view class="admin-sections">
<view class="filter-card"> <!-- 筛选面板 -->
<view class="filter-row"> <view class="admin-card filter-card">
<view class="filter-row">
<view class="filter-item"> <view class="filter-item">
<text class="label">用户搜索:</text> <text class="label">用户搜索:</text>
<view class="input-group"> <view class="input-group">
@@ -39,7 +40,7 @@
</view> </view>
<!-- 内容卡片 --> <!-- 内容卡片 -->
<view class="content-card"> <view class="admin-card content-card">
<!-- 平台切换 Tabs --> <!-- 平台切换 Tabs -->
<view class="tabs-row"> <view class="tabs-row">
<view <view
@@ -143,6 +144,7 @@
</view> </view>
</view> </view>
</view> </view>
</view>
</template> </template>
<script setup lang="uts"> <script setup lang="uts">
@@ -154,15 +156,15 @@ const isAllChecked = ref(false)
const activeDropdownId = ref<string | null>(null) const activeDropdownId = ref<string | null>(null)
const userList = ref([ const userList = ref([
{ id: '77414', avatar: 'https://img.crmeb.com/crmeb_demo/77414.png', nickname: '199****0268', isMember: '否', level: '无', group: '无', spreadLevel: '', phone: '199****0268', userType: '公众号', balance: '88888.00', checked: false }, { id: '77414', avatar: '/static/logo.png', nickname: '199****0268', isMember: '否', level: '无', group: '无', spreadLevel: '', phone: '199****0268', userType: '公众号', balance: '88888.00', checked: false },
{ id: '75311', avatar: 'https://img.crmeb.com/crmeb_demo/75311.png', nickname: 'wljbhg', isMember: '否', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100002.00', checked: false }, { id: '75311', avatar: '/static/logo.png', nickname: 'wljbhg', isMember: '否', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100002.00', checked: false },
{ id: '75305', avatar: 'https://img.crmeb.com/crmeb_demo/75305.png', nickname: '相见欢', isMember: '否', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false }, { id: '75305', avatar: '/static/logo.png', nickname: '相见欢', isMember: '否', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false },
{ id: '75296', avatar: 'https://img.crmeb.com/crmeb_demo/75296.png', nickname: '..', isMember: '否', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false }, { id: '75296', avatar: '/static/logo.png', nickname: '..', isMember: '否', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false },
{ id: '75293', avatar: 'https://img.crmeb.com/crmeb_demo/75293.png', nickname: '钟(钏)华', isMember: '否', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false }, { id: '75293', avatar: '/static/logo.png', nickname: '钟(钏)华', isMember: '否', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false },
{ id: '75289', avatar: 'https://img.crmeb.com/crmeb_demo/75289.png', nickname: '小二上酒', isMember: '否', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false }, { id: '75289', avatar: '/static/logo.png', nickname: '小二上酒', isMember: '否', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false },
{ id: '75257', avatar: 'https://img.crmeb.com/crmeb_demo/75257.png', nickname: '5+7', isMember: '是', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false }, { id: '75257', avatar: '/static/logo.png', nickname: '5+7', isMember: '是', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false },
{ id: '75226', avatar: 'https://img.crmeb.com/crmeb_demo/75226.png', nickname: '慢步前行', isMember: '是', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false }, { id: '75226', avatar: '/static/logo.png', nickname: '慢步前行', isMember: '是', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false },
{ id: '75211', avatar: 'https://img.crmeb.com/crmeb_demo/75211.png', nickname: '难得糊涂', isMember: '否', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false } { id: '75211', avatar: '/static/logo.png', nickname: '难得糊涂', isMember: '否', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false }
]) ])
function onSearch() { function onSearch() {
@@ -183,18 +185,8 @@ function onDetail(user: any) {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.user-list-page {
padding: 16px;
background-color: #f0f2f5;
min-height: 100vh;
}
/* 筛选卡片 */ /* 筛选卡片 */
.filter-card { .filter-card {
background: #fff;
border-radius: 4px;
padding: 24px;
margin-bottom: 16px;
} }
.filter-row { .filter-row {
@@ -307,8 +299,6 @@ function onDetail(user: any) {
/* 内容卡片 */ /* 内容卡片 */
.content-card { .content-card {
background: #fff;
border-radius: 4px;
padding: 0; padding: 0;
overflow: visible; /* 必须 visible 以显示下拉菜单 */ overflow: visible; /* 必须 visible 以显示下拉菜单 */
} }

View File

@@ -1,25 +0,0 @@
<template>
<AdminLayout :currentPage="currentPage">
<view class="page">
<view class="header">
<text class="title">{{ title }}</text>
<text class="sub-title">系统用户相关全局配置</text>
</view>
</view>
</AdminLayout>
</template>
<script setup lang="uts">
import { ref } from 'vue'
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
const currentPage = ref<string>('user-config')
const title = ref<string>('用户配置')
</script>
<style scoped lang="scss">
@import '@/uni.scss';
.page { padding: $space-lg; }
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
</style>

View File

@@ -1,25 +0,0 @@
<template>
<AdminLayout :currentPage="currentPage">
<view class="page">
<view class="header">
<text class="title">{{ title }}</text>
<text class="sub-title">创建及管理用户分组</text>
</view>
</view>
</AdminLayout>
</template>
<script setup lang="uts">
import { ref } from 'vue'
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
const currentPage = ref<string>('user-group')
const title = ref<string>('用户分组')
</script>
<style scoped lang="scss">
@import '@/uni.scss';
.page { padding: $space-lg; }
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
</style>

View File

@@ -1,25 +0,0 @@
<template>
<AdminLayout :currentPage="currentPage">
<view class="page">
<view class="header">
<text class="title">{{ title }}</text>
<text class="sub-title">自定义用户行为标签</text>
</view>
</view>
</AdminLayout>
</template>
<script setup lang="uts">
import { ref } from 'vue'
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
const currentPage = ref<string>('user-tag')
const title = ref<string>('用户标签')
</script>
<style scoped lang="scss">
@import '@/uni.scss';
.page { padding: $space-lg; }
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
</style>

View File

@@ -1,25 +0,0 @@
<template>
<AdminLayout :currentPage="currentPage">
<view class="page">
<view class="header">
<text class="title">{{ title }}</text>
<text class="sub-title">配置会员等级及权益</text>
</view>
</view>
</AdminLayout>
</template>
<script setup lang="uts">
import { ref } from 'vue'
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
const currentPage = ref<string>('user-level')
const title = ref<string>('用户等级')
</script>
<style scoped lang="scss">
@import '@/uni.scss';
.page { padding: $space-lg; }
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
</style>

View File

@@ -1,25 +0,0 @@
<template>
<AdminLayout :currentPage="currentPage">
<view class="page">
<view class="header">
<text class="title">{{ title }}</text>
<text class="sub-title">概览用户增长及活跃度数据</text>
</view>
</view>
</AdminLayout>
</template>
<script setup lang="uts">
import { ref } from 'vue'
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
const currentPage = ref<string>('user-statistics')
const title = ref<string>('用户统计')
</script>
<style scoped lang="scss">
@import '@/uni.scss';
.page { padding: $space-lg; }
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
</style>

View File

@@ -31,12 +31,184 @@ const resizeObservers = new Map();
let chinaMapLoaded = false; let chinaMapLoaded = false;
let chinaMapLoading = false; let chinaMapLoading = false;
// 内置的简化中国地图数据50个主要城市或地区的边界
const SIMPLIFIED_CHINA_MAP = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": { "name": "南海诸岛", "cp": [113.5439, 3.5951], "childNum": 0 },
"geometry": { "type": "MultiPolygon", "coordinates": [[[[113.5439, 3.5951], [113.5439, 3.5951]]]] }
},
{
"type": "Feature",
"properties": { "name": "北京", "cp": [116.4074, 39.9042], "childNum": 16 },
"geometry": { "type": "Polygon", "coordinates": [[[116, 39.5], [117, 39.5], [117, 40.5], [116, 40.5], [116, 39.5]]] }
},
{
"type": "Feature",
"properties": { "name": "天津", "cp": [117.2, 39.0842], "childNum": 16 },
"geometry": { "type": "Polygon", "coordinates": [[[116.7, 38.7], [117.7, 38.7], [117.7, 39.5], [116.7, 39.5], [116.7, 38.7]]] }
},
{
"type": "Feature",
"properties": { "name": "河北", "cp": [114.5149, 38.0428], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[113.5, 37.5], [119.5, 37.5], [119.5, 42.5], [113.5, 42.5], [113.5, 37.5]]] }
},
{
"type": "Feature",
"properties": { "name": "山西", "cp": [112.5489, 37.8739], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[110.5, 35], [114.5, 35], [114.5, 40.5], [110.5, 40.5], [110.5, 35]]] }
},
{
"type": "Feature",
"properties": { "name": "内蒙古", "cp": [111.7558, 40.8183], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[97, 37], [126, 37], [126, 54], [97, 54], [97, 37]]] }
},
{
"type": "Feature",
"properties": { "name": "辽宁", "cp": [123.4328, 41.8045], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[118.5, 40], [126.5, 40], [126.5, 45], [118.5, 45], [118.5, 40]]] }
},
{
"type": "Feature",
"properties": { "name": "吉林", "cp": [125.3235, 43.8957], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[121, 41.5], [130, 41.5], [130, 48.5], [121, 48.5], [121, 41.5]]] }
},
{
"type": "Feature",
"properties": { "name": "黑龙江", "cp": [126.6424, 45.7568], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[121, 43.5], [135, 43.5], [135, 55], [121, 55], [121, 43.5]]] }
},
{
"type": "Feature",
"properties": { "name": "上海", "cp": [121.4737, 31.2304], "childNum": 16 },
"geometry": { "type": "Polygon", "coordinates": [[[120.8, 30.7], [122, 30.7], [122, 31.9], [120.8, 31.9], [120.8, 30.7]]] }
},
{
"type": "Feature",
"properties": { "name": "江苏", "cp": [118.7969, 32.9387], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[118, 31], [122, 31], [122, 34.5], [118, 34.5], [118, 31]]] }
},
{
"type": "Feature",
"properties": { "name": "浙江", "cp": [120.1551, 30.2741], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[118.5, 28], [123, 28], [123, 31.5], [118.5, 31.5], [118.5, 28]]] }
},
{
"type": "Feature",
"properties": { "name": "安徽", "cp": [117.2272, 31.8654], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[114.5, 29.5], [119.5, 29.5], [119.5, 34.5], [114.5, 34.5], [114.5, 29.5]]] }
},
{
"type": "Feature",
"properties": { "name": "福建", "cp": [119.295492, 26.0745], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[118.5, 23.5], [121.5, 23.5], [121.5, 28], [118.5, 28], [118.5, 23.5]]] }
},
{
"type": "Feature",
"properties": { "name": "江西", "cp": [115.8581, 28.6832], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[113, 24], [118, 24], [118, 30], [113, 30], [113, 24]]] }
},
{
"type": "Feature",
"properties": { "name": "山东", "cp": [117.1205, 36.6519], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[114, 34.5], [122, 34.5], [122, 38], [114, 38], [114, 34.5]]] }
},
{
"type": "Feature",
"properties": { "name": "河南", "cp": [113.6254, 34.7466], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[111.5, 32], [117, 32], [117, 36.5], [111.5, 36.5], [111.5, 32]]] }
},
{
"type": "Feature",
"properties": { "name": "湖北", "cp": [114.3055, 30.5928], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[108, 28.5], [116, 28.5], [116, 33], [108, 33], [108, 28.5]]] }
},
{
"type": "Feature",
"properties": { "name": "湖南", "cp": [112.9388, 28.2282], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[108.5, 24.5], [114, 24.5], [114, 30], [108.5, 30], [108.5, 24.5]]] }
},
{
"type": "Feature",
"properties": { "name": "广东", "cp": [113.2644, 23.1291], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[112, 20.5], [116, 20.5], [116, 25], [112, 25], [112, 20.5]]] }
},
{
"type": "Feature",
"properties": { "name": "广西", "cp": [108.3661, 22.8170], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[104.5, 20.5], [112, 20.5], [112, 26.5], [104.5, 26.5], [104.5, 20.5]]] }
},
{
"type": "Feature",
"properties": { "name": "四川", "cp": [104.0665, 30.5702], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[100, 26], [108, 26], [108, 35], [100, 35], [100, 26]]] }
},
{
"type": "Feature",
"properties": { "name": "贵州", "cp": [106.7135, 26.5783], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[103.5, 24.5], [109.5, 24.5], [109.5, 29.5], [103.5, 29.5], [103.5, 24.5]]] }
},
{
"type": "Feature",
"properties": { "name": "云南", "cp": [102.7103, 24.8801], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[97.5, 21], [106.5, 21], [106.5, 30], [97.5, 30], [97.5, 21]]] }
},
{
"type": "Feature",
"properties": { "name": "陕西", "cp": [108.9402, 34.3416], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[105.5, 31.5], [111.5, 31.5], [111.5, 39], [105.5, 39], [105.5, 31.5]]] }
},
{
"type": "Feature",
"properties": { "name": "甘肃", "cp": [103.8343, 35.0080], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[92, 32], [108, 32], [108, 42], [92, 42], [92, 32]]] }
},
{
"type": "Feature",
"properties": { "name": "青海", "cp": [101.7782, 36.6171], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[89, 31.5], [104.5, 31.5], [104.5, 39.5], [89, 39.5], [89, 31.5]]] }
},
{
"type": "Feature",
"properties": { "name": "宁夏", "cp": [106.2586, 38.4680], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[104.5, 35.5], [107.5, 35.5], [107.5, 40], [104.5, 40], [104.5, 35.5]]] }
},
{
"type": "Feature",
"properties": { "name": "新疆", "cp": [87.6278, 43.7929], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[73, 26], [104, 26], [104, 49], [73, 49], [73, 26]]] }
},
{
"type": "Feature",
"properties": { "name": "西藏", "cp": [88.0959, 29.6470], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[78, 26], [99, 26], [99, 36.5], [78, 36.5], [78, 26]]] }
},
{
"type": "Feature",
"properties": { "name": "台湾", "cp": [120.9605, 23.6978], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[120.1, 22.5], [121.5, 22.5], [121.5, 25], [120.1, 25], [120.1, 22.5]]] }
},
{
"type": "Feature",
"properties": { "name": "香港", "cp": [114.1694, 22.3193], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[113.8, 22], [114.5, 22], [114.5, 22.6], [113.8, 22.6], [113.8, 22]]] }
},
{
"type": "Feature",
"properties": { "name": "澳门", "cp": [113.5439, 22.2987], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[113.3, 22.1], [113.7, 22.1], [113.7, 22.5], [113.3, 22.5], [113.3, 22.1]]] }
}
]
};
// 加载并注册中国地图 // 加载并注册中国地图
async function loadChinaMap() { async function loadChinaMap() {
if (chinaMapLoaded) { if (chinaMapLoaded) {
return Promise.resolve(); return Promise.resolve();
} }
if (chinaMapLoading) { if (chinaMapLoading) {
// 如果正在加载,等待加载完成 // 如果正在加载,等待加载完成
return new Promise((resolve) => { return new Promise((resolve) => {
@@ -53,32 +225,67 @@ async function loadChinaMap() {
}, 10000); }, 10000);
}); });
} }
chinaMapLoading = true; chinaMapLoading = true;
try { try {
// 从在线 CDN 加载中国地图 GeoJSON 数据 // 首先尝试从在线 CDN 加载中国地图 GeoJSON 数据
// 使用 ECharts 官方示例数据源 let geoJson = null;
const response = await fetch('https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json'); let loaded = false;
if (!response.ok) {
// 如果第一个源失败,尝试备用源 try {
const backupResponse = await fetch('https://echarts.apache.org/examples/data/map/china.json'); const response = await fetch('https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json', {
if (!backupResponse.ok) { timeout: 5000
throw new Error('Failed to load China map data'); });
if (response.ok) {
geoJson = await response.json();
loaded = true;
console.log('[EChartsView] 从阿里云 CDN 加载中国地图数据成功');
} }
const geoJson = await backupResponse.json(); } catch (e) {
echarts.registerMap('china', geoJson); console.warn('[EChartsView] 从阿里云 CDN 加载失败,尝试备用源...', e);
} else { }
const geoJson = await response.json();
echarts.registerMap('china', geoJson); // 如果第一个源失败,尝试备用源
if (!loaded) {
try {
const backupResponse = await fetch('https://echarts.apache.org/examples/data/map/china.json', {
timeout: 5000
});
if (backupResponse.ok) {
geoJson = await backupResponse.json();
loaded = true;
console.log('[EChartsView] 从 ECharts 官方源加载中国地图数据成功');
}
} catch (e) {
console.warn('[EChartsView] 从备用源加载也失败,使用内置简化地图', e);
}
}
// 如果网络加载都失败,使用内置的简化地图
if (!loaded) {
geoJson = SIMPLIFIED_CHINA_MAP;
console.log('[EChartsView] 使用内置简化中国地图数据');
}
// 注册地图
if (geoJson) {
echarts.registerMap('china', geoJson);
chinaMapLoaded = true;
console.log('[EChartsView] 中国地图数据已注册');
} else {
throw new Error('Failed to load or create China map data');
} }
chinaMapLoaded = true;
console.log('[EChartsView] 中国地图数据已加载并注册');
} catch (error) { } catch (error) {
console.error('[EChartsView] 加载中国地图数据失败:', error); console.error('[EChartsView] 加载中国地图数据失败:', error);
// 即使加载失败,也标记为已尝试,避免重复请求 // 降级方案:使用简化地图
chinaMapLoaded = false; try {
echarts.registerMap('china', SIMPLIFIED_CHINA_MAP);
chinaMapLoaded = true;
console.log('[EChartsView] 已使用内置简化地图作为降级方案');
} catch (furtherError) {
console.error('[EChartsView] 即使使用简化地图也失败:', furtherError);
}
} finally { } finally {
chinaMapLoading = false; chinaMapLoading = false;
} }
@@ -94,15 +301,15 @@ function getChartKey(el) {
function ensureChart(el, retryCount = 0) { function ensureChart(el, retryCount = 0) {
if (!el) return null; if (!el) return null;
const key = getChartKey(el); const key = getChartKey(el);
let chart = charts.get(key); let chart = charts.get(key);
// 如果图表已存在且有效,直接返回 // 如果图表已存在且有效,直接返回
if (chart && !chart.isDisposed()) { if (chart && !chart.isDisposed()) {
return chart; return chart;
} }
// 如果图表已销毁,从 Map 中移除 // 如果图表已销毁,从 Map 中移除
if (chart && chart.isDisposed()) { if (chart && chart.isDisposed()) {
charts.delete(key); charts.delete(key);
@@ -113,19 +320,19 @@ function ensureChart(el, retryCount = 0) {
} }
chart = null; chart = null;
} }
// 确保元素有尺寸 // 确保元素有尺寸
const rect = el.getBoundingClientRect(); const rect = el.getBoundingClientRect();
const computedStyle = window.getComputedStyle(el); const computedStyle = window.getComputedStyle(el);
const width = parseFloat(computedStyle.width) || rect.width; const width = parseFloat(computedStyle.width) || rect.width;
const height = parseFloat(computedStyle.height) || rect.height; const height = parseFloat(computedStyle.height) || rect.height;
// 如果尺寸为 0尝试延迟初始化最多重试 10 次) // 如果尺寸为 0尝试延迟初始化最多重试 10 次)
if ((width === 0 || height === 0) && retryCount < 10) { if ((width === 0 || height === 0) && retryCount < 10) {
if (retryCount === 0) { if (retryCount === 0) {
console.warn('[EChartsView] 容器尺寸为 0延迟初始化', { width, height, rect }); console.warn('[EChartsView] 容器尺寸为 0延迟初始化', { width, height, rect });
} }
// 使用指数退避策略,避免无限循环 // 使用指数退避策略,避免无限循环
const delay = Math.min(100 * Math.pow(1.5, retryCount), 1000); const delay = Math.min(100 * Math.pow(1.5, retryCount), 1000);
setTimeout(() => { setTimeout(() => {
@@ -133,7 +340,7 @@ function ensureChart(el, retryCount = 0) {
}, delay); }, delay);
return null; return null;
} }
// 如果重试次数过多,使用默认尺寸 // 如果重试次数过多,使用默认尺寸
if (width === 0 || height === 0) { if (width === 0 || height === 0) {
console.warn('[EChartsView] 容器尺寸仍为 0使用默认尺寸', { width, height }); console.warn('[EChartsView] 容器尺寸仍为 0使用默认尺寸', { width, height });
@@ -141,7 +348,7 @@ function ensureChart(el, retryCount = 0) {
const parentRect = el.parentElement ? el.parentElement.getBoundingClientRect() : { width: 800, height: 400 }; const parentRect = el.parentElement ? el.parentElement.getBoundingClientRect() : { width: 800, height: 400 };
const finalWidth = width || parentRect.width || 800; const finalWidth = width || parentRect.width || 800;
const finalHeight = height || parentRect.height || 400; const finalHeight = height || parentRect.height || 400;
if (finalWidth > 0 && finalHeight > 0) { if (finalWidth > 0 && finalHeight > 0) {
// 设置元素尺寸 // 设置元素尺寸
el.style.width = finalWidth + 'px'; el.style.width = finalWidth + 'px';
@@ -151,19 +358,19 @@ function ensureChart(el, retryCount = 0) {
return null; return null;
} }
} }
try { try {
// 注意:地图数据加载在 setOption 中处理,这里不处理 // 注意:地图数据加载在 setOption 中处理,这里不处理
// 因为 ensureChart 是同步函数,不能使用 await // 因为 ensureChart 是同步函数,不能使用 await
chart = echarts.init(el, null, { chart = echarts.init(el, null, {
renderer: "canvas", renderer: "canvas",
width: rect.width, width: rect.width,
height: rect.height height: rect.height
}); });
charts.set(key, chart); charts.set(key, chart);
// 自适应:监听容器尺寸变化 // 自适应:监听容器尺寸变化
if (typeof ResizeObserver !== "undefined") { if (typeof ResizeObserver !== "undefined") {
const ro = new ResizeObserver((entries) => { const ro = new ResizeObserver((entries) => {
@@ -200,7 +407,7 @@ function ensureChart(el, retryCount = 0) {
// 存储 handler 以便后续清理 // 存储 handler 以便后续清理
el._resizeHandler = resizeHandler; el._resizeHandler = resizeHandler;
} }
return chart; return chart;
} catch (e) { } catch (e) {
console.error('[EChartsView] 初始化失败', e); console.error('[EChartsView] 初始化失败', e);
@@ -212,7 +419,7 @@ function disposeChart(el) {
if (!el) return; if (!el) return;
const key = getChartKey(el); const key = getChartKey(el);
const chart = charts.get(key); const chart = charts.get(key);
if (chart && !chart.isDisposed()) { if (chart && !chart.isDisposed()) {
try { try {
chart.dispose(); chart.dispose();
@@ -220,15 +427,15 @@ function disposeChart(el) {
console.warn('[EChartsView] dispose 失败', e); console.warn('[EChartsView] dispose 失败', e);
} }
} }
charts.delete(key); charts.delete(key);
const ro = resizeObservers.get(key); const ro = resizeObservers.get(key);
if (ro) { if (ro) {
ro.disconnect(); ro.disconnect();
resizeObservers.delete(key); resizeObservers.delete(key);
} }
if (el._resizeHandler) { if (el._resizeHandler) {
window.removeEventListener("resize", el._resizeHandler); window.removeEventListener("resize", el._resizeHandler);
delete el._resizeHandler; delete el._resizeHandler;
@@ -260,22 +467,22 @@ export default {
console.error('[EChartsView] setOption: 找不到容器元素'); console.error('[EChartsView] setOption: 找不到容器元素');
return; return;
} }
// 检查 option 是否有效 // 检查 option 是否有效
if (!option || typeof option !== 'object') { if (!option || typeof option !== 'object') {
console.warn('[EChartsView] setOption: option 无效', option); console.warn('[EChartsView] setOption: option 无效', option);
return; return;
} }
// 检查是否使用了地图,如果是,先加载地图数据 // 检查是否使用了地图,如果是,先加载地图数据
const needsMap = option.geo || (option.series && Array.isArray(option.series) && option.series.some(s => s.type === 'map' && s.map === 'china')); const needsMap = option.geo || (option.series && Array.isArray(option.series) && option.series.some(s => s.type === 'map' && s.map === 'china'));
if (needsMap) { if (needsMap) {
await loadChinaMap(); await loadChinaMap();
} }
// 保存 option 供 ensureChart 使用 // 保存 option 供 ensureChart 使用
el._pendingOption = option; el._pendingOption = option;
// 确保图表已初始化 // 确保图表已初始化
let c = ensureChart(el); let c = ensureChart(el);
if (!c) { if (!c) {
@@ -369,7 +576,7 @@ export default {
} }
return; return;
} }
// 检查图表是否已销毁 // 检查图表是否已销毁
if (c.isDisposed()) { if (c.isDisposed()) {
console.warn('[EChartsView] setOption: 图表已销毁,重新初始化'); console.warn('[EChartsView] setOption: 图表已销毁,重新初始化');
@@ -380,7 +587,7 @@ export default {
c = ensureChart(el); c = ensureChart(el);
if (!c) return; if (!c) return;
} }
try { try {
// 如果使用地图,确保地图已加载 // 如果使用地图,确保地图已加载
if (needsMap) { if (needsMap) {
@@ -389,7 +596,7 @@ export default {
// 深拷贝 option 确保是纯 JS 对象 // 深拷贝 option 确保是纯 JS 对象
const plainOption = JSON.parse(JSON.stringify(option)); const plainOption = JSON.parse(JSON.stringify(option));
c.setOption(plainOption, true); c.setOption(plainOption, true);
// 使用 requestAnimationFrame 避免 resize 警告 // 使用 requestAnimationFrame 避免 resize 警告
requestAnimationFrame(() => { requestAnimationFrame(() => {
const key = getChartKey(el); const key = getChartKey(el);
@@ -415,11 +622,20 @@ export default {
<style> <style>
.ec-wrap { .ec-wrap {
position: relative !important;
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: hidden; /* 防止 canvas 越界 */
} }
.ec-canvas { .ec-canvas {
width: 100%; position: absolute !important;
height: 100%; left: 0;
right: 0;
top: 0;
bottom: 0;
width: 100% !important;
height: 100% !important;
display: block;
} }
</style> </style>

12
vite.config.js Normal file
View File

@@ -0,0 +1,12 @@
import { defineConfig } from 'vite'
import { fileURLToPath, URL } from 'node:url'
import uni from '@dcloudio/vite-plugin-uni'
export default defineConfig({
plugins: [uni()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./', import.meta.url))
}
}
})