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

1103 lines
41 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">
<scroll-view direction="vertical" class="main-scroll" :refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="onRefresh">
<!-- ===== 顶部工作台头部(浅蓝渐变,老年友好) ===== -->
<view class="wt-header">
<view class="wt-header-top">
<!-- 机构信息行 -->
<view class="wt-shop-row">
<image :src="shopInfo.shop_logo || '/static/logo.png'" class="wt-shop-logo" mode="aspectFill" @click="goToSettings" />
<view class="wt-shop-info">
<text class="wt-shop-name">{{ shopInfo.shop_name || '我的机构' }}</text>
<view class="wt-shop-meta">
<text class="wt-meta-txt">⭐ {{ shopInfo.rating_avg || 5.0 }}</text>
<text class="wt-meta-sep">|</text>
<text class="wt-meta-txt">已服务 {{ shopInfo.total_sales || 0 }} 次</text>
</view>
</view>
<!-- 头部右侧功能按钮 -->
<view class="wt-header-btns">
<view class="wt-icon-btn" @click="goToMessages">
<text class="wt-icon-txt">🔔</text>
<text class="wt-icon-label">通知</text>
<view v-if="unreadCount > 0" class="wt-badge">
<text class="wt-badge-txt">{{ unreadCount > 99 ? '99+' : unreadCount }}</text>
</view>
</view>
<view class="wt-icon-btn" @click="goToSettings">
<text class="wt-icon-txt">⚙️</text>
<text class="wt-icon-label">设置</text>
</view>
</view>
</view>
</view>
<!-- 今日数据双行面板(白色底,嵌在头部底部) -->
<view class="wt-data-panel">
<!-- 第一行:服务订单状态快捷入口 -->
<view class="wt-panel-row">
<view
v-for="item in orderStatusList"
:key="item.key"
class="wt-panel-item"
@click="goToOrders(item.key)"
>
<view class="wt-panel-num-wrap">
<text class="wt-panel-num">{{ item.count }}</text>
<view v-if="item.count > 0" class="wt-panel-dot"></view>
</view>
<text class="wt-panel-lbl">{{ item.label }}</text>
</view>
</view>
<view class="wt-panel-sep"></view>
<!-- 第二行:核心经营指标 -->
<view class="wt-panel-row">
<view class="wt-panel-item">
<text class="wt-panel-biz-val">¥{{ formatNumber(bizStats.saleAmount) }}</text>
<text class="wt-panel-biz-lbl">今日结算</text>
</view>
<view class="wt-panel-item">
<text class="wt-panel-biz-val">{{ bizStats.orderCount || 0 }}</text>
<text class="wt-panel-biz-lbl">今日服务单</text>
</view>
<view class="wt-panel-item">
<text class="wt-panel-biz-val">{{ bizStats.visitorCount || 0 }}</text>
<text class="wt-panel-biz-lbl">咨询人数</text>
</view>
<view class="wt-panel-item">
<text class="wt-panel-biz-val">{{ bizStats.reviewCount || 0 }}</text>
<text class="wt-panel-biz-lbl">待评价</text>
</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 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="wt-card">
<view class="wt-card-hd">
<text class="wt-card-title">常用功能</text>
</view>
<view class="feature-grid">
<view class="feature-item" @click="goToProducts('add')">
<view class="feat-icon-wrap"><text class="feat-icon-txt"></text></view>
<text class="feat-label">发布服务</text>
</view>
<view class="feature-item" @click="goToOrders('all')">
<view class="feat-icon-wrap"><text class="feat-icon-txt">📋</text></view>
<text class="feat-label">服务订单</text>
</view>
<view class="feature-item" @click="goToProducts('manage')">
<view class="feat-icon-wrap"><text class="feat-icon-txt">🏥</text></view>
<text class="feat-label">服务管理</text>
</view>
<view class="feature-item" @click="goToStatistics">
<view class="feat-icon-wrap"><text class="feat-icon-txt">📊</text></view>
<text class="feat-label">数据统计</text>
</view>
<view class="feature-item" @click="goToInventory">
<view class="feat-icon-wrap"><text class="feat-icon-txt">💊</text></view>
<text class="feat-label">药械库存</text>
</view>
<view class="feature-item" @click="goToReviews">
<view class="feat-icon-wrap"><text class="feat-icon-txt">⭐</text></view>
<text class="feat-label">服务评价</text>
</view>
<view class="feature-item" @click="goToFinance">
<view class="feat-icon-wrap"><text class="feat-icon-txt">💰</text></view>
<text class="feat-label">结算补贴</text>
</view>
<view class="feature-item" @click="goToMembers">
<view class="feat-icon-wrap"><text class="feat-icon-txt">👴</text></view>
<text class="feat-label">服务对象</text>
</view>
<view class="feature-item" @click="goToPromotions">
<view class="feat-icon-wrap"><text class="feat-icon-txt">🎁</text></view>
<text class="feat-label">关怀活动</text>
</view>
<view class="feature-item" @click="goToSettings">
<view class="feat-icon-wrap"><text class="feat-icon-txt">🏢</text></view>
<text class="feat-label">机构资料</text>
</view>
<view class="feature-item" @click="goToHealthManagement">
<view class="feat-icon-wrap"><text class="feat-icon-txt">❤️</text></view>
<text class="feat-label">健康管理</text>
</view>
<view class="feature-item" @click="goToAiConsultation">
<view class="feat-icon-wrap"><text class="feat-icon-txt">🤖</text></view>
<text class="feat-label">AI问诊</text>
</view>
</view>
</view>
<!-- 待办与通知卡片 -->
<view class="wt-card">
<view class="wt-card-hd">
<text class="wt-card-title">待办与通知</text>
<view class="wt-notice-badge-wrap" v-if="noticeBadgeCount > 0">
<text class="wt-notice-badge-txt">{{ noticeBadgeCount }} 条待处理</text>
</view>
</view>
<view class="notice-list">
<!-- 待上门 -->
<view class="notice-item" @click="goToOrders('pending')" v-if="pendingCounts.pending_shipment > 0">
<view class="notice-dot dot-orange"></view>
<view class="notice-body">
<view class="notice-title-row">
<text class="notice-title">待上门服务</text>
<view class="notice-tag tag-orange"><text>{{ pendingCounts.pending_shipment }}单</text></view>
</view>
<text class="notice-desc">有用户已预约,请安排服务人员上门</text>
</view>
</view>
<!-- 退款售后 -->
<view class="notice-item" @click="goToOrders('refund')" v-if="pendingCounts.refund_requests > 0">
<view class="notice-dot dot-red"></view>
<view class="notice-body">
<view class="notice-title-row">
<text class="notice-title">取消/售后申请</text>
<view class="notice-tag tag-red"><text>{{ pendingCounts.refund_requests }}单</text></view>
</view>
<text class="notice-desc">用户提交了取消或售后申请,请及时处理</text>
</view>
</view>
<!-- 库存预警 -->
<view class="notice-item" @click="goToInventory" v-if="pendingCounts.low_stock > 0">
<view class="notice-dot dot-orange"></view>
<view class="notice-body">
<view class="notice-title-row">
<text class="notice-title">药械库存预警</text>
<view class="notice-tag tag-orange"><text>{{ pendingCounts.low_stock }}种</text></view>
</view>
<text class="notice-desc">部分药品或医疗器械库存不足,请及时补货</text>
</view>
</view>
<!-- 未读消息 -->
<view class="notice-item" @click="goToMessages" v-if="unreadCount > 0">
<view class="notice-dot dot-gray"></view>
<view class="notice-body">
<view class="notice-title-row">
<text class="notice-title">咨询消息提醒</text>
<view class="notice-tag tag-gray"><text>{{ unreadCount }}条</text></view>
</view>
<text class="notice-desc">有新的用户咨询或系统消息待查看</text>
</view>
</view>
<!-- 空状态 -->
<view v-if="noticeBadgeCount === 0" class="notice-empty">
<text class="notice-empty-txt">暂无待处理事项,服务正常运转中 ✅</text>
</view>
</view>
</view>
<!-- 最新服务订单(商品运营风格:图片+名称+指标) -->
<view class="wt-card">
<view class="wt-card-hd">
<text class="wt-card-title">最新服务订单</text>
<text class="wt-card-more" @click="goToOrders('all')">查看全部 </text>
</view>
<view v-if="recentOrders.length === 0" class="pi-empty">
<text class="pi-empty-txt">暂无服务订单,有新订单时会在这里显示</text>
</view>
<view v-else class="pi-list">
<view
v-for="(order, idx) in recentOrders"
:key="order.id"
class="pi-item"
:class="{ 'pi-item-last': idx === recentOrders.length - 1 }"
@click="goToOrderDetail(order.id)"
>
<image
:src="order.items.length > 0 ? (order.items[0].image_url || '/static/images/default-product.png') : '/static/images/default-product.png'"
class="pi-img"
mode="aspectFill"
/>
<view class="pi-info">
<text class="pi-name">{{ order.items.length > 0 ? order.items[0].product_name : '服务订单' }}</text>
<view class="pi-metrics">
<view class="pi-metric">
<text class="pi-metric-val">{{ getOrderStatusText(order.order_status) }}</text>
<text class="pi-metric-lbl">服务状态</text>
</view>
<view class="pi-metric">
<text class="pi-metric-val">¥{{ order.total_amount.toFixed(2) }}</text>
<text class="pi-metric-lbl">服务金额</text>
</view>
<view class="pi-metric">
<text class="pi-metric-val">{{ formatTime(order.created_at) }}</text>
<text class="pi-metric-lbl">预约时间</text>
</view>
</view>
</view>
<view class="pi-action-btn">
<text class="pi-action-txt">查看</text>
</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'
import { requireMerchantAuth } from '@/utils/merchantAuth.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
}
// 经营指标(头部第二行)
type BizStatsType = {
saleAmount: number | null
orderCount: number | null
visitorCount: number | null
reviewCount: number | null
}
// 订单状态条单项类型
type OrderStatusItemType = {
key: string
label: string
count: number
}
export default {
components: { MerchantTabBar },
data() {
return {
merchantId: '',
authChecking: false,
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,
// 经营指标(头部第二行)
bizStats: {
saleAmount: null,
orderCount: null,
visitorCount: null,
reviewCount: 0
} as BizStatsType,
// 订单状态条(由 buildOrderStatusList() 填充)
orderStatusList: [] as OrderStatusItemType[]
}
},
computed: {
currentDate(): string {
const now = new Date()
return `${now.getMonth() + 1}月${now.getDate()}日`
},
// 待办徽标总数
noticeBadgeCount(): number {
return (Number(this.pendingCounts.pending_shipment) || 0)
+ (Number(this.pendingCounts.refund_requests) || 0)
+ (Number(this.pendingCounts.low_stock) || 0)
+ (this.unreadCount || 0)
}
},
onLoad() {
this.ensureMerchantAuth()
},
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) {}
this.handlePageShow()
},
onHide() {
this.stopRealtimeSubscription()
},
onUnload() {
this.stopRealtimeSubscription()
},
methods: {
async ensureMerchantAuth(): Promise<boolean> {
if (this.authChecking) {
return false
}
this.authChecking = true
try {
const result = await requireMerchantAuth({ redirectOnFail: true, toastOnFail: true })
if (!result.ok) {
this.merchantId = ''
this.stopRealtimeSubscription()
return false
}
const uid = result.userInfo != null && result.userInfo.id != null ? result.userInfo.id : ''
this.merchantId = uid
if (result.merchantInfo != null) {
this.shopInfo.id = result.merchantInfo.id
this.shopInfo.merchant_id = result.merchantInfo.merchant_id
this.shopInfo.shop_name = result.merchantInfo.shop_name !== '' ? result.merchantInfo.shop_name : this.shopInfo.shop_name
this.shopInfo.shop_logo = result.merchantInfo.shop_logo !== '' ? result.merchantInfo.shop_logo : this.shopInfo.shop_logo
this.shopInfo.shop_banner = result.merchantInfo.shop_banner !== '' ? result.merchantInfo.shop_banner : this.shopInfo.shop_banner
this.shopInfo.description = result.merchantInfo.description !== '' ? result.merchantInfo.description : this.shopInfo.description
this.shopInfo.contact_name = result.merchantInfo.contact_name !== '' ? result.merchantInfo.contact_name : this.shopInfo.contact_name
this.shopInfo.contact_phone = result.merchantInfo.contact_phone !== '' ? result.merchantInfo.contact_phone : this.shopInfo.contact_phone
this.shopInfo.status = result.merchantInfo.status
}
return this.merchantId !== ''
} finally {
this.authChecking = false
}
},
async handlePageShow() {
const passed = await this.ensureMerchantAuth()
if (!passed) {
return
}
this.loadAllData()
this.startRealtimeSubscription()
},
/** UUID 格式校验,非 UUID 不得用于 Supabase 过滤(否则 PostgREST 400*/
isValidUUID(id: string): boolean {
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)
},
async initMerchantId() {
try {
const session = supa.getSession()
if (session != null && session.user != null) {
const sid = session.user.getString('id') || ''
if (sid && this.isValidUUID(sid)) this.merchantId = sid
}
if (!this.merchantId) {
const stored = uni.getStorageSync('user_id') || ''
if (stored && this.isValidUUID(stored)) {
this.merchantId = stored
} else if (stored) {
// 测试账号(如 "demo-merchant-001")不是 UUID跳过 API 请求,进入离线演示模式
console.warn('[MerchantIndex] 非 UUID 用户 ID跳过 Supabase 请求:', stored)
this.isPageReady = true
}
}
} 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() {
// merchantId 为空时直接跳过,避免发出 merchant_id=eq. 的无效请求
if (!this.merchantId) { this.isPageReady = true; return }
await this.loadMerchantData()
await this.loadTodayStats()
await this.loadPendingCounts()
await this.loadRecentOrders()
await this.loadUnreadCount()
this.buildOrderStatusList()
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)
},
/**
* 构建订单状态条数据
* 数据口径与服务订单页完全对齐:
* 待接单 -> order_status=2
* 服务中 -> order_status=3
* 待评价 -> 暂占位,后期对接评价状态
* 退款售后 -> order_status=0
*/
buildOrderStatusList() {
this.orderStatusList = [
{ key: 'pending', label: '待接单', count: Number(this.pendingCounts.pending_shipment) || 0 },
{ key: 'shipped', label: '服务中', count: 0 },
{ key: 'review', label: '待评价', count: Number(this.pendingCounts.pending_reviews) || 0 },
{ key: 'refund', label: '退款售后', count: Number(this.pendingCounts.refund_requests) || 0 }
] as OrderStatusItemType[]
// 同步经营指标
this.bizStats = {
saleAmount: this.todayStats.sales,
orderCount: this.todayStats.orders,
visitorCount: this.todayStats.visitors,
reviewCount: Number(this.pendingCounts.pending_reviews) || 0
}
},
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}` })
},
goToHealthManagement() {
uni.navigateTo({ url: '/pages/mall/merchant/health-management' })
},
goToAiConsultation() {
uni.navigateTo({ url: '/pages/mall/merchant/ai-consultation' })
}
}
}
</script>
<style>
/* ===== 容器与滚动 ===== */
.merchant-container { background-color: #f4f5f7; min-height: 100vh; }
.main-scroll { height: 100vh; }
/* ===== 小程序顶部导航(蓝色,医养品牌色) ===== */
.mp-tab-navbar { height: calc(88rpx + var(--status-bar-height)); padding-top: var(--status-bar-height); background-color: #09C39D; display: flex; flex-direction: row; align-items: center; justify-content: center; }
.mp-tab-title { font-size: 34rpx; font-weight: bold; color: #ffffff; }
/* ===== 顶部工作台头部(浅蓝渐变,贴近 mall merchant 风格) ===== */
.wt-header { background: linear-gradient(135deg, #A6F1E4 0%, #69DFC2 100%); padding-bottom: 0; }
.wt-header-top { padding: calc(32rpx + var(--status-bar-height)) 28rpx 24rpx; }
.wt-shop-row { display: flex; flex-direction: row; align-items: center; }
.wt-shop-logo { width: 100rpx; height: 100rpx; border-radius: 16rpx; border-width: 3rpx; border-style: solid; border-color: rgba(255,255,255,0.7); margin-right: 20rpx; background-color: #fff; flex-shrink: 0; }
.wt-shop-info { flex: 1; }
.wt-shop-name { font-size: 36rpx; font-weight: bold; color: #1a1f36; margin-bottom: 8rpx; }
.wt-shop-meta { display: flex; flex-direction: row; align-items: center; }
.wt-meta-txt { font-size: 24rpx; color: #6b7a99; }
.wt-meta-sep { font-size: 24rpx; color: #c0c8d8; margin-left: 12rpx; margin-right: 12rpx; }
.wt-header-btns { display: flex; flex-direction: row; }
.wt-icon-btn { position: relative; margin-left: 20rpx; min-width: 80rpx; height: 80rpx; border-radius: 20rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; padding-left: 16rpx; padding-right: 16rpx; }
.wt-icon-txt { font-size: 32rpx; line-height: 1; color: #3a4a6b; }
.wt-icon-label { font-size: 20rpx; color: #3a4a6b; margin-top: 4rpx; }
.wt-badge { position: absolute; top: -4rpx; right: -4rpx; min-width: 32rpx; height: 32rpx; background-color: #E1251B; border-radius: 16rpx; display: flex; align-items: center; justify-content: center; padding-left: 6rpx; padding-right: 6rpx; border-width: 2rpx; border-style: solid; border-color: #fff; }
.wt-badge-txt { font-size: 18rpx; color: #fff; font-weight: bold; }
/* 头部双行数据面板(订单状态 + 经营指标) */
.wt-data-panel { background-color: #fff; border-radius: 16rpx 16rpx 0 0; padding-top: 8rpx; padding-bottom: 8rpx; }
.wt-panel-row { display: flex; flex-direction: row; justify-content: space-between; padding-top: 16rpx; padding-bottom: 16rpx; }
.wt-panel-sep { height: 1rpx; background-color: #f0f0f0; margin-left: 20rpx; margin-right: 20rpx; }
.wt-panel-item { flex: 1; display: flex; flex-direction: column; align-items: center; }
.wt-panel-num-wrap { position: relative; margin-bottom: 6rpx; }
.wt-panel-num { font-size: 40rpx; font-weight: bold; color: #333; line-height: 1; }
.wt-panel-dot { position: absolute; top: 0; right: -10rpx; width: 14rpx; height: 14rpx; background-color: #E1251B; border-radius: 7rpx; }
.wt-panel-lbl { font-size: 22rpx; color: #999; }
.wt-panel-biz-val { font-size: 30rpx; font-weight: bold; color: #333; margin-bottom: 6rpx; }
.wt-panel-biz-lbl { font-size: 22rpx; color: #999; }
/* ===== 内容区 ===== */
.content-area { padding: 0 0 30rpx; }
/* ===== 通用白卡片贴边无圆角mall merchant 风格) ===== */
.wt-card { background-color: #fff; margin-bottom: 16rpx; padding: 24rpx 28rpx; }
.wt-card-hd { display: flex; flex-direction: row; justify-content: space-between; align-items: center; margin-bottom: 20rpx; }
.wt-card-title { font-size: 30rpx; font-weight: bold; color: #333; }
.wt-card-more { font-size: 26rpx; color: #09C39D; }
/* ===== 功能宫格(无背景色,大图标,老年友好) ===== */
.feature-grid { display: flex; flex-direction: row; flex-wrap: wrap; }
.feature-item { width: 25%; display: flex; flex-direction: column; align-items: center; padding-top: 20rpx; padding-bottom: 20rpx; }
.feat-icon-wrap { width: 88rpx; height: 88rpx; display: flex; align-items: center; justify-content: center; margin-bottom: 12rpx; }
.feat-icon-txt { font-size: 52rpx; line-height: 1; }
.feat-label { font-size: 26rpx; color: #333; text-align: center; }
/* ===== 重要通知 ===== */
.wt-notice-badge-wrap { background-color: #FFF0F0; border-radius: 20rpx; padding-top: 4rpx; padding-bottom: 4rpx; padding-left: 14rpx; padding-right: 14rpx; }
.wt-notice-badge-txt { font-size: 22rpx; color: #E1251B; }
.notice-empty { padding-top: 20rpx; padding-bottom: 20rpx; display: flex; align-items: center; justify-content: center; }
.notice-empty-txt { font-size: 28rpx; color: #bbb; }
.notice-list { display: flex; flex-direction: column; }
.notice-item { display: flex; flex-direction: row; align-items: flex-start; padding-top: 16rpx; padding-bottom: 16rpx; border-bottom-width: 1rpx; border-bottom-style: solid; border-bottom-color: #f5f5f5; }
.notice-dot { width: 14rpx; height: 14rpx; border-radius: 7rpx; margin-top: 8rpx; margin-right: 18rpx; flex-shrink: 0; }
.dot-red { background-color: #E1251B; }
.dot-orange { background-color: #FF7800; }
.dot-gray { background-color: #bbb; }
.notice-body { flex: 1; }
.notice-title-row { display: flex; flex-direction: row; align-items: center; margin-bottom: 6rpx; }
.notice-title { font-size: 30rpx; color: #333; font-weight: 500; flex: 1; }
.notice-tag { font-size: 22rpx; padding-top: 3rpx; padding-bottom: 3rpx; padding-left: 10rpx; padding-right: 10rpx; border-radius: 8rpx; margin-left: 12rpx; flex-shrink: 0; }
.tag-red { background-color: #FFF0F0; color: #E1251B; }
.tag-orange { background-color: #FFF7E6; color: #FF7800; }
.tag-gray { background-color: #F5F5F5; color: #999; }
.notice-desc { font-size: 26rpx; color: #999; line-height: 1.5; }
/* ===== 最新订单(商品运营风格) ===== */
.pi-empty { padding-top: 20rpx; padding-bottom: 20rpx; display: flex; align-items: center; justify-content: center; }
.pi-empty-txt { font-size: 28rpx; color: #bbb; }
.pi-list { display: flex; flex-direction: column; }
.pi-item { display: flex; flex-direction: row; align-items: center; padding-top: 16rpx; padding-bottom: 16rpx; border-bottom-width: 1rpx; border-bottom-style: solid; border-bottom-color: #f5f5f5; }
.pi-item-last { border-bottom-width: 0; }
.pi-img { width: 108rpx; height: 108rpx; border-radius: 12rpx; margin-right: 20rpx; background-color: #f5f5f5; flex-shrink: 0; }
.pi-info { flex: 1; }
.pi-name { font-size: 30rpx; color: #333; font-weight: 500; margin-bottom: 10rpx; overflow: hidden; }
.pi-metrics { display: flex; flex-direction: row; }
.pi-metric { flex: 1; display: flex; flex-direction: column; }
.pi-metric-val { font-size: 26rpx; font-weight: bold; color: #333; margin-bottom: 2rpx; }
.pi-metric-lbl { font-size: 22rpx; color: #999; }
.pi-action-btn { background-color: #E3F7ED; border-radius: 10rpx; padding-top: 10rpx; padding-bottom: 10rpx; padding-left: 16rpx; padding-right: 16rpx; flex-shrink: 0; margin-left: 16rpx; }
.pi-action-txt { font-size: 24rpx; color: #09C39D; }
/* ===== 底部安全区 ===== */
.safe-bottom { height: 160rpx; }
/* ===== 骨架屏 ===== */
@keyframes ske-pulse { 0% { opacity: 1; } 50% { opacity: 0.45; } 100% { opacity: 1; } }
.ske-body { padding: 16rpx 0; }
.ske-card-wrap { background: #fff; padding: 28rpx; margin-bottom: 16rpx; }
.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; }
/* ===== 订单状态样式order-detail 等页面引用保留) ===== */
.status-pending { background-color: #FFF3E0; color: #FF9800; }
.status-paid { background-color: #E3F7ED; color: #09C39D; }
.status-shipped { background-color: #E8F5E9; color: #4CAF50; }
.status-completed { background-color: #F3E5F5; color: #9C27B0; }
</style>