zhuli 1 mesiac pred
rodič
commit
02ee7637de

+ 10 - 0
src/api/modules/scheduledService.ts

@@ -172,3 +172,13 @@ export const bookingSettingsSave = (data: Record<string, any>) => {
 export const getBookingBusinessHours = (params: { settingsId?: number | string }) => {
   return httpApi.get("/alienStore/store/reservation/getBookingBusinessHours", params);
 };
+
+/** 分类下桌号是否有预定(true 则不可编辑/删除分类) */
+export const getHasReservation = (params: { id: number | string }) => {
+  return httpApi.get(`/store/booking/category/hasReservation`, params);
+};
+
+/** 桌位是否有预定(true 则不可编辑/删除该桌) */
+export const getTableHasReservation = (params: { id: number | string }) => {
+  return httpApi.get(`/store/booking/table/hasReservation`, params);
+};

+ 152 - 69
src/views/appoinmentManagement/appoinmentInfo.vue

@@ -3,12 +3,6 @@
     <!-- 筛选:点击搜索后生效(与翻页联动) -->
     <div class="filter-bar">
       <div class="filter-row">
-        <span class="filter-label">姓名</span>
-        <el-input v-model="searchForm.name" placeholder="请输入" clearable style="width: 160px" />
-        <span class="filter-label">状态</span>
-        <el-select v-model="searchForm.status" placeholder="请选择" clearable style="width: 160px">
-          <el-option v-for="item in statusOptions" :key="String(item.value)" :label="item.label" :value="item.value" />
-        </el-select>
         <span class="filter-label">预订日期</span>
         <el-date-picker
           v-model="searchForm.dateRange"
@@ -17,7 +11,7 @@
           start-placeholder="请选择"
           end-placeholder="请选择"
           value-format="YYYY-MM-DD"
-          style="width: 260px"
+          style="width: 160px"
         />
         <el-button type="primary" @click="handleSearch"> 搜索 </el-button>
         <el-button @click="handleReset"> 重置 </el-button>
@@ -31,7 +25,7 @@
         </div>
       </template>
       <template #statusText="{ row }">
-        <el-tag :type="getStatusTagType(row.status)" size="small">
+        <el-tag :type="getStatusTagType(row.orderStatus)" size="small">
           {{ row.statusText }}
         </el-tag>
       </template>
@@ -41,35 +35,37 @@
         </el-tooltip>
         <span v-else>—</span>
       </template>
+      <!-- 操作按钮与商家端 1.vue 一致:orderStatus 0待支付 1待使用 2已完成 3已过期 4已取消 5已关闭 6退款中 7已退款 8商家预订 -->
       <template #operation="scope">
-        <template v-if="scope.row.status === 0 || scope.row.status === '0'">
+        <template v-if="isOrderStatus(scope.row, 1)">
           <el-button link type="primary" @click="handleCancel(scope.row)"> 取消 </el-button>
         </template>
-        <template v-else-if="scope.row.status === 1 || scope.row.status === '1'">
-          <el-button link type="primary" @click="handleAddTime(scope.row)"> 加时 </el-button>
+        <template v-else-if="isOrderStatus(scope.row, 7)">
           <el-button link type="primary" @click="handleDelete(scope.row)"> 删除 </el-button>
         </template>
-        <template v-else-if="scope.row.status === 2 || scope.row.status === '2'">
-          <el-button v-if="scope.row.canRefund !== false" link type="primary" @click="handleRefund(scope.row)"> 退款 </el-button>
+        <template
+          v-else-if="
+            (isOrderStatus(scope.row, 2) || isOrderStatus(scope.row, 7) || isOrderStatus(scope.row, 3)) &&
+            isMoreThanThreeHoursAfterEnd(scope.row)
+          "
+        >
           <el-button link type="primary" @click="handleDelete(scope.row)"> 删除 </el-button>
         </template>
-        <template v-else-if="scope.row.status === 3 || scope.row.status === '3'">
-          <el-button link type="primary" @click="handleViewReason(scope.row)"> 查看原因 </el-button>
+        <template
+          v-else-if="isOrderStatus(scope.row, 3) && !isMoreThanThreeHoursAfterEnd(scope.row) && orderCostTypeIsPaid(scope.row)"
+        >
+          <el-button link type="primary" @click="handleRefund(scope.row)"> 退款 </el-button>
           <el-button link type="primary" @click="handleDelete(scope.row)"> 删除 </el-button>
         </template>
-        <template v-else-if="scope.row.status === 4 || scope.row.status === '4'">
-          <el-button v-if="scope.row.canRefund !== false" link type="primary" @click="handleRefund(scope.row)"> 退款 </el-button>
+        <template v-else-if="isOrderStatus(scope.row, 4) && !orderCostTypeIsPaid(scope.row)">
           <el-button link type="primary" @click="handleDelete(scope.row)"> 删除 </el-button>
         </template>
-        <template v-else-if="scope.row.status === 5 || scope.row.status === '5'">
-          <el-button link type="primary" @click="handleViewReason(scope.row)"> 查看原因 </el-button>
-        </template>
-        <template v-else-if="scope.row.status === 6 || scope.row.status === '6'">
-          <el-button link type="primary" @click="handleViewReason(scope.row)"> 查看原因 </el-button>
+        <template v-else-if="orderCostTypeIsPaid(scope.row) && isOrderStatus(scope.row, 4) && cancelReasonIsEmpty(scope.row)">
+          <el-button link type="primary" @click="handleRefund(scope.row)"> 退款 </el-button>
           <el-button link type="primary" @click="handleDelete(scope.row)"> 删除 </el-button>
         </template>
-        <template v-else>
-          <el-button link type="primary" @click="handleDelete(scope.row)"> 删除 </el-button>
+        <template v-else-if="isOrderStatus(scope.row, 2) && !isMoreThanThreeHoursAfterEnd(scope.row)">
+          <el-button link type="primary" @click="handleAddTime(scope.row)"> 加时 </el-button>
         </template>
       </template>
     </ProTable>
@@ -86,13 +82,6 @@
         <el-button type="primary" :loading="addTimeLoading" @click="confirmAddTime"> 确定 </el-button>
       </template>
     </el-dialog>
-
-    <!-- 查看原因弹窗 -->
-    <el-dialog v-model="reasonVisible" title="查看原因" width="400px" append-to-body>
-      <p class="reason-text">
-        {{ currentReason || "—" }}
-      </p>
-    </el-dialog>
   </div>
 </template>
 
@@ -122,8 +111,18 @@ export interface ReservationRow {
   amount: string | number;
   customerName: string;
   phone: string;
+  /** 兼容旧字段展示/筛选 */
   status: number | string;
   statusText: string;
+  /** 与商家端 1.vue 一致:订单状态 */
+  orderStatus: number;
+  /** 0 免费 1 付费(退款按钮用) */
+  orderCostType: number;
+  /** 与 1.vue item.reason 一致:空=用户取消,非空=商家取消等 */
+  reason: string;
+  reservationDateRaw: string;
+  timeSlotRaw: string;
+  endTimeRaw?: string | Date;
   remark: string;
   cancelReason?: string;
   refundReason?: string;
@@ -145,26 +144,43 @@ const columns: ColumnProps<ReservationRow>[] = [
   { prop: "phone", label: "电话", width: 120, showOverflowTooltip: true },
   { prop: "statusText", label: "状态", width: 96, align: "center" },
   { prop: "remark", label: "备注", minWidth: 100, showOverflowTooltip: true },
-  { prop: "operation", label: "操作", fixed: "right", width: 170, align: "center" }
+  { prop: "operation", label: "操作", fixed: "right", width: 200, align: "center" }
 ];
 
+/** 与商家端 1.vue 筛选、列表 status 一致(orderStatus) */
 const statusOptions = [
   { label: "全部", value: "" },
-  { label: "待使用", value: 0 },
-  { label: "已完成", value: 1 },
-  { label: "退款", value: 5 }
+  { label: "待使用", value: 1 },
+  { label: "已完成", value: 2 },
+  { label: "退款", value: 6 } // 退款 tab:6退款中(接口若合并 7 需后端支持)
 ];
 
-const STATUS_MAP: Record<number, string> = {
-  0: "待使用",
-  1: "已完成",
-  2: "用户取消",
-  3: "商家取消",
-  4: "已过期",
-  5: "退款中",
-  6: "退款成功"
+/** orderStatus 文案,与 1.vue ORDER_STATUS_TEXT 一致 */
+const ORDER_STATUS_TEXT: Record<number, string> = {
+  0: "待支付",
+  1: "待使用",
+  2: "已完成",
+  3: "已过期",
+  4: "已取消",
+  5: "已关闭",
+  6: "退款中",
+  7: "已退款",
+  8: "商家预订"
+};
+
+/** 旧版 Web 列表 status → orderStatus(无 orderStatus 字段时的兜底) */
+const LEGACY_STATUS_TO_ORDER: Record<number, number> = {
+  0: 1, // 待使用
+  1: 2, // 已完成
+  2: 4, // 用户取消 → 已取消
+  3: 4, // 商家取消 → 已取消
+  4: 3, // 已过期
+  5: 6, // 退款中
+  6: 7 // 退款成功 → 已退款
 };
 
+const THREE_HOURS_MS = 3 * 60 * 60 * 1000;
+
 const initParam = reactive({
   storeId: localGet("geeker-user")?.userInfo?.storeId ?? localGet("createdId") ?? ""
 });
@@ -188,23 +204,60 @@ const addTimeLoading = ref(false);
 const addTimeForm = reactive({ minutes: 30 });
 const currentAddTimeRow = ref<ReservationRow | null>(null);
 
-const reasonVisible = ref(false);
-const currentReason = ref("");
-
 function getStoreId(): number | string | null {
   return localGet("geeker-user")?.userInfo?.storeId ?? localGet("createdId") ?? null;
 }
 
-function getStatusTagType(status: number | string): "primary" | "success" | "warning" | "danger" | "info" {
-  const s = Number(status);
-  if (s === 0) return "primary";
-  if (s === 1) return "success";
-  if (s === 2 || s === 3 || s === 4) return "info";
-  if (s === 5) return "warning";
-  if (s === 6) return "success";
+function getStatusTagType(orderStatus: number | string): "primary" | "success" | "warning" | "danger" | "info" {
+  const s = Number(orderStatus);
+  if (s === 0 || s === 1 || s === 8) return "primary";
+  if (s === 2) return "success";
+  if (s === 6) return "warning";
+  if (s === 7) return "success";
+  if (s === 3 || s === 4 || s === 5) return "info";
   return "info";
 }
 
+function isOrderStatus(row: ReservationRow, n: number) {
+  return Number(row.orderStatus) === n;
+}
+
+function orderCostTypeIsPaid(row: ReservationRow) {
+  return Number(row.orderCostType) === 1;
+}
+
+/** 与 1.vue 一致:reason === '' 时视为用户取消(付费可退款+删除) */
+function cancelReasonIsEmpty(row: ReservationRow) {
+  return String(row.reason ?? "").trim() === "";
+}
+
+function parseRowEndDateTime(row: ReservationRow): Date | null {
+  const et = row.endTimeRaw;
+  if (et != null && et !== "") {
+    const d = et instanceof Date ? et : new Date(et as string);
+    if (!isNaN(d.getTime())) return d;
+  }
+  const date = String(row.reservationDateRaw || "")
+    .trim()
+    .replace(/\//g, "-");
+  const slot = String(row.timeSlotRaw || "").trim();
+  const parts = slot.split("-");
+  const endTimeStr = parts.length > 1 ? parts[parts.length - 1].trim() : "";
+  if (date && endTimeStr) {
+    const normalized = endTimeStr.length <= 5 ? `${date} ${endTimeStr}:00` : `${date} ${endTimeStr}`;
+    const parsed = new Date(normalized.replace(/-/g, "/"));
+    if (!isNaN(parsed.getTime())) return parsed;
+  }
+  return null;
+}
+
+/** 结束时间已过且当前时间超过结束 3 小时(与 1.vue 一致) */
+function isMoreThanThreeHoursAfterEnd(row: ReservationRow) {
+  const end = parseRowEndDateTime(row);
+  if (!end) return false;
+  return Date.now() > end.getTime() + THREE_HOURS_MS;
+}
+
 function formatDateCol(val: string): string {
   if (!val) return "—";
   const d = new Date(val);
@@ -223,11 +276,39 @@ function getWeekDay(val: string): string {
 }
 
 function mapReservationRow(item: any): ReservationRow {
-  const status = item.status ?? item.reservationStatus ?? 0;
   const date = item.reservationDate ?? item.bookingDate ?? item.date ?? "";
   const timeStart = item.startTime ?? item.bookingStartTime ?? "";
-  const timeEnd = item.endTime ?? item.bookingEndTime ?? "";
-  const customerName = item.name ?? item.userName ?? item.contactName ?? "—";
+  const timeEnd = item.bookingEndTime ?? item.endTime ?? "";
+  const customerName =
+    item.customerName != null && String(item.customerName).trim() !== ""
+      ? String(item.customerName).trim()
+      : (item.name ?? item.userName ?? item.contactName ?? "—");
+
+  const hasOrderStatusField =
+    (item.orderStatus != null && item.orderStatus !== "") || (item.statusType != null && item.statusType !== "");
+  const legacySt = Number(item.status ?? item.reservationStatus ?? NaN);
+
+  let orderStatus: number;
+  if (item.orderStatus != null && item.orderStatus !== "") {
+    orderStatus = Number(item.orderStatus);
+  } else if (item.statusType != null && item.statusType !== "") {
+    orderStatus = Number(item.statusType);
+  } else if (Number.isFinite(legacySt) && LEGACY_STATUS_TO_ORDER[legacySt] !== undefined) {
+    orderStatus = LEGACY_STATUS_TO_ORDER[legacySt];
+  } else if (Number.isFinite(legacySt)) {
+    orderStatus = legacySt;
+  } else {
+    orderStatus = 1;
+  }
+
+  let reason = String(item.reason ?? item.cancelReason ?? "").trim();
+  if (!hasOrderStatusField && legacySt === 2) reason = "";
+  if (!hasOrderStatusField && legacySt === 3 && !reason) reason = "商家取消";
+
+  const orderCostType = Number(item.orderCostType ?? item.costType ?? item.reservationCostType ?? 0);
+
+  const timeSlotRaw = item.timeSlot ?? (timeStart && timeEnd ? `${String(timeStart).trim()}-${String(timeEnd).trim()}` : "");
+
   return {
     id: item.id,
     date: formatDateCol(date),
@@ -236,11 +317,23 @@ function mapReservationRow(item: any): ReservationRow {
     peopleCount: item.peopleCount ?? item.personCount ?? item.guestCount ?? "—",
     tableNo: item.tableNo ?? item.tableNumber ?? item.tableNumbers ?? "—",
     timeRange: timeStart && timeEnd ? `${timeStart}-${timeEnd}` : (item.timeRange ?? "—"),
-    amount: item.amount != null && item.amount !== "" ? item.amount : "—",
+    amount:
+      item.depositAmount != null && item.depositAmount !== ""
+        ? item.depositAmount
+        : item.amount != null && item.amount !== ""
+          ? item.amount
+          : "—",
     customerName,
     phone: item.phone ?? item.mobile ?? item.contactPhone ?? "—",
-    status,
-    statusText: STATUS_MAP[Number(status)] ?? item.statusText ?? "—",
+    status: orderStatus,
+    orderStatus,
+    orderCostType,
+    reason,
+    reservationDateRaw: String(date).trim(),
+    timeSlotRaw,
+    endTimeRaw: item.endTime ?? item.bookingEndTime ?? item.reservationEndTime,
+    statusText:
+      (item.orderStatusText && String(item.orderStatusText).trim()) || ORDER_STATUS_TEXT[orderStatus] || item.statusText || "—",
     remark: item.remark ?? item.remarkDesc ?? "",
     cancelReason: item.cancelReason ?? item.reason ?? "",
     refundReason: item.refundReason,
@@ -390,11 +483,6 @@ function handleRefund(row: ReservationRow) {
     })
     .catch(() => {});
 }
-
-function handleViewReason(row: ReservationRow) {
-  currentReason.value = row.cancelReason || row.refundReason || "暂无原因说明";
-  reasonVisible.value = true;
-}
 </script>
 
 <style scoped lang="scss">
@@ -435,9 +523,4 @@ function handleViewReason(row: ReservationRow) {
   white-space: nowrap;
   cursor: default;
 }
-.reason-text {
-  margin: 0;
-  color: #606266;
-  word-break: break-all;
-}
 </style>

+ 45 - 2
src/views/appoinmentManagement/classifyManagement.vue

@@ -82,7 +82,14 @@ import type { UploadRequestOptions, UploadUserFile } from "element-plus";
 import { Plus } from "@element-plus/icons-vue";
 import ProTable from "@/components/ProTable/index.vue";
 import type { ProTableInstance, ColumnProps } from "@/components/ProTable/interface";
-import { scheduleList, scheduleDel, scheduleAdd, scheduleEdit, scheduleDetail } from "@/api/modules/scheduledService";
+import {
+  scheduleList,
+  scheduleDel,
+  scheduleAdd,
+  scheduleEdit,
+  scheduleDetail,
+  getHasReservation
+} from "@/api/modules/scheduledService";
 import { uploadFileToOss } from "@/api/upload.js";
 import { localGet } from "@/utils";
 
@@ -223,6 +230,22 @@ function getStoreId(): number | string | null {
   return localGet("geeker-user")?.userInfo?.storeId ?? localGet("createdId") ?? null;
 }
 
+/** 接口返回 true 表示该分类下桌号存在预定,不可编辑/删除 */
+function parseHasReservationResponse(res: any): boolean {
+  const body = res?.data ?? res;
+  const val = body?.data !== undefined ? body.data : body;
+  if (typeof val === "boolean") return val;
+  if (val != null && typeof val === "object" && "hasReservation" in val) {
+    return Boolean((val as { hasReservation?: boolean }).hasReservation);
+  }
+  return Boolean(val);
+}
+
+async function categoryHasReservation(categoryId: number | string): Promise<boolean> {
+  const res: any = await getHasReservation({ id: categoryId });
+  return parseHasReservationResponse(res);
+}
+
 function handleNew() {
   editId.value = null;
   form.name = "";
@@ -234,6 +257,16 @@ function handleNew() {
 }
 
 async function handleEdit(row: CategoryRow) {
+  try {
+    const has = await categoryHasReservation(row.id);
+    if (has) {
+      ElMessage.warning("此分类所属桌号有预定信息,不可修改");
+      return;
+    }
+  } catch (e: any) {
+    ElMessage.error(e?.message || "校验失败,请稍后重试");
+    return;
+  }
   editId.value = row.id;
   dialogVisible.value = true;
   detailLoading.value = true;
@@ -273,7 +306,17 @@ async function handleEdit(row: CategoryRow) {
   }
 }
 
-function handleDelete(row: CategoryRow) {
+async function handleDelete(row: CategoryRow) {
+  try {
+    const has = await categoryHasReservation(row.id);
+    if (has) {
+      ElMessage.warning("此分类所属桌号有预定信息,不可修改");
+      return;
+    }
+  } catch (e: any) {
+    ElMessage.error(e?.message || "校验失败,请稍后重试");
+    return;
+  }
   ElMessageBox.confirm("是否确认删除?", "提示", {
     confirmButtonText: "确定",
     cancelButtonText: "取消",

+ 3 - 3
src/views/appoinmentManagement/infoManagement.vue

@@ -174,7 +174,7 @@
             </el-radio-group>
           </el-form-item>
           <div v-if="item.allDay === 'notAllDay'" class="special-time-row">
-            <el-form-item label="开始时间" label-width="80px">
+            <el-form-item label="开始时间" label-width="450px">
               <el-time-picker
                 v-model="item.startTime"
                 format="HH:mm"
@@ -187,7 +187,7 @@
                 @change="() => onSpecialTimeChange(item.name, 'start')"
               />
             </el-form-item>
-            <el-form-item label="结束时间" label-width="80px">
+            <el-form-item label="结束时间" label-width="450px">
               <el-time-picker
                 v-model="item.endTime"
                 format="HH:mm"
@@ -204,7 +204,7 @@
         </template>
       </el-form>
     </div>
-    <div style=" display: flex; justify-content: center;width: 100%">
+    <div style="display: flex; justify-content: center; width: 100%">
       <el-button type="primary" :loading="saveLoading" @click="onSave"> 保存 </el-button>
     </div>
   </div>

+ 46 - 2
src/views/appoinmentManagement/tableManagement.vue

@@ -70,7 +70,15 @@ import { ElMessage, ElMessageBox } from "element-plus";
 import type { FormInstance, FormRules } from "element-plus";
 import ProTable from "@/components/ProTable/index.vue";
 import type { ProTableInstance, ColumnProps } from "@/components/ProTable/interface";
-import { scheduleList, tableList, tableDetail, tableAdd, tableEdit, tableDel } from "@/api/modules/scheduledService";
+import {
+  scheduleList,
+  tableList,
+  tableDetail,
+  tableAdd,
+  tableEdit,
+  tableDel,
+  getTableHasReservation
+} from "@/api/modules/scheduledService";
 import { localGet } from "@/utils";
 
 export interface TableRow {
@@ -105,6 +113,22 @@ function getStoreId(): number | string | null {
   return localGet("geeker-user")?.userInfo?.storeId ?? localGet("createdId") ?? null;
 }
 
+/** 接口返回 true 表示该桌有预定,不可编辑/删除 */
+function parseHasReservationResponse(res: any): boolean {
+  const body = res?.data ?? res;
+  const val = body?.data !== undefined ? body.data : body;
+  if (typeof val === "boolean") return val;
+  if (val != null && typeof val === "object" && "hasReservation" in val) {
+    return Boolean((val as { hasReservation?: boolean }).hasReservation);
+  }
+  return Boolean(val);
+}
+
+async function tableHasReservation(tableId: number | string): Promise<boolean> {
+  const res: any = await getTableHasReservation({ id: tableId });
+  return parseHasReservationResponse(res);
+}
+
 async function loadCategories() {
   const storeId = getStoreId();
   if (!storeId) {
@@ -229,6 +253,16 @@ function handleNew() {
 }
 
 async function handleEdit(row: TableRow) {
+  try {
+    const has = await tableHasReservation(row.id);
+    if (has) {
+      ElMessage.warning("此分类所属桌号有预定信息,不可修改");
+      return;
+    }
+  } catch (e: any) {
+    ElMessage.error(e?.message || "校验失败,请稍后重试");
+    return;
+  }
   editId.value = row.id;
   dialogVisible.value = true;
   detailLoading.value = true;
@@ -257,7 +291,17 @@ async function handleEdit(row: TableRow) {
   }
 }
 
-function handleDelete(row: TableRow) {
+async function handleDelete(row: TableRow) {
+  try {
+    const has = await tableHasReservation(row.id);
+    if (has) {
+      ElMessage.warning("此分类所属桌号有预定信息,不可修改");
+      return;
+    }
+  } catch (e: any) {
+    ElMessage.error(e?.message || "校验失败,请稍后重试");
+    return;
+  }
   ElMessageBox.confirm(`是否确认删除?`, "提示", {
     confirmButtonText: "确定",
     cancelButtonText: "取消",

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 394 - 399
src/views/storeDecoration/businessHours/index.vue


Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov