228 lines
7.9 KiB
Plaintext
228 lines
7.9 KiB
Plaintext
<!-- 商家端 - 数据统计页面 -->
|
|
<template>
|
|
<view class="statistics-page">
|
|
<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>
|