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;