mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-ui.git
synced 2026-11-06 17:09:08 +08:00
refactor(goods): 重构商品管理模块的API接口和页面结构
- 移除manager端的预约改期日志相关API接口 - 移除seller端的预约改期相关API接口 - 更新seller端API请求方法,将putRequest替换为putRequestWithNoForm - 修改服务人员更新接口,使用POST方式并传入ID参数 - 重构seller端商品搜索表单,增加SKU名称、SKU货号、店铺分类等搜索条件 - 新增商品创建时间范围选择器和库存状态筛选功能 - 优化商品表格列结构,调整销售模式与商品类型显示逻辑 - 添加商品复制功能按钮 - 重构manager端商品详情页面,实现新的详情展示布局 - 新增商品审核弹窗和时段可约数量抽屉组件 - 优化商品详情页的商品参数展示逻辑 - 添加预约商品相关的特殊处理逻辑
This commit is contained in:
@@ -147,6 +147,7 @@ LILISHOP 是基于 Spring Boot / Spring Cloud / Vue / Uniapp 开发的 Java 开
|
||||
- ✅ 卡密商品
|
||||
- ✅ 二级分销
|
||||
- ✅ 定时任务管理
|
||||
- ✅ 服务商品
|
||||
- 🔜 主题色管理
|
||||
- 🔜 店铺结算汇总
|
||||
- 🔜 支付方式汇总
|
||||
|
||||
@@ -30,13 +30,16 @@
|
||||
>
|
||||
<div class="order-header">
|
||||
<div>
|
||||
<div>{{ filterOrderStatus(order.orderStatus) }}</div>
|
||||
<div>
|
||||
<div>{{ order.isAfterSale ? filterAfterSaleStatus(order.serviceStatus) : filterOrderStatus(order.orderStatus) }}</div>
|
||||
<div v-if="order.isAfterSale">
|
||||
售后单号:{{ order.serviceSn }} {{ order.createTime }}
|
||||
</div>
|
||||
<div v-else>
|
||||
订单号:{{ order.sn }} {{order.createTime}}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<el-button v-if="order.orderStatus === 'COMPLETED'" @click="delOrder(order.sn)" class="del-btn mr_10 fontsize_16" style="margin-top:-5px;" type="text" icon="ios-trash-outline" size="small"></el-button>
|
||||
<el-button v-if="!order.isAfterSale && order.orderStatus === 'COMPLETED'" @click="delOrder(order.sn)" class="del-btn mr_10 fontsize_16" style="margin-top:-5px;" type="text" icon="ios-trash-outline" size="small"></el-button>
|
||||
<span>{{ $filters.unitPrice(order.flowPrice, "¥") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -66,7 +69,10 @@
|
||||
<div>
|
||||
<span @click="shopPage(order.storeId)">{{ order.storeName }}</span>
|
||||
</div>
|
||||
<div class="order-actions">
|
||||
<div class="order-actions" v-if="order.isAfterSale">
|
||||
<el-button @click="goAfterSaleDetail(order.serviceSn)" type="warning" size="small">售后详情</el-button>
|
||||
</div>
|
||||
<div class="order-actions" v-else>
|
||||
<!-- 订单基础操作 -->
|
||||
<el-button @click="orderDetail(order.sn)" type="warning" size="small">订单详情</el-button>
|
||||
<el-button @click="handleCancelOrder(order.sn)" type="danger" v-if="order.allowOperationVO.cancel" size="small">取消订单</el-button>
|
||||
@@ -99,13 +105,16 @@
|
||||
>
|
||||
<div class="order-header">
|
||||
<div>
|
||||
<div>{{ filterOrderStatus(order.orderStatus) }}</div>
|
||||
<div>
|
||||
<div>{{ order.isAfterSale ? filterAfterSaleStatus(order.serviceStatus) : filterOrderStatus(order.orderStatus) }}</div>
|
||||
<div v-if="order.isAfterSale">
|
||||
售后单号:{{ order.serviceSn }} {{ order.createTime }}
|
||||
</div>
|
||||
<div v-else>
|
||||
订单号:{{ order.sn }} {{order.createTime}}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<el-button v-if="order.orderStatus === 'COMPLETED'" @click="delOrder(order.sn)" class="del-btn mr_10 fontsize_16" style="margin-top:-5px;" type="text" icon="ios-trash-outline" size="small"></el-button>
|
||||
<el-button v-if="!order.isAfterSale && order.orderStatus === 'COMPLETED'" @click="delOrder(order.sn)" class="del-btn mr_10 fontsize_16" style="margin-top:-5px;" type="text" icon="ios-trash-outline" size="small"></el-button>
|
||||
<span>{{ $filters.unitPrice(order.flowPrice, "¥") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -135,7 +144,10 @@
|
||||
<div>
|
||||
<span @click="shopPage(order.storeId)">{{ order.storeName }}</span>
|
||||
</div>
|
||||
<div class="order-actions">
|
||||
<div class="order-actions" v-if="order.isAfterSale">
|
||||
<el-button @click="goAfterSaleDetail(order.serviceSn)" type="warning" size="small">售后详情</el-button>
|
||||
</div>
|
||||
<div class="order-actions" v-else>
|
||||
<!-- 订单基础操作 -->
|
||||
<el-button @click="orderDetail(order.sn)" type="warning" size="small">订单详情</el-button>
|
||||
<el-button @click="handleCancelOrder(order.sn)" type="danger" v-if="order.allowOperationVO.cancel" size="small">取消订单</el-button>
|
||||
@@ -178,8 +190,8 @@
|
||||
<script>
|
||||
import { Message, Modal } from "@/utils/message";
|
||||
import { getOrderList, sureReceived, cancelOrder, delOrder } from '@/api/order';
|
||||
import { afterSaleReason } from '@/api/member';
|
||||
import { orderStatusList } from '../enumeration.js'
|
||||
import { afterSaleReason, afterSaleList } from '@/api/member';
|
||||
import { orderStatusList, afterSaleStatusList } from '../enumeration.js'
|
||||
export default {
|
||||
name: 'MyOrder',
|
||||
props: {
|
||||
@@ -204,7 +216,8 @@ export default {
|
||||
},
|
||||
// 状态数组
|
||||
orderStatusList,
|
||||
changeWay: ['全部订单', '待付款', '待收货', '已完成'], // 订单状态
|
||||
afterSaleStatusList,
|
||||
changeWay: ['全部', '待支付', '待发货', '待收货/使用', '评价', '售后'],
|
||||
activeOrderTab: 0,
|
||||
total: 0, // 数据总数
|
||||
spinShow: false, // 加载状态
|
||||
@@ -246,19 +259,11 @@ export default {
|
||||
change (index) {
|
||||
this.activeOrderTab = index;
|
||||
this.params.pageNumber = 1;
|
||||
switch (index) {
|
||||
case 0:
|
||||
this.params.tag = 'ALL'
|
||||
break;
|
||||
case 1:
|
||||
this.params.tag = 'WAIT_PAY'
|
||||
break;
|
||||
case 2:
|
||||
this.params.tag = 'WAIT_ROG'
|
||||
break;
|
||||
case 3:
|
||||
this.params.tag = 'COMPLETE'
|
||||
break;
|
||||
delete this.params.commentStatus;
|
||||
const tags = ['ALL', 'WAIT_PAY', 'WAIT_SHIP', 'WAIT_USE', 'ALL', 'AFTER_SALE'];
|
||||
this.params.tag = tags[index] || 'ALL';
|
||||
if (index === 4) {
|
||||
this.params.commentStatus = 'UNFINISHED';
|
||||
}
|
||||
this.getList()
|
||||
},
|
||||
@@ -271,9 +276,11 @@ export default {
|
||||
window.open(routeUrl.href, '_blank');
|
||||
},
|
||||
orderDetail (sn) {
|
||||
// 跳转订单详情
|
||||
this.$router.push({ name: 'OrderDetail', query: {sn} });
|
||||
},
|
||||
goAfterSaleDetail (sn) {
|
||||
this.$router.push({ name: 'AfterSaleDetail', query: { sn } });
|
||||
},
|
||||
received (sn) { // 确认收货
|
||||
sureReceived(sn).then(res => {
|
||||
if (res.success) {
|
||||
@@ -324,12 +331,31 @@ export default {
|
||||
onCancel: () => {}
|
||||
});
|
||||
},
|
||||
getList () { // 获取订单列表
|
||||
getList () {
|
||||
this.spinShow = true;
|
||||
if (this.activeOrderTab === 5) {
|
||||
afterSaleList({
|
||||
pageNumber: this.params.pageNumber,
|
||||
pageSize: this.params.pageSize,
|
||||
keywords: this.params.keywords,
|
||||
sort: 'createTime',
|
||||
order: 'desc',
|
||||
}).then(res => {
|
||||
this.spinShow = false
|
||||
if (res.success) {
|
||||
this.orderList = (res.result.records || []).map(item => this.mapAfterSaleOrder(item));
|
||||
this.total = res.result.total;
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
let params = JSON.parse(JSON.stringify(this.params))
|
||||
if (params.orderStatus === 'ALL') {
|
||||
delete params.orderStatus
|
||||
}
|
||||
if (params.tag === 'AFTER_SALE') {
|
||||
delete params.tag
|
||||
}
|
||||
getOrderList(params).then(res => {
|
||||
this.spinShow = false
|
||||
if (res.success) {
|
||||
@@ -338,6 +364,27 @@ export default {
|
||||
}
|
||||
});
|
||||
},
|
||||
mapAfterSaleOrder (item) {
|
||||
return {
|
||||
...item,
|
||||
isAfterSale: true,
|
||||
serviceSn: item.sn,
|
||||
sn: item.orderSn,
|
||||
orderStatus: item.serviceStatus,
|
||||
flowPrice: item.applyRefundPrice,
|
||||
orderItems: [
|
||||
{
|
||||
image: item.goodsImage,
|
||||
name: item.goodsName,
|
||||
skuId: item.skuId,
|
||||
goodsId: item.goodsId,
|
||||
goodsPrice: item.flowPrice,
|
||||
num: item.num,
|
||||
},
|
||||
],
|
||||
allowOperationVO: {},
|
||||
}
|
||||
},
|
||||
changePageNum (val) { // 修改页码
|
||||
this.params.pageNumber = val;
|
||||
this.getList()
|
||||
@@ -370,6 +417,10 @@ export default {
|
||||
filterOrderStatus (status) { // 获取订单状态中文
|
||||
const ob = this.orderStatusList.filter(e => { return e.status === status });
|
||||
return ob && ob[0] ? ob[0].name : status
|
||||
},
|
||||
filterAfterSaleStatus (status) {
|
||||
const ob = this.afterSaleStatusList.filter(e => e.status === status);
|
||||
return ob && ob[0] ? ob[0].name : status
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,9 +6,6 @@ import { getRequest, postRequestWithNoForm, putRequestWithNoForm } from "@/libs/
|
||||
export const getManagerWorkOrderPage = (params) =>
|
||||
getRequest("/appointment/work-orders", params);
|
||||
|
||||
export const getManagerRescheduleLogs = (params) =>
|
||||
getRequest("/appointment/reschedule-logs", params);
|
||||
|
||||
export const getAppointmentOrderSettings = () => getRequest("/appointment/settings");
|
||||
|
||||
export const saveAppointmentOrderSettings = (params) =>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -722,7 +722,7 @@ export default {
|
||||
if (type.indexOf("FULL_DISCOUNT") != -1) resultText += "满减 ";
|
||||
if (type.indexOf("POINTS_GOODS") != -1) resultText += "积分商品 ";
|
||||
}
|
||||
return resultText === "" ? "暂无未参与任何促销" : resultText;
|
||||
return resultText === "" ? "未参与" : resultText;
|
||||
},
|
||||
gotoHomes () {
|
||||
return false
|
||||
|
||||
@@ -9,9 +9,10 @@ import {
|
||||
putRequest,
|
||||
deleteRequest,
|
||||
postRequestWithNoForm,
|
||||
putRequestWithNoForm,
|
||||
} from "@/libs/axios";
|
||||
|
||||
/** 店铺预约通用配置(改期审核等) */
|
||||
/** 店铺预约通用配置 */
|
||||
export const getAppointmentSettings = () => getRequest("/appointment/settings");
|
||||
export const saveAppointmentSettings = (params) =>
|
||||
putRequest("/appointment/settings", params);
|
||||
@@ -19,12 +20,12 @@ export const saveAppointmentSettings = (params) =>
|
||||
/** 到店服务设置 */
|
||||
export const getInStoreSettings = () => getRequest("/appointment/settings/in-store");
|
||||
export const saveInStoreSettings = (params) =>
|
||||
putRequest("/appointment/settings/in-store", params);
|
||||
putRequestWithNoForm("/appointment/settings/in-store", params);
|
||||
|
||||
/** 上门服务设置 */
|
||||
export const getOnSiteSettings = () => getRequest("/appointment/settings/on-site");
|
||||
export const saveOnSiteSettings = (params) =>
|
||||
putRequest("/appointment/settings/on-site", params);
|
||||
putRequestWithNoForm("/appointment/settings/on-site", params);
|
||||
|
||||
/** 服务人员 §14 */
|
||||
export const getServiceStaffPage = (params) =>
|
||||
@@ -32,7 +33,7 @@ export const getServiceStaffPage = (params) =>
|
||||
export const createServiceStaff = (params) =>
|
||||
postRequestWithNoForm("/appointment/service-staff", params);
|
||||
export const updateServiceStaff = (id, params) =>
|
||||
putRequest(`/appointment/service-staff/${id}`, params);
|
||||
postRequestWithNoForm("/appointment/service-staff", { ...params, id });
|
||||
export const updateServiceStaffStatus = (id, status) =>
|
||||
putRequest(`/appointment/service-staff/${id}/status`, { status });
|
||||
export const deleteServiceStaff = (id) =>
|
||||
@@ -69,13 +70,3 @@ export const getServiceCalendarDay = (params) =>
|
||||
/** 预约单列表 §24 */
|
||||
export const getAppointmentBookPage = (params) =>
|
||||
getRequest("/appointment/books", params);
|
||||
|
||||
/** 改期日志 */
|
||||
export const getRescheduleLogPage = (params) =>
|
||||
getRequest("/appointment/reschedule-logs", params);
|
||||
export const approveReschedule = (logId) =>
|
||||
postRequest(`/appointment/reschedule/${logId}/approve`);
|
||||
export const rejectReschedule = (logId, params) =>
|
||||
postRequestWithNoForm(`/appointment/reschedule/${logId}/reject`, params);
|
||||
export const merchantRescheduleBook = (bookId, params) =>
|
||||
postRequestWithNoForm(`/appointment/books/${bookId}/reschedule`, params);
|
||||
|
||||
@@ -55,6 +55,11 @@ export const userInfo = params => {
|
||||
export const userMsg = params => {
|
||||
return getRequest('/settings/storeSettings', params)
|
||||
}
|
||||
|
||||
/** 当前店铺首页小程序葵花码 */
|
||||
export const getStoreMpQrcode = () => {
|
||||
return getRequest("/settings/storeSettings/mp-qrcode");
|
||||
};
|
||||
//验证短信验证码
|
||||
export const validateCode = params => {
|
||||
return postRequestWithNoToken("/passport/login/resetByMobile", params);
|
||||
|
||||
@@ -126,6 +126,18 @@ export const orderTake = (sn, verificationCode) => {
|
||||
return putRequest(`/order/order/take/${sn}/${verificationCode}`);
|
||||
};
|
||||
|
||||
export const previewVerification = (code) => {
|
||||
return getRequest(`/order/order/verification/preview`, { code });
|
||||
};
|
||||
|
||||
export const confirmVerification = (code) => {
|
||||
return postRequestWithNoForm(`/order/order/verification/confirm`, { code });
|
||||
};
|
||||
|
||||
export const getVerificationRecords = (params) => {
|
||||
return getRequest(`/order/order/verification/records`, params);
|
||||
};
|
||||
|
||||
// 售后服务单
|
||||
export const afterSaleOrderPage = params => {
|
||||
return getRequest(`/order/afterSale/page`, params);
|
||||
|
||||
@@ -15,6 +15,12 @@ module.exports = {
|
||||
//FIXME 请检查当前高德key创建的日期,如果2021年12月02日之前申请的 无需填写安全密钥
|
||||
aMapSecurityJsCode: "2bd0fbf621881f4c77be74f0e76495f3", // 高德web端js申请的安全密钥
|
||||
aMapKey: "7f11113750315d8543daaf5c3ba353ca", //高德web端js申请的api key
|
||||
/**
|
||||
* 腾讯位置服务 JavaScript API GL,用于上门服务电子围栏
|
||||
* https://lbs.qq.com/webApi/javascriptGL/glGuide/glOverview
|
||||
* 与后端 lili.lbs.key 同一把 Key,控制台需开通 JavaScript API GL
|
||||
*/
|
||||
tMapKey: "ACCBZ-MINKJ-CZ2FN-DJFLN-GKORV-B3BIL",
|
||||
website: "https://www.pickmall.cn", //官网地址
|
||||
aMapSwitch:false, //是否开启高德定位
|
||||
enableCDN: true, //生产环境 是否启用cdn加载 vue等js
|
||||
|
||||
@@ -92,6 +92,16 @@ export const result = [{
|
||||
path: "virtualOrderList",
|
||||
component: "order/order/virtualOrderList",
|
||||
children: null
|
||||
},
|
||||
{
|
||||
name: "orderVerification",
|
||||
level: 2,
|
||||
type: 0,
|
||||
keepAlive: true,
|
||||
title: "订单核销",
|
||||
path: "orderVerification",
|
||||
component: "order/order/orderVerification",
|
||||
children: null
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -114,12 +114,6 @@ export const otherRouter = {
|
||||
name: "appointment-system-form-designer",
|
||||
component: () => import("@/views/appointment/systemFormDesigner.vue")
|
||||
},
|
||||
{
|
||||
path: "appointment/reschedule-logs",
|
||||
title: "改期记录",
|
||||
name: "appointment-reschedule-logs",
|
||||
component: () => import("@/views/appointment/rescheduleLogList.vue")
|
||||
},
|
||||
{
|
||||
path: "add-coupon",
|
||||
title: "店铺优惠券",
|
||||
@@ -162,6 +156,12 @@ export const otherRouter = {
|
||||
name: "order-detail",
|
||||
component: () => import("@/views/order/order/orderDetail.vue")
|
||||
},
|
||||
{
|
||||
path: "orderVerification",
|
||||
title: "订单核销",
|
||||
name: "orderVerification",
|
||||
component: () => import("@/views/order/order/orderVerification.vue")
|
||||
},
|
||||
{
|
||||
path: "/floorList/main",
|
||||
title: "移动装修",
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
align="middle"
|
||||
class="user-dropdown-innercon"
|
||||
>
|
||||
<store-qrcode />
|
||||
<ul class="nav-list">
|
||||
<li class="nav-item" @click="im">
|
||||
<el-tooltip content="联系客服" placement="bottom">
|
||||
@@ -84,6 +85,7 @@ import shrinkableMenu from "./main-components/shrinkable-menu/shrinkable-menu.vu
|
||||
import tagsPageOpened from "./main-components/tags-page-opened.vue";
|
||||
import circleLoading from "@/views/my-components/lili/circle-loading.vue";
|
||||
import configDrawer from "@/views/main-components/config-drawer.vue";
|
||||
import storeQrcode from "@/views/main-components/store-qrcode.vue";
|
||||
import Cookies from "js-cookie";
|
||||
import util from "@/libs/util.js";
|
||||
import { logout } from "@/api/index";
|
||||
@@ -100,6 +102,7 @@ export default {
|
||||
tagsPageOpened,
|
||||
circleLoading,
|
||||
configDrawer,
|
||||
storeQrcode,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
878
seller/src/views/appointment/components/ServiceAreaFence.vue
Normal file
878
seller/src/views/appointment/components/ServiceAreaFence.vue
Normal file
@@ -0,0 +1,878 @@
|
||||
<template>
|
||||
<div class="fence">
|
||||
<div class="fence-toolbar">
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
placeholder="搜索地点,定位后再绘制"
|
||||
clearable
|
||||
@keyup.enter="search"
|
||||
/>
|
||||
<el-button @click="search">搜索</el-button>
|
||||
<el-button :type="mode === 'draw' ? 'primary' : ''" @click="drawing && pointCount ? finishDraw() : startDraw()">
|
||||
{{ drawing && pointCount ? "完成绘制" : "绘制模式" }}
|
||||
</el-button>
|
||||
<el-button :type="mode === 'edit' ? 'primary' : ''" @click="startEdit">编辑模式</el-button>
|
||||
<el-button :disabled="mode !== 'edit'" @click="deleteSelected">删除选中</el-button>
|
||||
</div>
|
||||
<ul v-if="tips.length" class="fence-tips">
|
||||
<li v-for="(tip, index) in tips" :key="index" @click="selectTip(tip)">
|
||||
<p>{{ tip.name }}</p>
|
||||
<p>{{ tip.address }}</p>
|
||||
</li>
|
||||
</ul>
|
||||
<div ref="mapEl" class="fence-map"></div>
|
||||
<div v-if="drawing && pointCount" class="fence-hint">当前图形 {{ pointCount }} 个顶点。</div>
|
||||
<div v-else-if="areaCount" class="fence-hint">已绘制 {{ areaCount }} 个区域。</div>
|
||||
<ul class="fence-desc">
|
||||
<li><span>绘制模式</span>鼠标左键点击及移动即可绘制图形,双击结束绘制。可以绘制多个区域</li>
|
||||
<li><span>编辑模式</span>可以拖拽顶点调整多边形形状,点击边线添加新顶点,地图会自动定位到绘制区域中心</li>
|
||||
<li><span>删除功能</span>编辑模式下选中多边形后点击「删除选中」删除指定区域</li>
|
||||
<li><span>多区域支持</span>支持绘制和编辑多个独立的服务区域,每个区域可以单独编辑和删除</li>
|
||||
<li><span>其他操作</span>ESC 键中断绘制,Ctrl+Z 撤销操作,右键撤销绘制点</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { markRaw } from "vue";
|
||||
import config from "@/config/index";
|
||||
|
||||
const OVERLAY_ID = "polygon";
|
||||
|
||||
export default {
|
||||
name: "ServiceAreaFence",
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
data() {
|
||||
return {
|
||||
keyword: "",
|
||||
tips: [],
|
||||
pointCount: 0,
|
||||
areaCount: 0,
|
||||
drawing: false,
|
||||
mode: "edit",
|
||||
ready: false,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.map = null;
|
||||
this.editor = null;
|
||||
this.polygon = null;
|
||||
this.suggestion = null;
|
||||
this.TMap = null;
|
||||
this.geometryId = "";
|
||||
this.draftRing = [];
|
||||
this.clickRing = [];
|
||||
this.areas = [];
|
||||
this.booting = false;
|
||||
this.alive = true;
|
||||
},
|
||||
watch: {
|
||||
visible(val) {
|
||||
if (!val) {
|
||||
this.destroyMap();
|
||||
return;
|
||||
}
|
||||
this.$nextTick(() => this.scheduleInit());
|
||||
},
|
||||
modelValue(val) {
|
||||
if (!this.ready || val === this.currentJson()) {
|
||||
return;
|
||||
}
|
||||
this.renderPolygons(parsePolygons(val));
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.observer = new ResizeObserver(() => {
|
||||
if (this.map) {
|
||||
this.resizeMap();
|
||||
return;
|
||||
}
|
||||
this.scheduleInit();
|
||||
});
|
||||
if (this.$refs.mapEl) {
|
||||
this.observer.observe(this.$refs.mapEl);
|
||||
}
|
||||
this.scheduleInit();
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.alive = false;
|
||||
if (this.observer) {
|
||||
this.observer.disconnect();
|
||||
}
|
||||
this.destroyMap();
|
||||
},
|
||||
methods: {
|
||||
scheduleInit() {
|
||||
if (this.map || this.booting || !this.visible || !this.alive) {
|
||||
return;
|
||||
}
|
||||
const el = this.$refs.mapEl;
|
||||
if (!el || el.clientWidth < 10 || el.clientHeight < 10) {
|
||||
return;
|
||||
}
|
||||
this.initMap();
|
||||
},
|
||||
initMap() {
|
||||
if (!config.tMapKey) {
|
||||
this.$message.error("未配置腾讯地图 Key");
|
||||
return;
|
||||
}
|
||||
const el = this.$refs.mapEl;
|
||||
if (!el || el.clientWidth < 10 || el.clientHeight < 10) {
|
||||
return;
|
||||
}
|
||||
this.booting = true;
|
||||
loadTMap(config.tMapKey)
|
||||
.then((TMap) => {
|
||||
if (!this.alive || !this.visible) {
|
||||
this.booting = false;
|
||||
return;
|
||||
}
|
||||
const box = this.$refs.mapEl;
|
||||
if (!box || box.clientWidth < 10 || box.clientHeight < 10) {
|
||||
this.booting = false;
|
||||
return;
|
||||
}
|
||||
this.TMap = markRaw(TMap);
|
||||
this.map = markRaw(
|
||||
new TMap.Map(box, {
|
||||
zoom: 11,
|
||||
center: new TMap.LatLng(39.90923, 116.397428),
|
||||
doubleClickZoom: false,
|
||||
})
|
||||
);
|
||||
if (typeof this.map.setDoubleClickZoom === "function") {
|
||||
this.map.setDoubleClickZoom(false);
|
||||
}
|
||||
this.bindFinishGesture(box);
|
||||
const polygonStyle = new TMap.PolygonStyle({
|
||||
color: "rgba(64,158,255,0.25)",
|
||||
showBorder: true,
|
||||
borderColor: "#409EFF",
|
||||
borderWidth: 2,
|
||||
});
|
||||
this.polygon = markRaw(
|
||||
new TMap.MultiPolygon({
|
||||
map: this.map,
|
||||
styles: {
|
||||
default: polygonStyle,
|
||||
highlight: polygonStyle,
|
||||
},
|
||||
geometries: [],
|
||||
})
|
||||
);
|
||||
this.editor = markRaw(
|
||||
new TMap.tools.GeometryEditor({
|
||||
map: this.map,
|
||||
overlayList: [
|
||||
{
|
||||
id: OVERLAY_ID,
|
||||
overlay: this.polygon,
|
||||
drawingStyleId: "highlight",
|
||||
selectedStyleId: "highlight",
|
||||
},
|
||||
],
|
||||
actionMode: TMap.tools.constants.EDITOR_ACTION.INTERACT,
|
||||
activeOverlayId: OVERLAY_ID,
|
||||
selectable: true,
|
||||
snappable: true,
|
||||
enableKeyboardEsc: true,
|
||||
enableUndoDraw: true,
|
||||
})
|
||||
);
|
||||
if (typeof this.editor.setSnapPolyStartPointOnDrawing === "function") {
|
||||
this.editor.setSnapPolyStartPointOnDrawing(true);
|
||||
}
|
||||
if (typeof this.editor.setUndoDrawEnable === "function") {
|
||||
this.editor.setUndoDrawEnable(true);
|
||||
}
|
||||
if (typeof this.editor.setKeyboardEscEnable === "function") {
|
||||
this.editor.setKeyboardEscEnable(true);
|
||||
}
|
||||
this.map.on("click", (event) => this.onMapClick(event));
|
||||
this.editor.on("drawing", (evt) => this.onDrawing(evt));
|
||||
this.editor.on("draw_complete", (geometry) => this.onDrawComplete(geometry));
|
||||
this.editor.on("draw_error", () => {
|
||||
this.$message.warning("围栏不能自相交,请调整顶点后再结束");
|
||||
});
|
||||
this.editor.on("adjust_complete", (geometry) => this.onAdjustComplete(geometry));
|
||||
this.editor.on("delete_complete", (list) => this.onDeleteComplete(list));
|
||||
this.suggestion = markRaw(new TMap.service.Suggestion({ pageSize: 8 }));
|
||||
this.ready = true;
|
||||
this.booting = false;
|
||||
this.renderPolygons(parsePolygons(this.modelValue));
|
||||
this.$nextTick(() => this.resizeMap());
|
||||
})
|
||||
.catch((error) => {
|
||||
this.destroyMap();
|
||||
const text = String(error && error.message ? error.message : error);
|
||||
if (text.includes("far <= 0") || text.includes("near <= 0")) {
|
||||
return;
|
||||
}
|
||||
this.$message.error("腾讯地图加载失败,请检查 Key 是否开通 JavaScript API GL");
|
||||
});
|
||||
},
|
||||
resizeMap() {
|
||||
const el = this.$refs.mapEl;
|
||||
if (!this.map || !el || el.clientWidth < 10 || el.clientHeight < 10) {
|
||||
return;
|
||||
}
|
||||
if (typeof this.map.resize === "function") {
|
||||
this.map.resize();
|
||||
}
|
||||
},
|
||||
search() {
|
||||
const keyword = (this.keyword || "").trim();
|
||||
if (!this.suggestion || !keyword) {
|
||||
this.tips = [];
|
||||
return;
|
||||
}
|
||||
this.suggestion
|
||||
.getSuggestions({ keyword })
|
||||
.then((result) => {
|
||||
this.tips = (result.data || [])
|
||||
.filter((item) => item.location)
|
||||
.map((item) => ({
|
||||
name: item.title,
|
||||
address: item.address || "",
|
||||
location: item.location,
|
||||
}));
|
||||
})
|
||||
.catch(() => {
|
||||
this.tips = [];
|
||||
});
|
||||
},
|
||||
selectTip(tip) {
|
||||
if (!tip || !tip.location || !this.map) {
|
||||
return;
|
||||
}
|
||||
this.map.setCenter(tip.location);
|
||||
this.map.setZoom(15);
|
||||
this.tips = [];
|
||||
},
|
||||
bindFinishGesture(box) {
|
||||
this.unbindFinishGesture();
|
||||
this.mapBox = box;
|
||||
this.onMapDblClick = (event) => {
|
||||
if (!this.drawing || this.pickedCount() < 3) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.finishDraw();
|
||||
};
|
||||
this.onMapContextMenu = (event) => {
|
||||
if (this.mode !== "draw") {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.undoDrawPoint();
|
||||
};
|
||||
box.addEventListener("dblclick", this.onMapDblClick, true);
|
||||
box.addEventListener("contextmenu", this.onMapContextMenu, true);
|
||||
},
|
||||
unbindFinishGesture() {
|
||||
if (this.mapBox && this.onMapDblClick) {
|
||||
this.mapBox.removeEventListener("dblclick", this.onMapDblClick, true);
|
||||
}
|
||||
if (this.mapBox && this.onMapContextMenu) {
|
||||
this.mapBox.removeEventListener("contextmenu", this.onMapContextMenu, true);
|
||||
}
|
||||
this.mapBox = null;
|
||||
this.onMapDblClick = null;
|
||||
this.onMapContextMenu = null;
|
||||
},
|
||||
undoDrawPoint() {
|
||||
if (!this.editor) {
|
||||
return;
|
||||
}
|
||||
if (typeof this.editor.undo === "function") {
|
||||
this.editor.undo();
|
||||
return;
|
||||
}
|
||||
const event = new KeyboardEvent("keydown", {
|
||||
key: "z",
|
||||
code: "KeyZ",
|
||||
ctrlKey: true,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
if (this.mapBox) {
|
||||
this.mapBox.dispatchEvent(event);
|
||||
}
|
||||
document.dispatchEvent(event);
|
||||
},
|
||||
onMapClick(event) {
|
||||
if (!this.drawing) {
|
||||
return;
|
||||
}
|
||||
const point = latLngOf(event && (event.latLng || event.latlng));
|
||||
if (!point) {
|
||||
return;
|
||||
}
|
||||
this.clickRing = dedupeRing((this.clickRing || []).concat([point]));
|
||||
this.pointCount = this.pickedCount();
|
||||
},
|
||||
onDrawing(evt) {
|
||||
const ring = dedupeRing(ringFromPaths(evt && evt.geometry && evt.geometry.paths));
|
||||
if (!ring.length) {
|
||||
return;
|
||||
}
|
||||
this.draftRing = ring;
|
||||
this.pointCount = this.pickedCount();
|
||||
},
|
||||
pickedCount() {
|
||||
return Math.max((this.draftRing || []).length, (this.clickRing || []).length);
|
||||
},
|
||||
pickedRing() {
|
||||
const editorRing = dedupeRing(this.draftRing || []);
|
||||
const clickRing = dedupeRing(this.clickRing || []);
|
||||
if (clickRing.length >= 3) {
|
||||
return clickRing;
|
||||
}
|
||||
return editorRing.length >= clickRing.length ? editorRing : clickRing;
|
||||
},
|
||||
startDraw() {
|
||||
if (!this.editor || !this.polygon) {
|
||||
return;
|
||||
}
|
||||
this.mode = "draw";
|
||||
this.drawing = true;
|
||||
this.pointCount = 0;
|
||||
this.draftRing = [];
|
||||
this.clickRing = [];
|
||||
this.pendingRing = null;
|
||||
this.editor.setActiveOverlay(OVERLAY_ID);
|
||||
if (typeof this.editor.setSelectable === "function") {
|
||||
this.editor.setSelectable(false);
|
||||
}
|
||||
this.editor.setActionMode(this.TMap.tools.constants.EDITOR_ACTION.DRAW);
|
||||
},
|
||||
startEdit() {
|
||||
if (!this.editor) {
|
||||
return;
|
||||
}
|
||||
if (this.drawing && this.pickedCount() >= 3) {
|
||||
this.finishDraw();
|
||||
} else if (typeof this.editor.stop === "function") {
|
||||
try {
|
||||
this.editor.stop();
|
||||
} catch (e) {
|
||||
// 没有进行中的绘制时停止会抛错
|
||||
}
|
||||
}
|
||||
this.mode = "edit";
|
||||
this.drawing = false;
|
||||
this.pointCount = 0;
|
||||
this.draftRing = [];
|
||||
this.clickRing = [];
|
||||
this.pendingRing = null;
|
||||
if (typeof this.editor.setSelectable === "function") {
|
||||
this.editor.setSelectable(true);
|
||||
}
|
||||
this.editor.setActionMode(this.TMap.tools.constants.EDITOR_ACTION.INTERACT);
|
||||
this.fitAll();
|
||||
},
|
||||
finishDraw() {
|
||||
if (!this.editor || !this.polygon || !this.drawing) {
|
||||
return;
|
||||
}
|
||||
const ring = this.pickedRing();
|
||||
if (ring.length < 3) {
|
||||
this.$message.warning("至少点击 3 个顶点后再结束");
|
||||
return;
|
||||
}
|
||||
this.pendingRing = ring;
|
||||
const before = (this.areas || []).length;
|
||||
if (typeof this.editor.stop === "function") {
|
||||
try {
|
||||
this.editor.stop();
|
||||
} catch (e) {
|
||||
// 编辑器中断绘制时可能抛错,随后用已选顶点落图
|
||||
}
|
||||
}
|
||||
if ((this.areas || []).length === before && this.pendingRing) {
|
||||
this.appendArea("fence-" + Date.now(), this.pendingRing);
|
||||
}
|
||||
this.pendingRing = null;
|
||||
},
|
||||
deleteSelected() {
|
||||
if (!this.editor || this.mode !== "edit") {
|
||||
this.$message.warning("请先进入编辑模式并选中区域");
|
||||
return;
|
||||
}
|
||||
const selected = typeof this.editor.getSelectedList === "function" ? this.editor.getSelectedList() || [] : [];
|
||||
if (!selected.length) {
|
||||
this.$message.warning("请先选中要删除的区域");
|
||||
return;
|
||||
}
|
||||
const ids = selected.map((item) => item && item.id).filter(Boolean);
|
||||
if (typeof this.editor.delete === "function") {
|
||||
this.editor.delete();
|
||||
}
|
||||
this.areas = (this.areas || []).filter((area) => !ids.includes(area.id));
|
||||
this.redrawAreas();
|
||||
this.publishAreas();
|
||||
},
|
||||
onDrawComplete(geometry) {
|
||||
if (!this.polygon) {
|
||||
return;
|
||||
}
|
||||
let ring = ringFromGeometry(geometry);
|
||||
if (ring.length < 3) {
|
||||
ring = this.pendingRing || this.pickedRing();
|
||||
}
|
||||
this.pendingRing = null;
|
||||
if (ring.length < 3) {
|
||||
return;
|
||||
}
|
||||
this.appendArea((geometry && geometry.id) || "fence-" + Date.now(), ring);
|
||||
},
|
||||
onAdjustComplete(geometry) {
|
||||
const ring = ringFromGeometry(geometry);
|
||||
const id = geometry && geometry.id;
|
||||
if (!id || ring.length < 3) {
|
||||
return;
|
||||
}
|
||||
const areas = (this.areas || []).slice();
|
||||
const index = areas.findIndex((item) => item.id === id);
|
||||
if (index >= 0) {
|
||||
areas[index] = { id, ring };
|
||||
} else {
|
||||
areas.push({ id, ring });
|
||||
}
|
||||
this.areas = areas;
|
||||
this.publishAreas();
|
||||
},
|
||||
onDeleteComplete(list) {
|
||||
const ids = (list || []).map((item) => item && item.id).filter(Boolean);
|
||||
if (!ids.length) {
|
||||
return;
|
||||
}
|
||||
this.areas = (this.areas || []).filter((area) => !ids.includes(area.id));
|
||||
this.publishAreas();
|
||||
},
|
||||
appendArea(id, ring) {
|
||||
const clean = dedupeRing(ring || []);
|
||||
if (clean.length < 3) {
|
||||
return;
|
||||
}
|
||||
const areas = (this.areas || []).filter((area) => area.id !== id);
|
||||
areas.push({ id, ring: clean });
|
||||
this.areas = areas;
|
||||
this.redrawAreas();
|
||||
this.publishAreas();
|
||||
this.draftRing = [];
|
||||
this.clickRing = [];
|
||||
this.pointCount = 0;
|
||||
this.fitAll();
|
||||
if (this.mode === "draw") {
|
||||
this.drawing = true;
|
||||
if (typeof this.editor.setSelectable === "function") {
|
||||
this.editor.setSelectable(false);
|
||||
}
|
||||
this.editor.setActionMode(this.TMap.tools.constants.EDITOR_ACTION.DRAW);
|
||||
}
|
||||
},
|
||||
redrawAreas() {
|
||||
if (!this.polygon || !this.TMap) {
|
||||
return;
|
||||
}
|
||||
this.polygon.setGeometries(
|
||||
(this.areas || []).map((area) => ({
|
||||
id: area.id,
|
||||
styleId: "highlight",
|
||||
paths: area.ring.map((point) => new this.TMap.LatLng(point.lat, point.lng)),
|
||||
}))
|
||||
);
|
||||
},
|
||||
publishAreas() {
|
||||
const areas = this.areas || [];
|
||||
this.areaCount = areas.length;
|
||||
if (!areas.length) {
|
||||
this.committedJson = "";
|
||||
if (this.modelValue) {
|
||||
this.$emit("update:modelValue", "");
|
||||
}
|
||||
return;
|
||||
}
|
||||
const json = JSON.stringify({
|
||||
paths: areas.map((area) =>
|
||||
area.ring.map((point) => [
|
||||
Number(Number(point.lng).toFixed(6)),
|
||||
Number(Number(point.lat).toFixed(6)),
|
||||
])
|
||||
),
|
||||
});
|
||||
this.committedJson = json;
|
||||
if (json !== (this.modelValue || "")) {
|
||||
this.$emit("update:modelValue", json);
|
||||
}
|
||||
},
|
||||
renderPolygons(polygons) {
|
||||
if (!this.polygon || !this.TMap || !this.editor) {
|
||||
return;
|
||||
}
|
||||
if (typeof this.editor.stop === "function") {
|
||||
try {
|
||||
this.editor.stop();
|
||||
} catch (e) {
|
||||
// 初始化时没有进行中的绘制
|
||||
}
|
||||
}
|
||||
this.areas = (polygons || []).map((ring, index) => ({
|
||||
id: "fence-" + index,
|
||||
ring,
|
||||
}));
|
||||
this.redrawAreas();
|
||||
this.areaCount = this.areas.length;
|
||||
this.committedJson = this.modelValue || "";
|
||||
this.mode = "edit";
|
||||
this.drawing = false;
|
||||
this.pointCount = 0;
|
||||
this.editor.setActiveOverlay(OVERLAY_ID);
|
||||
if (typeof this.editor.setSelectable === "function") {
|
||||
this.editor.setSelectable(true);
|
||||
}
|
||||
this.editor.setActionMode(this.TMap.tools.constants.EDITOR_ACTION.INTERACT);
|
||||
this.fitAll();
|
||||
},
|
||||
fitAll() {
|
||||
if (!this.map || !this.TMap) {
|
||||
return;
|
||||
}
|
||||
const points = [];
|
||||
(this.areas || []).forEach((area) => {
|
||||
(area.ring || []).forEach((point) => points.push(point));
|
||||
});
|
||||
if (points.length < 2) {
|
||||
return;
|
||||
}
|
||||
const bounds = new this.TMap.LatLngBounds();
|
||||
points.forEach((point) => bounds.extend(new this.TMap.LatLng(point.lat, point.lng)));
|
||||
this.map.fitBounds(bounds, { padding: 48 });
|
||||
},
|
||||
currentJson() {
|
||||
return this.committedJson || "";
|
||||
},
|
||||
destroyMap() {
|
||||
this.ready = false;
|
||||
this.booting = false;
|
||||
this.drawing = false;
|
||||
this.unbindFinishGesture();
|
||||
const editor = this.editor;
|
||||
const map = this.map;
|
||||
this.editor = null;
|
||||
this.map = null;
|
||||
this.polygon = null;
|
||||
this.suggestion = null;
|
||||
try {
|
||||
if (editor && typeof editor.destroy === "function") {
|
||||
editor.destroy();
|
||||
}
|
||||
} catch (e) {
|
||||
// 容器已隐藏时,腾讯地图会在销毁过程中抛 far <= 0
|
||||
}
|
||||
try {
|
||||
if (map && typeof map.destroy === "function") {
|
||||
map.destroy();
|
||||
}
|
||||
} catch (e) {
|
||||
// 同上
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function ensureProcessShim() {
|
||||
const current = window.process;
|
||||
if (!current || typeof current !== "object") {
|
||||
window.process = {
|
||||
env: { NODE_ENV: "production" },
|
||||
browser: true,
|
||||
stderr: { isTTY: false },
|
||||
emitWarning() {},
|
||||
};
|
||||
return;
|
||||
}
|
||||
if (!current.env) {
|
||||
current.env = { NODE_ENV: "production" };
|
||||
}
|
||||
if (!current.stderr) {
|
||||
current.stderr = { isTTY: false };
|
||||
}
|
||||
if (typeof current.emitWarning !== "function") {
|
||||
current.emitWarning = function () {};
|
||||
}
|
||||
}
|
||||
|
||||
function loadTMap(key) {
|
||||
if (window.TMap && window.TMap.tools && window.TMap.service) {
|
||||
return Promise.resolve(window.TMap);
|
||||
}
|
||||
if (!loadTMap.pending) {
|
||||
loadTMap.pending = new Promise((resolve, reject) => {
|
||||
ensureProcessShim();
|
||||
const script = document.createElement("script");
|
||||
script.charset = "utf-8";
|
||||
script.async = false;
|
||||
script.src =
|
||||
"https://map.qq.com/api/gljs?v=1.exp&libraries=tools,service&key=" +
|
||||
encodeURIComponent(key);
|
||||
script.onload = () => {
|
||||
if (window.TMap) {
|
||||
resolve(window.TMap);
|
||||
} else {
|
||||
reject(new Error("TMap missing"));
|
||||
}
|
||||
};
|
||||
script.onerror = () => reject(new Error("TMap load failed"));
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
return loadTMap.pending;
|
||||
}
|
||||
|
||||
function dedupeRing(ring) {
|
||||
const result = [];
|
||||
ring.forEach((point) => {
|
||||
const last = result[result.length - 1];
|
||||
if (last && Math.abs(last.lat - point.lat) < 1e-6 && Math.abs(last.lng - point.lng) < 1e-6) {
|
||||
return;
|
||||
}
|
||||
result.push(point);
|
||||
});
|
||||
if (result.length > 3) {
|
||||
const first = result[0];
|
||||
const end = result[result.length - 1];
|
||||
if (Math.abs(first.lat - end.lat) < 1e-6 && Math.abs(first.lng - end.lng) < 1e-6) {
|
||||
result.pop();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function asArray(value) {
|
||||
if (!value) {
|
||||
return [];
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value.getLength === "function" && typeof value.getAt === "function") {
|
||||
const list = [];
|
||||
const size = value.getLength();
|
||||
for (let i = 0; i < size; i += 1) {
|
||||
list.push(value.getAt(i));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function ringFromGeometry(geometry) {
|
||||
if (!geometry) {
|
||||
return [];
|
||||
}
|
||||
const ring = longestRing(geometry.paths || geometry.path || geometry.points || geometry);
|
||||
return dedupeRing(ring);
|
||||
}
|
||||
|
||||
function longestRing(value, depth) {
|
||||
if (!value || depth > 5) {
|
||||
return [];
|
||||
}
|
||||
const point = latLngOf(value);
|
||||
if (point) {
|
||||
return [point];
|
||||
}
|
||||
const list = asArray(value);
|
||||
if (list.length) {
|
||||
const flat = list.map(latLngOf).filter(Boolean);
|
||||
if (flat.length >= 3) {
|
||||
return flat;
|
||||
}
|
||||
let best = flat;
|
||||
list.forEach((item) => {
|
||||
const nested = longestRing(item, (depth || 0) + 1);
|
||||
if (nested.length > best.length) {
|
||||
best = nested;
|
||||
}
|
||||
});
|
||||
return best;
|
||||
}
|
||||
if (typeof value === "object") {
|
||||
return longestRing(value.paths || value.path || value.points, (depth || 0) + 1);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function ringFromPaths(paths) {
|
||||
return dedupeRing(longestRing(paths, 0));
|
||||
}
|
||||
|
||||
function latLngOf(point) {
|
||||
if (!point || typeof point !== "object") {
|
||||
return null;
|
||||
}
|
||||
if (Array.isArray(point)) {
|
||||
return latLngFromPair(point[0], point[1]);
|
||||
}
|
||||
const lat = readNumber(point, ["getLat", "getLatitude"], ["lat", "latitude"]);
|
||||
const lng = readNumber(point, ["getLng", "getLongitude"], ["lng", "longitude"]);
|
||||
if (!Number.isFinite(lat) || !Number.isFinite(lng)) {
|
||||
return null;
|
||||
}
|
||||
return { lat, lng };
|
||||
}
|
||||
|
||||
function latLngFromPair(first, second) {
|
||||
const a = Number(first);
|
||||
const b = Number(second);
|
||||
if (!Number.isFinite(a) || !Number.isFinite(b)) {
|
||||
return null;
|
||||
}
|
||||
if (Math.abs(a) > 90 && Math.abs(b) <= 90) {
|
||||
return { lng: a, lat: b };
|
||||
}
|
||||
if (Math.abs(b) > 90 && Math.abs(a) <= 90) {
|
||||
return { lng: b, lat: a };
|
||||
}
|
||||
return { lng: a, lat: b };
|
||||
}
|
||||
|
||||
function readNumber(point, methods, fields) {
|
||||
for (let i = 0; i < methods.length; i += 1) {
|
||||
const name = methods[i];
|
||||
if (typeof point[name] === "function") {
|
||||
const value = Number(point[name]());
|
||||
if (Number.isFinite(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < fields.length; i += 1) {
|
||||
const value = Number(point[fields[i]]);
|
||||
if (Number.isFinite(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return NaN;
|
||||
}
|
||||
|
||||
function parsePolygons(raw) {
|
||||
if (!raw) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const data = typeof raw === "string" ? JSON.parse(raw) : raw;
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
const source = Array.isArray(data.paths) ? data.paths : Array.isArray(data.path) ? [data.path] : [];
|
||||
return source.map(readRing).filter((ring) => ring.length >= 3);
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function readRing(path) {
|
||||
if (!Array.isArray(path)) {
|
||||
return [];
|
||||
}
|
||||
return dedupeRing(
|
||||
path
|
||||
.map((point) => {
|
||||
if (!Array.isArray(point) || point.length < 2) {
|
||||
return null;
|
||||
}
|
||||
const lng = Number(point[0]);
|
||||
const lat = Number(point[1]);
|
||||
if (!Number.isFinite(lng) || !Number.isFinite(lat)) {
|
||||
return null;
|
||||
}
|
||||
return { lng, lat };
|
||||
})
|
||||
.filter(Boolean)
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fence {
|
||||
position: relative;
|
||||
width: 720px;
|
||||
max-width: 100%;
|
||||
}
|
||||
.fence-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.fence-toolbar .el-input {
|
||||
width: 280px;
|
||||
}
|
||||
.fence-tips {
|
||||
position: absolute;
|
||||
z-index: 1001;
|
||||
left: 0;
|
||||
top: 40px;
|
||||
width: 280px;
|
||||
max-height: 240px;
|
||||
overflow: auto;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
background: #fff;
|
||||
border: 1px solid #e4e7ed;
|
||||
}
|
||||
.fence-tips li {
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.fence-tips li:hover {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
.fence-tips p {
|
||||
margin: 0;
|
||||
}
|
||||
.fence-tips p:last-child {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
.fence-map {
|
||||
width: 100%;
|
||||
height: 480px;
|
||||
border: 1px solid #e4e7ed;
|
||||
}
|
||||
.fence-hint {
|
||||
margin-top: 8px;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.fence-desc {
|
||||
margin: 8px 0 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
.fence-desc span {
|
||||
margin-right: 6px;
|
||||
color: #606266;
|
||||
}
|
||||
</style>
|
||||
@@ -1,24 +1,18 @@
|
||||
<template>
|
||||
<div class="search appointment-settings-page">
|
||||
<el-card>
|
||||
<el-alert type="info" show-icon :closable="false" class="mb_10">
|
||||
配置到店服务履约节点;改期审核等通用项见上门服务设置。
|
||||
</el-alert>
|
||||
<el-form v-loading="loading" :model="form" label-width="140px" style="max-width: 720px">
|
||||
<el-form-item label="用户预约">
|
||||
<el-switch v-model="form.userBooking" disabled />
|
||||
<span class="hint">必要节点,不可关闭</span>
|
||||
<span class="hint">用户预约为必要节点,不可关闭</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="工单展示">
|
||||
<el-form-item label="工单">
|
||||
<el-switch v-model="form.workOrder" />
|
||||
<span class="hint">关闭后 §15 不可见,工单仍会生成</span>
|
||||
<span class="hint">工单开启核销后会生成服务工单,关闭不生成工单,订单状态直接流转已收货、已服务</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="结束服务">
|
||||
<el-switch v-model="form.endService" disabled />
|
||||
<span class="hint">展示态须在工单上结束服务</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="客户改期审核">
|
||||
<el-switch v-model="form.customerRescheduleAuditEnabled" />
|
||||
<span class="hint">服务人员结束服务</span>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="saving" @click="handleSave">保存</el-button>
|
||||
@@ -42,7 +36,6 @@ export default {
|
||||
userBooking: true,
|
||||
workOrder: true,
|
||||
endService: true,
|
||||
customerRescheduleAuditEnabled: false,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -6,36 +6,38 @@
|
||||
<el-form v-loading="loading" :model="form" label-width="160px" style="max-width: 760px">
|
||||
<el-form-item label="用户预约">
|
||||
<el-switch v-model="form.userBooking" disabled />
|
||||
<span class="hint">用户预约为必要节点,不可关闭</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="接单">
|
||||
<el-switch v-model="form.acceptOrder" />
|
||||
<span class="hint">接单开启,服务人员可以抢单</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="上门打卡">
|
||||
<el-switch v-model="form.onSiteCheckin" />
|
||||
<span class="hint">上门打卡关闭之后,则无需开始服务直接进入下一环节</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="服务过程留凭">
|
||||
<el-switch v-model="form.serviceEvidence" />
|
||||
<span class="hint">服务过程留凭根据关联系统表单填写提交,服务过程选填可跳过,关闭则直接进入下一环节</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="结束服务">
|
||||
<el-switch v-model="form.endService" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="客户改期审核">
|
||||
<el-switch v-model="form.customerRescheduleAuditEnabled" />
|
||||
<span class="hint">服务人员结束服务</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="服务区域" name="area">
|
||||
<el-form v-loading="loading" :model="form" label-width="140px" style="max-width: 760px">
|
||||
<el-form-item label="开启服务区域">
|
||||
<el-tab-pane label="服务区域" name="area" lazy>
|
||||
<el-form v-loading="loading" :model="form" label-width="140px">
|
||||
<el-form-item label="是否开启">
|
||||
<el-switch v-model="form.serviceAreaEnabled" />
|
||||
</el-form-item>
|
||||
<el-form-item label="区域说明">
|
||||
<el-input
|
||||
<el-form-item label="服务围栏">
|
||||
<service-area-fence
|
||||
v-if="form.serviceAreaEnabled"
|
||||
v-model="form.serviceAreaGeo"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="地图选点数据(JSON),后端接入后替换为地图组件"
|
||||
:visible="activeTab === 'area'"
|
||||
/>
|
||||
<span v-else class="hint">开启后在地图上绘制上门范围</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
@@ -50,9 +52,11 @@
|
||||
|
||||
<script>
|
||||
import { getOnSiteSettings, saveOnSiteSettings } from "@/api/appointment";
|
||||
import ServiceAreaFence from "./components/ServiceAreaFence.vue";
|
||||
|
||||
export default {
|
||||
name: "appointmentOnSiteSettings",
|
||||
components: { ServiceAreaFence },
|
||||
data() {
|
||||
return {
|
||||
activeTab: "flow",
|
||||
@@ -64,7 +68,6 @@ export default {
|
||||
onSiteCheckin: true,
|
||||
serviceEvidence: false,
|
||||
endService: true,
|
||||
customerRescheduleAuditEnabled: false,
|
||||
serviceAreaEnabled: false,
|
||||
serviceAreaGeo: "",
|
||||
},
|
||||
@@ -101,3 +104,11 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.hint {
|
||||
margin-left: 8px;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
<template>
|
||||
<div class="search">
|
||||
<el-card>
|
||||
<el-table v-loading="loading" :data="data" border>
|
||||
<el-table-column prop="orderSn" label="订单编号" min-width="180" />
|
||||
<el-table-column label="原时段" min-width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatSlot(row.fromSlotDate, row.fromStartTime, row.fromEndTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="新时段" min-width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatSlot(row.toSlotDate, row.toStartTime, row.toEndTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="action" label="动作" width="120" />
|
||||
<el-table-column prop="createTime" label="时间" min-width="170" />
|
||||
<el-table-column label="操作" width="160" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.action === 'APPLY'">
|
||||
<el-button link type="primary" @click="handleApprove(row)">通过</el-button>
|
||||
<el-button link type="danger" @click="handleReject(row)">拒绝</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
class="mt_10"
|
||||
v-model:current-page="pageNumber"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
layout="total, prev, pager, next"
|
||||
@current-change="loadData"
|
||||
/>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getRescheduleLogPage, approveReschedule, rejectReschedule } from "@/api/appointment";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
|
||||
export default {
|
||||
name: "appointmentRescheduleLogs",
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
data: [],
|
||||
total: 0,
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.loadData();
|
||||
},
|
||||
methods: {
|
||||
loadData() {
|
||||
this.loading = true;
|
||||
getRescheduleLogPage({ pageNumber: this.pageNumber, pageSize: this.pageSize })
|
||||
.then((res) => {
|
||||
if (res.success) {
|
||||
this.data = res.result?.records || [];
|
||||
this.total = res.result?.total || 0;
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
formatSlot(date, start, end) {
|
||||
const day = date ? String(date).slice(0, 10) : "";
|
||||
return `${day} ${start || ""}-${end || ""}`.trim();
|
||||
},
|
||||
handleApprove(row) {
|
||||
approveReschedule(row.id).then((res) => {
|
||||
if (res.success) {
|
||||
ElMessage.success("已通过");
|
||||
this.loadData();
|
||||
}
|
||||
});
|
||||
},
|
||||
handleReject(row) {
|
||||
ElMessageBox.prompt("拒绝原因(选填)", "拒绝改期", { inputType: "textarea" }).then(({ value }) => {
|
||||
rejectReschedule(row.id, { remark: value || "" }).then((res) => {
|
||||
if (res.success) {
|
||||
ElMessage.success("已拒绝");
|
||||
this.loadData();
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -19,10 +19,9 @@
|
||||
</el-card>
|
||||
|
||||
<el-card>
|
||||
<el-table v-loading="loading" :data="data" border style="width: 100%">
|
||||
<el-table-column prop="staffName" label="员工姓名" min-width="120" />
|
||||
<el-table-column prop="nickname" label="昵称" min-width="120" />
|
||||
<el-table-column prop="contactPhone" label="联系电话" min-width="130" />
|
||||
<el-table v-loading="loading" :data="data" border :fit="false">
|
||||
<el-table-column prop="staffName" label="员工姓名" width="160" />
|
||||
<el-table-column prop="contactPhone" label="联系电话" width="140" />
|
||||
<el-table-column prop="sortOrder" label="排序" width="80" />
|
||||
<el-table-column label="人员状态" width="110">
|
||||
<template #default="{ row }">
|
||||
@@ -33,6 +32,13 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" width="170" />
|
||||
<el-table-column label="操作" width="140" align="center">
|
||||
<template #default="{ row }">
|
||||
<a class="link-text" @click="openEdit(row)">编辑</a>
|
||||
<span class="op-split">|</span>
|
||||
<a class="link-text" @click="handleDelete(row)">删除</a>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
||||
<el-pagination
|
||||
@@ -46,16 +52,34 @@
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="createVisible" title="新增服务人员" width="480px" destroy-on-close>
|
||||
<el-dialog v-model="createVisible" :title="createForm.id ? '编辑服务人员' : '新增服务人员'" width="480px" destroy-on-close>
|
||||
<el-form :model="createForm" label-width="100px">
|
||||
<el-form-item label="店员用户ID">
|
||||
<el-input v-model="createForm.clerkUserId" placeholder="关联店员 clerk_user_id" />
|
||||
<el-form-item label="店员">
|
||||
<el-select
|
||||
v-model="createForm.clerkUserId"
|
||||
filterable
|
||||
clearable
|
||||
placeholder="请选择店员"
|
||||
style="width: 100%"
|
||||
:loading="clerkLoading"
|
||||
@change="onClerkChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="clerk in clerkOptions"
|
||||
:key="clerk.memberId"
|
||||
:label="clerkOptionLabel(clerk)"
|
||||
:value="clerk.memberId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="员工姓名">
|
||||
<el-input v-model="createForm.staffName" />
|
||||
<el-input v-model="createForm.staffName" placeholder="选择店员后自动填入" />
|
||||
</el-form-item>
|
||||
<el-form-item label="联系电话">
|
||||
<el-input v-model="createForm.contactPhone" />
|
||||
<el-input v-model="createForm.contactPhone" placeholder="选择店员后自动填入" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<el-input-number v-model="createForm.sortOrder" :min="0" :max="9999" controls-position="right" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@@ -67,10 +91,14 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ElMessageBox } from "element-plus";
|
||||
import { getUserListData } from "@/api/index";
|
||||
import {
|
||||
getServiceStaffPage,
|
||||
createServiceStaff,
|
||||
updateServiceStaff,
|
||||
updateServiceStaffStatus,
|
||||
deleteServiceStaff,
|
||||
} from "@/api/appointment";
|
||||
|
||||
export default {
|
||||
@@ -88,10 +116,15 @@ export default {
|
||||
},
|
||||
createVisible: false,
|
||||
createLoading: false,
|
||||
clerkLoading: false,
|
||||
clerkOptions: [],
|
||||
createForm: {
|
||||
id: "",
|
||||
clerkUserId: "",
|
||||
staffName: "",
|
||||
contactPhone: "",
|
||||
sortOrder: 0,
|
||||
status: "ENABLED",
|
||||
},
|
||||
};
|
||||
},
|
||||
@@ -116,16 +149,89 @@ export default {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
openCreate() {
|
||||
this.createForm = { clerkUserId: "", staffName: "", contactPhone: "" };
|
||||
this.createVisible = true;
|
||||
emptyForm() {
|
||||
return {
|
||||
id: "",
|
||||
clerkUserId: "",
|
||||
staffName: "",
|
||||
contactPhone: "",
|
||||
sortOrder: 0,
|
||||
status: "ENABLED",
|
||||
};
|
||||
},
|
||||
submitCreate() {
|
||||
this.createLoading = true;
|
||||
createServiceStaff(this.createForm)
|
||||
openCreate() {
|
||||
this.createForm = this.emptyForm();
|
||||
this.createVisible = true;
|
||||
this.loadClerks();
|
||||
},
|
||||
openEdit(row) {
|
||||
this.createForm = {
|
||||
id: row.id,
|
||||
clerkUserId: row.clerkUserId || "",
|
||||
staffName: row.staffName || "",
|
||||
contactPhone: row.contactPhone || "",
|
||||
sortOrder: row.sortOrder == null ? 0 : row.sortOrder,
|
||||
status: row.status || "ENABLED",
|
||||
};
|
||||
this.createVisible = true;
|
||||
this.loadClerks();
|
||||
},
|
||||
loadClerks() {
|
||||
this.clerkLoading = true;
|
||||
getUserListData({ pageNumber: 1, pageSize: 200 })
|
||||
.then((res) => {
|
||||
if (res.success) {
|
||||
this.$message.success("创建成功");
|
||||
const records = res.result?.records || [];
|
||||
const usedIds = new Set(
|
||||
(this.data || [])
|
||||
.filter((staff) => staff.id !== this.createForm.id)
|
||||
.map((staff) => staff.clerkUserId)
|
||||
.filter(Boolean)
|
||||
);
|
||||
const options = records.filter(
|
||||
(clerk) => clerk.memberId && clerk.status !== false && !usedIds.has(clerk.memberId)
|
||||
);
|
||||
const currentId = this.createForm.clerkUserId;
|
||||
if (currentId && !options.some((clerk) => clerk.memberId === currentId)) {
|
||||
options.unshift({
|
||||
memberId: currentId,
|
||||
clerkName: this.createForm.staffName || "当前店员",
|
||||
});
|
||||
}
|
||||
this.clerkOptions = options;
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.clerkLoading = false;
|
||||
});
|
||||
},
|
||||
clerkOptionLabel(clerk) {
|
||||
return clerk.clerkName || "未命名店员";
|
||||
},
|
||||
onClerkChange(memberId) {
|
||||
const clerk = this.clerkOptions.find((item) => item.memberId === memberId);
|
||||
if (!clerk) {
|
||||
return;
|
||||
}
|
||||
this.createForm.staffName = clerk.clerkName || "";
|
||||
this.createForm.contactPhone = clerk.mobile || "";
|
||||
},
|
||||
submitCreate() {
|
||||
if (!this.createForm.clerkUserId) {
|
||||
this.$message.warning("请选择店员");
|
||||
return;
|
||||
}
|
||||
if (!this.createForm.staffName) {
|
||||
this.$message.warning("请填写员工姓名");
|
||||
return;
|
||||
}
|
||||
const editing = !!this.createForm.id;
|
||||
const request = editing ? updateServiceStaff(this.createForm.id, this.createForm) : createServiceStaff(this.createForm);
|
||||
this.createLoading = true;
|
||||
request
|
||||
.then((res) => {
|
||||
if (res.success) {
|
||||
this.$message.success(editing ? "已保存" : "创建成功");
|
||||
this.createVisible = false;
|
||||
this.loadData();
|
||||
}
|
||||
@@ -134,6 +240,17 @@ export default {
|
||||
this.createLoading = false;
|
||||
});
|
||||
},
|
||||
handleDelete(row) {
|
||||
ElMessageBox.confirm(`确认删除服务人员「${row.staffName || ""}」?`, "提示", { type: "warning" })
|
||||
.then(() => deleteServiceStaff(row.id))
|
||||
.then((res) => {
|
||||
if (res && res.success) {
|
||||
this.$message.success("已删除");
|
||||
this.loadData();
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
toggleStatus(row, enabled) {
|
||||
updateServiceStaffStatus(row.id, enabled ? "ENABLED" : "DISABLED").then((res) => {
|
||||
if (res.success) {
|
||||
|
||||
@@ -9,54 +9,137 @@
|
||||
class="search-form"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<el-form-item label="商品名称" prop="goodsName">
|
||||
<el-input
|
||||
v-model="searchForm.goodsName"
|
||||
placeholder="请输入商品名称"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
<div class="search-form-row">
|
||||
<el-form-item label="商品名称" prop="goodsName">
|
||||
<el-input
|
||||
v-model="searchForm.goodsName"
|
||||
placeholder="请输入商品名称"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="SKU名称" prop="skuName">
|
||||
<el-input
|
||||
v-model="searchForm.skuName"
|
||||
placeholder="请输入SKU名称"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="SKU货号" prop="skuSn">
|
||||
<el-input
|
||||
v-model="searchForm.skuSn"
|
||||
placeholder="请输入SKU货号"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="商品ID" prop="id">
|
||||
<el-input
|
||||
v-model="searchForm.id"
|
||||
placeholder="请输入商品ID"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="search-form-row">
|
||||
<el-form-item label="店铺分类" prop="storeCategoryPath">
|
||||
<el-cascader
|
||||
v-model="searchForm.storeCategoryPath"
|
||||
:options="shopCategoryList"
|
||||
:props="shopCategoryProps"
|
||||
clearable
|
||||
filterable
|
||||
placeholder="请选择店铺分类"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="平台分类" prop="categoryPath">
|
||||
<el-cascader
|
||||
v-model="searchForm.categoryPath"
|
||||
:options="platformCategoryList"
|
||||
:props="platformCategoryProps"
|
||||
clearable
|
||||
filterable
|
||||
placeholder="请选择平台商品分类"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="商品品牌" prop="brandId">
|
||||
<el-select
|
||||
v-model="searchForm.brandId"
|
||||
placeholder="请选择商品品牌"
|
||||
clearable
|
||||
filterable
|
||||
>
|
||||
<el-option
|
||||
v-for="item in brandList"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="商品类型" prop="goodsType">
|
||||
<el-select
|
||||
v-model="searchForm.goodsType"
|
||||
placeholder="请选择"
|
||||
clearable
|
||||
>
|
||||
<el-option label="实物商品" value="PHYSICAL_GOODS" />
|
||||
<el-option label="虚拟商品" value="VIRTUAL_GOODS" />
|
||||
<!-- E_COUPON:卡密商品,可跳转卡池管理 -->
|
||||
<el-option label="电子卡券" value="E_COUPON" />
|
||||
<el-option label="预约商品" value="APPOINTMENT_GOODS" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="创建时间">
|
||||
<el-date-picker
|
||||
v-model="createRange"
|
||||
type="datetimerange"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
start-placeholder="开始"
|
||||
end-placeholder="结束"
|
||||
style="width: 360px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="商品编号" prop="id">
|
||||
<el-input
|
||||
v-model="searchForm.id"
|
||||
placeholder="请输入商品编号"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="店铺名称" prop="storeName">
|
||||
<el-input
|
||||
v-model="searchForm.storeName"
|
||||
placeholder="请输入店铺名称"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="销售模式" prop="salesModel">
|
||||
<el-form-item label="库存状态" prop="stockStatus">
|
||||
<el-select
|
||||
v-model="searchForm.salesModel"
|
||||
placeholder="请选择"
|
||||
v-model="searchForm.stockStatus"
|
||||
placeholder="全部"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
@change="onStockStatusChange"
|
||||
>
|
||||
<el-option label="零售" value="RETAIL" />
|
||||
<el-option label="批发" value="WHOLESALE" />
|
||||
<el-option label="有货" value="IN_STOCK" />
|
||||
<el-option label="无货" value="OUT_OF_STOCK" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="商品类型" prop="goodsType">
|
||||
<el-select
|
||||
v-model="searchForm.goodsType"
|
||||
placeholder="请选择"
|
||||
<el-form-item v-if="searchForm.stockStatus === 'IN_STOCK'" label="库存区间">
|
||||
<el-input
|
||||
v-model="searchForm.stockMin"
|
||||
placeholder="≥"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
>
|
||||
<el-option label="实物商品" value="PHYSICAL_GOODS" />
|
||||
<el-option label="虚拟商品" value="VIRTUAL_GOODS" />
|
||||
<!-- E_COUPON:卡密商品,可跳转卡池管理 -->
|
||||
<el-option label="电子卡券" value="E_COUPON" />
|
||||
<el-option label="预约商品" value="APPOINTMENT_GOODS" />
|
||||
</el-select>
|
||||
style="width: 110px"
|
||||
/>
|
||||
<span style="margin: 0 6px">~</span>
|
||||
<el-input
|
||||
v-model="searchForm.stockMax"
|
||||
placeholder="≤"
|
||||
clearable
|
||||
style="width: 110px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="售价区间">
|
||||
<el-input
|
||||
v-model="searchForm.priceMin"
|
||||
placeholder="≥"
|
||||
clearable
|
||||
style="width: 110px"
|
||||
/>
|
||||
<span style="margin: 0 6px">~</span>
|
||||
<el-input
|
||||
v-model="searchForm.priceMax"
|
||||
placeholder="≤"
|
||||
clearable
|
||||
style="width: 110px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
|
||||
@@ -130,16 +213,16 @@
|
||||
</el-table-column>
|
||||
<el-table-column prop="buyCount" label="销量" width="90" />
|
||||
<el-table-column prop="quantity" label="库存" width="90" />
|
||||
<el-table-column label="销售模式" width="150">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ salesModelText(row.salesModel) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="商品类型" width="150">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ goodsTypeText(row.goodsType) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="商品分类" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ platformCategoryText(row?.categoryPath) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="150">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ marketEnableText(row.marketEnable) }}</span>
|
||||
@@ -150,8 +233,8 @@
|
||||
<span v-if="row">{{ authFlagText(row.authFlag) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="storeName" label="店铺名称" width="200" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="260" align="center" fixed="right">
|
||||
<el-table-column prop="createTime" label="创建时间" width="170" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="320" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.marketEnable === 'DOWN'">
|
||||
<a class="link-text" @click="upper(row)">上架</a>
|
||||
@@ -163,6 +246,8 @@
|
||||
<span class="op-split">|</span>
|
||||
<a class="link-text" @click="editGoods(row)">编辑</a>
|
||||
</template>
|
||||
<span class="op-split">|</span>
|
||||
<a class="link-text" @click="copyGoods(row)">复制商品</a>
|
||||
<!-- E_COUPON 专属:卡池管理入口(原型 P-03) -->
|
||||
<template v-if="row.goodsType === 'E_COUPON'">
|
||||
<span class="op-split">|</span>
|
||||
@@ -202,9 +287,12 @@
|
||||
|
||||
<script>
|
||||
import {
|
||||
getBrandListData,
|
||||
getCategoryTree,
|
||||
getGoodsListData,
|
||||
getGoodsNumerData,
|
||||
getQueryGoodsIdGoodsList,
|
||||
getShopGoodsLabelListSeller,
|
||||
upGoods,
|
||||
lowGoods,
|
||||
} from "@/api/goods";
|
||||
@@ -215,12 +303,40 @@ export default {
|
||||
id: "", //要操作的id
|
||||
loading: true, // 表单加载状态
|
||||
modalVisible: false, // 添加或编辑显示
|
||||
createRange: [],
|
||||
searchForm: {
|
||||
// 搜索框初始化对象
|
||||
pageNumber: 1, // 当前页数
|
||||
pageSize: 20, // 页面大小
|
||||
sort: "create_time", // 默认排序字段
|
||||
order: "desc", // 默认排序方式
|
||||
storeCategoryPath: "",
|
||||
categoryPath: "",
|
||||
skuName: "",
|
||||
skuSn: "",
|
||||
brandId: "",
|
||||
stockStatus: "",
|
||||
stockMin: "",
|
||||
stockMax: "",
|
||||
priceMin: "",
|
||||
priceMax: "",
|
||||
},
|
||||
brandList: [],
|
||||
platformCategoryList: [],
|
||||
shopCategoryList: [],
|
||||
platformCategoryProps: {
|
||||
value: "id",
|
||||
label: "name",
|
||||
children: "children",
|
||||
checkStrictly: true,
|
||||
emitPath: false,
|
||||
},
|
||||
shopCategoryProps: {
|
||||
value: "value",
|
||||
label: "label",
|
||||
children: "children",
|
||||
checkStrictly: true,
|
||||
emitPath: false,
|
||||
},
|
||||
underForm: {
|
||||
// 下架原因
|
||||
@@ -253,11 +369,6 @@ export default {
|
||||
onStatusTabClick(tab) {
|
||||
this.goodsStatusClick(tab.paneName);
|
||||
},
|
||||
salesModelText(v) {
|
||||
if (v === "RETAIL") return "零售";
|
||||
if (v === "WHOLESALE") return "批发";
|
||||
return "其他类型";
|
||||
},
|
||||
goodsTypeText(v) {
|
||||
if (v === "PHYSICAL_GOODS") return "实物商品";
|
||||
if (v === "VIRTUAL_GOODS") return "虚拟商品";
|
||||
@@ -265,6 +376,25 @@ export default {
|
||||
if (v === "APPOINTMENT_GOODS") return "预约商品";
|
||||
return v || "—";
|
||||
},
|
||||
findTreeNodeLabel(tree, targetId, valueKey, labelKey) {
|
||||
for (const node of tree || []) {
|
||||
if (String(node[valueKey]) === String(targetId)) {
|
||||
return node[labelKey];
|
||||
}
|
||||
const childLabel = this.findTreeNodeLabel(node.children, targetId, valueKey, labelKey);
|
||||
if (childLabel) return childLabel;
|
||||
}
|
||||
return "";
|
||||
},
|
||||
platformCategoryText(path) {
|
||||
if (!path) return "—";
|
||||
const ids = String(path).split(",").filter(Boolean);
|
||||
if (!ids.length) return "—";
|
||||
const labels = ids
|
||||
.map((id) => this.findTreeNodeLabel(this.platformCategoryList, id, "id", "name"))
|
||||
.filter(Boolean);
|
||||
return labels.length ? labels.join(" / ") : "—";
|
||||
},
|
||||
/** 跳转卡池管理;单 SKU 商品默认取第一个 SKU(card-key-pool 需 skuId) */
|
||||
async goCardKeyPool(row) {
|
||||
let skuId = row.skuId;
|
||||
@@ -304,9 +434,77 @@ export default {
|
||||
},
|
||||
// 初始化数据
|
||||
init() {
|
||||
this.loadFilterOptions();
|
||||
this.getDataList();
|
||||
this.getNumberData();
|
||||
},
|
||||
loadFilterOptions() {
|
||||
getBrandListData().then((res) => {
|
||||
this.brandList = Array.isArray(res) ? res : res?.result || [];
|
||||
});
|
||||
getShopGoodsLabelListSeller().then((res) => {
|
||||
if (res?.success) {
|
||||
this.shopCategoryList = this.formatShopCategory(res.result || []);
|
||||
}
|
||||
});
|
||||
getCategoryTree().then((res) => {
|
||||
if (res?.success) {
|
||||
this.platformCategoryList = res.result || [];
|
||||
}
|
||||
});
|
||||
},
|
||||
onStockStatusChange(value) {
|
||||
if (value !== "IN_STOCK") {
|
||||
this.searchForm.stockMin = "";
|
||||
this.searchForm.stockMax = "";
|
||||
}
|
||||
},
|
||||
buildSearchParams(includeGoodsStatus = true) {
|
||||
const { stockStatus, stockMin, stockMax, priceMin, priceMax, ...rest } = this.searchForm;
|
||||
const params = { ...rest };
|
||||
|
||||
if (!includeGoodsStatus) {
|
||||
delete params.goodsStatus;
|
||||
}
|
||||
|
||||
if (this.createRange && this.createRange.length === 2) {
|
||||
params.startDate = this.createRange[0];
|
||||
params.endDate = this.createRange[1];
|
||||
}
|
||||
|
||||
if (stockStatus === "IN_STOCK") {
|
||||
params.geQuantity = stockMin !== "" && stockMin != null ? Number(stockMin) : 1;
|
||||
if (stockMax !== "" && stockMax != null) {
|
||||
params.leQuantity = Number(stockMax);
|
||||
}
|
||||
} else if (stockStatus === "OUT_OF_STOCK") {
|
||||
params.geQuantity = 0;
|
||||
params.leQuantity = 0;
|
||||
} else {
|
||||
if (stockMin !== "" && stockMin != null) params.geQuantity = Number(stockMin);
|
||||
if (stockMax !== "" && stockMax != null) params.leQuantity = Number(stockMax);
|
||||
}
|
||||
|
||||
if (priceMin !== "" && priceMax !== "") {
|
||||
params.price = `${priceMin}_${priceMax}`;
|
||||
} else if (priceMin !== "") {
|
||||
params.price = String(priceMin);
|
||||
} else if (priceMax !== "") {
|
||||
params.price = `0_${priceMax}`;
|
||||
}
|
||||
|
||||
return params;
|
||||
},
|
||||
formatShopCategory(list) {
|
||||
return (list || []).map((item) => ({
|
||||
value: item.id,
|
||||
label: item.labelName,
|
||||
children:
|
||||
item.children && item.children.length
|
||||
? this.formatShopCategory(item.children)
|
||||
: undefined,
|
||||
}));
|
||||
},
|
||||
// 分页 改变页码
|
||||
changePage(v) {
|
||||
this.searchForm.pageNumber = v;
|
||||
@@ -327,7 +525,7 @@ export default {
|
||||
// 获取数据
|
||||
getDataList() {
|
||||
this.loading = true;
|
||||
getGoodsListData(this.searchForm).then((res) => {
|
||||
getGoodsListData(this.buildSearchParams()).then((res) => {
|
||||
this.loading = false;
|
||||
if (res.success) {
|
||||
this.data = res.result.records;
|
||||
@@ -336,9 +534,7 @@ export default {
|
||||
});
|
||||
},
|
||||
getNumberData() {
|
||||
// 创建一个不包含goodsStatus字段的搜索参数
|
||||
const { goodsStatus, ...searchParams } = this.searchForm;
|
||||
getGoodsNumerData(searchParams).then((res) => {
|
||||
getGoodsNumerData(this.buildSearchParams(false)).then((res) => {
|
||||
if (res.success) {
|
||||
this.goodsNumerData = res.result;
|
||||
}
|
||||
@@ -400,6 +596,14 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
// 复制商品
|
||||
copyGoods(v) {
|
||||
this.$filters.customRouterPush({
|
||||
name: "goods-operation",
|
||||
query: { copyId: v.id },
|
||||
});
|
||||
},
|
||||
|
||||
// 商品状态筛选
|
||||
goodsStatusClick(name) {
|
||||
if (name === "ALL" || name === "" || name === undefined || name === null) {
|
||||
@@ -500,6 +704,31 @@ export default {
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.search-form-row {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
margin-bottom: 18px;
|
||||
|
||||
:deep(.el-form-item) {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-right: 12px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.el-form-item:last-child) {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
:deep(.el-input),
|
||||
:deep(.el-select),
|
||||
:deep(.el-cascader) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// Tab组件样式
|
||||
.goods-tab {
|
||||
:deep(.el-tabs__item) {
|
||||
|
||||
203
seller/src/views/main-components/store-qrcode.vue
Normal file
203
seller/src/views/main-components/store-qrcode.vue
Normal file
@@ -0,0 +1,203 @@
|
||||
<template>
|
||||
<div class="store-qr-entry">
|
||||
<el-button size="small" @click="open">店铺二维码</el-button>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="店铺二维码"
|
||||
width="720px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
@opened="loadCodes"
|
||||
>
|
||||
<div class="store-qr-dialog">
|
||||
<div class="store-qr-col">
|
||||
<vue-qr
|
||||
v-if="h5Url"
|
||||
:text="h5Url"
|
||||
:margin="0"
|
||||
color-dark="#000"
|
||||
color-light="#fff"
|
||||
:size="180"
|
||||
/>
|
||||
<div v-else class="store-qr-tip">请先在系统设置中填写 wap 站点地址</div>
|
||||
<div class="store-qr-title">H5 二维码</div>
|
||||
</div>
|
||||
<div class="store-qr-col">
|
||||
<img v-if="mpQrSrc" class="store-qr-img" :src="mpQrSrc" alt="小程序葵花码" />
|
||||
<div v-else class="store-qr-tip">{{ mpError || "小程序码生成中..." }}</div>
|
||||
<div class="store-qr-title">小程序葵花码</div>
|
||||
</div>
|
||||
<div class="store-qr-col">
|
||||
<el-button type="primary" :disabled="!pcUrl" @click="copyText(pcUrl)">复制链接</el-button>
|
||||
<div class="store-qr-title">PC 店铺首页</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Cookies from "js-cookie";
|
||||
import vueQr from "vue-qr";
|
||||
import { getStoreMpQrcode } from "@/api/index";
|
||||
import { getBaseSite } from "@/api/common.js";
|
||||
|
||||
export default {
|
||||
name: "store-qrcode",
|
||||
components: { "vue-qr": vueQr },
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
storeId: "",
|
||||
wapUrl: "",
|
||||
pcUrlBase: "",
|
||||
mpQrcode: "",
|
||||
mpError: "",
|
||||
mpLoaded: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
h5Url() {
|
||||
if (!this.storeId || !this.wapUrl) return "";
|
||||
return this.joinUrl(this.wapUrl, `/pages/product/shopPage?id=${this.storeId}`);
|
||||
},
|
||||
pcUrl() {
|
||||
if (!this.storeId || !this.pcUrlBase) return "";
|
||||
return this.joinUrl(this.pcUrlBase, `/merchant?id=${this.storeId}`);
|
||||
},
|
||||
mpQrSrc() {
|
||||
if (!this.mpQrcode) return "";
|
||||
const mime = String(this.mpQrcode).startsWith("/9j/")
|
||||
? "image/jpeg"
|
||||
: "image/png";
|
||||
return `data:${mime};base64,${this.mpQrcode}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
joinUrl(base, path) {
|
||||
return String(base || "").replace(/\/$/, "") + path;
|
||||
},
|
||||
parseSettingValue(raw) {
|
||||
if (!raw) return {};
|
||||
if (typeof raw === "object") return raw;
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
initStore() {
|
||||
try {
|
||||
const userInfo = JSON.parse(Cookies.get("userInfoSeller") || "{}") || {};
|
||||
this.storeId = userInfo.id || userInfo.storeId || "";
|
||||
} catch (e) {
|
||||
this.storeId = "";
|
||||
}
|
||||
},
|
||||
open() {
|
||||
this.initStore();
|
||||
this.visible = true;
|
||||
},
|
||||
loadCodes() {
|
||||
this.initStore();
|
||||
getBaseSite()
|
||||
.then((res) => {
|
||||
const data = this.parseSettingValue(res && res.result && res.result.settingValue);
|
||||
const fallback = typeof window !== "undefined" ? window.BASE || {} : {};
|
||||
// wap/站点地址常配官网,H5、PC 店铺页用买家端域名
|
||||
this.wapUrl = fallback.WAP_URL || data.staticPageWapAddress || "";
|
||||
this.pcUrlBase = fallback.PC_URL || data.staticPageAddress || "";
|
||||
})
|
||||
.catch(() => {
|
||||
const fallback = typeof window !== "undefined" ? window.BASE || {} : {};
|
||||
this.wapUrl = fallback.WAP_URL || "";
|
||||
this.pcUrlBase = fallback.PC_URL || "";
|
||||
});
|
||||
this.loadMpQrcode();
|
||||
},
|
||||
loadMpQrcode() {
|
||||
if (!this.storeId || this.mpLoaded) return;
|
||||
this.mpError = "";
|
||||
getStoreMpQrcode()
|
||||
.then((res) => {
|
||||
if (res && res.success && res.result) {
|
||||
if (String(res.result).startsWith("eyJ")) {
|
||||
this.mpError = "请先配置微信小程序";
|
||||
return;
|
||||
}
|
||||
this.mpQrcode = res.result;
|
||||
this.mpLoaded = true;
|
||||
this.mpError = "";
|
||||
} else {
|
||||
this.mpError = "请先配置微信小程序";
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.mpError = "请先配置微信小程序";
|
||||
});
|
||||
},
|
||||
copyText(text) {
|
||||
if (!text) return;
|
||||
const ok = () => this.$Message.success("复制成功");
|
||||
const fail = () => this.$Message.error("复制失败");
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(ok).catch(fail);
|
||||
return;
|
||||
}
|
||||
const textArea = document.createElement("textarea");
|
||||
textArea.value = text;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
try {
|
||||
document.execCommand("copy");
|
||||
ok();
|
||||
} catch (e) {
|
||||
fail();
|
||||
}
|
||||
document.body.removeChild(textArea);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.store-qr-entry {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.store-qr-dialog {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 8px 8px 16px;
|
||||
}
|
||||
.store-qr-col {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 240px;
|
||||
gap: 12px;
|
||||
}
|
||||
.store-qr-img {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
display: block;
|
||||
}
|
||||
.store-qr-title {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
.store-qr-tip {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
@@ -226,22 +226,25 @@
|
||||
}
|
||||
|
||||
.single-page-con {
|
||||
min-width: 740px;
|
||||
position: relative;
|
||||
position: absolute;
|
||||
left: 180px;
|
||||
top: 106px;
|
||||
right: 0;
|
||||
top: 106px;
|
||||
bottom: 0;
|
||||
height: calc(100% - 116px);
|
||||
width: calc(100% - 180px);
|
||||
overflow: auto;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
background-color: #f0f0f0;
|
||||
z-index: 1;
|
||||
transition: left 0.3s, width 0.3s;
|
||||
transition: left 0.3s;
|
||||
box-sizing: border-box;
|
||||
|
||||
.single-page {
|
||||
position: relative;
|
||||
margin: 10px;
|
||||
max-width: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,7 +259,8 @@
|
||||
|
||||
.single-page-con {
|
||||
left: 80px;
|
||||
width: calc(100% - 80px);
|
||||
right: 0;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,8 +37,9 @@
|
||||
>打印发货单</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card class="mt_10 clearfix">
|
||||
<div style="width: 30%; float: left; margin-left: 20px">
|
||||
<el-card class="mt_10">
|
||||
<div class="order-info-grid">
|
||||
<div class="order-info-col">
|
||||
<div class="div-item">
|
||||
<div class="div-item-left">订单号:</div>
|
||||
<div class="div-item-right">{{ orderInfo.order.sn }}</div>
|
||||
@@ -95,7 +96,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="width: 30%; float: left; margin-left: 20px">
|
||||
<div class="order-info-col">
|
||||
<div class="div-item" v-if="orderInfo.order.needReceipt == false">
|
||||
<div class="div-item-left">发票信息:</div>
|
||||
<div class="div-item-right">暂无发票信息</div>
|
||||
@@ -146,8 +147,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="width: 36%; float: left">
|
||||
<div class="div-item" v-if="!isECouponOrder && orderInfo.order.deliveryMethod != 'SELF_PICK_UP'">
|
||||
<div class="order-info-col">
|
||||
<div class="div-item" v-if="!isECouponOrder && !isAppointmentOrder && orderInfo.order.deliveryMethod != 'SELF_PICK_UP'">
|
||||
<div class="div-item-left">收货信息:</div>
|
||||
<div class="div-item-right">
|
||||
{{ orderInfo.order.consigneeName }}
|
||||
@@ -236,51 +237,86 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card class="mt_10">
|
||||
<el-table v-loading="loading" border :data="data" ref="table" style="width: 100%">
|
||||
<el-table-column label="商品" min-width="200">
|
||||
<el-card v-if="orderPackages.length" class="mt_10 order-goods-card">
|
||||
<el-divider content-position="left">发货记录</el-divider>
|
||||
<el-table :data="orderPackages" border style="width: 100%">
|
||||
<el-table-column label="发货方式" width="120">
|
||||
<template #default="{ row }">{{ getFulfillmentModeLabel(row.fulfillmentMode) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="物流信息" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row" style="margin-top: 5px; height: 80px; display: flex">
|
||||
<div>
|
||||
<img :src="row.image" style="height: 60px; margin-top: 1px; width: 60px" />
|
||||
</div>
|
||||
<div style="margin-left: 13px">
|
||||
<div class="div-zoom">
|
||||
<a class="link-text" @click="linkTo(row.goodsId, row.skuId)">{{ row.goodsName }}</a>
|
||||
</div>
|
||||
<span v-for="(item, key) in parseOrderItemSpecs(row.specs)" :key="key">
|
||||
<span v-show="key != 'images'" style="font-size: 12px; color: #999999">
|
||||
{{ key }} : {{ item }}</span>
|
||||
</span>
|
||||
<el-popover trigger="hover" title="扫码在手机中查看" width="180">
|
||||
<template #reference>
|
||||
<img src="../../../assets/qrcode.svg" class="hover-pointer" width="20" height="20" alt="" style="display: block" />
|
||||
</template>
|
||||
<vue-qr :text="wapLinkTo(row.goodsId, row.skuId)" :margin="0" colorDark="#000" colorLight="#fff" :size="150" />
|
||||
</el-popover>
|
||||
</div>
|
||||
<template v-if="isNoDeliveryPackage(row)">
|
||||
<span>无需配送</span>
|
||||
<span v-if="row.deliveryRemark" class="delivery-remark-text">({{ row.deliveryRemark }})</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ row.logisticsName || "-" }} / {{ row.logisticsNo || "-" }}
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="logisticsTime" label="发货时间" width="170" />
|
||||
<el-table-column label="商品明细" min-width="260">
|
||||
<template #default="{ row }">
|
||||
<div v-for="(item, idx) in row.orderPackageItemList || []" :key="idx" class="package-item-line">
|
||||
{{ item.goodsName }} × {{ item.deliverNumber }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="优惠" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ getPromotionText(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="单价" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ $filters.unitPrice(row.unitPrice || 0, "¥") }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="num" label="数量" min-width="80" />
|
||||
<el-table-column prop="returnGoodsNumber" label="退款数量" min-width="80" />
|
||||
<el-table-column label="小计" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ $filters.unitPrice(row.flowPrice, "¥") }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
<el-card class="mt_10 order-goods-card">
|
||||
<div v-loading="loading" class="order-fit-wrap">
|
||||
<table class="order-fit-table">
|
||||
<colgroup>
|
||||
<col style="width: 40%" />
|
||||
<col style="width: 18%" />
|
||||
<col style="width: 12%" />
|
||||
<col style="width: 8%" />
|
||||
<col style="width: 12%" />
|
||||
<col style="width: 10%" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>商品</th>
|
||||
<th>优惠</th>
|
||||
<th>单价</th>
|
||||
<th>数量</th>
|
||||
<th>退款数量</th>
|
||||
<th>小计</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in data" :key="row.sn || row.id">
|
||||
<td>
|
||||
<div class="order-goods-cell">
|
||||
<img :src="row.image" class="order-goods-img" />
|
||||
<div class="order-goods-meta">
|
||||
<div class="div-zoom">
|
||||
<a class="link-text" @click="linkTo(row.goodsId, row.skuId)">{{ row.goodsName }}</a>
|
||||
</div>
|
||||
<span v-for="(item, key) in parseOrderItemSpecs(row.specs)" :key="key">
|
||||
<span v-show="key != 'images'" class="order-goods-spec">{{ key }} : {{ item }}</span>
|
||||
</span>
|
||||
<el-popover trigger="hover" title="扫码在手机中查看" width="180">
|
||||
<template #reference>
|
||||
<img src="../../../assets/qrcode.svg" class="hover-pointer" width="20" height="20" alt="" />
|
||||
</template>
|
||||
<vue-qr :text="wapLinkTo(row.goodsId, row.skuId)" :margin="0" colorDark="#000" colorLight="#fff" :size="150" />
|
||||
</el-popover>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ getPromotionText(row) }}</td>
|
||||
<td>{{ $filters.unitPrice(row.unitPrice || 0, "¥") }}</td>
|
||||
<td>{{ row.num }}</td>
|
||||
<td>{{ row.returnGoodsNumber }}</td>
|
||||
<td>{{ $filters.unitPrice(row.flowPrice, "¥") }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 卡密信息(当前订单 orderItems;满赠赠品卡密在赠品子单详情查看) -->
|
||||
<div v-if="hasCardKeySection" class="ecoupon-card-keys mt_10">
|
||||
@@ -298,28 +334,40 @@
|
||||
:title="resolveCardKeyFulfillMessage(line)"
|
||||
class="mb_10"
|
||||
/>
|
||||
<el-table
|
||||
v-else-if="line.cardKeys.length"
|
||||
border
|
||||
:data="lineCardKeyRows(line)"
|
||||
style="width: 100%"
|
||||
class="mb_10"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column prop="cardNo" label="卡号" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="cardSecret" label="卡密" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="allocatedTime" label="发卡时间" width="170" />
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ formatCardKeyStatus(row.status || 'ALLOCATED') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row" link type="primary" @click="copyCardKey(row)">复制</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div v-else-if="line.cardKeys.length" class="order-fit-wrap mb_10">
|
||||
<table class="order-fit-table">
|
||||
<colgroup>
|
||||
<col style="width: 8%" />
|
||||
<col style="width: 16%" />
|
||||
<col style="width: 36%" />
|
||||
<col style="width: 20%" />
|
||||
<col style="width: 10%" />
|
||||
<col style="width: 10%" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>序号</th>
|
||||
<th>卡号</th>
|
||||
<th>卡密</th>
|
||||
<th>发卡时间</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, index) in lineCardKeyRows(line)" :key="row.id || row.cardNo || index">
|
||||
<td>{{ index + 1 }}</td>
|
||||
<td>{{ row.cardNo }}</td>
|
||||
<td class="card-secret-cell">{{ row.cardSecret }}</td>
|
||||
<td>{{ row.allocatedTime }}</td>
|
||||
<td>{{ formatCardKeyStatus(row.status || 'ALLOCATED') }}</td>
|
||||
<td>
|
||||
<el-button link type="primary" class="card-key-copy" @click="copyCardKey(row)">复制</el-button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -409,34 +457,6 @@
|
||||
</ul>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card v-if="orderPackages.length" class="mt_10">
|
||||
<el-divider content-position="left">发货记录</el-divider>
|
||||
<el-table :data="orderPackages" border style="width: 100%">
|
||||
<el-table-column label="发货方式" width="120">
|
||||
<template #default="{ row }">{{ getFulfillmentModeLabel(row.fulfillmentMode) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="物流信息" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<template v-if="isNoDeliveryPackage(row)">
|
||||
<span>无需配送</span>
|
||||
<span v-if="row.deliveryRemark" class="delivery-remark-text">({{ row.deliveryRemark }})</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ row.logisticsName || "-" }} / {{ row.logisticsNo || "-" }}
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="logisticsTime" label="发货时间" width="170" />
|
||||
<el-table-column label="商品明细" min-width="260">
|
||||
<template #default="{ row }">
|
||||
<div v-for="(item, idx) in row.orderPackageItemList || []" :key="idx" class="package-item-line">
|
||||
{{ item.goodsName }} × {{ item.deliverNumber }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="modal" title="修改金额" width="530px">
|
||||
@@ -1179,7 +1199,7 @@ export default {
|
||||
if (type.indexOf("FULL_DISCOUNT") != -1) resultText += "满减 ";
|
||||
if (type.indexOf("POINTS_GOODS") != -1) resultText += "积分商品 ";
|
||||
}
|
||||
return resultText === "" ? "暂无未参与任何促销" : resultText;
|
||||
return resultText === "" ? "未参与" : resultText;
|
||||
},
|
||||
gotoHomes () {
|
||||
return false
|
||||
@@ -1841,6 +1861,93 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.order-goods-card {
|
||||
overflow-x: hidden;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.order-info-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 24px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.order-info-col {
|
||||
flex: 1 1 280px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.order-fit-wrap {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.order-fit-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.order-fit-table th,
|
||||
.order-fit-table td {
|
||||
border: 1px solid #ebeef5;
|
||||
padding: 10px 12px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.order-fit-table th {
|
||||
background: #f5f7fa;
|
||||
color: #909399;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.order-fit-table .is-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card-secret-cell {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.card-key-copy {
|
||||
padding: 0;
|
||||
height: auto;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.order-goods-cell {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.order-goods-img {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
flex-shrink: 0;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.order-goods-meta {
|
||||
margin-left: 13px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.order-goods-spec {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.delivery-empty {
|
||||
color: #999;
|
||||
}
|
||||
@@ -1953,6 +2060,9 @@ export default {
|
||||
}
|
||||
|
||||
.search {
|
||||
max-width: 100%;
|
||||
overflow-x: clip;
|
||||
|
||||
.operation {
|
||||
margin-bottom: 2vh;
|
||||
}
|
||||
@@ -1990,32 +2100,37 @@ export default {
|
||||
}
|
||||
|
||||
.goods-total {
|
||||
box-sizing: border-box;
|
||||
padding: 20px;
|
||||
height: 220px;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
ul {
|
||||
margin-right: 10px;
|
||||
display: block;
|
||||
float: right;
|
||||
margin: 0 10px 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
list-style-type: none;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
li {
|
||||
text-align: -webkit-match-parent;
|
||||
}
|
||||
li {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.label {
|
||||
float: left;
|
||||
width: 500px;
|
||||
font-size: 14px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.txt {
|
||||
float: left;
|
||||
min-width: 88px;
|
||||
font-size: 14px;
|
||||
width: 130px;
|
||||
text-align: right;
|
||||
font-family: verdana;
|
||||
}
|
||||
|
||||
@@ -4,33 +4,36 @@
|
||||
<el-form
|
||||
ref="searchForm"
|
||||
:model="searchForm"
|
||||
inline
|
||||
label-width="70px"
|
||||
class="search-form"
|
||||
class="search-form order-search-form"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<el-form-item label="关键字" prop="keywords" style="display: block; width: 100%">
|
||||
<el-form-item label="关键字" prop="keywords">
|
||||
<el-input
|
||||
v-model="searchForm.keywords"
|
||||
placeholder="请输入商品名称/收货人/收货人手机号/店铺名称"
|
||||
placeholder="商品名称/收货人/手机号/店铺名称"
|
||||
clearable
|
||||
style="width: 500px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="订单号" prop="orderSn">
|
||||
<el-input v-model="searchForm.orderSn" placeholder="请输入订单号" clearable style="width: 240px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="会员名称" prop="buyerName">
|
||||
<el-input v-model="searchForm.buyerName" placeholder="请输入会员名称" clearable style="width: 240px" />
|
||||
<el-input v-model="searchForm.orderSn" placeholder="请输入订单号" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="商品名称" prop="goodsName">
|
||||
<el-input v-model="searchForm.goodsName" placeholder="请输入商品名称" clearable style="width: 240px" />
|
||||
<el-input v-model="searchForm.goodsName" placeholder="请输入商品名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="商品类型" prop="goodsType">
|
||||
<el-select v-model="searchForm.goodsType" placeholder="请选择商品类型" clearable>
|
||||
<el-option label="实物商品" value="PHYSICAL_GOODS" />
|
||||
<el-option label="虚拟商品" value="VIRTUAL_GOODS" />
|
||||
<el-option label="卡密商品" value="E_COUPON" />
|
||||
<el-option label="预约商品" value="APPOINTMENT_GOODS" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="收货人" prop="shipName">
|
||||
<el-input v-model="searchForm.shipName" placeholder="请输入收货人姓名" clearable style="width: 240px" />
|
||||
<el-input v-model="searchForm.shipName" placeholder="请输入收货人姓名" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="订单类型" prop="orderType">
|
||||
<el-select v-model="searchForm.orderPromotionType" placeholder="请选择" clearable style="width: 240px">
|
||||
<el-select v-model="searchForm.orderPromotionType" placeholder="请选择" clearable>
|
||||
<el-option label="普通订单" value="NORMAL" />
|
||||
<el-option label="拼团订单" value="PINTUAN" />
|
||||
<el-option label="赠品订单" value="GIFT" />
|
||||
@@ -39,7 +42,7 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="支付方式" prop="paymentMethod">
|
||||
<el-select v-model="searchForm.paymentMethod" placeholder="请选择支付方式" clearable style="width: 240px">
|
||||
<el-select v-model="searchForm.paymentMethod" placeholder="请选择支付方式" clearable>
|
||||
<el-option label="微信支付" value="WECHAT" />
|
||||
<el-option label="支付宝" value="ALIPAY" />
|
||||
<el-option label="余额支付" value="WALLET" />
|
||||
@@ -54,12 +57,11 @@
|
||||
clearable
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
style="width: 360px"
|
||||
@change="selectDateRange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
|
||||
<el-form-item class="order-search-action">
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
@@ -79,46 +81,71 @@
|
||||
<el-button type="info" class="export" @click="exportOrder">导出订单</el-button>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="data" ref="table" class="mt_10" style="width: 100%">
|
||||
<el-table-column prop="sn" label="订单号" min-width="240" show-overflow-tooltip />
|
||||
<el-table-column label="订单来源" width="120">
|
||||
<template #default="{ row }">{{ clientTypeText(row.clientType) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="订单类型" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="orderPromotionTagType(row.orderPromotionType)">
|
||||
<div v-loading="loading" class="doudian-list mt_10">
|
||||
<div class="doudian-head">
|
||||
<div>商品信息</div>
|
||||
<div>单价/数量</div>
|
||||
<div>售后状态</div>
|
||||
<div>应付</div>
|
||||
<div>买家/收货人</div>
|
||||
<div>订单状态</div>
|
||||
<div>操作</div>
|
||||
</div>
|
||||
<div v-if="!data.length && !loading" class="doudian-empty">暂无订单</div>
|
||||
<div v-for="row in data" :key="row.sn" class="doudian-order">
|
||||
<div class="doudian-bar">
|
||||
<span>下单时间:{{ row.createTime || "—" }}</span>
|
||||
<span class="doudian-sn">
|
||||
订单编号:{{ row.sn }}
|
||||
<a class="copy-icon" title="复制订单编号" @click="copyText(row.sn)">
|
||||
<el-icon><CopyDocument /></el-icon>
|
||||
</a>
|
||||
</span>
|
||||
<span class="doudian-bar-extra">
|
||||
{{ orderPromotionText(row.orderPromotionType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="memberName" label="买家名称" min-width="130" show-overflow-tooltip />
|
||||
<el-table-column prop="memberId" label="会员ID" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="storeName" label="店铺名称" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="订单金额" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<span :style="{ color: $mainColor }">{{ $filters.unitPrice(row.flowPrice, '¥') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="支付方式" width="120">
|
||||
<template #default="{ row }">{{ paymentMethodText(row.paymentMethod) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="订单状态" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="orderStatusTagType(row.orderStatus)">{{ orderStatusText(row.orderStatus) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="下单时间" width="170" />
|
||||
<el-table-column label="操作" width="160" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<a class="link-text" @click="detail(row)">查看</a>
|
||||
<a
|
||||
v-if="canShip(row)"
|
||||
class="link-text continue-ship-link"
|
||||
@click="detail(row, true)"
|
||||
>{{ row.orderStatus === "PARTS_DELIVERED" ? "继续发货" : "发货" }}</a>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
· {{ clientTypeText(row.clientType) }}
|
||||
· {{ paymentMethodText(row.paymentMethod) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="doudian-body">
|
||||
<div class="goods-stack">
|
||||
<div
|
||||
v-for="(item, index) in orderGoods(row)"
|
||||
:key="item.sn || index"
|
||||
class="goods-row"
|
||||
>
|
||||
<div class="goods-info">
|
||||
<img class="goods-img" :src="item.image" alt="" />
|
||||
<div class="goods-text">
|
||||
<div class="goods-name">{{ item.name || "—" }}</div>
|
||||
<div v-if="item.specs" class="goods-spec">{{ item.specs }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="goods-price">
|
||||
<div>{{ item.goodsPrice != null ? $filters.unitPrice(item.goodsPrice, "¥") : "—" }}</div>
|
||||
<div class="goods-num">x{{ item.num || 0 }}</div>
|
||||
</div>
|
||||
<div class="goods-after">{{ afterSaleText(item.afterSaleStatus) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="merge-cell pay-cell">{{ $filters.unitPrice(row.flowPrice, "¥") }}</div>
|
||||
<div class="merge-cell buyer-cell">
|
||||
<div>{{ row.memberName || "—" }}</div>
|
||||
<div v-if="row.consigneeName">{{ row.consigneeName }}</div>
|
||||
<div v-if="row.consigneeMobile" class="buyer-mobile">{{ row.consigneeMobile }}</div>
|
||||
</div>
|
||||
<div class="merge-cell">
|
||||
<el-tag :type="orderStatusTagType(row.orderStatus)">{{ orderStatusText(row.orderStatus) }}</el-tag>
|
||||
</div>
|
||||
<div class="merge-cell action-cell">
|
||||
<a class="link-text" @click="detail(row)">详情</a>
|
||||
<a v-if="canShip(row)" class="link-text" @click="detail(row, true)">
|
||||
{{ row.orderStatus === "PARTS_DELIVERED" ? "继续发货" : "发货" }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
||||
<el-pagination
|
||||
@@ -137,10 +164,12 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { CopyDocument } from "@element-plus/icons-vue";
|
||||
import * as API_Order from "@/api/order";
|
||||
import { canShowShipButton, isNonShippableOrder } from "@/utils/orderFulfill";
|
||||
export default {
|
||||
name: "orderList",
|
||||
components: { CopyDocument },
|
||||
data() {
|
||||
return {
|
||||
fields: {
|
||||
@@ -176,8 +205,8 @@ export default {
|
||||
orderType: "",
|
||||
orderSn: "",
|
||||
keywords: "",
|
||||
buyerName: "",
|
||||
goodsName: "",
|
||||
goodsType: "",
|
||||
shipName: "",
|
||||
orderStatus: "",
|
||||
paymentMethod: "",
|
||||
@@ -237,6 +266,40 @@ export default {
|
||||
onStatusTabClick(tab) {
|
||||
this.orderStatusClick(tab.paneName);
|
||||
},
|
||||
orderGoods(row) {
|
||||
const items = row && row.orderItems;
|
||||
return items && items.length ? items : [{}];
|
||||
},
|
||||
afterSaleText(status) {
|
||||
const map = {
|
||||
NOT_APPLIED: "—",
|
||||
NEW: "—",
|
||||
PART_AFTER_SALE: "部分售后",
|
||||
ALREADY_APPLIED: "售后中",
|
||||
EXPIRED: "不可售后",
|
||||
};
|
||||
return map[status] || "—";
|
||||
},
|
||||
copyText(text) {
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
const done = () => this.$Message.success("已复制订单编号");
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(done).catch(() => this.copyTextFallback(text, done));
|
||||
return;
|
||||
}
|
||||
this.copyTextFallback(text, done);
|
||||
},
|
||||
copyTextFallback(text, done) {
|
||||
const input = document.createElement("textarea");
|
||||
input.value = text;
|
||||
document.body.appendChild(input);
|
||||
input.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(input);
|
||||
done();
|
||||
},
|
||||
clientTypeText(v) {
|
||||
const map = { H5: "移动端", PC: "PC端", WECHAT_MP: "小程序端", APP: "移动应用端" };
|
||||
return map[v] || v || "-";
|
||||
@@ -397,7 +460,7 @@ export default {
|
||||
this.getDataList();
|
||||
},
|
||||
getOrderNumData() {
|
||||
// orderNum 接口仅查 li_order,不含 order_item 关联;keywords/goodsName 会引用 oi 字段导致 SQL 报错
|
||||
// orderNum 接口仅查 li_order;keywords/goodsName 会引用 oi 字段。goodsType 用 EXISTS,可以带上。
|
||||
const { orderStatus, keywords, goodsName, ...searchParams } = this.searchForm;
|
||||
API_Order.getOrderNum(searchParams)
|
||||
.then((res) => {
|
||||
@@ -422,6 +485,33 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.order-search-form {
|
||||
display: grid !important;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
column-gap: 16px;
|
||||
align-items: start;
|
||||
|
||||
> :deep(.el-form-item) {
|
||||
display: flex;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
margin: 8px 0 !important;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__content) {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
:deep(.el-input),
|
||||
:deep(.el-select),
|
||||
:deep(.el-date-editor) {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
.export {
|
||||
margin: 10px 20px 10px 0;
|
||||
}
|
||||
@@ -431,11 +521,163 @@ export default {
|
||||
}
|
||||
}
|
||||
.link-text {
|
||||
color: #409eff;
|
||||
color: #1966ff;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
.continue-ship-link {
|
||||
margin-left: 8px;
|
||||
.doudian-list {
|
||||
min-height: 120px;
|
||||
}
|
||||
.doudian-head,
|
||||
.doudian-body {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
minmax(240px, 2.4fr)
|
||||
minmax(108px, 1fr)
|
||||
minmax(96px, 0.95fr)
|
||||
minmax(108px, 1fr)
|
||||
minmax(148px, 1.2fr)
|
||||
minmax(100px, 0.9fr)
|
||||
88px;
|
||||
}
|
||||
.doudian-head {
|
||||
background: #f7f8fa;
|
||||
color: #86909c;
|
||||
font-size: 13px;
|
||||
line-height: 40px;
|
||||
}
|
||||
.doudian-head > div:nth-child(n + 6) {
|
||||
text-align: center;
|
||||
}
|
||||
.doudian-head > div,
|
||||
.goods-row > div,
|
||||
.merge-cell {
|
||||
padding: 0 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.doudian-empty {
|
||||
padding: 48px 0;
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
}
|
||||
.doudian-order {
|
||||
margin-top: 12px;
|
||||
border: 1px solid #eeeff0;
|
||||
}
|
||||
.doudian-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 8px 12px;
|
||||
background: #f7f8fa;
|
||||
color: #565960;
|
||||
font-size: 12px;
|
||||
}
|
||||
.doudian-sn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.copy-icon {
|
||||
display: inline-flex;
|
||||
color: #86909c;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
.copy-icon:hover {
|
||||
color: #1966ff;
|
||||
}
|
||||
.doudian-bar-extra {
|
||||
margin-left: auto;
|
||||
color: #86909c;
|
||||
}
|
||||
.doudian-body {
|
||||
align-items: stretch;
|
||||
}
|
||||
.goods-stack {
|
||||
grid-column: 1 / 4;
|
||||
display: grid;
|
||||
grid-template-columns: subgrid;
|
||||
min-width: 0;
|
||||
}
|
||||
.goods-row {
|
||||
display: grid;
|
||||
grid-column: 1 / -1;
|
||||
grid-template-columns: subgrid;
|
||||
min-height: 84px;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #f2f3f5;
|
||||
}
|
||||
.goods-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.goods-info {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
.goods-img {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
background: #f5f7fa;
|
||||
flex: none;
|
||||
}
|
||||
.goods-text {
|
||||
min-width: 0;
|
||||
}
|
||||
.goods-name {
|
||||
color: #252931;
|
||||
line-height: 20px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.goods-spec,
|
||||
.goods-num,
|
||||
.buyer-mobile {
|
||||
margin-top: 4px;
|
||||
color: #86909c;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
.goods-price,
|
||||
.goods-after,
|
||||
.merge-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
text-align: left;
|
||||
color: #252931;
|
||||
font-size: 13px;
|
||||
}
|
||||
.doudian-body > .merge-cell:nth-child(n + 4) {
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
.goods-price,
|
||||
.goods-after {
|
||||
min-height: 84px;
|
||||
border-left: 1px solid #f2f3f5;
|
||||
}
|
||||
.merge-cell {
|
||||
border-left: 1px solid #f2f3f5;
|
||||
padding: 12px;
|
||||
}
|
||||
.pay-cell {
|
||||
color: #252931;
|
||||
font-weight: 500;
|
||||
}
|
||||
.buyer-cell {
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
}
|
||||
.action-cell .link-text {
|
||||
line-height: 24px;
|
||||
}
|
||||
</style>
|
||||
|
||||
233
seller/src/views/order/order/orderVerification.vue
Normal file
233
seller/src/views/order/order/orderVerification.vue
Normal file
@@ -0,0 +1,233 @@
|
||||
<template>
|
||||
<div class="search">
|
||||
<el-card>
|
||||
<el-tabs v-model="activeTab" @tab-change="onTabChange">
|
||||
<el-tab-pane label="订单核销" name="verify">
|
||||
<el-form class="verify-form" @submit.prevent>
|
||||
<el-form-item label="核销码">
|
||||
<el-input
|
||||
v-model="code"
|
||||
placeholder="请输入核销码"
|
||||
clearable
|
||||
maxlength="32"
|
||||
style="width: 280px"
|
||||
@keyup.enter="confirmVerify"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="lookingUp" @click="confirmVerify">确定</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="核销记录" name="records">
|
||||
<el-table v-loading="loading" :data="records" border style="width: 100%">
|
||||
<el-table-column prop="code" label="核销码" min-width="120" />
|
||||
<el-table-column prop="orderSn" label="订单编号" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="orderItemSn" label="订单货物编号" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="商品信息" min-width="260">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row" class="goods-cell">
|
||||
<img v-if="row.image" :src="row.image" class="goods-image" alt="" />
|
||||
<div>
|
||||
<div>{{ row.goodsName || "—" }}</div>
|
||||
<div v-if="specText(row.specs)" class="goods-spec">{{ specText(row.specs) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="verifiedTime" label="核销时间" width="170" />
|
||||
<el-table-column prop="verifierName" label="核销员" min-width="120" show-overflow-tooltip />
|
||||
</el-table>
|
||||
<div class="pager">
|
||||
<el-pagination
|
||||
v-model:current-page="pageNumber"
|
||||
v-model:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
size="small"
|
||||
@current-change="loadRecords"
|
||||
@size-change="onSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="confirmVisible" title="确认核销" width="720px" :close-on-click-modal="false">
|
||||
<div class="preview-meta">
|
||||
<span>核销码:{{ preview.code || "—" }}</span>
|
||||
<span>订单编号:{{ preview.orderSn || "—" }}</span>
|
||||
</div>
|
||||
<el-table :data="preview.goods || []" border>
|
||||
<el-table-column prop="orderItemSn" label="订单货物编号" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="商品信息" min-width="260">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row" class="goods-cell">
|
||||
<img v-if="row.image" :src="row.image" class="goods-image" alt="" />
|
||||
<div>
|
||||
<div>{{ row.goodsName || "—" }}</div>
|
||||
<div v-if="specText(row.specs)" class="goods-spec">{{ specText(row.specs) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="num" label="数量" width="80" />
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-button @click="confirmVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submitVerify">确认核销</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { confirmVerification, getVerificationRecords, previewVerification } from "@/api/order";
|
||||
|
||||
export default {
|
||||
name: "orderVerification",
|
||||
data() {
|
||||
return {
|
||||
activeTab: "verify",
|
||||
code: "",
|
||||
lookingUp: false,
|
||||
submitting: false,
|
||||
confirmVisible: false,
|
||||
preview: {
|
||||
code: "",
|
||||
orderSn: "",
|
||||
goods: [],
|
||||
},
|
||||
loading: false,
|
||||
records: [],
|
||||
total: 0,
|
||||
pageNumber: 1,
|
||||
pageSize: 20,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
specText(specs) {
|
||||
if (!specs) return "";
|
||||
try {
|
||||
const obj = typeof specs === "string" ? JSON.parse(specs) : specs;
|
||||
if (!obj || typeof obj !== "object") return "";
|
||||
return Object.keys(obj)
|
||||
.filter((key) => key !== "images")
|
||||
.map((key) => `${key}:${obj[key]}`)
|
||||
.join(" ");
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
confirmVerify() {
|
||||
const code = (this.code || "").trim();
|
||||
if (!code) {
|
||||
this.$Message.warning("请输入核销码");
|
||||
return;
|
||||
}
|
||||
this.lookingUp = true;
|
||||
previewVerification(code)
|
||||
.then((res) => {
|
||||
this.lookingUp = false;
|
||||
if (res.success && res.result) {
|
||||
this.preview = {
|
||||
code: res.result.code || code,
|
||||
orderSn: res.result.orderSn || "",
|
||||
goods: res.result.goods || [],
|
||||
};
|
||||
this.confirmVisible = true;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.lookingUp = false;
|
||||
});
|
||||
},
|
||||
submitVerify() {
|
||||
const code = (this.preview.code || "").trim();
|
||||
if (!code) {
|
||||
this.$Message.warning("请输入核销码");
|
||||
return;
|
||||
}
|
||||
this.submitting = true;
|
||||
confirmVerification(code)
|
||||
.then((res) => {
|
||||
this.submitting = false;
|
||||
if (res.success) {
|
||||
this.$Message.success(res.result || "核销成功");
|
||||
this.confirmVisible = false;
|
||||
this.code = "";
|
||||
this.preview = { code: "", orderSn: "", goods: [] };
|
||||
this.pageNumber = 1;
|
||||
this.loadRecords();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.submitting = false;
|
||||
});
|
||||
},
|
||||
onTabChange(name) {
|
||||
if (name === "records") {
|
||||
this.loadRecords();
|
||||
}
|
||||
},
|
||||
onSizeChange() {
|
||||
this.pageNumber = 1;
|
||||
this.loadRecords();
|
||||
},
|
||||
loadRecords() {
|
||||
this.loading = true;
|
||||
getVerificationRecords({
|
||||
pageNumber: this.pageNumber,
|
||||
pageSize: this.pageSize,
|
||||
})
|
||||
.then((res) => {
|
||||
this.loading = false;
|
||||
if (res.success) {
|
||||
this.records = res.result.records || [];
|
||||
this.total = res.result.total || 0;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.verify-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 12px 0 4px;
|
||||
}
|
||||
.preview-meta {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
margin-bottom: 12px;
|
||||
color: #303133;
|
||||
}
|
||||
.goods-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.goods-image {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.goods-spec {
|
||||
margin-top: 4px;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
.pager {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -39,6 +39,10 @@ module.exports = {
|
||||
if (message.includes("ResizeObserver loop")) {
|
||||
return false;
|
||||
}
|
||||
// 腾讯地图 GL 在跨域脚本里抛错时,浏览器只上报 Script error.
|
||||
if (message === "Script error." || message.includes("far <= 0") || message.includes("near <= 0")) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user