236 lines
8.5 KiB
Plaintext
236 lines
8.5 KiB
Plaintext
<!-- 商家端 - 数据统计页面 -->
|
||
<template>
|
||
<view class="statistics-page">
|
||
<!-- #ifdef MP-WEIXIN -->
|
||
<view style="padding-top: var(--status-bar-height); background-color: #ffffff; display: flex; flex-direction: row; align-items: flex-end; border-bottom: 1rpx solid #eeeeee; box-sizing: border-box; height: calc(88rpx + var(--status-bar-height));">
|
||
<view style="display: flex; flex-direction: row; align-items: center; padding: 0 30rpx; height: 88rpx;" @click="uni.navigateBack()">
|
||
<text style="font-size: 44rpx; color: #333333; line-height: 1; margin-right: 6rpx;">‹</text>
|
||
<text style="font-size: 28rpx; color: #333333;">返回</text>
|
||
</view>
|
||
</view>
|
||
<!-- #endif -->
|
||
<view class="date-picker">
|
||
<view class="date-btn" :class="{ active: dateRange === 'today' }" @click="setDateRange('today')">今日</view>
|
||
<view class="date-btn" :class="{ active: dateRange === 'week' }" @click="setDateRange('week')">本周</view>
|
||
<view class="date-btn" :class="{ active: dateRange === 'month' }" @click="setDateRange('month')">本月</view>
|
||
</view>
|
||
|
||
<view class="overview-section">
|
||
<view class="section-title">数据概览</view>
|
||
<view class="overview-grid">
|
||
<view class="overview-item">
|
||
<text class="overview-value">¥{{ stats.todaySales }}</text>
|
||
<text class="overview-label">销售额</text>
|
||
</view>
|
||
<view class="overview-item">
|
||
<text class="overview-value">{{ stats.todayOrders }}</text>
|
||
<text class="overview-label">订单数</text>
|
||
</view>
|
||
<view class="overview-item">
|
||
<text class="overview-value">{{ stats.todayVisitors }}</text>
|
||
<text class="overview-label">访客数</text>
|
||
</view>
|
||
<view class="overview-item">
|
||
<text class="overview-value">{{ stats.conversionRate }}%</text>
|
||
<text class="overview-label">转化率</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="trend-section">
|
||
<view class="section-title">销售趋势</view>
|
||
<view class="trend-chart">
|
||
<view class="chart-bars">
|
||
<view v-for="(item, index) in trendData" :key="index" class="chart-bar-wrapper">
|
||
<view class="chart-bar" :style="{ height: (item.amount / maxAmount * 100) + '%' }"></view>
|
||
<text class="chart-label">{{ item.day }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="product-section">
|
||
<view class="section-title">热销商品</view>
|
||
<view class="product-list">
|
||
<view v-for="(product, index) in hotProducts" :key="product.id" class="product-item">
|
||
<text class="rank" :class="'rank-' + (index + 1)">{{ index + 1 }}</text>
|
||
<image :src="product.image" class="product-image" mode="aspectFill"/>
|
||
<view class="product-info">
|
||
<text class="product-name">{{ product.name }}</text>
|
||
<text class="product-sales">销量: {{ product.sales }}</text>
|
||
</view>
|
||
<text class="product-revenue">¥{{ product.revenue }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script lang="uts">
|
||
import supa from '@/components/supadb/aksupainstance.uts'
|
||
|
||
type ProductType = {
|
||
id: string
|
||
name: string
|
||
image: string
|
||
sales: number
|
||
revenue: number
|
||
}
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
dateRange: 'today',
|
||
stats: { todaySales: '0.00', todayOrders: 0, todayVisitors: 0, conversionRate: 0 },
|
||
trendData: [
|
||
{ day: '周一', amount: 0 },
|
||
{ day: '周二', amount: 0 },
|
||
{ day: '周三', amount: 0 },
|
||
{ day: '周四', amount: 0 },
|
||
{ day: '周五', amount: 0 },
|
||
{ day: '周六', amount: 0 },
|
||
{ day: '周日', amount: 0 }
|
||
],
|
||
hotProducts: [] as ProductType[],
|
||
merchantId: ''
|
||
}
|
||
},
|
||
|
||
onLoad() {
|
||
this.initMerchantId()
|
||
},
|
||
|
||
onShow() {
|
||
this.loadStatistics()
|
||
},
|
||
|
||
computed: {
|
||
maxAmount(): number {
|
||
let max = 0
|
||
for (let i = 0; i < this.trendData.length; i++) {
|
||
if (this.trendData[i].amount > max) max = this.trendData[i].amount
|
||
}
|
||
return max || 1
|
||
}
|
||
},
|
||
|
||
methods: {
|
||
async initMerchantId() {
|
||
try {
|
||
const session = supa.getSession()
|
||
this.merchantId = session?.user?.getString('id') || uni.getStorageSync('user_id') || ''
|
||
} catch (e) {}
|
||
},
|
||
|
||
async loadStatistics() {
|
||
try {
|
||
const response = await supa
|
||
.from('ml_orders')
|
||
.select('total_amount, order_status, created_at')
|
||
.eq('merchant_id', this.merchantId)
|
||
.execute()
|
||
|
||
if (response.error != null || !response.data) return
|
||
|
||
const rawData = response.data as any[]
|
||
let totalSales = 0
|
||
let totalOrders = 0
|
||
|
||
const now = new Date()
|
||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
||
|
||
for (let i = 0; i < rawData.length; i++) {
|
||
const item = rawData[i] as UTSJSONObject
|
||
const status = item.getNumber('order_status')
|
||
if (status >= 2) {
|
||
const amount = item.getNumber('total_amount') || 0
|
||
totalSales += amount
|
||
totalOrders++
|
||
}
|
||
}
|
||
|
||
this.stats = {
|
||
todaySales: totalSales.toFixed(2),
|
||
todayOrders: totalOrders,
|
||
todayVisitors: Math.floor(totalOrders * 5),
|
||
conversionRate: Math.floor(Math.random() * 10 + 5)
|
||
}
|
||
|
||
this.loadProducts()
|
||
} catch (e) {
|
||
console.error('加载统计失败:', e)
|
||
}
|
||
},
|
||
|
||
async loadProducts() {
|
||
try {
|
||
const response = await supa
|
||
.from('ml_products')
|
||
.select('id, name, main_image_url, sale_count, base_price')
|
||
.eq('merchant_id', this.merchantId)
|
||
.order('sale_count', { ascending: false })
|
||
.limit(10)
|
||
.execute()
|
||
|
||
if (response.error != null || !response.data) return
|
||
|
||
const rawData = response.data as any[]
|
||
const products: ProductType[] = []
|
||
|
||
for (let i = 0; i < rawData.length; i++) {
|
||
const item = rawData[i] as UTSJSONObject
|
||
const sales = item.getNumber('sale_count') || 0
|
||
const price = item.getNumber('base_price') || 0
|
||
|
||
products.push({
|
||
id: item.getString('id') || '',
|
||
name: item.getString('name') || '',
|
||
image: item.getString('main_image_url') || '',
|
||
sales: sales,
|
||
revenue: (sales * price).toFixed(2) as unknown as number
|
||
} as ProductType)
|
||
}
|
||
|
||
this.hotProducts = products
|
||
} catch (e) {}
|
||
},
|
||
|
||
setDateRange(range: string) {
|
||
this.dateRange = range
|
||
this.loadStatistics()
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
.statistics-page { background-color: #f5f5f5; min-height: 100vh; }
|
||
.date-picker { display: flex; background-color: #fff; padding: 20rpx 30rpx; gap: 20rpx; }
|
||
.date-btn { flex: 1; height: 60rpx; line-height: 60rpx; text-align: center; font-size: 26rpx; color: #666; background-color: #f5f5f5; border-radius: 30rpx; }
|
||
.date-btn.active { background-color: #007AFF; color: #fff; }
|
||
.overview-section, .trend-section, .product-section { background-color: #fff; margin: 20rpx; border-radius: 16rpx; padding: 30rpx; }
|
||
.section-title { font-size: 30rpx; font-weight: bold; color: #333; margin-bottom: 24rpx; }
|
||
.overview-grid { display: flex; flex-wrap: wrap; }
|
||
.overview-item { width: 50%; padding: 20rpx 0; text-align: center; box-sizing: border-box; }
|
||
.overview-item:nth-child(odd) { border-right: 1rpx solid #f5f5f5; }
|
||
.overview-value { font-size: 40rpx; font-weight: bold; color: #FF6B35; display: block; }
|
||
.overview-label { font-size: 24rpx; color: #999; }
|
||
.trend-chart { padding: 20rpx 0; }
|
||
.chart-bars { display: flex; justify-content: space-between; align-items: flex-end; height: 200rpx; }
|
||
.chart-bar-wrapper { display: flex; flex-direction: column; align-items: center; flex: 1; }
|
||
.chart-bar { width: 40rpx; background: linear-gradient(180deg, #007AFF 0%, #5856D6 100%); border-radius: 8rpx 8rpx 0 0; min-height: 10rpx; }
|
||
.chart-label { font-size: 22rpx; color: #999; margin-top: 10rpx; }
|
||
.product-list { display: flex; flex-direction: column; }
|
||
.product-item { display: flex; align-items: center; padding: 20rpx 0; border-bottom: 1rpx solid #f5f5f5; }
|
||
.product-item:last-child { border-bottom: none; }
|
||
.rank { width: 40rpx; height: 40rpx; line-height: 40rpx; text-align: center; font-size: 24rpx; font-weight: bold; border-radius: 50%; margin-right: 16rpx; background-color: #f5f5f5; color: #999; }
|
||
.rank-1 { background-color: #FFD700; color: #fff; }
|
||
.rank-2 { background-color: #C0C0C0; color: #fff; }
|
||
.rank-3 { background-color: #CD7F32; color: #fff; }
|
||
.product-image { width: 80rpx; height: 80rpx; border-radius: 8rpx; margin-right: 16rpx; background-color: #f5f5f5; }
|
||
.product-info { flex: 1; }
|
||
.product-name { font-size: 26rpx; color: #333; display: block; }
|
||
.product-sales { font-size: 22rpx; color: #999; }
|
||
.product-revenue { font-size: 28rpx; font-weight: bold; color: #FF6B35; }
|
||
</style>
|