Files
medical-mall/pages/mall/merchant/index.uvue

875 lines
20 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.
<!-- 商家端首页 - UTS Android 兼容 -->
<template>
<view class="merchant-container">
<!-- 头部导航 -->
<view class="header">
<view class="header-content">
<view class="shop-info">
<image :src="shopInfo.shop_logo || '/static/images/default-shop.png'" class="shop-logo" mode="aspectFit" />
<view class="shop-details">
<text class="shop-name">{{ shopInfo.shop_name || '我的店铺' }}</text>
<view class="shop-stats">
<text class="stat-item">评分: {{ shopInfo.rating_avg || 5.0 }}</text>
<text class="stat-item">销量: {{ shopInfo.total_sales || 0 }}</text>
</view>
</view>
</view>
<view class="header-actions">
<text class="action-btn" @click="goToMessages">消息</text>
<text class="action-btn" @click="goToSettings">设置</text>
</view>
</view>
</view>
<!-- 数据概览 -->
<view class="overview-section">
<text class="section-title">今日数据</text>
<view class="overview-grid">
<view class="overview-item">
<text class="overview-value">{{ todayStats.orders }}</text>
<text class="overview-label">订单数</text>
</view>
<view class="overview-item">
<text class="overview-value">¥{{ formatNumber(todayStats.sales) }}</text>
<text class="overview-label">销售额</text>
</view>
<view class="overview-item">
<text class="overview-value">{{ todayStats.visitors }}</text>
<text class="overview-label">访客数</text>
</view>
<view class="overview-item">
<text class="overview-value">{{ todayStats.conversion }}%</text>
<text class="overview-label">转化率</text>
</view>
</view>
</view>
<!-- 待处理事项 -->
<view class="pending-section">
<text class="section-title">待处理</text>
<view class="pending-list">
<view class="pending-item" @click="goToOrders('pending')">
<text class="pending-icon">📦</text>
<text class="pending-text">待发货订单</text>
<text class="pending-count">{{ pendingCounts.pending_shipment }}</text>
</view>
<view class="pending-item" @click="goToOrders('refund')">
<text class="pending-icon">↩️</text>
<text class="pending-text">退款处理</text>
<text class="pending-count">{{ pendingCounts.refund_requests }}</text>
</view>
<view class="pending-item" @click="goToProducts('low_stock')">
<text class="pending-icon">⚠️</text>
<text class="pending-text">库存预警</text>
<text class="pending-count">{{ pendingCounts.low_stock }}</text>
</view>
<view class="pending-item" @click="goToReviews">
<text class="pending-icon">💬</text>
<text class="pending-text">待回复评价</text>
<text class="pending-count">{{ pendingCounts.pending_reviews }}</text>
</view>
</view>
</view>
<!-- 快捷功能 -->
<view class="shortcuts-section">
<text class="section-title">快捷功能</text>
<view class="shortcuts-grid">
<view class="shortcut-item" @click="goToProducts('add')">
<text class="shortcut-icon"></text>
<text class="shortcut-text">添加商品</text>
</view>
<view class="shortcut-item" @click="goToOrders('all')">
<text class="shortcut-icon">📋</text>
<text class="shortcut-text">订单管理</text>
</view>
<view class="shortcut-item" @click="goToProducts('manage')">
<text class="shortcut-icon">📦</text>
<text class="shortcut-text">商品管理</text>
</view>
<view class="shortcut-item" @click="goToPromotions">
<text class="shortcut-icon">🎯</text>
<text class="shortcut-text">营销活动</text>
</view>
<view class="shortcut-item" @click="goToStatistics">
<text class="shortcut-icon">📊</text>
<text class="shortcut-text">数据统计</text>
</view>
<view class="shortcut-item" @click="goToFinance">
<text class="shortcut-icon">💰</text>
<text class="shortcut-text">财务结算</text>
</view>
</view>
</view>
<!-- 最新订单 -->
<view class="recent-orders-section">
<view class="section-header">
<text class="section-title">最新订单</text>
<text class="section-more" @click="goToOrders('all')">查看全部</text>
</view>
<view v-if="recentOrders.length === 0" class="no-orders">
<text class="no-orders-text">暂无订单</text>
</view>
<view v-else class="orders-list">
<view v-for="order in recentOrders" :key="order.id" class="order-item" @click="goToOrderDetail(order.id)">
<view class="order-header">
<text class="order-no">{{ order.order_no }}</text>
<text class="order-status" :class="getOrderStatusClass(order.order_status)">{{ getOrderStatusText(order.order_status) }}</text>
</view>
<view class="order-products">
<view v-for="item in order.items" :key="item.id" class="product-item">
<image :src="item.image_url || '/static/images/default-product.png'" class="product-image" mode="aspectFit" />
<view class="product-info">
<text class="product-name">{{ item.product_name }}</text>
<text class="product-spec">{{ item.sku_specifications || '' }}</text>
<text class="product-price">¥{{ item.price }} × {{ item.quantity }}</text>
</view>
</view>
</view>
<view class="order-footer">
<text class="order-amount">合计: ¥{{ order.total_amount }}</text>
<text class="order-time">{{ formatTime(order.created_at) }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script lang="uts">
import supa from '@/components/supadb/aksupainstance.uts'
type ShopInfoType = {
id: string | null
merchant_id: string | null
shop_name: string | null
shop_logo: string | null
shop_banner: string | null
description: string | null
contact_name: string | null
contact_phone: string | null
rating_avg: number | null
total_sales: number | null
status: number | null
}
type OrderItemType = {
id: string
order_id: string
product_id: string
sku_id: string
product_name: string
sku_name: string
price: number
quantity: number
image_url: string
sku_snapshot: string
}
type OrderType = {
id: string
order_no: string
order_status: number
total_amount: number
created_at: string
items: OrderItemType[]
}
type TodayStatsType = {
orders: number | null
sales: number | null
visitors: number | null
conversion: number | null
}
type PendingCountsType = {
pending_shipment: number | null
refund_requests: number | null
low_stock: number | null
pending_reviews: number | null
}
export default {
data() {
return {
merchantId: '',
shopInfo: {
id: null,
merchant_id: null,
shop_name: null,
shop_logo: null,
shop_banner: null,
description: null,
contact_name: null,
contact_phone: null,
rating_avg: null,
total_sales: null,
status: null
} as ShopInfoType,
todayStats: {
orders: null,
sales: null,
visitors: null,
conversion: null
} as TodayStatsType,
pendingCounts: {
pending_shipment: 0,
refund_requests: 0,
low_stock: 0,
pending_reviews: 0
} as PendingCountsType,
recentOrders: [] as OrderType[]
}
},
onLoad() {
this.initMerchantId()
},
onShow() {
if (this.merchantId) {
this.loadMerchantData()
this.loadTodayStats()
this.loadPendingCounts()
this.loadRecentOrders()
} else {
setTimeout(() => {
this.loadMerchantData()
this.loadTodayStats()
this.loadPendingCounts()
this.loadRecentOrders()
}, 500)
}
},
methods: {
formatNumber(value: number | null): string {
if (value == null) return '0.00'
return value.toFixed(2)
},
async initMerchantId() {
try {
const session = supa.getSession()
if (session != null && session.user != null) {
this.merchantId = session.user.getString('id') || ''
}
if (!this.merchantId) {
this.merchantId = uni.getStorageSync('user_id') || ''
}
} catch (e) {
console.error('获取商户ID失败:', e)
}
},
async loadMerchantData() {
try {
const response = await supa
.from('ml_shops')
.select('*')
.eq('merchant_id', this.merchantId)
.limit(1)
.execute()
if (response.error != null || !response.data || (response.data as any[]).length === 0) {
this.shopInfo = {
id: null,
merchant_id: this.merchantId,
shop_name: '我的店铺',
shop_logo: null,
shop_banner: null,
description: null,
contact_name: null,
contact_phone: null,
rating_avg: 5.0,
total_sales: 0,
status: 1
}
return
}
const rawData = (response.data as any[])[0] as UTSJSONObject
this.shopInfo = {
id: rawData.getString('id') || null,
merchant_id: rawData.getString('merchant_id') || null,
shop_name: rawData.getString('shop_name') || '我的店铺',
shop_logo: rawData.getString('shop_logo') || null,
shop_banner: rawData.getString('shop_banner') || null,
description: rawData.getString('description') || null,
contact_name: rawData.getString('contact_name') || null,
contact_phone: rawData.getString('contact_phone') || null,
rating_avg: rawData.getNumber('rating_avg') || 5.0,
total_sales: rawData.getNumber('total_sales') || 0,
status: rawData.getNumber('status') || 1
}
} catch (e) {
console.error('加载店铺信息失败:', e)
}
},
async loadTodayStats() {
try {
const response = await supa
.from('ml_orders')
.select('total_amount, order_status', { count: 'exact' })
.eq('merchant_id', this.merchantId)
.execute()
if (response.error != null) {
console.error('获取统计数据失败:', response.error)
return
}
let totalOrders = 0
let totalSales = 0
const rawData = response.data as any[]
if (rawData != null) {
for (let i = 0; i < rawData.length; i++) {
const item = rawData[i] as UTSJSONObject
const status = item.getNumber('order_status')
if (status >= 2) {
totalOrders++
totalSales += item.getNumber('total_amount') || 0
}
}
}
this.todayStats = {
orders: totalOrders,
sales: totalSales,
visitors: Math.floor(totalOrders * 3),
conversion: totalOrders > 0 ? 15 : 0
}
} catch (e) {
console.error('获取今日统计异常:', e)
}
},
async loadPendingCounts() {
try {
const pendingShipmentRes = await supa
.from('ml_orders')
.select('id', { count: 'exact' })
.eq('merchant_id', this.merchantId)
.eq('order_status', 2)
.execute()
const refundRes = await supa
.from('ml_orders')
.select('id', { count: 'exact' })
.eq('merchant_id', this.merchantId)
.eq('order_status', 0)
.execute()
const lowStockRes = await supa
.from('ml_products')
.select('id', { count: 'exact' })
.eq('merchant_id', this.merchantId)
.lte('total_stock', 10)
.gte('total_stock', 0)
.execute()
this.pendingCounts = {
pending_shipment: pendingShipmentRes.total || 0,
refund_requests: refundRes.total || 0,
low_stock: lowStockRes.total || 0,
pending_reviews: 0
}
} catch (e) {
console.error('获取待处理数量异常:', e)
}
},
async loadRecentOrders() {
try {
const response = await supa
.from('ml_orders')
.select(`
*,
order_items!inner (
id,
product_id,
product_name,
sku_name,
price,
quantity,
image_url
)
`)
.eq('merchant_id', this.merchantId)
.order('created_at', { ascending: false })
.limit(5)
.execute()
if (response.error != null || !response.data) {
this.recentOrders = []
return
}
const rawData = response.data as any[]
const ordersData: OrderType[] = []
for (let i = 0; i < rawData.length; i++) {
const item = rawData[i] as UTSJSONObject
const order: OrderType = {
id: item.getString('id') || '',
order_no: item.getString('order_no') || '',
order_status: item.getNumber('order_status') || 1,
total_amount: item.getNumber('total_amount') || 0,
created_at: item.getString('created_at') || '',
items: []
}
const itemsObj = item.get('order_items')
if (itemsObj != null && Array.isArray(itemsObj)) {
const itemsArray = itemsObj as any[]
for (let j = 0; j < itemsArray.length; j++) {
const orderItem = itemsArray[j] as UTSJSONObject
order.items.push({
id: orderItem.getString('id') || '',
order_id: '',
product_id: orderItem.getString('product_id') || '',
sku_id: '',
product_name: orderItem.getString('product_name') || '',
sku_name: orderItem.getString('sku_name') || '',
price: orderItem.getNumber('price') || 0,
quantity: orderItem.getNumber('quantity') || 0,
image_url: orderItem.getString('image_url') || '',
sku_snapshot: ''
} as OrderItemType)
}
}
ordersData.push(order)
}
this.recentOrders = ordersData
} catch (e) {
console.error('加载最新订单异常:', e)
}
},
getOrderStatusClass(status: number): string {
switch (status) {
case 1: return 'status-pending'
case 2: return 'status-paid'
case 3: return 'status-shipped'
case 4: return 'status-delivered'
case 5: return 'status-completed'
default: return 'status-default'
}
},
getOrderStatusText(status: number): string {
switch (status) {
case 1: return '待付款'
case 2: return '待发货'
case 3: return '已发货'
case 4: return '已收货'
case 5: return '已完成'
case 0: return '退款中'
default: return '未知状态'
}
},
formatTime(timeStr: string): string {
if (!timeStr) return ''
const date = new Date(timeStr)
const now = new Date()
const diff = now.getTime() - date.getTime()
const minutes = Math.floor(diff / (1000 * 60))
if (minutes < 60) {
return `${minutes}分钟前`
} else if (minutes < 1440) {
return `${Math.floor(minutes / 60)}小时前`
} else {
return `${Math.floor(minutes / 1440)}天前`
}
},
goToMessages() {
uni.navigateTo({
url: '/pages/mall/merchant/messages'
})
},
goToSettings() {
uni.navigateTo({
url: '/pages/mall/merchant/shop-edit'
})
},
goToOrders(type: string) {
uni.navigateTo({
url: `/pages/mall/merchant/orders?type=${type}`
})
},
goToProducts(type: string) {
uni.navigateTo({
url: `/pages/mall/merchant/products?type=${type}`
})
},
goToPromotions() {
uni.navigateTo({
url: '/pages/mall/merchant/promotions'
})
},
goToStatistics() {
uni.navigateTo({
url: '/pages/mall/merchant/statistics'
})
},
goToFinance() {
uni.navigateTo({
url: '/pages/mall/merchant/finance'
})
},
goToReviews() {
uni.navigateTo({
url: '/pages/mall/merchant/reviews'
})
},
goToOrderDetail(orderId: string) {
uni.navigateTo({
url: `/pages/mall/merchant/order-detail?id=${orderId}`
})
}
}
}
</script>
<style>
.merchant-container {
background-color: #f5f5f5;
min-height: 100vh;
}
.header {
background-color: #fff;
padding: 20rpx 30rpx;
border-bottom: 1rpx solid #e5e5e5;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.shop-info {
display: flex;
align-items: center;
}
.shop-logo {
width: 80rpx;
height: 80rpx;
border-radius: 40rpx;
margin-right: 20rpx;
background-color: #f5f5f5;
}
.shop-details {
display: flex;
flex-direction: column;
}
.shop-name {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
}
.shop-stats {
display: flex;
}
.stat-item {
font-size: 24rpx;
color: #666;
margin-right: 20rpx;
}
.header-actions {
display: flex;
}
.action-btn {
font-size: 28rpx;
color: #007AFF;
margin-left: 30rpx;
}
.overview-section {
background-color: #fff;
margin: 20rpx;
padding: 30rpx;
border-radius: 16rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 30rpx;
}
.overview-grid {
display: flex;
justify-content: space-between;
}
.overview-item {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.overview-value {
font-size: 36rpx;
font-weight: bold;
color: #FF6B35;
margin-bottom: 10rpx;
}
.overview-label {
font-size: 24rpx;
color: #666;
}
.pending-section {
background-color: #fff;
margin: 20rpx;
padding: 30rpx;
border-radius: 16rpx;
}
.pending-list {
display: flex;
flex-direction: column;
}
.pending-item {
display: flex;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.pending-item:last-child {
border-bottom: none;
}
.pending-icon {
font-size: 32rpx;
margin-right: 20rpx;
width: 40rpx;
}
.pending-text {
font-size: 28rpx;
color: #333;
flex: 1;
}
.pending-count {
font-size: 28rpx;
color: #FF6B35;
font-weight: bold;
}
.shortcuts-section {
background-color: #fff;
margin: 20rpx;
padding: 30rpx;
border-radius: 16rpx;
}
.shortcuts-grid {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.shortcut-item {
display: flex;
flex-direction: column;
align-items: center;
width: 30%;
margin-bottom: 30rpx;
}
.shortcut-icon {
font-size: 48rpx;
margin-bottom: 15rpx;
}
.shortcut-text {
font-size: 24rpx;
color: #333;
text-align: center;
}
.recent-orders-section {
background-color: #fff;
margin: 20rpx;
padding: 30rpx;
border-radius: 16rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
}
.section-more {
font-size: 24rpx;
color: #007AFF;
}
.no-orders {
text-align: center;
padding: 60rpx 0;
}
.no-orders-text {
font-size: 26rpx;
color: #999;
}
.orders-list {
display: flex;
flex-direction: column;
}
.order-item {
border: 1rpx solid #e5e5e5;
border-radius: 12rpx;
padding: 20rpx;
margin-bottom: 20rpx;
}
.order-item:last-child {
margin-bottom: 0;
}
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15rpx;
}
.order-no {
font-size: 28rpx;
color: #333;
font-weight: bold;
}
.order-status {
font-size: 24rpx;
padding: 8rpx 16rpx;
border-radius: 20rpx;
}
.status-pending {
background-color: #FFF3CD;
color: #856404;
}
.status-paid {
background-color: #D4EDDA;
color: #155724;
}
.status-shipped {
background-color: #CCE5FF;
color: #004085;
}
.status-delivered {
background-color: #E2E3E5;
color: #383D41;
}
.status-completed {
background-color: #D1ECF1;
color: #0C5460;
}
.order-products {
margin-bottom: 15rpx;
}
.product-item {
display: flex;
align-items: center;
margin-bottom: 10rpx;
}
.product-item:last-child {
margin-bottom: 0;
}
.product-image {
width: 80rpx;
height: 80rpx;
border-radius: 8rpx;
margin-right: 15rpx;
background-color: #f5f5f5;
}
.product-info {
display: flex;
flex-direction: column;
flex: 1;
}
.product-name {
font-size: 26rpx;
color: #333;
margin-bottom: 5rpx;
}
.product-spec {
font-size: 22rpx;
color: #999;
margin-bottom: 5rpx;
}
.product-price {
font-size: 24rpx;
color: #666;
}
.order-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.order-amount {
font-size: 28rpx;
color: #FF6B35;
font-weight: bold;
}
.order-time {
font-size: 22rpx;
color: #999;
}
</style>