登录注册/数据分析

This commit is contained in:
comlibmb
2026-01-22 21:15:02 +08:00
parent 75fad97d5d
commit fdbee0fa32
41 changed files with 5812 additions and 2102 deletions

1
CRMEB Submodule

Submodule CRMEB added at d3dba751ca

View File

@@ -1,14 +1,23 @@
// Supabase 配置
// 开发环境 - 本地 Supabase
// 内网环境 - 本地部署的 Supabase
// IP: 192.168.1.63
// Kong HTTP Port: 8000
export const SUPA_URL: string = 'http://192.168.1.63:8000'
export const SUPA_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzY4ODMwNjI0LCJleHAiOjE5MjY1MTA2MjR9.mDVl-kIOdRK9v6VTxo0TDF8r7X7xk3PZXazaavHyVvg'
// WebSocket 实时连接(内网使用 ws:// 而非 wss://
export const WS_URL: string = 'ws://192.168.1.63:8000/realtime/v1/websocket'
// 备用配置(已注释,如需切换可取消注释)
// 开发环境 - 其他内网地址
// export const SUPA_URL: string = 'http://192.168.0.150:8080'
// export const SUPA_KEY: string = 'your-anon-key'
// export const WS_URL: string = 'ws://192.168.0.150:8080/realtime/v1/websocket'
// 生产环境 - Supabase 云服务
export const SUPA_URL: string = 'https://ak3.oulog.com'
export const SUPA_KEY: string = 'your-anon-key'
// WebSocket 实时连接
export const WS_URL: string = 'wss://ak3.oulog.com/realtime/v1/websocket'
// 生产环境 - Supabase 云服务(已注释)
// export const SUPA_URL: string = 'https://ak3.oulog.com'
// export const SUPA_KEY: string = 'your-anon-key'
// export const WS_URL: string = 'wss://ak3.oulog.com/realtime/v1/websocket'
// 路由配置
export const HOME_REDIRECT: string = '/pages/mall/consumer/index'

View File

@@ -0,0 +1,60 @@
<template>
<view class="chart-wrap" :style="{ height: heightPx }">
<EChartsView :option="chartOption" class="chart" />
</view>
</template>
<script lang="uts">
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
type Item = { name: string; value: number }
export default {
components: {
EChartsView
},
props: {
items: { type: Array, default: () => [] },
height: { type: Number, default: 300 }
},
data() {
return {
heightPx: '300px',
chartOption: {} as any
}
},
watch: {
items: { handler() { this.updateOption() }, deep: true },
height: {
handler() {
this.heightPx = `${this.height}px`
}
}
},
mounted() {
this.heightPx = `${this.height}px`
this.updateOption()
},
methods: {
updateOption() {
const x = (this.items as Array<Item>).map((it) => it.name)
const y = (this.items as Array<Item>).map((it) => {
const n = Number(it.value)
return isFinite(n) ? n : 0
})
this.chartOption = {
grid: { left: 80, right: 24, top: 18, bottom: 18 },
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
xAxis: { type: 'value', axisLabel: { color: 'rgba(0,0,0,0.55)' }, splitLine: { lineStyle: { color: 'rgba(0,0,0,0.06)' } } },
yAxis: { type: 'category', data: x, axisTick: { show: false }, axisLabel: { color: 'rgba(0,0,0,0.65)' } },
series: [{ type: 'bar', data: y, barWidth: 14, itemStyle: { borderRadius: 6 } }]
}
}
}
}
</script>
<style>
.chart-wrap { width: 100%; }
.chart { width: 100%; height: 100%; }
</style>

View File

