Files
medical-mall/pages/mall/delivery/test/merchant-order-list.uvue
2026-02-09 16:53:20 +08:00

403 lines
9.6 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="container">
<view class="header">
<text class="back-link" @click="goBack">⬅ 返回</text>
<text class="title">商家发货管理</text>
</view>
<!-- 筛选栏 -->
<view class="filter-bar">
<view class="filter-item" :class="{active: currentFilter === 'all'}" @click="setFilter('all')">全部</view>
<view class="filter-item" :class="{active: currentFilter === 'pending'}" @click="setFilter('pending')">待发货</view>
<view class="filter-item" :class="{active: currentFilter === 'shipped'}" @click="setFilter('shipped')">已发货</view>
</view>
<scroll-view class="order-list" scroll-y="true">
<view v-for="(order, index) in filteredOrders" :key="order.order_no" class="order-card">
<view class="order-header">
<text class="order-no">订单号: {{ order.order_no }}</text>
<text class="order-status">{{ getStatusText(order.status) }}</text>
</view>
<view class="order-body">
<view class="info-row">
<text class="label">下单时间:</text>
<text class="value">{{ order.created_at }}</text>
</view>
<view class="info-row">
<text class="label">收件人:</text>
<text class="value">{{ order.receiver_name }} {{ order.receiver_masked_phone }}</text>
</view>
<view class="info-row">
<text class="label">运单号:</text>
<text class="value">{{ order.tracking_no ? order.tracking_no + ' (' + order.carrier + ')' : '暂无运单号' }}</text>
</view>
</view>
<view class="order-footer">
<button v-if="order.status === 'PENDING' || order.status === 'ORDER_PLACED'" class="btn-primary" @click="openShipModal(order)">去发货</button>
<button class="btn-secondary" @click="viewDetail(order)">详情</button>
</view>
</view>
<view v-if="filteredOrders.length === 0" class="empty-state">
<text>暂无订单</text>
</view>
</scroll-view>
<!-- 发货弹窗 -->
<view v-if="showShipModal" class="modal-mask">
<view class="modal-content">
<view class="modal-header">
<view class="title-box">
<text class="modal-title">订单发货</text>
<text v-if="selectedOrder != null" class="modal-order-no">单号: {{ selectedOrder!.order_no }}</text>
</view>
<text class="close-btn" @click="showShipModal = false">×</text>
</view>
<view class="modal-body">
<view class="form-item">
<text class="label">选择承运商</text>
<picker :range="carriers" range-key="label" @change="onCarrierChange">
<view class="picker-val">{{ currentCarrier || '请选择' }}</view>
</picker>
</view>
<view class="form-item">
<text class="label">运单号</text>
<view class="input-wrapper">
<input v-model="trackingNo" placeholder="请输入或扫码录入" class="input" />
<view class="scan-btn" @click="scanCode">
<text class="scan-icon">📷</text>
</view>
</view>
</view>
</view>
<view class="modal-footer">
<button class="btn-cancel" @click="showShipModal = false">取消</button>
<button class="btn-confirm" @click="confirmShip">确认发货</button>
</view>
</view>
</view>
</view>
</template>
<script uts>
import supa from '@/components/supadb/aksupainstance.uts'
import { mockService, MockOrder, LogisticsConstants } from './mock-service.uts'
export default {
data() {
return {
currentFilter: 'all',
orders: [] as MockOrder[],
showShipModal: false,
selectedOrder: null as MockOrder | null,
carriers: LogisticsConstants.CARRIERS,
currentCarrier: '顺丰速运',
trackingNo: ''
}
},
onShow() {
this.loadData()
},
computed: {
filteredOrders() : MockOrder[] {
const list = this.orders
if (this.currentFilter === 'all') return list
// 待发货:包含已下单和待发货状态
if (this.currentFilter === 'pending') {
return list.filter((o : MockOrder) : boolean =>
o.status === 'PENDING' || o.status === 'ORDER_PLACED'
)
}
// 已发货:只要已经有了运单号或状态已转为发货后的
if (this.currentFilter === 'shipped') {
const shippedStates = ['SHIPPED', 'IN_TRANSIT', 'OUT_FOR_DELIVERY', 'READY_FOR_PICKUP', 'DELIVERED', 'EXCEPTION', 'RETURNED']
return list.filter((o : MockOrder) : boolean => shippedStates.includes(o.status))
}
return []
}
},
methods: {
goBack() {
uni.navigateBack()
},
async loadData() {
const data = await mockService.getMockOrders()
this.orders = [...data]
},
setFilter(filter: string) {
this.currentFilter = filter
},
getStatusText(status: string) : string {
return mockService.getStatusText(status)
},
openShipModal(order: MockOrder) {
this.selectedOrder = order
this.currentCarrier = '顺丰速运'
this.trackingNo = ''
this.showShipModal = true
},
onCarrierChange(e: any) {
const index = e.detail.value as number
this.currentCarrier = this.carriers[index]['value'] as string
},
async confirmShip() {
if (!this.trackingNo) {
uni.showToast({ title: '请输入运单号', icon: 'none' })
return
}
uni.showLoading({ title: '提交中...' })
const orderNo = this.selectedOrder?.order_no
if (orderNo != null) {
// 1. 同步修改 数据库 中的数据
const success = await mockService.bindShipment(orderNo, this.currentCarrier, this.trackingNo)
if (success) {
// 2. 直接修改本地选中的订单对象状态,确保 Vue 响应式立即触发
if (this.selectedOrder != null) {
const target = this.selectedOrder!
target.status = 'SHIPPED'
target.carrier = this.currentCarrier
target.tracking_no = this.trackingNo
}
// 3. 重新加载数据(从数据库刷新)
await this.loadData()
uni.hideLoading()
this.showShipModal = false
uni.showToast({ title: '发货成功' })
} else {
uni.hideLoading()
uni.showToast({ title: '发货失败,请重试', icon: 'none' })
}
}
},
viewDetail(order: MockOrder) {
uni.navigateTo({
url: `/pages/mall/delivery/test/merchant-order-detail?order_no=${order.order_no}`
})
},
scanCode() {
uni.scanCode({
success: (res) => {
this.trackingNo = res.result
},
fail: (_) => {
uni.showToast({
title: '该功能尚未完成',
icon: 'none'
})
}
})
}
}
}
</script>
<style lang="scss">
.container {
padding: 20px;
background-color: #f5f5f5;
height: 100vh;
display: flex;
flex-direction: column;
}
.order-list {
flex: 1;
overflow: hidden;
}
.header {
padding: 30rpx 0;
display: flex;
flex-direction: row;
align-items: center;
gap: 20rpx;
}
.back-link {
font-size: 26rpx;
color: #007AFF;
cursor: pointer;
}
.title {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.filter-bar {
display: flex;
background-color: #fff;
border-radius: 12rpx;
margin-bottom: 20rpx;
}
.filter-item {
flex: 1;
text-align: center;
padding: 20rpx 0;
font-size: 28rpx;
color: #666;
}
.filter-item.active {
color: #007AFF;
font-weight: bold;
border-bottom: 4rpx solid #007AFF;
}
.order-card {
background-color: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
}
.order-header {
display: flex;
justify-content: space-between;
border-bottom: 1rpx solid #eee;
padding-bottom: 16rpx;
margin-bottom: 16rpx;
}
.order-no {
font-size: 26rpx;
color: #999;
}
.order-status {
font-size: 26rpx;
color: #f39c12;
}
.info-row {
display: flex;
margin-bottom: 12rpx;
}
.label {
width: 140rpx;
font-size: 26rpx;
color: #666;
}
.value {
flex: 1;
font-size: 26rpx;
color: #333;
}
.order-footer {
display: flex;
justify-content: flex-end;
margin-top: 20rpx;
gap: 20rpx;
}
.btn-primary {
background-color: #007AFF;
color: #fff;
font-size: 24rpx;
padding: 10rpx 30rpx;
border-radius: 30rpx;
}
.btn-secondary {
background-color: #fff;
color: #666;
border: 1rpx solid #ddd;
font-size: 24rpx;
padding: 10rpx 30rpx;
border-radius: 30rpx;
}
.modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0,0,0,0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 100;
}
.modal-content {
width: 80%;
background-color: #fff;
border-radius: 20rpx;
padding: 40rpx;
}
.modal-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 30rpx;
}
.title-box {
display: flex;
flex-direction: column;
}
.modal-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.modal-order-no {
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
}
.close-btn {
font-size: 40rpx;
color: #ccc;
padding: 10rpx;
}
.form-item {
margin-bottom: 30rpx;
}
.picker-val {
border: 1rpx solid #ddd;
padding: 16rpx;
border-radius: 8rpx;
margin-top: 10rpx;
}
.input-wrapper {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
margin-top: 10rpx;
}
.input {
flex: 1;
border: 1rpx solid #ddd;
padding: 16rpx;
padding-right: 80rpx;
border-radius: 8rpx;
}
.scan-btn {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 80rpx;
display: flex;
justify-content: center;
align-items: center;
z-index: 10;
}
.scan-icon {
font-size: 36rpx;
}
.modal-footer {
display: flex;
gap: 20rpx;
margin-top: 40rpx;
}
.btn-confirm {
flex: 1;
background-color: #007AFF;
color: #fff;
}
.btn-cancel {
flex: 1;
background-color: #eee;
color: #666;
}
</style>