consumer模块完成度95%,能编译在安卓端运行,在解决数据获取和页面布局问题

This commit is contained in:
cyh666666
2026-02-27 08:20:43 +08:00
parent e606c597ca
commit b9acce6c35
1554 changed files with 23471 additions and 8551 deletions

View File

@@ -0,0 +1,996 @@
<!-- 商家端 - 订单管理页面 -->
<template>
<view class="orders-page">
<!-- 标签页切换 -->
<view class="tabs-container">
<view class="tabs-scroll">
<view
v-for="(tab, index) in tabs"
:key="index"
class="tab-item"
:class="{ active: currentTab === tab.status }"
@click="switchTab(tab.status)"
>
<text class="tab-text">{{ tab.name }}</text>
<view v-if="tab.count > 0" class="tab-badge">{{ tab.count }}</view>
</view>
</view>
</view>
<!-- 搜索栏 -->
<view class="search-bar">
<input
class="search-input"
type="text"
v-model="searchKeyword"
placeholder="搜索订单号/商品名称"
@confirm="handleSearch"
/>
<view class="search-btn" @click="handleSearch">搜索</view>
</view>
<!-- 订单列表 -->
<scroll-view
class="orders-list"
scroll-y
:refresher-enabled="true"
:refresher-triggered="refreshing"
@refresherrefresh="onRefresh"
@scrolltolower="loadMore"
>
<view v-if="loading && orders.length === 0" class="loading-container">
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="orders.length === 0" class="empty-container">
<text class="empty-icon">📋</text>
<text class="empty-text">暂无订单</text>
</view>
<view v-else>
<view
v-for="order in orders"
:key="order.id"
class="order-card"
@click="viewOrderDetail(order.id)"
>
<!-- 订单头部 -->
<view class="order-header">
<view class="order-info-left">
<text class="order-no">订单号: {{ order.order_no }}</text>
<text class="order-time">{{ formatTime(order.created_at) }}</text>
</view>
<text class="order-status" :class="'status-' + order.order_status">
{{ getStatusText(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="aspectFill"
/>
<view class="product-info">
<text class="product-name">{{ item.product_name }}</text>
<text class="product-spec">{{ item.sku_name || '标准规格' }}</text>
</view>
<view class="product-right">
<text class="product-price">¥{{ item.price }}</text>
<text class="product-quantity">x{{ item.quantity }}</text>
</view>
</view>
</view>
<!-- 订单底部 -->
<view class="order-footer">
<view class="order-amount">
<text class="amount-label">共{{ getTotalQuantity(order.items) }}件商品</text>
<text class="amount-value">合计: ¥{{ order.total_amount }}</text>
</view>
<view class="order-actions">
<view
v-if="order.order_status === 1"
class="action-btn primary"
@click.stop="shipOrder(order)"
>
发货
</view>
<view
v-if="order.order_status === 2"
class="action-btn info"
@click.stop="viewLogistics(order)"
>
查看物流
</view>
<view
v-if="order.order_status === -1 || order.order_status === 5"
class="action-btn default"
@click.stop="deleteOrder(order)"
>
删除
</view>
</view>
</view>
</view>
</view>
<view v-if="loadingMore" class="load-more">
<text class="load-more-text">加载中...</text>
</view>
<view v-if="!hasMore && orders.length > 0" class="no-more">
<text class="no-more-text">没有更多了</text>
</view>
</scroll-view>
<!-- 发货弹窗 -->
<view v-if="showShipModal" class="modal-mask" @click="closeShipModal">
<view class="modal-content" @click.stop>
<view class="modal-header">
<text class="modal-title">发货</text>
<text class="modal-close" @click="closeShipModal">×</text>
</view>
<view class="modal-body">
<view class="form-item">
<text class="form-label">物流公司</text>
<picker
class="form-picker"
:range="logisticsCompanies"
range-key="name"
@change="onLogisticsChange"
>
<view class="picker-value">
{{ selectedLogistics.name || '请选择物流公司' }}
</view>
</picker>
</view>
<view class="form-item">
<text class="form-label">物流单号</text>
<input
class="form-input"
v-model="trackingNumber"
placeholder="请输入物流单号"
/>
</view>
</view>
<view class="modal-footer">
<view class="modal-btn cancel" @click="closeShipModal">取消</view>
<view class="modal-btn confirm" @click="confirmShip">确认发货</view>
</view>
</view>
</view>
</view>
</template>
<script lang="uts">
import supa from '@/components/supadb/aksupainstance.uts'
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
user_id: string
merchant_id: string
order_status: number
total_amount: number
product_amount: number
shipping_fee: number
paid_amount: number
shipping_address: string
remark: string
created_at: string
updated_at: string
items: OrderItemType[]
}
type TabType = {
name: string
status: number
count: number
}
type LogisticsType = {
name: string | null
code: string | null
}
export default {
data() {
return {
tabs: [
{ name: '全部', status: -2, count: 0 },
{ name: '待付款', status: 1, count: 0 },
{ name: '待发货', status: 2, count: 0 },
{ name: '待收货', status: 3, count: 0 },
{ name: '已完成', status: 4, count: 0 },
{ name: '退款', status: 0, count: 0 }
] as TabType[],
currentTab: -2,
searchKeyword: '',
orders: [] as OrderType[],
loading: false,
loadingMore: false,
refreshing: false,
page: 1,
limit: 20,
hasMore: true,
merchantId: '',
showShipModal: false,
currentOrder: null as OrderType | null,
logisticsCompanies: [
{ name: '顺丰速运', code: 'SF' },
{ name: '圆通速递', code: 'YTO' },
{ name: '中通快递', code: 'ZTO' },
{ name: '韵达快递', code: 'YD' },
{ name: '申通快递', code: 'STO' },
{ name: 'EMS', code: 'EMS' },
{ name: '京东物流', code: 'JD' }
] as LogisticsType[],
selectedLogistics: null as LogisticsType | null,
trackingNumber: ''
}
},
onLoad(options: any) {
const type = options.type as string
if (type && type !== 'all') {
const statusMap: Record<string, number> = {
'pending': 1,
'shipped': 3,
'refund': 0,
'completed': 4
}
this.currentTab = statusMap[type] ?? -2
}
this.initMerchantId()
},
onShow() {
if (this.merchantId) {
this.loadOrders()
this.loadOrderCounts()
} else {
setTimeout(() => {
this.loadOrders()
this.loadOrderCounts()
}, 500)
}
},
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)
}
},
async loadOrders() {
if (this.loading) return
this.loading = true
try {
let query = supa
.from('ml_orders')
.select(`
*,
order_items!inner (
id,
order_id,
product_id,
sku_id,
product_name,
sku_name,
price,
quantity,
image_url,
sku_snapshot
)
`)
.eq('merchant_id', this.merchantId)
.order('created_at', { ascending: false })
.page(this.page)
.limit(this.limit)
if (this.currentTab !== -2) {
if (this.currentTab === 0) {
query = query.eq('order_status', 0)
} else {
query = query.eq('order_status', this.currentTab)
}
}
if (this.searchKeyword) {
query = query.like('order_no', `%${this.searchKeyword}%`)
}
const response = await query.execute()
if (response.error != null) {
console.error('获取订单失败:', response.error)
uni.showToast({ title: '加载失败', icon: 'none' })
return
}
const rawData = response.data as any[]
if (rawData == null || rawData.length === 0) {
this.orders = []
this.hasMore = false
return
}
const ordersData: OrderType[] = []
for (let i = 0; i < rawData.length; i++) {
const item = rawData[i]
const orderObj = item as UTSJSONObject
const order: OrderType = {
id: orderObj.getString('id') || '',
order_no: orderObj.getString('order_no') || '',
user_id: orderObj.getString('user_id') || '',
merchant_id: orderObj.getString('merchant_id') || '',
order_status: orderObj.getNumber('order_status') || 1,
total_amount: orderObj.getNumber('total_amount') || 0,
product_amount: orderObj.getNumber('product_amount') || 0,
shipping_fee: orderObj.getNumber('shipping_fee') || 0,
paid_amount: orderObj.getNumber('paid_amount') || 0,
shipping_address: orderObj.getString('shipping_address') || '',
remark: orderObj.getString('remark') || '',
created_at: orderObj.getString('created_at') || '',
updated_at: orderObj.getString('updated_at') || '',
items: []
}
const itemsObj = orderObj.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: orderItem.getString('order_id') || '',
product_id: orderItem.getString('product_id') || '',
sku_id: orderItem.getString('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)
}
if (this.page === 1) {
this.orders = ordersData
} else {
this.orders = [...this.orders, ...ordersData]
}
this.hasMore = rawData.length >= this.limit
} catch (e) {
console.error('获取订单异常:', e)
} finally {
this.loading = false
this.refreshing = false
}
},
async loadOrderCounts() {
try {
const response = await supa
.from('ml_orders')
.select('order_status', { count: 'exact' })
.eq('merchant_id', this.merchantId)
.execute()
if (response.error != null || response.total == null) return
const counts = {
1: 0, 2: 0, 3: 0, 4: 0, 0: 0
}
let total = 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') || 1
if (status === 1) counts[1]++
else if (status === 2) counts[2]++
else if (status === 3) counts[3]++
else if (status === 4) counts[4]++
else if (status === 0) counts[0]++
total++
}
}
this.tabs[0].count = total
this.tabs[1].count = counts[1] || 0
this.tabs[2].count = counts[2] || 0
this.tabs[3].count = counts[3] || 0
this.tabs[4].count = counts[4] || 0
this.tabs[5].count = counts[0] || 0
} catch (e) {
console.error('获取订单数量异常:', e)
}
},
switchTab(status: number) {
this.currentTab = status
this.page = 1
this.hasMore = true
this.loadOrders()
},
handleSearch() {
this.page = 1
this.hasMore = true
this.loadOrders()
},
onRefresh() {
this.refreshing = true
this.page = 1
this.loadOrders()
this.loadOrderCounts()
},
loadMore() {
if (!this.loadingMore && this.hasMore) {
this.loadingMore = true
this.page++
this.loadOrders().then(() => {
this.loadingMore = false
})
}
},
viewOrderDetail(orderId: string) {
uni.navigateTo({
url: `/pages/mall/merchant/order-detail?id=${orderId}`
})
},
shipOrder(order: OrderType) {
this.currentOrder = order
this.showShipModal = true
},
closeShipModal() {
this.showShipModal = false
this.currentOrder = null
this.selectedLogistics = null
this.trackingNumber = ''
},
onLogisticsChange(e: any) {
const index = e.detail.value as number
this.selectedLogistics = this.logisticsCompanies[index]
},
async confirmShip() {
if (!this.selectedLogistics.name) {
uni.showToast({ title: '请选择物流公司', icon: 'none' })
return
}
if (!this.trackingNumber) {
uni.showToast({ title: '请输入物流单号', icon: 'none' })
return
}
try {
const response = await supa
.from('ml_orders')
.update({
order_status: 3,
shipping_company: this.selectedLogistics.name,
tracking_number: this.trackingNumber,
shipped_at: new Date().toISOString(),
updated_at: new Date().toISOString()
})
.eq('id', this.currentOrder!.id)
.execute()
if (response.error != null) {
uni.showToast({ title: '发货失败', icon: 'none' })
return
}
uni.showToast({ title: '发货成功', icon: 'success' })
this.closeShipModal()
this.loadOrders()
this.loadOrderCounts()
} catch (e) {
uni.showToast({ title: '发货失败', icon: 'none' })
}
},
viewLogistics(order: OrderType) {
uni.navigateTo({
url: `/pages/mall/merchant/logistics?orderId=${order.id}`
})
},
async deleteOrder(order: OrderType) {
uni.showModal({
title: '确认删除',
content: '确定要删除该订单吗?',
success: async (res) => {
if (res.confirm) {
try {
const response = await supa
.from('ml_orders')
.delete()
.eq('id', order.id)
.execute()
if (response.error != null) {
uni.showToast({ title: '删除失败', icon: 'none' })
return
}
uni.showToast({ title: '删除成功', icon: 'success' })
this.loadOrders()
this.loadOrderCounts()
} catch (e) {
uni.showToast({ title: '删除失败', icon: 'none' })
}
}
}
})
},
getStatusText(status: number): string {
if (status === 1) return '待付款'
if (status === 2) return '待发货'
if (status === 3) return '待收货'
if (status === 4) return '已完成'
if (status === 0) return '退款中'
if (status === 5 || status === -1) return '已取消'
return '未知'
},
getTotalQuantity(items: OrderItemType[]): number {
let total = 0
for (let i = 0; i < items.length; i++) {
total += items[i].quantity
}
return total
},
formatTime(timeStr: string): string {
if (!timeStr) return ''
const date = new Date(timeStr)
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
const hour = date.getHours().toString().padStart(2, '0')
const minute = date.getMinutes().toString().padStart(2, '0')
return `${month}-${day} ${hour}:${minute}`
}
}
}
</script>
<style>
.orders-page {
background-color: #f5f5f5;
min-height: 100vh;
}
.tabs-container {
background-color: #fff;
position: sticky;
top: 0;
z-index: 100;
}
.tabs-scroll {
display: flex;
overflow-x: auto;
white-space: nowrap;
padding: 0 20rpx;
}
.tab-item {
position: relative;
padding: 24rpx 30rpx;
text-align: center;
flex-shrink: 0;
}
.tab-text {
font-size: 28rpx;
color: #666;
}
.tab-item.active .tab-text {
color: #007AFF;
font-weight: bold;
}
.tab-item.active::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 40rpx;
height: 4rpx;
background-color: #007AFF;
border-radius: 2rpx;
}
.tab-badge {
position: absolute;
top: 10rpx;
right: 10rpx;
min-width: 32rpx;
height: 32rpx;
padding: 0 8rpx;
background-color: #FF3B30;
color: #fff;
font-size: 20rpx;
border-radius: 16rpx;
text-align: center;
line-height: 32rpx;
}
.search-bar {
display: flex;
align-items: center;
padding: 20rpx 30rpx;
background-color: #fff;
margin-bottom: 20rpx;
}
.search-input {
flex: 1;
height: 64rpx;
background-color: #f5f5f5;
border-radius: 32rpx;
padding: 0 30rpx;
font-size: 26rpx;
}
.search-btn {
margin-left: 20rpx;
padding: 16rpx 30rpx;
background-color: #007AFF;
color: #fff;
font-size: 26rpx;
border-radius: 32rpx;
}
.orders-list {
padding: 0 20rpx;
height: calc(100vh - 300rpx);
}
.loading-container, .empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.empty-icon {
font-size: 100rpx;
margin-bottom: 20rpx;
}
.empty-text, .loading-text {
font-size: 28rpx;
color: #999;
}
.order-card {
background-color: #fff;
border-radius: 16rpx;
margin-bottom: 20rpx;
overflow: hidden;
}
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 24rpx;
border-bottom: 1rpx solid #f5f5f5;
}
.order-info-left {
display: flex;
flex-direction: column;
}
.order-no {
font-size: 26rpx;
color: #333;
font-weight: 500;
}
.order-time {
font-size: 22rpx;
color: #999;
margin-top: 8rpx;
}
.order-status {
font-size: 24rpx;
padding: 8rpx 16rpx;
border-radius: 20rpx;
}
.status-1 {
background-color: #FFF3E0;
color: #F57C00;
}
.status-2 {
background-color: #E3F2FD;
color: #1976D2;
}
.status-3 {
background-color: #E8F5E9;
color: #388E3C;
}
.status-4, .status-5 {
background-color: #F5F5F5;
color: #999;
}
.status-0, .status--1 {
background-color: #FFEBEE;
color: #F44336;
}
.order-products {
padding: 0 24rpx;
}
.product-item {
display: flex;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f5f5f5;
}
.product-item:last-child {
border-bottom: none;
}
.product-image {
width: 120rpx;
height: 120rpx;
border-radius: 8rpx;
margin-right: 20rpx;
background-color: #f5f5f5;
}
.product-info {
flex: 1;
display: flex;
flex-direction: column;
}
.product-name {
font-size: 26rpx;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.product-spec {
font-size: 22rpx;
color: #999;
margin-top: 8rpx;
}
.product-right {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.product-price {
font-size: 26rpx;
color: #333;
font-weight: 500;
}
.product-quantity {
font-size: 22rpx;
color: #999;
margin-top: 8rpx;
}
.order-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 24rpx;
border-top: 1rpx solid #f5f5f5;
}
.order-amount {
display: flex;
align-items: baseline;
}
.amount-label {
font-size: 22rpx;
color: #999;
}
.amount-value {
font-size: 28rpx;
color: #FF3B30;
font-weight: bold;
margin-left: 10rpx;
}
.order-actions {
display: flex;
gap: 16rpx;
}
.action-btn {
padding: 12rpx 24rpx;
font-size: 24rpx;
border-radius: 28rpx;
}
.action-btn.primary {
background-color: #007AFF;
color: #fff;
}
.action-btn.info {
background-color: #E3F2FD;
color: #1976D2;
}
.action-btn.default {
background-color: #F5F5F5;
color: #666;
}
.load-more, .no-more {
padding: 30rpx 0;
text-align: center;
}
.load-more-text, .no-more-text {
font-size: 24rpx;
color: #999;
}
.modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: flex-end;
justify-content: center;
z-index: 1000;
}
.modal-content {
width: 100%;
background-color: #fff;
border-radius: 24rpx 24rpx 0 0;
padding-bottom: env(safe-area-inset-bottom);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #f5f5f5;
}
.modal-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.modal-close {
font-size: 44rpx;
color: #999;
line-height: 1;
}
.modal-body {
padding: 30rpx;
}
.form-item {
margin-bottom: 30rpx;
}
.form-label {
font-size: 28rpx;
color: #333;
display: block;
margin-bottom: 16rpx;
}
.form-picker, .form-input {
height: 72rpx;
border: 1rpx solid #e5e5e5;
border-radius: 8rpx;
padding: 0 20rpx;
font-size: 28rpx;
}
.picker-value {
height: 72rpx;
line-height: 72rpx;
color: #333;
}
.modal-footer {
display: flex;
border-top: 1rpx solid #f5f5f5;
}
.modal-btn {
flex: 1;
height: 88rpx;
line-height: 88rpx;
text-align: center;
font-size: 28rpx;
}
.modal-btn.cancel {
color: #666;
border-right: 1rpx solid #f5f5f5;
}
.modal-btn.confirm {
color: #007AFF;
font-weight: bold;
}
</style>