@@ -0,0 +1,116 @@
<template>
<view class="chart-wrap" :style="{ height: heightPx }">
<EChartsView :option="chartOption" class="chart" />
</view>
</template>
<script lang="uts">
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
export default {
components: {
EChartsView
},
props: {
xLabels: { type: Array, default: () => [] },
gmv: { type: Array, default: () => [] },
orders: { type: Array, default: () => [] },
height: { type: Number, default: 320 }
},
data() {
return {
heightPx: '320px',
chartOption: {} as any
}
},
watch: {
xLabels: { handler() { this.updateOption() }, deep: true },
gmv: { handler() { this.updateOption() }, deep: true },
orders: { handler() { this.updateOption() }, deep: true },
height: {
handler() {
this.heightPx = `${this.height}px`
}
}
},
mounted() {
this.heightPx = `${this.height}px`
this.updateOption()
},
methods: {
updateOption() {
const x = (this.xLabels as Array<any>).map((s) => String(s))
const bar = (this.gmv as Array<any>).map((v) => {
const n = Number(v)
return isFinite(n) ? n : 0
})
const line = (this.orders as Array<any>).map((v) => {
const n = Number(v)
return isFinite(n) ? n : 0
})
this.chartOption = {
grid: { left: 44, right: 44, top: 24, bottom: 36 },
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
legend: { top: 0, left: 0, itemWidth: 10, itemHeight: 10, textStyle: { fontSize: 12 } },
xAxis: {
type: 'category',
data: x,
axisTick: { show: false },
axisLine: { lineStyle: { color: 'rgba(0,0,0,0.12)' } },
axisLabel: { color: 'rgba(0,0,0,0.55)' }
},
yAxis: [
{
type: 'value',
name: 'GMV',
axisLine: { show: false },
splitLine: { lineStyle: { color: 'rgba(0,0,0,0.06)' } },
axisLabel: { color: 'rgba(0,0,0,0.55)' }
},
{
type: 'value',
name: '订单',
axisLine: { show: false },
splitLine: { show: false },
axisLabel: { color: 'rgba(0,0,0,0.55)' }
}
],
series: [
{
name: 'GMV',
type: 'bar',
data: bar,
barWidth: 14,
itemStyle: { borderRadius: [6, 6, 0, 0] }
},
{
name: '订单数',
type: 'line',
yAxisIndex: 1,
data: line,
smooth: true,
symbol: 'circle',
symbolSize: 6,
lineStyle: { width: 2 }
}
]
}
}
}
}
</script>
<style>
.chart-wrap {
width: 100%;
}
.chart {
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,70 @@
<template>
<view class="chart-wrap" :style="{ height: heightPx }">
<EChartsView :option="chartOption" class="chart" />
</view>
</template>
<script lang="uts">
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
type Item = { name: string; value: number }
export default {
components: {
EChartsView
},
props: {
items: { type: Array, default: () => [] },
height: { type: Number, default: 300 }
},
data() {
return {
heightPx: '300px',
chartOption: {} as any
}
},
watch: {
items: { handler() { this.updateOption() }, deep: true },
height: {
handler() {
this.heightPx = `${this.height}px`
}
}
},
mounted() {
this.heightPx = `${this.height}px`
this.updateOption()
},
methods: {
updateOption() {
const data = (this.items as Array<Item>).map((it) => ({
name: it.name,
value: (() => {
const n = Number(it.value)
return isFinite(n) ? n : 0
})()
}))
this.chartOption = {
tooltip: { trigger: 'item' },
legend: { left: 0, bottom: 0, itemWidth: 10, itemHeight: 10, textStyle: { fontSize: 12 } },
series: [
{
type: 'pie',
radius: ['55%', '75%'],
center: ['50%', '45%'],
avoidLabelOverlap: true,
label: { show: true, formatter: '{b}\n{d}%' },
labelLine: { length: 10, length2: 10 },
data
}
]
}
}
}
}
</script>
<style>
.chart-wrap { width: 100%; }
.chart { width: 100%; height: 100%; }
</style>

View File

@@ -0,0 +1,57 @@
<template>
<view class="card">
<view class="hd">
<view class="left">
<text class="t">{{ title }}</text>
<text class="d" v-if="desc">{{ desc }}</text>
</view>
<slot name="extra"></slot>
</view>
<view class="bd">
<slot></slot>
</view>
</view>
</template>
<script lang="uts">
export default {
props: {
title: { type: String, default: '' },
desc: { type: String, default: '' }
}
}
</script>
<style>
.card {
border: 1rpx solid rgba(17, 17, 17, 0.08);
border-radius: 16rpx;
padding: 14rpx;
background: #fff;
flex: 1;
}
.hd {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 10rpx;
}
.t {
font-size: 24rpx;
font-weight: 800;
color: #111;
}
.d {
font-size: 20rpx;
color: rgba(17, 17, 17, 0.55);
margin-top: 4rpx;
display: block;
}
.bd {
padding-top: 4rpx;
}
</style>

View File

@@ -0,0 +1,98 @@
<template>
<view class="kpi" :class="tone">
<text class="t">{{ title }}</text>
<text class="v">{{ value }}</text>
<view class="row" v-if="!deltaHidden">
<text class="delta" :class="delta >= 0 ? 'pos' : 'neg'">
{{ delta >= 0 ? '+' : '' }}{{ delta.toFixed(1) }}%
</text>
<text class="s">{{ subtitle }}</text>
</view>
<text class="s" v-else>{{ subtitle }}</text>
</view>
</template>
<script lang="uts">
export default {
props: {
title: { type: String, default: '' },
value: { type: String, default: '' },
subtitle: { type: String, default: '' },
delta: { type: Number, default: 0 },
tone: { type: String, default: 'danger' },
deltaHidden: { type: Boolean, default: false }
}
}
</script>
<style>
.kpi {
width: calc(50% - 7rpx);
padding: 16rpx;
border-radius: 16rpx;
color: #fff;
}
.t {
font-size: 22rpx;
opacity: 0.9;
}
.v {
font-size: 36rpx;
font-weight: 800;
margin-top: 10rpx;
display: block;
}
.row {
display: flex;
align-items: center;
gap: 10rpx;
margin-top: 10rpx;
}
.s {
font-size: 20rpx;
opacity: 0.85;
}
.delta {
font-size: 20rpx;
padding: 2rpx 10rpx;
border-radius: 999rpx;
background: rgba(255, 255, 255, 0.18);
font-weight: 700;
}
.delta.pos {
}
.delta.neg {
}
/* tones */
.danger {
background: linear-gradient(135deg, #ff6b6b 0%, #ff4d4f 100%);
}
.teal {
background: linear-gradient(135deg, #4ecdc4 0%, #44a08d 100%);
}
.green {
background: linear-gradient(135deg, #a8e6cf 0%, #7fcdbb 100%);
}
.amber {
background: linear-gradient(135deg, #ffd93d 0%, #ffa07a 100%);
}
@media screen and (max-width: 768px) {
.kpi {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,49 @@
<template>
<view class="tabs">
<view
v-for="it in items"
:key="it.value"
class="tab"
:class="value === it.value ? 'on' : ''"
@click="pick(it.value)"
>
<text>{{ it.label }}</text>
</view>
</view>
</template>
<script lang="uts">
export default {
props: {
value: { type: String, default: '30d' },
items: { type: Array, default: () => [] }
},
methods: {
pick(v: string) {
this.$emit('change', v)
}
}
}
</script>
<style>
.tabs {
display: flex;
background: rgba(17, 17, 17, 0.04);
border-radius: 999rpx;
padding: 6rpx;
}
.tab {
padding: 10rpx 16rpx;
border-radius: 999rpx;
color: rgba(17, 17, 17, 0.65);
font-size: 22rpx;
}
.tab.on {
color: #fff;
font-weight: 700;
background: linear-gradient(135deg, #ff4d4f 0%, #ff7a45 100%);
}
</style>

View File

@@ -0,0 +1,23 @@
<template>
<view class="wrap" :style="{ height: height + 'px' }">
<EChartsView :option="option" />
</view>
</template>
<script lang="uts">
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
export default {
components: { EChartsView },
props: {
option: { type: Object, default: () => ({}) },
height: { type: Number, default: 280 }
}
}
</script>
<style>
.wrap {
width: 100%;
}
</style>

View File

@@ -0,0 +1,23 @@
<template>
<view class="wrap" :style="{ height: height + 'px' }">
<EChartsView :option="option" />
</view>
</template>
<script lang="uts">
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
export default {
components: { EChartsView },
props: {
option: { type: Object, default: () => ({}) },
height: { type: Number, default: 320 }
}
}
</script>
<style>
.wrap {
width: 100%;
}
</style>

View File

@@ -0,0 +1,23 @@
<template>
<view class="wrap" :style="{ height: height + 'px' }">
<EChartsView :option="option" />
</view>
</template>
<script lang="uts">
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
export default {
components: { EChartsView },
props: {
option: { type: Object, default: () => ({}) },
height: { type: Number, default: 280 }
}
}
</script>
<style>
.wrap {
width: 100%;
}
</style>

665
docs/ANALYTICS_UI_DESIGN.md Normal file
View File

@@ -0,0 +1,665 @@
# 数据分析页面 UI 设计文档
## 📋 文档说明
本文档记录了**数据分析中心页面**的 UI 设计实现,遵循项目的 UI 设计规范,采用现代简约的设计风格,提供清晰直观的数据可视化界面。
### 页面访问 URL
#### 主页面路径
- **完整路径**`/pages/mall/analytics/index`
- **页面标题**:数据分析中心
- **导航栏样式**:自定义导航栏(`navigationStyle: "custom"`
- **路由配置位置**`subPackages``pages/mall/analytics``index`
#### 相关子页面 URL
- **销售报表**`/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`
- **报表详情**`/pages/mall/analytics/report-detail`
- **数据分析详情**`/pages/mall/analytics/data-detail`
- **数据洞察详情**`/pages/mall/analytics/insight-detail`
### 访问方式
#### 1. 代码中跳转uni-app x
```typescript
// 方式一:使用 navigateTo推荐保留返回栈
uni.navigateTo({
url: '/pages/mall/analytics/index'
})
// 方式二:使用 redirectTo替换当前页面不保留返回栈
uni.redirectTo({
url: '/pages/mall/analytics/index'
})
// 方式三:使用 reLaunch关闭所有页面打开新页面
uni.reLaunch({
url: '/pages/mall/analytics/index'
})
```
#### 2. 从其他页面跳转示例
```typescript
// 从管理后台跳转到数据分析中心
const goToAnalytics = () => {
uni.navigateTo({
url: '/pages/mall/analytics/index'
})
}
// 从首页跳转到数据分析中心
const goToDataAnalysis = () => {
uni.navigateTo({
url: '/pages/mall/analytics/index',
success: () => {
console.log('跳转成功')
},
fail: (err) => {
console.error('跳转失败:', err)
}
})
}
```
#### 3. 带参数跳转
```typescript
// 跳转并传递参数
uni.navigateTo({
url: '/pages/mall/analytics/index?period=30d&refresh=true'
})
// 在目标页面接收参数index.uvue 的 onLoad
onLoad(options: any) {
const period = options.period || '30d'
const refresh = options.refresh === 'true'
if (period) {
this.period = period
}
if (refresh) {
this.refreshData()
}
}
```
#### 4. 页面间跳转
```typescript
// 从数据分析首页跳转到销售报表
const goToSalesReport = () => {
uni.navigateTo({
url: '/pages/mall/analytics/sales-report'
})
}
// 从数据分析首页跳转到用户分析
const goToUserAnalysis = () => {
uni.navigateTo({
url: '/pages/mall/analytics/user-analysis'
})
}
```
#### 5. 权限控制示例
```typescript
// 跳转前检查权限
const goToAnalyticsWithAuth = () => {
// 检查用户是否有数据分析权限
const userType = getUserType() // 假设这是获取用户类型的函数
if (userType === 'admin' || userType === 'analyst') {
uni.navigateTo({
url: '/pages/mall/analytics/index'
})
} else {
uni.showToast({
title: '您没有访问权限',
icon: 'none'
})
}
}
```
#### 6. 在页面配置中设置入口
如果需要在 TabBar 或其他导航中添加入口,可以在 `pages.json` 中配置:
```json
{
"tabBar": {
"list": [
{
"pagePath": "pages/mall/analytics/index",
"text": "数据分析"
}
]
}
}
```
### 设计原则
1. **数据优先**:突出核心数据指标,减少视觉干扰
2. **层次清晰**:通过卡片、阴影、间距建立清晰的信息层次
3. **视觉统一**:遵循项目统一的颜色系统和设计规范
4. **响应式适配**:适配不同屏幕尺寸,确保良好的用户体验
---
## 一、页面结构
### 1.1 整体布局
```
┌─────────────────────────────────────┐
│ Header头部控制面板
│ - 标题 + 最后更新时间 │
│ - 刷新/导出按钮 │
├─────────────────────────────────────┤
│ 实时大屏4个核心指标卡片
│ - GMV / 订单 / 用户 / 转化率 │
├─────────────────────────────────────┤
│ 时间筛选Tab切换
│ - 今日/本周/本月/本季度 │
├─────────────────────────────────────┤
│ 销售分析 │
│ - 销售趋势图 │
│ - 商品销量排行 │
├─────────────────────────────────────┤
│ 用户行为分析 │
│ - 用户活跃度 │
│ - 流量来源 │
├─────────────────────────────────────┤
│ 商家表现 │
│ - 商家排行榜 │
├─────────────────────────────────────┤
│ 配送效率 │
│ - 配送时效 │
│ - 配送覆盖 │
├─────────────────────────────────────┤
│ 快速分析工具6个工具卡片
│ - 销售报表/用户分析/商品洞察等 │
└─────────────────────────────────────┘
```
### 1.2 模块划分
1. **Header 区域**:页面标题、更新时间、操作按钮
2. **实时大屏**核心业务指标GMV、订单、用户、转化率
3. **时间筛选**:时间维度切换(今日/本周/本月/本季度)
4. **销售分析**:销售趋势、商品排行
5. **用户行为分析**:用户活跃度、流量来源
6. **商家表现**:商家销售排行榜
7. **配送效率**:配送时效、覆盖情况
8. **快速工具**:常用分析工具入口
---
## 二、设计规范应用
### 2.1 CSS 变量系统
所有设计 token 通过 CSS 变量定义,便于统一管理和换肤:
```css
:root {
/* 主题色 */
--theme-primary: #FF4D4F;
--theme-primary-light: #FF7875;
--theme-primary-dark: #CF1322;
--gradient-start: #FF4D4F;
--gradient-end: #FF7A45;
/* 功能色 */
--success: #52C41A;
--warning: #FAAD14;
--error: #FF4D4F;
--info: #1890FF;
/* 文字颜色 */
--text-primary: #111111;
--text-secondary: #333333;
--text-tertiary: #666666;
--text-disabled: #999999;
/* 背景颜色 */
--bg-primary: #FFFFFF;
--bg-secondary: #F7F8FA;
--bg-tertiary: #F5F5F5;
/* 边框 */
--border-color: rgba(0, 0, 0, 0.06);
/* 阴影 */
--shadow-sm: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
--shadow-md: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
/* 圆角 */
--radius-md: 12rpx;
--radius-lg: 20rpx;
--radius-xl: 24rpx;
/* 间距 */
--spacing-sm: 16rpx;
--spacing-md: 24rpx;
--spacing-lg: 32rpx;
}
```
### 2.2 卡片式设计
所有数据模块采用卡片式设计:
- **圆角**`border-radius: 20rpx`(符合 UI 规范)
- **阴影**`box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06)`(轻微阴影)
- **背景**:白色背景 `#FFFFFF`
- **内边距**`32rpx`(统一间距)
- **外边距**`24rpx`(卡片间距)
### 2.3 颜色系统应用
#### Header 渐变背景
```css
background: linear-gradient(135deg, #FF4D4F 0%, #FF7A45 100%);
```
#### 实时指标卡片渐变
- **GMV收入**`#FF6B6B → #FF4D4F`(红色渐变)
- **订单**`#4ECDC4 → #44A08D`(青色渐变)
- **用户**`#A8E6CF → #7FCDBB`(绿色渐变)
- **转化率**`#FFD93D → #FFA07A`(黄色渐变)
#### 状态颜色
- **增长(正)**:绿色 `#52C41A` + 浅绿背景
- **下降(负)**:红色 `#FF4D4F` + 浅红背景
- **中性**:灰色 `#666666` + 浅灰背景
---
## 三、组件设计详解
### 3.1 Header 头部
**设计特点**
- 使用主题色渐变背景
- 左侧显示标题和更新时间
- 右侧操作按钮使用毛玻璃效果
**实现代码**
```css
.header {
background: linear-gradient(135deg, var(--gradient-start) 0%, var(--gradient-end) 100%);
padding: var(--spacing-lg) var(--spacing-md) var(--spacing-md);
box-shadow: var(--shadow-md);
}
.refresh-btn,
.export-btn {
width: 64rpx;
height: 64rpx;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
backdrop-filter: blur(10rpx);
}
```
### 3.2 实时大屏卡片
**设计特点**
- 2x2 网格布局
- 每个卡片使用不同渐变背景
- 数值突出显示,增长指标使用标签样式
**布局**
```css
.dashboard-grid {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: var(--spacing-md);
}
.dashboard-card {
width: calc(50% - var(--spacing-md) / 2);
padding: var(--spacing-lg) var(--spacing-md);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
}
```
### 3.3 时间筛选 Tab
**设计特点**
- 激活状态使用主题色渐变
- 添加阴影效果增强层次
- 平滑过渡动画
**实现**
```css
.filter-tab.active {
background: linear-gradient(135deg, var(--gradient-start) 0%, var(--gradient-end) 100%);
color: #fff;
font-weight: 500;
box-shadow: 0 2rpx 8rpx rgba(255, 77, 79, 0.3);
}
```
### 3.4 销售分析卡片
**设计特点**
- 图表区域使用浅色背景区分
- 统计项使用独立卡片样式
- 数值使用主题色突出
**布局**
```css
.trend-stats {
display: flex;
justify-content: space-around;
gap: var(--spacing-md);
}
.stat-item {
flex: 1;
padding: var(--spacing-md);
background: var(--bg-secondary);
border-radius: var(--radius-md);
}
```
### 3.5 商品排行
**设计特点**
- 排名使用圆形徽章,使用主题色渐变
- 商品名称和销量清晰对比
- 统一的分割线
**实现**
```css
.rank-number {
width: 56rpx;
height: 56rpx;
background: linear-gradient(135deg, var(--gradient-start) 0%, var(--gradient-end) 100%);
border-radius: 50%;
box-shadow: var(--shadow-sm);
}
```
### 3.6 用户行为分析
**设计特点**
- 指标使用列表式展示
- 流量来源使用进度条可视化
- 进度条使用主题色渐变
**进度条实现**
```css
.progress-bar {
height: 100%;
background: linear-gradient(135deg, var(--gradient-start) 0%, var(--gradient-end) 100%);
border-radius: 12rpx;
transition: width 0.3s ease;
}
```
### 3.7 商家排行榜
**设计特点**
- 排名徽章使用金银铜渐变
- 增长率使用彩色标签
- 商家信息清晰展示
**排名徽章**
```css
.rank-gold {
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
}
.rank-silver {
background: linear-gradient(135deg, #C0C0C0 0%, #A0A0A0 100%);
}
.rank-bronze {
background: linear-gradient(135deg, #CD7F32 0%, #B8860B 100%);
}
```
### 3.8 快速工具卡片
**设计特点**
- 3 列网格布局
- 点击反馈效果
- 图标 + 标题 + 描述
**交互效果**
```css
.tool-card:active {
transform: scale(0.98);
box-shadow: var(--shadow-md);
}
```
---
## 四、响应式设计
### 4.1 断点设置
- **默认**>= 768rpx双列布局
- **小屏**< 768rpx单列布局
### 4.2 响应式规则
```css
@media screen and (max-width: 768rpx) {
/* 实时大屏卡片改为单列 */
.dashboard-card {
width: 100%;
}
/* 工具卡片改为 2 列 */
.tool-card {
width: calc(50% - var(--spacing-md) / 2);
}
/* 配送分析改为纵向布局 */
.delivery-metrics {
flex-direction: column;
}
}
```
### 4.3 适配策略
1. **实时大屏**:小屏下改为单列,保持卡片完整性
2. **工具卡片**:小屏下改为 2 列,提升空间利用率
3. **配送分析**:小屏下改为纵向堆叠,避免内容挤压
---
## 五、交互设计
### 5.1 操作反馈
- **按钮点击**:缩放效果 `transform: scale(0.98)`
- **Tab 切换**:平滑过渡动画 `transition: all 0.3s`
- **进度条**:宽度变化动画 `transition: width 0.3s ease`
### 5.2 数据刷新
- **刷新按钮**:点击后显示加载状态
- **数据更新**:更新时间实时显示
- **错误处理**:友好的错误提示
### 5.3 导出功能
- **导出选项**Excel / PDF / 图片
- **加载提示**:导出过程中显示加载动画
- **成功反馈**:导出成功后 Toast 提示
---
## 六、数据可视化
### 6.1 图表占位
当前使用占位符,后续可集成图表库:
- **销售趋势图**:折线图或面积图
- **商品销量排行**:柱状图或条形图
- **流量来源**:饼图或环形图
### 6.2 推荐图表库
- **uni-app x 兼容**:需使用原生图表组件
- **Web 端**:可使用 ECharts、Chart.js 等
- **移动端**:可使用 uni-charts 或自定义 Canvas 绘制
---
## 七、性能优化
### 7.1 数据加载
- **分步加载**:先加载核心指标,再加载详细数据
- **缓存策略**:合理使用数据缓存,减少重复请求
- **防抖处理**:刷新操作添加防抖,避免频繁请求
### 7.2 渲染优化
- **虚拟列表**:长列表使用虚拟滚动
- **懒加载**:非首屏内容延迟加载
- **图片优化**:使用合适的图片格式和尺寸
---
## 八、后续优化建议
### 8.1 功能增强
1. **实时数据推送**:使用 WebSocket 实现数据实时更新
2. **数据钻取**:点击指标可查看详细数据
3. **自定义看板**:允许用户自定义指标和布局
4. **数据对比**:支持多时间段数据对比
5. **导出增强**:支持自定义导出字段和格式
### 8.2 UI 优化
1. **图表集成**:集成专业图表库,替换占位符
2. **动画效果**:添加数据变化动画,提升视觉体验
3. **暗色模式**:支持暗色主题切换
4. **国际化**:支持多语言切换
### 8.3 交互优化
1. **下拉刷新**:支持下拉刷新数据
2. **上拉加载**:长列表支持上拉加载更多
3. **手势操作**:支持滑动切换时间维度
4. **快捷操作**:添加常用操作的快捷入口
---
## 九、技术实现
### 9.1 文件结构
```
pages/mall/analytics/
├── index.uvue # 数据分析首页
├── profile.uvue # 数据分析个人中心
└── report-detail.uvue # 报表详情页
```
### 9.2 核心代码结构
```vue
<template>
<!-- Header -->
<view class="header">...</view>
<!-- 实时大屏 -->
<view class="dashboard-section">...</view>
<!-- 时间筛选 -->
<view class="time-filter-section">...</view>
<!-- 各分析模块 -->
<view class="sales-analysis-section">...</view>
...
</template>
<script lang="uts">
// 数据定义
// 方法实现
</script>
<style>
/* CSS 变量定义 */
/* 组件样式 */
/* 响应式样式 */
</style>
```
### 9.3 数据接口
当前使用 Supabase 查询数据,主要涉及:
- `orders` 表:订单数据
- `users` 表:用户数据
- `user_sessions` 表:用户会话数据(如果存在)
---
## 十、设计规范遵循
### 10.1 颜色系统 ✅
- ✅ 使用主题色 `#FF4D4F` 作为主色调
- ✅ 使用渐变色增强视觉效果
- ✅ 使用功能色表示状态(成功/警告/错误)
### 10.2 间距系统 ✅
- ✅ 使用统一的间距变量(`--spacing-sm/md/lg`
- ✅ 卡片间距统一为 `24rpx`
- ✅ 内容内边距统一为 `32rpx`
### 10.3 圆角系统 ✅
- ✅ 卡片圆角统一为 `20rpx`
- ✅ 小元素圆角为 `12rpx`
- ✅ 圆形元素使用 `50%`
### 10.4 阴影系统 ✅
- ✅ 卡片使用轻微阴影 `0 2rpx 8rpx rgba(0, 0, 0, 0.06)`
- ✅ 激活状态使用增强阴影
- ✅ 按钮使用阴影增强层次
---
## 十一、总结
数据分析页面 UI 设计完全遵循项目的 UI 设计规范,实现了:
1.**统一的视觉风格**:使用项目统一的颜色系统和设计 token
2.**清晰的信息层次**:通过卡片、阴影、间距建立清晰层次
3.**良好的用户体验**:响应式设计、交互反馈、数据可视化
4.**可维护的代码**CSS 变量系统、模块化设计、清晰的代码结构
页面已实现核心功能,后续可根据实际需求进行功能增强和 UI 优化。
---
**文档版本**: v1.0
**创建时间**: 2025-01-XX
**最后更新**: 2025-01-XX
**状态**: ✅ 已完成

View File

@@ -0,0 +1,940 @@
# CRMEB 项目重构计划文档
## 📋 文档说明
本文档基于 **Clean-Room 重构原则**,通过分析 CRMEB 项目的**可观察行为与功能规格**,制定将 CRMEB 商城系统迁移到 **uni-app x (uvue) + Supabase** 技术栈的重构计划。
### 重构原则
1. **禁止复制源码**:不直接使用 CRMEB 的任何 PHP/Vue 源码
2. **规格驱动开发**:基于功能规格和行为描述进行独立实现
3. **技术栈迁移**
- 前端Vue/UniApp → **uvue (uni-app x)**
- 后端PHP/ThinkPHP → **Supabase (PostgreSQL + RLS + Edge Functions)**
4. **组件复用**:尽量不修改 `components/supadb`,仅通过接口调用
---
## 一、功能规格提取 (Spec Extraction)
### 1.1 核心业务模块
基于 CRMEB 项目结构分析,提取以下核心功能模块:
#### 1.1.1 用户系统 (User System)
**功能清单**
- 用户注册/登录(手机号、微信、邮箱)
- 用户信息管理(昵称、头像、性别)
- 用户角色(消费者、商家、配送员、客服、管理员)
- 用户认证(实名认证、商家认证)
- 用户地址管理(多地址、默认地址)
- 用户余额/积分管理
**数据字段规格**
- 用户基础信息id, phone, email, nickname, avatar_url, gender, user_type, status
- 用户扩展信息real_name, id_card, credit_score, verification_status
- 地址信息receiver_name, receiver_phone, province, city, district, address_detail, is_default
**权限矩阵**
- 用户只能查看/修改自己的信息
- 管理员可查看所有用户信息
- 商家可查看自己的店铺信息
#### 1.1.2 商品系统 (Product System)
**功能清单**
- 商品管理(创建、编辑、上架、下架)
- 商品分类(多级分类、分类树)
- 商品品牌管理
- 商品规格/SKU多规格、价格、库存
- 商品图片(主图、详情图、轮播图)
- 商品搜索(关键词、分类、品牌、价格区间)
- 商品排序(价格、销量、时间、综合)
**数据字段规格**
- 商品基础id, merchant_id, category_id, name, description, base_price, original_price, stock, sales, status
- 商品图片main_image_url, image_urls (JSONB)
- 商品规格sku_code, specifications (JSONB), price, stock
- 分类id, pid (parent_id), name, icon, sort
**业务规则**
- 商品状态0-下架1-上架2-审核中
- 库存扣减:下单时扣减,取消订单时恢复
- 价格计算:支持原价、现价、会员价
#### 1.1.3 订单系统 (Order System)
**功能清单**
- 订单创建(购物车结算、立即购买)
- 订单确认(地址选择、优惠券选择、运费计算)
- 订单支付(微信支付、支付宝、余额支付)
- 订单状态流转(待支付→已支付→已发货→已收货→已完成)
- 订单取消(用户取消、超时取消)
- 订单退款(申请退款、审核退款、退款完成)
- 订单评价(商品评价、商家回复)
**数据字段规格**
- 订单主表id, order_no, user_id, merchant_id, total_amount, discount_amount, delivery_fee, actual_amount, order_status, payment_status, payment_method, delivery_address (JSONB)
- 订单商品order_id, product_id, sku_id, product_name, price, quantity, total_amount
- 订单状态1-待支付2-已支付3-已发货4-已收货5-已完成6-已取消7-退款中8-已退款
**业务规则**
- 订单号生成唯一订单号ORD20240101000001
- 超时取消30分钟未支付自动取消
- 库存检查:下单时检查库存,库存不足提示
- 价格锁定:订单创建时锁定商品价格
#### 1.1.4 购物车系统 (Cart System)
**功能清单**
- 购物车添加商品、SKU、数量
- 购物车编辑(数量修改、删除)
- 购物车选择(单选、全选)
- 购物车结算(批量结算、价格计算)
**数据字段规格**
- 购物车id, user_id, product_id, sku_id, quantity, selected
**业务规则**
- 同一商品同一SKU合并数量
- 商品下架时从购物车移除提示
- 库存不足时提示并限制数量
#### 1.1.5 营销系统 (Marketing System)
**功能清单**
- 优惠券(满减券、折扣券、免运费券)
- 优惠券领取(用户领取、系统发放)
- 优惠券使用(下单时选择、自动抵扣)
- 拼团活动(创建拼团、参团、成团)
- 秒杀活动(限时秒杀、库存限制)
- 砍价活动(发起砍价、好友助力)
- 积分系统(积分获取、积分兑换)
- 签到奖励(每日签到、连续签到)
**数据字段规格**
- 优惠券模板id, name, coupon_type, discount_type, discount_value, min_order_amount, total_quantity, start_time, end_time, status
- 用户优惠券id, user_id, template_id, coupon_code, status, used_at, expire_at
- 优惠券类型1-满减2-折扣3-免运费4-新人券5-会员券
**业务规则**
- 优惠券有效期检查
- 优惠券使用条件(满额、指定商品、指定分类)
- 每人限领数量
#### 1.1.6 配送系统 (Delivery System)
**功能清单**
- 配送员管理(注册、认证、审核)
- 配送任务分配(自动分配、手动分配)
- 配送状态跟踪(待接单→已接单→已取货→配送中→已送达)
- 配送费用计算(距离、重量、时间)
**数据字段规格**
- 配送员id, user_id, real_name, id_card, vehicle_type, work_status, current_location (JSONB), service_areas (Array)
- 配送任务id, order_id, driver_id, pickup_address (JSONB), delivery_address (JSONB), distance, delivery_fee, status
**业务规则**
- 配送员认证审核
- 配送范围限制
- 配送费用计算规则
#### 1.1.7 评价系统 (Review System)
**功能清单**
- 商品评价(评分、文字、图片)
- 商家回复
- 评价展示(全部、好评、中评、差评)
- 评价统计(好评率、平均分)
**数据字段规格**
- 评价id, order_id, product_id, user_id, rating, content, images (Array), reply_content, reply_time, status
**业务规则**
- 仅已收货订单可评价
- 评价后不可修改,可追评
- 匿名评价选项
#### 1.1.8 店铺系统 (Shop System)
**功能清单**
- 店铺创建(商家注册店铺)
- 店铺信息管理名称、Logo、简介、轮播图
- 店铺认证(营业执照、资质审核)
- 店铺营业状态(营业中、休息中、关闭)
- 店铺评分(综合评分、服务评分、商品评分)
**数据字段规格**
- 店铺id, merchant_id, shop_name, shop_logo, shop_banner, shop_description, shop_status, rating, total_sales
**业务规则**
- 商家认证后才能开店
- 店铺关闭后商品自动下架
- 店铺评分计算规则
### 1.2 管理后台功能
#### 1.2.1 商品管理
- 商品列表(搜索、筛选、排序)
- 商品审核(上架审核、下架管理)
- 分类管理(分类树、排序)
- 品牌管理
#### 1.2.2 订单管理
- 订单列表(状态筛选、搜索)
- 订单详情查看
- 订单发货
- 退款审核
#### 1.2.3 用户管理
- 用户列表
- 用户详情
- 用户状态管理(冻结、解冻)
#### 1.2.4 营销管理
- 优惠券管理(创建、发放、统计)
- 活动管理(拼团、秒杀、砍价)
#### 1.2.5 系统设置
- 系统配置(支付配置、物流配置)
- 权限管理
- 数据统计
### 1.3 前端页面清单
#### 1.3.1 消费者端 (Consumer)
- 首页(轮播图、分类导航、推荐商品、热门商品)
- 分类页(分类列表、商品列表)
- 商品详情页商品信息、SKU选择、评价、推荐
- 购物车页
- 订单确认页
- 订单列表页
- 订单详情页
- 个人中心(信息、订单、地址、优惠券、收藏)
- 搜索页
- 评价页
#### 1.3.2 商家端 (Merchant)
- 商家中心首页(数据统计、订单概览)
- 商品管理(列表、添加、编辑)
- 订单管理(列表、详情、发货)
- 店铺设置
- 数据统计
#### 1.3.3 配送端 (Delivery)
- 配送中心首页(待接单、配送中、已完成)
- 订单详情
- 配送路线
#### 1.3.4 管理后台 (Admin)
- 登录页
- 首页(数据概览)
- 商品管理
- 订单管理
- 用户管理
- 营销管理
- 系统设置
---
## 二、架构映射 (Architecture Mapping)
### 2.1 前端架构映射Vue/UniApp → uvue
#### 2.1.1 技术栈对比
| CRMEB 原技术 | 目标技术 (uvue) | 说明 |
| ------------ | ------------------------------ | -------------- |
| Vue 2/3 | `<script setup lang="uts">` | 使用 UTS 语法 |
| Vuex | `ref/reactive` + 状态管理工具 | 响应式状态管理 |
| uni-app | uni-app x | 跨平台框架 |
| .vue 文件 | .uvue 文件 | 页面/组件文件 |
| axios | `components/supadb/aksupa.uts` | HTTP 请求封装 |
#### 2.1.2 页面结构映射
**CRMEB 页面结构**
```
template/uni-app/
├── pages/
│ ├── index/ # 首页
│ ├── goods_details/ # 商品详情
│ ├── order/ # 订单相关
│ └── user/ # 用户中心
```
**目标项目结构**
```
pages/mall/
├── consumer/ # 消费者端
│ ├── index.uvue # 首页
│ ├── product-detail.uvue
│ ├── cart.uvue
│ ├── order-detail.uvue
│ └── profile.uvue
├── merchant/ # 商家端
│ ├── index.uvue
│ └── product-detail.uvue
├── delivery/ # 配送端
│ └── index.uvue
└── admin/ # 管理后台
└── index.uvue
```
#### 2.1.3 组件映射
**CRMEB 组件****目标组件**
- `goods-list``components/mall/ProductList.uvue`
- `order-item``components/mall/OrderItem.uvue`
- `address-selector``components/mall/AddressSelector.uvue`
- `coupon-selector``components/mall/CouponSelector.uvue`
#### 2.1.4 状态管理映射
**CRMEB (Vuex)**
```javascript
// store/modules/user.js
state: { userInfo: null }
mutations: { SET_USER_INFO }
actions: { getUserInfo }
```
**目标项目 (uvue)**
```typescript
// utils/store.uts
export const useUserStore = () => {
const userInfo = ref<UserType | null>(null)
const getUserInfo = async () => {
// 使用 supadb 获取用户信息
}
return { userInfo, getUserInfo }
}
```
### 2.2 后端架构映射PHP/ThinkPHP → Supabase
#### 2.2.1 技术栈对比
| CRMEB 原技术 | 目标技术 (Supabase) | 说明 |
| -------------- | ----------------------------- | ------------ |
| ThinkPHP 6 | Supabase (PostgreSQL) | 数据库 + API |
| MySQL | PostgreSQL | 数据库 |
| PHP Controller | Supabase RLS + Edge Functions | 业务逻辑 |
| PHP Service | Database Functions + Triggers | 业务处理 |
| Session | Supabase Auth (JWT) | 认证 |
| File Upload | Supabase Storage | 文件存储 |
#### 2.2.2 API 映射
**CRMEB API 结构**
```
/api/v1/store/product/lst # 商品列表
/api/v1/store/product/detail # 商品详情
/api/v1/order/confirm # 订单确认
/api/v1/order/create # 创建订单
```
**Supabase API 结构**
```
GET /rest/v1/ml_products # 商品列表PostgREST 自动生成)
GET /rest/v1/ml_products?id=eq.{id} # 商品详情
POST /rest/v1/rpc/create_order # 创建订单Edge Function
POST /rest/v1/rpc/confirm_order # 订单确认Edge Function
```
#### 2.2.3 数据表映射
**CRMEB 数据表****Supabase 数据表**
| CRMEB 表名 | Supabase 表名 | 说明 |
| ----------------------- | --------------------- | ---------- |
| `eb_store_product` | `ml_products` | 商品表 |
| `eb_store_order` | `ml_orders` | 订单表 |
| `eb_store_cart` | `ml_shopping_cart` | 购物车 |
| `eb_user` | `ak_users` (复用) | 用户表 |
| `eb_store_category` | `ml_categories` | 分类表 |
| `eb_store_coupon_issue` | `ml_coupon_templates` | 优惠券模板 |
| `eb_store_coupon_user` | `ml_user_coupons` | 用户优惠券 |
#### 2.2.4 业务逻辑映射
**CRMEB PHP Service****Supabase 实现方式**
1. **商品列表查询**
- **原实现**`StoreProductServices::getSearchList()`
- **新实现**PostgREST 查询 + RLS 策略
```typescript
// 前端调用
await supa.from('ml_products')
.select('*')
.eq('status', 1)
.order('created_at', { ascending: false })
.limit(20)
```
2. **订单创建**
- **原实现**`StoreOrderCreateServices::createOrder()`
- **新实现**Edge Function 或 Database Function
```sql
-- Database Function
CREATE FUNCTION create_order(...) RETURNS uuid AS $$
BEGIN
-- 库存检查
-- 价格计算
-- 订单创建
-- 库存扣减
END;
$$ LANGUAGE plpgsql;
```
3. **权限控制**
- **原实现**PHP Middleware + Session
- **新实现**Supabase RLS 策略
```sql
-- RLS 策略示例
CREATE POLICY ml_orders_select_policy ON ml_orders
FOR SELECT USING (
auth.uid()::text = (SELECT auth_id FROM ak_users WHERE id = user_id)
);
```
#### 2.2.5 文件上传映射
**CRMEB**
- PHP 文件上传处理
- 本地存储或云存储OSS
**Supabase**
- Supabase Storage
- 公共/私有存储桶
- 自动生成访问 URL
```typescript
// 文件上传
await supa.storage.from('product-images').upload(filePath, file)
```
### 2.3 认证与权限映射
#### 2.3.1 认证方式
**CRMEB**
- Session 认证
- JWT Token部分接口
**Supabase**
- Supabase Auth
- JWT Token自动管理
- 多种登录方式邮箱、手机、微信、OAuth
```typescript
// 登录
await supa.auth.signInWithPassword({ email, password })
// Token 自动附加到后续请求
```
#### 2.3.2 权限控制
**CRMEB**
- PHP Middleware 权限检查
- 角色权限表
**Supabase**
- RLS (Row Level Security) 行级安全
- 基于 `auth.uid()` 的自动过滤
---
## 三、实现计划 (Implementation Plan)
### 3.1 目录结构设计
```
项目根目录/
├── pages/
│ └── mall/
│ ├── consumer/ # 消费者端页面
│ │ ├── index.uvue
│ │ ├── product-detail.uvue
│ │ ├── cart.uvue
│ │ ├── order-confirm.uvue
│ │ ├── order-list.uvue
│ │ ├── order-detail.uvue
│ │ ├── profile.uvue
│ │ ├── address-list.uvue
│ │ ├── coupon-list.uvue
│ │ └── search.uvue
│ ├── merchant/ # 商家端页面
│ │ ├── index.uvue
│ │ ├── product-list.uvue
│ │ ├── product-edit.uvue
│ │ ├── order-list.uvue
│ │ └── shop-settings.uvue
│ ├── delivery/ # 配送端页面
│ │ ├── index.uvue
│ │ └── order-detail.uvue
│ └── admin/ # 管理后台
│ ├── index.uvue
│ ├── product-management.uvue
│ ├── order-management.uvue
│ └── user-management.uvue
├── components/
│ ├── supadb/ # Supabase 封装(不修改)
│ └── mall/ # 商城业务组件
│ ├── ProductList.uvue
│ ├── ProductCard.uvue
│ ├── OrderItem.uvue
│ ├── AddressSelector.uvue
│ ├── CouponSelector.uvue
│ └── PaymentMethod.uvue
├── services/ # 业务服务层
│ └── mall/
│ ├── product-service.uts
│ ├── order-service.uts
│ ├── cart-service.uts
│ ├── coupon-service.uts
│ └── user-service.uts
├── types/
│ └── mall-types.uts # 类型定义(已存在)
└── utils/
└── mall-utils.uts # 工具函数
```
### 3.2 数据库设计
#### 3.2.1 数据表清单
基于功能规格,设计以下数据表(使用 `ml_` 前缀):
1. **用户相关**
- `ml_user_profiles` - 用户扩展信息
- `ml_user_addresses` - 用户地址
2. **商品相关**
- `ml_products` - 商品主表
- `ml_product_skus` - 商品SKU
- `ml_categories` - 商品分类
- `ml_brands` - 品牌表
- `ml_product_specs` - 商品规格模板
3. **店铺相关**
- `ml_shops` - 店铺表
4. **订单相关**
- `ml_orders` - 订单主表
- `ml_order_items` - 订单商品明细
5. **购物车**
- `ml_shopping_cart` - 购物车
6. **营销相关**
- `ml_coupon_templates` - 优惠券模板
- `ml_user_coupons` - 用户优惠券
7. **配送相关**
- `ml_delivery_drivers` - 配送员
- `ml_delivery_tasks` - 配送任务
8. **评价相关**
- `ml_product_reviews` - 商品评价
9. **用户行为**
- `ml_user_favorites` - 收藏
- `ml_browse_history` - 浏览历史
- `ml_search_history` - 搜索记录
10. **系统配置**
- `ml_system_configs` - 系统配置
- `ml_regions` - 地区数据
#### 3.2.2 数据库脚本位置
- 主数据库脚本:`doc_mall/database/complete_mall_database.sql`
- RLS 策略脚本:`doc_mall/database/rls_policies.sql`
- 触发器脚本:`doc_mall/database/triggers.sql`
- 函数脚本:`doc_mall/database/functions.sql`
### 3.3 开发里程碑
#### 阶段 1基础架构搭建1-2周
- [ ] 数据库表结构创建
- [ ] RLS 策略配置
- [ ] 触发器与函数创建
- [ ] 类型定义完善(`types/mall-types.uts`
- [ ] 服务层基础封装(`services/mall/`
#### 阶段 2用户系统1周
- [ ] 用户注册/登录页面
- [ ] 用户信息管理
- [ ] 地址管理功能
- [ ] 权限验证
#### 阶段 3商品系统2周
- [ ] 商品列表页
- [ ] 商品详情页
- [ ] 商品搜索
- [ ] 分类导航
- [ ] 商家商品管理
#### 阶段 4购物车与订单2周
- [ ] 购物车功能
- [ ] 订单确认页
- [ ] 订单创建
- [ ] 订单列表
- [ ] 订单详情
- [ ] 订单支付集成支付SDK
#### 阶段 5营销系统1-2周
- [ ] 优惠券功能
- [ ] 优惠券使用
- [ ] 活动页面(拼团、秒杀等,可选)
#### 阶段 6管理后台2周
- [ ] 管理员登录
- [ ] 商品管理
- [ ] 订单管理
- [ ] 用户管理
- [ ] 数据统计
#### 阶段 7测试与优化1-2周
- [ ] 功能测试
- [ ] 性能优化
- [ ] 安全测试
- [ ] 用户体验优化
### 3.4 关键技术实现点
#### 3.4.1 使用 supadb 组件(不修改)
```typescript
// 引入 supadb 实例
import supa from '@/components/supadb/aksupainstance.uts'
// 查询商品列表
const getProducts = async (filters: any) => {
const result = await supa
.from('ml_products')
.select('*')
.eq('status', 1)
.order('created_at', { ascending: false })
.limit(20)
.executeAs<ProductType[]>()
return result.data || []
}
// 创建订单
const createOrder = async (orderData: any) => {
const result = await supa
.from('ml_orders')
.insert(orderData)
.executeAs<OrderType>()
return result.data
}
```
#### 3.4.2 服务层封装
```typescript
// services/mall/product-service.uts
import supa from '@/components/supadb/aksupainstance.uts'
import type { ProductType } from '@/types/mall-types.uts'
export class ProductService {
// 获取商品列表
static async getProducts(params: {
categoryId?: string
keyword?: string
minPrice?: number
maxPrice?: number
page?: number
pageSize?: number
}) {
let query = supa.from('ml_products').select('*')
if (params.categoryId) {
query = query.eq('category_id', params.categoryId)
}
if (params.keyword) {
query = query.ilike('name', `%${params.keyword}%`)
}
if (params.minPrice) {
query = query.gte('base_price', params.minPrice)
}
if (params.maxPrice) {
query = query.lte('base_price', params.maxPrice)
}
const page = params.page || 1
const pageSize = params.pageSize || 20
const from = (page - 1) * pageSize
const to = from + pageSize - 1
query = query.range(from, to).order('created_at', { ascending: false })
const result = await query.executeAs<ProductType[]>()
return result.data || []
}
// 获取商品详情
static async getProductById(id: string) {
const result = await supa
.from('ml_products')
.select('*')
.eq('id', id)
.single()
.executeAs<ProductType>()
return result.data
}
}
```
#### 3.4.3 页面组件实现
```vue
<!-- pages/mall/consumer/product-detail.uvue -->
<template>
<view class="product-detail">
<view class="product-images">
<swiper>
<swiper-item v-for="img in product.image_urls" :key="img">
<image :src="img" mode="aspectFill" />
</swiper-item>
</swiper>
</view>
<view class="product-info">
<text class="product-name">{{ product.name }}</text>
<text class="product-price">¥{{ product.base_price }}</text>
</view>
<!-- SKU 选择 -->
<view class="sku-selector">
<!-- SKU 选择组件 -->
</view>
<!-- 操作按钮 -->
<view class="action-buttons">
<button @click="addToCart">加入购物车</button>
<button @click="buyNow">立即购买</button>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import { ProductService } from '@/services/mall/product-service.uts'
import type { ProductType } from '@/types/mall-types.uts'
const props = defineProps<{
productId: string
}>()
const product = ref<ProductType | null>(null)
onMounted(async () => {
product.value = await ProductService.getProductById(props.productId)
})
const addToCart = async () => {
// 加入购物车逻辑
}
const buyNow = async () => {
// 立即购买逻辑
}
</script>
```
---
## 四、验证清单 (Verification Checklist)
### 4.1 功能验证
#### 4.1.1 用户系统
- [ ] 用户注册(手机号、邮箱)
- [ ] 用户登录(密码、验证码)
- [ ] 用户信息修改
- [ ] 地址添加/编辑/删除
- [ ] 默认地址设置
#### 4.1.2 商品系统
- [ ] 商品列表展示(分页、筛选、排序)
- [ ] 商品详情展示
- [ ] 商品搜索
- [ ] 分类导航
- [ ] SKU 选择
- [ ] 商家商品管理(增删改查)
#### 4.1.3 购物车
- [ ] 添加商品到购物车
- [ ] 修改购物车商品数量
- [ ] 删除购物车商品
- [ ] 购物车商品选择
- [ ] 购物车价格计算
#### 4.1.4 订单系统
- [ ] 订单确认页(地址、优惠券、运费)
- [ ] 创建订单
- [ ] 订单支付
- [ ] 订单列表(状态筛选)
- [ ] 订单详情
- [ ] 订单取消
- [ ] 订单退款
#### 4.1.5 营销系统
- [ ] 优惠券列表
- [ ] 优惠券领取
- [ ] 优惠券使用
- [ ] 优惠券过期处理
### 4.2 权限验证
- [ ] 用户只能查看/修改自己的数据
- [ ] 商家只能管理自己的商品和订单
- [ ] 管理员可以查看所有数据
- [ ] 未登录用户只能查看公开商品
- [ ] RLS 策略正确生效
### 4.3 性能验证
- [ ] 商品列表加载速度(< 2秒
- [ ] 图片加载优化(懒加载)
- [ ] 分页加载流畅
- [ ] 数据库查询性能(索引优化)
### 4.4 安全验证
- [ ] SQL 注入防护(参数化查询)
- [ ] XSS 防护(数据转义)
- [ ] 权限验证RLS
- [ ] 敏感数据加密
### 4.5 兼容性验证
- [ ] Android 平台
- [ ] iOS 平台(如支持)
- [ ] Web 平台(如支持)
- [ ] 不同屏幕尺寸适配
---
## 五、差异清单 (Difference List)
### 5.1 允许的差异
1. **UI 风格差异**
- 可以重新设计 UI不要求与 CRMEB 完全一致
- 遵循现代 UI 设计规范
2. **交互细节差异**
- 动画效果可以不同
- 页面布局可以优化
3. **非核心功能差异**
- 某些营销活动(如砍价、拼团)可以简化或延后实现
- 数据统计功能可以简化
### 5.2 不允许的差异
1. **核心业务流程**
- 订单流程必须完整(创建→支付→发货→收货→完成)
- 库存扣减逻辑必须正确
- 价格计算必须准确
2. **数据一致性**
- 订单金额必须准确
- 库存数据必须准确
- 用户余额必须准确
3. **权限控制**
- 用户权限必须正确
- 数据访问权限必须正确
---
## 六、反抄袭自证
### 6.1 参考资料说明
本文档仅参考以下公开资料:
1. **CRMEB 项目 README.md**
- 仅用于了解项目功能模块和业务范围
- 未参考任何 PHP 源码实现
2. **Supabase 官方文档**
- https://supabase.com/docs
- 用于了解 Supabase API 使用方式
3. **uni-app x 官方文档**
- https://uniapp.dcloud.net.cn/uni-app-x/
- 用于了解 uvue 语法和规范
4. **PostgreSQL 官方文档**
- https://www.postgresql.org/docs/
- 用于了解数据库功能和语法
### 6.2 实现方式说明
1. **数据库设计**
- 基于业务需求独立设计表结构
- 使用 `ml_` 前缀区分商城模块
- 参考现有 `doc_mall/database/` 中的设计
2. **前端实现**
- 使用 uvue 语法独立编写页面
- 通过 `components/supadb` 调用 Supabase API
- 不复制任何 Vue 组件代码
3. **后端实现**
- 使用 Supabase RLS + Edge Functions
- 不编写任何 PHP 代码
- 业务逻辑通过数据库函数和触发器实现
### 6.3 代码原创性声明
- 所有代码均为基于功能规格的独立实现
- 未复制 CRMEB 项目的任何源码
- 未复制任何第三方项目的实现代码
- 仅参考公开 API 文档和规范
---
## 七、参考资料列表
### 7.1 官方文档
1. **Supabase**
- REST API: https://supabase.com/docs/reference/javascript/introduction
- Auth: https://supabase.com/docs/guides/auth
- RLS: https://supabase.com/docs/guides/auth/row-level-security
- Storage: https://supabase.com/docs/guides/storage
2. **uni-app x**
- 概述: https://uniapp.dcloud.net.cn/uni-app-x/
- uvue: https://uniapp.dcloud.net.cn/uni-app-x/component/
- UTS: https://uniapp.dcloud.net.cn/uni-app-x/uts/
3. **PostgreSQL**
- 官方文档: https://www.postgresql.org/docs/
- JSONB: https://www.postgresql.org/docs/current/datatype-json.html
### 7.2 项目内部文档
1. `doc_mall/MODULE_ANALYSIS.md` - 模块分析报告
2. `doc_mall/database/` - 数据库设计文档
3. `components/supadb/SIMPLIFIED_API_GUIDE.md` - Supabase API 使用指南
4. `types/mall-types.uts` - 类型定义
---
## 八、总结
本文档基于 **Clean-Room 重构原则**,通过分析 CRMEB 项目的功能规格,制定了完整的重构计划:
1. **功能规格提取**:明确了所有核心业务模块的功能需求
2. **架构映射**:完成了 Vue→uvue、PHP→Supabase 的技术栈映射
3. **实现计划**:制定了详细的开发里程碑和目录结构
4. **验证清单**:提供了完整的测试验证标准
所有实现将基于功能规格独立开发,不复制任何源码,确保代码的原创性和合规性。
---
**文档版本**: v1.0
**创建时间**: 2025-01-XX
**状态**: ✅ 待实施

View File

@@ -0,0 +1,660 @@
# UI 设计规范文档
## 📋 文档说明
本文档基于 **CRMEB 项目的 UI 设计理念和交互模式**,提取设计思想、布局方式、组件模式等,为项目提供统一的 UI 设计规范。**所有实现均为原创,不复制任何源码**。
### 设计原则
1. **参考不复制**:仅参考 CRMEB 的设计理念,不复制任何代码
2. **现代简约**:遵循现代电商 UI 设计趋势
3. **用户体验优先**:注重交互流畅性和视觉舒适度
4. **一致性**:保持整体设计风格统一
---
## 一、设计风格分析
### 1.1 CRMEB 设计特点
基于对 CRMEB 项目的分析,提取以下设计特点:
#### 1.1.1 视觉风格
- **色彩**:以红色系为主色调(#E93323),体现电商活力
- **圆角**大量使用圆角设计20rpx-24rpx柔和现代
- **阴影**:轻微阴影效果,增强层次感
- **间距**:宽松的间距设计,提升可读性
#### 1.1.2 布局特点
- **卡片式设计**:信息以卡片形式呈现,清晰分组
- **瀑布流布局**:商品列表采用瀑布流,提升浏览体验
- **模块化设计**:首页采用模块化组件,灵活配置
- **响应式适配**:适配不同屏幕尺寸
#### 1.1.3 交互特点
- **流畅动画**:页面切换和操作有平滑过渡
- **即时反馈**:操作有明确的视觉反馈
- **加载状态**:优雅的加载动画和骨架屏
- **错误处理**:友好的错误提示和空状态
---
## 二、颜色系统
### 2.1 主色调
基于 CRMEB 的设计理念,定义以下颜色系统:
```scss
// 主题色
$theme-primary: #FF4D4F; // 主色(红色系,体现电商活力)
$theme-primary-light: #FF7875; // 主色浅色
$theme-primary-dark: #CF1322; // 主色深色
// 渐变色
$gradient-start: #FF4D4F; // 渐变起始色
$gradient-end: #FF7A45; // 渐变结束色
// 功能色
$success: #52C41A; // 成功色
$warning: #FAAD14; // 警告色
$error: #FF4D4F; // 错误色
$info: #1890FF; // 信息色
```
### 2.2 中性色
```scss
// 文字颜色
$text-primary: #111111; // 主要文字
$text-secondary: #333333; // 次要文字
$text-tertiary: #666666; // 辅助文字
$text-disabled: #999999; // 禁用文字
$text-placeholder: #CCCCCC; // 占位文字
// 背景颜色
$bg-primary: #FFFFFF; // 主背景
$bg-secondary: #F7F8FA; // 次背景
$bg-tertiary: #F5F5F5; // 三级背景
$bg-hover: #F1F1F1; // 悬停背景
// 边框颜色
$border-color: rgba(0, 0, 0, 0.06); // 边框色
$border-color-light: rgba(0, 0, 0, 0.08); // 浅边框
```
### 2.3 使用规范
- **主色**:用于按钮、链接、重要信息
- **渐变色**:用于主要操作按钮、强调元素
- **中性色**:用于文字、背景、边框
- **功能色**:用于状态提示、警告信息
---
## 三、布局规范
### 3.1 间距系统
```scss
// 基础间距单位(基于 rpx
$spacing-xs: 8rpx; // 极小间距
$spacing-sm: 16rpx; // 小间距
$spacing-md: 24rpx; // 中等间距
$spacing-lg: 32rpx; // 大间距
$spacing-xl: 48rpx; // 超大间距
// 页面边距
$page-padding: 24rpx; // 页面左右边距
$section-margin: 20rpx; // 模块间距
```
### 3.2 圆角规范
```scss
$radius-sm: 8rpx; // 小圆角
$radius-md: 12rpx; // 中等圆角
$radius-lg: 20rpx; // 大圆角
$radius-xl: 24rpx; // 超大圆角
$radius-circle: 50%; // 圆形
```
### 3.3 阴影规范
```scss
// 轻微阴影(卡片)
$shadow-sm: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
// 中等阴影(悬浮)
$shadow-md: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
// 大阴影(弹窗)
$shadow-lg: 0 8rpx 24rpx rgba(0, 0, 0, 0.12);
```
### 3.4 布局模式
#### 3.4.1 卡片布局
- **特点**:信息以卡片形式呈现,有圆角和阴影
- **应用**:商品卡片、订单卡片、信息卡片
- **实现**:使用 `view` 容器,添加圆角和阴影样式
#### 3.4.2 列表布局
- **特点**:信息以列表形式呈现,清晰有序
- **应用**:订单列表、地址列表、设置列表
- **实现**:使用 `view` 容器,添加分割线
#### 3.4.3 网格布局
- **特点**:信息以网格形式呈现,整齐排列
- **应用**:商品网格、分类网格、功能入口
- **实现**:使用 `view` 容器,配合 flex 布局
---
## 四、组件设计规范
### 4.1 按钮组件
#### 4.1.1 主要按钮Primary
```scss
// 样式特点
- 背景渐变色#FF4D4F #FF7A45
- 文字白色
- 圆角18rpx
- 高度92rpx
- 阴影0 16rpx 32rpx rgba(255, 77, 79, 0.24)
```
#### 4.1.2 次要按钮Secondary
```scss
// 样式特点
- 背景白色
- 文字主题色
- 边框1rpx solid 主题色
- 圆角18rpx
- 高度92rpx
```
#### 4.1.3 文字按钮Text
```scss
// 样式特点
- 背景透明
- 文字主题色
- 无边框
- 无圆角
```
### 4.2 输入框组件
#### 4.2.1 标准输入框
```scss
// 样式特点
- 背景#F6F7F9
- 边框2rpx solid rgba(0, 0, 0, 0.06)
- 圆角14rpx
- 高度84rpx
- 内边距0 14rpx
- 错误状态边框变红背景变浅红
```
### 4.3 商品卡片组件
#### 4.3.1 商品列表卡片
```scss
// 布局特点
- 图片180rpx × 180rpx圆角 20rpx
- 信息商品名称价格销量
- 间距左右 30rpx上下 20rpx
- 活动标签左上角显示秒杀/砍价/拼团
```
#### 4.3.2 商品网格卡片
```scss
// 布局特点
- 图片宽高比 1:1圆角 20rpx
- 信息商品名称价格原价删除线
- 间距网格间距 16rpx
- 两列或三列布局
```
### 4.4 订单卡片组件
#### 4.4.1 订单列表卡片
```scss
// 布局特点
- 背景白色
- 圆角20rpx
- 内边距24rpx
- 阴影轻微阴影
- 内容订单号商品信息价格状态
```
### 4.5 导航栏组件
#### 4.5.1 顶部导航栏
```scss
// 样式特点
- 背景白色或透明滚动时变化
- 高度88rpx含状态栏
- 文字主题色或白色
- 返回按钮左侧
- 搜索框居中可选
```
### 4.6 标签组件
#### 4.6.1 活动标签
```scss
// 样式特点
- 背景主题色或渐变色
- 文字白色
- 圆角4rpx 圆形
- 位置商品图片左上角
- 文字秒杀/砍价/拼团
```
---
## 五、交互模式
### 5.1 页面切换
- **动画**:使用 uni-app 的页面切换动画
- **返回**支持滑动返回iOS 风格)
- **加载**:页面切换时显示加载状态
### 5.2 数据加载
- **骨架屏**:数据加载时显示骨架屏
- **下拉刷新**:列表页面支持下拉刷新
- **上拉加载**:列表页面支持上拉加载更多
- **加载状态**:显示加载动画和提示文字
### 5.3 操作反馈
- **点击反馈**:使用 `hover-class` 提供点击反馈
- **成功提示**:使用 `uni.showToast` 显示成功提示
- **错误提示**:使用 `uni.showModal` 显示错误信息
- **加载提示**:使用 `uni.showLoading` 显示加载状态
### 5.4 表单交互
- **实时验证**:输入时实时验证,显示错误信息
- **提交反馈**:提交时显示加载状态,防止重复提交
- **成功跳转**:提交成功后自动跳转或提示
---
## 六、页面设计规范
### 6.1 首页设计
#### 6.1.1 布局结构
```
┌─────────────────────────┐
│ 顶部搜索栏(可选) │
├─────────────────────────┤
│ 轮播图Banner
├─────────────────────────┤
│ 分类导航(横向滚动) │
├─────────────────────────┤
│ 营销模块(可选) │
│ - 秒杀/拼团/砍价 │
├─────────────────────────┤
│ 商品推荐 │
│ - 瀑布流布局 │
└─────────────────────────┘
```
#### 6.1.2 设计要点
- **模块化**:首页由多个模块组成,可配置
- **瀑布流**:商品列表采用瀑布流布局
- **懒加载**:图片和内容懒加载,提升性能
- **下拉刷新**:支持下拉刷新
### 6.2 商品详情页
#### 6.2.1 布局结构
```
┌─────────────────────────┐
│ 商品轮播图 │
├─────────────────────────┤
│ 商品信息 │
│ - 价格(突出显示) │
│ - 标题 │
│ - 标签 │
├─────────────────────────┤
│ 优惠信息 │
│ - 优惠券 │
│ - 活动 │
├─────────────────────────┤
│ 规格选择(弹窗) │
├─────────────────────────┤
│ 商品详情Tab切换
│ - 详情 │
│ - 评价 │
│ - 推荐 │
├─────────────────────────┤
│ 底部操作栏(固定) │
│ - 购物车/立即购买 │
└─────────────────────────┘
```
#### 6.2.2 设计要点
- **图片展示**:轮播图展示商品图片
- **价格突出**:价格使用大字号和主题色
- **规格选择**:点击规格弹出选择弹窗
- **底部固定**:操作按钮固定在底部
### 6.3 购物车页面
#### 6.3.1 布局结构
```
┌─────────────────────────┐
│ 商品列表 │
│ ┌───────────────────┐ │
│ │ ☑ 商品卡片 │ │
│ │ 数量选择 │ │
│ └───────────────────┘ │
├─────────────────────────┤
│ 底部结算栏(固定) │
│ - 全选 │
│ - 合计金额 │
│ - 结算按钮 │
└─────────────────────────┘
```
#### 6.3.2 设计要点
- **选择状态**:每个商品可单独选择
- **数量编辑**:支持增减数量
- **价格计算**:实时计算总价
- **底部固定**:结算栏固定在底部
### 6.4 订单确认页
#### 6.4.1 布局结构
```
┌─────────────────────────┐
│ 收货地址(可选) │
├─────────────────────────┤
│ 商品列表 │
├─────────────────────────┤
│ 优惠券选择(可选) │
├─────────────────────────┤
│ 配送方式 │
├─────────────────────────┤
│ 备注信息 │
├─────────────────────────┤
│ 价格明细 │
│ - 商品总额 │
│ - 运费 │
│ - 优惠金额 │
│ - 实付金额 │
├─────────────────────────┤
│ 提交订单按钮(固定) │
└─────────────────────────┘
```
### 6.5 个人中心页
#### 6.5.1 布局结构
```
┌─────────────────────────┐
│ 用户信息卡片 │
│ - 头像 │
│ - 昵称 │
│ - 会员信息 │
├─────────────────────────┤
│ 订单状态(快捷入口) │
│ - 待付款/待发货等 │
├─────────────────────────┤
│ 功能菜单 │
│ - 我的订单 │
│ - 我的地址 │
│ - 我的优惠券 │
│ - 设置 │
└─────────────────────────┘
```
---
## 七、实现指南
### 7.1 样式变量定义
创建 `styles/variables.uts` 文件:
```typescript
// 颜色变量
export const THEME_PRIMARY = '#FF4D4F'
export const THEME_GRADIENT_START = '#FF4D4F'
export const THEME_GRADIENT_END = '#FF7A45'
// 间距变量
export const SPACING_XS = '8rpx'
export const SPACING_SM = '16rpx'
export const SPACING_MD = '24rpx'
export const SPACING_LG = '32rpx'
// 圆角变量
export const RADIUS_SM = '8rpx'
export const RADIUS_MD = '12rpx'
export const RADIUS_LG = '20rpx'
```
### 7.2 通用样式类
创建 `styles/common.uvue` 或使用 `<style>` 标签:
```css
/* 卡片样式 */
.card {
background: #FFFFFF;
border-radius: 20rpx;
padding: 24rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
/* 按钮样式 */
.btn-primary {
background: linear-gradient(135deg, #FF4D4F 0%, #FF7A45 100%);
color: #FFFFFF;
border-radius: 18rpx;
height: 92rpx;
box-shadow: 0 16rpx 32rpx rgba(255, 77, 79, 0.24);
}
/* 输入框样式 */
.input {
background: #F6F7F9;
border: 2rpx solid rgba(0, 0, 0, 0.06);
border-radius: 14rpx;
height: 84rpx;
padding: 0 14rpx;
}
```
### 7.3 组件实现示例
#### 7.3.1 商品卡片组件
```vue
<template>
<view class="product-card" @click="handleClick">
<view class="product-image">
<image :src="product.image_url" mode="aspectFill" />
<view v-if="product.activity" class="activity-tag">
{{ activityText }}
</view>
</view>
<view class="product-info">
<text class="product-name">{{ product.name }}</text>
<view class="product-price">
<text class="price-symbol">¥</text>
<text class="price-value">{{ product.price }}</text>
</view>
<text class="product-sales">已售 {{ product.sales }}</text>
</view>
</view>
</template>
<script setup lang="uts">
import { ref } from 'vue'
const props = defineProps<{
product: ProductType
}>()
const activityText = ref<string>('')
// 根据活动类型设置标签文字
</script>
<style>
.product-card {
background: #FFFFFF;
border-radius: 20rpx;
overflow: hidden;
margin-bottom: 20rpx;
}
.product-image {
width: 100%;
height: 360rpx;
position: relative;
}
.product-image image {
width: 100%;
height: 100%;
}
.activity-tag {
position: absolute;
top: 10rpx;
left: 10rpx;
background: linear-gradient(135deg, #FF4D4F 0%, #FF7A45 100%);
color: #FFFFFF;
padding: 4rpx 12rpx;
border-radius: 4rpx;
font-size: 20rpx;
}
.product-info {
padding: 16rpx;
}
.product-name {
font-size: 28rpx;
color: #111111;
display: block;
margin-bottom: 12rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.product-price {
display: flex;
align-items: baseline;
margin-bottom: 8rpx;
}
.price-symbol {
font-size: 24rpx;
color: #FF4D4F;
}
.price-value {
font-size: 36rpx;
color: #FF4D4F;
font-weight: 600;
}
.product-sales {
font-size: 22rpx;
color: #999999;
}
</style>
```
---
## 八、最佳实践
### 8.1 性能优化
1. **图片优化**
- 使用合适的图片尺寸
- 启用图片懒加载
- 使用 WebP 格式(如支持)
2. **列表优化**
- 使用虚拟列表(长列表)
- 分页加载数据
- 避免不必要的重新渲染
3. **动画优化**
- 使用 CSS 动画而非 JS 动画
- 避免过度动画
- 使用 `transform``opacity` 做动画
### 8.2 可访问性
1. **文字大小**
- 最小字号24rpx
- 主要文字28rpx-32rpx
- 标题文字36rpx-40rpx
2. **点击区域**
- 最小点击区域88rpx × 88rpx
- 按钮高度:至少 80rpx
3. **颜色对比**
- 文字与背景对比度:至少 4.5:1
- 重要信息使用高对比度
### 8.3 响应式设计
1. **屏幕适配**
- 使用 rpx 单位
- 使用 flex 布局
- 适配不同屏幕尺寸
2. **横竖屏适配**
- 考虑横屏布局
- 使用媒体查询(如需要)
---
## 九、设计资源
### 9.1 图标系统
- **图标库**:使用 uni-app 内置图标或自定义图标
- **图标大小**24rpx、32rpx、48rpx
- **图标颜色**:主题色或中性色
### 9.2 字体规范
- **字体家族**:系统默认字体
- **字重**Regular400、Medium500、Bold600
- **字号**24rpx、28rpx、32rpx、36rpx、40rpx
---
## 十、总结
本文档基于 CRMEB 项目的设计理念,提取了以下核心要点:
1. **设计风格**:现代简约,以红色系为主色调
2. **布局方式**:卡片式、列表式、网格式
3. **交互模式**:流畅动画、即时反馈、友好提示
4. **组件设计**:统一的组件样式和交互规范
所有实现均为原创,遵循现代 UI 设计最佳实践,确保用户体验和视觉一致性。
---
**文档版本**: v1.0
**创建时间**: 2025-01-XX
**状态**: ✅ 待实施

34
package-lock.json generated Normal file
View File

@@ -0,0 +1,34 @@
{
"name": "mall",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"echarts": "^6.0.0"
}
},
"node_modules/echarts": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz",
"integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==",
"dependencies": {
"tslib": "2.3.0",
"zrender": "6.0.0"
}
},
"node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
},
"node_modules/zrender": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz",
"integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==",
"dependencies": {
"tslib": "2.3.0"
}
}
}
}

5
package.json Normal file
View File

@@ -0,0 +1,5 @@
{
"dependencies": {
"echarts": "^6.0.0"
}
}

View File

@@ -83,6 +83,12 @@
"navigationBarTitleText": "数据分析",
"navigationStyle": "custom"
}
},
{
"path": "pages/mall/analytics/test/test-connection",
"style": {
"navigationBarTitleText": "Supabase 连接测试"
}
}
],
"subPackages": [
@@ -207,6 +213,10 @@
{
"name": "登录页",
"path": "pages/user/login"
},
{
"name": "数据分析端首页",
"path": "pages/mall/analytics/index"
}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -428,7 +428,7 @@ function changeTrendPeriod(period: string) {
function viewReportDetail(reportId: string) {
uni.navigateTo({
url: `/pages/mall/analytics/report-detail?id=${reportId}`
url: `/pages/mall/analytics/report-detail?reportId=${reportId}`
})
}

View File

@@ -248,9 +248,18 @@ export default {
}
},
onLoad(options: any) {
const reportId = options.reportId as string
// 兼容两种参数名reportId 和 id
const reportId = (options.reportId || options.id) as string
if (reportId) {
this.loadReportDetail(reportId)
} else {
uni.showToast({
title: '缺少报表ID',
icon: 'none'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
},
methods: {

View File

@@ -0,0 +1,181 @@
# 数据分析实时大屏 - 测试数据说明
本目录包含用于测试数据分析实时大屏功能的 SQL 脚本和测试数据。
## 文件说明
### 1. `01_create_tables.sql`
创建所需的数据表结构,包括:
- `orders` - 订单表
- `user_sessions` - 用户会话表
- `users` - 用户表
- `products` - 商品表(可选)
- `order_items` - 订单商品关联表(可选)
- `page_views` - 访问日志表(可选)
**执行顺序:** 首先执行此文件创建表结构
### 2. `02_insert_test_data.sql`
插入测试数据,包括:
- 8个测试用户
- 5个在线用户会话最近5分钟内有活动
- 15个今日订单用于计算实时GMV和订单数
- 10个昨日同时段订单用于计算增长率
- 15条访问日志用于转化率计算
**执行顺序:** 在创建表后执行此文件插入测试数据
### 3. `03_test_queries.sql`
包含各种测试查询,用于验证数据计算逻辑:
- 实时GMV查询
- 在线用户查询
- 转化率查询
- 综合实时大屏数据查询
- 数据验证查询
**执行顺序:** 在插入测试数据后执行此文件验证数据
## 使用方法
### 方式 1: 通过 Supabase Dashboard推荐
1. **访问 Dashboard**
```
http://192.168.1.63:8000
http://192.168.1.63:3000 (Studio 默认端口)
```
2. **登录**
- 用户名:`supabase`
- 密码:`D4ce5p8YBpfYzEoDGZ_7MzehZcWrdCNyDEj_VSUBmOw`
3. **打开 SQL Editor**
- 在左侧菜单找到 "SQL Editor"
- 点击 "New Query"
4. **执行脚本**
- 复制 `01_create_tables.sql` 的内容,粘贴并执行
- 复制 `02_insert_test_data.sql` 的内容,粘贴并执行
- (可选)复制 `03_test_queries.sql` 的内容,验证数据
### 方式 2: 使用命令行PostgreSQL
```bash
# 连接到内网 Supabase 数据库
psql -h 192.168.1.63 -p 5432 -U postgres -d postgres
# 输入密码yxyHINygZMLSq9jLddrZQBB-CoyGHSF5DwlwWmbrYXc
# 执行 SQL 文件(需要完整路径)
\i D:/datas/hfkj/mall/pages/mall/analytics/test/01_create_tables.sql
\i D:/datas/hfkj/mall/pages/mall/analytics/test/02_insert_test_data.sql
\i D:/datas/hfkj/mall/pages/mall/analytics/test/03_test_queries.sql
```
### 方式 3: 使用图形工具DBeaver / pgAdmin
1. **创建连接**
- 主机:`192.168.1.63`
- 端口:`5432`
- 数据库:`postgres`
- 用户名:`postgres`
- 密码:`yxyHINygZMLSq9jLddrZQBB-CoyGHSF5DwlwWmbrYXc`
2. **执行 SQL**
- 打开 SQL 编辑器
- 复制 SQL 文件内容并执行
**详细说明请查看:`SQL_USAGE_GUIDE.md`**
## 测试数据说明
### 实时GMV测试数据
- **今日订单总数:** 15笔
- **今日GMV** 约 3,500 元(根据订单金额累加)
- **昨日同时段订单:** 10笔
- **昨日同时段GMV** 约 2,200 元
- **预期增长率:** 约 59%(3500-2200)/2200 * 100
### 实时订单测试数据
- **今日订单数:** 15笔
- **昨日同时段订单数:** 10笔
- **预期增长率:** 50%(15-10)/10 * 100
### 在线用户测试数据
- **最近5分钟内有活动的用户** 5个
- 这些用户会在实时大屏中显示为"在线用户"
### 转化率测试数据
- **今日访问用户数:** 约 10-15个从 user_sessions 表统计)
- **今日下单用户数:** 约 8个从 orders 表去重统计)
- **预期转化率:** 约 53-80%(根据实际数据计算)
## 注意事项
1. **时间依赖**
- 测试数据使用了 `NOW()` 和相对时间(如 `INTERVAL '1 hour'`
- 每次执行时,数据的时间戳会基于当前时间生成
- 建议在测试前先清空相关表的数据(谨慎操作)
2. **数据冲突**
- 脚本使用了 `ON CONFLICT DO NOTHING` 或 `ON CONFLICT DO UPDATE`
- 可以多次执行而不会产生重复数据
- 如需重新生成数据,请先清空表
3. **状态值**
- 订单状态:`2` 表示已支付/已完成
- 用户会话:`is_active = true` 表示活跃会话
4. **UUID 格式**
- 所有 ID 使用 UUID 格式
- 测试数据使用了固定的 UUID 便于识别
## 清理测试数据
如果需要清理测试数据,可以执行:
```sql
-- 谨慎操作:清空测试数据
TRUNCATE TABLE orders, user_sessions, users, order_items, page_views CASCADE;
```
或者删除特定时间范围的数据:
```sql
-- 删除今日的测试订单
DELETE FROM orders WHERE created_at >= DATE_TRUNC('day', NOW());
-- 删除测试用户会话
DELETE FROM user_sessions WHERE created_at >= DATE_TRUNC('day', NOW());
```
## 验证实时大屏功能
执行完测试数据后,在数据分析页面应该能看到:
1. **实时GMV** 约 ¥3,500根据实际订单金额
2. **实时订单:** 15笔
3. **在线用户:** 5人
4. **转化率:** 约 50-80%(根据实际计算)
增长率会根据昨日同时段的数据自动计算。
## 问题排查
如果实时大屏显示异常,可以:
1. 执行 `03_test_queries.sql` 中的查询验证数据
2. 检查订单状态是否为 `2`(已支付)
3. 检查时间范围是否正确(今日 vs 昨日同时段)
4. 检查用户会话的 `last_active_at` 是否在最近5分钟内
5. 查看浏览器控制台的错误信息
## 扩展测试数据
如果需要更多测试数据,可以:
1. 修改 `02_insert_test_data.sql` 中的 INSERT 语句
2. 调整订单金额、数量和时间分布
3. 添加更多用户和会话数据
4. 使用循环生成大量测试数据(注意性能)

View File

@@ -0,0 +1,304 @@
# SQL 测试脚本使用指南
本指南说明如何在内网 Supabase 环境中执行测试 SQL 脚本。
## 📋 目录结构
```
pages/mall/analytics/test/
├── 01_create_tables.sql # 创建表结构
├── 02_insert_test_data.sql # 插入测试数据
├── 03_test_queries.sql # 测试查询
├── 04_cleanup.sql # 清理数据
└── SQL_USAGE_GUIDE.md # 本指南
```
## 🚀 执行方式
### 方式 1: 通过 Supabase Dashboard推荐
如果您的内网 Supabase 有 Dashboard 界面:
1. **访问 Dashboard**
```
http://192.168.1.63:8000
http://192.168.1.63:3000 (Studio 默认端口)
```
2. **登录**
- 用户名:`supabase`(根据您的配置)
- 密码:`D4ce5p8YBpfYzEoDGZ_7MzehZcWrdCNyDEj_VSUBmOw`
3. **打开 SQL Editor**
- 在左侧菜单找到 "SQL Editor" 或 "SQL"
- 点击 "New Query"
4. **执行脚本**
- 复制 `01_create_tables.sql` 的内容
- 粘贴到 SQL Editor
- 点击 "Run" 或按 `Ctrl+Enter`
- 等待执行完成
5. **依次执行其他脚本**
- 执行 `02_insert_test_data.sql`(插入测试数据)
- 执行 `03_test_queries.sql`(验证数据,可选)
### 方式 2: 通过 PostgreSQL 客户端psql
如果 Dashboard 不可用,可以直接连接 PostgreSQL
1. **连接数据库**
```bash
# 使用 psql 连接
psql -h 192.168.1.63 -p 5432 -U postgres -d postgres
# 输入密码(根据您的配置)
# POSTGRES_PASSWORD=yxyHINygZMLSq9jLddrZQBB-CoyGHSF5DwlwWmbrYXc
```
2. **执行 SQL 文件**
```sql
-- 在 psql 中执行
\i /path/to/01_create_tables.sql
\i /path/to/02_insert_test_data.sql
\i /path/to/03_test_queries.sql
```
或者直接复制粘贴 SQL 内容到 psql 中执行。
### 方式 3: 通过 DBeaver / pgAdmin 等图形工具
1. **创建新连接**
- 主机:`192.168.1.63`
- 端口:`5432`
- 数据库:`postgres`
- 用户名:`postgres`
- 密码:`yxyHINygZMLSq9jLddrZQBB-CoyGHSF5DwlwWmbrYXc`
2. **执行 SQL**
- 打开 SQL 编辑器
- 复制 SQL 文件内容
- 执行脚本
### 方式 4: 通过 HTTP API程序化执行
使用 Supabase REST API 执行 SQL需要 service_role key
```javascript
// 注意:这种方式需要 Supabase 的 SQL 执行功能
// 通常不推荐,因为安全风险较高
const response = await fetch('http://192.168.1.63:8000/rest/v1/rpc/exec_sql', {
method: 'POST',
headers: {
'apikey': 'YOUR_SERVICE_ROLE_KEY',
'Authorization': 'Bearer YOUR_SERVICE_ROLE_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
sql: 'SELECT * FROM users LIMIT 1;'
})
})
```
## 📝 执行顺序
**重要:必须按顺序执行!**
1. ✅ **第一步:创建表结构**
```sql
-- 执行 01_create_tables.sql
-- 这会创建所有需要的表和索引
```
2. ✅ **第二步:插入测试数据**
```sql
-- 执行 02_insert_test_data.sql
-- 这会插入测试用户、订单、会话等数据
```
3. ✅ **第三步:验证数据(可选)**
```sql
-- 执行 03_test_queries.sql
-- 验证数据是否正确插入,查看统计信息
```
4. ⚠️ **清理数据(需要时)**
```sql
-- 执行 04_cleanup.sql
-- 谨慎使用:会删除测试数据
```
## 🔍 验证执行结果
### 检查表是否创建成功
```sql
-- 查看所有表
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
ORDER BY table_name;
-- 应该看到:
-- orders
-- user_sessions
-- users
-- products (可选)
-- order_items (可选)
-- page_views (可选)
```
### 检查数据是否插入成功
```sql
-- 检查用户数量
SELECT COUNT(*) FROM users;
-- 应该返回 8
-- 检查订单数量
SELECT COUNT(*) FROM orders WHERE created_at >= DATE_TRUNC('day', NOW());
-- 应该返回 15今日订单
-- 检查在线用户
SELECT COUNT(*) FROM user_sessions
WHERE last_active_at >= NOW() - INTERVAL '5 minutes' AND is_active = true;
-- 应该返回 5
```
### 检查实时大屏数据
```sql
-- 执行 03_test_queries.sql 中的综合查询
-- 应该能看到:
-- - 实时GMV: 约 3,500 元
-- - 实时订单: 15 笔
-- - 在线用户: 5 人
-- - 转化率: 约 50-80%
```
## ⚠️ 注意事项
### 1. 权限问题
如果遇到权限错误:
```sql
-- 确保 postgres 用户有足够权限
GRANT ALL PRIVILEGES ON DATABASE postgres TO postgres;
GRANT ALL PRIVILEGES ON SCHEMA public TO postgres;
```
### 2. 表已存在
如果表已存在,脚本会使用 `CREATE TABLE IF NOT EXISTS`,不会报错。
但如果需要重新创建:
```sql
-- 先删除表(谨慎操作)
DROP TABLE IF EXISTS order_items CASCADE;
DROP TABLE IF EXISTS page_views CASCADE;
DROP TABLE IF EXISTS user_sessions CASCADE;
DROP TABLE IF EXISTS orders CASCADE;
DROP TABLE IF EXISTS users CASCADE;
DROP TABLE IF EXISTS products CASCADE;
```
### 3. 时间依赖
测试数据使用 `NOW()` 函数,每次执行都会基于当前时间生成。
- 今日订单:基于当前日期
- 昨日订单:当前时间往前推 24 小时
- 在线用户:最近 5 分钟内有活动
### 4. UUID 冲突
如果重复执行插入脚本,由于使用了 `ON CONFLICT DO NOTHING`,不会产生重复数据。
但如果需要重新插入,先执行清理脚本。
## 🐛 常见问题
### Q1: 连接被拒绝
```
Error: connection refused
```
**解决:**
- 检查 Supabase 服务是否运行
- 检查防火墙设置
- 确认端口 5432 是否开放
### Q2: 认证失败
```
Error: password authentication failed
```
**解决:**
- 确认密码是否正确:`yxyHINygZMLSq9jLddrZQBB-CoyGHSF5DwlwWmbrYXc`
- 检查用户名是否为 `postgres`
### Q3: 表已存在错误
```
Error: relation "orders" already exists
```
**解决:**
- 脚本已使用 `IF NOT EXISTS`,通常不会报错
- 如需重新创建,先删除表
### Q4: 权限不足
```
Error: permission denied
```
**解决:**
- 使用 postgres 超级用户执行
- 或授予相应权限
## 📊 执行后的预期结果
执行完所有脚本后,您应该能看到:
1. **数据库表**
- 6 个表已创建orders, user_sessions, users, products, order_items, page_views
- 所有索引已创建
2. **测试数据**
- 8 个测试用户
- 15 个今日订单
- 10 个昨日订单
- 5 个在线用户会话
- 15 条访问日志
3. **实时大屏显示**
- 在数据分析页面应该能看到实时数据
- GMV、订单数、在线用户、转化率都有值
## 🔄 重新执行
如果需要重新生成测试数据:
1. **清理数据**
```sql
-- 执行 04_cleanup.sql
```
2. **重新插入**
```sql
-- 执行 02_insert_test_data.sql
```
## 📞 获取帮助
如果遇到问题:
1. 检查 Supabase 日志
2. 查看数据库连接状态
3. 验证配置文件 `ak/config.uts` 是否正确
4. 使用测试页面验证连接:`/pages/mall/analytics/test/test-connection`
## 🎯 快速开始
**最简单的执行方式:**
1. 打开 Supabase Dashboard如果有
2. 进入 SQL Editor
3. 复制 `01_create_tables.sql` 内容,执行
4. 复制 `02_insert_test_data.sql` 内容,执行
5. 完成!
现在可以开始测试实时大屏功能了!🎉

View File

@@ -0,0 +1,529 @@
<!-- Supabase 连接测试页面 -->
<template>
<view class="test-container">
<view class="header">
<text class="title">Supabase 连接测试</text>
</view>
<view class="config-section">
<text class="section-title">当前配置</text>
<view class="config-item">
<text class="config-label">Supabase URL:</text>
<text class="config-value">{{ configUrl }}</text>
</view>
<view class="config-item">
<text class="config-label">API Key:</text>
<text class="config-value">{{ configKey.substring(0, 20) }}...</text>
</view>
<view class="config-item">
<text class="config-label">WebSocket URL:</text>
<text class="config-value">{{ configWs }}</text>
</view>
</view>
<view class="test-section">
<button class="test-btn" @click="testConnection" :disabled="isTesting">
{{ isTesting ? '测试中...' : '测试连接' }}
</button>
</view>
<view class="result-section" v-if="testResult">
<text class="section-title">测试结果</text>
<view class="result-item" :class="{ success: testResult.success, error: !testResult.success }">
<text class="result-icon">{{ testResult.success ? '✅' : '❌' }}</text>
<text class="result-text">{{ testResult.message }}</text>
</view>
<view v-if="testResult.details" class="result-details">
<text class="details-title">详细信息:</text>
<text class="details-text">{{ testResult.details }}</text>
</view>
<view v-if="testResult.data" class="result-data">
<text class="data-title">返回数据:</text>
<text class="data-text">{{ JSON.stringify(testResult.data, null, 2) }}</text>
</view>
</view>
<view class="test-list">
<text class="section-title">测试项目</text>
<view class="test-item" v-for="(test, index) in testList" :key="index">
<view class="test-info">
<text class="test-name">{{ test.name }}</text>
<text class="test-status" :class="test.status">{{ getStatusText(test.status) }}</text>
</view>
<button class="test-item-btn" @click="runTest(test)" :disabled="isTesting">执行</button>
</view>
</view>
</view>
</template>
<script lang="uts">
import supa from '@/components/supadb/aksupainstance.uts'
import { SUPA_URL, SUPA_KEY, WS_URL } from '@/ak/config.uts'
type TestResultType = {
success: boolean
message: string
details?: string
data?: any
}
type TestItemType = {
name: string
status: string
func: () => Promise<TestResultType>
}
export default {
data() {
return {
configUrl: SUPA_URL,
configKey: SUPA_KEY,
configWs: WS_URL,
isTesting: false,
testResult: null as TestResultType | null,
testList: [
{
name: '1. 基础连接测试',
status: 'pending',
func: this.testBasicConnection
} as TestItemType,
{
name: '2. 查询测试(查询用户表)',
status: 'pending',
func: this.testQuery
} as TestItemType,
{
name: '3. 认证测试',
status: 'pending',
func: this.testAuth
} as TestItemType,
{
name: '4. 实时连接测试',
status: 'pending',
func: this.testRealtime
} as TestItemType
] as Array<TestItemType>
}
},
methods: {
// 综合连接测试
async testConnection() {
this.isTesting = true
this.testResult = null
try {
// 测试1: 基础连接
const basicResult = await this.testBasicConnection()
this.updateTestStatus(0, basicResult.success ? 'success' : 'error')
if (!basicResult.success) {
this.testResult = basicResult
this.isTesting = false
return
}
// 测试2: 查询测试
const queryResult = await this.testQuery()
this.updateTestStatus(1, queryResult.success ? 'success' : 'error')
// 测试3: 认证测试
const authResult = await this.testAuth()
this.updateTestStatus(2, authResult.success ? 'success' : 'error')
// 汇总结果
const allSuccess = basicResult.success && queryResult.success && authResult.success
this.testResult = {
success: allSuccess,
message: allSuccess
? '所有测试通过Supabase 连接正常。'
: '部分测试失败,请查看详细信息。',
details: `基础连接: ${basicResult.success ? '✓' : '✗'}, 查询: ${queryResult.success ? '✓' : '✗'}, 认证: ${authResult.success ? '✓' : '✗'}`,
data: {
basic: basicResult,
query: queryResult,
auth: authResult
}
}
} catch (err) {
this.testResult = {
success: false,
message: '测试过程中发生错误',
details: err?.toString() || '未知错误'
}
} finally {
this.isTesting = false
}
},
// 测试1: 基础连接
async testBasicConnection(): Promise<TestResultType> {
try {
// 尝试访问 Supabase REST API
const response = await uni.request({
url: `${SUPA_URL}/rest/v1/`,
method: 'GET',
header: {
'apikey': SUPA_KEY,
'Authorization': `Bearer ${SUPA_KEY}`
},
timeout: 5000
})
if (response.statusCode === 200 || response.statusCode === 404) {
// 404 也是正常的,说明服务器响应了
return {
success: true,
message: '基础连接成功',
details: `HTTP 状态码: ${response.statusCode}`,
data: response.data
}
} else {
return {
success: false,
message: '连接失败',
details: `HTTP 状态码: ${response.statusCode}`
}
}
} catch (err) {
return {
success: false,
message: '无法连接到 Supabase',
details: err?.toString() || '网络错误或服务器不可达'
}
}
},
// 测试2: 查询测试
async testQuery(): Promise<TestResultType> {
try {
// 尝试查询 users 表(如果存在)
const { data, error } = await supa
.from('users')
.select('id, phone, nickname')
.limit(5)
if (error !== null) {
// 如果表不存在,尝试查询其他表
if (error.message?.includes('relation') || error.message?.includes('does not exist')) {
// 尝试查询 orders 表
const { data: orderData, error: orderError } = await supa
.from('orders')
.select('id')
.limit(1)
if (orderError !== null) {
return {
success: false,
message: '查询失败',
details: `错误: ${orderError.message || orderError.toString()}`
}
}
return {
success: true,
message: '查询成功(使用 orders 表)',
details: 'users 表不存在,但 orders 表可访问',
data: orderData
}
}
return {
success: false,
message: '查询失败',
details: `错误: ${error.message || error.toString()}`
}
}
return {
success: true,
message: '查询成功',
details: `返回 ${data?.length || 0} 条记录`,
data: data
}
} catch (err) {
return {
success: false,
message: '查询测试失败',
details: err?.toString() || '未知错误'
}
}
},
// 测试3: 认证测试
async testAuth(): Promise<TestResultType> {
try {
// 检查是否已登录
const { data: sessionData, error: sessionError } = await supa.auth.getSession()
if (sessionError !== null) {
return {
success: false,
message: '获取会话失败',
details: sessionError.message || sessionError.toString()
}
}
if (sessionData?.session !== null) {
return {
success: true,
message: '认证成功',
details: `用户已登录: ${sessionData.session.user.email || sessionData.session.user.phone || '未知'}`,
data: {
user: sessionData.session.user,
expires_at: sessionData.session.expires_at
}
}
} else {
return {
success: false,
message: '未登录',
details: '需要先登录才能测试认证功能'
}
}
} catch (err) {
return {
success: false,
message: '认证测试失败',
details: err?.toString() || '未知错误'
}
}
},
// 测试4: 实时连接测试
async testRealtime(): Promise<TestResultType> {
try {
// WebSocket 连接测试比较复杂,这里只做 URL 验证
if (WS_URL.startsWith('ws://') || WS_URL.startsWith('wss://')) {
return {
success: true,
message: 'WebSocket URL 格式正确',
details: `URL: ${WS_URL}`
}
} else {
return {
success: false,
message: 'WebSocket URL 格式错误',
details: `URL 应以 ws:// 或 wss:// 开头`
}
}
} catch (err) {
return {
success: false,
message: '实时连接测试失败',
details: err?.toString() || '未知错误'
}
}
},
// 运行单个测试
async runTest(test: TestItemType) {
this.isTesting = true
test.status = 'testing'
try {
const result = await test.func()
test.status = result.success ? 'success' : 'error'
this.testResult = result
} catch (err) {
test.status = 'error'
this.testResult = {
success: false,
message: '测试执行失败',
details: err?.toString() || '未知错误'
}
} finally {
this.isTesting = false
}
},
// 更新测试状态
updateTestStatus(index: number, status: string) {
if (this.testList[index]) {
this.testList[index].status = status
}
},
// 获取状态文本
getStatusText(status: string): string {
const statusMap: Record<string, string> = {
'pending': '待测试',
'testing': '测试中...',
'success': '✓ 通过',
'error': '✗ 失败'
}
return statusMap[status] || '未知'
}
}
}
</script>
<style>
.test-container {
padding: 40rpx;
background-color: #f5f5f5;
min-height: 100vh;
}
.header {
margin-bottom: 40rpx;
}
.title {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.config-section, .test-section, .result-section, .test-list {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 30rpx;
}
.section-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
}
.config-item {
display: flex;
flex-direction: column;
margin-bottom: 20rpx;
}
.config-label {
font-size: 24rpx;
color: #666;
margin-bottom: 8rpx;
}
.config-value {
font-size: 22rpx;
color: #333;
word-break: break-all;
}
.test-btn {
width: 100%;
height: 80rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
border-radius: 12rpx;
font-size: 28rpx;
border: none;
}
.test-btn:disabled {
background: #ccc;
}
.result-item {
display: flex;
align-items: center;
padding: 20rpx;
border-radius: 12rpx;
margin-bottom: 20rpx;
}
.result-item.success {
background-color: #e8f5e8;
}
.result-item.error {
background-color: #ffebee;
}
.result-icon {
font-size: 32rpx;
margin-right: 15rpx;
}
.result-text {
font-size: 26rpx;
color: #333;
flex: 1;
}
.result-details, .result-data {
margin-top: 20rpx;
padding: 20rpx;
background-color: #f8f9fa;
border-radius: 8rpx;
}
.details-title, .data-title {
font-size: 24rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
}
.details-text, .data-text {
font-size: 22rpx;
color: #666;
word-break: break-all;
white-space: pre-wrap;
}
.test-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 25rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.test-item:last-child {
border-bottom: none;
}
.test-info {
flex: 1;
}
.test-name {
font-size: 26rpx;
color: #333;
margin-bottom: 8rpx;
}
.test-status {
font-size: 22rpx;
}
.test-status.pending {
color: #999;
}
.test-status.testing {
color: #2196f3;
}
.test-status.success {
color: #4caf50;
}
.test-status.error {
color: #f44336;
}
.test-item-btn {
padding: 12rpx 24rpx;
background-color: #667eea;
color: #fff;
border-radius: 8rpx;
font-size: 24rpx;
border: none;
}
.test-item-btn:disabled {
background-color: #ccc;
}
</style>

View File

@@ -169,34 +169,44 @@ const loadBanners = async () => {
if (error !== null) {
console.error('加载轮播图失败:', error)
bannerList.value = []
return
}
bannerList.value = data ?? []
} catch (err) {
console.error('加载轮播图异常:', err)
bannerList.value = []
}
}
// 加载分类
const loadCategories = async () => {
try {
// 查询所有活跃分类然后在前端过滤出顶级分类parent_id 为 null
// 这样可以避免 UTS 中 .is(null) 的类型问题
const { data, error } = await supa
.from('categories')
.select('*')
.eq('is_active', true)
.is('parent_id', null)
.order('sort_order', { ascending: true })
.limit(8)
if (error !== null) {
console.error('加载分类失败:', error)
categoryList.value = []
return
}
categoryList.value = data ?? []
// 过滤出顶级分类parent_id 为 null 或 undefined
const topCategories = (data ?? []).filter((item: any) => {
return item.parent_id === null || item.parent_id === undefined || item.parent_id === ''
}).slice(0, 8) // 限制为前8个
categoryList.value = topCategories
} catch (err) {
console.error('加载分类异常:', err)
// 如果查询失败,使用空数组避免页面崩溃
categoryList.value = []
}
}
@@ -204,22 +214,25 @@ const loadCategories = async () => {
const loadCoupons = async () => {
try {
const now = new Date().toISOString()
// 查询当前时间在有效期内的优惠券start_time <= now <= end_time
const { data, error } = await supa
.from('coupon_templates')
.select('*')
.eq('status', 1)
.gte('end_time', now)
.lte('start_time', now)
.lte('start_time', now) // 开始时间 <= 当前时间
.gte('end_time', now) // 结束时间 >= 当前时间
.limit(5)
if (error !== null) {
console.error('加载优惠券失败:', error)
couponList.value = []
return
}
couponList.value = data ?? []
} catch (err) {
console.error('加载优惠券异常:', err)
couponList.value = []
}
}
@@ -242,6 +255,9 @@ const loadProducts = async (loadMore: boolean = false) => {
if (error !== null) {
console.error('加载商品失败:', error)
if (!loadMore) {
productList.value = []
}
isLoading.value = false
return
}
@@ -261,6 +277,9 @@ const loadProducts = async (loadMore: boolean = false) => {
hasMore.value = newProducts.length === pageSize.value
} catch (err) {
console.error('加载商品异常:', err)
if (!loadMore) {
productList.value = []
}
} finally {
isLoading.value = false
}

View File

@@ -83,13 +83,6 @@
"navigationStyle": "custom"
}
},
{
"path": "pages/mall/analytics/index",
"style": {
"navigationBarTitleText": "数据分析",
"navigationStyle": "custom"
}
},
{
"path": "pages/mall/consumer/product-detail",
"style": {
@@ -461,6 +454,13 @@
{
"root": "pages/mall/analytics",
"pages": [
{
"path": "index",
"style": {
"navigationBarTitleText": "数据分析中心",
"navigationStyle": "custom"
}
},
{
"path": "sales-report",
"style": {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,3 @@
// 参考链接 https://doc.dcloud.net.cn/uni-app-x/tutorial/ls-plugin.html#setting
{
"targets": [
"APP-ANDROID"

View File

@@ -0,0 +1,60 @@
# 图片资源复制说明
## 需要复制的图片文件
请从 `CRMEB/template/uni-app/pages/users/static/` 目录复制以下文件到 `static/user/` 目录:
### 必需文件
1. **phone_1.png** - 手机号输入框图标
- 尺寸24rpx × 34rpx
- 用途:手机号输入框左侧图标
2. **code_1.png** - 密码输入框图标
- 尺寸28rpx × 32rpx
- 用途:密码输入框左侧图标
3. **code_2.png** - 验证码输入框图标
- 尺寸28rpx × 32rpx
- 用途:验证码输入框左侧图标
### 可选文件
4. **logo2.png** - Logo 图片(可选)
- 用途:注册/找回密码页面 Logo
- 如果不存在,将使用 `/static/logo.png`
## 复制方法
### Windows PowerShell
```powershell
# 在项目根目录执行
Copy-Item "CRMEB\template\uni-app\pages\users\static\phone_1.png" -Destination "static\user\"
Copy-Item "CRMEB\template\uni-app\pages\users\static\code_1.png" -Destination "static\user\"
Copy-Item "CRMEB\template\uni-app\pages\users\static\code_2.png" -Destination "static\user\"
Copy-Item "CRMEB\template\uni-app\pages\users\static\logo2.png" -Destination "static\user\" -ErrorAction SilentlyContinue
```
### 手动复制
1. 打开 `CRMEB/template/uni-app/pages/users/static/` 目录
2. 复制上述文件到 `static/user/` 目录
3. 确保文件路径正确
## 文件结构
复制后的目录结构应该是:
```
static/
└── user/
├── phone_1.png
├── code_1.png
├── code_2.png
├── logo2.png (可选)
└── README.md
```
## 注意事项
- 如果图片文件不存在,页面会显示占位符或空白
- 建议使用原 CRMEB 项目的图片资源以保持设计一致性
- 图片路径在代码中使用:`/static/user/phone_1.png`

20
static/user/README.md Normal file
View File

@@ -0,0 +1,20 @@
# 用户相关图片资源
## 说明
这些图片资源来自 CRMEB 项目,用于登录注册页面。
## 需要复制的图片文件
请从 `CRMEB/template/uni-app/pages/users/static/` 目录复制以下文件到此目录:
1. `phone_1.png` - 手机号输入框图标24rpx × 34rpx
2. `code_1.png` - 密码输入框图标28rpx × 32rpx
3. `code_2.png` - 验证码输入框图标28rpx × 32rpx
4. `logo2.png` - Logo 图片(可选,用于注册/找回密码页面)
## 使用方式
在页面中使用:
```vue
<image src="/static/user/phone_1.png" />
```

View File

@@ -0,0 +1,72 @@
<template>
<view class="ec-wrap">
<!-- 通过 props option 喂给 renderjs -->
<view
class="ec-canvas"
:prop="option"
:change:prop="ec.setOption"
:data-theme="theme"
/>
</view>
</template>
<script>
export default {
name: "EChartsView",
props: {
option: { type: Object, default: () => ({}) },
theme: { type: String, default: "light" },
},
};
</script>
<script module="ec" lang="renderjs">
import * as echarts from "echarts";
let chart = null;
function ensureChart(el) {
if (chart) return chart;
chart = echarts.init(el, null, { renderer: "canvas" });
// 自适应:监听容器尺寸变化
if (typeof ResizeObserver !== "undefined") {
const ro = new ResizeObserver(() => {
chart && chart.resize();
});
ro.observe(el);
} else {
// 兜底
window.addEventListener("resize", () => chart && chart.resize());
}
return chart;
}
export default {
mounted() {
const el = this.$el.querySelector(".ec-canvas") || this.$el;
ensureChart(el);
},
methods: {
setOption(option) {
const el = this.$el.querySelector(".ec-canvas") || this.$el;
const c = ensureChart(el);
if (!option) return;
c.setOption(option, true);
// 首次渲染后再 resize 一次,避免 H5 初始宽高为 0
setTimeout(() => c.resize(), 16);
},
},
};
</script>
<style>
.ec-wrap {
width: 100%;
height: 100%;
}
.ec-canvas {
width: 100%;
height: 100%;
}
</style>