From 1bad9a14dbf0eb5107c5981e2b274a264b1bf0a1 Mon Sep 17 00:00:00 2001 From: "pikachu1995@126.com" Date: Sun, 20 Sep 2026 17:06:16 +0800 Subject: [PATCH] =?UTF-8?q?feat(appointment):=20=E9=A2=84=E7=BA=A6?= =?UTF-8?q?=E5=95=86=E5=93=81=E5=8F=91=E5=B8=83=E6=94=B9=E4=B8=BA=E5=8F=AF?= =?UTF-8?q?=E8=A7=86=E5=8C=96=E9=85=8D=E7=BD=AE=EF=BC=8C=E5=B9=B6=E6=8E=A5?= =?UTF-8?q?=E5=85=A5=E8=A1=A8=E5=8D=95=E8=AE=BE=E8=AE=A1=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 商家端补齐预约规则、时段库存与系统表单设计;运营端增加预约工单与订单设置。 Co-authored-by: Cursor --- manager/src/api/appointment.js | 24 + manager/src/constants/goodsType.js | 7 + manager/src/router/router.js | 12 + .../src/views/appointment/orderSettings.vue | 76 ++ .../src/views/appointment/workOrderList.vue | 80 ++ manager/src/views/goods/goods-info/goods.vue | 2 + manager/src/views/order/order/orderDetail.vue | 182 +++- manager/src/views/order/order/orderList.vue | 7 +- .../setting-manage/setting/ORDER_SETTING.vue | 2 +- seller/src/api/appointment.js | 81 ++ seller/src/constants/appointment.js | 135 +++ seller/src/constants/goodsType.js | 7 + seller/src/constants/systemForm.js | 327 +++++++ seller/src/router/router.js | 49 + .../components/SystemFormFieldPreview.vue | 400 +++++++++ .../src/views/appointment/inStoreSettings.vue | 87 ++ .../src/views/appointment/onSiteSettings.vue | 103 +++ .../views/appointment/rescheduleLogList.vue | 95 ++ .../src/views/appointment/serviceCalendar.vue | 127 +++ .../views/appointment/serviceStaffList.vue | 146 +++ .../views/appointment/systemFormDesigner.vue | 849 ++++++++++++++++++ .../src/views/appointment/systemFormList.vue | 137 +++ .../src/views/appointment/workOrderList.vue | 184 ++++ .../components/AppointmentPublishPanel.vue | 836 +++++++++++++++++ seller/src/views/goods/goods-seller/goods.vue | 2 + .../goods/goods-seller/goodsOperation.vue | 3 +- .../goods-seller/goodsOperationFirst.vue | 183 ++-- .../goods/goods-seller/goodsOperationSec.vue | 810 +++++++++++++---- seller/src/views/order/order/orderDetail.vue | 210 ++++- 29 files changed, 4933 insertions(+), 230 deletions(-) create mode 100644 manager/src/api/appointment.js create mode 100644 manager/src/views/appointment/orderSettings.vue create mode 100644 manager/src/views/appointment/workOrderList.vue create mode 100644 seller/src/api/appointment.js create mode 100644 seller/src/constants/appointment.js create mode 100644 seller/src/constants/systemForm.js create mode 100644 seller/src/views/appointment/components/SystemFormFieldPreview.vue create mode 100644 seller/src/views/appointment/inStoreSettings.vue create mode 100644 seller/src/views/appointment/onSiteSettings.vue create mode 100644 seller/src/views/appointment/rescheduleLogList.vue create mode 100644 seller/src/views/appointment/serviceCalendar.vue create mode 100644 seller/src/views/appointment/serviceStaffList.vue create mode 100644 seller/src/views/appointment/systemFormDesigner.vue create mode 100644 seller/src/views/appointment/systemFormList.vue create mode 100644 seller/src/views/appointment/workOrderList.vue create mode 100644 seller/src/views/goods/goods-seller/components/AppointmentPublishPanel.vue diff --git a/manager/src/api/appointment.js b/manager/src/api/appointment.js new file mode 100644 index 00000000..83d129b8 --- /dev/null +++ b/manager/src/api/appointment.js @@ -0,0 +1,24 @@ +/** + * 运营端预约服务 API + */ +import { getRequest, postRequestWithNoForm, putRequestWithNoForm } from "@/libs/axios"; + +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) => + putRequestWithNoForm("/appointment/settings", params); + +export const platformRefund = (orderSn, params) => + postRequestWithNoForm(`/appointment/orders/${orderSn}/platform-refund`, params); + +export const noShowRefund = (orderSn, params) => + postRequestWithNoForm(`/appointment/orders/${orderSn}/no-show-refund`, params); + +export const getAppointmentOrderDetail = (orderSn) => + getRequest(`/appointment/orders/${orderSn}`); diff --git a/manager/src/constants/goodsType.js b/manager/src/constants/goodsType.js index ee8660f2..e66e1f04 100644 --- a/manager/src/constants/goodsType.js +++ b/manager/src/constants/goodsType.js @@ -5,14 +5,20 @@ * @date 2026-08-02 */ export const E_COUPON_GOODS_TYPE = "E_COUPON"; +export const APPOINTMENT_GOODS_TYPE = "APPOINTMENT_GOODS"; export function isECoupon(goodsType) { return goodsType === E_COUPON_GOODS_TYPE; } +export function isAppointmentGoods(goodsType) { + return goodsType === APPOINTMENT_GOODS_TYPE; +} + export function goodsTypeLabel(goodsType) { const map = { E_COUPON: "电子卡券", + APPOINTMENT_GOODS: "预约商品", VIRTUAL_GOODS: "虚拟商品", PHYSICAL_GOODS: "实物商品", }; @@ -21,6 +27,7 @@ export function goodsTypeLabel(goodsType) { export function goodsTypeTagType(goodsType) { if (goodsType === E_COUPON_GOODS_TYPE) return "warning"; + if (goodsType === APPOINTMENT_GOODS_TYPE) return "success"; if (goodsType === "VIRTUAL_GOODS") return "info"; return ""; } diff --git a/manager/src/router/router.js b/manager/src/router/router.js index 06dfe87f..e861d650 100644 --- a/manager/src/router/router.js +++ b/manager/src/router/router.js @@ -128,6 +128,18 @@ export const otherRouter = { name: "order-detail", component: () => import("@/views/order/order/orderDetail.vue") }, + { + path: "appointment/work-orders", + title: "预约工单", + name: "manager-appointment-work-orders", + component: () => import("@/views/appointment/workOrderList.vue") + }, + { + path: "appointment/order-settings", + title: "预约订单设置", + name: "manager-appointment-order-settings", + component: () => import("@/views/appointment/orderSettings.vue") + }, { path: "after-order-detail", title: "售后单详情", diff --git a/manager/src/views/appointment/orderSettings.vue b/manager/src/views/appointment/orderSettings.vue new file mode 100644 index 00000000..655c3f57 --- /dev/null +++ b/manager/src/views/appointment/orderSettings.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/manager/src/views/appointment/workOrderList.vue b/manager/src/views/appointment/workOrderList.vue new file mode 100644 index 00000000..5152b5fa --- /dev/null +++ b/manager/src/views/appointment/workOrderList.vue @@ -0,0 +1,80 @@ + + + diff --git a/manager/src/views/goods/goods-info/goods.vue b/manager/src/views/goods/goods-info/goods.vue index 9064e3ea..3d234712 100644 --- a/manager/src/views/goods/goods-info/goods.vue +++ b/manager/src/views/goods/goods-info/goods.vue @@ -55,6 +55,7 @@ + @@ -388,6 +389,7 @@ export default { if (v === "PHYSICAL_GOODS") return "实物商品"; if (v === "VIRTUAL_GOODS") return "虚拟商品"; if (v === "E_COUPON") return "电子卡券"; // 卡密商品 + if (v === "APPOINTMENT_GOODS") return "预约商品"; return v || "—"; }, marketEnableText(v) { diff --git a/manager/src/views/order/order/orderDetail.vue b/manager/src/views/order/order/orderDetail.vue index 7c9b9e73..f7f9e9e4 100644 --- a/manager/src/views/order/order/orderDetail.vue +++ b/manager/src/views/order/order/orderDetail.vue @@ -6,6 +6,8 @@ 调整价格 修改收货地址 订单取消 + 平台退款 + 爽约退款 收款 订单日志 打印发货单 @@ -37,6 +39,20 @@ {{ orderInfo.order.createTime }} +
+
预约时段:
+
{{ appointmentSlotText }}
+
+
+
{{ appointmentFormGroups.length > 1 ? `补充信息${gi + 1}:` : '补充信息:' }}
+
+
{{ row.fieldLabel }}:{{ row.value }}
+
+
@@ -463,11 +479,48 @@ + + + + + + 未服务 + 已服务 + + + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/seller/src/views/appointment/inStoreSettings.vue b/seller/src/views/appointment/inStoreSettings.vue new file mode 100644 index 00000000..fbdbffb4 --- /dev/null +++ b/seller/src/views/appointment/inStoreSettings.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/seller/src/views/appointment/onSiteSettings.vue b/seller/src/views/appointment/onSiteSettings.vue new file mode 100644 index 00000000..b0d85d41 --- /dev/null +++ b/seller/src/views/appointment/onSiteSettings.vue @@ -0,0 +1,103 @@ + + + diff --git a/seller/src/views/appointment/rescheduleLogList.vue b/seller/src/views/appointment/rescheduleLogList.vue new file mode 100644 index 00000000..f8bb545e --- /dev/null +++ b/seller/src/views/appointment/rescheduleLogList.vue @@ -0,0 +1,95 @@ + + + diff --git a/seller/src/views/appointment/serviceCalendar.vue b/seller/src/views/appointment/serviceCalendar.vue new file mode 100644 index 00000000..8c9a007f --- /dev/null +++ b/seller/src/views/appointment/serviceCalendar.vue @@ -0,0 +1,127 @@ + + + + + diff --git a/seller/src/views/appointment/serviceStaffList.vue b/seller/src/views/appointment/serviceStaffList.vue new file mode 100644 index 00000000..b0e9dc33 --- /dev/null +++ b/seller/src/views/appointment/serviceStaffList.vue @@ -0,0 +1,146 @@ + + + diff --git a/seller/src/views/appointment/systemFormDesigner.vue b/seller/src/views/appointment/systemFormDesigner.vue new file mode 100644 index 00000000..37c3b229 --- /dev/null +++ b/seller/src/views/appointment/systemFormDesigner.vue @@ -0,0 +1,849 @@ + + + + + diff --git a/seller/src/views/appointment/systemFormList.vue b/seller/src/views/appointment/systemFormList.vue new file mode 100644 index 00000000..19517bb0 --- /dev/null +++ b/seller/src/views/appointment/systemFormList.vue @@ -0,0 +1,137 @@ + + + diff --git a/seller/src/views/appointment/workOrderList.vue b/seller/src/views/appointment/workOrderList.vue new file mode 100644 index 00000000..eea736f8 --- /dev/null +++ b/seller/src/views/appointment/workOrderList.vue @@ -0,0 +1,184 @@ + + + diff --git a/seller/src/views/goods/goods-seller/components/AppointmentPublishPanel.vue b/seller/src/views/goods/goods-seller/components/AppointmentPublishPanel.vue new file mode 100644 index 00000000..b1035f9a --- /dev/null +++ b/seller/src/views/goods/goods-seller/components/AppointmentPublishPanel.vue @@ -0,0 +1,836 @@ + + + + + diff --git a/seller/src/views/goods/goods-seller/goods.vue b/seller/src/views/goods/goods-seller/goods.vue index 56aa8cdc..5843466a 100644 --- a/seller/src/views/goods/goods-seller/goods.vue +++ b/seller/src/views/goods/goods-seller/goods.vue @@ -55,6 +55,7 @@ + @@ -261,6 +262,7 @@ export default { if (v === "PHYSICAL_GOODS") return "实物商品"; if (v === "VIRTUAL_GOODS") return "虚拟商品"; if (v === "E_COUPON") return "电子卡券"; // 卡密商品 + if (v === "APPOINTMENT_GOODS") return "预约商品"; return v || "—"; }, /** 跳转卡池管理;单 SKU 商品默认取第一个 SKU(card-key-pool 需 skuId) */ diff --git a/seller/src/views/goods/goods-seller/goodsOperation.vue b/seller/src/views/goods/goods-seller/goodsOperation.vue index df7a6bb0..d60a1aff 100644 --- a/seller/src/views/goods/goods-seller/goodsOperation.vue +++ b/seller/src/views/goods/goods-seller/goodsOperation.vue @@ -1,6 +1,7 @@ - - + + + @@ -844,6 +930,12 @@ import { resolveDefaultFulfillmentMode, canShowShipButton, } from "@/utils/orderFulfill"; +import { + getEnabledServiceStaff, + getAppointmentVerificationByOrderSn, + verifyAppointmentCode, +} from "@/api/appointment"; +import { WORK_ORDER_STATUS_LABEL, parseFormAnswerGroups } from "@/constants/appointment"; export default { name: "orderDetail", components: { @@ -921,6 +1013,23 @@ export default { orderTakeForm: { qrCode: "", }, + appointmentVerification: { + codes: [], + totalCount: 0, + verifiedCount: 0, + workOrderStatus: "", + workOrderVisible: false, + }, + enabledStaffList: [], + appointmentTakeForm: { + staffId: "", + code: "", + }, + appointmentVerifyLoading: false, + appointmentTakeValidate: { + staffId: [{ required: true, message: "请选择服务人员", trigger: "change" }], + code: [{ required: true, message: "核销码不能为空", trigger: "blur" }], + }, orderTakeValidate: { qrCode: [{ required: true, message: "订单核销码不能为空", trigger: "blur" }], }, @@ -967,7 +1076,27 @@ export default { return t === "VIRTUAL" || this.$route.query.orderType === "VIRTUAL"; }, isNonPhysicalOrder() { - return this.isVirtualOrder || this.isECouponOrder; + return this.isVirtualOrder || this.isECouponOrder || this.isAppointmentOrder; + }, + isAppointmentOrder() { + const t = this.orderInfo?.order?.orderType; + // 详情未回时用路由 query 兜底 + return t === "APPOINTMENT" || this.$route.query.orderType === "APPOINTMENT"; + }, + appointmentWorkOrderStatusText() { + return WORK_ORDER_STATUS_LABEL[this.appointmentVerification.workOrderStatus] + || this.appointmentVerification.workOrderStatus + || "—"; + }, + appointmentSlotText() { + const v = this.appointmentVerification || {}; + if (!v.slotDate) return ""; + const date = String(v.slotDate).slice(0, 10); + const slot = v.slotStartTime && v.slotEndTime ? `${v.slotStartTime}-${v.slotEndTime}` : ""; + return [date, slot].filter(Boolean).join(" "); + }, + appointmentFormGroups() { + return parseFormAnswerGroups(this.appointmentVerification?.formAnswerJson); }, hasCardKeySection() { return orderHasCardKeySection({ @@ -997,6 +1126,12 @@ export default { }); }, canOrderTake() { + if (this.isAppointmentOrder) { + const order = this.orderInfo?.order; + if (!order || order.orderStatus !== "TAKE") return false; + return (this.appointmentVerification.verifiedCount || 0) + < (this.appointmentVerification.totalCount || 0); + } if (this.allowOperation.take) return true; const order = this.orderInfo?.order; if (!order) return false; @@ -1299,6 +1434,9 @@ export default { : []; this.getContentPrice(); this.getOrderPrice(); + if (this.isAppointmentOrder) { + this.loadAppointmentVerification(); + } if ( this.autoOpenFulfill && this.canShip && @@ -1487,13 +1625,73 @@ export default { handelCancel () { this.orderLogModal = false; }, + loadAppointmentVerification() { + getAppointmentVerificationByOrderSn(this.sn) + .then((res) => { + if (res.success && res.result) { + this.appointmentVerification = res.result; + } + }) + .catch(() => {}); + }, + loadEnabledStaff() { + getEnabledServiceStaff() + .then((res) => { + if (res.success) { + this.enabledStaffList = res.result || []; + } + }) + .catch(() => {}); + }, orderTake() { + if (this.isAppointmentOrder) { + this.appointmentTakeForm = { staffId: "", code: "" }; + this.loadEnabledStaff(); + this.loadAppointmentVerification(); + this.orderTakeModal = true; + this.$nextTick(() => { + this.$refs.appointmentTakeForm?.clearValidate(); + }); + return; + } this.orderTakeForm.qrCode = this.orderInfo.order.verificationCode || ""; this.orderTakeModal = true; this.$nextTick(() => { this.$refs.orderTakeForm?.clearValidate(); }); }, + appointmentVerifySubmit() { + this.$refs.appointmentTakeForm.validate((valid) => { + if (!valid) return; + this.appointmentVerifyLoading = true; + verifyAppointmentCode({ + code: this.appointmentTakeForm.code, + staffId: this.appointmentTakeForm.staffId, + }) + .then((res) => { + if (!res.success) return; + const result = res.result || {}; + this.appointmentTakeForm.code = ""; + this.loadAppointmentVerification(); + if (result.allVerified) { + if (result.orderCompleted) { + this.$Message.success("全部核销完成,订单已完成"); + this.orderTakeModal = false; + this.getDataDetail(); + } else { + this.$Message.success("全部核销完成,请在工单中结束服务"); + this.orderTakeModal = false; + this.getDataDetail(); + } + } else { + this.$Message.success(`核销成功(${result.verifiedCount}/${result.totalCount})`); + } + }) + .finally(() => { + this.appointmentVerifyLoading = false; + }); + }); + }, orderTakeSubmit() { this.$refs.orderTakeForm.validate((valid) => { if (!valid) return;