feat(订单模块): 添加批量发出功能及进度对话框

- 新增筛选单本书的复选框选项
- 添加批量发出按钮,支持多选订单发出
- 实现批量发出进度对话框,显示处理状态和失败明细
- 优化订单数据处理逻辑,提升用户体验
This commit is contained in:
2026-07-03 13:36:26 +08:00
parent 64b2f08e88
commit 72c1684f30
2 changed files with 608 additions and 18 deletions

View File

@@ -86,19 +86,44 @@
</div>
</el-form-item>
<el-form-item>
<div class="block">
<el-checkbox size="mini"
align="right" v-model="dataForm.showOne"> 筛选单本书
</el-checkbox>
</div>
</el-form-item>
<el-form-item>
<el-button @click="pageIndex = 1;getDataList()">查询</el-button>
</el-form-item>
</el-form>
<div class="buttonGroup">
<span v-if="tabChange.tabActiveName == 1" style="display: inline-block;margin-bottom: 20px;">
<div v-if="tabChange.tabActiveName == 1" style="display: inline-block; margin-bottom: 20px;">
<el-badge :value="mergeList.length" class="item">
<router-link :to="{ path: 'buyorder-mergeorder', query: {} }">
<el-button style="" size="mini" v-if="isAuth('book:buyorder')" type="primary"
:disabled="mergeList.length > 0 ? false : true">合并发出</el-button>
</router-link>
</el-badge>
</span>
<el-button
style="margin-left: 10px;"
size="mini"
v-if="isAuth('book:buyorder')"
type="primary"
:disabled="checkedOrders.length <= 0"
:loading="batchDeliverLoading"
@click="batchDeliver">批量发出</el-button>
<div style="margin-top: 8px;">
<el-checkbox
:disabled="multipleDisabled"
:indeterminate="checkboxGroup.isIndeterminate"
v-model="checkboxGroup.checkAll"
class="checkAll showLabel"
size="mini"
@change="handleCheckAllChange">全选</el-checkbox>
</div>
</div>
<span style="" v-if="tabChange.tabActiveName == 2 && tabChange.isPrint == 1">
<el-button style="margin-left: 10px;" size="mini" v-if="isAuth('book:buyorder')" type="warning"
:disabled="SheetSelectionList.length <= 0" @click="printHandle(null,null,'piliang')">批量打印</el-button>
@@ -589,6 +614,111 @@
<el-button type="primary" @click="refundDetailVisible = false"> </el-button>
</span>
</el-dialog>
<el-dialog
title=""
:visible.sync="batchDeliverProgressVisible"
width="560px"
custom-class="batch-deliver-progress-dialog"
append-to-body
:close-on-click-modal="false"
:close-on-press-escape="false"
:show-close="batchDeliverProgress.finished"
@close="handleBatchDeliverProgressClose">
<div class="batch-deliver-progress-panel">
<div class="batch-deliver-hero" :class="'is-' + batchDeliverStatusType">
<div class="batch-deliver-hero-bg"></div>
<div class="batch-deliver-hero-content">
<div class="batch-deliver-status-icon">
<i v-if="!batchDeliverProgress.finished" class="el-icon-loading"></i>
<i v-else-if="batchDeliverProgress.failCount > 0" class="el-icon-warning-outline"></i>
<i v-else class="el-icon-circle-check"></i>
</div>
<div class="batch-deliver-status-copy">
<div class="batch-deliver-status-title">{{ batchDeliverStatusTitle }}</div>
<div class="batch-deliver-status-desc">{{ batchDeliverStatusDesc }}</div>
</div>
</div>
</div>
<div class="batch-deliver-progress-main">
<div class="batch-deliver-progress-ring">
<el-progress
type="circle"
:percentage="batchDeliverPercent"
:width="128"
:stroke-width="10"
:color="batchDeliverProgressColor">
</el-progress>
</div>
<div class="batch-deliver-stats-grid">
<div class="batch-deliver-stat-card is-total">
<div class="stat-icon"><i class="el-icon-s-order"></i></div>
<div class="stat-meta">
<div class="stat-value">{{ batchDeliverProgress.totalCount }}</div>
<div class="stat-label">订单总数</div>
</div>
</div>
<div class="batch-deliver-stat-card is-processed">
<div class="stat-icon"><i class="el-icon-refresh-right"></i></div>
<div class="stat-meta">
<div class="stat-value">{{ batchDeliverProgress.processedCount }}</div>
<div class="stat-label">已处理</div>
</div>
</div>
<div class="batch-deliver-stat-card is-success">
<div class="stat-icon"><i class="el-icon-circle-check"></i></div>
<div class="stat-meta">
<div class="stat-value">{{ batchDeliverProgress.successCount }}</div>
<div class="stat-label">成功</div>
</div>
</div>
<div class="batch-deliver-stat-card is-fail">
<div class="stat-icon"><i class="el-icon-circle-close"></i></div>
<div class="stat-meta">
<div class="stat-value">{{ batchDeliverProgress.failCount }}</div>
<div class="stat-label">失败</div>
</div>
</div>
</div>
</div>
<div v-if="batchDeliverProgress.failMessage" class="batch-deliver-alert is-error">
<i class="el-icon-warning"></i>
<span>{{ batchDeliverProgress.failMessage }}</span>
</div>
<div v-if="!batchDeliverProgress.finished" class="batch-deliver-alert is-info">
<i class="el-icon-time"></i>
<span>后台正在处理发货任务请勿关闭页面</span>
</div>
<div v-if="batchDeliverProgress.failedItems.length" class="batch-deliver-failed-section">
<div class="batch-deliver-failed-header">
<span class="batch-deliver-failed-title">失败明细</span>
<span class="batch-deliver-failed-count"> {{ batchDeliverProgress.failedItems.length }} </span>
</div>
<el-table
:data="batchDeliverProgress.failedItems"
size="mini"
class="batch-deliver-failed-table"
max-height="220">
<el-table-column prop="orderSn" label="订单编号" min-width="150"></el-table-column>
<el-table-column prop="failMessage" label="失败原因" min-width="200" show-overflow-tooltip></el-table-column>
</el-table>
</div>
</div>
<span slot="footer" class="batch-deliver-progress-footer">
<el-button
type="primary"
size="small"
round
:disabled="!batchDeliverProgress.finished"
@click="closeBatchDeliverProgressDialog">
{{ batchDeliverProgress.finished ? '完成' : '处理中...' }}
</el-button>
</span>
</el-dialog>
<el-pagination v-if="tabChange.isPrint != 1" @size-change="sizeChangeHandle" @current-change="currentChangeHandle"
:current-page="pageIndex" :page-sizes="[20, 40, 60, 100, 150]" :page-size="pageSize" :total="totalPage"
layout="total, sizes, prev, pager, next, jumper">
@@ -646,20 +776,35 @@
dataForm: {
key: '',
time: '',
productName: ''
productName: '',
filterPresale: false,
showOne: false
},
PrintSheetList: [], //打印列表
sheetListLoading: false,
selectedPrintList: [], // 选中的面单列表
mergeList: [], // 检查订单可合并项数组
checkboxGroup: {
isIndeterminate: true,
isIndeterminate: false,
checkAll: false
},
multipleDisabled: false, // 待发出多选是否可用
//childrenChecked:[],
checkedOrders: [], // 新的筛选
fullscreenLoading: false,
batchDeliverLoading: false,
batchDeliverProgressVisible: false,
batchDeliverPollTimer: null,
batchDeliverProgress: {
taskId: null,
totalCount: 0,
successCount: 0,
failCount: 0,
processedCount: 0,
finished: false,
failMessage: null,
failedItems: []
},
deliverDetailVisible: false,
splitDeliverVisible: false,
mergeDliverVisible: false,
@@ -824,7 +969,36 @@
return n > 0 ? n : 0
}
return paidNum
},
batchDeliverPercent() {
const { processedCount, totalCount } = this.batchDeliverProgress
if (!totalCount) return 0
return Math.min(100, Math.round((processedCount / totalCount) * 100))
},
batchDeliverStatusType() {
if (!this.batchDeliverProgress.finished) return 'processing'
if (this.batchDeliverProgress.failCount > 0) return 'warning'
return 'success'
},
batchDeliverStatusTitle() {
if (!this.batchDeliverProgress.finished) return '正在批量发出'
if (this.batchDeliverProgress.failCount > 0) return '发出完成(部分失败)'
return '全部发出成功'
},
batchDeliverStatusDesc() {
if (!this.batchDeliverProgress.finished) {
return `已完成 ${this.batchDeliverProgress.processedCount} / ${this.batchDeliverProgress.totalCount}`
}
return `成功 ${this.batchDeliverProgress.successCount} 单,失败 ${this.batchDeliverProgress.failCount}`
},
batchDeliverProgressColor() {
if (!this.batchDeliverProgress.finished) return '#409EFF'
if (this.batchDeliverProgress.failCount > 0) return '#E6A23C'
return '#67C23A'
}
},
beforeDestroy() {
this.stopBatchDeliverPolling()
},
methods: {
computedVipType(userVips) {
@@ -1073,15 +1247,27 @@
});
},
// 全选/全不选
// 获取可勾选的待发出订单 id
getSelectableOrderIds() {
return this.dataList
.filter(item => item.isSend != 1)
.map(item => item.orderId)
},
// 同步全选复选框状态
syncCheckAllState() {
const selectableIds = this.getSelectableOrderIds()
const checkedCount = this.checkedOrders.length
this.checkboxGroup.checkAll = checkedCount > 0 && checkedCount === selectableIds.length
this.checkboxGroup.isIndeterminate = checkedCount > 0 && checkedCount < selectableIds.length
},
// 全选/取消全选
handleCheckAllChange(val) {
// console.log(this.selectOptions)------------
let newidList = this.dataList.map(item => item.orderId);
this.checkedOrders = newidList
this.checkboxGroup.isIndeterminate = !this.checkboxGroup.isIndeterminate;
if (this.checkboxGroup.isIndeterminate) {
if (val) {
this.checkedOrders = this.getSelectableOrderIds()
} else {
this.checkedOrders = []
}
this.checkboxGroup.isIndeterminate = false
},
// 添加-更新-订单备注
editBeizhu(val) {
@@ -1121,6 +1307,7 @@
"pageIndex": this.pageIndex,
"pageSize": this.pageSize,
"filterPresale": this.dataForm.filterPresale?1:0,
"showOne": this.dataForm.showOne ? 1 : 0,
"orderType": this.orderType
})
}).then(({data}) => {
@@ -1130,6 +1317,9 @@
this.totalPage = data.result.total
if (this.tabChange.tabActiveName === '1') { // 判断可选按钮是否可用
this.isMultipleDisabled()
this.checkedOrders = []
this.checkboxGroup.checkAll = false
this.checkboxGroup.isIndeterminate = false
}
//如果是名医精彩 出现导出按钮
if(this.orderType=='mingyijingcai'){
@@ -1507,13 +1697,120 @@
this.setDeliverVisible = false
this.getDataList()
this.checkedOrders = []
this.isIndeterminate = true
this.checkboxGroup.checkAll = false
this.checkboxGroup.isIndeterminate = false
},
// 关闭面单预览
closeSheetDliverDialog(val) {
this.sheetVisible = val
},
// 批量发出
batchDeliver() {
if (!this.checkedOrders.length) {
this.$message.warning('请先勾选要发出的订单')
return
}
this.$confirm(`共选中了${this.checkedOrders.length}条订单,是否批量发出?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.batchDeliverLoading = true
this.$http({
url: this.$http.adornUrl('/book/buyOrder/batchDelivery'),
method: 'post',
data: this.checkedOrders,
timeout: 0
}).then(({ data }) => {
if (data && data.code === 0) {
if (data.taskId) {
this.checkedOrders = []
this.checkboxGroup.checkAll = false
this.checkboxGroup.isIndeterminate = false
this.openBatchDeliverProgress(data)
} else {
this.$message.success('批量发出成功')
this.checkedOrders = []
this.checkboxGroup.checkAll = false
this.checkboxGroup.isIndeterminate = false
this.getDataList()
}
} else if (data) {
if (data.msg === '订单列表不能为空') {
this.$message.error('订单列表不能为空')
} else {
this.$message.error(`以下订单不符合发出条件:${data.msg}`)
}
}
}).catch(() => {
this.$message.error('批量发出失败,请稍后重试')
}).finally(() => {
this.batchDeliverLoading = false
})
}).catch(() => {})
},
openBatchDeliverProgress(submitData) {
this.batchDeliverProgress = {
taskId: submitData.taskId,
totalCount: submitData.totalCount || 0,
successCount: 0,
failCount: 0,
processedCount: 0,
finished: false,
failMessage: null,
failedItems: []
}
this.batchDeliverProgressVisible = true
this.pollBatchDeliveryProgress()
this.batchDeliverPollTimer = setInterval(() => {
this.pollBatchDeliveryProgress()
}, 1000)
},
pollBatchDeliveryProgress() {
if (!this.batchDeliverProgress.taskId) return
this.$http({
url: this.$http.adornUrl('/book/buyOrder/getBatchDeliveryProgress'),
method: 'get',
params: this.$http.adornParams({
taskId: this.batchDeliverProgress.taskId
})
}).then(({ data }) => {
if (data && data.code === 0) {
this.batchDeliverProgress = {
...this.batchDeliverProgress,
totalCount: data.totalCount,
successCount: data.successCount,
failCount: data.failCount,
processedCount: data.processedCount,
finished: data.finished,
failMessage: data.failMessage,
failedItems: data.failedItems || []
}
if (data.finished) {
this.stopBatchDeliverPolling()
this.getDataList()
if (data.failCount > 0) {
this.$message.warning(`批量发出完成,成功 ${data.successCount} 条,失败 ${data.failCount}`)
} else {
this.$message.success('批量发出完成')
}
}
}
}).catch(() => {})
},
stopBatchDeliverPolling() {
if (this.batchDeliverPollTimer) {
clearInterval(this.batchDeliverPollTimer)
this.batchDeliverPollTimer = null
}
},
closeBatchDeliverProgressDialog() {
this.batchDeliverProgressVisible = false
this.stopBatchDeliverPolling()
},
handleBatchDeliverProgressClose() {
this.stopBatchDeliverPolling()
},
setDeliver() {
this.setDeliverVisible = true
},
@@ -1590,8 +1887,8 @@
this.setDeliverVisible = true
},
handleCheckedChange(val) {
console.log(val)
this.checkedOrders = val
this.syncCheckAllState()
},
//名医精彩-导出
handleExport(){
@@ -1646,7 +1943,8 @@
watch: {
tabChange: {
handler(val, oldVal) {
this.dataForm.filterPresale=false
this.dataForm.filterPresale = false
this.dataForm.showOne = false
//console.log(val)
this.getDataList()
if (this.tabChange.tabActiveName === '' || this.tabChange.tabActiveName === '0' || this.tabChange
@@ -1679,6 +1977,28 @@
text-align: left;
border-bottom: 1px solid #f1f1f1;
}
.batch-deliver-progress-dialog {
border-radius: 12px;
overflow: hidden;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.12);
}
.batch-deliver-progress-dialog .el-dialog__header {
display: none;
padding: 0;
border-bottom: none;
}
.batch-deliver-progress-dialog .el-dialog__body {
padding: 0;
}
.batch-deliver-progress-dialog .el-dialog__footer {
padding: 16px 24px 24px;
border-top: 1px solid #f0f2f5;
text-align: center;
}
</style>
<style lang="less" scoped>
.booksUl {
@@ -2114,4 +2434,277 @@
font-size: 13px;
}
}
/deep/ .batch-deliver-progress-dialog {
.el-dialog__header,
.el-dialog__body,
.el-dialog__footer {
box-sizing: border-box;
}
}
.batch-deliver-progress-panel {
background: #fff;
}
.batch-deliver-hero {
position: relative;
padding: 24px 24px 20px;
overflow: hidden;
&.is-processing {
.batch-deliver-hero-bg {
background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
}
.batch-deliver-status-icon {
background: rgba(255, 255, 255, 0.18);
color: #fff;
}
}
&.is-success {
.batch-deliver-hero-bg {
background: linear-gradient(135deg, #67c23a 0%, #95d475 100%);
}
.batch-deliver-status-icon {
background: rgba(255, 255, 255, 0.18);
color: #fff;
}
}
&.is-warning {
.batch-deliver-hero-bg {
background: linear-gradient(135deg, #e6a23c 0%, #f3c06b 100%);
}
.batch-deliver-status-icon {
background: rgba(255, 255, 255, 0.18);
color: #fff;
}
}
}
.batch-deliver-hero-bg {
position: absolute;
inset: 0;
z-index: 0;
}
.batch-deliver-hero-content {
position: relative;
z-index: 1;
display: flex;
align-items: center;
gap: 16px;
}
.batch-deliver-status-icon {
width: 52px;
height: 52px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 26px;
flex-shrink: 0;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08);
}
.batch-deliver-status-copy {
min-width: 0;
color: #fff;
}
.batch-deliver-status-title {
font-size: 18px;
font-weight: 600;
line-height: 1.4;
margin-bottom: 4px;
}
.batch-deliver-status-desc {
font-size: 13px;
opacity: 0.92;
line-height: 1.5;
}
.batch-deliver-progress-main {
display: flex;
align-items: center;
gap: 20px;
padding: 24px;
background: linear-gradient(180deg, #fafbfc 0%, #fff 100%);
}
.batch-deliver-progress-ring {
flex-shrink: 0;
text-align: center;
}
.batch-deliver-progress-ring-label {
margin-top: 8px;
font-size: 12px;
color: #909399;
}
.batch-deliver-stats-grid {
flex: 1;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
min-width: 0;
}
.batch-deliver-stat-card {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 14px;
border-radius: 10px;
background: #fff;
border: 1px solid #ebeef5;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
transition: transform 0.2s ease, box-shadow 0.2s ease;
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
}
.stat-icon {
width: 34px;
height: 34px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
flex-shrink: 0;
}
.stat-value {
font-size: 20px;
font-weight: 700;
line-height: 1.2;
color: #303133;
}
.stat-label {
margin-top: 2px;
font-size: 12px;
color: #909399;
line-height: 1.2;
}
&.is-total .stat-icon {
background: #ecf5ff;
color: #409eff;
}
&.is-processed .stat-icon {
background: #f4f4f5;
color: #909399;
}
&.is-success .stat-icon {
background: #f0f9eb;
color: #67c23a;
}
&.is-fail .stat-icon {
background: #fef0f0;
color: #f56c6c;
}
}
.batch-deliver-alert {
display: flex;
align-items: flex-start;
gap: 8px;
margin: 0 24px 16px;
padding: 10px 12px;
border-radius: 8px;
font-size: 13px;
line-height: 1.5;
i {
margin-top: 2px;
flex-shrink: 0;
}
&.is-info {
background: #f4f8ff;
color: #409eff;
border: 1px solid #d9ecff;
}
&.is-error {
background: #fef0f0;
color: #f56c6c;
border: 1px solid #fde2e2;
}
}
.batch-deliver-failed-section {
margin: 0 24px 24px;
padding: 14px;
border-radius: 10px;
background: #fafafa;
border: 1px solid #f0f0f0;
}
.batch-deliver-failed-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.batch-deliver-failed-title {
font-size: 14px;
font-weight: 600;
color: #303133;
}
.batch-deliver-failed-count {
font-size: 12px;
color: #909399;
background: #fff;
padding: 2px 8px;
border-radius: 10px;
border: 1px solid #ebeef5;
}
.batch-deliver-failed-table {
border-radius: 8px;
overflow: hidden;
/deep/ .el-table {
background: transparent;
}
/deep/ .el-table th {
background: #f5f7fa;
color: #606266;
font-weight: 600;
}
/deep/ .el-table td,
/deep/ .el-table th {
padding: 8px 0;
}
}
.batch-deliver-progress-footer {
display: block;
width: 100%;
text-align: center;
.el-button {
min-width: 120px;
padding: 10px 28px;
}
}
</style>

View File

@@ -387,7 +387,7 @@ this.$emit('delete',[row])
this.$forceUpdate();
},
getRowKeys(row) {
return row.productId;
return row.id;
},
handleSelectionChange(val) {
console.log("🚀 ~ handleSelectionChange ~ val:", val);
@@ -447,9 +447,6 @@ this.$emit('delete',[row])
},
// 获取关联数据列表
async getAssociatedGoodsList() {
console.log("🚀 ~ getAssociatedGoodsList ~ form:", form);
// :currentId="addForm.id"
// currentType="bookLabelId"
var form = { ...this.dataForm };
if (this.currentType == "bookLabelId") {
@@ -476,10 +473,10 @@ this.$emit('delete',[row])
}),
}).then(async ({ data }) => {
if (data && data.code === 0) {
this.dataListLoading = false;
var list = [...data.result.records];
// this.associatedGoodsList = [...data.result.records];
this.dataListLoading = false;
list.forEach((item) => {
item.isEdit = false
})