2025-11-20 17:10:28 +08:00

858 lines
24 KiB
Vue
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>
<div class="order-list-page">
<el-card class="status-card" shadow="never">
<div class="status-tabs">
<el-tabs v-model="activeTab" @tab-change="onTabClick">
<el-tab-pane
v-for="tab in tabs"
:key="tab.name"
:name="tab.name"
:label="`${tab.label} ${tab.count ? `(${tab.count})` : ''}`"
/>
</el-tabs>
</div>
<!-- <div class="status-shortcut">
<div
v-for="shortcut in statusShortcuts"
:key="shortcut.label"
class="status-shortcut__item"
>
<span class="label">{{ shortcut.label }}</span>
<span v-if="typeof shortcut.count !== 'undefined'" class="count">{{
`(${shortcut.count})`
}}</span>
</div>
</div> -->
<el-form :inline="true" class="filter-form">
<el-form-item label="商品ID">
<el-input
v-model="filters[activeTab as keyof typeof filters].productId"
placeholder="请输入商ID"
clearable
/>
</el-form-item>
<el-form-item label="商品名称">
<el-input
v-model="filters[activeTab as keyof typeof filters].productName"
placeholder="请输入商品名称"
clearable
/>
</el-form-item>
<el-form-item label="订单号">
<el-input
v-model="filters[activeTab as keyof typeof filters].tradeOrderIds"
placeholder="请输入订单号"
clearable
/>
</el-form-item>
<el-form-item label="创建时间排序">
<el-select
v-model="filters[activeTab as keyof typeof filters].createTimestampSort"
style="width: 180px"
>
<el-option
v-for="option in sortOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</el-form-item>
<el-form-item label="订单时间">
<el-date-picker
v-model="filters[activeTab as keyof typeof filters].orderRange"
type="datetimerange"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
@click="handleGetOrderList(filters[activeTab as keyof typeof filters])"
>查询</el-button
>
<el-button @click="resetFilters">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="order-table-card" shadow="never">
<div class="order-table-header">
<div class="col goods">商品</div>
<div class="col total text-center">总计</div>
<!-- <div class="col delivery">发货</div> -->
<div class="col status text-center">状态</div>
<div class="col actions text-center">操作</div>
</div>
<div class="order-list">
<div v-for="order in orders" :key="order.tradeOrderId" class="order-item">
<div class="order-top">
<div class="top-left">
<el-checkbox v-model="order.checked" />
<span class="platform">{{ order.platform }}</span>
<el-divider direction="vertical" />
<!-- <el-link :underline="false" type="primary">{{ order.summary }}</el-link> -->
</div>
<div class="top-right">
<span class="seller">{{ order.buyerWeixin }}</span>
<el-divider direction="vertical" />
<span
class="order-meta"
style="text-decoration: underline"
@click="handleViewOrderDetail(order.tradeOrderId)"
>订单号:{{ order.tradeOrderId }}</span
>
<el-divider direction="vertical" />
<span class="order-meta">创建时间:{{ order.createTimestamp }}</span>
</div>
</div>
<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>
<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].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>
</div>
</div>
<div class="pagination">
<el-pagination
background
layout="prev, pager, next, jumper"
:page-size="20"
:total="summary.totalOrders"
/>
</div>
</el-card>
<!-- 物流弹窗 -->
<logistics-dialog
v-model="logisticsDialogVisible"
:track-number="curTrackNumber"
@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'
interface OrderItem {
id: number
productName: string
// sku: string
num: number
promotionPrice: string
skuInfo: string
status: string
orderActionList: OrderAction[]
productMainImageUrl: string
chinesePromotionPrice: string
}
interface PackageItem {
orderActionList: OrderAction[]
vvTradeOrderLineDOList: OrderItem[]
trackNumber: number
}
interface OrderAction {
desc: string
buttonName: string
interfaceUri: string
}
interface Order {
platform: string
// warehouse: string
buyerWeixin: string
// summary: string
tradeOrderId: string
createTimestamp: string
allPrice: string
status: string
/*
awb: string
pickupStatus: string
bagStatus: string
pickupTime: string
delivery: {
type: string
warehouse: string
} */
packageList: PackageItem[]
checked: boolean
}
const tabs = reactive([
{ name: 'all', label: '全部' },
{ name: 'wait_shipping', label: '待发货', count: 3544 },
{ name: 'shipping', label: '已发货' },
{ name: 'shipped', label: '已投递' },
{ name: 'delivered', label: '已妥投' },
{ name: 'apply_cancel', label: '买家取消' },
{ name: 'cancel', label: '卖家取消' },
{ name: 'close', label: '已取消' },
{ name: 'delete', label: '已删除' },
{ name: 'refund', label: '已退款' }
])
/*
const statusShortcuts = reactive([
{ label: '待打包', count: 3544 },
{ label: '待安排物流', count: 492 },
{ label: '待物流确认揽件', count: 669 },
{ label: '待取件', count: 0 }
]) */
const summary = reactive({
totalOrders: 3544
})
const orders = ref<Order[]>([])
const filters = reactive({
all: {
productId: '',
productName: '',
tradeOrderIds: '',
orderRange: [] as string[] | [],
createTimestampSort: 'create_DESC' as SortWay
},
wait_shipping: {
productId: '',
productName: '',
tradeOrderIds: '',
orderRange: [] as string[] | [],
createTimestampSort: 'create_DESC' as SortWay
},
close: {
productId: '',
productName: '',
tradeOrderIds: '',
orderRange: [] as string[] | [],
createTimestampSort: 'create_DESC' as SortWay
},
shipping: {
productId: '',
productName: '',
tradeOrderIds: '',
orderRange: [] as string[] | [],
createTimestampSort: 'create_DESC' as SortWay
},
shipped: {
productId: '',
productName: '',
tradeOrderIds: '',
orderRange: [] as string[] | [],
createTimestampSort: 'create_DESC' as SortWay
},
delivered: {
productId: '',
productName: '',
tradeOrderIds: '',
orderRange: [] as string[] | [],
createTimestampSort: 'create_DESC' as SortWay
},
apply_cancel: {
productId: '',
productName: '',
tradeOrderIds: '',
orderRange: [] as string[] | [],
createTimestampSort: 'create_DESC' as SortWay
},
cancel: {
productId: '',
productName: '',
tradeOrderIds: '',
orderRange: [] as string[] | [],
createTimestampSort: 'create_DESC' as SortWay
},
refund: {
productId: '',
productName: '',
tradeOrderIds: '',
orderRange: [] as string[] | [],
createTimestampSort: 'create_DESC' as SortWay
},
delete: {
productId: '',
productName: '',
tradeOrderIds: '',
orderRange: [] as string[] | [],
createTimestampSort: 'create_DESC' as SortWay
}
})
const sortOptions = [
{ label: '创建时间:最新在前', value: 'create_DESC' },
{ label: '创建时间:最早在前', value: 'create_ASC' },
{ label: '修改时间:最新在前', value: 'modify_DESC' },
{ label: '修改时间:最早在前', value: 'modify_ASC' }
]
const activeTab = ref(tabs[0].name)
const resetFilters = () => {
filters[activeTab.value as keyof typeof filters] = {
productName: '',
productId: '',
tradeOrderIds: '',
orderRange: [] as string[] | [],
createTimestampSort: 'create_DESC' as SortWay
}
}
const onTabClick = () => {
handleGetOrderList()
}
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(packageData)
} else {
// testzc其他按钮的接口
const apiMap = {
'/mm/order/unpack': 'unpackOrder',
'/mm/order/delivered': 'finishDeliver'
}
handleMessageBox({
msg: '是否确认该操作?',
success:
api.order[apiMap[interfaceUri as keyof typeof apiMap] as keyof typeof api.order].post!,
data: { id: orderLine.id }
}).then(() => {
handleGetOrderList()
})
}
}
// 物流弹窗相关
const logisticsDialogVisible = ref(false)
const curTrackNumber = ref<number>(NaN)
// 查看物流
const handleViewLogistics = (packageData: PackageItem) => {
curTrackNumber.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
curTrackNumber.value = NaN
}
const orderStatusMap = {
create: '已创建',
wait_pay: '待支付',
wait_shipping: '待发货',
shipping: '已发货',
shipped: '已投递',
delivered: '已妥投',
apply_cancel: '买家申请取消',
cancel: '卖家同意取消',
refund: '已退款',
close: '买家关闭订单'
}
const router = useRouter()
const handleViewOrderDetail = (id: number) => {
router.push({ path: '/order/list/detail', query: { id } })
}
const handleGetOrderList = async (searchData: any = {}) => {
const search = {
...searchData,
productId: searchData.productId || undefined,
productName: searchData.productName || undefined,
tradeOrderIds: searchData.tradeOrderIds || undefined
}
if (search.createTimestampSort?.startsWith('create_')) {
search.createTimestampSort = search.createTimestampSort.split('_')[1]
} else if (search.createTimestampSort?.startsWith('modify_')) {
search.modifyTimestampSort = search.createTimestampSort.split('_')[1]
delete search.createTimestampSort
}
const res = await api.order.getOrderList.post!<any>({
...search,
minCreateTimestamp: search.orderRange?.[0],
maxCreateTimestamp: search.orderRange?.[1],
tradeOrderIds: search.tradeOrderIds?.split(','),
status: activeTab.value === 'all' ? undefined : activeTab.value,
orderRange: undefined,
pageNum: 1,
pageSize: 20
})
/* 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
item.tradeOrderId = item.id
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],
promotionPrice: '¥' + childOrder.promotionPrice.toFixed(2),
skuInfo: JSON.parse(childOrder.skuInfo)
.map((it: any) => `${it.propertyName}:${it.propertyValue}`)
.join(','),
checked: false
}
}) */
return item
})
}
handleGetOrderList()
</script>
<style scoped lang="scss">
.order-list-page {
display: flex;
flex-direction: column;
gap: 16px;
background-color: #f5f7fa;
padding: 16px;
.status-card {
.status-shortcut {
display: flex;
gap: 24px;
margin-bottom: 12px;
padding: 0 4px;
font-size: 13px;
color: #606266;
&__item {
display: inline-flex;
align-items: center;
gap: 4px;
cursor: pointer;
.label {
color: #303133;
}
.count {
color: #606266;
}
}
}
.filter-form {
display: flex;
flex-wrap: wrap;
gap: 8px 16px;
align-items: center;
:deep(.el-form-item__label) {
font-weight: 500;
color: #303133;
}
}
}
.order-table-card {
padding: 0 0 16px;
.order-table-header {
display: grid;
grid-template-columns: 46% 12% 16% 1fr;
padding: 12px 24px;
background: #f9fafc;
border-bottom: 1px solid #ebeef5;
font-size: 13px;
color: #909399;
.col:first-child {
padding-left: 44px;
}
}
.order-list {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px 0;
}
.order-item {
border: 1px solid #ebeef5;
border-radius: 6px;
background: #fff;
overflow: hidden;
.order-top {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 24px;
background: #fafafa;
border-bottom: 1px solid #f0f2f5;
font-size: 13px;
color: #606266;
.top-left,
.top-right {
display: flex;
align-items: center;
gap: 8px;
}
.platform {
font-weight: 600;
color: #303133;
}
.seller {
color: #303133;
}
.order-meta {
color: #909399;
}
}
.order-content {
display: grid;
grid-template-columns: 46% 12% 16% 1fr;
padding: 0;
font-size: 13px;
color: #606266;
border-bottom: 1px solid #ebeef5;
.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;
.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;
}
}
}
.pagination {
display: flex;
justify-content: flex-end;
padding: 0 24px;
margin-top: 8px;
}
}
}
</style>