feat(mer_plat_admin): 平台端配送人员、物流管理与配套能力

新增配送人员/员工接口与页面、物流创建页、Detail 基础组件与 useRefundOrder;增加 FullCalendar、moment 依赖并升级 Vue 至 2.6.12;补充变更说明文档;README 仅保留远程仓库地址,避免将凭据写入仓库。

Made-with: Cursor
This commit is contained in:
AriadenCaseblg
2026-04-12 08:05:41 +08:00
parent ad6fbc30ab
commit 37e08a5a14
11 changed files with 628 additions and 2 deletions

View File

@@ -299,6 +299,13 @@ bash test_integration_consumer.sh # 验证多商户消费结果
mysql -u root -p < test_verify_e2e.sql # 端到端数据确认
```
---
## git 仓库
远程地址:<http://49.235.131.69:3000/scottpan/MER-2.2_2601.git>(凭据请使用个人访问令牌或本地凭据管理,勿写入文档。)
---
## 已知问题与修复记录

13
docs/change-0410.md Normal file
View File

@@ -0,0 +1,13 @@
# 补充修改
## 商户端管理后台中订单管理的功能移植到平台端管理后台
- **已修复**功能说明文档https://doc.crmeb.com/crmebjavalandmer/mer_v2_2/34068
- **已修复**移植过来的功能菜单名称加商户的前缀,区别于平台端管理后台原有的订单管理功能页
- **已修复**不改变平台端管理后台原有的订单管理功能页
- **已修复**在平台端管理后台左侧的订单目录下加入移植过来的功能页面菜单
## 订单相关
- **已修复**1. 新增“订单打印”功能点击订单打印按钮不同于打印小票新打开一个订单详情含商品信息收货信息的单独页面不需要页面layout点击页面上的“打印”按钮可以直接使用浏览器打印
- **已修复**2. 订单打印详情页中的商品相关信息使用eb_sync_order_detail_staging中的product_nameinfo字段的商品详细信息

View File

@@ -42,6 +42,8 @@
},
"dependencies": {
"@babel/parser": "^7.9.6",
"@fullcalendar/resource-timeline": "^6.1.20",
"@fullcalendar/vue": "^6.1.20",
"@riophae/vue-treeselect": "0.4.0",
"async-validator": "^1.11.2",
"axios": "^0.24.0",
@@ -57,6 +59,7 @@
"js-cookie": "2.2.0",
"jsonlint": "1.6.3",
"jszip": "3.2.1",
"moment": "^2.30.1",
"mpvue-calendar": "^2.3.7",
"normalize.css": "7.0.0",
"nprogress": "0.2.0",
@@ -68,7 +71,7 @@
"script-loader": "0.7.2",
"throttle-debounce": "^2.1.0",
"vconsole": "^3.3.2",
"vue": "2.6.10",
"vue": "2.6.12",
"vue-awesome-swiper": "^3.1.3",
"vue-cropper": "^0.5.8",
"vue-echarts": "^4.0.3",
@@ -109,7 +112,7 @@
"svg-sprite-loader": "4.1.3",
"svgo": "1.2.0",
"vue-lazyload": "^1.3.3",
"vue-template-compiler": "2.6.10"
"vue-template-compiler": "2.6.12"
},
"engines": {
"node": ">=8.9",

View File

@@ -0,0 +1,54 @@
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
import request from '@/utils/request';
/**
* @description 商户配送人员分页列表
*/
export function personnelListApi(params) {
return request({
url: '/admin/merchant/delivery/personnel/page',
method: 'get',
params,
});
}
/**
* @description 新增商户配送人员
*/
export function personnelSaveApi(data) {
return request({
url: '/admin/merchant/delivery/personnel/save',
method: 'post',
data,
});
}
/**
* @description 编辑商户配送人员
*/
export function personnelEditApi(data) {
return request({
url: '/admin/merchant/delivery/personnel/edit',
method: 'post',
data,
});
}
/**
* @description 删除商户配送人员
*/
export function personnelDeleteApi(id) {
return request({
url: `admin/merchant/delivery/personnel/delete/${id}`,
method: 'post',
});
}

View File

@@ -0,0 +1,173 @@
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
import request from '@/utils/request';
/**
* 分页列表
* @param
*/
export function employeeRoleList(pram) {
const data = {
page: pram.page,
limit: pram.limit,
keywords: pram.keywords,
status: pram.status,
};
return request({
url: '/admin/merchant/employee/list',
method: 'get',
params: data,
});
}
/**
* 删除
* @param
*/
export function employeeDelRole(id) {
const data = {
id: id,
};
return request({
url: 'admin/merchant/employee/delete',
method: 'get',
params: data,
});
}
/**
* 详情
* @param
*/
export function employeeGetInfo(pram) {
return request({
url: `/admin/merchant/employee/info/${pram}`,
method: 'GET',
});
}
/**
* 新增
* @param
*/
export function employeeAddRole(pram) {
const data = {
avatar: pram.avatar,
name: pram.name,
phone: pram.phone,
role: pram.role,
status: pram.status,
uid: pram.uid,
id: pram.id,
};
return request({
url: '/admin/merchant/employee/save',
method: 'POST',
data: data,
});
}
/**
* 修改
* @param
*/
export function employeeUpdateRole(pram) {
const data = {
avatar: pram.avatar,
name: pram.name,
phone: pram.phone,
role: pram.role,
status: pram.status,
uid: pram.uid,
id: pram.id,
};
return request({
url: '/admin/merchant/employee/update',
method: 'post',
data: data,
});
}
/**
* 员工服务分页列表
* @param
*/
export function serviceStaffListApi(pram) {
const data = {
page: pram.page,
limit: pram.limit,
keywords: pram.keywords,
status: pram.status,
};
return request({
url: '/admin/merchant/service/staff/list',
method: 'get',
params: data,
});
}
/**
* 员工服务删除
* @param
*/
export function serviceStaffDelRoleApi(id) {
return request({
url: `/admin/merchant/service/staff/delete/${id}`,
method: 'post',
});
}
/**
* 员工服务新增
* @param
*/
export function serviceStaffAddApi(pram) {
const data = {
idPhoto: pram.idPhoto,
name: pram.name,
phone: pram.phone,
sort: pram.sort,
status: pram.status,
userId: pram.userId,
id: pram.id,
};
return request({
url: '/admin/merchant/service/staff/save',
method: 'POST',
data: data,
});
}
/**
* 员工服务修改
* @param
*/
export function serviceStaffUpdateApi(pram) {
const data = {
idPhoto: pram.idPhoto,
name: pram.name,
phone: pram.phone,
sort: pram.sort,
status: pram.status,
userId: pram.userId,
id: pram.id,
};
return request({
url: '/admin/merchant/service/staff/update',
method: 'post',
data: data,
});
}
/**
* 员工服务修改状态
* @param
*/
export function serviceStaffStatusApi(id) {
return request({
url: `/admin/merchant/service/staff/status/${id}`,
method: 'post',
});
}

View File

@@ -0,0 +1,64 @@
<template>
<div class="detailHead">
<div class="acea-row row-between headerBox">
<div class="full">
<div class="order_icon"><span class="iconfont" :class="icon"></span></div>
<div class="text">
<div class="title">{{ title }}</div>
<div v-if="titleLable">
<span class="mr20" :class="colClass?colClass:''">{{ titleLable }}</span>
</div>
</div>
</div>
<div class="acea-row justify-content">
<slot name="operation"></slot>
</div>
</div>
<ul v-if="list.length" class="list">
<li class="item" v-for="(item, index) in list" :key="index">
<div class="title">{{ item.label }}</div>
<div :class="item.color">{{ item.value }}</div>
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'DetailHeader',
props: {
// 标题
title: {
type: String,
default: '',
},
// 图标 class
icon: {
type: String,
default: '',
},
// 单号
orderNo: {
type: [String, Number],
default: '',
},
// 单号标签
titleLable: {
type: String,
default: '',
},
colClass: { //样式
type: String,
default: '',
},
// 底部列表 [{label: '', value: '', color: ''}]
list: {
type: Array,
default: () => [],
},
},
};
</script>
<style scoped></style>

View File

@@ -0,0 +1,37 @@
<template>
<div class="detail-info-container">
<div v-for="(section, index) in list" :key="index" class="detailSection">
<div class="title">{{ section.title }}</div>
<ul class="list">
<li v-for="(item, i) in section.list" :key="i" class="item" :class="item.colClass">
<div class="lang" v-if="item.label">{{ item.label }}</div>
<div class="value">
<slot v-if="item.slot" :name="item.slot" :row="item"></slot>
<span v-else :class="item.class">{{ item.value }}</span>
</div>
</li>
</ul>
<!-- 额外的底部内容插槽 -->
<slot v-if="section.bottomSlot" :name="section.bottomSlot"></slot>
</div>
</div>
</template>
<script>
export default {
name: 'DetailInfo',
props: {
list: {
type: Array,
default: () => [],
},
},
};
</script>
<style scoped lang="scss">
.detailSection .item.width100 {
flex: 0 0 100% !important;
width: 100% !important;
}
</style>

View File

@@ -0,0 +1,57 @@
import { couponDeleteApi } from '@/api/product';
import { orderAuditApi, refundOrderReceivingApi } from '@/api/order';
import modalSure from '@/libs/modal-sure';
import { MessageBox, Message } from 'element-ui';
export default function useRefundOrder() {
//商家确认收货
const onConfirmReceipt = (refundOrderNo) => {
return new Promise((resolve, reject) => {
MessageBox.confirm('确定已经收到所有退款商品吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(async () => {
await refundOrderReceivingApi(refundOrderNo).then(() => {
Message.success('确认收货成功');
return resolve();
});
})
.catch(() => {
reject();
Message({
type: 'info',
message: '已取消',
});
});
});
};
//审核通过到店退款
const onApprovedReview = (data) => {
return new Promise((resolve, reject) => {
MessageBox.confirm('您确定同意此退款单吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(async () => {
await orderAuditApi(data).then(() => {
Message.success('审核成功');
return resolve();
});
})
.catch(() => {
reject();
Message({
type: 'info',
message: '已取消',
});
});
});
};
return {
onConfirmReceipt,
onApprovedReview,
};
}

View File

@@ -0,0 +1,116 @@
<template>
<el-dialog :title="title" :visible.sync="dialogVisible" width="540px" append-to-body :before-close="handleResetForm">
<el-form
v-if="dialogVisible && formValidate"
ref="formValidate"
class="formValidate"
:model="formValidate"
:rules="rules"
@submit.native.prevent
label-width="80px"
>
<el-form-item label="配送人员:" prop="personnelName">
<el-input
v-model.trim="formValidate.personnelName"
:maxlength="16"
placeholder="请输入配送人员姓名"
size="small"
clearable
>
</el-input>
<div class="from-tips mb5">订单采用商家直接配送的方式发货根据配送人员的姓名来进行选择</div>
</el-form-item>
<el-form-item label="联系电话:" prop="personnelPhone">
<el-input v-model.trim="formValidate.personnelPhone" placeholder="请输入配送人员联系电话"></el-input>
<div class="from-tips mb5">订单采用商家直接配送的方式发货后用户可通过手机号码联系该配送员</div>
</el-form-item>
<el-form-item label="排序:">
<el-input-number
v-model.trim="formValidate.sort"
:min="0"
:max="99"
:step="1"
step-strictly
placeholder="请输入排序"
label="排序"
></el-input-number>
<div class="from-tips mb5">请输入0~99的数字数字越大越靠前</div>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="handleResetForm"> </el-button>
<el-button type="primary" @click="handleSure" :loading="loadingBtn"> </el-button>
</span>
</el-dialog>
</template>
<script>
import { expressRelateApi } from '@/api/logistics';
import { useLogisticsAllList } from '@/hooks/use-order';
import { validatePhone } from '@/utils/toolsValidate';
import { personnelEditApi, personnelSaveApi } from '@/api/deliveryPersonnel';
import { defaultData } from '@/views/systemSetting/deliveryPersonnel/default';
export default {
name: 'CreatPersonnel',
props: {
dialogVisible: {
type: Boolean,
default: false,
},
editData: {
type: Object,
default: {},
},
},
watch: {
editData: {
handler(nVal, oVal) {
if (nVal) {
this.formValidate = this.editData;
this.title = this.formValidate.id ? '修改配送员' : '新增配送员';
}
},
deep: true,
},
},
data() {
return {
title: '',
formValidate: Object.assign({}, defaultData),
loadingBtn: false,
rules: {
personnelName: [{ required: true, message: '请输入配送人员姓名', trigger: 'blue' }],
personnelPhone: [{ required: true, validator: validatePhone, trigger: 'blur' }],
},
};
},
mounted() {},
methods: {
//取消
handleResetForm() {
this.$emit('handlerCloseFrom');
this.$refs.formValidate.resetFields();
},
// 提交
handleSure() {
this.$refs.formValidate.validate(async (valid) => {
if (valid) {
try {
this.loadingBtn = true;
const data = this.formValidate.id
? await personnelEditApi(this.formValidate)
: await personnelSaveApi(this.formValidate);
if (data) this.$message.success(data);
this.$emit('handlerSuccessSubmit');
this.handleResetForm();
this.loadingBtn = false;
} catch (e) {
this.loadingBtn = false;
}
}
});
},
},
};
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,6 @@
export const defaultData = {
id: 0,
personnelName: '',
personnelPhone: '',
sort: 0,
};

View File

@@ -0,0 +1,96 @@
<template>
<el-dialog
title="新增物流公司"
:visible.sync="dialogVisible"
width="540px"
append-to-body
:before-close="handleResetForm"
>
<el-form
v-if="dialogVisible"
ref="formValidate"
class="formValidate"
:model="formValidate"
:rules="rules"
@submit.native.prevent
label-width="80px"
>
<el-form-item label="物流公司:" required prop="expressId">
<el-select v-model="formValidate.expressId" placeholder="请选择" clearable filterable style="width: 100%">
<el-option v-for="item in expressAllList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="handleResetForm"> </el-button>
<el-button type="primary" @click="handleSure" :loading="loadingBtn"> </el-button>
</span>
</el-dialog>
</template>
<script>
import { expressRelateApi } from '@/api/logistics';
import { useLogisticsAllList } from '@/hooks/use-order';
export default {
props: {
datekey: {
type: Number,
default: 0,
},
},
watch: {
datekey() {
this.dialogVisible = true;
if (localStorage.getItem('expressAllList'))
this.expressAllList = JSON.parse(localStorage.getItem('expressAllList'));
},
},
data() {
return {
dialogVisible: false,
formValidate: {
expressId: null,
},
expressAllList: [],
loadingBtn: false,
rules: {
expressId: [{ required: true, message: '请选择物流公司', trigger: 'change' }],
},
};
},
mounted() {
if (localStorage.getItem('expressAllList'))
this.expressAllList = JSON.parse(localStorage.getItem('expressAllList'));
},
methods: {
// 物流公司列表
async getExpressList() {
this.expressAllList = await useLogisticsAllList();
},
//取消
handleResetForm() {
this.dialogVisible = false;
this.$refs.formValidate.resetFields();
},
// 提交
handleSure() {
this.$refs.formValidate.validate((valid) => {
if (valid) {
expressRelateApi(this.formValidate)
.then(async (res) => {
this.$message.success('新增成功');
this.$emit('handlerSuccessSubmit');
this.handleResetForm();
this.loadingBtn = false;
})
.catch((res) => {
this.loadingBtn = false;
});
}
});
},
},
};
</script>
<style scoped lang="scss"></style>