feat: 订单列表

This commit is contained in:
zc 2025-11-15 23:16:45 +08:00
parent 566f9fb9ef
commit 4797a630f3
4 changed files with 747 additions and 6 deletions

View File

@ -179,14 +179,13 @@ export default [
modifyTime: '2024-09-10 16:17:41',
tag: null,
childList: []
}
/*
{
},
{
id: 38,
resourceName: '首页商品',
resourceName: '订单评价',
resourceType: 1,
resourceCode: null,
path: '/goods/home-goods/index',
path: '/order/user-reviews/index',
pid: 43,
resourceDesc: null,
tenantId: 2,
@ -198,7 +197,9 @@ export default [
modifyTime: '2024-06-19 17:56:01',
tag: null,
childList: []
}, {
}
/*
{
id: 45,
resourceName: '客户详情',
resourceType: 1,

View File

@ -24,6 +24,12 @@ export const constantRoutes: Array<RouteRecordRaw> = [
name: '/goods/detail',
component: () => import('@/views/goods/commodity/detail-dialog/index.vue'),
meta: { title: '商品详情', hidden: true }
},
{
path: '/order/list/detail',
name: '/order/list/detail',
component: () => import('@/views/order/list/detail/index.vue'),
meta: { title: '订单详情', hidden: true }
}
]
},

View File

@ -0,0 +1,734 @@
<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 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>
</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>
<!-- <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>
</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"
:order-id="currentOrderId"
@close="handleCloseLogisticsDialog"
/>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import LogisticsDialog from './logistics-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
}
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
} */
vvTradeOrderLineList: OrderItem[]
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, orderLine: OrderItem) => {
if ('/mm/order/toShipping' === interfaceUri) {
//
} else if ('/mm/order/testzc' === interfaceUri) {
// testzc
handleViewLogistics(orderLine)
} 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 currentOrderId = ref<number>(NaN)
//
const handleViewLogistics = (orderLine: OrderItem) => {
currentOrderId.value = orderLine.id
logisticsDialogVisible.value = true
}
//
const handleCloseLogisticsDialog = () => {
logisticsDialogVisible.value = false
currentOrderId.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
})
orders.value = res.data.map((item: any) => {
item.platform = '购de着小程序'
item.allPrice = '¥' + item.allPrice.toFixed(2)
item.tradeOrderId = item.id
item.vvTradeOrderLineList = item.vvTradeOrderLineDOList.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;
.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-actions-item {
width: 100%;
}
.goods-actions-item + .goods-actions-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>