feat: 订单列表+打包

This commit is contained in:
zc 2025-11-17 23:45:39 +08:00
parent 4797a630f3
commit 580cd25221
4 changed files with 1301 additions and 77 deletions

View File

@ -3,7 +3,8 @@ const order = {
*
*/
getOrderList: ['/order/list'], // 获取订单管理
packOrder: ['/order/pack'], // 打包
getOrderDetail: ['/order/detail'], // 获取订单详情
packOrder: ['/order/toShipping'], // 打包
unpackOrder: ['/order/unpack'], // 取消打包
finishDeliver: ['/order/delivered'] // 妥投
}

View File

@ -0,0 +1,788 @@
<template>
<div class="order-detail-page">
<div class="order-detail-main-container">
<!-- 左侧消费者信息和订单详情 -->
<div class="left-section">
<div class="flex items-center gap-2">
<!-- 消费者信息 -->
<div class="consumer-info flex-1">
<h3 class="section-title">消费者信息</h3>
<div class="consumer-info-content">
<!-- 买家姓名 -->
<div class="info-item">
<div class="label">买家姓名</div>
<div class="value buyer-name">
<el-image class="flag-icon" :src="orderDetail.orderInfo.imgUrl" fit="cover">
</el-image>
<span class="username">{{ orderDetail.orderInfo.buyerName }}</span>
</div>
</div>
<!-- 买家ID -->
<div class="info-item">
<div class="label">买家ID</div>
<div class="value">{{ orderDetail.orderInfo.buyerId }}</div>
</div>
<!-- 付款方式 -->
<div class="info-item">
<div class="label">付款方式</div>
<div class="value">微信支付</div>
</div>
</div>
</div>
<!-- 消费者地址 -->
<div class="consumer-info flex-1">
<h3 class="section-title">消费者地址</h3>
<div class="consumer-info-content">
<div class="info-item">
<div class="label">收件人姓名</div>
<div class="value">{{ orderDetail.orderInfo.buyerName }}</div>
</div>
<div class="info-item">
<div class="label">收件人电话</div>
<div class="value">{{ orderDetail.orderInfo.mobile }}</div>
</div>
<div class="info-item">
<div class="label">具体地址</div>
<div class="value">{{ orderDetail.orderInfo.buyerDetailAddress }}</div>
</div>
</div>
</div>
</div>
<!-- 订单详情 -->
<div class="order-detail-section">
<!-- 顶部订单号和创建时间 -->
<div class="order-header">
<div class="header-left">
<span class="order-number-label">订单号</span>
<span class="order-number-value">{{ orderDetail.orderInfo.orderNumber }}</span>
<span class="order-number-label ml-5">微信交易订单号</span>
<span class="order-number-value">{{ orderDetail.orderInfo.transactionId }}</span>
<span class="create-time-label ml-5">创建时间</span>
<span class="create-time-value">{{ orderDetail.orderInfo.createTime }}</span>
</div>
<!-- <div class="header-right">
<span class="platform-name">购de着</span>
</div> -->
</div>
<!-- 待处理商品 -->
<div class="order-section">
<div class="order-table-header">
<div class="col goods">商品</div>
<div class="col status text-center">状态</div>
<div class="col total text-center">总计</div>
<div class="col status text-center">退款金额</div>
<div class="col status text-center">退款数量</div>
<div class="col total text-center">销售价格</div>
<div class="col total text-center">成本价</div>
<div class="col status text-center">运费</div>
<div class="col status text-center">利润</div>
</div>
<div
v-for="(packageData, packageIndex) in orderDetail.packageList"
:key="packageIndex"
class="mt-2"
>
<div class="order-content">
<div class="col goods">
<div
v-for="item in packageData.vvTradeOrderLineDOList"
:key="item.id"
class="goods-item"
>
<el-image class="goods-image" :src="item.productMainImageUrl" fit="cover">
</el-image>
<div class="goods-info">
<div class="title">{{ item.productName }}</div>
<div class="sub">
<span>{{ item.skuInfo }}</span>
<span>skuId{{ item.skuId }}</span>
</div>
</div>
<div class="goods-price">{{ item.chinesePromotionPrice }} x {{ item.num }}</div>
</div>
</div>
<div class="col status">
<div class="goods-status-item flex-1 flex justify-center items-center">
<div class="status-label">
{{ packageData.vvTradeOrderLineDOList[0].status }}
</div>
</div>
</div>
<div class="col total">
<div class="amount">
{{
packageData.vvTradeOrderLineDOList.reduce((acc: number, it: any) => {
return acc + it.promotionPrice * it.num
}, 0)
}}
</div>
</div>
<div class="col status">
<div class="goods-status-item flex-1 flex justify-center items-center">
<div class="status-label">
{{ packageData.vvTradeOrderLineDOList[0].refundAmount }}
</div>
</div>
</div>
<div class="col status">
<div class="goods-status-item flex-1 flex justify-center items-center">
<div class="status-label">
{{ packageData.vvTradeOrderLineDOList[0].refundCount }}
</div>
</div>
</div>
<div class="col status">
<div class="goods-status-item flex-1 flex justify-center items-center">
<div class="status-label">
{{ packageData.vvTradeOrderLineDOList[0].salePrice }}
</div>
</div>
</div>
<div class="col status">
<div class="goods-status-item flex-1 flex justify-center items-center">
<div class="status-label">
{{ packageData.vvTradeOrderLineDOList[0].originPrice }}
</div>
</div>
</div>
<div class="col status">
<div class="goods-status-item flex-1 flex justify-center items-center">
<div class="status-label">
{{ packageData.vvTradeOrderLineDOList[0].shippingAmount }}
</div>
</div>
</div>
<div class="col status">
<div class="goods-status-item flex-1 flex justify-center items-center">
<div class="status-label">
{{ packageData.vvTradeOrderLineDOList[0].profitAmount }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 消费者地址 -->
<div class="consumer-info">
<h3 class="section-title">订单备注</h3>
<div class="">
<div class="info-item">
<div class="label">买家备注</div>
<div class="value">{{ orderDetail.orderInfo.buyerName }}</div>
</div>
<div class="info-item">
<div class="label">卖家备注</div>
<div class="value">{{ orderDetail.orderInfo.mobile }}</div>
</div>
</div>
</div>
</div>
<!-- 右侧订单备注和消费者地址 -->
<!-- <div class="right-section">
<div class="order-notes">
<h3 class="section-title">订单备注</h3>
<div class="note-item">
<div class="label">买家备注</div>
<div class="value note-content">暂无</div>
</div>
<div class="note-item">
<div class="label">卖家备注</div>
<div class="value">
<div class="note-header"></div>
<div class="note-content">暂无</div>
</div>
</div>
</div>
<div class="address-section">
<h3 class="section-title">消费者地址</h3>
<div class="address-block">
<h4 class="address-subtitle">邮寄地址</h4>
<div class="address-item">
<div class="label">具体地址</div>
<div class="value">{{ orderDetail.orderInfo.buyerDetailAddress }}</div>
</div>
<div class="address-item">
<div class="label">收件人姓名</div>
<div class="value">{{ orderDetail.orderInfo.buyerName }}</div>
</div>
<div class="address-item">
<div class="label">收件人电话</div>
<div class="value">{{ orderDetail.orderInfo.mobile }}</div>
</div>
</div>
</div>
</div> -->
</div>
</div>
</template>
<script setup lang="ts">
const route = useRoute()
interface OrderItem {
id: number
productName: string
// sku: string
num: number
promotionPrice: string
skuInfo: string
status: string
orderActionList: OrderAction[]
productMainImageUrl: string
chinesePromotionPrice: string
refundAmount: number
refundCount: number
salePrice: number
originPrice: number
shippingAmount: number
profitAmount: number
skuId: number
}
interface OrderAction {
desc: string
buttonName: string
interfaceUri: string
}
interface PackageItem {
orderActionList: OrderAction[]
vvTradeOrderLineDOList: OrderItem[]
trackNumber: number
}
interface OrderInfo {
orderNumber: string
createTime: string
num: number
allPrice: number
buyerId: number
mobile: string
buyerName: string
imgUrl: string
buyerDetailAddress: string
transactionId: string
}
//
const orderDetail = reactive<{ packageList: PackageItem[]; orderInfo: OrderInfo }>({
orderInfo: {
orderNumber: '',
createTime: '',
num: 0,
allPrice: 0,
buyerId: 0,
mobile: '',
buyerName: '',
imgUrl: '',
buyerDetailAddress: '',
transactionId: ''
},
packageList: []
})
const orderStatusMap = {
create: '已创建',
wait_pay: '待支付',
wait_shipping: '待发货',
shipping: '已发货',
shipped: '已投递',
delivered: '已妥投',
apply_cancel: '买家申请取消',
cancel: '卖家同意取消',
refund: '已退款',
close: '买家关闭订单'
}
//
const parseOrderData = (data: any) => {
const order = data.vvTradeOrderResp
const buyerInfo = data.vvBuyerEntity
orderDetail.orderInfo = {
imgUrl: buyerInfo.imgUrl,
orderNumber: order.id?.toString() || '',
createTime: new Date(order.createTimestamp).toLocaleString(),
num: order.num,
allPrice: order.allPrice,
buyerId: buyerInfo.id,
buyerName: buyerInfo.buyerName,
mobile: order.mobile,
buyerDetailAddress:
order.contry + order.province + order.city + order.district + order.buyerDetailAddress,
transactionId: order.transactionId
}
orderDetail.packageList = order.packageList.map((packageData: any) => {
packageData.vvTradeOrderLineDOList = packageData.vvTradeOrderLineDOList.map((item: any) => {
return {
...item,
status: orderStatusMap[item.status as keyof typeof orderStatusMap],
chinesePromotionPrice: '¥' + item.promotionPrice.toFixed(2),
skuInfo: JSON.parse(item.skuInfo)
.map((it: any) => `${it.propertyName}:${it.propertyValue}`)
.join(',')
}
})
return packageData
})
}
//
const fetchOrderDetail = async () => {
const orderId = route.query.id || route.params.id
if (!orderId) {
console.warn('订单ID不存在')
return
}
try {
// TODO: API
const res = await api.order.getOrderDetail.post!({ tradeOrderId: orderId })
parseOrderData(res.data)
} catch (error) {
console.error('获取订单详情失败', error)
}
}
//
onMounted(() => {
fetchOrderDetail()
})
</script>
<style scoped lang="scss">
.order-detail-page {
background-color: #f5f7fa;
overflow-x: scroll;
//
.order-detail-main-container {
display: flex;
gap: 20px;
margin: 0 auto;
//
.left-section {
flex: 3;
display: flex;
flex-direction: column;
gap: 12px;
.consumer-info {
background: #fff;
border-radius: 8px;
padding: 12px 12px 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
.section-title {
font-size: 14px;
font-weight: 600;
color: #303133;
margin: 0 0 8px 0;
padding-bottom: 4px;
border-bottom: 1px solid #ebeef5;
}
.consumer-info-content {
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.info-item {
margin-bottom: 20px;
.label {
font-size: 13px;
color: #909399;
margin-bottom: 8px;
font-weight: 400;
}
.value {
font-size: 14px;
color: #303133;
word-break: break-all;
&.buyer-name {
display: flex;
align-items: center;
gap: 8px;
.flag-icon {
width: 20px;
height: 20px;
border-radius: 2px;
overflow: hidden;
}
.icon-placeholder {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
background: #f2f3f5;
}
.user-icon {
font-size: 18px;
color: #909399;
}
.username {
font-weight: 500;
color: #303133;
}
.chat-icon {
font-size: 18px;
color: #409eff;
cursor: pointer;
margin-left: 4px;
&:hover {
color: #66b1ff;
}
}
}
}
}
}
.order-detail-section {
background: #fff;
border-radius: 8px;
padding: 12px 12px 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 4px;
padding-bottom: 8px;
margin-bottom: 12px;
border-bottom: 1px solid #ebeef5;
.header-left {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
.order-number-label,
.create-time-label {
color: #909399;
}
.order-number-value,
.create-time-value {
color: #303133;
font-weight: 500;
}
.header-icon {
color: #909399;
font-size: 16px;
}
}
.header-right {
.platform-name {
font-size: 14px;
color: #303133;
font-weight: 500;
}
}
}
}
}
//
.right-section {
flex: 1;
min-width: 300px;
display: flex;
flex-direction: column;
gap: 20px;
.order-notes,
.address-section {
background: #fff;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
.section-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin: 0 0 20px 0;
padding-bottom: 12px;
border-bottom: 1px solid #ebeef5;
}
}
.order-notes {
.note-item {
margin-bottom: 20px;
.label {
font-size: 13px;
color: #909399;
margin-bottom: 8px;
font-weight: 400;
}
.value {
font-size: 14px;
color: #303133;
word-break: break-all;
.note-header {
margin-bottom: 8px;
display: flex;
align-items: center;
:deep(.el-button) {
padding: 0;
height: auto;
font-size: 13px;
display: flex;
align-items: center;
gap: 4px;
}
}
.note-content {
color: #909399;
font-size: 13px;
line-height: 1.5;
}
}
}
}
.address-section {
.address-block {
margin-bottom: 24px;
&:last-of-type {
margin-bottom: 0;
}
.address-subtitle {
font-size: 15px;
font-weight: 600;
color: #303133;
margin: 0 0 16px 0;
}
.address-item {
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
.label {
font-size: 13px;
color: #909399;
margin-bottom: 6px;
}
.value {
font-size: 14px;
color: #303133;
word-break: break-all;
}
}
}
}
}
}
.order-table-header {
display: grid;
grid-template-columns: 1fr 80px 60px 60px 60px 60px 60px 30px 30px;
padding: 12px 0 12px 0;
background: #f9fafc;
border-top: 1px solid #ebeef5;
border-bottom: 1px solid #ebeef5;
font-size: 13px;
color: #909399;
overflow-x: auto;
white-space: nowrap;
grid-auto-flow: column;
.col:first-child {
padding-left: 44px;
}
}
.order-content {
display: grid;
grid-template-columns: 1fr 80px 60px 60px 60px 60px 60px 30px 30px;
padding: 0;
font-size: 13px;
color: #606266;
border: 1px solid #ebeef5;
border-radius: 6px;
overflow-x: auto;
white-space: nowrap;
grid-auto-flow: column;
.goods-item + .goods-item {
border-top: 1px solid #f0f2f5;
}
.goods-status-item {
width: 100%;
}
.goods-status-item + .goods-status-item {
border-top: 1px solid #f0f2f5;
}
.goods {
display: flex;
flex-direction: column;
.goods-item {
display: flex;
gap: 12px;
border-right: 1px solid #f0f2f5;
border-radius: 6px;
padding: 12px;
align-items: center;
flex: 1;
.goods-image {
width: 60px;
height: 60px;
border-radius: 6px;
overflow: hidden;
}
.image-fallback {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
background: #f2f3f5;
color: #909399;
font-size: 12px;
}
.goods-info {
flex: 1;
.title {
font-size: 14px;
font-weight: 500;
color: #303133;
margin-bottom: 6px;
}
.sub {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 12px;
color: #909399;
}
}
.goods-price {
font-weight: 600;
color: #303133;
}
}
}
.total {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 4px;
border-left: 1px solid #f0f2f5;
.amount {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.remark {
font-size: 12px;
color: #909399;
}
}
.delivery {
display: flex;
flex-direction: column;
justify-content: center;
gap: 4px;
.delivery-type {
font-weight: 600;
color: #303133;
}
.delivery-warehouse {
font-size: 12px;
color: #909399;
}
}
.status {
border-left: 1px solid #f0f2f5;
display: flex;
flex-direction: column;
justify-content: center;
.status-label {
font-weight: 600;
color: #303133;
}
.status-detail {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 4px;
font-size: 12px;
color: #909399;
}
}
.actions {
border-left: 1px solid #f0f2f5;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 8px 0;
}
}
}
@media (max-width: 768px) {
.order-detail-page {
.order-detail-main-container {
flex-direction: column;
.left-section,
.right-section {
flex: 1;
}
}
.order-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
}
}
</style>

View File

@ -109,71 +109,78 @@
<span class="order-meta">创建时间{{ order.createTimestamp }}</span>
</div>
</div>
<div class="order-content">
<div class="col goods">
<div v-for="item in order.vvTradeOrderLineList" :key="item.id" class="goods-item">
<el-image class="goods-image" :src="item.productMainImageUrl" fit="cover">
</el-image>
<div class="goods-info">
<div class="title">{{ item.productName }}</div>
<div class="sub">
<span>{{ item.skuInfo }}</span>
<!-- <span>卖家SKU{{ item.sku }}</span> -->
<div v-for="(packageData, packageIndex) in order.packageList" :key="packageIndex">
<div class="order-content">
<div class="col goods">
<div
v-for="item in packageData.vvTradeOrderLineDOList"
:key="item.id"
class="goods-item"
>
<el-image class="goods-image" :src="item.productMainImageUrl" fit="cover">
</el-image>
<div class="goods-info">
<div class="title">{{ item.productName }}</div>
<div class="sub">
<span>{{ item.skuInfo }}</span>
<!-- <span>卖家SKU{{ item.sku }}</span> -->
</div>
</div>
<div class="goods-price">{{ item.chinesePromotionPrice }} x {{ item.num }}</div>
</div>
<div class="goods-price">{{ item.promotionPrice }} x {{ item.num }}</div>
</div>
</div>
<div class="col total">
<div class="amount">{{ order.allPrice }}</div>
</div>
<!-- <div class="col delivery">
<div class="delivery-type">{{ order.delivery.type }}</div>
<div class="delivery-warehouse">签收仓库{{ order.delivery.warehouse }}</div>
</div> -->
<div class="col status">
<div
v-for="item in order.vvTradeOrderLineList"
:key="item.id"
class="goods-status-item flex-1 flex justify-center items-center"
>
<div class="status-label">{{ item.status }}</div>
<div class="col total">
<div class="amount">
{{
packageData.vvTradeOrderLineDOList.reduce((acc: number, it: any) => {
return acc + it.promotionPrice * it.num
}, 0)
}}
</div>
</div>
<!-- <ul class="status-detail">
<li>AWB{{ order.awb }}</li>
<li>揽收{{ order.pickupStatus }}</li>
<li>装袋{{ order.bagStatus }}</li>
</ul> -->
</div>
<div class="col actions">
<div
v-for="orderLine in order.vvTradeOrderLineList"
:key="orderLine.id"
class="goods-actions-item flex-1 flex flex-col gap-2 justify-center items-center"
>
<el-button
v-for="item in orderLine.orderActionList.filter(
(item) => !item.desc.includes('app')
)"
:key="item.interfaceUri"
class="ml-0!"
type="primary"
size="small"
@click="onButtonClick(item.interfaceUri, orderLine)"
>{{ item.desc.replace('admin', '').replace('按钮', '') }}</el-button
>
<el-button
v-if="
orderLine.orderActionList.every(
(item) => item.interfaceUri !== '/mm/order/toShipping'
)
"
type="success"
class="!ml-0"
size="small"
@click="handleViewLogistics(orderLine)"
>查看物流信息</el-button
<div class="col status">
<div class="goods-status-item flex-1 flex justify-center items-center">
<div class="status-label">{{ packageData.vvTradeOrderLineDOList[0].status }}</div>
</div>
<!-- <ul class="status-detail">
<li>AWB{{ order.awb }}</li>
<li>揽收{{ order.pickupStatus }}</li>
<li>装袋{{ order.bagStatus }}</li>
</ul> -->
</div>
<!-- <div class="col delivery">
<div class="delivery-type">{{ order.delivery.type }}</div>
<div class="delivery-warehouse">签收仓库{{ order.delivery.warehouse }}</div>
</div> -->
<div class="col actions">
<div
v-for="(orderAction, orderActionIndex) in packageData.orderActionList"
:key="orderActionIndex"
:class="[
'goods-actions-item flex flex-col gap-2 justify-center items-center',
orderActionIndex ? '!mt-2' : ''
]"
>
<el-button
class="ml-0!"
type="primary"
size="small"
@click="onButtonClick(orderAction.interfaceUri, packageData)"
>{{ orderAction.desc.replace('admin', '').replace('按钮', '') }}</el-button
>
<el-button
v-if="
packageData.orderActionList.every(
(item) => item.interfaceUri !== '/mm/order/toShipping'
)
"
type="success"
class="!ml-0"
size="small"
@click="handleViewLogistics(packageData)"
>查看物流信息</el-button
>
</div>
</div>
</div>
</div>
@ -196,12 +203,19 @@
:order-id="currentOrderId"
@close="handleCloseLogisticsDialog"
/>
<!-- 打包 -->
<pack-dialog
v-model="dialogPackVisible"
:orderLineData="currentOrderLineData"
@close="dialogPackVisible = false"
/>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import LogisticsDialog from './logistics-dialog.vue'
import PackDialog from './pack-dialog.vue'
type SortWay = 'create_desc' | 'create_asc' | 'pickup_desc' | 'pickup_asc'
@ -215,6 +229,12 @@ interface OrderItem {
status: string
orderActionList: OrderAction[]
productMainImageUrl: string
chinesePromotionPrice: string
}
interface PackageItem {
orderActionList: OrderAction[]
vvTradeOrderLineDOList: OrderItem[]
trackNumber: number
}
interface OrderAction {
desc: string
@ -240,7 +260,7 @@ interface Order {
type: string
warehouse: string
} */
vvTradeOrderLineList: OrderItem[]
packageList: PackageItem[]
checked: boolean
}
@ -256,7 +276,7 @@ const tabs = reactive([
{ name: 'delete', label: '已删除' },
{ name: 'refund', label: '已退款' }
])
/*
/*
const statusShortcuts = reactive([
{ label: '待打包', count: 3544 },
{ label: '待安排物流', count: 492 },
@ -366,12 +386,14 @@ const onTabClick = () => {
handleGetOrderList()
}
const onButtonClick = (interfaceUri: string, orderLine: OrderItem) => {
const onButtonClick = (interfaceUri: string, packageData: PackageItem) => {
console.warn('----- my data is packageData: ', packageData)
if ('/mm/order/toShipping' === interfaceUri) {
//
handlePack(packageData.vvTradeOrderLineDOList)
} else if ('/mm/order/testzc' === interfaceUri) {
// testzc
handleViewLogistics(orderLine)
handleViewLogistics(packageData)
} else {
// testzc
const apiMap = {
@ -393,11 +415,19 @@ const logisticsDialogVisible = ref(false)
const currentOrderId = ref<number>(NaN)
//
const handleViewLogistics = (orderLine: OrderItem) => {
currentOrderId.value = orderLine.id
const handleViewLogistics = (packageData: PackageItem) => {
currentOrderId.value = packageData.trackNumber
logisticsDialogVisible.value = true
}
//
const dialogPackVisible = ref(false)
const currentOrderLineData = ref<OrderItem[]>([])
const handlePack = (orderLines: OrderItem[]) => {
currentOrderLineData.value = orderLines
dialogPackVisible.value = true
}
//
const handleCloseLogisticsDialog = () => {
logisticsDialogVisible.value = false
@ -445,11 +475,109 @@ const handleGetOrderList = async (searchData: any = {}) => {
pageNum: 1,
pageSize: 20
})
orders.value = res.data.map((item: any) => {
/* const res = {
data: [
{
id: 202511172456,
isDelete: 0,
createTime: 1762506683000,
modifyTime: 1762506683000,
num: 2,
allPrice: 0,
buyerId: 7,
createTimestamp: 1762506683235,
modifyTimestamp: 1762506683235,
contry: '中国',
province: '山西省',
city: '阳泉市',
district: '城区',
buyerDetailAddress: '信息港9期',
gmtDownOrder: 1762506683000,
packageList: [
{
vvTradeOrderLineDOList: [
{
activityAwardCount: 0,
batchNum: 2,
createTime: 1762506683000,
id: 202511172457,
isDelete: 0,
modifyTime: 1762506683000,
num: 2,
originPrice: 16,
productId: 58,
productMainImageUrl:
'https://heyuimage.ihzhy.com/prd/202511/9ea39f99ec120fb0.jpg?key=xxxxxxx?key=xxxxxxx?key=xxxxxxx?key=xxxxxxx?key=xxxxxxx?key=xxxxxxx',
productName: '32322332',
profitAmount: 0,
promotionPrice: 20,
refundAmount: 0,
refundCount: 0,
salePrice: 0,
shippingAmount: 0,
skuId: 346,
skuInfo: '[{"propertyName":"长度","propertyValue":"11333"}]',
status: 'wait_pay',
tradeOrderId: 202511172456,
mergeIdList: [202511172457, 202511172456]
},
{
activityAwardCount: 0,
batchNum: 2,
createTime: 1762506683000,
id: 202511172457,
isDelete: 0,
modifyTime: 1762506683000,
num: 2,
originPrice: 16,
productId: 58,
productMainImageUrl:
'https://heyuimage.ihzhy.com/prd/202511/9ea39f99ec120fb0.jpg?key=xxxxxxx?key=xxxxxxx?key=xxxxxxx?key=xxxxxxx?key=xxxxxxx?key=xxxxxxx',
productName: '32322332',
profitAmount: 0,
promotionPrice: 20,
refundAmount: 0,
refundCount: 0,
salePrice: 0,
shippingAmount: 0,
skuId: 346,
skuInfo: '[{"propertyName":"长度","propertyValue":"11333"}]',
status: 'wait_pay',
tradeOrderId: 202511172456,
mergeIdList: [202511172457, 202511172456]
}
],
orderActionList: [
{
buttonName: 'App_Close',
desc: 'app关闭订单',
interfaceUri: '/app/order/close'
}
]
}
]
}
]
} */
orders.value = res.data.rows.map((item: any) => {
item.platform = '购de着小程序'
item.allPrice = '¥' + item.allPrice.toFixed(2)
item.allPrice = '¥' + item.allPrice
item.tradeOrderId = item.id
item.vvTradeOrderLineList = item.vvTradeOrderLineDOList.map((childOrder: any) => {
item.packageList = item.packageList.map((packageData: any) => {
packageData.vvTradeOrderLineDOList = packageData.vvTradeOrderLineDOList.map((item: any) => {
return {
...item,
status: orderStatusMap[item.status as keyof typeof orderStatusMap],
chinesePromotionPrice: '¥' + item.promotionPrice.toFixed(2),
skuInfo: JSON.parse(item.skuInfo)
.map((it: any) => `${it.propertyName}:${it.propertyValue}`)
.join(',')
}
})
return packageData
})
/* item.vvTradeOrderLineList = item.packageList.map((childOrder: any) => {
return {
...childOrder,
status: orderStatusMap[childOrder.status as keyof typeof orderStatusMap],
@ -459,7 +587,7 @@ const handleGetOrderList = async (searchData: any = {}) => {
.join(','),
checked: false
}
})
}) */
return item
})
}
@ -580,6 +708,7 @@ handleGetOrderList()
padding: 0;
font-size: 13px;
color: #606266;
border-bottom: 1px solid #ebeef5;
.goods-item + .goods-item {
border-top: 1px solid #f0f2f5;
@ -590,12 +719,6 @@ handleGetOrderList()
.goods-status-item + .goods-status-item {
border-top: 1px solid #f0f2f5;
}
.goods-actions-item {
width: 100%;
}
.goods-actions-item + .goods-actions-item {
border-top: 1px solid #f0f2f5;
}
.goods {
display: flex;
flex-direction: column;

View File

@ -0,0 +1,312 @@
<template>
<el-dialog
v-model="dialogVisible"
title="打包管理"
width="900px"
:before-close="() => handleClose()"
@close="() => handleClose()"
>
<div class="pack-dialog">
<!-- 表格区域 -->
<div class="table-section">
<h3
class="text-base font-semibold mb-5 flex items-center"
style="
color: #303133;
letter-spacing: 0.5px;
padding-left: 18px;
background: linear-gradient(90deg, #f4f8ff 0%, #fff 100%);
border-radius: 6px 0 0 6px;
height: 36px;
align-items: center;
"
>
请选择以下要打包的订单
</h3>
<el-table
ref="tableRef"
:data="tableData"
style="width: 100%"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="productName" label="商品名称" min-width="150" />
<el-table-column prop="skuInfo" label="SKU信息" min-width="120" />
<el-table-column prop="num" label="数量" width="80" align="center" />
<el-table-column prop="promotionPrice" label="单价" width="100" align="right" />
<el-table-column prop="status" label="状态" width="100" align="center" />
</el-table>
</div>
<!-- 表单区域 -->
<div class="form-section" v-if="selectedRows.length > 0">
<h3
class="text-base font-semibold mb-5 flex items-center"
style="
color: #303133;
letter-spacing: 0.5px;
padding-left: 18px;
background: linear-gradient(90deg, #f4f8ff 0%, #fff 100%);
border-radius: 6px 0 0 6px;
height: 36px;
align-items: center;
"
>
物流信息
</h3>
<el-form ref="formRef" :model="formData" inline :rules="formRules" label-width="100px">
<el-form-item label="物流单号" prop="trackingNumber">
<el-input v-model="formData.trackingNumber" placeholder="请输入物流单号" clearable />
</el-form-item>
<el-form-item label="物流公司" prop="logisticsCompany">
<el-input v-model="formData.logisticsCompany" placeholder="请输入物流公司" clearable />
</el-form-item>
<el-form-item label="运费" prop="shippingAmount">
<el-input
v-model="formData.shippingAmount"
placeholder="请输入运费"
clearable
style="width: 100px"
/>
</el-form-item>
<el-form-item label="上传照片" prop="packageImageUrls">
<fileUploadBtn accept="image/*" :multiple="true" @change="handlePhotoChange" />
<el-button class="ml-2" @click="onClickChooseResourceBtn('mainImageUrl')"
>资源库导入</el-button
>
</el-form-item>
<div v-if="formData.packageImageUrls.length > 0" class="photo-preview ml-24">
<div
v-for="(photo, index) in formData.packageImageUrls"
:key="index"
class="photo-item"
>
<el-image
:src="photo"
fit="cover"
style="width: 70px; height: 70px; border-radius: 4px"
:preview-src-list="formData.packageImageUrls"
/>
<el-button type="danger" size="small" link @click="handleRemovePhoto(index)">
删除
</el-button>
</div>
</div>
</el-form>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose()">关闭弹窗</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitting">打包订单</el-button>
</div>
</template>
<FileExplorerDialog
v-model="showFileExplorer"
v-model:initPathArray="currentPathArray"
@select="(files: FileItem[]) => handleChooseResourceFileCallback(files, curFileData)"
/>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
// @ts-ignore
import fileUploadBtn from '@/components/FileUploadBtn/index.vue'
import FileExplorerDialog, { FileItem } from '@/components/FileExplorerDialog/index.vue'
interface TableItem {
id: number
productName: string
skuInfo: string
num: number
promotionPrice: string
status: string
}
interface FormData {
trackingNumber: string
logisticsCompany: string
shippingAmount: string
packageImageUrls: string[]
}
interface OrderItem {
orderLineId: number
productName: string
skuInfo: string
num: number
promotionPrice: string
status: string
mergeIdList: number[]
}
const props = defineProps<{
modelValue: boolean
orderLineData?: OrderItem[]
}>()
const emit = defineEmits<{
'update:modelValue': [value: boolean]
close: []
}>()
const dialogVisible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
//
const tableRef = ref()
const tableData = ref<OrderItem[]>([])
const selectedRows = ref<OrderItem[]>([])
//
const formRef = ref<FormInstance>()
const formData = ref<FormData>({
trackingNumber: '',
logisticsCompany: '',
shippingAmount: '',
packageImageUrls: []
})
const formRules: FormRules = {
trackingNumber: [{ required: true, message: '请输入物流单号', trigger: 'blur' }],
logisticsCompany: [{ required: true, message: '请输入物流公司', trigger: 'blur' }],
shippingAmount: [{ required: true, message: '请输入运费', trigger: 'blur' }],
packageImageUrls: [{ required: true, message: '请上传照片', trigger: 'blur' }]
}
//
const submitting = ref(false)
//
const handleSelectionChange = (selection: TableItem[]) => {
selectedRows.value = selection
}
//
const handlePhotoChange = (
files: Array<{ fileName: string; resourceUrl: string; type: 'image' | 'video' }>
) => {
formData.value.packageImageUrls.push(...files.map((item) => item.resourceUrl))
}
//
const handleRemovePhoto = (index: number) => {
formData.value.packageImageUrls.splice(index, 1)
}
//
const handleSubmit = async () => {
if (!formRef.value) return
formRef.value.validate(async (valid) => {
if (!valid) {
return
}
if (selectedRows.value.length === 0) {
ElMessage.warning('请至少选择一项数据进行操作')
return
}
submitting.value = true
try {
const submitData = {
tradeOrderLineIds: selectedRows.value.map((row) => row.orderLineId),
...formData.value
}
await api.order.packOrder.post!(submitData)
ElMessage.success('打包成功')
handleClose(false)
selectedRows.value = []
} catch (error) {
ElMessage.error('打包失败')
} finally {
submitting.value = false
}
})
}
const currentPathArray = ref<{ name: string; id: number }[]>([{ name: '根目录', id: 0 }])
const showFileExplorer = ref(false)
const onClickChooseResourceBtn = () => {
showFileExplorer.value = true
}
const handleChooseResourceFileCallback = async (files: FileItem[]) => {
formData.value.packageImageUrls.push(...files.map((item) => item.resourceUrl!))
}
//
const handleClose = (isClose = true) => {
//
selectedRows.value = []
formData.value = {
trackingNumber: '',
logisticsCompany: '',
shippingAmount: '',
packageImageUrls: []
}
formRef.value?.resetFields()
tableRef.value?.clearSelection()
isClose && emit('close')
}
// id
watch(
() => dialogVisible.value,
(visible) => {
if (visible) {
tableData.value = []
;(props.orderLineData || []).forEach((item: OrderItem) => {
tableData.value.push(
...item.mergeIdList.map((orderLineId: number) => {
return { ...item, orderLineId }
})
)
})
}
}
)
</script>
<style scoped lang="scss">
.pack-dialog {
.table-section {
margin-bottom: 24px;
.batch-actions {
margin-top: 16px;
padding: 12px 0;
border-top: 1px solid #ebeef5;
}
}
.form-section {
.photo-preview {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 0px;
.photo-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
}
}
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
</style>