Files
medical-mall/pages/mall/merchant/statistics.uvue
2026-04-13 11:32:31 +08:00

254 lines
9.3 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 机构端 - 服务数据统计页面 -->
<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'
import { USE_MOCK, MOCK_MERCHANT_ID, getMockStats, getMockTrendData, MOCK_HOT_PRODUCTS } from '@/pages/mall/merchant/mock/merchant-mock-data.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() {
if (USE_MOCK) {
this.merchantId = MOCK_MERCHANT_ID
return
}
try {
const session = supa.getSession()
this.merchantId = session?.user?.getString('id') || uni.getStorageSync('user_id') || ''
} catch (e) {}
},
async loadStatistics() {
if (USE_MOCK) {
const s = getMockStats(this.dateRange)
this.stats = s
this.trendData = getMockTrendData(this.dateRange)
this.hotProducts = MOCK_HOT_PRODUCTS as ProductType[]
return
}
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: #f4f5f7; min-height: 100vh; }
.date-picker { display: flex;
flex-direction: row;
background-color: #fff; padding: 20rpx 30rpx; gap: 20rpx; margin-bottom: 2rpx; }
.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: #09C39D; color: #fff; font-weight: bold; }
.overview-section, .trend-section, .product-section { background-color: #fff; margin: 20rpx 20rpx 0; border-radius: 16rpx; padding: 30rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04); }
.section-title { font-size: 30rpx; font-weight: bold; color: #333; margin-bottom: 24rpx; padding-left: 18rpx; border-left-width: 6rpx; border-left-style: solid; border-left-color: #09C39D; }
.overview-grid { display: flex;
flex-direction: row;
flex-wrap: wrap; }
.overview-item { width: 50%; padding: 20rpx 0; text-align: center; box-sizing: border-box; }
.overview-item:nth-child(odd) { border-right-width: 1rpx; border-right-style: solid; border-right-color: #f5f5f5; }
.overview-value { font-size: 40rpx; font-weight: bold; color: #09C39D; display: block; }
.overview-label { font-size: 24rpx; color: #999; }
.trend-chart { padding: 20rpx 0; }
.chart-bars { display: flex;
flex-direction: row;
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, #A6F1E4 0%, #69DFC2 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-width: 1rpx; border-bottom-style: solid; border-bottom-color: #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: #09C39D; }
</style>