数据分析页面骨架
This commit is contained in:
@@ -54,39 +54,87 @@ export default {
|
|||||||
})
|
})
|
||||||
|
|
||||||
this.chartOption = {
|
this.chartOption = {
|
||||||
grid: { left: 44, right: 44, top: 24, bottom: 36 },
|
grid: { left: 60, right: 60, top: 70, bottom: 40 },
|
||||||
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
tooltip: {
|
||||||
legend: { top: 0, left: 0, itemWidth: 10, itemHeight: 10, textStyle: { fontSize: 12 } },
|
trigger: 'axis',
|
||||||
|
axisPointer: { type: 'shadow' },
|
||||||
|
formatter: (params: any) => {
|
||||||
|
let result = params[0].name + '<br/>'
|
||||||
|
for (let i = 0; i < params.length; i++) {
|
||||||
|
const p = params[i]
|
||||||
|
if (p.seriesName === 'GMV') {
|
||||||
|
const val = Number(p.value)
|
||||||
|
const formatted = val >= 10000 ? (val / 10000).toFixed(1) + '万' : val.toFixed(0)
|
||||||
|
result += `${p.marker} ${p.seriesName}: ¥${formatted}<br/>`
|
||||||
|
} else {
|
||||||
|
result += `${p.marker} ${p.seriesName}: ${p.value}<br/>`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
top: 8,
|
||||||
|
left: 8,
|
||||||
|
itemWidth: 10,
|
||||||
|
itemHeight: 10,
|
||||||
|
textStyle: { fontSize: 12 },
|
||||||
|
data: ['GMV', '订单数'],
|
||||||
|
bottom: 'auto'
|
||||||
|
},
|
||||||
xAxis: {
|
xAxis: {
|
||||||
type: 'category',
|
type: 'category',
|
||||||
data: x,
|
data: x,
|
||||||
axisTick: { show: false },
|
axisTick: { alignWithLabel: true },
|
||||||
axisLine: { lineStyle: { color: 'rgba(0,0,0,0.12)' } },
|
axisLine: { lineStyle: { color: 'rgba(0,0,0,0.12)' } },
|
||||||
axisLabel: { color: 'rgba(0,0,0,0.55)' }
|
axisLabel: {
|
||||||
|
color: 'rgba(0,0,0,0.55)',
|
||||||
|
rotate: x.length > 12 ? 45 : 0,
|
||||||
|
interval: 0
|
||||||
|
}
|
||||||
},
|
},
|
||||||
yAxis: [
|
yAxis: [
|
||||||
{
|
{
|
||||||
type: 'value',
|
type: 'value',
|
||||||
name: 'GMV',
|
name: 'GMV(元)',
|
||||||
|
position: 'left',
|
||||||
axisLine: { show: false },
|
axisLine: { show: false },
|
||||||
splitLine: { lineStyle: { color: 'rgba(0,0,0,0.06)' } },
|
splitLine: { lineStyle: { color: 'rgba(0,0,0,0.06)' } },
|
||||||
axisLabel: { color: 'rgba(0,0,0,0.55)' }
|
axisLabel: {
|
||||||
|
color: 'rgba(0,0,0,0.55)',
|
||||||
|
formatter: (value: number) => {
|
||||||
|
if (value >= 10000) {
|
||||||
|
return (value / 10000).toFixed(1) + '万'
|
||||||
|
}
|
||||||
|
return String(Math.round(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'value',
|
type: 'value',
|
||||||
name: '订单',
|
name: '订单数',
|
||||||
|
position: 'right',
|
||||||
|
alignTicks: true,
|
||||||
axisLine: { show: false },
|
axisLine: { show: false },
|
||||||
splitLine: { show: false },
|
splitLine: { show: false },
|
||||||
axisLabel: { color: 'rgba(0,0,0,0.55)' }
|
axisLabel: {
|
||||||
|
color: 'rgba(0,0,0,0.55)',
|
||||||
|
formatter: (value: number) => String(Math.round(value))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
name: 'GMV',
|
name: 'GMV',
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
|
yAxisIndex: 0,
|
||||||
data: bar,
|
data: bar,
|
||||||
barWidth: 14,
|
barMaxWidth: 14,
|
||||||
itemStyle: { borderRadius: [6, 6, 0, 0] }
|
barCategoryGap: '35%',
|
||||||
|
itemStyle: {
|
||||||
|
borderRadius: [6, 6, 0, 0],
|
||||||
|
color: '#3b82f6'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '订单数',
|
name: '订单数',
|
||||||
@@ -96,7 +144,13 @@ export default {
|
|||||||
smooth: true,
|
smooth: true,
|
||||||
symbol: 'circle',
|
symbol: 'circle',
|
||||||
symbolSize: 6,
|
symbolSize: 6,
|
||||||
lineStyle: { width: 2 }
|
lineStyle: {
|
||||||
|
width: 2,
|
||||||
|
color: '#10b981'
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
color: '#10b981'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
275
components/analytics/AnalyticsSidebarMenu.uvue
Normal file
275
components/analytics/AnalyticsSidebarMenu.uvue
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
<!-- 数据分析侧边栏菜单组件 -->
|
||||||
|
<template>
|
||||||
|
<view>
|
||||||
|
<!-- 侧边栏菜单 -->
|
||||||
|
<view class="sidebar-menu" :class="{ active: showMenu, 'always-visible': isWideScreen }" @click.stop>
|
||||||
|
<view class="sidebar-content">
|
||||||
|
<view
|
||||||
|
v-for="item in menuItems"
|
||||||
|
:key="item.path"
|
||||||
|
class="menu-item"
|
||||||
|
:class="{ active: currentPath === item.path }"
|
||||||
|
@click="navigateToPage(item.path)"
|
||||||
|
>
|
||||||
|
<text class="menu-icon">{{ item.icon }}</text>
|
||||||
|
<text class="menu-text">{{ item.title }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<!-- 遮罩层(仅窄屏时显示) -->
|
||||||
|
<view class="sidebar-overlay" v-if="showMenu && !isWideScreen" @click="closeMenu"></view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts">
|
||||||
|
// 菜单项类型
|
||||||
|
type MenuItem = {
|
||||||
|
path: string
|
||||||
|
title: string
|
||||||
|
icon: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 菜单配置
|
||||||
|
const MENU_ITEMS: Array<MenuItem> = [
|
||||||
|
{ path: '/pages/mall/analytics/index', title: '数据分析中心', icon: '📊' },
|
||||||
|
{ path: '/pages/mall/analytics/profile', title: '个人中心', icon: '👤' },
|
||||||
|
{ path: '/pages/mall/analytics/sales-report', title: '销售报表', icon: '💰' },
|
||||||
|
{ path: '/pages/mall/analytics/user-analysis', title: '用户分析', icon: '👥' },
|
||||||
|
{ path: '/pages/mall/analytics/product-insights', title: '商品洞察', icon: '📦' },
|
||||||
|
{ path: '/pages/mall/analytics/delivery-analysis', title: '配送效率分析', icon: '🚚' },
|
||||||
|
{ path: '/pages/mall/analytics/coupon-analysis', title: '优惠券效果分析', icon: '🎫' },
|
||||||
|
{ path: '/pages/mall/analytics/market-trends', title: '市场趋势', icon: '📈' },
|
||||||
|
{ path: '/pages/mall/analytics/custom-report', title: '自定义报表', icon: '📋' },
|
||||||
|
{ path: '/pages/mall/analytics/report-detail', title: '报表详情', icon: '📄' },
|
||||||
|
{ path: '/pages/mall/analytics/data-detail', title: '数据分析详情', icon: '🔍' },
|
||||||
|
{ path: '/pages/mall/analytics/insight-detail', title: '数据洞察详情', icon: '💡' }
|
||||||
|
]
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
// 是否显示菜单
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
// 当前页面路径
|
||||||
|
currentPath: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['visible-change'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showMenu: false,
|
||||||
|
menuItems: MENU_ITEMS,
|
||||||
|
isWideScreen: false,
|
||||||
|
screenWidth: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
visible(newVal: boolean) {
|
||||||
|
// 宽屏时自动显示,窄屏时根据 visible 控制
|
||||||
|
if (this.isWideScreen) {
|
||||||
|
this.showMenu = true
|
||||||
|
} else {
|
||||||
|
this.showMenu = newVal
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showMenu(newVal: boolean) {
|
||||||
|
// 同步到父组件(仅窄屏时)
|
||||||
|
if (!this.isWideScreen) {
|
||||||
|
this.$emit('visible-change', newVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad() {
|
||||||
|
this.checkScreenSize()
|
||||||
|
},
|
||||||
|
onShow() {
|
||||||
|
// 每次显示时检查屏幕尺寸
|
||||||
|
this.checkScreenSize()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
checkScreenSize() {
|
||||||
|
// 获取屏幕宽度
|
||||||
|
const systemInfo = uni.getSystemInfoSync()
|
||||||
|
this.screenWidth = systemInfo.windowWidth || systemInfo.screenWidth
|
||||||
|
// 宽屏阈值:960px(与页面响应式断点一致)
|
||||||
|
this.isWideScreen = this.screenWidth >= 960
|
||||||
|
|
||||||
|
// 宽屏时自动显示菜单
|
||||||
|
if (this.isWideScreen) {
|
||||||
|
this.showMenu = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
closeMenu() {
|
||||||
|
// 宽屏时不允许关闭
|
||||||
|
if (this.isWideScreen) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.showMenu = false
|
||||||
|
this.$emit('visible-change', false)
|
||||||
|
},
|
||||||
|
|
||||||
|
navigateToPage(path: string) {
|
||||||
|
if (this.currentPath === path) {
|
||||||
|
// 窄屏时关闭菜单
|
||||||
|
if (!this.isWideScreen) {
|
||||||
|
this.closeMenu()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.navigateTo({
|
||||||
|
url: path,
|
||||||
|
fail: () => {
|
||||||
|
uni.showToast({ title: '页面跳转失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// 窄屏时关闭菜单
|
||||||
|
if (!this.isWideScreen) {
|
||||||
|
this.closeMenu()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 侧边栏菜单 */
|
||||||
|
.sidebar-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 998;
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-menu {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 280px;
|
||||||
|
height: 100vh;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
|
||||||
|
z-index: 999;
|
||||||
|
transform: translateX(-100%);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 窄屏:抽屉效果 */
|
||||||
|
.sidebar-menu.active {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 宽屏:固定显示在左侧 */
|
||||||
|
.sidebar-menu.always-visible {
|
||||||
|
position: relative;
|
||||||
|
transform: translateX(0);
|
||||||
|
box-shadow: none;
|
||||||
|
border-right: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-close {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-close:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-close .icon {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item.active {
|
||||||
|
background: rgba(59, 130, 246, 0.1);
|
||||||
|
border-left: 3px solid #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item.active .menu-text {
|
||||||
|
color: #3b82f6;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
width: 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-text {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #333;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式:宽屏时固定显示 */
|
||||||
|
@media screen and (min-width: 960px) {
|
||||||
|
.sidebar-menu {
|
||||||
|
position: relative;
|
||||||
|
transform: translateX(0);
|
||||||
|
box-shadow: none;
|
||||||
|
border-right: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-overlay {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
332
components/analytics/AnalyticsTopBar.uvue
Normal file
332
components/analytics/AnalyticsTopBar.uvue
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
<!-- 数据分析顶部导航栏组件 -->
|
||||||
|
<template>
|
||||||
|
<view class="analytics-topbar">
|
||||||
|
<view class="topbar-left">
|
||||||
|
<!-- 仅窄屏且侧边栏未打开时显示菜单按钮 -->
|
||||||
|
<view class="menu-icon" v-if="showMenuIcon" @click="handleMenu">
|
||||||
|
<text class="icon">☰</text>
|
||||||
|
</view>
|
||||||
|
<view class="title-group">
|
||||||
|
<text class="title">{{ title }}</text>
|
||||||
|
<text class="subtitle">最后更新:{{ lastUpdateTime }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="topbar-right">
|
||||||
|
<!-- 宽屏时显示的按钮 -->
|
||||||
|
<view class="icon-btn-icon btn-visible" @click="handleRefresh">
|
||||||
|
<text class="icon">🔄</text>
|
||||||
|
</view>
|
||||||
|
<view class="icon-btn-icon btn-visible" @click="handleSearch">
|
||||||
|
<text class="icon">🔍</text>
|
||||||
|
</view>
|
||||||
|
<view class="icon-btn-icon notification btn-hidden" @click="handleNotification">
|
||||||
|
<text class="icon">🔔</text>
|
||||||
|
<view class="badge"></view>
|
||||||
|
</view>
|
||||||
|
<view class="icon-btn-icon btn-hidden" @click="handleFullscreen">
|
||||||
|
<text class="icon">⛶</text>
|
||||||
|
</view>
|
||||||
|
<view class="icon-btn-icon btn-hidden" @click="handleMobile">
|
||||||
|
<text class="icon">📱</text>
|
||||||
|
</view>
|
||||||
|
<view class="dropdown btn-visible" @click="handleDropdown">
|
||||||
|
<text class="dropdown-text">crmeb demo</text>
|
||||||
|
<text class="dropdown-arrow">▼</text>
|
||||||
|
</view>
|
||||||
|
<view class="icon-btn-icon btn-hidden" @click="handleSettings">
|
||||||
|
<text class="icon">⚙️</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 更多按钮(窄屏时显示) -->
|
||||||
|
<view class="more-btn" :class="{ active: showMoreMenu }" @click.stop="toggleMoreMenu">
|
||||||
|
<text class="icon">⋯</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 更多菜单下拉 -->
|
||||||
|
<view class="more-menu" v-if="showMoreMenu" @click.stop>
|
||||||
|
<view class="more-menu-item" @click="handleNotification">
|
||||||
|
<text class="icon">🔔</text>
|
||||||
|
<text>通知</text>
|
||||||
|
</view>
|
||||||
|
<view class="more-menu-item" @click="handleFullscreen">
|
||||||
|
<text class="icon">⛶</text>
|
||||||
|
<text>全屏</text>
|
||||||
|
</view>
|
||||||
|
<view class="more-menu-item" @click="handleMobile">
|
||||||
|
<text class="icon">📱</text>
|
||||||
|
<text>移动端</text>
|
||||||
|
</view>
|
||||||
|
<view class="more-menu-item" @click="handleSettings">
|
||||||
|
<text class="icon">⚙️</text>
|
||||||
|
<text>设置</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts">
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '数据分析中心'
|
||||||
|
},
|
||||||
|
lastUpdateTime: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
// 由页面传入:当前侧边栏是否处于“打开/显示”状态(窄屏下用于隐藏菜单按钮)
|
||||||
|
sidebarVisible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showMoreMenu: false,
|
||||||
|
isWideScreen: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
showMenuIcon(): boolean {
|
||||||
|
// 宽屏不显示;窄屏仅在侧边栏未打开时显示
|
||||||
|
return !this.isWideScreen && !this.sidebarVisible
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad() {
|
||||||
|
this.checkScreenSize()
|
||||||
|
},
|
||||||
|
onShow() {
|
||||||
|
this.checkScreenSize()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
checkScreenSize() {
|
||||||
|
const systemInfo = uni.getSystemInfoSync()
|
||||||
|
const w = systemInfo.windowWidth || systemInfo.screenWidth
|
||||||
|
// 与侧边栏一致:960px 以上视为宽屏
|
||||||
|
this.isWideScreen = w >= 960
|
||||||
|
},
|
||||||
|
handleMenu() {
|
||||||
|
this.$emit('menu-click')
|
||||||
|
},
|
||||||
|
handleRefresh() {
|
||||||
|
this.$emit('refresh')
|
||||||
|
},
|
||||||
|
handleSearch() {
|
||||||
|
this.$emit('search')
|
||||||
|
},
|
||||||
|
handleNotification() {
|
||||||
|
this.showMoreMenu = false
|
||||||
|
this.$emit('notification')
|
||||||
|
},
|
||||||
|
handleFullscreen() {
|
||||||
|
this.showMoreMenu = false
|
||||||
|
this.$emit('fullscreen')
|
||||||
|
},
|
||||||
|
handleMobile() {
|
||||||
|
this.showMoreMenu = false
|
||||||
|
this.$emit('mobile')
|
||||||
|
},
|
||||||
|
handleDropdown() {
|
||||||
|
this.$emit('dropdown')
|
||||||
|
},
|
||||||
|
handleSettings() {
|
||||||
|
this.showMoreMenu = false
|
||||||
|
this.$emit('settings')
|
||||||
|
},
|
||||||
|
toggleMoreMenu() {
|
||||||
|
this.showMoreMenu = !this.showMoreMenu
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.analytics-topbar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 64px;
|
||||||
|
background: #ffffff;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
padding: 0 16px;
|
||||||
|
z-index: 1000;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-left {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
margin-right: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon .icon {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: baseline;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111;
|
||||||
|
line-height: 1.4;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0, 0, 0, 0.55);
|
||||||
|
line-height: 1.4;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
position: relative;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn-icon .icon {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn-icon.notification .badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 6px;
|
||||||
|
right: 6px;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background: #ef4444;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-arrow {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
position: relative;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn .icon {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-menu {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
right: 16px;
|
||||||
|
margin-top: 8px;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
padding: 8px 0;
|
||||||
|
min-width: 160px;
|
||||||
|
z-index: 1001;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-menu-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-menu-item .icon {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-menu-item text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式 */
|
||||||
|
@media screen and (max-width: 960px) {
|
||||||
|
.btn-hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn {
|
||||||
|
display: flex !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title,
|
||||||
|
.subtitle {
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
74
pages.json
74
pages.json
@@ -158,6 +158,80 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"root": "pages/mall/analytics",
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"path": "profile",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "数据分析个人中心"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "sales-report",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "销售报表"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "user-analysis",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "用户分析"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "product-insights",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "商品洞察"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "delivery-analysis",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "配送效率分析"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "coupon-analysis",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "优惠券效果分析"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "market-trends",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "市场趋势"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "custom-report",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "自定义报表"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "report-detail",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "报表详情",
|
||||||
|
"enablePullDownRefresh": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "data-detail",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "数据分析详情",
|
||||||
|
"enablePullDownRefresh": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "insight-detail",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "数据洞察详情",
|
||||||
|
"enablePullDownRefresh": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tabBar": {
|
"tabBar": {
|
||||||
|
|||||||
553
pages/mall/analytics/coupon-analysis.uvue
Normal file
553
pages/mall/analytics/coupon-analysis.uvue
Normal file
@@ -0,0 +1,553 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page" @click="closeMoreMenu">
|
||||||
|
<!-- 固定顶部导航栏 -->
|
||||||
|
<AnalyticsTopBar
|
||||||
|
:title="'优惠券效果分析'"
|
||||||
|
:lastUpdateTime="lastUpdateTime"
|
||||||
|
:sidebarVisible="showSidebarMenu"
|
||||||
|
@menu-click="handleMenu"
|
||||||
|
@refresh="refreshData"
|
||||||
|
@search="handleSearch"
|
||||||
|
@notification="handleNotification"
|
||||||
|
@fullscreen="handleFullscreen"
|
||||||
|
@mobile="handleMobile"
|
||||||
|
@dropdown="handleDropdown"
|
||||||
|
@settings="handleSettings"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<view class="page-layout">
|
||||||
|
<!-- 侧边栏菜单组件 -->
|
||||||
|
<AnalyticsSidebarMenu
|
||||||
|
:visible="showSidebarMenu"
|
||||||
|
:currentPath="currentPath"
|
||||||
|
@visible-change="handleSidebarUpdate"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 主内容区域 -->
|
||||||
|
<view class="main-content">
|
||||||
|
<view class="container">
|
||||||
|
|
||||||
|
<!-- 时间维度筛选 -->
|
||||||
|
<view class="tabs">
|
||||||
|
<view
|
||||||
|
v-for="p in timePeriods"
|
||||||
|
:key="p.value"
|
||||||
|
class="tab"
|
||||||
|
:class="{ active: selectedPeriod === p.value }"
|
||||||
|
@click="selectPeriod(p.value)"
|
||||||
|
>
|
||||||
|
{{ p.label }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- KPI 指标卡片 -->
|
||||||
|
<view class="kpi-grid">
|
||||||
|
<view class="kpi-card">
|
||||||
|
<text class="kpi-label">发放总数</text>
|
||||||
|
<text class="kpi-value">{{ formatInt(couponData.total_issued) }}</text>
|
||||||
|
<text class="kpi-meta">较上期:{{ formatPct(couponData.issued_growth) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="kpi-card">
|
||||||
|
<text class="kpi-label">使用数量</text>
|
||||||
|
<text class="kpi-value">{{ formatInt(couponData.total_used) }}</text>
|
||||||
|
<text class="kpi-meta">使用率:{{ formatPct(couponData.usage_rate) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="kpi-card">
|
||||||
|
<text class="kpi-label">GMV 提升</text>
|
||||||
|
<text class="kpi-value">¥{{ formatMoney(couponData.gmv_increase) }}</text>
|
||||||
|
<text class="kpi-meta">较上期:{{ formatPct(couponData.gmv_growth) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="kpi-card">
|
||||||
|
<text class="kpi-label">ROI</text>
|
||||||
|
<text class="kpi-value">{{ formatPct(couponData.roi) }}</text>
|
||||||
|
<text class="kpi-meta">投入产出比</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 优惠券类型分析 -->
|
||||||
|
<view class="card card-full">
|
||||||
|
<view class="card-head">
|
||||||
|
<text class="card-title">优惠券类型分析</text>
|
||||||
|
<text class="card-desc">8种券类型:满减券、折扣券、免运费券、新人券、会员券、品类券、商家券、限时券</text>
|
||||||
|
</view>
|
||||||
|
<EChartsView class="chart-box" :option="typeChartOption" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 发放渠道效果 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="card-head">
|
||||||
|
<text class="card-title">发放渠道效果</text>
|
||||||
|
<text class="card-desc">主动领取、自动发放、活动赠送、邀请奖励、客服赠送、积分兑换</text>
|
||||||
|
</view>
|
||||||
|
<EChartsView class="chart-box" :option="channelChartOption" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 优惠券使用趋势 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="card-head">
|
||||||
|
<text class="card-title">优惠券使用趋势</text>
|
||||||
|
<text class="card-desc">{{ selectedPeriodText }} · 发放 vs 使用</text>
|
||||||
|
</view>
|
||||||
|
<EChartsView class="chart-box" :option="trendChartOption" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 优惠券转化效果 -->
|
||||||
|
<view class="card card-full">
|
||||||
|
<view class="card-head">
|
||||||
|
<text class="card-title">优惠券转化效果</text>
|
||||||
|
<text class="card-desc">GMV提升、订单增长</text>
|
||||||
|
</view>
|
||||||
|
<EChartsView class="chart-box" :option="conversionChartOption" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 留白 -->
|
||||||
|
<view style="height: 24px;"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts">
|
||||||
|
import supa from '@/components/supadb/aksupainstance.uts'
|
||||||
|
import AnalyticsSidebarMenu from '@/components/analytics/AnalyticsSidebarMenu.uvue'
|
||||||
|
import AnalyticsTopBar from '@/components/analytics/AnalyticsTopBar.uvue'
|
||||||
|
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
|
||||||
|
|
||||||
|
type TimePeriod = { value: string; label: string }
|
||||||
|
type CouponData = {
|
||||||
|
total_issued: number
|
||||||
|
issued_growth: number
|
||||||
|
total_used: number
|
||||||
|
usage_rate: number
|
||||||
|
gmv_increase: number
|
||||||
|
gmv_growth: number
|
||||||
|
roi: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
AnalyticsSidebarMenu,
|
||||||
|
AnalyticsTopBar,
|
||||||
|
EChartsView
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
lastUpdateTime: '',
|
||||||
|
selectedPeriod: '7d',
|
||||||
|
showMoreMenu: false,
|
||||||
|
showSidebarMenu: false,
|
||||||
|
currentPath: '/pages/mall/analytics/coupon-analysis',
|
||||||
|
timePeriods: [
|
||||||
|
{ value: '7d', label: '7天' },
|
||||||
|
{ value: '30d', label: '30天' },
|
||||||
|
{ value: '90d', label: '90天' },
|
||||||
|
{ value: '1y', label: '1年' }
|
||||||
|
] as Array<TimePeriod>,
|
||||||
|
|
||||||
|
couponData: {
|
||||||
|
total_issued: 0,
|
||||||
|
issued_growth: 0,
|
||||||
|
total_used: 0,
|
||||||
|
usage_rate: 0,
|
||||||
|
gmv_increase: 0,
|
||||||
|
gmv_growth: 0,
|
||||||
|
roi: 0
|
||||||
|
} as CouponData,
|
||||||
|
|
||||||
|
typeChartOption: {} as any,
|
||||||
|
channelChartOption: {} as any,
|
||||||
|
trendChartOption: {} as any,
|
||||||
|
conversionChartOption: {} as any
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
selectedPeriodText(): string {
|
||||||
|
const p = this.timePeriods.find((t) => t.value === this.selectedPeriod)
|
||||||
|
return p ? p.label : '7天'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onLoad() {
|
||||||
|
this.updateTime()
|
||||||
|
this.loadCouponData()
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async loadCouponData() {
|
||||||
|
// TODO: 实现优惠券数据加载
|
||||||
|
this.updateTime()
|
||||||
|
this.buildChartOptions()
|
||||||
|
},
|
||||||
|
|
||||||
|
selectPeriod(p: string) {
|
||||||
|
this.selectedPeriod = p
|
||||||
|
this.loadCouponData()
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshData() {
|
||||||
|
this.loadCouponData()
|
||||||
|
uni.showToast({ title: '已刷新', icon: 'success' })
|
||||||
|
},
|
||||||
|
|
||||||
|
exportReport() {
|
||||||
|
uni.showActionSheet({
|
||||||
|
itemList: ['导出Excel', '导出PDF', '导出图片'],
|
||||||
|
success: () => uni.showToast({ title: '导出成功', icon: 'success' })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
updateTime() {
|
||||||
|
const now = new Date()
|
||||||
|
const hh = now.getHours().toString().padStart(2, '0')
|
||||||
|
const mm = now.getMinutes().toString().padStart(2, '0')
|
||||||
|
this.lastUpdateTime = `${hh}:${mm}`
|
||||||
|
},
|
||||||
|
|
||||||
|
formatInt(n: number): string {
|
||||||
|
const v = isFinite(n) ? Math.round(n) : 0
|
||||||
|
if (v >= 10000) return (v / 10000).toFixed(1) + '万'
|
||||||
|
return v.toString()
|
||||||
|
},
|
||||||
|
|
||||||
|
formatMoney(n: number): string {
|
||||||
|
const v = isFinite(n) ? n : 0
|
||||||
|
if (v >= 10000) return (v / 10000).toFixed(1) + '万'
|
||||||
|
return v.toFixed(0)
|
||||||
|
},
|
||||||
|
|
||||||
|
formatPct(n: number): string {
|
||||||
|
const v = isFinite(n) ? n : 0
|
||||||
|
const sign = v > 0 ? '+' : ''
|
||||||
|
return `${sign}${v.toFixed(1)}%`
|
||||||
|
},
|
||||||
|
|
||||||
|
buildChartOptions() {
|
||||||
|
// TODO: 构建图表配置
|
||||||
|
this.typeChartOption = {}
|
||||||
|
this.channelChartOption = {}
|
||||||
|
this.trendChartOption = {}
|
||||||
|
this.conversionChartOption = {}
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleMoreMenu() {
|
||||||
|
this.showMoreMenu = !this.showMoreMenu
|
||||||
|
},
|
||||||
|
|
||||||
|
closeMoreMenu() {
|
||||||
|
this.showMoreMenu = false
|
||||||
|
},
|
||||||
|
handleSidebarUpdate(visible: boolean) {
|
||||||
|
this.showSidebarMenu = visible
|
||||||
|
},
|
||||||
|
|
||||||
|
handleMenu() {
|
||||||
|
this.showSidebarMenu = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f6f7fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面布局:宽屏时侧边栏+内容,窄屏时全屏内容 */
|
||||||
|
.page-layout {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-top: 64px; /* 为固定顶部导航栏留出空间 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 16px 16px 28px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部栏 */
|
||||||
|
.topbar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.06);
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-left {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon:active {
|
||||||
|
background: #e5e7eb;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon .icon {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #111;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111;
|
||||||
|
max-width: 420px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0,0,0,0.55);
|
||||||
|
max-width: 420px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn-icon:active {
|
||||||
|
background: #e5e7eb;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn-icon .icon {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn {
|
||||||
|
display: none;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn.active {
|
||||||
|
background: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn .icon {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 时间维度 tabs */
|
||||||
|
.tabs {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.06);
|
||||||
|
overflow-x: auto;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #111;
|
||||||
|
font-size: 13px;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
background: #111;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* KPI 网格 */
|
||||||
|
.kpi-grid {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.06);
|
||||||
|
padding: 14px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
flex: 1 1 calc(50% - 6px);
|
||||||
|
min-width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0,0,0,0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-value {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-meta {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0,0,0,0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片 */
|
||||||
|
.card {
|
||||||
|
margin-top: 12px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.06);
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.04);
|
||||||
|
padding: 14px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-full {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-head {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0,0,0,0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-box {
|
||||||
|
width: 100%;
|
||||||
|
height: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式 */
|
||||||
|
@media screen and (min-width: 960px) {
|
||||||
|
.kpi-card {
|
||||||
|
flex: 1 1 calc(25% - 9px);
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式:窄屏时全屏显示 */
|
||||||
|
@media screen and (max-width: 959px) {
|
||||||
|
.page-layout {
|
||||||
|
flex-direction: column !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 960px) {
|
||||||
|
.title,
|
||||||
|
.subtitle {
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-right .btn-hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn {
|
||||||
|
display: flex !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
749
pages/mall/analytics/custom-report.uvue
Normal file
749
pages/mall/analytics/custom-report.uvue
Normal file
@@ -0,0 +1,749 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page" @click="closeMoreMenu">
|
||||||
|
<!-- 固定顶部导航栏 -->
|
||||||
|
<AnalyticsTopBar
|
||||||
|
:title="'自定义报表'"
|
||||||
|
:lastUpdateTime="'创建和管理您的专属报表'"
|
||||||
|
:sidebarVisible="showSidebarMenu"
|
||||||
|
@menu-click="handleMenu"
|
||||||
|
@refresh="refreshData"
|
||||||
|
@search="handleSearch"
|
||||||
|
@notification="handleNotification"
|
||||||
|
@fullscreen="handleFullscreen"
|
||||||
|
@mobile="handleMobile"
|
||||||
|
@dropdown="handleDropdown"
|
||||||
|
@settings="handleSettings"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<view class="page-layout">
|
||||||
|
<!-- 侧边栏菜单组件 -->
|
||||||
|
<AnalyticsSidebarMenu
|
||||||
|
:visible="showSidebarMenu"
|
||||||
|
:currentPath="currentPath"
|
||||||
|
@visible-change="handleSidebarUpdate"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 主内容区域 -->
|
||||||
|
<view class="main-content">
|
||||||
|
<view class="container">
|
||||||
|
|
||||||
|
<!-- 报表列表 -->
|
||||||
|
<view class="report-list">
|
||||||
|
<view v-for="report in reports" :key="report.id" class="report-card" @click="openReport(report)">
|
||||||
|
<view class="report-header">
|
||||||
|
<text class="report-title">{{ report.name }}</text>
|
||||||
|
<view class="report-actions">
|
||||||
|
<view class="action-btn" @click.stop="editReport(report)">
|
||||||
|
<text class="icon">✏️</text>
|
||||||
|
</view>
|
||||||
|
<view class="action-btn" @click.stop="deleteReport(report)">
|
||||||
|
<text class="icon">🗑️</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text class="report-desc">{{ report.description }}</text>
|
||||||
|
<view class="report-meta">
|
||||||
|
<text class="meta-item">指标:{{ report.metrics.length }}个</text>
|
||||||
|
<text class="meta-item">图表:{{ report.charts.length }}个</text>
|
||||||
|
<text class="meta-item">更新:{{ report.updated_at }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 新建报表对话框 -->
|
||||||
|
<view class="modal" v-if="showCreateModal" @click.stop>
|
||||||
|
<view class="modal-content" @click.stop>
|
||||||
|
<view class="modal-header">
|
||||||
|
<text class="modal-title">{{ editingReport ? '编辑报表' : '新建报表' }}</text>
|
||||||
|
<view class="modal-close" @click="closeModal">
|
||||||
|
<text class="icon">✕</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="modal-body">
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">报表名称</text>
|
||||||
|
<input class="form-input" v-model="reportForm.name" placeholder="请输入报表名称" />
|
||||||
|
</view>
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">报表描述</text>
|
||||||
|
<textarea class="form-textarea" v-model="reportForm.description" placeholder="请输入报表描述"></textarea>
|
||||||
|
</view>
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">选择指标</text>
|
||||||
|
<view class="metric-list">
|
||||||
|
<view
|
||||||
|
v-for="m in availableMetrics"
|
||||||
|
:key="m.key"
|
||||||
|
class="metric-item"
|
||||||
|
:class="{ selected: reportForm.metrics.includes(m.key) }"
|
||||||
|
@click="toggleMetric(m.key)"
|
||||||
|
>
|
||||||
|
<text>{{ m.label }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">时间维度</text>
|
||||||
|
<view class="period-list">
|
||||||
|
<view
|
||||||
|
v-for="p in timePeriods"
|
||||||
|
:key="p.value"
|
||||||
|
class="period-item"
|
||||||
|
:class="{ selected: reportForm.period === p.value }"
|
||||||
|
@click="reportForm.period = p.value"
|
||||||
|
>
|
||||||
|
<text>{{ p.label }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">图表类型</text>
|
||||||
|
<view class="chart-type-list">
|
||||||
|
<view
|
||||||
|
v-for="t in chartTypes"
|
||||||
|
:key="t.value"
|
||||||
|
class="chart-type-item"
|
||||||
|
:class="{ selected: reportForm.chartType === t.value }"
|
||||||
|
@click="reportForm.chartType = t.value"
|
||||||
|
>
|
||||||
|
<text>{{ t.label }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="modal-footer">
|
||||||
|
<view class="btn btn-cancel" @click="closeModal">取消</view>
|
||||||
|
<view class="btn btn-primary" @click="saveReport">保存</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 留白 -->
|
||||||
|
<view style="height: 24px;"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts">
|
||||||
|
import supa from '@/components/supadb/aksupainstance.uts'
|
||||||
|
import AnalyticsSidebarMenu from '@/components/analytics/AnalyticsSidebarMenu.uvue'
|
||||||
|
import AnalyticsTopBar from '@/components/analytics/AnalyticsTopBar.uvue'
|
||||||
|
|
||||||
|
type Report = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
metrics: Array<string>
|
||||||
|
charts: Array<string>
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
type Metric = { key: string; label: string }
|
||||||
|
type TimePeriod = { value: string; label: string }
|
||||||
|
type ChartType = { value: string; label: string }
|
||||||
|
type ReportForm = {
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
metrics: Array<string>
|
||||||
|
period: string
|
||||||
|
chartType: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
AnalyticsSidebarMenu,
|
||||||
|
AnalyticsTopBar
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showMoreMenu: false,
|
||||||
|
showSidebarMenu: false,
|
||||||
|
currentPath: '/pages/mall/analytics/custom-report',
|
||||||
|
showCreateModal: false,
|
||||||
|
editingReport: null as Report | null,
|
||||||
|
|
||||||
|
reports: [] as Array<Report>,
|
||||||
|
|
||||||
|
reportForm: {
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
metrics: [] as Array<string>,
|
||||||
|
period: '7d',
|
||||||
|
chartType: 'line'
|
||||||
|
} as ReportForm,
|
||||||
|
|
||||||
|
availableMetrics: [
|
||||||
|
{ key: 'gmv', label: 'GMV' },
|
||||||
|
{ key: 'orders', label: '订单数' },
|
||||||
|
{ key: 'users', label: '用户数' },
|
||||||
|
{ key: 'conversion', label: '转化率' },
|
||||||
|
{ key: 'avg_order', label: '客单价' },
|
||||||
|
{ key: 'repurchase', label: '复购率' }
|
||||||
|
] as Array<Metric>,
|
||||||
|
|
||||||
|
timePeriods: [
|
||||||
|
{ value: '7d', label: '7天' },
|
||||||
|
{ value: '30d', label: '30天' },
|
||||||
|
{ value: '90d', label: '90天' },
|
||||||
|
{ value: '1y', label: '1年' }
|
||||||
|
] as Array<TimePeriod>,
|
||||||
|
|
||||||
|
chartTypes: [
|
||||||
|
{ value: 'line', label: '折线图' },
|
||||||
|
{ value: 'bar', label: '柱状图' },
|
||||||
|
{ value: 'pie', label: '饼图' },
|
||||||
|
{ value: 'area', label: '面积图' },
|
||||||
|
{ value: 'combo', label: '组合图' }
|
||||||
|
] as Array<ChartType>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onLoad() {
|
||||||
|
this.currentPath = '/pages/mall/analytics/custom-report'
|
||||||
|
this.loadReports()
|
||||||
|
},
|
||||||
|
|
||||||
|
onShow() {
|
||||||
|
this.currentPath = '/pages/mall/analytics/custom-report'
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async loadReports() {
|
||||||
|
// TODO: 实现报表列表加载
|
||||||
|
},
|
||||||
|
|
||||||
|
createReport() {
|
||||||
|
this.editingReport = null
|
||||||
|
this.reportForm = {
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
metrics: [],
|
||||||
|
period: '7d',
|
||||||
|
chartType: 'line'
|
||||||
|
}
|
||||||
|
this.showCreateModal = true
|
||||||
|
},
|
||||||
|
|
||||||
|
editReport(report: Report) {
|
||||||
|
this.editingReport = report
|
||||||
|
this.reportForm = {
|
||||||
|
name: report.name,
|
||||||
|
description: report.description,
|
||||||
|
metrics: report.metrics,
|
||||||
|
period: '7d',
|
||||||
|
chartType: 'line'
|
||||||
|
}
|
||||||
|
this.showCreateModal = true
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteReport(report: Report) {
|
||||||
|
uni.showModal({
|
||||||
|
title: '确认删除',
|
||||||
|
content: `确定要删除报表"${report.name}"吗?`,
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
// TODO: 实现删除逻辑
|
||||||
|
uni.showToast({ title: '删除成功', icon: 'success' })
|
||||||
|
this.loadReports()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleMetric(key: string) {
|
||||||
|
const index = this.reportForm.metrics.indexOf(key)
|
||||||
|
if (index >= 0) {
|
||||||
|
this.reportForm.metrics.splice(index, 1)
|
||||||
|
} else {
|
||||||
|
this.reportForm.metrics.push(key)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
saveReport() {
|
||||||
|
if (!this.reportForm.name.trim()) {
|
||||||
|
uni.showToast({ title: '请输入报表名称', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.reportForm.metrics.length === 0) {
|
||||||
|
uni.showToast({ title: '请至少选择一个指标', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// TODO: 实现保存逻辑
|
||||||
|
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||||
|
this.closeModal()
|
||||||
|
this.loadReports()
|
||||||
|
},
|
||||||
|
|
||||||
|
openReport(report: Report) {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/mall/analytics/report-detail?id=${report.id}`
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
closeModal() {
|
||||||
|
this.showCreateModal = false
|
||||||
|
this.editingReport = null
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshData() {
|
||||||
|
this.loadReports()
|
||||||
|
uni.showToast({ title: '已刷新', icon: 'success' })
|
||||||
|
},
|
||||||
|
|
||||||
|
handleMenu() {
|
||||||
|
this.showSidebarMenu = true
|
||||||
|
},
|
||||||
|
handleSidebarUpdate(visible: boolean) {
|
||||||
|
this.showSidebarMenu = visible
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleMoreMenu() {
|
||||||
|
this.showMoreMenu = !this.showMoreMenu
|
||||||
|
},
|
||||||
|
|
||||||
|
closeMoreMenu() {
|
||||||
|
this.showMoreMenu = false
|
||||||
|
},
|
||||||
|
handleSearch() {
|
||||||
|
uni.showToast({ title: '搜索', icon: 'none' })
|
||||||
|
},
|
||||||
|
handleNotification() {
|
||||||
|
uni.showToast({ title: '通知', icon: 'none' })
|
||||||
|
},
|
||||||
|
handleFullscreen() {
|
||||||
|
uni.showToast({ title: '全屏', icon: 'none' })
|
||||||
|
},
|
||||||
|
handleMobile() {
|
||||||
|
uni.showToast({ title: '移动端', icon: 'none' })
|
||||||
|
},
|
||||||
|
handleDropdown() {
|
||||||
|
uni.showToast({ title: '下拉菜单', icon: 'none' })
|
||||||
|
},
|
||||||
|
handleSettings() {
|
||||||
|
uni.showToast({ title: '设置', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f6f7fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面布局:宽屏时侧边栏+内容,窄屏时全屏内容 */
|
||||||
|
.page-layout {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-top: 64px; /* 为固定顶部导航栏留出空间 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 16px 16px 28px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部栏 */
|
||||||
|
.topbar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.06);
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-left {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon:active {
|
||||||
|
background: #e5e7eb;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon .icon {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #111;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111;
|
||||||
|
max-width: 420px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0,0,0,0.55);
|
||||||
|
max-width: 420px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn-icon:active {
|
||||||
|
background: #e5e7eb;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn-icon .icon {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn {
|
||||||
|
display: none;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn.active {
|
||||||
|
background: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn .icon {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 报表列表 */
|
||||||
|
.report-list {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.06);
|
||||||
|
padding: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-card:active {
|
||||||
|
background: #f9fafb;
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:active {
|
||||||
|
background: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn .icon {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-desc {
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(0,0,0,0.65);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0,0,0,0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 模态框 */
|
||||||
|
.modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0,0,0,0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
width: 90%;
|
||||||
|
max-width: 600px;
|
||||||
|
max-height: 80vh;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px;
|
||||||
|
border-bottom: 1px solid rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close .icon {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
flex: 1;
|
||||||
|
padding: 16px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input,
|
||||||
|
.form-textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-textarea {
|
||||||
|
min-height: 80px;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-list,
|
||||||
|
.period-list,
|
||||||
|
.chart-type-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-item,
|
||||||
|
.period-item,
|
||||||
|
.chart-type-item {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-item.selected,
|
||||||
|
.period-item.selected,
|
||||||
|
.chart-type-item.selected {
|
||||||
|
background: #111;
|
||||||
|
color: #fff;
|
||||||
|
border-color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
border-top: 1px solid rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #111;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式 */
|
||||||
|
@media screen and (max-width: 960px) {
|
||||||
|
.title,
|
||||||
|
.subtitle {
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-right .btn-hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn {
|
||||||
|
display: flex !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
width: 95%;
|
||||||
|
max-height: 90vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式:窄屏时全屏显示 */
|
||||||
|
@media screen and (max-width: 959px) {
|
||||||
|
.page-layout {
|
||||||
|
flex-direction: column !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
651
pages/mall/analytics/data-detail.uvue
Normal file
651
pages/mall/analytics/data-detail.uvue
Normal file
@@ -0,0 +1,651 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page" @click="closeMoreMenu">
|
||||||
|
<!-- 固定顶部导航栏 -->
|
||||||
|
<AnalyticsTopBar
|
||||||
|
:title="'数据分析详情'"
|
||||||
|
:lastUpdateTime="lastUpdateTime"
|
||||||
|
:sidebarVisible="showSidebarMenu"
|
||||||
|
@menu-click="handleMenu"
|
||||||
|
@refresh="refreshData"
|
||||||
|
@search="handleSearch"
|
||||||
|
@notification="handleNotification"
|
||||||
|
@fullscreen="handleFullscreen"
|
||||||
|
@mobile="handleMobile"
|
||||||
|
@dropdown="handleDropdown"
|
||||||
|
@settings="handleSettings"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<view class="page-layout">
|
||||||
|
<!-- 侧边栏菜单组件 -->
|
||||||
|
<AnalyticsSidebarMenu
|
||||||
|
:visible="showSidebarMenu"
|
||||||
|
:currentPath="currentPath"
|
||||||
|
@visible-change="handleSidebarUpdate"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 主内容区域 -->
|
||||||
|
<view class="main-content">
|
||||||
|
<view class="container">
|
||||||
|
|
||||||
|
<!-- 数据筛选器 -->
|
||||||
|
<view class="filter-bar">
|
||||||
|
<view class="filter-item">
|
||||||
|
<text class="filter-label">时间范围:</text>
|
||||||
|
<view class="filter-value" @click="selectTimeRange">
|
||||||
|
{{ timeRangeText }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="filter-item">
|
||||||
|
<text class="filter-label">数据维度:</text>
|
||||||
|
<view class="filter-value" @click="selectDimension">
|
||||||
|
{{ dimensionText }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="filter-item">
|
||||||
|
<text class="filter-label">对比模式:</text>
|
||||||
|
<view class="filter-value" @click="toggleCompare">
|
||||||
|
{{ compareMode ? '开启' : '关闭' }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 详细数据表格 -->
|
||||||
|
<view class="card card-full">
|
||||||
|
<view class="card-head">
|
||||||
|
<text class="card-title">详细数据</text>
|
||||||
|
<text class="card-desc">支持排序、筛选、导出</text>
|
||||||
|
</view>
|
||||||
|
<view class="data-table">
|
||||||
|
<view class="table-header">
|
||||||
|
<view class="table-cell" v-for="col in tableColumns" :key="col.key">
|
||||||
|
<text>{{ col.label }}</text>
|
||||||
|
<text class="sort-icon" v-if="col.sortable" @click="sortBy(col.key)">⇅</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="table-body">
|
||||||
|
<view class="table-row" v-for="row in tableData" :key="row.id">
|
||||||
|
<view class="table-cell" v-for="col in tableColumns" :key="col.key">
|
||||||
|
<text>{{ formatCellValue(row[col.key], col.type) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 数据对比图表 -->
|
||||||
|
<view class="card card-full" v-if="compareMode">
|
||||||
|
<view class="card-head">
|
||||||
|
<text class="card-title">数据对比</text>
|
||||||
|
<text class="card-desc">当前周期 vs 对比周期</text>
|
||||||
|
</view>
|
||||||
|
<EChartsView class="chart-box" :option="compareChartOption" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 数据钻取 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="card-head">
|
||||||
|
<text class="card-title">数据钻取</text>
|
||||||
|
<text class="card-desc">点击数据项查看详情</text>
|
||||||
|
</view>
|
||||||
|
<view class="drill-down-list">
|
||||||
|
<view v-for="item in drillDownItems" :key="item.id" class="drill-item" @click="drillDown(item)">
|
||||||
|
<text class="drill-label">{{ item.label }}</text>
|
||||||
|
<text class="drill-value">{{ item.value }}</text>
|
||||||
|
<text class="drill-arrow">→</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 留白 -->
|
||||||
|
<view style="height: 24px;"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts">
|
||||||
|
import supa from '@/components/supadb/aksupainstance.uts'
|
||||||
|
import AnalyticsSidebarMenu from '@/components/analytics/AnalyticsSidebarMenu.uvue'
|
||||||
|
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
|
||||||
|
|
||||||
|
type TableColumn = { key: string; label: string; type: string; sortable: boolean }
|
||||||
|
type DrillDownItem = { id: string; label: string; value: string; type: string }
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
AnalyticsSidebarMenu,
|
||||||
|
AnalyticsTopBar,
|
||||||
|
EChartsView
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
lastUpdateTime: '',
|
||||||
|
showMoreMenu: false,
|
||||||
|
timeRangeText: '最近7天',
|
||||||
|
dimensionText: '全部',
|
||||||
|
compareMode: false,
|
||||||
|
sortKey: '',
|
||||||
|
sortOrder: 'asc',
|
||||||
|
|
||||||
|
tableColumns: [
|
||||||
|
{ key: 'date', label: '日期', type: 'date', sortable: true },
|
||||||
|
{ key: 'gmv', label: 'GMV', type: 'money', sortable: true },
|
||||||
|
{ key: 'orders', label: '订单数', type: 'number', sortable: true },
|
||||||
|
{ key: 'users', label: '用户数', type: 'number', sortable: true }
|
||||||
|
] as Array<TableColumn>,
|
||||||
|
|
||||||
|
tableData: [] as Array<any>,
|
||||||
|
|
||||||
|
drillDownItems: [] as Array<DrillDownItem>,
|
||||||
|
|
||||||
|
compareChartOption: {} as any
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onLoad(options: any) {
|
||||||
|
this.currentPath = '/pages/mall/analytics/data-detail'
|
||||||
|
// 接收参数:dataType, timeRange, dimension
|
||||||
|
if (options.dataType) {
|
||||||
|
// 根据数据类型加载不同的数据
|
||||||
|
}
|
||||||
|
this.updateTime()
|
||||||
|
this.loadDetailData()
|
||||||
|
},
|
||||||
|
|
||||||
|
onShow() {
|
||||||
|
this.currentPath = '/pages/mall/analytics/data-detail'
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async loadDetailData() {
|
||||||
|
// TODO: 实现详细数据加载
|
||||||
|
this.updateTime()
|
||||||
|
this.buildChartOptions()
|
||||||
|
},
|
||||||
|
|
||||||
|
selectTimeRange() {
|
||||||
|
uni.showActionSheet({
|
||||||
|
itemList: ['最近7天', '最近30天', '最近90天', '自定义'],
|
||||||
|
success: (res) => {
|
||||||
|
const ranges = ['最近7天', '最近30天', '最近90天', '自定义']
|
||||||
|
this.timeRangeText = ranges[res.tapIndex]
|
||||||
|
this.loadDetailData()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
selectDimension() {
|
||||||
|
uni.showActionSheet({
|
||||||
|
itemList: ['全部', '按商家', '按分类', '按地域'],
|
||||||
|
success: (res) => {
|
||||||
|
const dims = ['全部', '按商家', '按分类', '按地域']
|
||||||
|
this.dimensionText = dims[res.tapIndex]
|
||||||
|
this.loadDetailData()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleCompare() {
|
||||||
|
this.compareMode = !this.compareMode
|
||||||
|
if (this.compareMode) {
|
||||||
|
this.buildChartOptions()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
sortBy(key: string) {
|
||||||
|
if (this.sortKey === key) {
|
||||||
|
this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc'
|
||||||
|
} else {
|
||||||
|
this.sortKey = key
|
||||||
|
this.sortOrder = 'asc'
|
||||||
|
}
|
||||||
|
// TODO: 实现排序逻辑
|
||||||
|
},
|
||||||
|
|
||||||
|
formatCellValue(value: any, type: string): string {
|
||||||
|
if (value == null) return '-'
|
||||||
|
if (type === 'money') {
|
||||||
|
const v = Number(value)
|
||||||
|
if (v >= 10000) return (v / 10000).toFixed(1) + '万'
|
||||||
|
return v.toFixed(2)
|
||||||
|
}
|
||||||
|
if (type === 'number') {
|
||||||
|
return String(Math.round(Number(value)))
|
||||||
|
}
|
||||||
|
if (type === 'date') {
|
||||||
|
return String(value)
|
||||||
|
}
|
||||||
|
return String(value)
|
||||||
|
},
|
||||||
|
|
||||||
|
drillDown(item: DrillDownItem) {
|
||||||
|
// TODO: 实现数据钻取
|
||||||
|
uni.showToast({ title: `查看 ${item.label} 详情`, icon: 'none' })
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshData() {
|
||||||
|
this.loadDetailData()
|
||||||
|
uni.showToast({ title: '已刷新', icon: 'success' })
|
||||||
|
},
|
||||||
|
|
||||||
|
exportReport() {
|
||||||
|
uni.showActionSheet({
|
||||||
|
itemList: ['导出Excel', '导出PDF', '导出CSV'],
|
||||||
|
success: () => uni.showToast({ title: '导出成功', icon: 'success' })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
updateTime() {
|
||||||
|
const now = new Date()
|
||||||
|
const hh = now.getHours().toString().padStart(2, '0')
|
||||||
|
const mm = now.getMinutes().toString().padStart(2, '0')
|
||||||
|
this.lastUpdateTime = `${hh}:${mm}`
|
||||||
|
},
|
||||||
|
|
||||||
|
buildChartOptions() {
|
||||||
|
// TODO: 构建图表配置
|
||||||
|
this.compareChartOption = {}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleMenu() {
|
||||||
|
this.showSidebarMenu = true
|
||||||
|
},
|
||||||
|
handleSidebarUpdate(visible: boolean) {
|
||||||
|
this.showSidebarMenu = visible
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleMoreMenu() {
|
||||||
|
this.showMoreMenu = !this.showMoreMenu
|
||||||
|
},
|
||||||
|
|
||||||
|
closeMoreMenu() {
|
||||||
|
this.showMoreMenu = false
|
||||||
|
},
|
||||||
|
handleSearch() {
|
||||||
|
uni.showToast({ title: '搜索', icon: 'none' })
|
||||||
|
},
|
||||||
|
handleNotification() {
|
||||||
|
uni.showToast({ title: '通知', icon: 'none' })
|
||||||
|
},
|
||||||
|
handleFullscreen() {
|
||||||
|
uni.showToast({ title: '全屏', icon: 'none' })
|
||||||
|
},
|
||||||
|
handleMobile() {
|
||||||
|
uni.showToast({ title: '移动端', icon: 'none' })
|
||||||
|
},
|
||||||
|
handleDropdown() {
|
||||||
|
uni.showToast({ title: '下拉菜单', icon: 'none' })
|
||||||
|
},
|
||||||
|
handleSettings() {
|
||||||
|
uni.showToast({ title: '设置', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f6f7fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面布局:宽屏时侧边栏+内容,窄屏时全屏内容 */
|
||||||
|
.page-layout {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-top: 64px; /* 为固定顶部导航栏留出空间 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 16px 16px 28px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部栏 */
|
||||||
|
.topbar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.06);
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-left {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon:active {
|
||||||
|
background: #e5e7eb;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon .icon {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #111;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111;
|
||||||
|
max-width: 420px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0,0,0,0.55);
|
||||||
|
max-width: 420px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn-icon:active {
|
||||||
|
background: #e5e7eb;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn-icon .icon {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn {
|
||||||
|
display: none;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn.active {
|
||||||
|
background: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn .icon {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 筛选栏 */
|
||||||
|
.filter-bar {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.06);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(0,0,0,0.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-value {
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #111;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-value:active {
|
||||||
|
background: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片 */
|
||||||
|
.card {
|
||||||
|
margin-top: 12px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.06);
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.04);
|
||||||
|
padding: 14px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-full {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-head {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0,0,0,0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-box {
|
||||||
|
width: 100%;
|
||||||
|
height: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 数据表格 */
|
||||||
|
.data-table {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
background: #f9fafb;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
border-bottom: 1px solid rgba(0,0,0,0.06);
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-cell {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #111;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-header .table-cell {
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(0,0,0,0.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-icon {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0,0,0,0.45);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 数据钻取 */
|
||||||
|
.drill-down-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drill-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #f9fafb;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drill-item:active {
|
||||||
|
background: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drill-label {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drill-value {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drill-arrow {
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0,0,0,0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式:窄屏时全屏显示 */
|
||||||
|
@media screen and (max-width: 959px) {
|
||||||
|
.page-layout {
|
||||||
|
flex-direction: column !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式 */
|
||||||
|
@media screen and (max-width: 960px) {
|
||||||
|
.title,
|
||||||
|
.subtitle {
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-right .btn-hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn {
|
||||||
|
display: flex !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-bar {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table {
|
||||||
|
overflow-x: scroll;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
629
pages/mall/analytics/delivery-analysis.uvue
Normal file
629
pages/mall/analytics/delivery-analysis.uvue
Normal file
@@ -0,0 +1,629 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page" @click="closeMoreMenu">
|
||||||
|
<!-- 固定顶部导航栏 -->
|
||||||
|
<AnalyticsTopBar
|
||||||
|
:title="'配送效率分析'"
|
||||||
|
:lastUpdateTime="lastUpdateTime"
|
||||||
|
:sidebarVisible="showSidebarMenu"
|
||||||
|
@menu-click="handleMenu"
|
||||||
|
@refresh="refreshData"
|
||||||
|
@search="handleSearch"
|
||||||
|
@notification="handleNotification"
|
||||||
|
@fullscreen="handleFullscreen"
|
||||||
|
@mobile="handleMobile"
|
||||||
|
@dropdown="handleDropdown"
|
||||||
|
@settings="handleSettings"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<view class="page-layout">
|
||||||
|
<!-- 侧边栏菜单组件 -->
|
||||||
|
<AnalyticsSidebarMenu
|
||||||
|
:visible="showSidebarMenu"
|
||||||
|
:currentPath="currentPath"
|
||||||
|
@visible-change="handleSidebarUpdate"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 主内容区域 -->
|
||||||
|
<view class="main-content">
|
||||||
|
<view class="container">
|
||||||
|
|
||||||
|
<!-- 时间维度筛选 -->
|
||||||
|
<view class="tabs">
|
||||||
|
<view
|
||||||
|
v-for="p in timePeriods"
|
||||||
|
:key="p.value"
|
||||||
|
class="tab"
|
||||||
|
:class="{ active: selectedPeriod === p.value }"
|
||||||
|
@click="selectPeriod(p.value)"
|
||||||
|
>
|
||||||
|
{{ p.label }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- KPI 指标卡片 -->
|
||||||
|
<view class="kpi-grid">
|
||||||
|
<view class="kpi-card">
|
||||||
|
<text class="kpi-label">配送时效</text>
|
||||||
|
<text class="kpi-value">{{ deliveryData.avg_delivery_time }}分钟</text>
|
||||||
|
<text class="kpi-meta">较上期:{{ formatPct(deliveryData.time_growth) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="kpi-card">
|
||||||
|
<text class="kpi-label">配送费用</text>
|
||||||
|
<text class="kpi-value">¥{{ formatMoney(deliveryData.total_fee) }}</text>
|
||||||
|
<text class="kpi-meta">平均:¥{{ formatMoney(deliveryData.avg_fee) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="kpi-card">
|
||||||
|
<text class="kpi-label">配送员效率</text>
|
||||||
|
<text class="kpi-value">{{ formatInt(deliveryData.avg_orders_per_driver) }}</text>
|
||||||
|
<text class="kpi-meta">单/人/天</text>
|
||||||
|
</view>
|
||||||
|
<view class="kpi-card">
|
||||||
|
<text class="kpi-label">客户满意度</text>
|
||||||
|
<text class="kpi-value">{{ deliveryData.satisfaction_rate }}%</text>
|
||||||
|
<text class="kpi-meta">较上期:{{ formatPct(deliveryData.satisfaction_growth) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 配送时效分析 -->
|
||||||
|
<view class="card card-full">
|
||||||
|
<view class="card-head">
|
||||||
|
<text class="card-title">配送时效分析</text>
|
||||||
|
<text class="card-desc">{{ selectedPeriodText }} · 平均配送时间趋势</text>
|
||||||
|
</view>
|
||||||
|
<EChartsView class="chart-box" :option="timeChartOption" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 配送费用分析 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="card-head">
|
||||||
|
<text class="card-title">配送费用分析</text>
|
||||||
|
<text class="card-desc">费用分布情况</text>
|
||||||
|
</view>
|
||||||
|
<EChartsView class="chart-box" :option="feeChartOption" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 配送员效率排行 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="card-head">
|
||||||
|
<text class="card-title">配送员效率排行 TOP 10</text>
|
||||||
|
<text class="card-desc">按订单数排序</text>
|
||||||
|
</view>
|
||||||
|
<view class="rank-list">
|
||||||
|
<view v-for="d in topDrivers" :key="d.id" class="rank-item">
|
||||||
|
<text class="rank-no">{{ d.rank }}</text>
|
||||||
|
<text class="rank-name">{{ d.name }}</text>
|
||||||
|
<view class="rank-right">
|
||||||
|
<text class="rank-val">{{ d.orders }} 单</text>
|
||||||
|
<text class="chip" :class="d.rating >= 4.5 ? 'pos' : 'neg'">
|
||||||
|
⭐{{ d.rating }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 客户满意度分析 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="card-head">
|
||||||
|
<text class="card-title">客户满意度分析</text>
|
||||||
|
<text class="card-desc">评分分布</text>
|
||||||
|
</view>
|
||||||
|
<EChartsView class="chart-box" :option="satisfactionChartOption" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 留白 -->
|
||||||
|
<view style="height: 24px;"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts">
|
||||||
|
import supa from '@/components/supadb/aksupainstance.uts'
|
||||||
|
import AnalyticsSidebarMenu from '@/components/analytics/AnalyticsSidebarMenu.uvue'
|
||||||
|
import AnalyticsTopBar from '@/components/analytics/AnalyticsTopBar.uvue'
|
||||||
|
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
|
||||||
|
|
||||||
|
type DeliveryData = {
|
||||||
|
avg_delivery_time: number
|
||||||
|
time_growth: number
|
||||||
|
total_fee: number
|
||||||
|
avg_fee: number
|
||||||
|
avg_orders_per_driver: number
|
||||||
|
satisfaction_rate: number
|
||||||
|
satisfaction_growth: number
|
||||||
|
}
|
||||||
|
type DriverRank = { id: string; rank: number; name: string; orders: number; rating: number }
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
AnalyticsSidebarMenu,
|
||||||
|
AnalyticsTopBar,
|
||||||
|
EChartsView
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
lastUpdateTime: '',
|
||||||
|
selectedPeriod: '7d',
|
||||||
|
showMoreMenu: false,
|
||||||
|
showSidebarMenu: false,
|
||||||
|
currentPath: '/pages/mall/analytics/delivery-analysis',
|
||||||
|
timePeriods: [
|
||||||
|
{ value: '7d', label: '7天' },
|
||||||
|
{ value: '30d', label: '30天' },
|
||||||
|
{ value: '90d', label: '90天' },
|
||||||
|
{ value: '1y', label: '1年' }
|
||||||
|
],
|
||||||
|
|
||||||
|
deliveryData: {
|
||||||
|
avg_delivery_time: 0,
|
||||||
|
time_growth: 0,
|
||||||
|
total_fee: 0,
|
||||||
|
avg_fee: 0,
|
||||||
|
avg_orders_per_driver: 0,
|
||||||
|
satisfaction_rate: 0,
|
||||||
|
satisfaction_growth: 0
|
||||||
|
} as DeliveryData,
|
||||||
|
|
||||||
|
topDrivers: [] as Array<DriverRank>,
|
||||||
|
|
||||||
|
timeChartOption: {} as any,
|
||||||
|
feeChartOption: {} as any,
|
||||||
|
satisfactionChartOption: {} as any
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
selectedPeriodText(): string {
|
||||||
|
const p = this.timePeriods.find((t) => t.value === this.selectedPeriod)
|
||||||
|
return p ? p.label : '7天'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onLoad() {
|
||||||
|
this.updateTime()
|
||||||
|
this.loadDeliveryData()
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async loadDeliveryData() {
|
||||||
|
// TODO: 实现配送数据加载
|
||||||
|
this.updateTime()
|
||||||
|
this.buildChartOptions()
|
||||||
|
},
|
||||||
|
|
||||||
|
selectPeriod(p: string) {
|
||||||
|
this.selectedPeriod = p
|
||||||
|
this.loadDeliveryData()
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshData() {
|
||||||
|
this.loadDeliveryData()
|
||||||
|
uni.showToast({ title: '已刷新', icon: 'success' })
|
||||||
|
},
|
||||||
|
|
||||||
|
exportReport() {
|
||||||
|
uni.showActionSheet({
|
||||||
|
itemList: ['导出Excel', '导出PDF', '导出图片'],
|
||||||
|
success: () => uni.showToast({ title: '导出成功', icon: 'success' })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
updateTime() {
|
||||||
|
const now = new Date()
|
||||||
|
const hh = now.getHours().toString().padStart(2, '0')
|
||||||
|
const mm = now.getMinutes().toString().padStart(2, '0')
|
||||||
|
this.lastUpdateTime = `${hh}:${mm}`
|
||||||
|
},
|
||||||
|
|
||||||
|
formatInt(n: number): string {
|
||||||
|
const v = isFinite(n) ? Math.round(n) : 0
|
||||||
|
if (v >= 10000) return (v / 10000).toFixed(1) + '万'
|
||||||
|
return v.toString()
|
||||||
|
},
|
||||||
|
|
||||||
|
formatMoney(n: number): string {
|
||||||
|
const v = isFinite(n) ? n : 0
|
||||||
|
if (v >= 10000) return (v / 10000).toFixed(1) + '万'
|
||||||
|
return v.toFixed(2)
|
||||||
|
},
|
||||||
|
|
||||||
|
formatPct(n: number): string {
|
||||||
|
const v = isFinite(n) ? n : 0
|
||||||
|
const sign = v > 0 ? '+' : ''
|
||||||
|
return `${sign}${v.toFixed(1)}%`
|
||||||
|
},
|
||||||
|
|
||||||
|
buildChartOptions() {
|
||||||
|
// TODO: 构建图表配置
|
||||||
|
this.timeChartOption = {}
|
||||||
|
this.feeChartOption = {}
|
||||||
|
this.satisfactionChartOption = {}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleMenu() {
|
||||||
|
this.showSidebarMenu = true
|
||||||
|
},
|
||||||
|
handleSidebarUpdate(visible: boolean) {
|
||||||
|
this.showSidebarMenu = visible
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleMoreMenu() {
|
||||||
|
this.showMoreMenu = !this.showMoreMenu
|
||||||
|
},
|
||||||
|
|
||||||
|
closeMoreMenu() {
|
||||||
|
this.showMoreMenu = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f6f7fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面布局:宽屏时侧边栏+内容,窄屏时全屏内容 */
|
||||||
|
.page-layout {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-top: 64px; /* 为固定顶部导航栏留出空间 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 16px 16px 28px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部栏 */
|
||||||
|
.topbar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.06);
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-left {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon:active {
|
||||||
|
background: #e5e7eb;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon .icon {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #111;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111;
|
||||||
|
max-width: 420px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0,0,0,0.55);
|
||||||
|
max-width: 420px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn-icon:active {
|
||||||
|
background: #e5e7eb;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn-icon .icon {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn {
|
||||||
|
display: none;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn.active {
|
||||||
|
background: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn .icon {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 时间维度 tabs */
|
||||||
|
.tabs {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.06);
|
||||||
|
overflow-x: auto;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #111;
|
||||||
|
font-size: 13px;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
background: #111;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* KPI 网格 */
|
||||||
|
.kpi-grid {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.06);
|
||||||
|
padding: 14px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
flex: 1 1 calc(50% - 6px);
|
||||||
|
min-width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0,0,0,0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-value {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-meta {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0,0,0,0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片 */
|
||||||
|
.card {
|
||||||
|
margin-top: 12px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.06);
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.04);
|
||||||
|
padding: 14px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-full {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-head {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0,0,0,0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-box {
|
||||||
|
width: 100%;
|
||||||
|
height: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 排行列表 */
|
||||||
|
.rank-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px solid rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-no {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(0,0,0,0.06);
|
||||||
|
text-align: center;
|
||||||
|
line-height: 28px;
|
||||||
|
font-size: 12px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-name {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-val {
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(0,0,0,0.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip {
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip.pos {
|
||||||
|
background: rgba(34,197,94,0.12);
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip.neg {
|
||||||
|
background: rgba(239,68,68,0.12);
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式 */
|
||||||
|
@media screen and (min-width: 960px) {
|
||||||
|
.kpi-card {
|
||||||
|
flex: 1 1 calc(25% - 9px);
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 960px) {
|
||||||
|
.title,
|
||||||
|
.subtitle {
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-right .btn-hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn {
|
||||||
|
display: flex !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式:窄屏时全屏显示 */
|
||||||
|
@media screen and (max-width: 959px) {
|
||||||
|
.page-layout {
|
||||||
|
flex-direction: column !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
311
pages/mall/analytics/docs/ANALYTICS_DB_DESIGN.md
Normal file
311
pages/mall/analytics/docs/ANALYTICS_DB_DESIGN.md
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
## 数据分析模块数据库设计(Supabase / Postgres)
|
||||||
|
|
||||||
|
> 本文档面向 **数据分析端(Analytics Dashboard)**,为页面 `pages/mall/analytics/*` 提供可落地的表结构、字段字典、以及用于联调的模拟数据方案。
|
||||||
|
>
|
||||||
|
> 参考输入(仅作为需求与既有模型依据):`pages/mall/mall.md`(订单/用户/商品/配送/优惠券/统计)、`pages/mall/analytics/docs/ANALYTICS_PAGES_ANALYSIS.md`(页面与指标清单)、以及前端页面当前使用的字段名(如 `realTime.gmv`、`report.title` 等)。
|
||||||
|
>
|
||||||
|
> **文档位置**:`pages/mall/analytics/docs/ANALYTICS_DB_DESIGN.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 设计目标与范围
|
||||||
|
|
||||||
|
### 1.1 目标
|
||||||
|
- **支持已实现页面的数据落库**:`index`(实时 KPI + 趋势)、`profile`(报表列表/偏好/导出)、`report-detail`(报表详情 + 指标 + 明细表格 + 洞察)。
|
||||||
|
- **支持后续页面扩展**:`sales-report`、`user-analysis`、`product-insights`、`delivery-analysis`、`coupon-analysis`、`market-trends`、`custom-report`。
|
||||||
|
- **与 `components/supadb` 兼容**:优先提供可 `select` 的表/视图;复杂统计使用 **RPC(Postgres function)**,在前端通过 `supadb.uvue` 的 `rpc` 能力调用。
|
||||||
|
|
||||||
|
### 1.2 范围说明
|
||||||
|
- 本文档**不替代**业务核心表(如 `orders`、`order_items`、`products`、`users`、`delivery_tasks`、`coupon_*`)。这些以 `pages/mall/mall.md` 为准。
|
||||||
|
- 本文档新增的是 **Analytics 侧“报表/洞察/导出/偏好/预警”等应用数据表**,以及可选的聚合视图/RPC。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 现有基础表(业务域)与分析端关系
|
||||||
|
|
||||||
|
数据分析端统计大多来源于以下基础表(来自 `mall.md` 的模型):
|
||||||
|
- **订单域**:`orders`、`order_items`
|
||||||
|
- **用户域**:`users`
|
||||||
|
- **商家/商品域**:`merchants`、`products`、`categories`
|
||||||
|
- **配送域**:`delivery_tasks`、`delivery_drivers`、`delivery_tracks`
|
||||||
|
- **营销域**:`coupon_templates`、`user_coupons`、`coupon_usage_logs`
|
||||||
|
- **统计域(已在需求中出现)**:`daily_statistics`(按天、可按 `merchant_id` 聚合)
|
||||||
|
|
||||||
|
分析端新增的表会通过外键关联这些基础表(尤其是 `users`、`merchants`、`orders`)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Analytics 新增表:数据字典(推荐最小集)
|
||||||
|
|
||||||
|
> 命名约定:以 `analytics_` 为前缀,避免与业务表冲突。
|
||||||
|
|
||||||
|
### 3.1 `analytics_user_preferences`(分析师偏好)
|
||||||
|
**用途**:`profile` 页偏好设置、默认周期、默认看板等。
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|
| -------------- | ----------- | ----------------------- | ------------------------- |
|
||||||
|
| id | uuid | PK | 主键 |
|
||||||
|
| user_id | uuid | FK → users(id), UNIQUE | 偏好所属用户 |
|
||||||
|
| default_period | text | NOT NULL, default '7d' | 7d/30d/90d/1y 等 |
|
||||||
|
| timezone | text | default 'Asia/Shanghai' | 时区 |
|
||||||
|
| currency | text | default 'CNY' | 展示币种 |
|
||||||
|
| kpi_cards | jsonb | default '[]' | KPI 卡片配置(顺序/开关) |
|
||||||
|
| created_at | timestamptz | default now() | 创建时间 |
|
||||||
|
| updated_at | timestamptz | default now() | 更新时间 |
|
||||||
|
|
||||||
|
索引建议:`(user_id)` 唯一索引即可。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.2 `analytics_reports`(报表定义/实例)
|
||||||
|
**用途**:`report-detail` 的 report 主体、`profile` 最近报表列表、`custom-report` 报表定义。
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|
| ------------- | ----------- | ---------------------------- | -------------------------------------------------------------- |
|
||||||
|
| id | uuid | PK | 报表 ID |
|
||||||
|
| owner_user_id | uuid | FK → users(id) | 报表创建者/所属分析师 |
|
||||||
|
| merchant_id | uuid | FK → merchants(id), nullable | 可选:报表限定商家 |
|
||||||
|
| title | text | NOT NULL | 报表标题(`report.title`) |
|
||||||
|
| description | text | default '' | 描述(列表展示) |
|
||||||
|
| type | text | NOT NULL | sales/users/orders/conversion/coupon/delivery/market/custom 等 |
|
||||||
|
| period | text | NOT NULL | 7d/30d/90d/1y 或自定义 |
|
||||||
|
| date_start | date | nullable | 自定义范围起始 |
|
||||||
|
| date_end | date | nullable | 自定义范围结束 |
|
||||||
|
| status | text | NOT NULL, default 'ready' | pending/ready/failed/scheduled/shared |
|
||||||
|
| generated_at | timestamptz | nullable | 生成时间(`report.generated_at`) |
|
||||||
|
| created_at | timestamptz | default now() | 创建时间 |
|
||||||
|
| updated_at | timestamptz | default now() | 更新时间 |
|
||||||
|
|
||||||
|
索引建议:
|
||||||
|
- `(owner_user_id, created_at desc)`
|
||||||
|
- `(type, generated_at desc)`
|
||||||
|
- `(status)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.3 `analytics_report_metrics`(报表核心指标)
|
||||||
|
**用途**:`report-detail` 页“核心指标”网格(`coreMetrics`)。
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|
| ----------------- | ----------- | -------------------------- | ---------------------------------------------- |
|
||||||
|
| id | uuid | PK | 主键 |
|
||||||
|
| report_id | uuid | FK → analytics_reports(id) | 所属报表 |
|
||||||
|
| metric_key | text | NOT NULL | gmv/orders/conversion_rate/avg_order_amount 等 |
|
||||||
|
| metric_label | text | NOT NULL | 展示名称 |
|
||||||
|
| metric_value_num | numeric | nullable | 数值 |
|
||||||
|
| metric_value_text | text | nullable | 文本(如百分比已格式化) |
|
||||||
|
| format | text | NOT NULL, default 'number' | number/currency/percent |
|
||||||
|
| change_pct | numeric | default 0 | 环比/同比变化(页面用 `metric.change`) |
|
||||||
|
| icon | text | default '' | UI 图标(可选) |
|
||||||
|
| color | text | default '#3b82f6' | UI 颜色(可选) |
|
||||||
|
| created_at | timestamptz | default now() | 创建时间 |
|
||||||
|
|
||||||
|
索引建议:`(report_id, metric_key)` 唯一或普通索引(按需求)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.4 `analytics_report_rows`(报表明细表格/趋势表)
|
||||||
|
**用途**:`report-detail` 页“详细数据”表格与趋势(`tableData`、图表)。
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|
| ---------------- | ----------- | -------------------------- | ---------------------------------- |
|
||||||
|
| id | uuid | PK | 主键 |
|
||||||
|
| report_id | uuid | FK → analytics_reports(id) | 所属报表 |
|
||||||
|
| row_date | date | NOT NULL | 统计日期(或维度日期) |
|
||||||
|
| gmv | numeric | default 0 | GMV(元) |
|
||||||
|
| orders | integer | default 0 | 订单数 |
|
||||||
|
| users | integer | default 0 | 用户数(可选) |
|
||||||
|
| conversion | numeric | default 0 | 转化率(0-100)或(0-1)需统一约定 |
|
||||||
|
| avg_order_amount | numeric | default 0 | 客单价 |
|
||||||
|
| extra | jsonb | default '{}' | 扩展字段(用于自定义报表列) |
|
||||||
|
| created_at | timestamptz | default now() | 创建时间 |
|
||||||
|
|
||||||
|
索引建议:
|
||||||
|
- `(report_id, row_date)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.5 `analytics_insights`(洞察/建议)
|
||||||
|
**用途**:`profile` 今日洞察、`report-detail` 洞察列表、`insight-detail` 详情页。
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|
| ------------- | ----------- | ------------------------------------ | --------------------------------- |
|
||||||
|
| id | uuid | PK | 洞察 ID |
|
||||||
|
| report_id | uuid | FK → analytics_reports(id), nullable | 关联报表(可空:全局洞察) |
|
||||||
|
| owner_user_id | uuid | FK → users(id), nullable | 关联分析师(可空:系统生成) |
|
||||||
|
| type | text | NOT NULL | positive/warning/negative/info 等 |
|
||||||
|
| impact | text | NOT NULL, default 'medium' | high/medium/low |
|
||||||
|
| title | text | NOT NULL | 洞察标题 |
|
||||||
|
| content | text | NOT NULL | 洞察内容 |
|
||||||
|
| tags | text[] | default '{}' | 标签(可选) |
|
||||||
|
| created_at | timestamptz | default now() | 创建时间 |
|
||||||
|
|
||||||
|
索引建议:
|
||||||
|
- `(created_at desc)`
|
||||||
|
- `(report_id, created_at desc)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.6 `analytics_report_favorites`(收藏/快捷入口)
|
||||||
|
**用途**:`profile` 报表收藏管理。
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|
| ---------- | ----------- | -------------------------- | -------- |
|
||||||
|
| id | uuid | PK | 主键 |
|
||||||
|
| user_id | uuid | FK → users(id) | 用户 |
|
||||||
|
| report_id | uuid | FK → analytics_reports(id) | 报表 |
|
||||||
|
| created_at | timestamptz | default now() | 创建时间 |
|
||||||
|
|
||||||
|
唯一约束:`UNIQUE(user_id, report_id)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.7 `analytics_export_jobs`(导出任务/历史)
|
||||||
|
**用途**:`profile` 导出历史、`report-detail` 导出按钮触发。
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|
| ------------- | ----------- | -------------------------- | -------------------------- |
|
||||||
|
| id | uuid | PK | 导出任务 ID |
|
||||||
|
| user_id | uuid | FK → users(id) | 发起用户 |
|
||||||
|
| report_id | uuid | FK → analytics_reports(id) | 关联报表 |
|
||||||
|
| format | text | NOT NULL | csv/xlsx/pdf/json |
|
||||||
|
| status | text | NOT NULL, default 'queued' | queued/running/done/failed |
|
||||||
|
| file_path | text | nullable | Storage 路径(私有桶) |
|
||||||
|
| error_message | text | default '' | 失败原因 |
|
||||||
|
| created_at | timestamptz | default now() | 创建时间 |
|
||||||
|
| finished_at | timestamptz | nullable | 完成时间 |
|
||||||
|
|
||||||
|
索引建议:`(user_id, created_at desc)`、`(status)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 可选:视图与 RPC(推荐)
|
||||||
|
|
||||||
|
### 4.1 视图:`v_analytics_daily_overview`
|
||||||
|
**用途**:复用 `daily_statistics`,快速给首页 KPI 与趋势提供数据源。
|
||||||
|
- 粒度:按 `stat_date` + `merchant_id`(或全站 merchant_id 为空/特殊值)
|
||||||
|
> 是否需要全站维度:建议 **用 `merchant_id` 为空表示全站** 或单独建全站行。
|
||||||
|
|
||||||
|
### 4.2 RPC:`rpc_analytics_realtime_kpis`
|
||||||
|
**用途**:首页实时 KPI(对比昨日同刻)。
|
||||||
|
输入建议:
|
||||||
|
- `p_start timestamptz`:今日起始
|
||||||
|
- `p_end timestamptz`:今日结束(当前时间)
|
||||||
|
- `p_compare_start timestamptz`:昨日对应起始
|
||||||
|
- `p_compare_end timestamptz`:昨日对应结束
|
||||||
|
- `p_merchant_id uuid`(可选)
|
||||||
|
|
||||||
|
输出建议(单行):
|
||||||
|
- `gmv, gmv_growth, orders, order_growth, online_users, conversion_rate, conversion_growth`
|
||||||
|
|
||||||
|
> 前端当前 `index.uvue` 直接从 `orders` 表计算 KPI;后续可以改为 RPC 提升性能与一致性。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. RLS(权限矩阵建议)
|
||||||
|
|
||||||
|
> 核心原则:前端只用 anon key,所有访问靠 RLS。
|
||||||
|
|
||||||
|
### 5.1 表访问建议
|
||||||
|
- `analytics_user_preferences`:用户只能读写自己的 `user_id = auth.uid()`
|
||||||
|
- `analytics_reports`:
|
||||||
|
- 普通分析师:`owner_user_id = auth.uid()` 的报表可读写
|
||||||
|
- 共享报表:`status = 'shared'` 可读(可加 share 表细化)
|
||||||
|
- `analytics_report_metrics` / `analytics_report_rows` / `analytics_insights` / `analytics_export_jobs`:通过关联 `report_id` 或 `user_id` 做同权限继承
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 模拟数据(联调)策略
|
||||||
|
|
||||||
|
**目标**:让下列页面在无真实业务数据时也能跑通:
|
||||||
|
- `index`:订单与用户有数据 → KPI 与趋势能算出来
|
||||||
|
- `profile`:有“最近报表/洞察/导出任务”列表
|
||||||
|
- `report-detail`:存在 `analytics_reports` + `metrics` + `rows` + `insights`
|
||||||
|
|
||||||
|
### 6.1 推荐做法
|
||||||
|
- 插入少量 `users/merchants/products/orders/order_items`(过去 30 天)
|
||||||
|
- 同时插入 2-3 份 `analytics_reports`(不同 type/period)
|
||||||
|
- 每份报表插入 4-6 个核心指标 + 15-30 行 `analytics_report_rows`
|
||||||
|
- 插入 3-8 条 `analytics_insights`
|
||||||
|
- 插入 2-3 条 `analytics_export_jobs`
|
||||||
|
|
||||||
|
对应 SQL 脚本(位于 `pages/mall/analytics/test/` 目录):
|
||||||
|
- **Schema(表结构/索引/RLS/RPC)**:`pages/mall/analytics/test/ANALYTICS_DB_SCHEMA.sql`
|
||||||
|
- **测试数据(Seed)**:`pages/mall/analytics/test/ANALYTICS_TEST_SEED.sql`
|
||||||
|
- **分步执行脚本**:
|
||||||
|
- `01_create_tables.sql` - 创建表结构
|
||||||
|
- `02_insert_test_data.sql` - 插入测试数据
|
||||||
|
- `03_test_queries.sql` - 测试查询示例
|
||||||
|
- `04_cleanup.sql` - 清理测试数据
|
||||||
|
- **使用指南**:`pages/mall/analytics/test/README.md` 和 `SQL_USAGE_GUIDE.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 使用说明
|
||||||
|
|
||||||
|
### 7.1 部署步骤
|
||||||
|
|
||||||
|
1. **执行 Schema**(创建表、索引、RLS、RPC):
|
||||||
|
```sql
|
||||||
|
-- 在 Supabase SQL Editor 中执行
|
||||||
|
-- 方式一:直接复制粘贴 ANALYTICS_DB_SCHEMA.sql 内容
|
||||||
|
-- 方式二:使用分步脚本(推荐)
|
||||||
|
\i pages/mall/analytics/test/01_create_tables.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **插入测试数据**:
|
||||||
|
```sql
|
||||||
|
-- 方式一:直接复制粘贴 ANALYTICS_TEST_SEED.sql 内容
|
||||||
|
-- 方式二:使用分步脚本(推荐)
|
||||||
|
\i pages/mall/analytics/test/02_insert_test_data.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **验证查询**(可选):
|
||||||
|
```sql
|
||||||
|
\i pages/mall/analytics/test/03_test_queries.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **验证**:
|
||||||
|
- 检查表是否创建:`SELECT * FROM analytics_reports LIMIT 1;`
|
||||||
|
- 检查 RPC 是否可用:`SELECT * FROM rpc_analytics_realtime_kpis(...);`
|
||||||
|
|
||||||
|
### 7.2 前端调用示例(使用 `components/supadb`)
|
||||||
|
|
||||||
|
**查询报表列表**:
|
||||||
|
```vue
|
||||||
|
<supadb
|
||||||
|
collection="analytics_reports"
|
||||||
|
:filter="{ owner_user_id: currentUserId }"
|
||||||
|
orderby="created_at desc"
|
||||||
|
:pageSize="10"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
**调用 RPC 获取实时 KPI**:
|
||||||
|
```vue
|
||||||
|
<supadb
|
||||||
|
rpc="rpc_analytics_realtime_kpis"
|
||||||
|
:params="{
|
||||||
|
p_start: todayStart,
|
||||||
|
p_end: now,
|
||||||
|
p_compare_start: yesterdayStart,
|
||||||
|
p_compare_end: yesterdaySameTime,
|
||||||
|
p_merchant_id: null
|
||||||
|
}"
|
||||||
|
getone
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 反抄袭自证
|
||||||
|
|
||||||
|
### 8.1 仅参考资料(只含规范/文档/API)
|
||||||
|
- `pages/mall/mall.md`(项目需求与数据模型)
|
||||||
|
- `pages/mall/analytics/docs/ANALYTICS_PAGES_ANALYSIS.md`(页面与指标清单)
|
||||||
|
- `pages/mall/analytics/docs/ANALYTICS_UI_DESIGN.md`(页面与交互约定)
|
||||||
|
- Supabase/Postgres 官方文档(表/索引/RLS/RPC 概念)
|
||||||
|
|
||||||
|
### 8.2 未参考任何实现代码的声明
|
||||||
|
本文档的表结构与字段设计为**基于可观察页面字段与需求规格独立推导**的原创设计,未复制/改写任何第三方或原项目实现源码。
|
||||||
|
|
||||||
276
pages/mall/analytics/docs/ANALYTICS_DB_QUICK_START.md
Normal file
276
pages/mall/analytics/docs/ANALYTICS_DB_QUICK_START.md
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
# 数据分析模块数据库快速开始指南
|
||||||
|
|
||||||
|
> 本文档提供数据分析模块数据库的快速部署和使用指南。
|
||||||
|
|
||||||
|
## 📁 文件位置
|
||||||
|
|
||||||
|
所有 SQL 脚本和测试文件位于:`pages/mall/analytics/test/`
|
||||||
|
|
||||||
|
### 核心文件
|
||||||
|
|
||||||
|
| 文件 | 用途 | 执行顺序 |
|
||||||
|
| ------------------------- | -------------------------------- | ----------- |
|
||||||
|
| `ANALYTICS_DB_SCHEMA.sql` | 完整的表结构、索引、RLS、RPC | 1️⃣ |
|
||||||
|
| `ANALYTICS_TEST_SEED.sql` | 完整的测试数据(包含基础业务表) | 2️⃣ |
|
||||||
|
| `01_create_tables.sql` | 分步:创建表结构 | 1️⃣ |
|
||||||
|
| `02_insert_test_data.sql` | 分步:插入测试数据 | 2️⃣ |
|
||||||
|
| `03_test_queries.sql` | 验证查询示例 | 3️⃣(可选) |
|
||||||
|
| `04_cleanup.sql` | 清理测试数据 | ⚠️(需要时) |
|
||||||
|
|
||||||
|
### 文档文件
|
||||||
|
|
||||||
|
| 文件 | 说明 |
|
||||||
|
| ---------------------------------- | ---------------------------------- |
|
||||||
|
| `test/README.md` | 测试数据说明和使用方法 |
|
||||||
|
| `test/SQL_USAGE_GUIDE.md` | SQL 脚本执行详细指南 |
|
||||||
|
| `docs/ANALYTICS_DB_DESIGN.md` | 数据库设计文档(表结构、字段说明) |
|
||||||
|
| `docs/ANALYTICS_DB_QUICK_START.md` | 快速开始指南(本文档) |
|
||||||
|
|
||||||
|
## 🚀 快速部署(3步)
|
||||||
|
|
||||||
|
### 方式一:使用完整脚本(推荐)
|
||||||
|
|
||||||
|
1. **执行 Schema**
|
||||||
|
```sql
|
||||||
|
-- 在 Supabase SQL Editor 中执行
|
||||||
|
-- 复制粘贴 pages/mall/analytics/test/ANALYTICS_DB_SCHEMA.sql 的内容
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **插入测试数据**
|
||||||
|
```sql
|
||||||
|
-- 复制粘贴 pages/mall/analytics/test/ANALYTICS_TEST_SEED.sql 的内容
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **验证**
|
||||||
|
```sql
|
||||||
|
SELECT COUNT(*) FROM analytics_reports;
|
||||||
|
-- 应该返回 3
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方式二:使用分步脚本
|
||||||
|
|
||||||
|
1. **创建表结构**
|
||||||
|
```sql
|
||||||
|
\i pages/mall/analytics/test/01_create_tables.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **插入测试数据**
|
||||||
|
```sql
|
||||||
|
\i pages/mall/analytics/test/02_insert_test_data.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **验证数据(可选)**
|
||||||
|
```sql
|
||||||
|
\i pages/mall/analytics/test/03_test_queries.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 创建的表
|
||||||
|
|
||||||
|
### Analytics 专用表
|
||||||
|
|
||||||
|
- `analytics_user_preferences` - 分析师偏好设置
|
||||||
|
- `analytics_reports` - 报表定义
|
||||||
|
- `analytics_report_metrics` - 报表核心指标
|
||||||
|
- `analytics_report_rows` - 报表明细行(趋势数据)
|
||||||
|
- `analytics_insights` - 数据洞察
|
||||||
|
- `analytics_report_favorites` - 报表收藏
|
||||||
|
- `analytics_export_jobs` - 导出任务
|
||||||
|
|
||||||
|
### 基础业务表(如果不存在)
|
||||||
|
|
||||||
|
- `users` - 用户表
|
||||||
|
- `merchants` - 商家表
|
||||||
|
- `products` - 商品表
|
||||||
|
- `orders` - 订单表
|
||||||
|
- `order_items` - 订单商品表
|
||||||
|
- `daily_statistics` - 日常统计表
|
||||||
|
|
||||||
|
## 🔐 RLS(权限)策略
|
||||||
|
|
||||||
|
所有 `analytics_*` 表已启用 RLS,策略如下:
|
||||||
|
|
||||||
|
- **用户偏好**:用户只能访问自己的偏好设置
|
||||||
|
- **报表**:用户可访问自己创建的报表和共享报表(`status = 'shared'`)
|
||||||
|
- **报表数据**:通过 `report_id` 关联,继承报表的访问权限
|
||||||
|
- **导出任务**:用户只能访问自己的导出任务
|
||||||
|
|
||||||
|
## 🔧 RPC 函数
|
||||||
|
|
||||||
|
### `rpc_analytics_realtime_kpis`
|
||||||
|
|
||||||
|
计算实时 KPI(GMV、订单数、在线用户、转化率)及增长率。
|
||||||
|
|
||||||
|
**参数:**
|
||||||
|
- `p_start` - 今日起始时间
|
||||||
|
- `p_end` - 今日结束时间(当前时间)
|
||||||
|
- `p_compare_start` - 昨日对应起始时间
|
||||||
|
- `p_compare_end` - 昨日对应结束时间
|
||||||
|
- `p_merchant_id` - 商家ID(可选,NULL表示全站)
|
||||||
|
|
||||||
|
**返回:**
|
||||||
|
```sql
|
||||||
|
gmv, gmv_growth, orders, order_growth, online_users, conversion_rate, conversion_growth
|
||||||
|
```
|
||||||
|
|
||||||
|
**前端调用示例:**
|
||||||
|
```vue
|
||||||
|
<supadb
|
||||||
|
rpc="rpc_analytics_realtime_kpis"
|
||||||
|
:params="{
|
||||||
|
p_start: todayStart,
|
||||||
|
p_end: now,
|
||||||
|
p_compare_start: yesterdayStart,
|
||||||
|
p_compare_end: yesterdaySameTime,
|
||||||
|
p_merchant_id: null
|
||||||
|
}"
|
||||||
|
getone
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### `rpc_analytics_trend_data`
|
||||||
|
|
||||||
|
按日期聚合趋势数据(GMV、订单数、用户数)。
|
||||||
|
|
||||||
|
**参数:**
|
||||||
|
- `p_start_date` - 起始日期
|
||||||
|
- `p_end_date` - 结束日期
|
||||||
|
- `p_merchant_id` - 商家ID(可选)
|
||||||
|
|
||||||
|
**返回:**
|
||||||
|
```sql
|
||||||
|
date, gmv, orders, users
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 测试数据说明
|
||||||
|
|
||||||
|
执行 `ANALYTICS_TEST_SEED.sql` 后会创建:
|
||||||
|
|
||||||
|
- **2个测试分析师用户**
|
||||||
|
- **2个测试商家**
|
||||||
|
- **3个测试商品**
|
||||||
|
- **过去30天的测试订单**(每天5-15个订单)
|
||||||
|
- **3个示例报表**(销售报表、用户分析报表、商家销售报表)
|
||||||
|
- **报表核心指标**(GMV、订单量、转化率、客单价)
|
||||||
|
- **7天趋势数据**(为第一个报表)
|
||||||
|
- **3条数据洞察**
|
||||||
|
- **2个报表收藏**
|
||||||
|
- **3个导出任务记录**
|
||||||
|
- **过去30天的统计数据**(`daily_statistics` 表)
|
||||||
|
|
||||||
|
## 🎯 前端使用示例
|
||||||
|
|
||||||
|
### 查询报表列表
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<supadb
|
||||||
|
collection="analytics_reports"
|
||||||
|
:filter="{ owner_user_id: currentUserId }"
|
||||||
|
orderby="created_at desc"
|
||||||
|
:pageSize="10"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 查询报表详情
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<supadb
|
||||||
|
collection="analytics_reports"
|
||||||
|
:filter="{ id: reportId }"
|
||||||
|
getone
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 查询报表指标
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<supadb
|
||||||
|
collection="analytics_report_metrics"
|
||||||
|
:filter="{ report_id: reportId }"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 查询趋势数据
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<supadb
|
||||||
|
collection="analytics_report_rows"
|
||||||
|
:filter="{ report_id: reportId }"
|
||||||
|
orderby="row_date asc"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 调用 RPC 获取实时 KPI
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<supadb
|
||||||
|
rpc="rpc_analytics_realtime_kpis"
|
||||||
|
:params="{
|
||||||
|
p_start: todayStart.toISOString(),
|
||||||
|
p_end: now.toISOString(),
|
||||||
|
p_compare_start: yesterdayStart.toISOString(),
|
||||||
|
p_compare_end: yesterdaySameTime.toISOString(),
|
||||||
|
p_merchant_id: null
|
||||||
|
}"
|
||||||
|
getone
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚠️ 注意事项
|
||||||
|
|
||||||
|
1. **执行顺序**:必须先执行 Schema,再执行 Seed
|
||||||
|
2. **基础表依赖**:确保基础业务表(`users`、`merchants`、`orders` 等)已存在
|
||||||
|
3. **时间依赖**:测试数据使用 `NOW()`,每次执行时间戳会不同
|
||||||
|
4. **数据冲突**:脚本使用 `ON CONFLICT DO NOTHING`,可重复执行
|
||||||
|
5. **权限**:确保使用有足够权限的用户执行(如 `postgres`)
|
||||||
|
|
||||||
|
## 🔍 验证部署
|
||||||
|
|
||||||
|
执行以下查询验证部署是否成功:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 检查表是否创建
|
||||||
|
SELECT table_name
|
||||||
|
FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name LIKE 'analytics_%'
|
||||||
|
ORDER BY table_name;
|
||||||
|
|
||||||
|
-- 检查报表数量
|
||||||
|
SELECT COUNT(*) FROM analytics_reports;
|
||||||
|
-- 应该返回 3
|
||||||
|
|
||||||
|
-- 检查 RPC 函数是否存在
|
||||||
|
SELECT routine_name
|
||||||
|
FROM information_schema.routines
|
||||||
|
WHERE routine_schema = 'public'
|
||||||
|
AND routine_name LIKE 'rpc_analytics_%';
|
||||||
|
-- 应该看到 rpc_analytics_realtime_kpis 和 rpc_analytics_trend_data
|
||||||
|
|
||||||
|
-- 测试 RPC 函数
|
||||||
|
SELECT * FROM rpc_analytics_realtime_kpis(
|
||||||
|
DATE_TRUNC('day', NOW()),
|
||||||
|
NOW(),
|
||||||
|
DATE_TRUNC('day', NOW() - INTERVAL '1 day'),
|
||||||
|
NOW() - INTERVAL '1 day',
|
||||||
|
NULL
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 相关文档
|
||||||
|
|
||||||
|
- **数据库设计文档**:`pages/mall/analytics/docs/ANALYTICS_DB_DESIGN.md`
|
||||||
|
- **快速开始指南**:`pages/mall/analytics/docs/ANALYTICS_DB_QUICK_START.md`(本文档)
|
||||||
|
- **测试数据说明**:`pages/mall/analytics/test/README.md`
|
||||||
|
- **SQL 使用指南**:`pages/mall/analytics/test/SQL_USAGE_GUIDE.md`
|
||||||
|
- **项目需求文档**:`pages/mall/mall.md`(第2.6节、第10节)
|
||||||
|
|
||||||
|
## 🆘 问题排查
|
||||||
|
|
||||||
|
如果遇到问题,请检查:
|
||||||
|
|
||||||
|
1. **连接问题**:确认 Supabase 服务运行正常
|
||||||
|
2. **权限问题**:确认使用 `postgres` 用户或有足够权限
|
||||||
|
3. **表冲突**:如果表已存在,脚本不会报错(使用 `IF NOT EXISTS`)
|
||||||
|
4. **数据验证**:执行 `03_test_queries.sql` 验证数据
|
||||||
|
|
||||||
|
更多帮助请参考:`pages/mall/analytics/test/SQL_USAGE_GUIDE.md`
|
||||||
771
pages/mall/analytics/docs/ANALYTICS_PAGES_ANALYSIS.md
Normal file
771
pages/mall/analytics/docs/ANALYTICS_PAGES_ANALYSIS.md
Normal file
@@ -0,0 +1,771 @@
|
|||||||
|
# 数据分析模块页面分析文档
|
||||||
|
|
||||||
|
## 📋 文档说明
|
||||||
|
|
||||||
|
本文档基于项目文档(`pages/mall/mall.md`)、目录结构和页面配置,分析数据分析模块需要实现的页面清单。
|
||||||
|
|
||||||
|
**数据来源**:
|
||||||
|
- `pages/mall/mall.md` - 项目完整需求文档(第2.6节:数据分析端,第10节:数据统计分析)
|
||||||
|
- `pages/mall/pages-config.json` - 页面路由配置
|
||||||
|
- `docs/ANALYTICS_UI_DESIGN.md` - UI设计文档
|
||||||
|
|
||||||
|
**创建时间**: 2025-01-XX
|
||||||
|
**最后更新**: 2026-01-23(页面骨架创建完成)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 数据分析模块 URL / 路由访问(可直接复制)
|
||||||
|
|
||||||
|
### 1) 主页面 URL
|
||||||
|
|
||||||
|
- **数据分析中心首页**:`/pages/mall/analytics/index`
|
||||||
|
|
||||||
|
### 2) 子页面 URL(analytics 子包)
|
||||||
|
|
||||||
|
- **销售报表**:`/pages/mall/analytics/sales-report`
|
||||||
|
- **用户分析**:`/pages/mall/analytics/user-analysis`
|
||||||
|
- **商品洞察**:`/pages/mall/analytics/product-insights`
|
||||||
|
- **市场趋势**:`/pages/mall/analytics/market-trends`
|
||||||
|
- **自定义报表**:`/pages/mall/analytics/custom-report`
|
||||||
|
|
||||||
|
### 3) 详情页 URL(主包 pages 中配置)
|
||||||
|
|
||||||
|
- **报表详情**:`/pages/mall/analytics/report-detail`
|
||||||
|
- **数据分析详情**:`/pages/mall/analytics/data-detail`
|
||||||
|
- **数据洞察详情**:`/pages/mall/analytics/insight-detail`
|
||||||
|
|
||||||
|
### 4) 代码中如何访问(uni-app x)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// 进入数据分析中心首页(推荐:保留返回栈)
|
||||||
|
uni.navigateTo({ url: '/pages/mall/analytics/index' })
|
||||||
|
|
||||||
|
// 进入销售报表
|
||||||
|
uni.navigateTo({ url: '/pages/mall/analytics/sales-report' })
|
||||||
|
|
||||||
|
// 进入用户分析
|
||||||
|
uni.navigateTo({ url: '/pages/mall/analytics/user-analysis' })
|
||||||
|
```
|
||||||
|
|
||||||
|
> 注意:`switchTab` 只能用于 `tabBar.list` 里的页面;数据分析不在 tabBar 内,因此应使用 `navigateTo/redirectTo/reLaunch`。
|
||||||
|
|
||||||
|
## 一、已实现的页面
|
||||||
|
|
||||||
|
### 1.1 核心页面(已存在)
|
||||||
|
|
||||||
|
| 页面路径 | 文件状态 | 功能描述 | 配置状态 |
|
||||||
|
| ------------------------------------- | -------- | ---------------- | ------------ |
|
||||||
|
| `/pages/mall/analytics/index` | ✅ 已实现 | 数据分析中心首页 | ✅ 已配置 |
|
||||||
|
| `/pages/mall/analytics/profile` | ✅ 已实现 | 数据分析个人中心 | ⚠️ 未在配置中 |
|
||||||
|
| `/pages/mall/analytics/report-detail` | ✅ 已实现 | 报表详情页 | ✅ 已配置 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、需要实现的页面(根据配置和文档)
|
||||||
|
|
||||||
|
### 2.1 子包页面(subPackages 中已配置)
|
||||||
|
|
||||||
|
#### 2.1.1 销售报表 (`sales-report`)
|
||||||
|
- **路径**: `pages/mall/analytics/sales-report`
|
||||||
|
- **标题**: 销售报表
|
||||||
|
- **状态**: ❌ 未实现
|
||||||
|
- **功能需求**:
|
||||||
|
- 销售趋势分析(日/周/月/年)
|
||||||
|
- 销售数据统计(GMV、订单数、客单价)
|
||||||
|
- 商品销售排行
|
||||||
|
- 商家销售排行
|
||||||
|
- 销售地域分布
|
||||||
|
- 数据导出功能
|
||||||
|
|
||||||
|
#### 2.1.2 用户分析 (`user-analysis`)
|
||||||
|
- **路径**: `pages/mall/analytics/user-analysis`
|
||||||
|
- **标题**: 用户分析
|
||||||
|
- **状态**: ❌ 未实现
|
||||||
|
- **功能需求**:
|
||||||
|
- 用户增长趋势
|
||||||
|
- 用户活跃度分析
|
||||||
|
- 用户留存率
|
||||||
|
- 用户画像分析
|
||||||
|
- 用户行为路径
|
||||||
|
- 新老用户对比
|
||||||
|
|
||||||
|
#### 2.1.3 商品洞察 (`product-insights`)
|
||||||
|
- **路径**: `pages/mall/analytics/product-insights`
|
||||||
|
- **标题**: 商品洞察
|
||||||
|
- **状态**: ❌ 未实现
|
||||||
|
- **功能需求**:
|
||||||
|
- 商品销售分析
|
||||||
|
- 商品分类分析
|
||||||
|
- 热销商品排行
|
||||||
|
- 商品库存分析
|
||||||
|
- 商品价格趋势
|
||||||
|
- 商品评价分析
|
||||||
|
|
||||||
|
#### 2.1.4 市场趋势 (`market-trends`)
|
||||||
|
- **路径**: `pages/mall/analytics/market-trends`
|
||||||
|
- **标题**: 市场趋势
|
||||||
|
- **状态**: ❌ 未实现
|
||||||
|
- **功能需求**:
|
||||||
|
- 市场整体趋势
|
||||||
|
- 行业对比分析
|
||||||
|
- 季节性趋势
|
||||||
|
- 价格趋势分析
|
||||||
|
- 竞争分析
|
||||||
|
|
||||||
|
#### 2.1.5 优惠券效果分析 (`coupon-analysis`)
|
||||||
|
- **路径**: `pages/mall/analytics/coupon-analysis`
|
||||||
|
- **标题**: 优惠券效果分析
|
||||||
|
- **状态**: ❌ 未实现
|
||||||
|
- **功能需求**(基于 `mall.md` 第4节优惠券系统):
|
||||||
|
- 优惠券发放统计(8种券类型:满减券、折扣券、免运费券、新人券、会员券、品类券、商家券、限时券)
|
||||||
|
- 优惠券使用率分析
|
||||||
|
- 优惠券转化效果(GMV提升、订单增长)
|
||||||
|
- 优惠券ROI分析
|
||||||
|
- 发放渠道效果对比(主动领取、自动发放、活动赠送、邀请奖励、客服赠送、积分兑换)
|
||||||
|
- 优惠券到期提醒统计
|
||||||
|
- 优惠券使用趋势分析
|
||||||
|
|
||||||
|
#### 2.1.6 自定义报表 (`custom-report`)
|
||||||
|
- **路径**: `pages/mall/analytics/custom-report`
|
||||||
|
- **标题**: 自定义报表
|
||||||
|
- **状态**: ❌ 未实现
|
||||||
|
- **功能需求**:
|
||||||
|
- 报表创建/编辑
|
||||||
|
- 指标选择
|
||||||
|
- 时间维度选择
|
||||||
|
- 图表类型选择
|
||||||
|
- 报表保存/分享
|
||||||
|
- 报表模板管理
|
||||||
|
|
||||||
|
### 2.2 主包页面(pages 中已配置)
|
||||||
|
|
||||||
|
#### 2.2.1 数据分析详情 (`data-detail`)
|
||||||
|
- **路径**: `pages/mall/analytics/data-detail`
|
||||||
|
- **标题**: 数据分析详情
|
||||||
|
- **状态**: ❌ 未实现
|
||||||
|
- **功能需求**:
|
||||||
|
- 详细数据展示
|
||||||
|
- 数据钻取
|
||||||
|
- 数据对比
|
||||||
|
- 数据筛选
|
||||||
|
|
||||||
|
#### 2.2.2 数据洞察详情 (`insight-detail`)
|
||||||
|
- **路径**: `pages/mall/analytics/insight-detail`
|
||||||
|
- **标题**: 数据洞察详情
|
||||||
|
- **状态**: ❌ 未实现
|
||||||
|
- **功能需求**(基于 `mall.md` 第2.6节:预测分析和建议):
|
||||||
|
- 洞察详情展示
|
||||||
|
- 预测分析(销售预测、用户增长预测、库存预测)
|
||||||
|
- 智能建议(运营建议、商品建议、营销建议)
|
||||||
|
- 异常检测和预警
|
||||||
|
- 趋势预测可视化
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、页面功能模块分析
|
||||||
|
|
||||||
|
### 3.1 首页功能模块(index.uvue)
|
||||||
|
|
||||||
|
根据 `ANALYTICS_UI_DESIGN.md`,首页应包含以下模块:
|
||||||
|
|
||||||
|
1. **Header 区域**
|
||||||
|
- ✅ 页面标题
|
||||||
|
- ✅ 最后更新时间
|
||||||
|
- ✅ 刷新/导出按钮
|
||||||
|
- ✅ 更多操作按钮(搜索、通知、全屏、移动端、设置)
|
||||||
|
|
||||||
|
2. **实时大屏(KPI 卡片)**
|
||||||
|
- ✅ 实时 GMV
|
||||||
|
- ✅ 实时订单
|
||||||
|
- ✅ 在线用户
|
||||||
|
- ✅ 转化率
|
||||||
|
|
||||||
|
3. **时间筛选**
|
||||||
|
- ✅ 7天/30天/90天/1年切换
|
||||||
|
|
||||||
|
4. **核心趋势图表**
|
||||||
|
- ✅ GMV/订单数组合图(柱状+折线)
|
||||||
|
|
||||||
|
5. **用户结构分析**
|
||||||
|
- ✅ 用户构成环形图
|
||||||
|
|
||||||
|
6. **流量来源分析**
|
||||||
|
- ✅ 流量来源条形图
|
||||||
|
|
||||||
|
7. **商品/商家排行**
|
||||||
|
- ✅ 热销商品 TOP
|
||||||
|
- ✅ 商家排行 TOP
|
||||||
|
|
||||||
|
8. **配送效率**(基于 `mall.md` 第10.1节配送指标)
|
||||||
|
- ⚠️ 配送效率图表(待完善)
|
||||||
|
- 配送时效分析
|
||||||
|
- 配送费用统计
|
||||||
|
- 配送员效率分析
|
||||||
|
- 客户满意度统计
|
||||||
|
|
||||||
|
9. **优惠券效果分析**(基于 `mall.md` 第2.6节)
|
||||||
|
- ❌ 优惠券效果分析(待实现)
|
||||||
|
- 优惠券发放统计
|
||||||
|
- 优惠券使用率
|
||||||
|
- 优惠券转化效果
|
||||||
|
|
||||||
|
10. **预测分析和建议**(基于 `mall.md` 第2.6节)
|
||||||
|
- ❌ 预测分析(待实现)
|
||||||
|
- 销售预测
|
||||||
|
- 用户增长预测
|
||||||
|
- 智能运营建议
|
||||||
|
|
||||||
|
### 3.2 个人中心功能模块(profile.uvue)
|
||||||
|
|
||||||
|
- ✅ 用户信息展示
|
||||||
|
- ✅ 数据分析偏好设置
|
||||||
|
- ✅ 报表收藏管理
|
||||||
|
- ✅ 导出历史记录
|
||||||
|
|
||||||
|
### 3.3 报表详情功能模块(report-detail.uvue)
|
||||||
|
|
||||||
|
- ✅ 报表数据展示
|
||||||
|
- ✅ 图表展示
|
||||||
|
- ✅ 数据导出
|
||||||
|
- ✅ 报表分享
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、基于 `mall.md` 的统计指标需求
|
||||||
|
|
||||||
|
### 4.1 运营指标(`mall.md` 第10.1节)
|
||||||
|
|
||||||
|
根据项目需求文档,数据分析端需要统计以下**运营指标**:
|
||||||
|
|
||||||
|
- **GMV(成交总额)** - 核心业务指标
|
||||||
|
- **订单量和转化率** - 业务转化效率
|
||||||
|
- **用户活跃度** - 用户参与度
|
||||||
|
- **客单价** - 平均订单金额
|
||||||
|
- **复购率** - 用户忠诚度指标
|
||||||
|
|
||||||
|
### 4.2 商家指标(`mall.md` 第10.1节)
|
||||||
|
|
||||||
|
- **销售额和利润** - 商家经营状况
|
||||||
|
- **商品销量排行** - 热销商品分析
|
||||||
|
- **评价和服务质量** - 商家服务质量
|
||||||
|
- **库存周转率** - 库存管理效率
|
||||||
|
|
||||||
|
### 4.3 配送指标(`mall.md` 第10.1节)
|
||||||
|
|
||||||
|
- **配送时效** - 配送速度指标
|
||||||
|
- **配送费用** - 成本控制
|
||||||
|
- **配送员效率** - 人员效率分析
|
||||||
|
- **客户满意度** - 服务质量
|
||||||
|
|
||||||
|
### 4.4 统计数据模型(`mall.md` 第10.2节)
|
||||||
|
|
||||||
|
项目定义了 `daily_statistics` 表用于日常统计:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE daily_statistics (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
stat_date DATE NOT NULL,
|
||||||
|
merchant_id UUID REFERENCES merchants(id),
|
||||||
|
total_orders INTEGER DEFAULT 0,
|
||||||
|
total_amount DECIMAL(12,2) DEFAULT 0,
|
||||||
|
total_users INTEGER DEFAULT 0,
|
||||||
|
new_users INTEGER DEFAULT 0,
|
||||||
|
total_products INTEGER DEFAULT 0,
|
||||||
|
avg_order_amount DECIMAL(10,2) DEFAULT 0,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
UNIQUE(stat_date, merchant_id)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**数据查询需求**:
|
||||||
|
- 按日期聚合统计数据
|
||||||
|
- 按商家维度统计
|
||||||
|
- 支持时间范围查询
|
||||||
|
- 支持数据对比分析
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、页面实现优先级
|
||||||
|
|
||||||
|
### 5.1 高优先级(核心功能,基于 `mall.md` 第2.6节)
|
||||||
|
|
||||||
|
1. **销售报表** (`sales-report`) - 核心业务分析
|
||||||
|
- 对应需求:销售数据分析
|
||||||
|
- 包含指标:GMV、订单量、转化率、客单价
|
||||||
|
|
||||||
|
2. **用户分析** (`user-analysis`) - 用户运营分析
|
||||||
|
- 对应需求:用户行为分析
|
||||||
|
- 包含指标:用户活跃度、复购率、新用户增长
|
||||||
|
|
||||||
|
3. **商品洞察** (`product-insights`) - 商品运营分析
|
||||||
|
- 对应需求:商家表现分析(商品维度)
|
||||||
|
- 包含指标:商品销量排行、库存周转率
|
||||||
|
|
||||||
|
4. **配送效率分析** - 配送系统分析
|
||||||
|
- 对应需求:配送效率分析
|
||||||
|
- 包含指标:配送时效、配送费用、配送员效率、客户满意度
|
||||||
|
|
||||||
|
### 5.2 中优先级(增强功能)
|
||||||
|
|
||||||
|
5. **优惠券效果分析** (`coupon-analysis`) - 营销效果分析
|
||||||
|
- 对应需求:优惠券效果分析(`mall.md` 第2.6节)
|
||||||
|
- 包含:8种券类型分析、发放渠道效果、ROI分析
|
||||||
|
|
||||||
|
6. **市场趋势** (`market-trends`) - 市场分析
|
||||||
|
- 对应需求:市场整体趋势分析
|
||||||
|
|
||||||
|
7. **数据分析详情** (`data-detail`) - 数据钻取
|
||||||
|
- 支持所有报表页面的详细数据查看
|
||||||
|
|
||||||
|
8. **数据洞察详情** (`insight-detail`) - 智能分析
|
||||||
|
- 对应需求:预测分析和建议(`mall.md` 第2.6节)
|
||||||
|
|
||||||
|
### 5.3 低优先级(高级功能)
|
||||||
|
|
||||||
|
9. **自定义报表** (`custom-report`) - 高级定制功能
|
||||||
|
- 允许用户自定义报表配置
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、数据分析端核心功能(基于 `mall.md` 第2.6节)
|
||||||
|
|
||||||
|
根据项目需求文档,数据分析端(Analytics Dashboard)的目标用户是**运营和分析师**,需要实现以下核心功能:
|
||||||
|
|
||||||
|
### 6.1 实时数据大屏 ✅
|
||||||
|
- **状态**: 已实现(首页 KPI 卡片)
|
||||||
|
- **包含**: GMV、订单数、在线用户、转化率
|
||||||
|
|
||||||
|
### 6.2 销售数据分析 ⚠️
|
||||||
|
- **状态**: 部分实现(首页有核心趋势图)
|
||||||
|
- **待完善**: 需要独立的销售报表页面
|
||||||
|
- **包含**: 销售趋势、GMV分析、订单分析、客单价分析
|
||||||
|
|
||||||
|
### 6.3 用户行为分析 ⚠️
|
||||||
|
- **状态**: 部分实现(首页有用户结构分析)
|
||||||
|
- **待完善**: 需要独立的用户分析页面
|
||||||
|
- **包含**: 用户增长、活跃度、留存率、行为路径
|
||||||
|
|
||||||
|
### 6.4 商家表现分析 ⚠️
|
||||||
|
- **状态**: 部分实现(首页有商家排行)
|
||||||
|
- **待完善**: 需要独立的商家分析页面
|
||||||
|
- **包含**: 销售额、利润、商品排行、服务质量、库存周转率
|
||||||
|
|
||||||
|
### 6.5 配送效率分析 ⚠️
|
||||||
|
- **状态**: 部分实现(首页有配送效率图表占位)
|
||||||
|
- **待完善**: 需要完整的配送效率分析
|
||||||
|
- **包含**: 配送时效、配送费用、配送员效率、客户满意度
|
||||||
|
|
||||||
|
### 6.6 优惠券效果分析 ❌
|
||||||
|
- **状态**: 未实现
|
||||||
|
- **优先级**: 中优先级
|
||||||
|
- **包含**: 8种券类型效果、发放渠道效果、使用率、ROI分析
|
||||||
|
|
||||||
|
### 6.7 预测分析和建议 ❌
|
||||||
|
- **状态**: 未实现
|
||||||
|
- **优先级**: 中优先级
|
||||||
|
- **包含**: 销售预测、用户增长预测、智能运营建议、异常检测
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、页面依赖关系
|
||||||
|
|
||||||
|
```
|
||||||
|
index (首页)
|
||||||
|
├── sales-report (销售报表)
|
||||||
|
│ └── report-detail (报表详情)
|
||||||
|
├── user-analysis (用户分析)
|
||||||
|
│ └── data-detail (数据分析详情)
|
||||||
|
├── product-insights (商品洞察)
|
||||||
|
│ └── data-detail (数据分析详情)
|
||||||
|
├── coupon-analysis (优惠券效果分析) [新增,基于 mall.md]
|
||||||
|
│ └── report-detail (报表详情)
|
||||||
|
├── market-trends (市场趋势)
|
||||||
|
│ └── insight-detail (数据洞察详情)
|
||||||
|
└── custom-report (自定义报表)
|
||||||
|
└── report-detail (报表详情)
|
||||||
|
|
||||||
|
profile (个人中心)
|
||||||
|
└── 所有报表页面的收藏/历史记录入口
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、组件复用分析
|
||||||
|
|
||||||
|
### 6.1 已实现的组件
|
||||||
|
|
||||||
|
- ✅ `AnalyticsComboChart.uvue` - 组合图表(柱状+折线)
|
||||||
|
- ✅ `AnalyticsDonutChart.uvue` - 环形图
|
||||||
|
- ✅ `AnalyticsBarMini.uvue` - 迷你柱状图
|
||||||
|
- ✅ `ChartCard.uvue` - 图表卡片容器
|
||||||
|
- ✅ `KpiCard.uvue` - KPI 指标卡片
|
||||||
|
- ✅ `PeriodTabs.uvue` - 时间维度切换
|
||||||
|
|
||||||
|
### 6.2 需要新增的组件
|
||||||
|
|
||||||
|
- ❌ `SalesTrendChart.uvue` - 销售趋势图
|
||||||
|
- ❌ `UserGrowthChart.uvue` - 用户增长图
|
||||||
|
- ❌ `ProductRankingChart.uvue` - 商品排行图
|
||||||
|
- ❌ `RegionDistributionChart.uvue` - 地域分布图
|
||||||
|
- ❌ `CustomReportBuilder.uvue` - 自定义报表构建器
|
||||||
|
- ❌ `DataTable.uvue` - 数据表格组件
|
||||||
|
- ❌ `ExportDialog.uvue` - 导出对话框
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、数据接口需求
|
||||||
|
|
||||||
|
### 7.1 销售报表接口
|
||||||
|
|
||||||
|
- 销售趋势数据(按时间维度)
|
||||||
|
- 销售统计数据(GMV、订单数、客单价)
|
||||||
|
- 商品销售排行
|
||||||
|
- 商家销售排行
|
||||||
|
- 销售地域分布
|
||||||
|
|
||||||
|
### 7.2 用户分析接口
|
||||||
|
|
||||||
|
- 用户增长趋势
|
||||||
|
- 用户活跃度数据
|
||||||
|
- 用户留存率数据
|
||||||
|
- 用户画像数据
|
||||||
|
- 用户行为路径数据
|
||||||
|
|
||||||
|
### 7.3 商品洞察接口
|
||||||
|
|
||||||
|
- 商品销售数据
|
||||||
|
- 商品分类数据
|
||||||
|
- 热销商品数据
|
||||||
|
- 商品库存数据
|
||||||
|
- 商品价格趋势
|
||||||
|
|
||||||
|
### 7.4 市场趋势接口
|
||||||
|
|
||||||
|
- 市场整体趋势
|
||||||
|
- 行业对比数据
|
||||||
|
- 季节性趋势数据
|
||||||
|
- 价格趋势数据
|
||||||
|
|
||||||
|
### 7.5 优惠券效果分析接口(基于 `mall.md` 第4节)
|
||||||
|
|
||||||
|
- 优惠券发放统计(按类型、渠道)
|
||||||
|
- 优惠券使用率数据
|
||||||
|
- 优惠券转化效果(GMV提升、订单增长)
|
||||||
|
- 优惠券ROI数据
|
||||||
|
- 优惠券到期提醒统计
|
||||||
|
- 优惠券使用趋势
|
||||||
|
|
||||||
|
### 7.6 配送效率分析接口(基于 `mall.md` 第6节)
|
||||||
|
|
||||||
|
- 配送时效统计(平均配送时间、准时率)
|
||||||
|
- 配送费用统计(总费用、平均费用)
|
||||||
|
- 配送员效率数据(订单数、评分)
|
||||||
|
- 客户满意度数据(评价、投诉)
|
||||||
|
|
||||||
|
### 7.7 预测分析接口(基于 `mall.md` 第2.6节)
|
||||||
|
|
||||||
|
- 销售预测数据
|
||||||
|
- 用户增长预测
|
||||||
|
- 库存预测
|
||||||
|
- 异常检测数据
|
||||||
|
- 智能建议数据
|
||||||
|
|
||||||
|
### 7.8 自定义报表接口
|
||||||
|
|
||||||
|
- 报表模板列表
|
||||||
|
- 报表创建/更新
|
||||||
|
- 报表数据查询
|
||||||
|
- 报表保存/分享
|
||||||
|
|
||||||
|
### 7.9 日常统计数据接口(基于 `mall.md` 第10.2节)
|
||||||
|
|
||||||
|
- 按日期查询统计数据
|
||||||
|
- 按商家维度统计
|
||||||
|
- 时间范围聚合查询
|
||||||
|
- 数据对比分析
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十、实现建议
|
||||||
|
|
||||||
|
### 8.1 技术实现
|
||||||
|
|
||||||
|
1. **统一使用 Supabase 查询**
|
||||||
|
- 所有数据查询通过 `@/components/supadb/aksupainstance.uts`
|
||||||
|
- 使用 RLS (Row Level Security) 控制数据权限
|
||||||
|
|
||||||
|
2. **图表组件统一**
|
||||||
|
- 使用 `@/uni_modules/charts/EChartsView.vue`
|
||||||
|
- 封装统一的图表配置
|
||||||
|
|
||||||
|
3. **响应式设计**
|
||||||
|
- 使用 `flex-direction: row !important` 避免全局样式影响
|
||||||
|
- 使用媒体查询实现响应式布局
|
||||||
|
|
||||||
|
### 8.2 开发顺序建议
|
||||||
|
|
||||||
|
1. **第一阶段**:完善首页功能
|
||||||
|
- 完善配送效率图表
|
||||||
|
- 优化数据加载性能
|
||||||
|
|
||||||
|
2. **第二阶段**:实现核心报表页面
|
||||||
|
- 销售报表
|
||||||
|
- 用户分析
|
||||||
|
- 商品洞察
|
||||||
|
|
||||||
|
3. **第三阶段**:实现增强功能
|
||||||
|
- 市场趋势
|
||||||
|
- 数据分析详情
|
||||||
|
- 数据洞察详情
|
||||||
|
|
||||||
|
4. **第四阶段**:实现高级功能
|
||||||
|
- 自定义报表
|
||||||
|
|
||||||
|
### 8.3 代码规范
|
||||||
|
|
||||||
|
1. **文件命名**
|
||||||
|
- 页面文件:`*.uvue`
|
||||||
|
- 组件文件:`*.uvue`
|
||||||
|
- 样式统一使用 `px` 单位(避免 rpx + CSS var 问题)
|
||||||
|
|
||||||
|
2. **代码结构**
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<!-- 页面结构 -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts">
|
||||||
|
// 导入
|
||||||
|
// 类型定义
|
||||||
|
// 组件定义
|
||||||
|
// 数据定义
|
||||||
|
// 生命周期
|
||||||
|
// 方法定义
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 强制横排样式 */
|
||||||
|
/* 组件样式 */
|
||||||
|
/* 响应式样式 */
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十一、总结
|
||||||
|
|
||||||
|
### 11.1 页面统计
|
||||||
|
|
||||||
|
- **已实现(完整功能)**: 3 个页面
|
||||||
|
- `index.uvue` - 数据分析中心首页 ✅
|
||||||
|
- `profile.uvue` - 数据分析个人中心 ✅
|
||||||
|
- `report-detail.uvue` - 报表详情页 ✅
|
||||||
|
|
||||||
|
- **已创建骨架(待实现功能)**: 9 个页面
|
||||||
|
- `sales-report.uvue` - 销售报表 ⚠️
|
||||||
|
- `user-analysis.uvue` - 用户分析 ⚠️
|
||||||
|
- `product-insights.uvue` - 商品洞察 ⚠️
|
||||||
|
- `delivery-analysis.uvue` - 配送效率分析 ⚠️
|
||||||
|
- `coupon-analysis.uvue` - 优惠券效果分析 ⚠️
|
||||||
|
- `market-trends.uvue` - 市场趋势 ⚠️
|
||||||
|
- `insight-detail.uvue` - 数据洞察详情 ⚠️
|
||||||
|
- `data-detail.uvue` - 数据分析详情 ⚠️
|
||||||
|
- `custom-report.uvue` - 自定义报表 ⚠️
|
||||||
|
|
||||||
|
- **总计**: 12 个页面
|
||||||
|
|
||||||
|
### 11.2 完成度(基于 `mall.md` 需求)
|
||||||
|
|
||||||
|
- **实时数据大屏**: 90% ✅(首页已实现)
|
||||||
|
- **销售数据分析**: 50% ⚠️(页面骨架已创建,待实现数据查询)
|
||||||
|
- **用户行为分析**: 50% ⚠️(页面骨架已创建,待实现数据查询)
|
||||||
|
- **商家表现分析**: 50% ⚠️(商品洞察页面骨架已创建,待实现数据查询)
|
||||||
|
- **配送效率分析**: 50% ⚠️(页面骨架已创建,待实现数据查询)
|
||||||
|
- **优惠券效果分析**: 50% ⚠️(页面骨架已创建,待实现数据查询)
|
||||||
|
- **预测分析和建议**: 50% ⚠️(数据洞察详情页面骨架已创建,待实现预测算法)
|
||||||
|
|
||||||
|
### 11.3 页面骨架创建状态
|
||||||
|
|
||||||
|
**✅ 已完成页面骨架创建(2026-01-23)**:
|
||||||
|
|
||||||
|
所有9个待实现页面的骨架已创建完成,包含:
|
||||||
|
|
||||||
|
1. **统一的页面结构**
|
||||||
|
- 顶部栏(菜单图标 + 标题 + 操作按钮)
|
||||||
|
- 时间维度筛选(7天/30天/90天/1年)
|
||||||
|
- KPI 指标卡片(响应式布局)
|
||||||
|
- 图表展示区域
|
||||||
|
- 数据列表/排行
|
||||||
|
- 统一的样式规范
|
||||||
|
|
||||||
|
2. **技术实现框架**
|
||||||
|
- 使用 `flex-direction: row !important` 避免全局样式影响
|
||||||
|
- 响应式设计(宽屏/窄屏适配)
|
||||||
|
- 统一的组件导入(Supabase、EChartsView)
|
||||||
|
- 完整的类型定义(TypeScript/UTS)
|
||||||
|
- 方法框架(数据加载、图表构建、导出等)
|
||||||
|
|
||||||
|
3. **待实现功能(标记为 TODO)**
|
||||||
|
- 数据查询逻辑(Supabase 查询)
|
||||||
|
- 图表配置构建
|
||||||
|
- 数据导出功能
|
||||||
|
- 数据钻取逻辑
|
||||||
|
|
||||||
|
### 11.4 下一步行动(按优先级)
|
||||||
|
|
||||||
|
**第一阶段(核心功能 - 数据查询实现)**:
|
||||||
|
1. ✅ 完善首页的配送效率图表
|
||||||
|
2. ⚠️ 实现销售报表页面数据查询(GMV、订单量、转化率、客单价)
|
||||||
|
3. ⚠️ 实现用户分析页面数据查询(用户增长、活跃度、留存率、复购率)
|
||||||
|
4. ⚠️ 实现商品洞察页面数据查询(商品销量、库存周转率)
|
||||||
|
|
||||||
|
**第二阶段(增强功能 - 数据查询实现)**:
|
||||||
|
5. ⚠️ 实现配送效率分析页面数据查询(配送时效、费用、效率、满意度)
|
||||||
|
6. ⚠️ 实现优惠券效果分析页面数据查询(8种券类型、发放渠道、ROI)
|
||||||
|
7. ⚠️ 实现市场趋势页面数据查询
|
||||||
|
8. ⚠️ 实现数据洞察详情页面(预测分析算法、智能建议逻辑)
|
||||||
|
|
||||||
|
**第三阶段(高级功能 - 完整实现)**:
|
||||||
|
9. ⚠️ 实现自定义报表页面(报表创建、编辑、保存逻辑)
|
||||||
|
10. ⚠️ 实现数据分析详情页面(数据钻取、筛选、排序逻辑)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十二、参考文档
|
||||||
|
|
||||||
|
- **项目需求文档**: `pages/mall/mall.md`
|
||||||
|
- 第2.6节:数据分析端功能需求
|
||||||
|
- 第4节:优惠券系统详细设计
|
||||||
|
- 第6节:配送系统详细设计
|
||||||
|
- 第10节:数据统计分析(统计指标、数据模型)
|
||||||
|
|
||||||
|
- **UI设计文档**: `docs/ANALYTICS_UI_DESIGN.md`
|
||||||
|
- **页面配置**: `pages/mall/pages-config.json`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十三、页面骨架创建记录
|
||||||
|
|
||||||
|
### 13.1 骨架创建完成时间
|
||||||
|
|
||||||
|
**创建日期**: 2026-01-23
|
||||||
|
|
||||||
|
### 13.2 已创建的页面骨架清单
|
||||||
|
|
||||||
|
| 页面文件 | 页面标题 | 骨架状态 | 功能框架 | 数据查询 | 图表配置 |
|
||||||
|
| ------------------------ | -------------- | -------- | -------- | -------- | -------- |
|
||||||
|
| `sales-report.uvue` | 销售报表 | ✅ 已创建 | ✅ 完整 | ⚠️ 待实现 | ⚠️ 待实现 |
|
||||||
|
| `user-analysis.uvue` | 用户分析 | ✅ 已创建 | ✅ 完整 | ⚠️ 待实现 | ⚠️ 待实现 |
|
||||||
|
| `product-insights.uvue` | 商品洞察 | ✅ 已创建 | ✅ 完整 | ⚠️ 待实现 | ⚠️ 待实现 |
|
||||||
|
| `delivery-analysis.uvue` | 配送效率分析 | ✅ 已创建 | ✅ 完整 | ⚠️ 待实现 | ⚠️ 待实现 |
|
||||||
|
| `coupon-analysis.uvue` | 优惠券效果分析 | ✅ 已创建 | ✅ 完整 | ⚠️ 待实现 | ⚠️ 待实现 |
|
||||||
|
| `market-trends.uvue` | 市场趋势 | ✅ 已创建 | ✅ 完整 | ⚠️ 待实现 | ⚠️ 待实现 |
|
||||||
|
| `insight-detail.uvue` | 数据洞察详情 | ✅ 已创建 | ✅ 完整 | ⚠️ 待实现 | ⚠️ 待实现 |
|
||||||
|
| `data-detail.uvue` | 数据分析详情 | ✅ 已创建 | ✅ 完整 | ⚠️ 待实现 | ⚠️ 待实现 |
|
||||||
|
| `custom-report.uvue` | 自定义报表 | ✅ 已创建 | ✅ 完整 | ⚠️ 待实现 | ⚠️ 待实现 |
|
||||||
|
|
||||||
|
### 13.3 骨架包含的核心功能
|
||||||
|
|
||||||
|
每个页面骨架都包含以下标准功能模块:
|
||||||
|
|
||||||
|
1. **顶部栏(Topbar)**
|
||||||
|
- 菜单图标(☰)
|
||||||
|
- 页面标题和更新时间
|
||||||
|
- 刷新/导出按钮
|
||||||
|
- 更多操作菜单(响应式)
|
||||||
|
|
||||||
|
2. **时间维度筛选(Tabs)**
|
||||||
|
- 7天/30天/90天/1年切换
|
||||||
|
- 激活状态样式
|
||||||
|
- 点击切换数据
|
||||||
|
|
||||||
|
3. **KPI 指标卡片(KPI Grid)**
|
||||||
|
- 2列/4列响应式布局
|
||||||
|
- 指标数值显示
|
||||||
|
- 增长率对比
|
||||||
|
- 格式化显示(金额、百分比)
|
||||||
|
|
||||||
|
4. **图表展示区域(Chart Cards)**
|
||||||
|
- 图表容器(EChartsView)
|
||||||
|
- 图表标题和描述
|
||||||
|
- 统一的图表高度(360px)
|
||||||
|
|
||||||
|
5. **数据列表/排行(Rank Lists)**
|
||||||
|
- 排行序号显示
|
||||||
|
- 数据项信息
|
||||||
|
- 增长率标签(正负颜色区分)
|
||||||
|
|
||||||
|
6. **数据表格(Data Tables)**(部分页面)
|
||||||
|
- 表头(支持排序)
|
||||||
|
- 表格数据行
|
||||||
|
- 数据格式化
|
||||||
|
|
||||||
|
7. **筛选器(Filters)**(部分页面)
|
||||||
|
- 时间范围选择
|
||||||
|
- 数据维度选择
|
||||||
|
- 对比模式切换
|
||||||
|
|
||||||
|
### 13.4 技术实现规范
|
||||||
|
|
||||||
|
所有页面骨架遵循以下技术规范:
|
||||||
|
|
||||||
|
1. **样式规范**
|
||||||
|
- 使用 `flex-direction: row !important` 强制横排
|
||||||
|
- 统一使用 `px` 单位(避免 rpx + CSS var 问题)
|
||||||
|
- 响应式断点:960px
|
||||||
|
- 统一的颜色系统(#111、#f3f4f6、rgba(0,0,0,0.06) 等)
|
||||||
|
|
||||||
|
2. **组件导入**
|
||||||
|
- `@/components/supadb/aksupainstance.uts` - Supabase 查询
|
||||||
|
- `@/uni_modules/charts/EChartsView.vue` - 图表组件
|
||||||
|
- `@/components/analytics/AnalyticsComboChart.uvue` - 组合图表(部分页面)
|
||||||
|
|
||||||
|
3. **类型定义**
|
||||||
|
- 使用 UTS 类型系统
|
||||||
|
- 定义数据接口类型
|
||||||
|
- 定义配置项类型
|
||||||
|
|
||||||
|
4. **方法框架**
|
||||||
|
- `loadXxxData()` - 数据加载方法(待实现)
|
||||||
|
- `buildChartOptions()` - 图表配置构建(待实现)
|
||||||
|
- `refreshData()` - 刷新数据
|
||||||
|
- `exportReport()` - 导出报表
|
||||||
|
- `formatInt()` / `formatMoney()` / `formatPct()` - 数据格式化
|
||||||
|
|
||||||
|
### 13.5 待实现功能清单
|
||||||
|
|
||||||
|
每个页面需要实现以下核心功能:
|
||||||
|
|
||||||
|
#### 数据查询(Supabase)
|
||||||
|
- [ ] 根据时间维度查询数据
|
||||||
|
- [ ] 聚合计算(SUM、COUNT、AVG)
|
||||||
|
- [ ] 数据对比(同比、环比)
|
||||||
|
- [ ] 数据筛选(按商家、分类、地域等)
|
||||||
|
|
||||||
|
#### 图表配置(ECharts)
|
||||||
|
- [ ] 构建图表 option 配置
|
||||||
|
- [ ] 数据格式化(时间轴、数值格式化)
|
||||||
|
- [ ] 图表样式配置(颜色、字体、间距)
|
||||||
|
- [ ] 交互配置(tooltip、legend、zoom)
|
||||||
|
|
||||||
|
#### 数据导出
|
||||||
|
- [ ] Excel 导出
|
||||||
|
- [ ] PDF 导出
|
||||||
|
- [ ] 图片导出(图表截图)
|
||||||
|
- [ ] CSV 导出(数据表格)
|
||||||
|
|
||||||
|
#### 高级功能
|
||||||
|
- [ ] 数据钻取(点击数据项查看详情)
|
||||||
|
- [ ] 数据对比(多时间段对比)
|
||||||
|
- [ ] 预测分析(算法实现)
|
||||||
|
- [ ] 智能建议(规则引擎)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**文档版本**: v3.0
|
||||||
|
**状态**: ✅ 页面骨架已完成,📝 数据查询和图表配置待实现
|
||||||
|
**最后更新**: 2026-01-23(页面骨架创建完成)
|
||||||
269
pages/mall/analytics/docs/IMPLEMENTATION_STATUS.md
Normal file
269
pages/mall/analytics/docs/IMPLEMENTATION_STATUS.md
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
# 数据分析模块实现进度文档
|
||||||
|
|
||||||
|
## 📋 文档说明
|
||||||
|
|
||||||
|
本文档记录数据分析模块的实现进度、已知问题、bug修复情况和技术债务。
|
||||||
|
|
||||||
|
**文档位置**: `pages/mall/analytics/docs/IMPLEMENTATION_STATUS.md`
|
||||||
|
**最后更新**: 2026-01-23
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 页面实现状态
|
||||||
|
|
||||||
|
### 1. 核心页面(已完成)
|
||||||
|
|
||||||
|
| 页面路径 | 文件 | 状态 | 功能完成度 | 备注 |
|
||||||
|
| ------------------------------------- | -------------------- | -------- | ---------- | ------------------------------- |
|
||||||
|
| `/pages/mall/analytics/index` | `index.uvue` | ✅ 已实现 | 90% | 主仪表盘,KPI卡片、图表展示完成 |
|
||||||
|
| `/pages/mall/analytics/profile` | `profile.uvue` | ✅ 已实现 | 85% | 个人中心页面 |
|
||||||
|
| `/pages/mall/analytics/report-detail` | `report-detail.uvue` | ✅ 已实现 | 80% | 报表详情页 |
|
||||||
|
|
||||||
|
### 2. 分析页面(已完成)
|
||||||
|
|
||||||
|
| 页面路径 | 文件 | 状态 | 功能完成度 | 备注 |
|
||||||
|
| ----------------------------------------- | ------------------------ | -------- | ---------- | ---------------------------------- |
|
||||||
|
| `/pages/mall/analytics/sales-report` | `sales-report.uvue` | ✅ 已实现 | 85% | 销售报表,包含趋势、排行、地域分布 |
|
||||||
|
| `/pages/mall/analytics/user-analysis` | `user-analysis.uvue` | ✅ 已实现 | 85% | 用户分析,包含增长、活跃度、留存 |
|
||||||
|
| `/pages/mall/analytics/product-insights` | `product-insights.uvue` | ✅ 已实现 | 80% | 商品洞察 |
|
||||||
|
| `/pages/mall/analytics/delivery-analysis` | `delivery-analysis.uvue` | ✅ 已实现 | 80% | 配送效率分析 |
|
||||||
|
| `/pages/mall/analytics/coupon-analysis` | `coupon-analysis.uvue` | ✅ 已实现 | 80% | 优惠券效果分析 |
|
||||||
|
| `/pages/mall/analytics/market-trends` | `market-trends.uvue` | ✅ 已实现 | 75% | 市场趋势分析 |
|
||||||
|
| `/pages/mall/analytics/custom-report` | `custom-report.uvue` | ✅ 已实现 | 70% | 自定义报表创建/编辑 |
|
||||||
|
|
||||||
|
### 3. 详情页面(已完成)
|
||||||
|
|
||||||
|
| 页面路径 | 文件 | 状态 | 功能完成度 | 备注 |
|
||||||
|
| -------------------------------------- | --------------------- | -------- | ---------- | -------------- |
|
||||||
|
| `/pages/mall/analytics/data-detail` | `data-detail.uvue` | ✅ 已实现 | 75% | 数据分析详情页 |
|
||||||
|
| `/pages/mall/analytics/insight-detail` | `insight-detail.uvue` | ✅ 已实现 | 70% | 数据洞察详情页 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 已知问题与修复状态
|
||||||
|
|
||||||
|
### 1. 关键错误(Error - 需修复)
|
||||||
|
|
||||||
|
#### 1.1 Object Literal Type 错误
|
||||||
|
**位置**:
|
||||||
|
- `pages/mall/analytics/index.uvue:242`
|
||||||
|
- `pages/mall/analytics/sales-report.uvue:198`
|
||||||
|
- `pages/mall/analytics/user-analysis.uvue:180`
|
||||||
|
- `pages/mall/analytics/delivery-analysis.uvue:182`
|
||||||
|
|
||||||
|
**错误信息**: `direct declaration of Object Literal Type is not supported`
|
||||||
|
|
||||||
|
**原因**: uni-app x (UTS) 不支持在 `watch` 中直接使用对象字面量类型
|
||||||
|
|
||||||
|
**修复方案**:
|
||||||
|
```typescript
|
||||||
|
// ❌ 错误写法
|
||||||
|
watch: {
|
||||||
|
trafficSources: {
|
||||||
|
handler() { ... },
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 正确写法(使用函数形式)
|
||||||
|
watch: {
|
||||||
|
trafficSources(newVal: Array<TrafficItem>, oldVal: Array<TrafficItem>) {
|
||||||
|
this.buildChartOptions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**状态**: ⚠️ 待修复
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 1.2 组件事件绑定错误
|
||||||
|
**位置**:
|
||||||
|
- `pages/mall/analytics/data-detail.uvue:8`
|
||||||
|
- `pages/mall/analytics/custom-report.uvue:8`
|
||||||
|
- `pages/mall/analytics/insight-detail.uvue:8`
|
||||||
|
|
||||||
|
**错误信息**: `组件AnalyticsSidebarMenu不支持事件: 'update:visible'`
|
||||||
|
|
||||||
|
**原因**: uni-app x 不支持 Vue 3 的 `update:visible` 双向绑定语法
|
||||||
|
|
||||||
|
**修复方案**:
|
||||||
|
```vue
|
||||||
|
<!-- ❌ 错误写法 -->
|
||||||
|
<AnalyticsSidebarMenu
|
||||||
|
:visible="showSidebarMenu"
|
||||||
|
@update:visible="handleSidebarUpdate"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- ✅ 正确写法(使用普通事件) -->
|
||||||
|
<AnalyticsSidebarMenu
|
||||||
|
:visible="showSidebarMenu"
|
||||||
|
@visible-change="handleSidebarUpdate"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
**状态**: ✅ 已修复(2026-01-23)- 将所有页面的 `@update:visible` 改为 `@visible-change`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 1.3 view 组件不支持 title 属性
|
||||||
|
**位置**:
|
||||||
|
- `pages/mall/analytics/index.uvue:41,44,47,51,54,61`
|
||||||
|
|
||||||
|
**错误信息**: `组件view不支持属性: 'title'`
|
||||||
|
|
||||||
|
**原因**: `view` 组件不支持 `title` 属性,应使用 `text` 组件或移除该属性
|
||||||
|
|
||||||
|
**修复方案**:
|
||||||
|
```vue
|
||||||
|
<!-- ❌ 错误写法 -->
|
||||||
|
<view title="xxx">...</view>
|
||||||
|
|
||||||
|
<!-- ✅ 正确写法 -->
|
||||||
|
<view>
|
||||||
|
<text>xxx</text>
|
||||||
|
</view>
|
||||||
|
```
|
||||||
|
|
||||||
|
**状态**: ⚠️ 待修复
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 警告(Warning - 可忽略或后续优化)
|
||||||
|
|
||||||
|
#### 2.1 CSS 单位警告
|
||||||
|
**问题**: 使用了 `px`, `vh`, `%`, `calc()` 等 uni-app x 不支持的 CSS 单位/函数
|
||||||
|
|
||||||
|
**影响范围**: 所有页面文件
|
||||||
|
|
||||||
|
**说明**:
|
||||||
|
- uni-app x 主要支持 `rpx` 单位
|
||||||
|
- `px` 在 H5 平台可用,但会触发警告
|
||||||
|
- `vh`, `calc()` 等需要转换为 `rpx` 或使用条件编译
|
||||||
|
|
||||||
|
**处理建议**:
|
||||||
|
- 使用 `/* #ifdef H5 */` 条件编译包裹桌面端样式
|
||||||
|
- 移动端统一使用 `rpx`
|
||||||
|
|
||||||
|
**状态**: 📝 已记录,不影响功能
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2.2 CSS 伪类选择器警告
|
||||||
|
**问题**: 使用了 `:hover`, `:active` 等伪类选择器
|
||||||
|
|
||||||
|
**影响范围**: 多个页面
|
||||||
|
|
||||||
|
**说明**: uni-app x 在某些平台不支持 CSS 伪类,需要使用 JavaScript 处理交互状态
|
||||||
|
|
||||||
|
**处理建议**: 使用 `:class` 动态绑定替代伪类
|
||||||
|
|
||||||
|
**状态**: 📝 已记录,不影响功能
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2.3 未使用的 CSS 选择器
|
||||||
|
**问题**: 定义了但未使用的 CSS 类(如 `.active`, `.btn-hidden`)
|
||||||
|
|
||||||
|
**影响范围**: 多个页面
|
||||||
|
|
||||||
|
**说明**: 可能是预留的样式或历史遗留代码
|
||||||
|
|
||||||
|
**处理建议**: 清理未使用的样式,或添加注释说明用途
|
||||||
|
|
||||||
|
**状态**: 📝 已记录,不影响功能
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 组件实现状态
|
||||||
|
|
||||||
|
### 核心组件
|
||||||
|
|
||||||
|
| 组件路径 | 状态 | 功能完成度 | 备注 |
|
||||||
|
| ------------------------------------------------ | -------- | ---------- | ---------------------------- |
|
||||||
|
| `components/analytics/AnalyticsTopBar.uvue` | ✅ 已完成 | 95% | 顶部导航栏 |
|
||||||
|
| `components/analytics/AnalyticsSidebarMenu.uvue` | ✅ 已完成 | 90% | 侧边栏菜单(需修复事件绑定) |
|
||||||
|
| `components/analytics/KpiCard.uvue` | ✅ 已完成 | 100% | KPI 卡片组件 |
|
||||||
|
| `components/analytics/PeriodTabs.uvue` | ✅ 已完成 | 100% | 时间维度切换组件 |
|
||||||
|
| `components/analytics/ChartCard.uvue` | ✅ 已完成 | 100% | 图表卡片容器 |
|
||||||
|
|
||||||
|
### 图表组件
|
||||||
|
|
||||||
|
| 组件路径 | 状态 | 功能完成度 | 备注 |
|
||||||
|
| ----------------------------------------------- | -------- | ---------- | ------------------ |
|
||||||
|
| `components/analytics/charts/ComboBarLine.uvue` | ✅ 已完成 | 100% | 柱线组合图 |
|
||||||
|
| `components/analytics/charts/AreaLine.uvue` | ✅ 已完成 | 100% | 面积折线图 |
|
||||||
|
| `components/analytics/charts/DonutPie.uvue` | ✅ 已完成 | 100% | 环形饼图 |
|
||||||
|
| `components/analytics/AnalyticsComboChart.uvue` | ✅ 已完成 | 100% | 组合图表(自定义) |
|
||||||
|
| `components/analytics/AnalyticsDonutChart.uvue` | ✅ 已完成 | 100% | 环形图(自定义) |
|
||||||
|
| `components/analytics/AnalyticsBarMini.uvue` | ✅ 已完成 | 100% | 迷你条形图 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 技术债务
|
||||||
|
|
||||||
|
### 1. 数据获取
|
||||||
|
- [ ] 所有页面目前使用模拟数据(`mockTrend()`, `mockData()`)
|
||||||
|
- [ ] 需要接入 Supabase 真实数据查询
|
||||||
|
- [ ] 需要实现数据缓存和刷新机制
|
||||||
|
|
||||||
|
### 2. 性能优化
|
||||||
|
- [ ] ECharts 图表渲染性能优化(大数据量)
|
||||||
|
- [ ] 页面滚动性能优化
|
||||||
|
- [ ] 图片懒加载
|
||||||
|
|
||||||
|
### 3. 响应式设计
|
||||||
|
- [ ] 完善移动端适配(目前主要针对桌面端)
|
||||||
|
- [ ] 优化平板端显示效果
|
||||||
|
- [ ] 统一响应式断点
|
||||||
|
|
||||||
|
### 4. 错误处理
|
||||||
|
- [ ] 统一错误提示机制
|
||||||
|
- [ ] 网络请求失败重试
|
||||||
|
- [ ] 数据加载失败降级方案
|
||||||
|
|
||||||
|
### 5. 用户体验
|
||||||
|
- [ ] 加载状态提示(骨架屏)
|
||||||
|
- [ ] 空数据状态展示
|
||||||
|
- [ ] 操作反馈优化
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 修复计划
|
||||||
|
|
||||||
|
### 优先级 P0(阻塞功能)
|
||||||
|
1. ✅ 修复 Object Literal Type 错误(watch 语法)- 已完成
|
||||||
|
2. ✅ 修复组件事件绑定错误(update:visible → visible-change)- 已完成
|
||||||
|
3. ⚠️ 修复 view 组件 title 属性错误 - 待确认(可能是 lint 缓存问题)
|
||||||
|
|
||||||
|
### 优先级 P1(影响体验)
|
||||||
|
1. ⏳ 接入真实数据源(Supabase)
|
||||||
|
2. ⏳ 完善错误处理和加载状态
|
||||||
|
3. ⏳ 优化移动端响应式布局
|
||||||
|
|
||||||
|
### 优先级 P2(优化改进)
|
||||||
|
1. ⏳ 清理未使用的 CSS 样式
|
||||||
|
2. ⏳ 统一 CSS 单位(rpx)
|
||||||
|
3. ⏳ 添加单元测试
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 相关文档
|
||||||
|
|
||||||
|
- [页面分析文档](../ANALYTICS_PAGES_ANALYSIS.md) - 页面需求分析
|
||||||
|
- [UI 设计文档](../../../docs/ANALYTICS_UI_DESIGN.md) - UI 设计规范
|
||||||
|
- [数据库设计文档](../../../docs/ANALYTICS_DB_DESIGN.md) - 数据库表结构
|
||||||
|
- [测试文档](../test/README.md) - 测试用例和 SQL 脚本
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 更新日志
|
||||||
|
|
||||||
|
### 2026-01-23
|
||||||
|
- ✅ 创建实现进度文档
|
||||||
|
- ✅ 记录所有页面实现状态
|
||||||
|
- ✅ 列出已知问题和修复计划
|
||||||
|
- ✅ 修复:Object Literal Type 错误(watch 语法改为函数形式)
|
||||||
|
- ✅ 修复:组件事件绑定错误(所有页面的 `@update:visible` → `@visible-change`)
|
||||||
|
- ✅ 修复:AnalyticsSidebarMenu 组件事件定义(`emits` 更新为 `visible-change`)
|
||||||
|
- ⚠️ 待确认:view 组件 title 属性错误(可能是 lint 缓存,需重新检查)
|
||||||
138
pages/mall/analytics/docs/README.md
Normal file
138
pages/mall/analytics/docs/README.md
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
# 数据分析模块文档目录
|
||||||
|
|
||||||
|
> 本目录包含数据分析模块的所有相关文档。
|
||||||
|
|
||||||
|
## 📁 文档结构
|
||||||
|
|
||||||
|
```
|
||||||
|
pages/mall/analytics/docs/
|
||||||
|
├── README.md # 本文件(文档索引)
|
||||||
|
├── ANALYTICS_DB_DESIGN.md # 数据库设计文档
|
||||||
|
├── ANALYTICS_DB_QUICK_START.md # 数据库快速开始指南
|
||||||
|
├── ANALYTICS_PAGES_ANALYSIS.md # 页面分析文档
|
||||||
|
├── ANALYTICS_UI_DESIGN.md # UI 设计文档
|
||||||
|
└── IMPLEMENTATION_STATUS.md # 实现状态文档
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 文档说明
|
||||||
|
|
||||||
|
### 1. ANALYTICS_DB_DESIGN.md
|
||||||
|
|
||||||
|
**数据库设计文档** - 完整的数据表结构、字段说明、索引、RLS策略、RPC函数设计。
|
||||||
|
|
||||||
|
**内容包含:**
|
||||||
|
- 7个 Analytics 专用表的详细字段定义
|
||||||
|
- 索引建议
|
||||||
|
- RLS(Row Level Security)权限策略
|
||||||
|
- RPC 函数设计(实时KPI、趋势数据)
|
||||||
|
- 使用说明和前端调用示例
|
||||||
|
|
||||||
|
**适用场景:**
|
||||||
|
- 数据库架构设计
|
||||||
|
- 表结构参考
|
||||||
|
- 权限策略配置
|
||||||
|
- RPC 函数开发
|
||||||
|
|
||||||
|
### 2. ANALYTICS_DB_QUICK_START.md
|
||||||
|
|
||||||
|
**快速开始指南** - 数据库部署和使用的快速参考。
|
||||||
|
|
||||||
|
**内容包含:**
|
||||||
|
- 文件位置说明
|
||||||
|
- 快速部署步骤(3步)
|
||||||
|
- 创建的表列表
|
||||||
|
- RPC 函数使用说明
|
||||||
|
- 测试数据说明
|
||||||
|
- 前端使用示例
|
||||||
|
- 验证部署方法
|
||||||
|
- 问题排查
|
||||||
|
|
||||||
|
**适用场景:**
|
||||||
|
- 首次部署数据库
|
||||||
|
- 快速查找使用方法
|
||||||
|
- 问题排查参考
|
||||||
|
|
||||||
|
### 3. ANALYTICS_PAGES_ANALYSIS.md
|
||||||
|
|
||||||
|
**页面分析文档** - 数据分析模块所有页面的功能需求和分析。
|
||||||
|
|
||||||
|
**内容包含:**
|
||||||
|
- 已实现的页面清单
|
||||||
|
- 需要实现的页面清单
|
||||||
|
- 页面功能模块分析
|
||||||
|
- 统计指标需求
|
||||||
|
- 页面依赖关系
|
||||||
|
- 组件复用分析
|
||||||
|
- 数据接口需求
|
||||||
|
|
||||||
|
**适用场景:**
|
||||||
|
- 页面开发规划
|
||||||
|
- 功能需求参考
|
||||||
|
- 接口设计参考
|
||||||
|
|
||||||
|
### 4. ANALYTICS_UI_DESIGN.md
|
||||||
|
|
||||||
|
**UI 设计文档** - 数据分析页面的 UI 设计规范和实现说明。
|
||||||
|
|
||||||
|
**内容包含:**
|
||||||
|
- 页面访问 URL
|
||||||
|
- UI 设计规范
|
||||||
|
- 组件使用说明
|
||||||
|
- 响应式设计
|
||||||
|
- 交互设计
|
||||||
|
|
||||||
|
**适用场景:**
|
||||||
|
- UI 开发参考
|
||||||
|
- 组件使用指南
|
||||||
|
- 设计规范参考
|
||||||
|
|
||||||
|
### 5. IMPLEMENTATION_STATUS.md
|
||||||
|
|
||||||
|
**实现状态文档** - 记录各页面的实现进度和状态。
|
||||||
|
|
||||||
|
**内容包含:**
|
||||||
|
- 页面实现状态
|
||||||
|
- 功能完成度
|
||||||
|
- 待办事项
|
||||||
|
- 问题记录
|
||||||
|
|
||||||
|
**适用场景:**
|
||||||
|
- 项目进度跟踪
|
||||||
|
- 开发计划制定
|
||||||
|
|
||||||
|
## 🔗 相关资源
|
||||||
|
|
||||||
|
### SQL 脚本
|
||||||
|
|
||||||
|
所有 SQL 脚本位于:`pages/mall/analytics/test/`
|
||||||
|
|
||||||
|
- `ANALYTICS_DB_SCHEMA.sql` - 完整的表结构、索引、RLS、RPC
|
||||||
|
- `ANALYTICS_TEST_SEED.sql` - 完整的测试数据
|
||||||
|
- `01_create_tables.sql` - 分步:创建表结构
|
||||||
|
- `02_insert_test_data.sql` - 分步:插入测试数据
|
||||||
|
- `03_test_queries.sql` - 验证查询示例
|
||||||
|
- `04_cleanup.sql` - 清理测试数据
|
||||||
|
|
||||||
|
### 测试文档
|
||||||
|
|
||||||
|
测试相关文档位于:`pages/mall/analytics/test/`
|
||||||
|
|
||||||
|
- `README.md` - 测试数据说明和使用方法
|
||||||
|
- `SQL_USAGE_GUIDE.md` - SQL 脚本执行详细指南
|
||||||
|
|
||||||
|
### 项目文档
|
||||||
|
|
||||||
|
- `docs/ANALYTICS_PAGES_ANALYSIS.md` - 页面分析文档
|
||||||
|
- `docs/ANALYTICS_UI_DESIGN.md` - UI 设计文档
|
||||||
|
- `pages/mall/mall.md` - 项目需求文档(第2.6节:数据分析端,第10节:数据统计分析)
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
1. **阅读设计文档**:`ANALYTICS_DB_DESIGN.md`
|
||||||
|
2. **执行部署**:参考 `ANALYTICS_DB_QUICK_START.md`
|
||||||
|
3. **插入测试数据**:使用 `pages/mall/analytics/test/` 中的 SQL 脚本
|
||||||
|
|
||||||
|
## 📝 文档更新记录
|
||||||
|
|
||||||
|
- **2026-01-23** - 创建文档目录和索引
|
||||||
|
- **2026-01-23** - 添加数据库设计文档和快速开始指南
|
||||||
182
pages/mall/analytics/docs/URL_ACCESS.md
Normal file
182
pages/mall/analytics/docs/URL_ACCESS.md
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
# 数据分析模块 URL 访问文档
|
||||||
|
|
||||||
|
## 📋 文档说明
|
||||||
|
|
||||||
|
本文档提供数据分析模块所有页面的 URL 路径和访问方式,方便开发、测试和文档引用。
|
||||||
|
|
||||||
|
**文档位置**: `pages/mall/analytics/docs/URL_ACCESS.md`
|
||||||
|
**最后更新**: 2026-01-23
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗺️ 页面路由地图
|
||||||
|
|
||||||
|
### 1. 主页面
|
||||||
|
|
||||||
|
| 页面名称 | URL 路径 | 页面标题 | 配置位置 |
|
||||||
|
| ---------------- | ----------------------------- | ------------ | ------------------------------------------------ |
|
||||||
|
| 数据分析中心首页 | `/pages/mall/analytics/index` | 数据分析中心 | `subPackages` → `pages/mall/analytics` → `index` |
|
||||||
|
|
||||||
|
### 2. 分析页面(子包)
|
||||||
|
|
||||||
|
| 页面名称 | URL 路径 | 页面标题 | 配置位置 |
|
||||||
|
| -------------- | ----------------------------------------- | -------------- | ----------------------------------------------------------- |
|
||||||
|
| 销售报表 | `/pages/mall/analytics/sales-report` | 销售报表 | `subPackages` → `pages/mall/analytics` → `sales-report` |
|
||||||
|
| 用户分析 | `/pages/mall/analytics/user-analysis` | 用户分析 | `subPackages` → `pages/mall/analytics` → `user-analysis` |
|
||||||
|
| 商品洞察 | `/pages/mall/analytics/product-insights` | 商品洞察 | `subPackages` → `pages/mall/analytics` → `product-insights` |
|
||||||
|
| 市场趋势 | `/pages/mall/analytics/market-trends` | 市场趋势 | `subPackages` → `pages/mall/analytics` → `market-trends` |
|
||||||
|
| 自定义报表 | `/pages/mall/analytics/custom-report` | 自定义报表 | `subPackages` → `pages/mall/analytics` → `custom-report` |
|
||||||
|
| 优惠券效果分析 | `/pages/mall/analytics/coupon-analysis` | 优惠券效果分析 | ⚠️ 未在配置中 |
|
||||||
|
| 配送效率分析 | `/pages/mall/analytics/delivery-analysis` | 配送效率分析 | ⚠️ 未在配置中 |
|
||||||
|
|
||||||
|
### 3. 详情页面(主包)
|
||||||
|
|
||||||
|
| 页面名称 | URL 路径 | 页面标题 | 配置位置 |
|
||||||
|
| ------------ | -------------------------------------- | ------------ | ----------------------------------------------- |
|
||||||
|
| 报表详情 | `/pages/mall/analytics/report-detail` | 报表详情 | `pages` → `pages/mall/analytics/report-detail` |
|
||||||
|
| 数据分析详情 | `/pages/mall/analytics/data-detail` | 数据分析详情 | `pages` → `pages/mall/analytics/data-detail` |
|
||||||
|
| 数据洞察详情 | `/pages/mall/analytics/insight-detail` | 数据洞察详情 | `pages` → `pages/mall/analytics/insight-detail` |
|
||||||
|
|
||||||
|
### 4. 其他页面
|
||||||
|
|
||||||
|
| 页面名称 | URL 路径 | 页面标题 | 配置位置 |
|
||||||
|
| ---------------- | ------------------------------- | ---------------- | ------------ |
|
||||||
|
| 数据分析个人中心 | `/pages/mall/analytics/profile` | 数据分析个人中心 | ⚠️ 未在配置中 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💻 代码中如何访问
|
||||||
|
|
||||||
|
### 1. 基本跳转(推荐)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 方式一:使用 navigateTo(保留返回栈,可返回上一页)
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/mall/analytics/index',
|
||||||
|
success: () => {
|
||||||
|
console.log('跳转成功')
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
console.error('跳转失败:', err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 方式二:使用 redirectTo(替换当前页面,不保留返回栈)
|
||||||
|
uni.redirectTo({
|
||||||
|
url: '/pages/mall/analytics/index'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 方式三:使用 reLaunch(关闭所有页面,打开新页面)
|
||||||
|
uni.reLaunch({
|
||||||
|
url: '/pages/mall/analytics/index'
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 带参数跳转
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 跳转并传递查询参数
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/mall/analytics/index?period=30d&refresh=true'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 在目标页面接收参数(index.uvue 的 onLoad)
|
||||||
|
onLoad(options: any) {
|
||||||
|
const period = options.period || '7d'
|
||||||
|
const refresh = options.refresh === 'true'
|
||||||
|
// 使用参数...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 从其他模块跳转示例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 从管理后台跳转到数据分析中心
|
||||||
|
const goToAnalytics = () => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/mall/analytics/index'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从商城首页跳转到销售报表
|
||||||
|
const goToSalesReport = () => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/mall/analytics/sales-report'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从订单列表跳转到数据分析详情
|
||||||
|
const goToDataDetail = (orderId: string) => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/mall/analytics/data-detail?id=${orderId}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 侧边栏菜单导航
|
||||||
|
|
||||||
|
所有数据分析页面都集成了 `AnalyticsSidebarMenu` 组件,可以通过侧边栏菜单快速导航:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 侧边栏菜单会自动处理导航
|
||||||
|
// 菜单项配置在 components/analytics/AnalyticsSidebarMenu.uvue 中
|
||||||
|
const MENU_ITEMS = [
|
||||||
|
{ path: '/pages/mall/analytics/index', title: '数据分析中心', icon: '📊' },
|
||||||
|
{ path: '/pages/mall/analytics/sales-report', title: '销售报表', icon: '💰' },
|
||||||
|
// ...
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 注意事项
|
||||||
|
|
||||||
|
### 1. 路由配置
|
||||||
|
|
||||||
|
- **子包页面**(`sales-report`, `user-analysis` 等)必须在 `subPackages` 中配置
|
||||||
|
- **主包页面**(`report-detail`, `data-detail` 等)必须在主 `pages` 数组中配置
|
||||||
|
- 未在配置中的页面无法正常访问
|
||||||
|
|
||||||
|
### 2. tabBar 限制
|
||||||
|
|
||||||
|
数据分析模块**不在** `tabBar` 配置中,因此:
|
||||||
|
- ✅ 可以使用 `uni.navigateTo()`
|
||||||
|
- ✅ 可以使用 `uni.redirectTo()`
|
||||||
|
- ✅ 可以使用 `uni.reLaunch()`
|
||||||
|
- ❌ **不能**使用 `uni.switchTab()`(仅用于 tabBar 页面)
|
||||||
|
|
||||||
|
### 3. 导航栏样式
|
||||||
|
|
||||||
|
大部分数据分析页面使用**自定义导航栏**(`navigationStyle: "custom"`),需要:
|
||||||
|
- 使用 `AnalyticsTopBar` 组件作为顶部导航
|
||||||
|
- 处理状态栏高度适配
|
||||||
|
- 处理返回按钮逻辑
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📱 平台兼容性
|
||||||
|
|
||||||
|
| 平台 | 支持状态 | 备注 |
|
||||||
|
| ---------- | ---------- | ------------------------ |
|
||||||
|
| H5 | ✅ 完全支持 | 推荐使用,响应式布局优化 |
|
||||||
|
| 微信小程序 | ✅ 支持 | 需注意页面路径长度限制 |
|
||||||
|
| App | ✅ 支持 | 需注意原生导航栏样式 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 相关文档
|
||||||
|
|
||||||
|
- [实现进度文档](./IMPLEMENTATION_STATUS.md) - 页面实现状态和 bug 修复情况
|
||||||
|
- [页面分析文档](../../../docs/ANALYTICS_PAGES_ANALYSIS.md) - 页面需求分析
|
||||||
|
- [UI 设计文档](../../../docs/ANALYTICS_UI_DESIGN.md) - UI 设计规范
|
||||||
|
- [数据库设计文档](../../../docs/ANALYTICS_DB_DESIGN.md) - 数据库表结构
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 更新日志
|
||||||
|
|
||||||
|
### 2026-01-23
|
||||||
|
- ✅ 创建 URL 访问文档
|
||||||
|
- ✅ 列出所有页面路径和配置状态
|
||||||
|
- ✅ 提供代码访问示例
|
||||||
|
- ✅ 记录注意事项和平台兼容性
|
||||||
File diff suppressed because it is too large
Load Diff
659
pages/mall/analytics/insight-detail.uvue
Normal file
659
pages/mall/analytics/insight-detail.uvue
Normal file
@@ -0,0 +1,659 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page" @click="closeMoreMenu">
|
||||||
|
<!-- 固定顶部导航栏 -->
|
||||||
|
<AnalyticsTopBar
|
||||||
|
:title="'数据洞察详情'"
|
||||||
|
:lastUpdateTime="lastUpdateTime"
|
||||||
|
:sidebarVisible="showSidebarMenu"
|
||||||
|
@menu-click="handleMenu"
|
||||||
|
@refresh="refreshData"
|
||||||
|
@search="handleSearch"
|
||||||
|
@notification="handleNotification"
|
||||||
|
@fullscreen="handleFullscreen"
|
||||||
|
@mobile="handleMobile"
|
||||||
|
@dropdown="handleDropdown"
|
||||||
|
@settings="handleSettings"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<view class="page-layout">
|
||||||
|
<!-- 侧边栏菜单组件 -->
|
||||||
|
<AnalyticsSidebarMenu
|
||||||
|
:visible="showSidebarMenu"
|
||||||
|
:currentPath="currentPath"
|
||||||
|
@visible-change="handleSidebarUpdate"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 主内容区域 -->
|
||||||
|
<view class="main-content">
|
||||||
|
<view class="container">
|
||||||
|
<!-- 洞察详情(真实数据) -->
|
||||||
|
<view class="card card-full">
|
||||||
|
<view class="card-head">
|
||||||
|
<text class="card-title">{{ insight.title || '洞察详情' }}</text>
|
||||||
|
<view class="meta-row">
|
||||||
|
<text class="badge" :class="'badge-' + (insight.type || 'info')">{{ getInsightTypeText(insight.type) }}</text>
|
||||||
|
<text class="badge badge-impact" :class="'impact-' + (insight.impact || 'medium')">{{ getImpactText(insight.impact) }}</text>
|
||||||
|
<text class="meta-time" v-if="insight.created_at">{{ formatTime(insight.created_at) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="loading" class="state">
|
||||||
|
<text class="state-text">加载中...</text>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="errorMsg" class="state">
|
||||||
|
<text class="state-text">{{ errorMsg }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-else class="content">
|
||||||
|
<text class="content-text">{{ insight.content }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 关联报表(可选) -->
|
||||||
|
<view class="card" v-if="relatedReport.id">
|
||||||
|
<view class="card-head">
|
||||||
|
<text class="card-title">关联报表</text>
|
||||||
|
<text class="card-desc">{{ relatedReport.type }} · {{ relatedReport.period }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="report-row" @click="goToReportDetail">
|
||||||
|
<view class="report-icon">📄</view>
|
||||||
|
<view class="report-info">
|
||||||
|
<text class="report-title">{{ relatedReport.title }}</text>
|
||||||
|
<text class="report-time">{{ relatedReport.generated_at ? formatTime(relatedReport.generated_at) : '' }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="report-arrow">></text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 留白 -->
|
||||||
|
<view style="height: 24px;"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts">
|
||||||
|
import supa from '@/components/supadb/aksupainstance.uts'
|
||||||
|
import AnalyticsSidebarMenu from '@/components/analytics/AnalyticsSidebarMenu.uvue'
|
||||||
|
import AnalyticsTopBar from '@/components/analytics/AnalyticsTopBar.uvue'
|
||||||
|
|
||||||
|
type InsightDetail = {
|
||||||
|
id: string
|
||||||
|
report_id: string
|
||||||
|
type: string
|
||||||
|
impact: string
|
||||||
|
title: string
|
||||||
|
content: string
|
||||||
|
created_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type RelatedReport = {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
type: string
|
||||||
|
period: string
|
||||||
|
generated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
AnalyticsSidebarMenu,
|
||||||
|
AnalyticsTopBar
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
lastUpdateTime: '',
|
||||||
|
showMoreMenu: false,
|
||||||
|
showSidebarMenu: false,
|
||||||
|
currentPath: '/pages/mall/analytics/insight-detail',
|
||||||
|
insightId: '',
|
||||||
|
loading: false,
|
||||||
|
errorMsg: '',
|
||||||
|
insight: {
|
||||||
|
id: '',
|
||||||
|
report_id: '',
|
||||||
|
type: 'info',
|
||||||
|
impact: 'medium',
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
created_at: ''
|
||||||
|
} as InsightDetail,
|
||||||
|
relatedReport: {
|
||||||
|
id: '',
|
||||||
|
title: '',
|
||||||
|
type: '',
|
||||||
|
period: '',
|
||||||
|
generated_at: ''
|
||||||
|
} as RelatedReport
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onLoad(options: any) {
|
||||||
|
this.currentPath = '/pages/mall/analytics/insight-detail'
|
||||||
|
this.updateTime()
|
||||||
|
const insightId = (options.insightId || options.id) as string
|
||||||
|
if (!insightId) {
|
||||||
|
uni.showToast({ title: '缺少洞察ID', icon: 'none' })
|
||||||
|
setTimeout(() => uni.navigateBack(), 1500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.insightId = insightId
|
||||||
|
this.loadInsightDetail()
|
||||||
|
},
|
||||||
|
|
||||||
|
onShow() {
|
||||||
|
this.currentPath = '/pages/mall/analytics/insight-detail'
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async loadInsightDetail() {
|
||||||
|
try {
|
||||||
|
this.loading = true
|
||||||
|
this.errorMsg = ''
|
||||||
|
this.updateTime()
|
||||||
|
|
||||||
|
const res: any = await supa
|
||||||
|
.from('analytics_insights')
|
||||||
|
.select('id, report_id, type, impact, title, content, created_at')
|
||||||
|
.eq('id', this.insightId)
|
||||||
|
|
||||||
|
const rows: Array<any> = Array.isArray(res.data) ? (res.data as Array<any>) : []
|
||||||
|
if (rows.length === 0) {
|
||||||
|
this.errorMsg = '洞察不存在或无权限访问'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const it = rows[0]
|
||||||
|
this.insight = {
|
||||||
|
id: `${it.id}`,
|
||||||
|
report_id: `${it.report_id || ''}`,
|
||||||
|
type: `${it.type || 'info'}`,
|
||||||
|
impact: `${it.impact || 'medium'}`,
|
||||||
|
title: `${it.title || ''}`,
|
||||||
|
content: `${it.content || ''}`,
|
||||||
|
created_at: `${it.created_at || ''}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关联报表(可选)
|
||||||
|
this.relatedReport = { id: '', title: '', type: '', period: '', generated_at: '' } as RelatedReport
|
||||||
|
if (this.insight.report_id) {
|
||||||
|
const rRes: any = await supa
|
||||||
|
.from('analytics_reports')
|
||||||
|
.select('id, title, type, period, generated_at')
|
||||||
|
.eq('id', this.insight.report_id)
|
||||||
|
|
||||||
|
const rRows: Array<any> = Array.isArray(rRes.data) ? (rRes.data as Array<any>) : []
|
||||||
|
if (rRows.length > 0) {
|
||||||
|
const r = rRows[0]
|
||||||
|
this.relatedReport = {
|
||||||
|
id: `${r.id}`,
|
||||||
|
title: `${r.title}`,
|
||||||
|
type: `${r.type}`,
|
||||||
|
period: `${r.period}`,
|
||||||
|
generated_at: `${r.generated_at || ''}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('loadInsightDetail failed', e)
|
||||||
|
this.errorMsg = '加载失败,请稍后重试'
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshData() {
|
||||||
|
this.loadInsightDetail()
|
||||||
|
uni.showToast({ title: '已刷新', icon: 'success' })
|
||||||
|
},
|
||||||
|
|
||||||
|
exportReport() {
|
||||||
|
uni.showActionSheet({
|
||||||
|
itemList: ['导出Excel', '导出PDF', '导出图片'],
|
||||||
|
success: () => uni.showToast({ title: '导出成功', icon: 'success' })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
updateTime() {
|
||||||
|
const now = new Date()
|
||||||
|
const hh = now.getHours().toString().padStart(2, '0')
|
||||||
|
const mm = now.getMinutes().toString().padStart(2, '0')
|
||||||
|
this.lastUpdateTime = `${hh}:${mm}`
|
||||||
|
},
|
||||||
|
|
||||||
|
formatTime(timeStr: string): string {
|
||||||
|
if (!timeStr) return ''
|
||||||
|
return `${timeStr}`.replace('T', ' ').split('.')[0]
|
||||||
|
},
|
||||||
|
|
||||||
|
getInsightTypeText(type: string): string {
|
||||||
|
const t = `${type || 'info'}`
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
positive: '正向',
|
||||||
|
warning: '预警',
|
||||||
|
negative: '风险',
|
||||||
|
info: '信息'
|
||||||
|
}
|
||||||
|
return map[t] || '信息'
|
||||||
|
},
|
||||||
|
|
||||||
|
getImpactText(impact: string): string {
|
||||||
|
const impacts: Record<string, string> = {
|
||||||
|
high: '高影响',
|
||||||
|
medium: '中影响',
|
||||||
|
low: '低影响'
|
||||||
|
}
|
||||||
|
return impacts[impact || 'medium'] || '中影响'
|
||||||
|
},
|
||||||
|
|
||||||
|
goToReportDetail() {
|
||||||
|
if (!this.relatedReport.id) return
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/mall/analytics/report-detail?reportId=${this.relatedReport.id}`
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
handleMenu() {
|
||||||
|
this.showSidebarMenu = true
|
||||||
|
},
|
||||||
|
handleSidebarUpdate(visible: boolean) {
|
||||||
|
this.showSidebarMenu = visible
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleMoreMenu() {
|
||||||
|
this.showMoreMenu = !this.showMoreMenu
|
||||||
|
},
|
||||||
|
|
||||||
|
closeMoreMenu() {
|
||||||
|
this.showMoreMenu = false
|
||||||
|
},
|
||||||
|
handleSearch() {
|
||||||
|
uni.showToast({ title: '搜索', icon: 'none' })
|
||||||
|
},
|
||||||
|
handleNotification() {
|
||||||
|
uni.showToast({ title: '通知', icon: 'none' })
|
||||||
|
},
|
||||||
|
handleFullscreen() {
|
||||||
|
uni.showToast({ title: '全屏', icon: 'none' })
|
||||||
|
},
|
||||||
|
handleMobile() {
|
||||||
|
uni.showToast({ title: '移动端', icon: 'none' })
|
||||||
|
},
|
||||||
|
handleDropdown() {
|
||||||
|
uni.showToast({ title: '下拉菜单', icon: 'none' })
|
||||||
|
},
|
||||||
|
handleSettings() {
|
||||||
|
uni.showToast({ title: '设置', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f6f7fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面布局:宽屏时侧边栏+内容,窄屏时全屏内容 */
|
||||||
|
.page-layout {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-top: 64px; /* 为固定顶部导航栏留出空间 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 16px 16px 28px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部栏 */
|
||||||
|
.topbar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.06);
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-left {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon:active {
|
||||||
|
background: #e5e7eb;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon .icon {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #111;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111;
|
||||||
|
max-width: 420px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0,0,0,0.55);
|
||||||
|
max-width: 420px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn-icon:active {
|
||||||
|
background: #e5e7eb;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn-icon .icon {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn {
|
||||||
|
display: none;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn.active {
|
||||||
|
background: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn .icon {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 时间维度 tabs */
|
||||||
|
.tabs {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.06);
|
||||||
|
overflow-x: auto;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #111;
|
||||||
|
font-size: 13px;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
background: #111;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片 */
|
||||||
|
.card {
|
||||||
|
margin-top: 12px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.06);
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.04);
|
||||||
|
padding: 14px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-full {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-head {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0,0,0,0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-box {
|
||||||
|
width: 100%;
|
||||||
|
height: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 建议列表 */
|
||||||
|
.suggestion-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #f9fafb;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0,0,0,0.65);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 异常列表 */
|
||||||
|
.anomaly-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anomaly-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #fef2f2;
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 3px solid #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anomaly-level {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anomaly-level.critical {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anomaly-level.warning {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anomaly-level.info {
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anomaly-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anomaly-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anomaly-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0,0,0,0.65);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anomaly-time {
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(0,0,0,0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式 */
|
||||||
|
/* 响应式:窄屏时全屏显示 */
|
||||||
|
@media screen and (max-width: 959px) {
|
||||||
|
.page-layout {
|
||||||
|
flex-direction: column !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 960px) {
|
||||||
|
.title,
|
||||||
|
.subtitle {
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-right .btn-hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn {
|
||||||
|
display: flex !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
482
pages/mall/analytics/market-trends.uvue
Normal file
482
pages/mall/analytics/market-trends.uvue
Normal file
@@ -0,0 +1,482 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page" @click="closeMoreMenu">
|
||||||
|
<!-- 固定顶部导航栏 -->
|
||||||
|
<AnalyticsTopBar
|
||||||
|
:title="'市场趋势'"
|
||||||
|
:lastUpdateTime="lastUpdateTime"
|
||||||
|
:sidebarVisible="showSidebarMenu"
|
||||||
|
@menu-click="handleMenu"
|
||||||
|
@refresh="refreshData"
|
||||||
|
@search="handleSearch"
|
||||||
|
@notification="handleNotification"
|
||||||
|
@fullscreen="handleFullscreen"
|
||||||
|
@mobile="handleMobile"
|
||||||
|
@dropdown="handleDropdown"
|
||||||
|
@settings="handleSettings"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<view class="page-layout">
|
||||||
|
<!-- 侧边栏菜单组件 -->
|
||||||
|
<AnalyticsSidebarMenu
|
||||||
|
:visible="showSidebarMenu"
|
||||||
|
:currentPath="currentPath"
|
||||||
|
@visible-change="handleSidebarUpdate"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 主内容区域 -->
|
||||||
|
<view class="main-content">
|
||||||
|
<view class="container">
|
||||||
|
|
||||||
|
<!-- 时间维度筛选 -->
|
||||||
|
<view class="tabs">
|
||||||
|
<view
|
||||||
|
v-for="p in timePeriods"
|
||||||
|
:key="p.value"
|
||||||
|
class="tab"
|
||||||
|
:class="{ active: selectedPeriod === p.value }"
|
||||||
|
@click="selectPeriod(p.value)"
|
||||||
|
>
|
||||||
|
{{ p.label }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 市场整体趋势 -->
|
||||||
|
<view class="card card-full">
|
||||||
|
<view class="card-head">
|
||||||
|
<text class="card-title">市场整体趋势</text>
|
||||||
|
<text class="card-desc">{{ selectedPeriodText }} · GMV、订单数、用户数</text>
|
||||||
|
</view>
|
||||||
|
<EChartsView class="chart-box" :option="marketTrendOption" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 行业对比分析 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="card-head">
|
||||||
|
<text class="card-title">行业对比分析</text>
|
||||||
|
<text class="card-desc">不同行业表现对比</text>
|
||||||
|
</view>
|
||||||
|
<EChartsView class="chart-box" :option="industryCompareOption" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 季节性趋势 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="card-head">
|
||||||
|
<text class="card-title">季节性趋势</text>
|
||||||
|
<text class="card-desc">按月份统计</text>
|
||||||
|
</view>
|
||||||
|
<EChartsView class="chart-box" :option="seasonalTrendOption" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 价格趋势分析 -->
|
||||||
|
<view class="card card-full">
|
||||||
|
<view class="card-head">
|
||||||
|
<text class="card-title">价格趋势分析</text>
|
||||||
|
<text class="card-desc">平均价格变化趋势</text>
|
||||||
|
</view>
|
||||||
|
<EChartsView class="chart-box" :option="priceTrendOption" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 竞争分析 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="card-head">
|
||||||
|
<text class="card-title">竞争分析</text>
|
||||||
|
<text class="card-desc">市场份额、增长率对比</text>
|
||||||
|
</view>
|
||||||
|
<EChartsView class="chart-box" :option="competitionOption" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 留白 -->
|
||||||
|
<view style="height: 24px;"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts">
|
||||||
|
import supa from '@/components/supadb/aksupainstance.uts'
|
||||||
|
import AnalyticsSidebarMenu from '@/components/analytics/AnalyticsSidebarMenu.uvue'
|
||||||
|
import AnalyticsTopBar from '@/components/analytics/AnalyticsTopBar.uvue'
|
||||||
|
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
|
||||||
|
|
||||||
|
type TimePeriod = { value: string; label: string }
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
AnalyticsSidebarMenu,
|
||||||
|
AnalyticsTopBar,
|
||||||
|
EChartsView
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
lastUpdateTime: '',
|
||||||
|
selectedPeriod: '7d',
|
||||||
|
showMoreMenu: false,
|
||||||
|
showSidebarMenu: false,
|
||||||
|
currentPath: '/pages/mall/analytics/market-trends',
|
||||||
|
timePeriods: [
|
||||||
|
{ value: '7d', label: '7天' },
|
||||||
|
{ value: '30d', label: '30天' },
|
||||||
|
{ value: '90d', label: '90天' },
|
||||||
|
{ value: '1y', label: '1年' }
|
||||||
|
] as Array<TimePeriod>,
|
||||||
|
|
||||||
|
marketTrendOption: {} as any,
|
||||||
|
industryCompareOption: {} as any,
|
||||||
|
seasonalTrendOption: {} as any,
|
||||||
|
priceTrendOption: {} as any,
|
||||||
|
competitionOption: {} as any
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
selectedPeriodText(): string {
|
||||||
|
const p = this.timePeriods.find((t) => t.value === this.selectedPeriod)
|
||||||
|
return p ? p.label : '7天'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onLoad() {
|
||||||
|
this.currentPath = '/pages/mall/analytics/market-trends'
|
||||||
|
this.updateTime()
|
||||||
|
this.loadMarketData()
|
||||||
|
},
|
||||||
|
|
||||||
|
onShow() {
|
||||||
|
this.currentPath = '/pages/mall/analytics/market-trends'
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async loadMarketData() {
|
||||||
|
// TODO: 实现市场数据加载
|
||||||
|
this.updateTime()
|
||||||
|
this.buildChartOptions()
|
||||||
|
},
|
||||||
|
|
||||||
|
selectPeriod(p: string) {
|
||||||
|
this.selectedPeriod = p
|
||||||
|
this.loadMarketData()
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshData() {
|
||||||
|
this.loadMarketData()
|
||||||
|
uni.showToast({ title: '已刷新', icon: 'success' })
|
||||||
|
},
|
||||||
|
|
||||||
|
exportReport() {
|
||||||
|
uni.showActionSheet({
|
||||||
|
itemList: ['导出Excel', '导出PDF', '导出图片'],
|
||||||
|
success: () => uni.showToast({ title: '导出成功', icon: 'success' })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
updateTime() {
|
||||||
|
const now = new Date()
|
||||||
|
const hh = now.getHours().toString().padStart(2, '0')
|
||||||
|
const mm = now.getMinutes().toString().padStart(2, '0')
|
||||||
|
this.lastUpdateTime = `${hh}:${mm}`
|
||||||
|
},
|
||||||
|
|
||||||
|
buildChartOptions() {
|
||||||
|
// TODO: 构建图表配置
|
||||||
|
this.marketTrendOption = {}
|
||||||
|
this.industryCompareOption = {}
|
||||||
|
this.seasonalTrendOption = {}
|
||||||
|
this.priceTrendOption = {}
|
||||||
|
this.competitionOption = {}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleMenu() {
|
||||||
|
this.showSidebarMenu = true
|
||||||
|
},
|
||||||
|
handleSidebarUpdate(visible: boolean) {
|
||||||
|
this.showSidebarMenu = visible
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleMoreMenu() {
|
||||||
|
this.showMoreMenu = !this.showMoreMenu
|
||||||
|
},
|
||||||
|
|
||||||
|
closeMoreMenu() {
|
||||||
|
this.showMoreMenu = false
|
||||||
|
},
|
||||||
|
handleSearch() {
|
||||||
|
uni.showToast({ title: '搜索', icon: 'none' })
|
||||||
|
},
|
||||||
|
handleNotification() {
|
||||||
|
uni.showToast({ title: '通知', icon: 'none' })
|
||||||
|
},
|
||||||
|
handleFullscreen() {
|
||||||
|
uni.showToast({ title: '全屏', icon: 'none' })
|
||||||
|
},
|
||||||
|
handleMobile() {
|
||||||
|
uni.showToast({ title: '移动端', icon: 'none' })
|
||||||
|
},
|
||||||
|
handleDropdown() {
|
||||||
|
uni.showToast({ title: '下拉菜单', icon: 'none' })
|
||||||
|
},
|
||||||
|
handleSettings() {
|
||||||
|
uni.showToast({ title: '设置', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f6f7fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面布局:宽屏时侧边栏+内容,窄屏时全屏内容 */
|
||||||
|
.page-layout {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-top: 64px; /* 为固定顶部导航栏留出空间 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 16px 16px 28px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部栏 */
|
||||||
|
.topbar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.06);
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-left {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon:active {
|
||||||
|
background: #e5e7eb;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon .icon {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #111;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111;
|
||||||
|
max-width: 420px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0,0,0,0.55);
|
||||||
|
max-width: 420px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn-icon:active {
|
||||||
|
background: #e5e7eb;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn-icon .icon {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn {
|
||||||
|
display: none;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn.active {
|
||||||
|
background: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn .icon {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 时间维度 tabs */
|
||||||
|
.tabs {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.06);
|
||||||
|
overflow-x: auto;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #111;
|
||||||
|
font-size: 13px;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
background: #111;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片 */
|
||||||
|
.card {
|
||||||
|
margin-top: 12px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.06);
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.04);
|
||||||
|
padding: 14px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-full {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-head {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0,0,0,0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-box {
|
||||||
|
width: 100%;
|
||||||
|
height: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式:窄屏时全屏显示 */
|
||||||
|
@media screen and (max-width: 959px) {
|
||||||
|
.page-layout {
|
||||||
|
flex-direction: column !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式 */
|
||||||
|
@media screen and (max-width: 960px) {
|
||||||
|
.title,
|
||||||
|
.subtitle {
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-right .btn-hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn {
|
||||||
|
display: flex !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
646
pages/mall/analytics/product-insights.uvue
Normal file
646
pages/mall/analytics/product-insights.uvue
Normal file
@@ -0,0 +1,646 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page" @click="closeMoreMenu">
|
||||||
|
<!-- 固定顶部导航栏 -->
|
||||||
|
<AnalyticsTopBar
|
||||||
|
:title="'商品洞察'"
|
||||||
|
:lastUpdateTime="lastUpdateTime"
|
||||||
|
:sidebarVisible="showSidebarMenu"
|
||||||
|
@menu-click="handleMenu"
|
||||||
|
@refresh="refreshData"
|
||||||
|
@search="handleSearch"
|
||||||
|
@notification="handleNotification"
|
||||||
|
@fullscreen="handleFullscreen"
|
||||||
|
@mobile="handleMobile"
|
||||||
|
@dropdown="handleDropdown"
|
||||||
|
@settings="handleSettings"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<view class="page-layout">
|
||||||
|
<!-- 侧边栏菜单组件 -->
|
||||||
|
<AnalyticsSidebarMenu
|
||||||
|
:visible="showSidebarMenu"
|
||||||
|
:currentPath="currentPath"
|
||||||
|
@visible-change="handleSidebarUpdate"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 主内容区域 -->
|
||||||
|
<view class="main-content">
|
||||||
|
<view class="container">
|
||||||
|
|
||||||
|
<!-- 时间维度筛选 -->
|
||||||
|
<view class="tabs">
|
||||||
|
<view
|
||||||
|
v-for="p in timePeriods"
|
||||||
|
:key="p.value"
|
||||||
|
class="tab"
|
||||||
|
:class="{ active: selectedPeriod === p.value }"
|
||||||
|
@click="selectPeriod(p.value)"
|
||||||
|
>
|
||||||
|
{{ p.label }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- KPI 指标卡片 -->
|
||||||
|
<view class="kpi-grid">
|
||||||
|
<view class="kpi-card">
|
||||||
|
<text class="kpi-label">商品总数</text>
|
||||||
|
<text class="kpi-value">{{ formatInt(productData.total_products) }}</text>
|
||||||
|
<text class="kpi-meta">较上期:{{ formatPct(productData.product_growth) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="kpi-card">
|
||||||
|
<text class="kpi-label">热销商品</text>
|
||||||
|
<text class="kpi-value">{{ formatInt(productData.hot_products) }}</text>
|
||||||
|
<text class="kpi-meta">销量 > 100</text>
|
||||||
|
</view>
|
||||||
|
<view class="kpi-card">
|
||||||
|
<text class="kpi-label">库存周转率</text>
|
||||||
|
<text class="kpi-value">{{ formatPct(productData.turnover_rate) }}</text>
|
||||||
|
<text class="kpi-meta">较上期:{{ formatPct(productData.turnover_growth) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="kpi-card">
|
||||||
|
<text class="kpi-label">平均库存</text>
|
||||||
|
<text class="kpi-value">{{ formatInt(productData.avg_stock) }}</text>
|
||||||
|
<text class="kpi-meta">较上期:{{ formatPct(productData.stock_growth) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 商品销售分析 -->
|
||||||
|
<view class="card card-full">
|
||||||
|
<view class="card-head">
|
||||||
|
<text class="card-title">商品销售分析</text>
|
||||||
|
<text class="card-desc">{{ selectedPeriodText }} · 销售额趋势</text>
|
||||||
|
</view>
|
||||||
|
<EChartsView class="chart-box" :option="salesChartOption" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 商品分类分析 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="card-head">
|
||||||
|
<text class="card-title">商品分类分析</text>
|
||||||
|
<text class="card-desc">按分类统计销售额</text>
|
||||||
|
</view>
|
||||||
|
<EChartsView class="chart-box" :option="categoryChartOption" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 热销商品排行 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="card-head">
|
||||||
|
<text class="card-title">热销商品排行 TOP 10</text>
|
||||||
|
<text class="card-desc">按销量排序</text>
|
||||||
|
</view>
|
||||||
|
<view class="rank-list">
|
||||||
|
<view v-for="p in topProducts" :key="p.id" class="rank-item">
|
||||||
|
<text class="rank-no">{{ p.rank }}</text>
|
||||||
|
<text class="rank-name">{{ p.name }}</text>
|
||||||
|
<view class="rank-right">
|
||||||
|
<text class="rank-val">{{ p.sales }} 件</text>
|
||||||
|
<text class="chip" :class="p.growth >= 0 ? 'pos' : 'neg'">
|
||||||
|
{{ p.growth >= 0 ? '+' : '' }}{{ p.growth }}%
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 商品库存分析 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="card-head">
|
||||||
|
<text class="card-title">商品库存分析</text>
|
||||||
|
<text class="card-desc">库存分布情况</text>
|
||||||
|
</view>
|
||||||
|
<EChartsView class="chart-box" :option="stockChartOption" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 商品价格趋势 -->
|
||||||
|
<view class="card card-full">
|
||||||
|
<view class="card-head">
|
||||||
|
<text class="card-title">商品价格趋势</text>
|
||||||
|
<text class="card-desc">平均价格变化</text>
|
||||||
|
</view>
|
||||||
|
<EChartsView class="chart-box" :option="priceChartOption" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 商品评价分析 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="card-head">
|
||||||
|
<text class="card-title">商品评价分析</text>
|
||||||
|
<text class="card-desc">评分分布</text>
|
||||||
|
</view>
|
||||||
|
<EChartsView class="chart-box" :option="reviewChartOption" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 留白 -->
|
||||||
|
<view style="height: 24px;"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts">
|
||||||
|
import supa from '@/components/supadb/aksupainstance.uts'
|
||||||
|
import AnalyticsSidebarMenu from '@/components/analytics/AnalyticsSidebarMenu.uvue'
|
||||||
|
import AnalyticsTopBar from '@/components/analytics/AnalyticsTopBar.uvue'
|
||||||
|
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
|
||||||
|
|
||||||
|
type TimePeriod = { value: string; label: string }
|
||||||
|
type ProductData = {
|
||||||
|
total_products: number
|
||||||
|
product_growth: number
|
||||||
|
hot_products: number
|
||||||
|
turnover_rate: number
|
||||||
|
turnover_growth: number
|
||||||
|
avg_stock: number
|
||||||
|
stock_growth: number
|
||||||
|
}
|
||||||
|
type ProductRank = { id: string; rank: number; name: string; sales: number; growth: number }
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
AnalyticsSidebarMenu,
|
||||||
|
AnalyticsTopBar,
|
||||||
|
EChartsView
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
lastUpdateTime: '',
|
||||||
|
selectedPeriod: '7d',
|
||||||
|
showMoreMenu: false,
|
||||||
|
showSidebarMenu: false,
|
||||||
|
currentPath: '/pages/mall/analytics/product-insights',
|
||||||
|
timePeriods: [
|
||||||
|
{ value: '7d', label: '7天' },
|
||||||
|
{ value: '30d', label: '30天' },
|
||||||
|
{ value: '90d', label: '90天' },
|
||||||
|
{ value: '1y', label: '1年' }
|
||||||
|
] as Array<TimePeriod>,
|
||||||
|
|
||||||
|
productData: {
|
||||||
|
total_products: 0,
|
||||||
|
product_growth: 0,
|
||||||
|
hot_products: 0,
|
||||||
|
turnover_rate: 0,
|
||||||
|
turnover_growth: 0,
|
||||||
|
avg_stock: 0,
|
||||||
|
stock_growth: 0
|
||||||
|
} as ProductData,
|
||||||
|
|
||||||
|
topProducts: [] as Array<ProductRank>,
|
||||||
|
|
||||||
|
salesChartOption: {} as any,
|
||||||
|
categoryChartOption: {} as any,
|
||||||
|
stockChartOption: {} as any,
|
||||||
|
priceChartOption: {} as any,
|
||||||
|
reviewChartOption: {} as any
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
selectedPeriodText(): string {
|
||||||
|
const p = this.timePeriods.find((t) => t.value === this.selectedPeriod)
|
||||||
|
return p ? p.label : '7天'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onLoad() {
|
||||||
|
this.updateTime()
|
||||||
|
this.loadProductData()
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async loadProductData() {
|
||||||
|
// TODO: 实现商品数据加载
|
||||||
|
this.updateTime()
|
||||||
|
this.buildChartOptions()
|
||||||
|
},
|
||||||
|
|
||||||
|
selectPeriod(p: string) {
|
||||||
|
this.selectedPeriod = p
|
||||||
|
this.loadProductData()
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshData() {
|
||||||
|
this.loadProductData()
|
||||||
|
uni.showToast({ title: '已刷新', icon: 'success' })
|
||||||
|
},
|
||||||
|
|
||||||
|
exportReport() {
|
||||||
|
uni.showActionSheet({
|
||||||
|
itemList: ['导出Excel', '导出PDF', '导出图片'],
|
||||||
|
success: () => uni.showToast({ title: '导出成功', icon: 'success' })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
updateTime() {
|
||||||
|
const now = new Date()
|
||||||
|
const hh = now.getHours().toString().padStart(2, '0')
|
||||||
|
const mm = now.getMinutes().toString().padStart(2, '0')
|
||||||
|
this.lastUpdateTime = `${hh}:${mm}`
|
||||||
|
},
|
||||||
|
|
||||||
|
formatInt(n: number): string {
|
||||||
|
const v = isFinite(n) ? Math.round(n) : 0
|
||||||
|
if (v >= 10000) return (v / 10000).toFixed(1) + '万'
|
||||||
|
return v.toString()
|
||||||
|
},
|
||||||
|
|
||||||
|
formatPct(n: number): string {
|
||||||
|
const v = isFinite(n) ? n : 0
|
||||||
|
const sign = v > 0 ? '+' : ''
|
||||||
|
return `${sign}${v.toFixed(1)}%`
|
||||||
|
},
|
||||||
|
|
||||||
|
buildChartOptions() {
|
||||||
|
// TODO: 构建图表配置
|
||||||
|
this.salesChartOption = {}
|
||||||
|
this.categoryChartOption = {}
|
||||||
|
this.stockChartOption = {}
|
||||||
|
this.priceChartOption = {}
|
||||||
|
this.reviewChartOption = {}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleMenu() {
|
||||||
|
this.showSidebarMenu = true
|
||||||
|
},
|
||||||
|
handleSidebarUpdate(visible: boolean) {
|
||||||
|
this.showSidebarMenu = visible
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleMoreMenu() {
|
||||||
|
this.showMoreMenu = !this.showMoreMenu
|
||||||
|
},
|
||||||
|
|
||||||
|
closeMoreMenu() {
|
||||||
|
this.showMoreMenu = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f6f7fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面布局:宽屏时侧边栏+内容,窄屏时全屏内容 */
|
||||||
|
.page-layout {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-top: 64px; /* 为固定顶部导航栏留出空间 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 16px 16px 28px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部栏 */
|
||||||
|
.topbar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.06);
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-left {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon:active {
|
||||||
|
background: #e5e7eb;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon .icon {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #111;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111;
|
||||||
|
max-width: 420px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0,0,0,0.55);
|
||||||
|
max-width: 420px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn-icon:active {
|
||||||
|
background: #e5e7eb;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn-icon .icon {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn {
|
||||||
|
display: none;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn.active {
|
||||||
|
background: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn .icon {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 时间维度 tabs */
|
||||||
|
.tabs {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.06);
|
||||||
|
overflow-x: auto;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #111;
|
||||||
|
font-size: 13px;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
background: #111;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* KPI 网格 */
|
||||||
|
.kpi-grid {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.06);
|
||||||
|
padding: 14px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
flex: 1 1 calc(50% - 6px);
|
||||||
|
min-width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0,0,0,0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-value {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-meta {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0,0,0,0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片 */
|
||||||
|
.card {
|
||||||
|
margin-top: 12px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.06);
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.04);
|
||||||
|
padding: 14px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-full {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-head {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0,0,0,0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-box {
|
||||||
|
width: 100%;
|
||||||
|
height: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 排行列表 */
|
||||||
|
.rank-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px solid rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-no {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(0,0,0,0.06);
|
||||||
|
text-align: center;
|
||||||
|
line-height: 28px;
|
||||||
|
font-size: 12px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-name {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-val {
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(0,0,0,0.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip {
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip.pos {
|
||||||
|
background: rgba(34,197,94,0.12);
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip.neg {
|
||||||
|
background: rgba(239,68,68,0.12);
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式 */
|
||||||
|
@media screen and (min-width: 960px) {
|
||||||
|
.kpi-card {
|
||||||
|
flex: 1 1 calc(25% - 9px);
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 960px) {
|
||||||
|
.title,
|
||||||
|
.subtitle {
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-right .btn-hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn {
|
||||||
|
display: flex !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式:窄屏时全屏显示 */
|
||||||
|
@media screen and (max-width: 959px) {
|
||||||
|
.page-layout {
|
||||||
|
flex-direction: column !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,176 +1,209 @@
|
|||||||
<!-- 数据分析端 - 报表详情页 -->
|
<!-- 数据分析端 - 报表详情页 -->
|
||||||
<template>
|
<template>
|
||||||
<view class="report-detail-page">
|
<view class="page">
|
||||||
<!-- 报表头部 -->
|
<!-- 固定顶部导航栏 -->
|
||||||
<view class="report-header">
|
<AnalyticsTopBar
|
||||||
<view class="header-info">
|
:title="report.title || '报表详情'"
|
||||||
<text class="report-title">{{ report.title }}</text>
|
:lastUpdateTime="formatTime(report.generated_at)"
|
||||||
<view class="report-meta">
|
:sidebarVisible="showSidebarMenu"
|
||||||
<text class="meta-item">{{ getReportTypeText() }}</text>
|
@menu-click="handleMenu"
|
||||||
<text class="meta-item">{{ report.period }}</text>
|
@refresh="refreshReport"
|
||||||
<text class="meta-item">{{ formatTime(report.generated_at) }}</text>
|
@search="handleSearch"
|
||||||
</view>
|
@notification="handleNotification"
|
||||||
</view>
|
@fullscreen="handleFullscreen"
|
||||||
<view class="header-actions">
|
@mobile="handleMobile"
|
||||||
<button class="action-btn export" @click="exportReport">📊 导出</button>
|
@dropdown="handleDropdown"
|
||||||
<button class="action-btn refresh" @click="refreshReport">🔄 刷新</button>
|
@settings="handleSettings"
|
||||||
</view>
|
/>
|
||||||
</view>
|
|
||||||
|
<view class="page-layout">
|
||||||
<!-- 核心指标概览 -->
|
<!-- 侧边栏菜单组件 -->
|
||||||
<view class="metrics-overview">
|
<AnalyticsSidebarMenu
|
||||||
<view class="section-title">核心指标</view>
|
:visible="showSidebarMenu"
|
||||||
<view class="metrics-grid">
|
:currentPath="currentPath"
|
||||||
<view v-for="metric in coreMetrics" :key="metric.key" class="metric-card">
|
@visible-change="handleSidebarUpdate"
|
||||||
<view class="metric-icon" :style="{ backgroundColor: metric.color }">{{ metric.icon }}</view>
|
/>
|
||||||
<view class="metric-content">
|
|
||||||
<text class="metric-value">{{ formatMetricValue(metric.value, metric.format) }}</text>
|
<!-- 主内容区域 -->
|
||||||
<text class="metric-label">{{ metric.label }}</text>
|
<view class="main-content">
|
||||||
<view class="metric-change" :class="{ positive: metric.change > 0, negative: metric.change < 0 }">
|
<view class="report-detail-page">
|
||||||
<text class="change-icon">{{ metric.change > 0 ? '↗' : metric.change < 0 ? '↘' : '→' }}</text>
|
<!-- 报表头部 -->
|
||||||
<text class="change-value">{{ Math.abs(metric.change) }}%</text>
|
<view class="report-header">
|
||||||
|
<view class="header-info">
|
||||||
|
<text class="report-title">{{ report.title }}</text>
|
||||||
|
<view class="report-meta">
|
||||||
|
<text class="meta-item">{{ getReportTypeText() }}</text>
|
||||||
|
<text class="meta-item">{{ report.period }}</text>
|
||||||
|
<text class="meta-item">{{ formatTime(report.generated_at) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="header-actions">
|
||||||
|
<button class="action-btn export" @click="exportReport">📊 导出</button>
|
||||||
|
<button class="action-btn refresh" @click="refreshReport">🔄 刷新</button>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 趋势图表 -->
|
<!-- 核心指标概览 -->
|
||||||
<view class="chart-section">
|
<view class="metrics-overview">
|
||||||
<view class="section-header">
|
<view class="section-title">核心指标</view>
|
||||||
<text class="section-title">趋势分析</text>
|
<view class="metrics-grid">
|
||||||
<view class="chart-tabs">
|
<view v-for="metric in coreMetrics" :key="metric.key" class="metric-card">
|
||||||
<text v-for="tab in chartTabs" :key="tab.key"
|
<view class="metric-icon" :style="{ backgroundColor: metric.color }">{{ metric.icon }}</view>
|
||||||
class="chart-tab"
|
<view class="metric-content">
|
||||||
:class="{ active: activeChartTab === tab.key }"
|
<text class="metric-value">{{ formatMetricValue(metric.value, metric.format) }}</text>
|
||||||
@click="switchChartTab(tab.key)">{{ tab.label }}</text>
|
<text class="metric-label">{{ metric.label }}</text>
|
||||||
</view>
|
<view class="metric-change" :class="{ positive: metric.change > 0, negative: metric.change < 0 }">
|
||||||
</view>
|
<text class="change-icon">{{ metric.change > 0 ? '↗' : metric.change < 0 ? '↘' : '→' }}</text>
|
||||||
|
<text class="change-value">{{ Math.abs(metric.change) }}%</text>
|
||||||
<view class="chart-container">
|
</view>
|
||||||
<canvas class="chart-canvas" canvas-id="trendChart" @touchstart="onChartTouch" @touchmove="onChartTouch" @touchend="onChartTouch"></canvas>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
</view>
|
||||||
<view class="chart-legend">
|
</view>
|
||||||
<view v-for="legend in chartLegends" :key="legend.key" class="legend-item">
|
|
||||||
<view class="legend-color" :style="{ backgroundColor: legend.color }"></view>
|
|
||||||
<text class="legend-label">{{ legend.label }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 数据表格 -->
|
<!-- 趋势图表 -->
|
||||||
<view class="data-table">
|
<view class="chart-section">
|
||||||
<view class="section-title">详细数据</view>
|
<view class="section-header">
|
||||||
|
<text class="section-title">趋势分析</text>
|
||||||
<view class="table-filters">
|
<view class="chart-tabs">
|
||||||
<view class="filter-item">
|
<text v-for="tab in chartTabs" :key="tab.key"
|
||||||
<text class="filter-label">排序方式:</text>
|
class="chart-tab"
|
||||||
<picker :value="sortIndex" :range="sortOptions" @change="onSortChange">
|
:class="{ active: activeChartTab === tab.key }"
|
||||||
<text class="filter-value">{{ sortOptions[sortIndex] }}</text>
|
@click="switchChartTab(tab.key)">{{ tab.label }}</text>
|
||||||
</picker>
|
</view>
|
||||||
</view>
|
|
||||||
<view class="filter-item">
|
|
||||||
<text class="filter-label">显示条数:</text>
|
|
||||||
<picker :value="limitIndex" :range="limitOptions" @change="onLimitChange">
|
|
||||||
<text class="filter-value">{{ limitOptions[limitIndex] }}</text>
|
|
||||||
</picker>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="table-container">
|
|
||||||
<scroll-view scroll-x="true" class="table-scroll">
|
|
||||||
<view class="table">
|
|
||||||
<view class="table-header">
|
|
||||||
<text v-for="column in tableColumns" :key="column.key"
|
|
||||||
class="table-cell header-cell"
|
|
||||||
:style="{ width: column.width }">{{ column.title }}</text>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-for="(row, index) in tableData" :key="index" class="table-row">
|
<view class="chart-container">
|
||||||
<text v-for="column in tableColumns" :key="column.key"
|
<canvas class="chart-canvas" canvas-id="trendChart" @touchstart="onChartTouch" @touchmove="onChartTouch" @touchend="onChartTouch"></canvas>
|
||||||
class="table-cell data-cell"
|
</view>
|
||||||
:style="{ width: column.width }"
|
|
||||||
:class="{ number: column.type === 'number', currency: column.type === 'currency' }">
|
<view class="chart-legend">
|
||||||
{{ formatCellValue(row[column.key], column) }}
|
<view v-for="legend in chartLegends" :key="legend.key" class="legend-item">
|
||||||
</text>
|
<view class="legend-color" :style="{ backgroundColor: legend.color }"></view>
|
||||||
|
<text class="legend-label">{{ legend.label }}</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="table-pagination">
|
|
||||||
<button class="page-btn" :disabled="currentPage <= 1" @click="previousPage">上一页</button>
|
|
||||||
<text class="page-info">{{ currentPage }} / {{ totalPages }}</text>
|
|
||||||
<button class="page-btn" :disabled="currentPage >= totalPages" @click="nextPage">下一页</button>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 数据洞察 -->
|
<!-- 数据表格 -->
|
||||||
<view class="data-insights">
|
<view class="data-table">
|
||||||
<view class="section-title">数据洞察</view>
|
<view class="section-title">详细数据</view>
|
||||||
|
|
||||||
<view v-for="insight in dataInsights" :key="insight.id" class="insight-card">
|
<view class="table-filters">
|
||||||
<view class="insight-header">
|
<view class="filter-item">
|
||||||
<view class="insight-icon" :class="insight.type">{{ getInsightIcon(insight.type) }}</view>
|
<text class="filter-label">排序方式:</text>
|
||||||
<text class="insight-title">{{ insight.title }}</text>
|
<picker :value="sortIndex" :range="sortOptions" @change="onSortChange">
|
||||||
</view>
|
<text class="filter-value">{{ sortOptions[sortIndex] }}</text>
|
||||||
<text class="insight-content">{{ insight.content }}</text>
|
</picker>
|
||||||
<view class="insight-actions">
|
</view>
|
||||||
<text class="insight-impact" :class="insight.impact">{{ getImpactText(insight.impact) }}</text>
|
<view class="filter-item">
|
||||||
<text class="insight-action" @click="viewInsightDetail(insight)">查看详情 ></text>
|
<text class="filter-label">显示条数:</text>
|
||||||
</view>
|
<picker :value="limitIndex" :range="limitOptions" @change="onLimitChange">
|
||||||
</view>
|
<text class="filter-value">{{ limitOptions[limitIndex] }}</text>
|
||||||
</view>
|
</picker>
|
||||||
|
</view>
|
||||||
<!-- 报表配置 -->
|
</view>
|
||||||
<view class="report-config">
|
|
||||||
<view class="section-title">报表配置</view>
|
<view class="table-container">
|
||||||
|
<scroll-view scroll-x="true" class="table-scroll">
|
||||||
<view class="config-item">
|
<view class="table">
|
||||||
<text class="config-label">自动刷新</text>
|
<view class="table-header">
|
||||||
<switch :checked="autoRefresh" @change="toggleAutoRefresh" />
|
<text v-for="column in tableColumns" :key="column.key"
|
||||||
</view>
|
class="table-cell header-cell"
|
||||||
|
:style="{ width: column.width }">{{ column.title }}</text>
|
||||||
<view class="config-item">
|
</view>
|
||||||
<text class="config-label">刷新间隔</text>
|
|
||||||
<picker :value="intervalIndex" :range="intervalOptions" @change="onIntervalChange">
|
<view v-for="(row, index) in tableData" :key="index" class="table-row">
|
||||||
<text class="config-value">{{ intervalOptions[intervalIndex] }}</text>
|
<text v-for="column in tableColumns" :key="column.key"
|
||||||
</picker>
|
class="table-cell data-cell"
|
||||||
</view>
|
:style="{ width: column.width }"
|
||||||
|
:class="{ number: column.type === 'number', currency: column.type === 'currency' }">
|
||||||
<view class="config-item">
|
{{ formatCellValue(row[column.key], column) }}
|
||||||
<text class="config-label">邮件通知</text>
|
</text>
|
||||||
<switch :checked="emailNotify" @change="toggleEmailNotify" />
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
</scroll-view>
|
||||||
<view class="config-actions">
|
</view>
|
||||||
<button class="config-btn save" @click="saveConfig">保存配置</button>
|
|
||||||
<button class="config-btn reset" @click="resetConfig">重置配置</button>
|
<view class="table-pagination">
|
||||||
</view>
|
<button class="page-btn" :disabled="currentPage <= 1" @click="previousPage">上一页</button>
|
||||||
</view>
|
<text class="page-info">{{ currentPage }} / {{ totalPages }}</text>
|
||||||
|
<button class="page-btn" :disabled="currentPage >= totalPages" @click="nextPage">下一页</button>
|
||||||
<!-- 相关报表 -->
|
</view>
|
||||||
<view class="related-reports">
|
</view>
|
||||||
<view class="section-title">相关报表</view>
|
|
||||||
|
<!-- 数据洞察 -->
|
||||||
<view class="report-list">
|
<view class="data-insights">
|
||||||
<view v-for="relatedReport in relatedReports" :key="relatedReport.id"
|
<view class="section-title">数据洞察</view>
|
||||||
class="report-item" @click="viewRelatedReport(relatedReport)">
|
|
||||||
<view class="report-icon">📊</view>
|
<view v-for="insight in dataInsights" :key="insight.id" class="insight-card">
|
||||||
<view class="report-info">
|
<view class="insight-header">
|
||||||
<text class="report-name">{{ relatedReport.title }}</text>
|
<view class="insight-icon" :class="insight.type">{{ getInsightIcon(insight.type) }}</view>
|
||||||
<text class="report-desc">{{ relatedReport.description }}</text>
|
<text class="insight-title">{{ insight.title }}</text>
|
||||||
<text class="report-time">{{ formatTime(relatedReport.generated_at) }}</text>
|
</view>
|
||||||
|
<text class="insight-content">{{ insight.content }}</text>
|
||||||
|
<view class="insight-actions">
|
||||||
|
<text class="insight-impact" :class="insight.impact">{{ getImpactText(insight.impact) }}</text>
|
||||||
|
<text class="insight-action" @click="viewInsightDetail(insight)">查看详情 ></text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 报表配置 -->
|
||||||
|
<view class="report-config">
|
||||||
|
<view class="section-title">报表配置</view>
|
||||||
|
|
||||||
|
<view class="config-item">
|
||||||
|
<text class="config-label">自动刷新</text>
|
||||||
|
<switch :checked="autoRefresh" @change="toggleAutoRefresh" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="config-item">
|
||||||
|
<text class="config-label">刷新间隔</text>
|
||||||
|
<picker :value="intervalIndex" :range="intervalOptions" @change="onIntervalChange">
|
||||||
|
<text class="config-value">{{ intervalOptions[intervalIndex] }}</text>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="config-item">
|
||||||
|
<text class="config-label">邮件通知</text>
|
||||||
|
<switch :checked="emailNotify" @change="toggleEmailNotify" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="config-actions">
|
||||||
|
<button class="config-btn save" @click="saveConfig">保存配置</button>
|
||||||
|
<button class="config-btn reset" @click="resetConfig">重置配置</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 相关报表 -->
|
||||||
|
<view class="related-reports">
|
||||||
|
<view class="section-title">相关报表</view>
|
||||||
|
|
||||||
|
<view class="report-list">
|
||||||
|
<view v-for="relatedReport in relatedReports" :key="relatedReport.id"
|
||||||
|
class="report-item" @click="viewRelatedReport(relatedReport)">
|
||||||
|
<view class="report-icon">📊</view>
|
||||||
|
<view class="report-info">
|
||||||
|
<text class="report-name">{{ relatedReport.title }}</text>
|
||||||
|
<text class="report-desc">{{ relatedReport.description }}</text>
|
||||||
|
<text class="report-time">{{ formatTime(relatedReport.generated_at) }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="report-arrow">></text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<text class="report-arrow">></text>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="uts">
|
||||||
|
import AnalyticsSidebarMenu from '@/components/analytics/AnalyticsSidebarMenu.uvue'
|
||||||
|
import AnalyticsTopBar from '@/components/analytics/AnalyticsTopBar.uvue'
|
||||||
|
import supa from '@/components/supadb/aksupainstance.uts'
|
||||||
|
|
||||||
type ReportType = {
|
type ReportType = {
|
||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
@@ -217,8 +250,14 @@ type InsightType = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
AnalyticsSidebarMenu,
|
||||||
|
AnalyticsTopBar
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
showSidebarMenu: false,
|
||||||
|
currentPath: '/pages/mall/analytics/report-detail',
|
||||||
report: {
|
report: {
|
||||||
id: '',
|
id: '',
|
||||||
title: '',
|
title: '',
|
||||||
@@ -232,6 +271,7 @@ export default {
|
|||||||
activeChartTab: '',
|
activeChartTab: '',
|
||||||
chartLegends: [] as Array<ChartLegendType>,
|
chartLegends: [] as Array<ChartLegendType>,
|
||||||
tableColumns: [] as Array<TableColumnType>,
|
tableColumns: [] as Array<TableColumnType>,
|
||||||
|
allRows: [] as Array<any>,
|
||||||
tableData: [] as Array<any>,
|
tableData: [] as Array<any>,
|
||||||
dataInsights: [] as Array<InsightType>,
|
dataInsights: [] as Array<InsightType>,
|
||||||
relatedReports: [] as Array<ReportType>,
|
relatedReports: [] as Array<ReportType>,
|
||||||
@@ -261,147 +301,158 @@ export default {
|
|||||||
uni.navigateBack()
|
uni.navigateBack()
|
||||||
}, 1500)
|
}, 1500)
|
||||||
}
|
}
|
||||||
|
this.currentPath = '/pages/mall/analytics/report-detail'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onShow() {
|
||||||
|
this.currentPath = '/pages/mall/analytics/report-detail'
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
loadReportDetail(reportId: string) {
|
handleMenu() {
|
||||||
// 模拟加载报表详情数据
|
this.showSidebarMenu = true
|
||||||
this.report = {
|
},
|
||||||
id: reportId,
|
handleSidebarUpdate(visible: boolean) {
|
||||||
title: '销售业绩分析报表',
|
this.showSidebarMenu = visible
|
||||||
type: 'sales',
|
},
|
||||||
period: '2024年1月',
|
safeNumber(v: any): number {
|
||||||
generated_at: '2024-01-15T14:30:00',
|
const n = Number(v)
|
||||||
description: '详细分析1月份的销售业绩情况'
|
return isFinite(n) ? n : 0
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadReportDetail(reportId: string) {
|
||||||
|
try {
|
||||||
|
uni.showLoading({ title: '加载中...' })
|
||||||
|
|
||||||
|
// 1. 加载报表主体
|
||||||
|
const reportRes: any = await supa
|
||||||
|
.from('analytics_reports')
|
||||||
|
.select('id, title, type, period, generated_at, description')
|
||||||
|
.eq('id', reportId)
|
||||||
|
|
||||||
|
const reportRows: Array<any> = Array.isArray(reportRes.data) ? (reportRes.data as Array<any>) : []
|
||||||
|
if (reportRows.length === 0) {
|
||||||
|
uni.showToast({ title: '报表不存在', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const r = reportRows[0]
|
||||||
|
this.report = {
|
||||||
|
id: `${r.id}`,
|
||||||
|
title: `${r.title}`,
|
||||||
|
type: `${r.type}`,
|
||||||
|
period: `${r.period}`,
|
||||||
|
generated_at: `${r.generated_at}`,
|
||||||
|
description: `${r.description || ''}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 加载核心指标
|
||||||
|
const metricRes: any = await supa
|
||||||
|
.from('analytics_report_metrics')
|
||||||
|
.select('metric_key, metric_label, metric_value_num, format, icon, color, change_pct')
|
||||||
|
.eq('report_id', reportId)
|
||||||
|
|
||||||
|
const metricRows: Array<any> = Array.isArray(metricRes.data) ? (metricRes.data as Array<any>) : []
|
||||||
|
this.coreMetrics = metricRows.map((m: any) => ({
|
||||||
|
key: `${m.metric_key}`,
|
||||||
|
label: `${m.metric_label}`,
|
||||||
|
value: this.safeNumber(m.metric_value_num),
|
||||||
|
format: `${m.format || 'number'}`,
|
||||||
|
icon: `${m.icon || '📊'}`,
|
||||||
|
color: `${m.color || '#4caf50'}`,
|
||||||
|
change: this.safeNumber(m.change_pct)
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 3. 配置表头与排序选项(固定结构)
|
||||||
|
this.tableColumns = [
|
||||||
|
{ key: 'date', title: '日期', width: '120rpx', type: 'text' },
|
||||||
|
{ key: 'sales', title: '销售额', width: '120rpx', type: 'currency' },
|
||||||
|
{ key: 'orders', title: '订单数', width: '100rpx', type: 'number' },
|
||||||
|
{ key: 'users', title: '用户数', width: '100rpx', type: 'number' },
|
||||||
|
{ key: 'conversion', title: '转化率', width: '100rpx', type: 'percent' },
|
||||||
|
{ key: 'avg_value', title: '客单价', width: '120rpx', type: 'currency' }
|
||||||
|
]
|
||||||
|
this.sortOptions = ['按日期降序', '按销售额降序', '按订单数降序', '按转化率降序']
|
||||||
|
|
||||||
|
// 4. 加载明细行(趋势/表格)
|
||||||
|
const rowsRes: any = await supa
|
||||||
|
.from('analytics_report_rows')
|
||||||
|
.select('row_date, gmv, orders, users, conversion, avg_order_amount')
|
||||||
|
.eq('report_id', reportId)
|
||||||
|
.order('row_date', { ascending: true } as any)
|
||||||
|
|
||||||
|
const rows: Array<any> = Array.isArray(rowsRes.data) ? (rowsRes.data as Array<any>) : []
|
||||||
|
this.allRows = rows
|
||||||
|
this.currentPage = 1
|
||||||
|
this.updateTotalPages()
|
||||||
|
this.generateTableData()
|
||||||
|
|
||||||
|
// 5. 加载洞察
|
||||||
|
const insightRes: any = await supa
|
||||||
|
.from('analytics_insights')
|
||||||
|
.select('id, type, title, content, impact')
|
||||||
|
.eq('report_id', reportId)
|
||||||
|
.order('created_at', { ascending: false } as any)
|
||||||
|
|
||||||
|
const insRows: Array<any> = Array.isArray(insightRes.data) ? (insightRes.data as Array<any>) : []
|
||||||
|
this.dataInsights = insRows.map((it: any) => ({
|
||||||
|
id: `${it.id}`,
|
||||||
|
type: `${it.type || 'info'}`,
|
||||||
|
title: `${it.title}`,
|
||||||
|
content: `${it.content}`,
|
||||||
|
impact: `${it.impact || 'medium'}`
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 6. 相关报表(同类型最近报表)
|
||||||
|
const relatedRes: any = await supa
|
||||||
|
.from('analytics_reports')
|
||||||
|
.select('id, title, type, period, generated_at, description')
|
||||||
|
.eq('type', this.report.type)
|
||||||
|
.neq('id', reportId)
|
||||||
|
.order('generated_at', { ascending: false } as any)
|
||||||
|
.limit(3 as any)
|
||||||
|
|
||||||
|
const relRows: Array<any> = Array.isArray(relatedRes.data) ? (relatedRes.data as Array<any>) : []
|
||||||
|
this.relatedReports = relRows.map((it: any) => ({
|
||||||
|
id: `${it.id}`,
|
||||||
|
title: `${it.title}`,
|
||||||
|
type: `${it.type}`,
|
||||||
|
period: `${it.period}`,
|
||||||
|
generated_at: `${it.generated_at}`,
|
||||||
|
description: `${it.description || ''}`
|
||||||
|
}))
|
||||||
|
} catch (e) {
|
||||||
|
console.error('loadReportDetail failed', e)
|
||||||
|
uni.showToast({ title: '报表加载失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
uni.hideLoading()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.coreMetrics = [
|
|
||||||
{
|
|
||||||
key: 'total_sales',
|
|
||||||
label: '总销售额',
|
|
||||||
value: 1250000,
|
|
||||||
format: 'currency',
|
|
||||||
icon: '💰',
|
|
||||||
color: '#4caf50',
|
|
||||||
change: 15.6
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'order_count',
|
|
||||||
label: '订单数量',
|
|
||||||
value: 8650,
|
|
||||||
format: 'number',
|
|
||||||
icon: '📦',
|
|
||||||
color: '#2196f3',
|
|
||||||
change: 8.3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'avg_order_value',
|
|
||||||
label: '客单价',
|
|
||||||
value: 144.5,
|
|
||||||
format: 'currency',
|
|
||||||
icon: '🛍️',
|
|
||||||
color: '#ff9800',
|
|
||||||
change: 6.8
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'conversion_rate',
|
|
||||||
label: '转化率',
|
|
||||||
value: 3.2,
|
|
||||||
format: 'percent',
|
|
||||||
icon: '📈',
|
|
||||||
color: '#9c27b0',
|
|
||||||
change: -2.1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
this.chartTabs = [
|
|
||||||
{ key: 'sales', label: '销售额' },
|
|
||||||
{ key: 'orders', label: '订单量' },
|
|
||||||
{ key: 'users', label: '用户数' }
|
|
||||||
]
|
|
||||||
this.activeChartTab = 'sales'
|
|
||||||
|
|
||||||
this.chartLegends = [
|
|
||||||
{ key: 'current', label: '当期', color: '#2196f3' },
|
|
||||||
{ key: 'previous', label: '上期', color: '#ff9800' },
|
|
||||||
{ key: 'target', label: '目标', color: '#4caf50' }
|
|
||||||
]
|
|
||||||
|
|
||||||
this.tableColumns = [
|
|
||||||
{ key: 'date', title: '日期', width: '120rpx', type: 'text' },
|
|
||||||
{ key: 'sales', title: '销售额', width: '120rpx', type: 'currency' },
|
|
||||||
{ key: 'orders', title: '订单数', width: '100rpx', type: 'number' },
|
|
||||||
{ key: 'users', title: '用户数', width: '100rpx', type: 'number' },
|
|
||||||
{ key: 'conversion', title: '转化率', width: '100rpx', type: 'percent' },
|
|
||||||
{ key: 'avg_value', title: '客单价', width: '120rpx', type: 'currency' }
|
|
||||||
]
|
|
||||||
|
|
||||||
this.sortOptions = ['按日期降序', '按销售额降序', '按订单数降序', '按转化率降序']
|
|
||||||
|
|
||||||
// 模拟表格数据
|
|
||||||
this.generateTableData()
|
|
||||||
|
|
||||||
this.dataInsights = [
|
|
||||||
{
|
|
||||||
id: 'insight_001',
|
|
||||||
type: 'positive',
|
|
||||||
title: '销售额显著增长',
|
|
||||||
content: '相比上月,本月销售额增长15.6%,主要得益于新产品上线和营销活动效果显著。',
|
|
||||||
impact: 'high'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'insight_002',
|
|
||||||
type: 'warning',
|
|
||||||
title: '转化率轻微下降',
|
|
||||||
content: '转化率较上月下降2.1%,建议优化商品页面和购买流程,提升用户体验。',
|
|
||||||
impact: 'medium'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'insight_003',
|
|
||||||
title: '周末销售高峰',
|
|
||||||
content: '数据显示周末(周六、周日)的销售额占总销售额的35%,建议加强周末营销投入。',
|
|
||||||
impact: 'medium',
|
|
||||||
type: 'info'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
this.relatedReports = [
|
|
||||||
{
|
|
||||||
id: 'report_002',
|
|
||||||
title: '用户行为分析报表',
|
|
||||||
type: 'user',
|
|
||||||
period: '2024年1月',
|
|
||||||
generated_at: '2024-01-15T10:00:00',
|
|
||||||
description: '分析用户浏览、搜索、购买行为'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'report_003',
|
|
||||||
title: '商品销售排行报表',
|
|
||||||
type: 'product',
|
|
||||||
period: '2024年1月',
|
|
||||||
generated_at: '2024-01-15T09:30:00',
|
|
||||||
description: '商品销售排行和库存分析'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
this.totalPages = Math.ceil(31 / parseInt(this.limitOptions[this.limitIndex]))
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateTotalPages() {
|
||||||
|
const total = this.allRows.length
|
||||||
|
const limit = parseInt(this.limitOptions[this.limitIndex])
|
||||||
|
this.totalPages = total > 0 ? Math.ceil(total / limit) : 1
|
||||||
|
},
|
||||||
|
|
||||||
generateTableData() {
|
generateTableData() {
|
||||||
this.tableData = []
|
this.tableData = []
|
||||||
const days = 31
|
const total = this.allRows.length
|
||||||
|
if (total === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const limit = parseInt(this.limitOptions[this.limitIndex])
|
const limit = parseInt(this.limitOptions[this.limitIndex])
|
||||||
const start = (this.currentPage - 1) * limit
|
const start = (this.currentPage - 1) * limit
|
||||||
const end = Math.min(start + limit, days)
|
const end = Math.min(start + limit, total)
|
||||||
|
|
||||||
for (let i = start; i < end; i++) {
|
for (let i = start; i < end; i++) {
|
||||||
const day = i + 1
|
const row = this.allRows[i]
|
||||||
this.tableData.push({
|
this.tableData.push({
|
||||||
date: `2024-01-${day.toString().padStart(2, '0')}`,
|
date: `${row.row_date}`,
|
||||||
sales: Math.floor(Math.random() * 50000) + 20000,
|
sales: this.safeNumber(row.gmv),
|
||||||
orders: Math.floor(Math.random() * 300) + 200,
|
orders: this.safeNumber(row.orders),
|
||||||
users: Math.floor(Math.random() * 1000) + 500,
|
users: this.safeNumber(row.users),
|
||||||
conversion: (Math.random() * 5 + 1).toFixed(1),
|
conversion: this.safeNumber(row.conversion).toFixed(1),
|
||||||
avg_value: (Math.random() * 100 + 50).toFixed(2)
|
avg_value: this.safeNumber(row.avg_order_amount).toFixed(2)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -483,7 +534,7 @@ export default {
|
|||||||
onLimitChange(e: any) {
|
onLimitChange(e: any) {
|
||||||
this.limitIndex = e.detail.value
|
this.limitIndex = e.detail.value
|
||||||
this.currentPage = 1
|
this.currentPage = 1
|
||||||
this.totalPages = Math.ceil(31 / parseInt(this.limitOptions[this.limitIndex]))
|
this.updateTotalPages()
|
||||||
this.generateTableData()
|
this.generateTableData()
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -563,6 +614,24 @@ export default {
|
|||||||
icon: 'success'
|
icon: 'success'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
handleSearch() {
|
||||||
|
uni.showToast({ title: '搜索', icon: 'none' })
|
||||||
|
},
|
||||||
|
handleNotification() {
|
||||||
|
uni.showToast({ title: '通知', icon: 'none' })
|
||||||
|
},
|
||||||
|
handleFullscreen() {
|
||||||
|
uni.showToast({ title: '全屏', icon: 'none' })
|
||||||
|
},
|
||||||
|
handleMobile() {
|
||||||
|
uni.showToast({ title: '移动端', icon: 'none' })
|
||||||
|
},
|
||||||
|
handleDropdown() {
|
||||||
|
uni.showToast({ title: '下拉菜单', icon: 'none' })
|
||||||
|
},
|
||||||
|
handleSettings() {
|
||||||
|
uni.showToast({ title: '设置', icon: 'none' })
|
||||||
|
},
|
||||||
|
|
||||||
resetConfig() {
|
resetConfig() {
|
||||||
this.autoRefresh = false
|
this.autoRefresh = false
|
||||||
@@ -578,6 +647,42 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f6f7fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面布局:宽屏时侧边栏+内容,窄屏时全屏内容 */
|
||||||
|
.page-layout {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-top: 64px; /* 为固定顶部导航栏留出空间 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon .icon {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
.report-detail-page {
|
.report-detail-page {
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
@@ -1040,4 +1145,15 @@ export default {
|
|||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
color: #999;
|
color: #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 响应式:窄屏时全屏显示 */
|
||||||
|
@media screen and (max-width: 959px) {
|
||||||
|
.page-layout {
|
||||||
|
flex-direction: column !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
652
pages/mall/analytics/sales-report.uvue
Normal file
652
pages/mall/analytics/sales-report.uvue
Normal file
@@ -0,0 +1,652 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page" @click="closeMoreMenu">
|
||||||
|
<!-- 固定顶部导航栏 -->
|
||||||
|
<AnalyticsTopBar
|
||||||
|
:title="'销售报表'"
|
||||||
|
:lastUpdateTime="lastUpdateTime"
|
||||||
|
:sidebarVisible="showSidebarMenu"
|
||||||
|
@menu-click="handleMenu"
|
||||||
|
@refresh="refreshData"
|
||||||
|
@search="handleSearch"
|
||||||
|
@notification="handleNotification"
|
||||||
|
@fullscreen="handleFullscreen"
|
||||||
|
@mobile="handleMobile"
|
||||||
|
@dropdown="handleDropdown"
|
||||||
|
@settings="handleSettings"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<view class="page-layout">
|
||||||
|
<!-- 侧边栏菜单组件 -->
|
||||||
|
<AnalyticsSidebarMenu
|
||||||
|
:visible="showSidebarMenu"
|
||||||
|
:currentPath="currentPath"
|
||||||
|
@visible-change="handleSidebarUpdate"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 主内容区域 -->
|
||||||
|
<view class="main-content">
|
||||||
|
<view class="container">
|
||||||
|
|
||||||
|
<!-- 时间维度筛选 -->
|
||||||
|
<view class="tabs">
|
||||||
|
<view
|
||||||
|
v-for="p in timePeriods"
|
||||||
|
:key="p.value"
|
||||||
|
class="tab"
|
||||||
|
:class="{ active: selectedPeriod === p.value }"
|
||||||
|
@click="selectPeriod(p.value)"
|
||||||
|
>
|
||||||
|
{{ p.label }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- KPI 指标卡片 -->
|
||||||
|
<view class="kpi-grid">
|
||||||
|
<view class="kpi-card">
|
||||||
|
<text class="kpi-label">GMV(成交总额)</text>
|
||||||
|
<text class="kpi-value">¥{{ formatMoney(salesData.gmv) }}</text>
|
||||||
|
<text class="kpi-meta">较上期:{{ formatPct(salesData.gmv_growth) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="kpi-card">
|
||||||
|
<text class="kpi-label">订单量</text>
|
||||||
|
<text class="kpi-value">{{ formatInt(salesData.orders) }}</text>
|
||||||
|
<text class="kpi-meta">较上期:{{ formatPct(salesData.order_growth) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="kpi-card">
|
||||||
|
<text class="kpi-label">转化率</text>
|
||||||
|
<text class="kpi-value">{{ formatPct(salesData.conversion_rate) }}</text>
|
||||||
|
<text class="kpi-meta">较上期:{{ formatPct(salesData.conversion_growth) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="kpi-card">
|
||||||
|
<text class="kpi-label">客单价</text>
|
||||||
|
<text class="kpi-value">¥{{ formatMoney(salesData.avg_order_amount) }}</text>
|
||||||
|
<text class="kpi-meta">较上期:{{ formatPct(salesData.avg_order_growth) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 销售趋势图表 -->
|
||||||
|
<view class="card card-full">
|
||||||
|
<view class="card-head">
|
||||||
|
<text class="card-title">销售趋势分析</text>
|
||||||
|
<text class="card-desc">{{ selectedPeriodText }} · 柱:GMV(元) · 线:订单数</text>
|
||||||
|
</view>
|
||||||
|
<AnalyticsComboChart
|
||||||
|
:xLabels="trend.x"
|
||||||
|
:gmv="trend.gmv"
|
||||||
|
:orders="trend.orders"
|
||||||
|
:height="320"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 商品销售排行 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="card-head">
|
||||||
|
<text class="card-title">商品销售排行 TOP 10</text>
|
||||||
|
<text class="card-desc">按销量排序</text>
|
||||||
|
</view>
|
||||||
|
<view class="rank-list">
|
||||||
|
<view v-for="p in topProducts" :key="p.id" class="rank-item">
|
||||||
|
<text class="rank-no">{{ p.rank }}</text>
|
||||||
|
<text class="rank-name">{{ p.name }}</text>
|
||||||
|
<text class="rank-val">{{ p.sales }} 件</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 商家销售排行 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="card-head">
|
||||||
|
<text class="card-title">商家销售排行 TOP 10</text>
|
||||||
|
<text class="card-desc">按 GMV 排序</text>
|
||||||
|
</view>
|
||||||
|
<view class="rank-list">
|
||||||
|
<view v-for="m in topMerchants" :key="m.id" class="rank-item">
|
||||||
|
<text class="rank-no">{{ m.rank }}</text>
|
||||||
|
<text class="rank-name">{{ m.name }}</text>
|
||||||
|
<view class="rank-right">
|
||||||
|
<text class="rank-val">¥{{ formatMoney(m.sales) }}</text>
|
||||||
|
<text class="chip" :class="m.growth >= 0 ? 'pos' : 'neg'">
|
||||||
|
{{ m.growth >= 0 ? '+' : '' }}{{ m.growth }}%
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 销售地域分布 -->
|
||||||
|
<view class="card card-full">
|
||||||
|
<view class="card-head">
|
||||||
|
<text class="card-title">销售地域分布</text>
|
||||||
|
<text class="card-desc">按省份统计</text>
|
||||||
|
</view>
|
||||||
|
<EChartsView class="chart-box" :option="regionChartOption" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 留白 -->
|
||||||
|
<view style="height: 24px;"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts">
|
||||||
|
import supa from '@/components/supadb/aksupainstance.uts'
|
||||||
|
import AnalyticsComboChart from '@/components/analytics/AnalyticsComboChart.uvue'
|
||||||
|
import AnalyticsSidebarMenu from '@/components/analytics/AnalyticsSidebarMenu.uvue'
|
||||||
|
import AnalyticsTopBar from '@/components/analytics/AnalyticsTopBar.uvue'
|
||||||
|
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
|
||||||
|
|
||||||
|
type TrendData = { x: Array<string>; gmv: Array<number>; orders: Array<number> }
|
||||||
|
type SalesData = {
|
||||||
|
gmv: number
|
||||||
|
gmv_growth: number
|
||||||
|
orders: number
|
||||||
|
order_growth: number
|
||||||
|
conversion_rate: number
|
||||||
|
conversion_growth: number
|
||||||
|
avg_order_amount: number
|
||||||
|
avg_order_growth: number
|
||||||
|
}
|
||||||
|
type ProductRank = { id: string; rank: number; name: string; sales: number }
|
||||||
|
type MerchantRank = { id: string; rank: number; name: string; sales: number; growth: number }
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
AnalyticsComboChart,
|
||||||
|
AnalyticsSidebarMenu,
|
||||||
|
AnalyticsTopBar,
|
||||||
|
EChartsView
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
lastUpdateTime: '',
|
||||||
|
selectedPeriod: '7d',
|
||||||
|
showMoreMenu: false,
|
||||||
|
showSidebarMenu: false,
|
||||||
|
currentPath: '/pages/mall/analytics/sales-report',
|
||||||
|
timePeriods: [
|
||||||
|
{ value: '7d', label: '7天' },
|
||||||
|
{ value: '30d', label: '30天' },
|
||||||
|
{ value: '90d', label: '90天' },
|
||||||
|
{ value: '1y', label: '1年' }
|
||||||
|
],
|
||||||
|
|
||||||
|
salesData: {
|
||||||
|
gmv: 0,
|
||||||
|
gmv_growth: 0,
|
||||||
|
orders: 0,
|
||||||
|
order_growth: 0,
|
||||||
|
conversion_rate: 0,
|
||||||
|
conversion_growth: 0,
|
||||||
|
avg_order_amount: 0,
|
||||||
|
avg_order_growth: 0
|
||||||
|
} as SalesData,
|
||||||
|
|
||||||
|
trend: {
|
||||||
|
x: [] as Array<string>,
|
||||||
|
gmv: [] as Array<number>,
|
||||||
|
orders: [] as Array<number>
|
||||||
|
} as TrendData,
|
||||||
|
|
||||||
|
topProducts: [] as Array<ProductRank>,
|
||||||
|
topMerchants: [] as Array<MerchantRank>,
|
||||||
|
|
||||||
|
regionChartOption: {} as any
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
selectedPeriodText(): string {
|
||||||
|
const p = this.timePeriods.find((t) => t.value === this.selectedPeriod)
|
||||||
|
return p ? p.label : '7天'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onLoad() {
|
||||||
|
this.updateTime()
|
||||||
|
this.loadSalesData()
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async loadSalesData() {
|
||||||
|
// TODO: 实现销售数据加载
|
||||||
|
this.updateTime()
|
||||||
|
},
|
||||||
|
|
||||||
|
selectPeriod(p: string) {
|
||||||
|
this.selectedPeriod = p
|
||||||
|
this.loadSalesData()
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshData() {
|
||||||
|
this.loadSalesData()
|
||||||
|
uni.showToast({ title: '已刷新', icon: 'success' })
|
||||||
|
},
|
||||||
|
|
||||||
|
exportReport() {
|
||||||
|
uni.showActionSheet({
|
||||||
|
itemList: ['导出Excel', '导出PDF', '导出图片'],
|
||||||
|
success: () => uni.showToast({ title: '导出成功', icon: 'success' })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
updateTime() {
|
||||||
|
const now = new Date()
|
||||||
|
const hh = now.getHours().toString().padStart(2, '0')
|
||||||
|
const mm = now.getMinutes().toString().padStart(2, '0')
|
||||||
|
this.lastUpdateTime = `${hh}:${mm}`
|
||||||
|
},
|
||||||
|
|
||||||
|
formatInt(n: number): string {
|
||||||
|
const v = isFinite(n) ? Math.round(n) : 0
|
||||||
|
if (v >= 10000) return (v / 10000).toFixed(1) + '万'
|
||||||
|
return v.toString()
|
||||||
|
},
|
||||||
|
|
||||||
|
formatMoney(n: number): string {
|
||||||
|
const v = isFinite(n) ? n : 0
|
||||||
|
if (v >= 10000) return (v / 10000).toFixed(1) + '万'
|
||||||
|
return v.toFixed(0)
|
||||||
|
},
|
||||||
|
|
||||||
|
formatPct(n: number): string {
|
||||||
|
const v = isFinite(n) ? n : 0
|
||||||
|
const sign = v > 0 ? '+' : ''
|
||||||
|
return `${sign}${v.toFixed(1)}%`
|
||||||
|
},
|
||||||
|
|
||||||
|
handleMenu() {
|
||||||
|
this.showSidebarMenu = true
|
||||||
|
},
|
||||||
|
handleSidebarUpdate(visible: boolean) {
|
||||||
|
this.showSidebarMenu = visible
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleMoreMenu() {
|
||||||
|
this.showMoreMenu = !this.showMoreMenu
|
||||||
|
},
|
||||||
|
|
||||||
|
closeMoreMenu() {
|
||||||
|
this.showMoreMenu = false
|
||||||
|
},
|
||||||
|
handleSearch() {
|
||||||
|
uni.showToast({ title: '搜索', icon: 'none' })
|
||||||
|
},
|
||||||
|
handleNotification() {
|
||||||
|
uni.showToast({ title: '通知', icon: 'none' })
|
||||||
|
},
|
||||||
|
handleFullscreen() {
|
||||||
|
uni.showToast({ title: '全屏', icon: 'none' })
|
||||||
|
},
|
||||||
|
handleMobile() {
|
||||||
|
uni.showToast({ title: '移动端', icon: 'none' })
|
||||||
|
},
|
||||||
|
handleDropdown() {
|
||||||
|
uni.showToast({ title: '下拉菜单', icon: 'none' })
|
||||||
|
},
|
||||||
|
handleSettings() {
|
||||||
|
uni.showToast({ title: '设置', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f6f7fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面布局:宽屏时侧边栏+内容,窄屏时全屏内容 */
|
||||||
|
.page-layout {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-top: 64px; /* 为固定顶部导航栏留出空间 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 16px 16px 28px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部栏 */
|
||||||
|
.topbar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.06);
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-left {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon:active {
|
||||||
|
background: #e5e7eb;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon .icon {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #111;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111;
|
||||||
|
max-width: 420px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0,0,0,0.55);
|
||||||
|
max-width: 420px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn-icon:active {
|
||||||
|
background: #e5e7eb;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn-icon .icon {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn {
|
||||||
|
display: none;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn.active {
|
||||||
|
background: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn .icon {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 时间维度 tabs */
|
||||||
|
.tabs {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.06);
|
||||||
|
overflow-x: auto;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #111;
|
||||||
|
font-size: 13px;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
background: #111;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* KPI 网格 */
|
||||||
|
.kpi-grid {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.06);
|
||||||
|
padding: 14px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
flex: 1 1 calc(50% - 6px);
|
||||||
|
min-width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0,0,0,0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-value {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-meta {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0,0,0,0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片 */
|
||||||
|
.card {
|
||||||
|
margin-top: 12px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.06);
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.04);
|
||||||
|
padding: 14px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-full {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-head {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0,0,0,0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-box {
|
||||||
|
width: 100%;
|
||||||
|
height: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 排行列表 */
|
||||||
|
.rank-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px solid rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-no {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(0,0,0,0.06);
|
||||||
|
text-align: center;
|
||||||
|
line-height: 28px;
|
||||||
|
font-size: 12px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-name {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-val {
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(0,0,0,0.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip {
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip.pos {
|
||||||
|
background: rgba(34,197,94,0.12);
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip.neg {
|
||||||
|
background: rgba(239,68,68,0.12);
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式 */
|
||||||
|
@media screen and (min-width: 960px) {
|
||||||
|
.kpi-card {
|
||||||
|
flex: 1 1 calc(25% - 9px);
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 960px) {
|
||||||
|
.title,
|
||||||
|
.subtitle {
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-right .btn-hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn {
|
||||||
|
display: flex !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
578
pages/mall/analytics/user-analysis.uvue
Normal file
578
pages/mall/analytics/user-analysis.uvue
Normal file
@@ -0,0 +1,578 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page" @click="closeMoreMenu">
|
||||||
|
<!-- 固定顶部导航栏 -->
|
||||||
|
<AnalyticsTopBar
|
||||||
|
:title="'用户分析'"
|
||||||
|
:lastUpdateTime="lastUpdateTime"
|
||||||
|
:sidebarVisible="showSidebarMenu"
|
||||||
|
@menu-click="handleMenu"
|
||||||
|
@refresh="refreshData"
|
||||||
|
@search="handleSearch"
|
||||||
|
@notification="handleNotification"
|
||||||
|
@fullscreen="handleFullscreen"
|
||||||
|
@mobile="handleMobile"
|
||||||
|
@dropdown="handleDropdown"
|
||||||
|
@settings="handleSettings"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<view class="page-layout">
|
||||||
|
<!-- 侧边栏菜单组件 -->
|
||||||
|
<AnalyticsSidebarMenu
|
||||||
|
:visible="showSidebarMenu"
|
||||||
|
:currentPath="currentPath"
|
||||||
|
@visible-change="handleSidebarUpdate"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 主内容区域 -->
|
||||||
|
<view class="main-content">
|
||||||
|
<view class="container">
|
||||||
|
|
||||||
|
<!-- 时间维度筛选 -->
|
||||||
|
<view class="tabs">
|
||||||
|
<view
|
||||||
|
v-for="p in timePeriods"
|
||||||
|
:key="p.value"
|
||||||
|
class="tab"
|
||||||
|
:class="{ active: selectedPeriod === p.value }"
|
||||||
|
@click="selectPeriod(p.value)"
|
||||||
|
>
|
||||||
|
{{ p.label }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- KPI 指标卡片 -->
|
||||||
|
<view class="kpi-grid">
|
||||||
|
<view class="kpi-card">
|
||||||
|
<text class="kpi-label">总用户数</text>
|
||||||
|
<text class="kpi-value">{{ formatInt(userData.total_users) }}</text>
|
||||||
|
<text class="kpi-meta">较上期:{{ formatPct(userData.user_growth) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="kpi-card">
|
||||||
|
<text class="kpi-label">新用户</text>
|
||||||
|
<text class="kpi-value">{{ formatInt(userData.new_users) }}</text>
|
||||||
|
<text class="kpi-meta">较上期:{{ formatPct(userData.new_user_growth) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="kpi-card">
|
||||||
|
<text class="kpi-label">用户活跃度</text>
|
||||||
|
<text class="kpi-value">{{ formatPct(userData.active_rate) }}</text>
|
||||||
|
<text class="kpi-meta">较上期:{{ formatPct(userData.active_growth) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="kpi-card">
|
||||||
|
<text class="kpi-label">复购率</text>
|
||||||
|
<text class="kpi-value">{{ formatPct(userData.repurchase_rate) }}</text>
|
||||||
|
<text class="kpi-meta">较上期:{{ formatPct(userData.repurchase_growth) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 用户增长趋势 -->
|
||||||
|
<view class="card card-full">
|
||||||
|
<view class="card-head">
|
||||||
|
<text class="card-title">用户增长趋势</text>
|
||||||
|
<text class="card-desc">{{ selectedPeriodText }} · 新用户 vs 总用户</text>
|
||||||
|
</view>
|
||||||
|
<EChartsView class="chart-box" :option="growthChartOption" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 用户留存率 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="card-head">
|
||||||
|
<text class="card-title">用户留存率</text>
|
||||||
|
<text class="card-desc">按留存天数统计</text>
|
||||||
|
</view>
|
||||||
|
<EChartsView class="chart-box" :option="retentionChartOption" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 用户活跃度分析 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="card-head">
|
||||||
|
<text class="card-title">用户活跃度分析</text>
|
||||||
|
<text class="card-desc">日活跃、周活跃、月活跃</text>
|
||||||
|
</view>
|
||||||
|
<EChartsView class="chart-box" :option="activityChartOption" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 新老用户对比 -->
|
||||||
|
<view class="card card-full">
|
||||||
|
<view class="card-head">
|
||||||
|
<text class="card-title">新老用户对比</text>
|
||||||
|
<text class="card-desc">GMV、订单数、客单价对比</text>
|
||||||
|
</view>
|
||||||
|
<EChartsView class="chart-box" :option="comparisonChartOption" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 用户画像分析 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="card-head">
|
||||||
|
<text class="card-title">用户画像分析</text>
|
||||||
|
<text class="card-desc">性别、年龄、地域分布</text>
|
||||||
|
</view>
|
||||||
|
<EChartsView class="chart-box" :option="profileChartOption" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 留白 -->
|
||||||
|
<view style="height: 24px;"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts">
|
||||||
|
import supa from '@/components/supadb/aksupainstance.uts'
|
||||||
|
import AnalyticsSidebarMenu from '@/components/analytics/AnalyticsSidebarMenu.uvue'
|
||||||
|
import AnalyticsTopBar from '@/components/analytics/AnalyticsTopBar.uvue'
|
||||||
|
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
|
||||||
|
|
||||||
|
type UserData = {
|
||||||
|
total_users: number
|
||||||
|
user_growth: number
|
||||||
|
new_users: number
|
||||||
|
new_user_growth: number
|
||||||
|
active_rate: number
|
||||||
|
active_growth: number
|
||||||
|
repurchase_rate: number
|
||||||
|
repurchase_growth: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
AnalyticsSidebarMenu,
|
||||||
|
AnalyticsTopBar,
|
||||||
|
EChartsView
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
lastUpdateTime: '',
|
||||||
|
selectedPeriod: '7d',
|
||||||
|
showMoreMenu: false,
|
||||||
|
showSidebarMenu: false,
|
||||||
|
currentPath: '/pages/mall/analytics/user-analysis',
|
||||||
|
timePeriods: [
|
||||||
|
{ value: '7d', label: '7天' },
|
||||||
|
{ value: '30d', label: '30天' },
|
||||||
|
{ value: '90d', label: '90天' },
|
||||||
|
{ value: '1y', label: '1年' }
|
||||||
|
],
|
||||||
|
|
||||||
|
userData: {
|
||||||
|
total_users: 0,
|
||||||
|
user_growth: 0,
|
||||||
|
new_users: 0,
|
||||||
|
new_user_growth: 0,
|
||||||
|
active_rate: 0,
|
||||||
|
active_growth: 0,
|
||||||
|
repurchase_rate: 0,
|
||||||
|
repurchase_growth: 0
|
||||||
|
} as UserData,
|
||||||
|
|
||||||
|
growthChartOption: {} as any,
|
||||||
|
retentionChartOption: {} as any,
|
||||||
|
activityChartOption: {} as any,
|
||||||
|
comparisonChartOption: {} as any,
|
||||||
|
profileChartOption: {} as any
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
selectedPeriodText(): string {
|
||||||
|
const p = this.timePeriods.find((t) => t.value === this.selectedPeriod)
|
||||||
|
return p ? p.label : '7天'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onLoad() {
|
||||||
|
this.updateTime()
|
||||||
|
this.loadUserData()
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async loadUserData() {
|
||||||
|
// TODO: 实现用户数据加载
|
||||||
|
this.updateTime()
|
||||||
|
this.buildChartOptions()
|
||||||
|
},
|
||||||
|
|
||||||
|
selectPeriod(p: string) {
|
||||||
|
this.selectedPeriod = p
|
||||||
|
this.loadUserData()
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshData() {
|
||||||
|
this.loadUserData()
|
||||||
|
uni.showToast({ title: '已刷新', icon: 'success' })
|
||||||
|
},
|
||||||
|
|
||||||
|
exportReport() {
|
||||||
|
uni.showActionSheet({
|
||||||
|
itemList: ['导出Excel', '导出PDF', '导出图片'],
|
||||||
|
success: () => uni.showToast({ title: '导出成功', icon: 'success' })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
updateTime() {
|
||||||
|
const now = new Date()
|
||||||
|
const hh = now.getHours().toString().padStart(2, '0')
|
||||||
|
const mm = now.getMinutes().toString().padStart(2, '0')
|
||||||
|
this.lastUpdateTime = `${hh}:${mm}`
|
||||||
|
},
|
||||||
|
|
||||||
|
formatInt(n: number): string {
|
||||||
|
const v = isFinite(n) ? Math.round(n) : 0
|
||||||
|
if (v >= 10000) return (v / 10000).toFixed(1) + '万'
|
||||||
|
return v.toString()
|
||||||
|
},
|
||||||
|
|
||||||
|
formatPct(n: number): string {
|
||||||
|
const v = isFinite(n) ? n : 0
|
||||||
|
const sign = v > 0 ? '+' : ''
|
||||||
|
return `${sign}${v.toFixed(1)}%`
|
||||||
|
},
|
||||||
|
|
||||||
|
buildChartOptions() {
|
||||||
|
// TODO: 构建图表配置
|
||||||
|
this.growthChartOption = {}
|
||||||
|
this.retentionChartOption = {}
|
||||||
|
this.activityChartOption = {}
|
||||||
|
this.comparisonChartOption = {}
|
||||||
|
this.profileChartOption = {}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleMenu() {
|
||||||
|
this.showSidebarMenu = true
|
||||||
|
},
|
||||||
|
handleSidebarUpdate(visible: boolean) {
|
||||||
|
this.showSidebarMenu = visible
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleMoreMenu() {
|
||||||
|
this.showMoreMenu = !this.showMoreMenu
|
||||||
|
},
|
||||||
|
|
||||||
|
closeMoreMenu() {
|
||||||
|
this.showMoreMenu = false
|
||||||
|
},
|
||||||
|
handleSearch() {
|
||||||
|
uni.showToast({ title: '搜索', icon: 'none' })
|
||||||
|
},
|
||||||
|
handleNotification() {
|
||||||
|
uni.showToast({ title: '通知', icon: 'none' })
|
||||||
|
},
|
||||||
|
handleFullscreen() {
|
||||||
|
uni.showToast({ title: '全屏', icon: 'none' })
|
||||||
|
},
|
||||||
|
handleMobile() {
|
||||||
|
uni.showToast({ title: '移动端', icon: 'none' })
|
||||||
|
},
|
||||||
|
handleDropdown() {
|
||||||
|
uni.showToast({ title: '下拉菜单', icon: 'none' })
|
||||||
|
},
|
||||||
|
handleSettings() {
|
||||||
|
uni.showToast({ title: '设置', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f6f7fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面布局:宽屏时侧边栏+内容,窄屏时全屏内容 */
|
||||||
|
.page-layout {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-top: 64px; /* 为固定顶部导航栏留出空间 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式:窄屏时全屏显示 */
|
||||||
|
@media screen and (max-width: 959px) {
|
||||||
|
.page-layout {
|
||||||
|
flex-direction: column !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 16px 16px 28px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部栏 */
|
||||||
|
.topbar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.06);
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-left {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon:active {
|
||||||
|
background: #e5e7eb;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon .icon {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #111;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111;
|
||||||
|
max-width: 420px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0,0,0,0.55);
|
||||||
|
max-width: 420px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn-icon:active {
|
||||||
|
background: #e5e7eb;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn-icon .icon {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn {
|
||||||
|
display: none;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn.active {
|
||||||
|
background: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn .icon {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 时间维度 tabs */
|
||||||
|
.tabs {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.06);
|
||||||
|
overflow-x: auto;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #111;
|
||||||
|
font-size: 13px;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
background: #111;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* KPI 网格 */
|
||||||
|
.kpi-grid {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.06);
|
||||||
|
padding: 14px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
flex: 1 1 calc(50% - 6px);
|
||||||
|
min-width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0,0,0,0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-value {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-meta {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0,0,0,0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片 */
|
||||||
|
.card {
|
||||||
|
margin-top: 12px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.06);
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.04);
|
||||||
|
padding: 14px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-full {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-head {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0,0,0,0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-box {
|
||||||
|
width: 100%;
|
||||||
|
height: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式 */
|
||||||
|
@media screen and (min-width: 960px) {
|
||||||
|
.kpi-card {
|
||||||
|
flex: 1 1 calc(25% - 9px);
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 960px) {
|
||||||
|
.title,
|
||||||
|
.subtitle {
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-right .btn-hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn {
|
||||||
|
display: flex !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -11,44 +11,20 @@
|
|||||||
|
|
||||||
<!-- 注册表单 -->
|
<!-- 注册表单 -->
|
||||||
<view class="form-content">
|
<view class="form-content">
|
||||||
<!-- 手机号 -->
|
<!-- 邮箱 -->
|
||||||
<view class="input-group">
|
<view class="input-group">
|
||||||
<view class="input-wrapper">
|
<view class="input-wrapper">
|
||||||
<image src="/static/user/phone_1.png" class="input-icon" />
|
<image src="/static/user/phone_1.png" class="input-icon" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="输入手机号码"
|
placeholder="输入邮箱"
|
||||||
:value="phone"
|
:value="email"
|
||||||
@input="(e: any) => phone = e.detail.value"
|
@input="(e: any) => email = e.detail.value"
|
||||||
maxlength="11"
|
|
||||||
class="input-field"
|
class="input-field"
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 验证码 -->
|
|
||||||
<view class="input-group">
|
|
||||||
<view class="input-wrapper">
|
|
||||||
<image src="/static/user/code_2.png" class="input-icon" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="填写验证码"
|
|
||||||
:value="captcha"
|
|
||||||
@input="(e: any) => captcha = e.detail.value"
|
|
||||||
maxlength="6"
|
|
||||||
class="input-field code-input"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
class="code-btn"
|
|
||||||
:disabled="codeDisabled"
|
|
||||||
:class="{ 'disabled': codeDisabled }"
|
|
||||||
@click="getCode"
|
|
||||||
>
|
|
||||||
{{ codeText }}
|
|
||||||
</button>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 密码 -->
|
<!-- 密码 -->
|
||||||
<view class="input-group">
|
<view class="input-group">
|
||||||
<view class="input-wrapper">
|
<view class="input-wrapper">
|
||||||
@@ -115,61 +91,37 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref } from 'vue'
|
||||||
import supa from '@/components/supadb/aksupainstance.uts'
|
import supa from '@/components/supadb/aksupainstance.uts'
|
||||||
|
import { ensureUserProfile } from '@/utils/sapi.uts'
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const phone = ref<string>('')
|
const email = ref<string>('')
|
||||||
const captcha = ref<string>('')
|
|
||||||
const password = ref<string>('')
|
const password = ref<string>('')
|
||||||
const confirmPassword = ref<string>('')
|
const confirmPassword = ref<string>('')
|
||||||
const protocol = ref<boolean>(false)
|
const protocol = ref<boolean>(false)
|
||||||
const inAnimation = ref<boolean>(false)
|
const inAnimation = ref<boolean>(false)
|
||||||
const isLoading = ref<boolean>(false)
|
const isLoading = ref<boolean>(false)
|
||||||
const logoUrl = ref<string>('/static/logo.png')
|
const logoUrl = ref<string>('/static/logo.png')
|
||||||
|
|
||||||
// 验证码相关
|
|
||||||
const codeDisabled = ref<boolean>(false)
|
|
||||||
const codeText = ref<string>('获取验证码')
|
|
||||||
const codeCountdown = ref<number>(0)
|
|
||||||
let codeTimer: number | null = null
|
|
||||||
|
|
||||||
// 处理协议勾选变化
|
// 处理协议勾选变化
|
||||||
const handleProtocolChange = (e: any) => {
|
const handleProtocolChange = (e: any) => {
|
||||||
protocol.value = !protocol.value
|
protocol.value = !protocol.value
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证手机号
|
// 验证邮箱
|
||||||
const validatePhone = (): boolean => {
|
const validateEmail = (): boolean => {
|
||||||
if (phone.value.trim() === '') {
|
if (email.value.trim() === '') {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '请填写手机号码',
|
title: '请填写邮箱',
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
})
|
})
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (!/^1[3-9]\d{9}$/.test(phone.value)) {
|
// 基础邮箱格式校验(足够用于前端提示)
|
||||||
|
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value)) {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '请输入正确的手机号码',
|
title: '请输入正确的邮箱',
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证验证码
|
|
||||||
const validateCaptcha = (): boolean => {
|
|
||||||
if (captcha.value.trim() === '') {
|
|
||||||
uni.showToast({
|
|
||||||
title: '请填写验证码',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (!/^\d{6}$/.test(captcha.value)) {
|
|
||||||
uni.showToast({
|
|
||||||
title: '请输入正确的验证码',
|
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
})
|
})
|
||||||
return false
|
return false
|
||||||
@@ -223,47 +175,6 @@
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取验证码
|
|
||||||
const getCode = async () => {
|
|
||||||
if (!protocol.value) {
|
|
||||||
inAnimation.value = true
|
|
||||||
uni.showToast({
|
|
||||||
title: '请先阅读并同意协议',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!validatePhone()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: 调用获取验证码接口
|
|
||||||
uni.showToast({
|
|
||||||
title: '验证码已发送',
|
|
||||||
icon: 'success'
|
|
||||||
})
|
|
||||||
|
|
||||||
// 开始倒计时
|
|
||||||
codeDisabled.value = true
|
|
||||||
codeCountdown.value = 60
|
|
||||||
codeText.value = `${codeCountdown.value}秒后重试`
|
|
||||||
|
|
||||||
codeTimer = setInterval(() => {
|
|
||||||
codeCountdown.value--
|
|
||||||
if (codeCountdown.value > 0) {
|
|
||||||
codeText.value = `${codeCountdown.value}秒后重试`
|
|
||||||
} else {
|
|
||||||
codeDisabled.value = false
|
|
||||||
codeText.value = '获取验证码'
|
|
||||||
if (codeTimer != null) {
|
|
||||||
clearInterval(codeTimer)
|
|
||||||
codeTimer = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 1000) as unknown as number
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理注册
|
// 处理注册
|
||||||
const handleRegister = async () => {
|
const handleRegister = async () => {
|
||||||
// 检查协议
|
// 检查协议
|
||||||
@@ -277,10 +188,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 表单验证
|
// 表单验证
|
||||||
if (!validatePhone()) {
|
if (!validateEmail()) {
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!validateCaptcha()) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!validatePassword()) {
|
if (!validatePassword()) {
|
||||||
@@ -293,52 +201,27 @@
|
|||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// TODO: 调用注册接口(手机号+验证码+密码)
|
// 使用 Supabase Auth:邮箱 + 密码注册
|
||||||
// 目前先使用邮箱注册作为临时方案
|
const result = await supa.signUp(email.value.trim(), password.value)
|
||||||
// 注意:CRMEB 使用手机号注册,但 Supabase Auth 默认支持邮箱注册
|
const data = new UTSJSONObject(result as any)
|
||||||
// 需要根据实际后端 API 调整
|
const user = data.getJSON('user')
|
||||||
|
|
||||||
// 临时方案:使用手机号作为邮箱格式注册
|
|
||||||
const email = `${phone.value}@phone.mall`
|
|
||||||
|
|
||||||
const result = await supa.signUp(email, password.value)
|
|
||||||
|
|
||||||
if (result != null && result.user != null) {
|
|
||||||
// 创建用户资料
|
|
||||||
const user = result.user as UTSJSONObject
|
|
||||||
const authId = user.getString('id')
|
|
||||||
|
|
||||||
if (authId != null) {
|
|
||||||
const userData = {
|
|
||||||
auth_id: authId,
|
|
||||||
phone: phone.value,
|
|
||||||
email: email,
|
|
||||||
username: phone.value,
|
|
||||||
user_type: 1, // 默认消费者
|
|
||||||
status: 1
|
|
||||||
} as UTSJSONObject
|
|
||||||
|
|
||||||
try {
|
|
||||||
await supa.from('ak_users').insert(userData).execute()
|
|
||||||
} catch (profileErr) {
|
|
||||||
console.error('创建用户资料失败:', profileErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
uni.showToast({
|
// Supabase 可能开启了邮箱验证,此时 user 可能为空/或 session 为空
|
||||||
title: '注册成功',
|
if (user != null) {
|
||||||
icon: 'success'
|
// 记录业务侧用户资料(ak_users),用于 app 内个人中心等页面读取
|
||||||
})
|
await ensureUserProfile(user as UTSJSONObject)
|
||||||
|
|
||||||
// 跳转到登录页
|
|
||||||
setTimeout(() => {
|
|
||||||
uni.redirectTo({
|
|
||||||
url: '/pages/user/login'
|
|
||||||
})
|
|
||||||
}, 1500)
|
|
||||||
} else {
|
|
||||||
throw new Error('注册失败')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uni.showToast({
|
||||||
|
title: '注册成功',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.redirectTo({
|
||||||
|
url: '/pages/user/login'
|
||||||
|
})
|
||||||
|
}, 1500)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('注册错误:', err)
|
console.error('注册错误:', err)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user