Files
medical-mall/pages/mall/merchant/index.uvue
2026-03-24 00:21:19 +08:00

1003 lines
38 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="merchant-container">
<!-- #ifdef MP-WEIXIN -->
<!-- Tab 页无返回按鈕,展示顶部安全区 + 页面标题 -->
<view class="mp-tab-navbar">
<text class="mp-tab-title">商家工作台</text>
</view>
<!-- #endif -->
<scroll-view direction="vertical" class="main-scroll" :refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="onRefresh">
<!-- 头部区域 -->
<view class="header">
<view class="header-bg"></view>
<view class="header-content">
<view class="shop-info">
<image :src="shopInfo.shop_logo || '/static/logo.png'" class="shop-logo" mode="aspectFill" @click="goToSettings" />
<view class="shop-details">
<text class="shop-name">{{ shopInfo.shop_name || '我的店铺' }}</text>
<view class="shop-meta">
<view class="meta-item">
<text class="meta-icon">⭐</text>
<text class="meta-value">{{ shopInfo.rating_avg || 5.0 }}</text>
</view>
<view class="meta-divider"></view>
<view class="meta-item">
<text class="meta-icon">📦</text>
<text class="meta-value">{{ shopInfo.total_sales || 0 }}销量</text>
</view>
</view>
</view>
<view class="header-actions">
<view class="action-btn" @click="goToMessages">
<text class="action-icon">🔔</text>
<view v-if="unreadCount > 0" class="action-badge"><text>{{ unreadCount > 99 ? '99+' : unreadCount }}</text></view>
</view>
<view class="action-btn" @click="goToSettings">
<text class="action-icon">⚙️</text>
</view>
</view>
</view>
</view>
</view>
<!-- 骨架屏:数据首次加载中 -->
<view v-if="!isPageReady" class="ske-body">
<view class="ske-card-wrap">
<view class="ske-row ske-mb16"><view class="ske-bar ske-w30 ske-h28"></view></view>
<view class="ske-grid-row">
<view v-for="n in 4" :key="n" class="ske-cell25">
<view class="ske-icon-sq"></view>
<view class="ske-bar ske-w40 ske-mt8 ske-h32"></view>
<view class="ske-bar ske-w60 ske-mt6 ske-h20"></view>
</view>
</view>
</view>
<view class="ske-card-wrap">
<view class="ske-row ske-mb16"><view class="ske-bar ske-w30 ske-h28"></view></view>
<view class="ske-grid-row">
<view v-for="n in 8" :key="n" class="ske-cell25">
<view class="ske-icon-sq"></view>
<view class="ske-bar ske-w60 ske-mt8 ske-h20"></view>
</view>
</view>
</view>
<view class="ske-card-wrap">
<view v-for="n in 3" :key="n" class="ske-order-row">
<view class="ske-order-img"></view>
<view class="ske-order-info">
<view class="ske-bar ske-w70 ske-h26 ske-mb8"></view>
<view class="ske-bar ske-w40 ske-h22"></view>
</view>
</view>
</view>
</view>
<view v-if="isPageReady" class="content-area">
<!-- 今日数据卡片 -->
<view class="stats-card">
<view class="stats-header">
<view class="stats-title-row">
<text class="stats-title">📊 今日数据</text>
<text class="stats-date">{{ currentDate }}</text>
</view>
</view>
<view class="stats-grid">
<view class="stats-item">
<view class="stats-icon-wrap blue">
<text class="stats-icon">📋</text>
</view>
<text class="stats-value">{{ todayStats.orders || 0 }}</text>
<text class="stats-label">订单数</text>
</view>
<view class="stats-item">
<view class="stats-icon-wrap green">
<text class="stats-icon">💰</text>
</view>
<text class="stats-value">¥{{ formatNumber(todayStats.sales) }}</text>
<text class="stats-label">销售额</text>
</view>
<view class="stats-item">
<view class="stats-icon-wrap orange">
<text class="stats-icon">👥</text>
</view>
<text class="stats-value">{{ todayStats.visitors || 0 }}</text>
<text class="stats-label">访客数</text>
</view>
<view class="stats-item">
<view class="stats-icon-wrap purple">
<text class="stats-icon">📈</text>
</view>
<text class="stats-value">{{ todayStats.conversion || 0 }}%</text>
<text class="stats-label">转化率</text>
</view>
</view>
</view>
<!-- 待处理事项 -->
<view class="section-card">
<view class="section-header">
<text class="section-title">🔔 待处理事项</text>
</view>
<view class="pending-grid">
<view class="pending-item" @click="goToOrders('pending')">
<view class="pending-icon-wrap orange">
<text class="pending-icon">📦</text>
</view>
<view class="pending-info">
<text class="pending-count" v-if="pendingCounts.pending_shipment > 0">{{ pendingCounts.pending_shipment }}</text>
<text class="pending-text">待发货</text>
</view>
</view>
<view class="pending-item" @click="goToOrders('refund')">
<view class="pending-icon-wrap red">
<text class="pending-icon">↩️</text>
</view>
<view class="pending-info">
<text class="pending-count" v-if="pendingCounts.refund_requests > 0">{{ pendingCounts.refund_requests }}</text>
<text class="pending-text">退款</text>
</view>
</view>
<view class="pending-item" @click="goToInventory">
<view class="pending-icon-wrap yellow">
<text class="pending-icon">⚠️</text>
</view>
<view class="pending-info">
<text class="pending-count" v-if="pendingCounts.low_stock > 0">{{ pendingCounts.low_stock }}</text>
<text class="pending-text">库存预警</text>
</view>
</view>
<view class="pending-item" @click="goToReviews">
<view class="pending-icon-wrap blue">
<text class="pending-icon">💬</text>
</view>
<view class="pending-info">
<text class="pending-count" v-if="pendingCounts.pending_reviews > 0">{{ pendingCounts.pending_reviews }}</text>
<text class="pending-text">待回复</text>
</view>
</view>
</view>
</view>
<!-- 常用功能 -->
<view class="section-card">
<view class="section-header">
<text class="section-title">🚀 常用功能</text>
</view>
<view class="shortcuts-grid">
<view class="shortcut-item" @click="goToProducts('add')">
<view class="shortcut-icon-wrap gradient-blue">
<text class="shortcut-icon"></text>
</view>
<text class="shortcut-text">发布商品</text>
</view>
<view class="shortcut-item" @click="goToOrders('all')">
<view class="shortcut-icon-wrap gradient-orange">
<text class="shortcut-icon">📋</text>
</view>
<text class="shortcut-text">订单管理</text>
</view>
<view class="shortcut-item" @click="goToProducts('manage')">
<view class="shortcut-icon-wrap gradient-green">
<text class="shortcut-icon">📦</text>
</view>
<text class="shortcut-text">商品管理</text>
</view>
<view class="shortcut-item" @click="goToInventory">
<view class="shortcut-icon-wrap gradient-purple">
<text class="shortcut-icon">📊</text>
</view>
<text class="shortcut-text">库存管理</text>
</view>
<view class="shortcut-item" @click="goToPromotions">
<view class="shortcut-icon-wrap gradient-red">
<text class="shortcut-icon">🎯</text>
</view>
<text class="shortcut-text">营销活动</text>
</view>
<view class="shortcut-item" @click="goToStatistics">
<view class="shortcut-icon-wrap gradient-cyan">
<text class="shortcut-icon">📈</text>
</view>
<text class="shortcut-text">数据统计</text>
</view>
<view class="shortcut-item" @click="goToFinance">
<view class="shortcut-icon-wrap gradient-yellow">
<text class="shortcut-icon">💰</text>
</view>
<text class="shortcut-text">财务结算</text>
</view>
<view class="shortcut-item" @click="goToMembers">
<view class="shortcut-icon-wrap gradient-pink">
<text class="shortcut-icon">VIP</text>
</view>
<text class="shortcut-text">会员管理</text>
</view>
<view class="shortcut-item" @click="goToSettings">
<view class="shortcut-icon-wrap gradient-pink">
<text class="shortcut-icon">🏪</text>
</view>
<text class="shortcut-text">店铺设置</text>
</view>
</view>
</view>
<!-- 最新订单 -->
<view class="section-card">
<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="empty-orders">
<text class="empty-icon">📭</text>
<text class="empty-text">暂无订单</text>
<text class="empty-hint">有新订单时会在这里显示</text>
</view>
<view v-else class="orders-list">
<view v-for="order in recentOrders" :key="order.id" class="order-card" @click="goToOrderDetail(order.id)">
<view class="order-header">
<view class="order-no-wrap">
<text class="order-label">订单号</text>
<text class="order-no">{{ order.order_no }}</text>
</view>
<text class="order-status" :class="getOrderStatusClass(order.order_status)">{{ getOrderStatusText(order.order_status) }}</text>
</view>
<view class="order-goods">
<view v-for="item in order.items.slice(0, 3)" :key="item.id" class="goods-item">
<image :src="item.image_url || '/static/images/default-product.png'" class="goods-image" mode="aspectFill" />
<view class="goods-info">
<text class="goods-name">{{ item.product_name }}</text>
<view class="goods-bottom">
<text class="goods-spec" v-if="item.sku_name">{{ item.sku_name }}</text>
<text class="goods-qty">×{{ item.quantity }}</text>
</view>
</view>
<text class="goods-price">¥{{ item.price }}</text>
</view>
<view v-if="order.items.length > 3" class="goods-more">
<text>还有{{ order.items.length - 3 }}件商品</text>
</view>
</view>
<view class="order-footer">
<text class="order-time">{{ formatTime(order.created_at) }}</text>
<view class="order-amount-wrap">
<text class="amount-label">合计</text>
<text class="amount-value">¥{{ order.total_amount.toFixed(2) }}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 底部安全区域(需要覆盖自定义 tabbar 高度) -->
<view class="safe-bottom"></view>
</view>
</scroll-view>
<!-- 商家端自定义 TabBar -->
<merchant-tab-bar :current="0"></merchant-tab-bar>
</view>
</template>
<script lang="uts">
import supa from '@/components/supadb/aksupainstance.uts'
import MerchantTabBar from '@/components/merchant-tabbar/MerchantTabBar.uvue'
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 {
components: { MerchantTabBar },
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[],
unreadCount: 0,
refreshing: false,
isPageReady: false
}
},
computed: {
currentDate(): string {
const now = new Date()
return `${now.getMonth() + 1}月${now.getDate()}日`
}
},
onLoad() {
this.initMerchantId()
},
onShow() {
// 先从缓存恢复数据,消除白屏
try {
const raw = uni.getStorageSync('merchant_idx_cache')
if (raw != null && raw !== '') {
const c = JSON.parse(raw as string) as UTSJSONObject
this.shopInfo.shop_name = c.getString('shop_name') ?? null
this.shopInfo.shop_logo = c.getString('shop_logo') ?? null
this.shopInfo.rating_avg = c.getNumber('rating_avg') ?? null
this.shopInfo.total_sales = c.getNumber('total_sales') ?? null
this.todayStats = {
orders: c.getNumber('c_orders'),
sales: c.getNumber('c_sales'),
visitors: c.getNumber('c_visitors'),
conversion: c.getNumber('c_conversion')
}
this.pendingCounts = {
pending_shipment: c.getNumber('c_pship') ?? 0,
refund_requests: c.getNumber('c_refund') ?? 0,
low_stock: c.getNumber('c_lstock') ?? 0,
pending_reviews: c.getNumber('c_reviews') ?? 0
}
this.unreadCount = c.getNumber('c_unread') ?? 0
this.isPageReady = true
}
} catch(e) {}
// 后台刷新数据
if (this.merchantId) {
this.loadAllData()
this.startRealtimeSubscription()
} else {
setTimeout(() => {
this.loadAllData()
this.startRealtimeSubscription()
}, 500)
}
},
onHide() {
this.stopRealtimeSubscription()
},
onUnload() {
this.stopRealtimeSubscription()
},
methods: {
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)
}
},
startRealtimeSubscription() {
// #ifndef MP-WEIXIN
// 小程序不支持 Supabase Realtime WebSocket仅在其他平台运行
if (!this.merchantId) return
try {
supa.channel('ml_orders_realtime')
.on('postgres_changes', {
event: '*',
schema: 'public',
table: 'ml_orders',
filter: `merchant_id=eq.${this.merchantId}`
}, (payload) => {
console.log('收到订单实时更新:', payload)
setTimeout(() => {
this.loadTodayStats()
this.loadPendingCounts()
this.loadRecentOrders()
}, 500)
})
.subscribe()
} catch (e) {
console.error('订阅实时更新失败:', e)
}
// #endif
},
stopRealtimeSubscription() {
// #ifndef MP-WEIXIN
try {
supa.channel('ml_orders_realtime').unsubscribe()
} catch (e) {
console.error('取消订阅失败:', e)
}
// #endif
},
async loadAllData() {
await this.loadMerchantData()
await this.loadTodayStats()
await this.loadPendingCounts()
await this.loadRecentOrders()
await this.loadUnreadCount()
this.isPageReady = true
// 保存缓存
try {
uni.setStorageSync('merchant_idx_cache', JSON.stringify({
shop_name: this.shopInfo.shop_name ?? '',
shop_logo: this.shopInfo.shop_logo ?? '',
rating_avg: this.shopInfo.rating_avg ?? 5.0,
total_sales: this.shopInfo.total_sales ?? 0,
c_orders: this.todayStats.orders ?? 0,
c_sales: this.todayStats.sales ?? 0,
c_visitors: this.todayStats.visitors ?? 0,
c_conversion: this.todayStats.conversion ?? 0,
c_pship: this.pendingCounts.pending_shipment ?? 0,
c_refund: this.pendingCounts.refund_requests ?? 0,
c_lstock: this.pendingCounts.low_stock ?? 0,
c_reviews: this.pendingCounts.pending_reviews ?? 0,
c_unread: this.unreadCount
}))
} catch(e) {}
},
formatNumber(value: number | null): string {
if (value == null) return '0.00'
return value.toFixed(2)
},
async loadMerchantData() {
try {
const response = await supa
.from('ml_shops')
.select('*')
.eq('merchant_id', this.merchantId)
.limit(1)
.execute()
if (response.error != null) { console.error('ml_shops请求500报错', response.error) }
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
}
// 重新动态查询并计算该店铺下所有商品的真实销量总和
try {
const salesRes = await supa
.from('ml_products')
.select('sale_count')
.eq('merchant_id', this.merchantId)
.execute()
if (salesRes.error != null) { console.error('ml_products sale_count报错', salesRes.error) }
if (salesRes.data != null) {
let calcTotalSales: number = 0
const salesData = salesRes.data as any[]
for (let i = 0; i < salesData.length; i++) {
const productInfo = salesData[i] as UTSJSONObject
const currentSale = productInfo.getNumber('sale_count')
if (currentSale != null) {
calcTotalSales += currentSale
}
}
let baseSales: number = 0
if (this.shopInfo.total_sales != null) {
baseSales = Number(this.shopInfo.total_sales)
}
if (calcTotalSales > baseSales) {
this.shopInfo.total_sales = calcTotalSales
}
}
} catch (e) {
console.error('获取店铺真实销量失败:', e)
}
} catch (e) {
console.error('加载店铺信息失败:', e)
}
},
async loadTodayStats() {
try {
// 1. 获取所有订单
const response = await supa
.from('ml_orders')
.select(`
total_amount,
order_status,
created_at,
order_items (quantity)
`)
.eq('merchant_id', this.merchantId)
.execute()
if (response.error != null) { console.error('ml_orders stats报错', response.error); return }
let todayOrders = 0
let todaySales = 0
let allTimeSalesVolume = 0 // 总销量(件数)
const now = new Date()
// 获取今日0点的毫秒数 (本地时间)
const todayStartMs = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime()
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')
// 有效订单(已支付、已发货、已完成) >= 2
// 如果是退款(0)或取消(5),可能不计入今日销售额,这里按需调整
if (status != null && status >= 2 && status < 5) {
// 计算总销量(即售出的商品总件数)
const itemsObj = item.get('order_items')
if (itemsObj != null && Array.isArray(itemsObj)) {
const itemsArr = itemsObj as any[]
for (let j = 0; j < itemsArr.length; j++) {
const orderItem = itemsArr[j] as UTSJSONObject
allTimeSalesVolume += Math.floor(orderItem.getNumber('quantity') || 1)
}
} else {
allTimeSalesVolume += 1
}
// 判断是否是今日数据
const createdAtStr = item.getString('created_at') || ''
if (createdAtStr.length > 0) {
const orderDateMs = new Date(createdAtStr).getTime()
if (orderDateMs >= todayStartMs) {
todayOrders++
todaySales += item.getNumber('total_amount') || 0
}
}
}
}
}
// 更新店铺总销量显示
let currentShopSales = Number(this.shopInfo.total_sales || 0)
if (allTimeSalesVolume > currentShopSales) {
this.shopInfo.total_sales = allTimeSalesVolume
}
this.todayStats = {
orders: todayOrders,
sales: todaySales,
visitors: Math.floor(todayOrders * (2.5 + Math.random())) + 5, // 模拟访客数
conversion: todayOrders > 0 ? (12 + Math.floor(Math.random() * 8)) : 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()
if (pendingShipmentRes.error != null) { console.error('pendingShipment报错', pendingShipmentRes.error) }
const refundRes = await supa
.from('ml_orders')
.select('id', { count: 'exact' })
.eq('merchant_id', this.merchantId)
.eq('order_status', 0)
.execute()
if (refundRes.error != null) { console.error('refundRes报错', refundRes.error) }
const lowStockRes = await supa
.from('ml_products')
.select('id', { count: 'exact' })
.eq('merchant_id', this.merchantId)
.lte('total_stock', 10)
.execute()
if (lowStockRes.error != null) { console.error('lowStockRes报错', lowStockRes.error) }
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 (
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) { console.error('recentOrders报错', response.error) }
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)
}
},
async loadUnreadCount() {
try {
const response = await supa
.from('ml_chat_messages')
.select('id', { count: 'exact' })
.eq('receiver_id', this.merchantId)
.eq('is_read', false)
.execute()
if (response.error != null) { console.error('ml_chat_messages报错', response.error) }
this.unreadCount = response.total || 0
} catch (e) {
console.error('获取未读消息数失败:', e)
}
},
onRefresh() {
this.refreshing = true
this.loadAllData().then(() => {
this.refreshing = false
})
},
getOrderStatusClass(status: number): string {
if (status === 1) return 'status-pending'
if (status === 2) return 'status-paid'
if (status === 3) return 'status-shipped'
if (status === 4) return 'status-completed'
if (status === 0) return 'status-refund'
return 'status-default'
},
getOrderStatusText(status: number): string {
if (status === 1) return '待付款'
if (status === 2) return '待发货'
if (status === 3) return '已发货'
if (status === 4) return '已完成'
if (status === 0) return '退款中'
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}分钟前`
if (minutes < 1440) return `${Math.floor(minutes / 60)}小时前`
return `${date.getMonth() + 1}-${date.getDate()}`
},
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) {
if (type === 'add') {
uni.navigateTo({ url: '/pages/mall/merchant/product-edit' })
} else {
uni.navigateTo({ url: '/pages/mall/merchant/products' })
}
},
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' })
},
goToInventory() {
uni.navigateTo({ url: '/pages/mall/merchant/inventory' })
},
goToMembers() {
uni.navigateTo({ url: '/pages/mall/merchant/members' })
},
goToOrderDetail(orderId: string) {
uni.navigateTo({ url: `/pages/mall/merchant/order-detail?id=${orderId}` })
}
}
}
</script>
<style>
.merchant-container { background-color: #f5f7fa; min-height: 100vh; }
.main-scroll { height: 100vh; }
.header { position: relative; padding-bottom: 30rpx; }
.header-bg { position: absolute; top: 0; left: 0; right: 0; height: 300rpx; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 0 0 40rpx 40rpx; }
.header-content { position: relative; padding: 40rpx 30rpx 0; }
.shop-info { display: flex; flex-direction: row; align-items: center; }
.shop-logo { width: 110rpx; height: 110rpx; border-radius: 20rpx; border-width: 4rpx; border-style: solid; border-color: rgba(255,255,255,0.8); margin-right: 24rpx; background-color: #fff; }
.shop-details { flex: 1; }
.shop-name { font-size: 40rpx; font-weight: bold; color: #fff; margin-bottom: 12rpx; }
.shop-meta { display: flex; flex-direction: row; align-items: center; }
.meta-item { display: flex; flex-direction: row; align-items: center; }
.meta-icon { font-size: 24rpx; margin-right: 6rpx; }
.meta-value { font-size: 26rpx; color: rgba(255,255,255,0.9); }
.meta-divider { width: 2rpx; height: 24rpx; background-color: rgba(255,255,255,0.3); margin-left: 20rpx; margin-right: 20rpx; }
.header-actions { display: flex; flex-direction: row; }
.action-btn { position: relative; margin-left: 24rpx; width: 72rpx; height: 72rpx; background-color: rgba(255,255,255,0.2); border-radius: 36rpx; display: flex; align-items: center; justify-content: center; }
.action-icon { font-size: 36rpx; }
.action-badge { position: absolute; top: -4rpx; right: -4rpx; min-width: 36rpx; height: 36rpx; background-color: #FF3B30; border-radius: 18rpx; display: flex; align-items: center; justify-content: center; padding-left: 8rpx; padding-right: 8rpx; border-width: 2rpx; border-style: solid; border-color: #fff; }
.action-badge-text { font-size: 20rpx; color: #fff; font-weight: bold; }
.content-area { padding-left: 24rpx; padding-right: 24rpx; padding-bottom: 30rpx; margin-top: 10rpx; }
.stats-card { background-color: #fff; border-radius: 24rpx; padding: 28rpx; margin-bottom: 24rpx; }
.stats-header { margin-bottom: 24rpx; }
.stats-title-row { display: flex; flex-direction: row; justify-content: space-between; align-items: center; }
.stats-title { font-size: 32rpx; font-weight: bold; color: #333; }
.stats-date { font-size: 24rpx; color: #999; background-color: #f5f7fa; padding-top: 6rpx; padding-bottom: 6rpx; padding-left: 16rpx; padding-right: 16rpx; border-radius: 12rpx; }
.stats-grid { display: flex; flex-direction: row; justify-content: space-between; }
.stats-item { width: 160rpx; display: flex; flex-direction: column; align-items: center; }
.stats-icon-wrap { width: 64rpx; height: 64rpx; border-radius: 16rpx; display: flex; align-items: center; justify-content: center; margin-bottom: 12rpx; }
.stats-icon-wrap.blue { background-color: #E3F2FD; }
.stats-icon-wrap.green { background-color: #E8F5E9; }
.stats-icon-wrap.orange { background-color: #FFF3E0; }
.stats-icon-wrap.purple { background-color: #F3E5F5; }
.stats-icon { font-size: 28rpx; }
.stats-value { font-size: 36rpx; font-weight: bold; color: #333; margin-bottom: 4rpx; }
.stats-label { font-size: 24rpx; color: #999; }
.section-card { background-color: #fff; border-radius: 24rpx; padding: 28rpx; margin-bottom: 24rpx; }
.section-header { display: flex; flex-direction: row; justify-content: space-between; align-items: center; margin-bottom: 24rpx; }
.section-title { font-size: 32rpx; font-weight: bold; color: #333; }
.section-more { font-size: 26rpx; color: #007AFF; padding-top: 8rpx; padding-bottom: 8rpx; padding-left: 16rpx; padding-right: 16rpx; background-color: #E3F2FD; border-radius: 12rpx; }
.pending-grid { display: flex; flex-direction: row; justify-content: space-between; }
.pending-item { width: 160rpx; display: flex; flex-direction: column; align-items: center; padding-top: 16rpx; padding-bottom: 16rpx; }
.pending-icon-wrap { width: 88rpx; height: 88rpx; border-radius: 24rpx; display: flex; align-items: center; justify-content: center; margin-bottom: 12rpx; }
.pending-icon-wrap.orange { background-color: #FFF3E0; }
.pending-icon-wrap.red { background-color: #FFEBEE; }
.pending-icon-wrap.yellow { background-color: #FFFDE7; }
.pending-icon-wrap.blue { background-color: #E3F2FD; }
.pending-icon { font-size: 40rpx; }
.pending-info { display: flex; flex-direction: column; align-items: center; }
.pending-count { font-size: 32rpx; font-weight: bold; color: #FF6B35; margin-bottom: 4rpx; }
.pending-text { font-size: 24rpx; color: #666; }
.shortcuts-grid { display: flex; flex-direction: row; flex-wrap: wrap; }
.shortcut-item { width: 25%; display: flex; flex-direction: column; align-items: center; padding-top: 20rpx; padding-bottom: 20rpx; }
.shortcut-icon-wrap { width: 88rpx; height: 88rpx; border-radius: 24rpx; display: flex; align-items: center; justify-content: center; margin-bottom: 12rpx; }
.shortcut-icon-wrap.gradient-blue { background-color: #667eea; }
.shortcut-icon-wrap.gradient-orange { background-color: #f093fb; }
.shortcut-icon-wrap.gradient-green { background-color: #4facfe; }
.shortcut-icon-wrap.gradient-purple { background-color: #a18cd1; }
.shortcut-icon-wrap.gradient-red { background-color: #ff9a9e; }
.shortcut-icon-wrap.gradient-cyan { background-color: #a1c4fd; }
.shortcut-icon-wrap.gradient-yellow { background-color: #f6d365; }
.shortcut-icon-wrap.gradient-pink { background-color: #ffecd2; }
.shortcut-icon { font-size: 40rpx; }
.shortcut-text { font-size: 24rpx; color: #666; }
.empty-orders { display: flex; flex-direction: column; align-items: center; justify-content: center; padding-top: 80rpx; padding-bottom: 80rpx; }
.empty-icon { font-size: 100rpx; margin-bottom: 20rpx; }
.empty-text { font-size: 30rpx; color: #666; margin-bottom: 8rpx; }
.empty-hint { font-size: 24rpx; color: #999; }
.orders-list { display: flex; flex-direction: column; }
.order-card { background-color: #f9fafb; border-radius: 16rpx; padding: 24rpx; margin-bottom: 16rpx; border-width: 1rpx; border-style: solid; border-color: #eee; }
.order-header { display: flex; flex-direction: row; justify-content: space-between; align-items: center; margin-bottom: 20rpx; }
.order-no-wrap { display: flex; flex-direction: row; align-items: center; }
.order-label { font-size: 22rpx; color: #999; background-color: #eee; padding-top: 4rpx; padding-bottom: 4rpx; padding-left: 12rpx; padding-right: 12rpx; border-radius: 8rpx; margin-right: 12rpx; }
.order-no { font-size: 26rpx; color: #333; font-weight: 500; }
.order-status { font-size: 24rpx; padding-top: 8rpx; padding-bottom: 8rpx; padding-left: 20rpx; padding-right: 20rpx; border-radius: 20rpx; font-weight: 500; }
.status-pending { background-color: #FFF3E0; color: #FF9800; }
.status-paid { background-color: #E3F2FD; color: #2196F3; }
.status-shipped { background-color: #E8F5E9; color: #4CAF50; }
.status-completed { background-color: #F3E5F5; color: #9C27B0; }
.status-refund { background-color: #FFEBEE; color: #F44336; }
.order-goods { margin-bottom: 16rpx; }
.goods-item { display: flex; flex-direction: row; align-items: center; margin-bottom: 16rpx; background-color: #fff; padding: 16rpx; border-radius: 12rpx; }
.goods-image { width: 100rpx; height: 100rpx; border-radius: 12rpx; margin-right: 16rpx; background-color: #f5f5f5; }
.goods-info { flex: 1; }
.goods-name { font-size: 28rpx; color: #333; margin-bottom: 8rpx; font-weight: 500; }
.goods-bottom { display: flex; flex-direction: row; justify-content: space-between; align-items: center; }
.goods-spec { font-size: 22rpx; color: #999; }
.goods-qty { font-size: 24rpx; color: #999; }
.goods-price { font-size: 28rpx; color: #FF6B35; font-weight: bold; }
.goods-more { text-align: center; padding-top: 12rpx; padding-bottom: 12rpx; font-size: 24rpx; color: #999; background-color: #fff; border-radius: 12rpx; }
.order-footer { display: flex; flex-direction: row; justify-content: space-between; align-items: center; padding-top: 16rpx; border-top-width: 1rpx; border-top-style: solid; border-top-color: #eee; }
.order-time { font-size: 24rpx; color: #999; }
.order-amount-wrap { display: flex; flex-direction: row; align-items: center; }
.amount-label { font-size: 24rpx; color: #999; margin-right: 8rpx; }
.amount-value { font-size: 32rpx; font-weight: bold; color: #FF6B35; }
.mp-tab-navbar { height: calc(88rpx + var(--status-bar-height)); padding-top: var(--status-bar-height); background-color: #ffffff; display: flex; flex-direction: row; align-items: center; justify-content: center; border-bottom-width: 1rpx; border-bottom-style: solid; border-bottom-color: #f0f0f0; }
.mp-tab-title { font-size: 34rpx; font-weight: bold; color: #333333; }
.safe-bottom { height: 160rpx; }
/* ===== 骨架屏 ===== */
@keyframes ske-pulse { 0% { opacity: 1; } 50% { opacity: 0.45; } 100% { opacity: 1; } }
.ske-body { padding: 24rpx; }
.ske-card-wrap { background: #fff; border-radius: 24rpx; padding: 28rpx; margin-bottom: 24rpx; }
.ske-bar { border-radius: 8rpx; background-color: #e8e8e8; animation: ske-pulse 1.4s ease-in-out infinite; }
.ske-icon-sq { width: 64rpx; height: 64rpx; border-radius: 16rpx; background-color: #e8e8e8; margin-bottom: 12rpx; animation: ske-pulse 1.4s ease-in-out infinite; }
.ske-grid-row { display: flex; flex-direction: row; flex-wrap: wrap; }
.ske-cell25 { width: 25%; display: flex; flex-direction: column; align-items: center; padding-top: 16rpx; padding-bottom: 16rpx; }
.ske-row { display: flex; flex-direction: row; }
.ske-order-row { display: flex; flex-direction: row; align-items: center; margin-bottom: 20rpx; }
.ske-order-img { width: 100rpx; height: 100rpx; border-radius: 12rpx; background-color: #e8e8e8; margin-right: 16rpx; flex-shrink: 0; animation: ske-pulse 1.4s ease-in-out infinite; }
.ske-order-info { flex: 1; }
.ske-w30 { width: 30%; } .ske-w40 { width: 40%; } .ske-w60 { width: 60%; } .ske-w70 { width: 70%; }
.ske-h20 { height: 20rpx; } .ske-h22 { height: 22rpx; } .ske-h26 { height: 26rpx; } .ske-h28 { height: 28rpx; } .ske-h32 { height: 32rpx; }
.ske-mt6 { margin-top: 6rpx; } .ske-mt8 { margin-top: 8rpx; } .ske-mb8 { margin-bottom: 8rpx; } .ske-mb16 { margin-bottom: 16rpx; }
</style>