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,925 @@
<!-- 商家端 - 订单详情页面 -->
<template>
<view class="order-detail-page">
<!-- 订单状态头部 -->
<view class="status-header" :class="getStatusBgClass(order.order_status)">
<text class="status-icon">{{ getStatusIcon(order.order_status) }}</text>
<text class="status-text">{{ getStatusText(order.order_status) }}</text>
<text class="status-desc">{{ getStatusDesc(order.order_status) }}</text>
</view>
<!-- 物流信息 -->
<view v-if="order.order_status >= 3" class="section logistics-section">
<view class="section-title">物流信息</view>
<view class="logistics-info">
<view class="logistics-company">
<text class="label">物流公司:</text>
<text class="value">{{ order.shipping_company || '待填写' }}</text>
</view>
<view class="logistics-number">
<text class="label">物流单号:</text>
<text class="value">{{ order.tracking_number || '待填写' }}</text>
<text v-if="order.tracking_number" class="copy-btn" @click="copyTrackingNumber">复制</text>
</view>
</view>
</view>
<!-- 收货地址 -->
<view class="section address-section">
<view class="section-title">收货信息</view>
<view class="address-info">
<view class="address-user">
<text class="name">{{ addressData.recipient_name || '未知' }}</text>
<text class="phone">{{ addressData.phone || '未知' }}</text>
</view>
<view class="address-detail">
{{ addressData.province || '' }}{{ addressData.city || '' }}{{ addressData.district || '' }}{{ addressData.detail_address || '' }}
</view>
</view>
</view>
<!-- 订单信息 -->
<view class="section order-info-section">
<view class="section-title">订单信息</view>
<view class="info-list">
<view class="info-item">
<text class="label">订单编号:</text>
<text class="value">{{ order.order_no }}</text>
<text class="copy-btn" @click="copyOrderNo">复制</text>
</view>
<view class="info-item">
<text class="label">下单时间:</text>
<text class="value">{{ formatTime(order.created_at) }}</text>
</view>
<view v-if="order.paid_at" class="info-item">
<text class="label">付款时间:</text>
<text class="value">{{ formatTime(order.paid_at) }}</text>
</view>
<view v-if="order.shipped_at" class="info-item">
<text class="label">发货时间:</text>
<text class="value">{{ formatTime(order.shipped_at) }}</text>
</view>
<view v-if="order.remark" class="info-item">
<text class="label">订单备注:</text>
<text class="value">{{ order.remark }}</text>
</view>
</view>
</view>
<!-- 商品列表 -->
<view class="section products-section">
<view class="section-title">商品信息</view>
<view class="products-list">
<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>
<!-- 费用明细 -->
<view class="section fees-section">
<view class="section-title">费用明细</view>
<view class="fees-list">
<view class="fee-item">
<text class="label">商品金额:</text>
<text class="value">¥{{ order.product_amount }}</text>
</view>
<view class="fee-item">
<text class="label">运费:</text>
<text class="value">¥{{ order.shipping_fee }}</text>
</view>
<view v-if="order.discount_amount > 0" class="fee-item">
<text class="label">优惠:</text>
<text class="value discount">-¥{{ order.discount_amount }}</text>
</view>
<view class="fee-item total">
<text class="label">实付金额:</text>
<text class="value">¥{{ order.total_amount }}</text>
</view>
</view>
</view>
<!-- 操作按钮 -->
<view class="action-buttons">
<view
v-if="order.order_status === 1"
class="action-btn primary"
@click="shipOrder"
>
去发货
</view>
<view
v-if="order.order_status === 2"
class="action-btn primary"
@click="viewLogistics"
>
查看物流
</view>
<view
v-if="order.order_status === 3"
class="action-btn primary"
@click="confirmDelivery"
>
确认收货
</view>
<view
v-if="order.order_status === -1 || order.order_status === 5"
class="action-btn danger"
@click="deleteOrder"
>
删除订单
</view>
<view class="action-btn default" @click="contactBuyer">
联系买家
</view>
</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 AddressType = {
recipient_name: string
phone: string
province: string
city: string
district: string
detail_address: string
}
type LogisticsType = {
name: string
code: string
}
export default {
data() {
return {
orderId: '',
order: {
id: '',
order_no: '',
user_id: '',
merchant_id: '',
order_status: 1,
total_amount: 0,
product_amount: 0,
shipping_fee: 0,
discount_amount: 0,
paid_amount: 0,
shipping_address: '',
remark: '',
shipping_company: '',
tracking_number: '',
paid_at: '',
shipped_at: '',
created_at: '',
updated_at: '',
items: [] as OrderItemType[]
},
addressData: {} as AddressType,
showShipModal: false,
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: {} as LogisticsType,
trackingNumber: ''
}
},
onLoad(options: any) {
const id = options.id as string
if (id) {
this.orderId = id
this.loadOrderDetail()
}
},
methods: {
async loadOrderDetail() {
try {
const response = await 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('id', this.orderId)
.single()
.execute()
if (response.error != null) {
console.error('获取订单详情失败:', response.error)
uni.showToast({ title: '加载失败', icon: 'none' })
return
}
const rawData = response.data as UTSJSONObject
if (rawData == null) return
this.order = {
id: rawData.getString('id') || '',
order_no: rawData.getString('order_no') || '',
user_id: rawData.getString('user_id') || '',
merchant_id: rawData.getString('merchant_id') || '',
order_status: rawData.getNumber('order_status') || 1,
total_amount: rawData.getNumber('total_amount') || 0,
product_amount: rawData.getNumber('product_amount') || 0,
shipping_fee: rawData.getNumber('shipping_fee') || 0,
discount_amount: rawData.getNumber('discount_amount') || 0,
paid_amount: rawData.getNumber('paid_amount') || 0,
shipping_address: rawData.getString('shipping_address') || '{}',
remark: rawData.getString('remark') || '',
shipping_company: rawData.getString('shipping_company') || '',
tracking_number: rawData.getString('tracking_number') || '',
paid_at: rawData.getString('paid_at') || '',
shipped_at: rawData.getString('shipped_at') || '',
created_at: rawData.getString('created_at') || '',
updated_at: rawData.getString('updated_at') || '',
items: []
}
const itemsObj = rawData.get('order_items')
if (itemsObj != null && Array.isArray(itemsObj)) {
const itemsArray = itemsObj as any[]
for (let i = 0; i < itemsArray.length; i++) {
const orderItem = itemsArray[i] as UTSJSONObject
this.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)
}
}
this.parseAddress()
} catch (e) {
console.error('获取订单详情异常:', e)
}
},
parseAddress() {
try {
const addrStr = this.order.shipping_address
if (addrStr && addrStr !== '{}') {
const addrObj = JSON.parse(addrStr) as AddressType
this.addressData = addrObj
}
} catch (e) {
console.error('解析地址失败:', e)
}
},
getStatusIcon(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 '📋'
},
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 '未知'
},
getStatusDesc(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 ''
},
getStatusBgClass(status: number): string {
if (status === 1) return 'status-bg-1'
if (status === 2) return 'status-bg-2'
if (status === 3) return 'status-bg-3'
if (status === 4) return 'status-bg-4'
if (status === 0 || status === 5 || status === -1) return 'status-bg-0'
return 'status-bg-1'
},
formatTime(timeStr: string): string {
if (!timeStr) return '-'
const date = new Date(timeStr)
const year = date.getFullYear()
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 `${year}-${month}-${day} ${hour}:${minute}`
},
copyOrderNo() {
uni.setClipboardData({
data: this.order.order_no,
success: () => {
uni.showToast({ title: '复制成功', icon: 'success' })
}
})
},
copyTrackingNumber() {
uni.setClipboardData({
data: this.order.tracking_number,
success: () => {
uni.showToast({ title: '复制成功', icon: 'success' })
}
})
},
shipOrder() {
this.showShipModal = true
},
closeShipModal() {
this.showShipModal = false
this.selectedLogistics = {} as LogisticsType
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.order.id)
.execute()
if (response.error != null) {
uni.showToast({ title: '发货失败', icon: 'none' })
return
}
uni.showToast({ title: '发货成功', icon: 'success' })
this.closeShipModal()
this.loadOrderDetail()
} catch (e) {
uni.showToast({ title: '发货失败', icon: 'none' })
}
},
viewLogistics() {
uni.navigateTo({
url: `/pages/mall/merchant/logistics?orderId=${this.order.id}`
})
},
async confirmDelivery() {
uni.showModal({
title: '确认收货',
content: '确认买家已收到货物吗?',
success: async (res) => {
if (res.confirm) {
try {
const response = await supa
.from('ml_orders')
.update({
order_status: 4,
delivered_at: new Date().toISOString(),
completed_at: new Date().toISOString(),
updated_at: new Date().toISOString()
})
.eq('id', this.order.id)
.execute()
if (response.error != null) {
uni.showToast({ title: '操作失败', icon: 'none' })
return
}
uni.showToast({ title: '操作成功', icon: 'success' })
this.loadOrderDetail()
} catch (e) {
uni.showToast({ title: '操作失败', icon: 'none' })
}
}
}
})
},
async deleteOrder() {
uni.showModal({
title: '确认删除',
content: '确定要删除该订单吗?',
success: async (res) => {
if (res.confirm) {
try {
const response = await supa
.from('ml_orders')
.delete()
.eq('id', this.order.id)
.execute()
if (response.error != null) {
uni.showToast({ title: '删除失败', icon: 'none' })
return
}
uni.showToast({ title: '删除成功', icon: 'success' })
setTimeout(() => {
uni.navigateBack()
}, 1500)
} catch (e) {
uni.showToast({ title: '删除失败', icon: 'none' })
}
}
}
})
},
contactBuyer() {
uni.navigateTo({
url: `/pages/mall/merchant/chat?userId=${this.order.user_id}`
})
}
}
}
</script>
<style>
.order-detail-page {
background-color: #f5f5f5;
min-height: 100vh;
padding-bottom: 180rpx;
}
.status-header {
padding: 50rpx 30rpx;
text-align: center;
}
.status-bg-1 {
background: linear-gradient(135deg, #FF9800 0%, #FF5722 100%);
}
.status-bg-2 {
background: linear-gradient(135deg, #2196F3 0%, #1976D2 100%);
}
.status-bg-3 {
background: linear-gradient(135deg, #4CAF50 0%, #388E3C 100%);
}
.status-bg-4 {
background: linear-gradient(135deg, #9C27B0 0%, #7B1FA2 100%);
}
.status-bg-0, .status-bg-5 {
background: linear-gradient(135deg, #607D8B 0%, #455A64 100%);
}
.status-icon {
font-size: 60rpx;
display: block;
margin-bottom: 20rpx;
}
.status-text {
font-size: 36rpx;
font-weight: bold;
color: #fff;
display: block;
margin-bottom: 10rpx;
}
.status-desc {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.8);
}
.section {
background-color: #fff;
margin-bottom: 20rpx;
padding: 30rpx;
}
.section-title {
font-size: 30rpx;
font-weight: bold;
color: #333;
margin-bottom: 24rpx;
}
.logistics-info {
font-size: 28rpx;
}
.logistics-company, .logistics-number {
display: flex;
align-items: center;
margin-bottom: 16rpx;
}
.logistics-number .label {
margin-right: 16rpx;
}
.logistics-number .value {
flex: 1;
}
.label {
color: #999;
margin-right: 16rpx;
}
.value {
color: #333;
}
.address-info {
font-size: 28rpx;
}
.address-user {
display: flex;
align-items: center;
margin-bottom: 16rpx;
}
.address-user .name {
font-weight: bold;
margin-right: 20rpx;
}
.address-user .phone {
color: #666;
}
.address-detail {
color: #666;
line-height: 1.5;
}
.info-list {
font-size: 28rpx;
}
.info-item {
display: flex;
align-items: center;
margin-bottom: 16rpx;
}
.info-item .label {
min-width: 140rpx;
}
.info-item .value {
flex: 1;
}
.copy-btn {
color: #007AFF;
font-size: 24rpx;
margin-left: 16rpx;
}
.products-list {
display: flex;
flex-direction: column;
}
.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: 140rpx;
height: 140rpx;
border-radius: 8rpx;
margin-right: 20rpx;
background-color: #f5f5f5;
}
.product-info {
flex: 1;
display: flex;
flex-direction: column;
}
.product-name {
font-size: 28rpx;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.product-spec {
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
}
.product-right {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.product-price {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.product-quantity {
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
}
.fees-list {
font-size: 28rpx;
}
.fee-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
.fee-item .value {
color: #333;
}
.fee-item .value.discount {
color: #FF5722;
}
.fee-item.total {
margin-top: 20rpx;
padding-top: 20rpx;
border-top: 1rpx solid #f5f5f5;
}
.fee-item.total .label {
font-weight: bold;
color: #333;
}
.fee-item.total .value {
font-size: 32rpx;
font-weight: bold;
color: #FF3B30;
}
.action-buttons {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
gap: 20rpx;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background-color: #fff;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.action-btn {
flex: 1;
height: 80rpx;
line-height: 80rpx;
text-align: center;
font-size: 28rpx;
border-radius: 40rpx;
}
.action-btn.primary {
background-color: #007AFF;
color: #fff;
}
.action-btn.default {
background-color: #f5f5f5;
color: #333;
}
.action-btn.danger {
background-color: #FF3B30;
color: #fff;
}
.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>