zhuli 3 недель назад
Родитель
Сommit
f8c37d9f24

+ 19 - 7
src/api/modules/scheduledService.ts

@@ -101,13 +101,15 @@ export const tableDel = (params: { id: number | string }) => {
 // ==================== 预约信息 ====================
 const BASE_RESERVATION = "/alienStore/store/reservation";
 
-/** 预约列表(分页,支持姓名、状态、预订日期筛选) */
+/** 预约列表(分页,支持预订人姓名、订单状态、预订日期筛选) */
 export const reservationList = (params: {
   storeId?: number | string;
-  name?: string;
-  status?: number | string;
+  reservationUserName?: string;
+  orderStatus?: number | string;
   startDate?: string;
   endDate?: string;
+  dateFrom?: string;
+  dateTo?: string;
   pageNum?: number;
   pageSize?: number;
   [key: string]: any;
@@ -115,8 +117,13 @@ export const reservationList = (params: {
   return httpApi.get(`${BASE_RESERVATION}/list`, params);
 };
 
-/** 加时 */
-export const reservationAddTime = (params: { id: number | string; [key: string]: any }) => {
+/** 加时(与 uni 商家端一致:reservationId、addTimeMinutes、addTimeStart) */
+export const reservationAddTime = (params: {
+  reservationId?: number | string;
+  addTimeMinutes?: number;
+  addTimeStart?: string;
+  [key: string]: any;
+}) => {
   const formData = Object.keys(params)
     .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(String(params[key]))}`)
     .join("&");
@@ -125,8 +132,13 @@ export const reservationAddTime = (params: { id: number | string; [key: string]:
   });
 };
 
-/** 取消预约 */
-export const reservationCancel = (params: { id: number | string; [key: string]: any }) => {
+/** 取消预约(与 uni scheduledInfo:reservationId + cancelReason) */
+export const reservationCancel = (params: {
+  id?: number | string;
+  reservationId?: number | string;
+  cancelReason?: string;
+  [key: string]: any;
+}) => {
   const formData = Object.keys(params)
     .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(String(params[key]))}`)
     .join("&");

+ 14 - 0
src/assets/json/authMenuList.json

@@ -1346,6 +1346,20 @@
           }
         }
       ]
+    },
+     {
+      "path": "/appoinmentManagement/appoinmentInfo",
+      "name": "appoinmentInfo",
+      "component": "/appoinmentManagement/appoinmentInfo",
+      "meta":{
+        "icon": "ChatDotSquare",
+        "title": "预定信息",
+        "isLink": "",
+        "isHide": false,
+        "isFull": false,
+        "isAffix": false,
+        "isKeepAlive": false
+      }
     }
   ],
   "msg": "成功"

+ 340 - 97
src/views/appoinmentManagement/appoinmentInfo.vue

@@ -1,18 +1,31 @@
 <template>
   <div class="table-box appointment-info">
-    <!-- 筛选:点击搜索后生效(与翻页联动) -->
-    <div class="filter-bar" style="width: 800px">
+    <!-- 筛选:点击搜索后生效(与翻页联动);列表参数 reservationUserName、orderStatus -->
+    <div class="filter-bar">
       <div class="filter-row">
-        <span class="filter-label">预订日期</span>
-        <el-date-picker
-          v-model="searchForm.dateRange"
-          type="daterange"
-          range-separator="至"
-          start-placeholder="请选择"
-          end-placeholder="请选择"
-          value-format="YYYY-MM-DD"
-          style="width: 60px"
-        />
+        <span class="filter-label">姓名</span>
+        <el-input v-model="searchForm.reservationUserName" placeholder="请输入姓名" clearable style="width: 160px" />
+        <span class="filter-label">状态</span>
+        <el-select v-model="searchForm.orderStatus" placeholder="请选择" clearable style="width: 180px">
+          <el-option
+            v-for="opt in statusOptions"
+            :key="`${opt.label}-${String(opt.value)}`"
+            :label="opt.label"
+            :value="opt.value"
+          />
+        </el-select>
+        <div style="width: 600px">
+          <span class="filter-label">预订日期</span>
+          <el-date-picker
+            v-model="searchForm.dateRange"
+            type="daterange"
+            range-separator="至"
+            start-placeholder="请选择"
+            end-placeholder="请选择"
+            value-format="YYYY-MM-DD"
+            style="width: 80%; margin-left: 10px"
+          />
+        </div>
         <el-button type="primary" @click="handleSearch"> 搜索 </el-button>
         <el-button @click="handleReset"> 重置 </el-button>
       </div>
@@ -21,7 +34,7 @@
     <ProTable ref="proTable" :columns="columns" :request-api="getTableList" :init-param="initParam" :pagination="false">
       <template #tableHeader>
         <div class="table-header">
-          <el-button type="primary" @click="handleNew"> 新建 </el-button>
+          <!-- <el-button type="primary" @click="handleNew"> 新建 </el-button> -->
         </div>
       </template>
       <template #statusText="{ row }">
@@ -35,46 +48,108 @@
         </el-tooltip>
         <span v-else>—</span>
       </template>
-      <!-- 操作按钮与商家端 1.vue 一致:orderStatus 0待支付 1待使用 2已完成 3已过期 4已取消 5已关闭 6退款中 7已退款 8商家预订 -->
+      <!-- 操作按钮与 group_merchant scheduledInfo.vue 一致(orderStatus 判断顺序一致) -->
       <template #operation="scope">
-        <template v-if="isOrderStatus(scope.row, 1)">
-          <el-button link type="primary" @click="handleCancel(scope.row)"> 取消 </el-button>
-        </template>
-        <template v-else-if="isOrderStatus(scope.row, 7)">
-          <el-button link type="primary" @click="handleDelete(scope.row)"> 删除 </el-button>
-        </template>
-        <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="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="isOrderStatus(scope.row, 4) && !orderCostTypeIsPaid(scope.row)">
-          <el-button link type="primary" @click="handleDelete(scope.row)"> 删除 </el-button>
-        </template>
-        <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-if="isOrderStatus(scope.row, 2) && !isMoreThanThreeHoursAfterEnd(scope.row)">
-          <el-button link type="primary" @click="handleAddTime(scope.row)"> 加时 </el-button>
-        </template>
+        <div class="op-actions">
+          <el-button v-if="hasReasonText(scope.row)" link type="primary" @click="openReasonDialog(scope.row)">
+            查看原因
+          </el-button>
+          <template v-if="isOrderStatus(scope.row, 1)">
+            <el-button link type="primary" @click="handleCancel(scope.row)"> 取消 </el-button>
+          </template>
+          <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="isOrderStatus(scope.row, 7) && !isMoreThanThreeHoursAfterEnd(scope.row)">
+            <el-button link type="primary" @click="handleDelete(scope.row)"> 删除 </el-button>
+          </template>
+          <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="isOrderStatus(scope.row, 4) && !orderCostTypeIsPaid(scope.row)">
+            <el-button link type="primary" @click="handleDelete(scope.row)"> 删除 </el-button>
+          </template>
+          <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-if="isOrderStatus(scope.row, 2) && !isMoreThanThreeHoursAfterEnd(scope.row)">
+            <el-button link type="primary" @click="handleAddTime(scope.row)"> 加时 </el-button>
+          </template>
+        </div>
       </template>
     </ProTable>
 
-    <!-- 加时弹窗 -->
-    <el-dialog v-model="addTimeVisible" title="加时" width="400px" append-to-body>
-      <el-form :model="addTimeForm" label-width="80px">
-        <el-form-item label="加时时长">
-          <el-input-number v-model="addTimeForm.minutes" :min="1" :max="480" placeholder="分钟" style="width: 100%" />
+    <!-- 查看原因 -->
+    <el-dialog v-model="reasonDialogVisible" title="原因" width="420px" append-to-body destroy-on-close>
+      <div class="reason-dialog-body">取消原因:</div>
+      <div class="reason-dialog-body">
+        {{ reasonDialogText || "—" }}
+      </div>
+      <template #footer>
+        <el-button type="primary" @click="reasonDialogVisible = false"> 知道了 </el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 取消原因(与 group_merchant scheduledInfo:reservationId + cancelReason) -->
+    <el-dialog
+      v-model="cancelReasonVisible"
+      title="取消原因"
+      width="480px"
+      append-to-body
+      destroy-on-close
+      class="cancel-reason-dialog"
+      @closed="resetCancelReasonDialog"
+    >
+      <el-form label-position="top" class="cancel-reason-form">
+        <el-form-item label="取消原因" required>
+          <el-input
+            v-model="cancelReasonText"
+            type="textarea"
+            :rows="4"
+            maxlength="500"
+            show-word-limit
+            placeholder="请输入取消原因"
+          />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="cancelReasonVisible = false"> 取消 </el-button>
+        <el-button type="primary" :loading="cancelReasonLoading" @click="confirmCancelReason"> 确定 </el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 加时弹窗(与 group_merchant scheduledInfo:1-99 分钟、reservationId + addTimeMinutes + addTimeStart) -->
+    <el-dialog
+      v-model="addTimeVisible"
+      title="加时"
+      width="480px"
+      append-to-body
+      destroy-on-close
+      class="add-time-dialog"
+      @closed="resetAddTimeDialog"
+    >
+      <el-form :model="addTimeForm" label-position="top" require-asterisk-position="right" class="add-time-form">
+        <el-form-item label="加时时长" required>
+          <el-input
+            :model-value="addTimeForm.minutesInput"
+            placeholder="请输入"
+            clearable
+            maxlength="2"
+            @update:model-value="onAddTimeMinutesInput"
+          >
+            <template #append>
+              <span class="add-time-append-unit">分钟</span>
+            </template>
+          </el-input>
         </el-form-item>
       </el-form>
       <template #footer>
@@ -101,6 +176,8 @@ import { localGet } from "@/utils";
 
 export interface ReservationRow {
   id: number | string;
+  /** 与商家端加时接口一致,缺省时用 id */
+  reservationId?: number | string;
   _seq?: number;
   date: string;
   weekDay: string;
@@ -139,20 +216,28 @@ const columns: ColumnProps<ReservationRow>[] = [
   { prop: "peopleCount", label: "人数", width: 70, align: "center" },
   { prop: "tableNo", label: "桌号", minWidth: 90, showOverflowTooltip: true },
   { prop: "timeRange", label: "时间", width: 120, align: "center" },
-  { prop: "amount", label: "预订金额(元)", width: 110, align: "center" },
   { prop: "customerName", label: "姓名", width: 100, showOverflowTooltip: true },
   { 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: 200, align: "center" }
+  { prop: "operation", label: "操作", fixed: "right", width: 240, align: "center" }
 ];
 
-/** 与商家端 1.vue 筛选、列表 status 一致(orderStatus) */
-const statusOptions = [
+/**
+ * 筛选状态:商家取消 / 用户取消 均为 orderStatus=4,列表展示区分见 mapReservationRow(reason 空=用户取消)
+ * 接口仅支持 orderStatus 时,请求 4 后在本地按 reason 再筛一层
+ */
+const STATUS_FILTER_MERCHANT_CANCEL = "merchant_cancel";
+const STATUS_FILTER_USER_CANCEL = "user_cancel";
+
+const statusOptions: { label: string; value: number | string }[] = [
   { label: "全部", value: "" },
   { label: "待使用", value: 1 },
-  { label: "已完成", value: 2 },
-  { label: "退款", value: 6 } // 退款 tab:6退款中(接口若合并 7 需后端支持)
+  { label: "退款成功", value: 7 },
+  { label: "商家取消", value: STATUS_FILTER_MERCHANT_CANCEL },
+  { label: "用户取消", value: STATUS_FILTER_USER_CANCEL },
+  { label: "已过期", value: 3 },
+  { label: "已完成", value: 2 }
 ];
 
 /** orderStatus 文案,与 1.vue ORDER_STATUS_TEXT 一致 */
@@ -185,36 +270,42 @@ const initParam = reactive({
   storeId: localGet("geeker-user")?.userInfo?.storeId ?? localGet("createdId") ?? ""
 });
 
-/** 已生效的查询条件(点击搜索后写入) */
+/** 已生效的查询条件(点击搜索后写入;接口字段 reservationUserName、orderStatus) */
 const listFilter = reactive({
-  name: "",
-  status: "" as number | string,
+  reservationUserName: "",
+  orderStatus: "" as number | string,
   startDate: "",
   endDate: ""
 });
 
 const searchForm = reactive({
-  name: "",
-  status: "" as number | string,
+  reservationUserName: "",
+  orderStatus: "" as number | string,
   dateRange: [] as string[]
 });
 
 const addTimeVisible = ref(false);
 const addTimeLoading = ref(false);
-const addTimeForm = reactive({ minutes: 30 });
+const addTimeForm = reactive({ minutesInput: "" });
 const currentAddTimeRow = ref<ReservationRow | null>(null);
 
+/** 取消预约:原因弹窗(对齐 scheduledInfo) */
+const cancelReasonVisible = ref(false);
+const cancelReasonLoading = ref(false);
+const cancelReasonText = ref("");
+const cancelCurrentRow = ref<ReservationRow | null>(null);
+
 function getStoreId(): number | string | null {
   return localGet("geeker-user")?.userInfo?.storeId ?? localGet("createdId") ?? null;
 }
 
+/** 与 scheduledInfo normalizeReservationItem:pending(0,1,8) | done(2) | refund(6,7) | closed(3,4,5) */
 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 === 6 || s === 7) return "warning";
   if (s === 3 || s === 4 || s === 5) return "info";
+  if (s === 0 || s === 1 || s === 8) return "primary";
   return "info";
 }
 
@@ -231,6 +322,18 @@ function cancelReasonIsEmpty(row: ReservationRow) {
   return String(row.reason ?? "").trim() === "";
 }
 
+function hasReasonText(row: ReservationRow) {
+  return String(row.reason ?? "").trim() !== "";
+}
+
+const reasonDialogVisible = ref(false);
+const reasonDialogText = ref("");
+
+function openReasonDialog(row: ReservationRow) {
+  reasonDialogText.value = String(row.reason ?? "").trim() || "—";
+  reasonDialogVisible.value = true;
+}
+
 function parseRowEndDateTime(row: ReservationRow): Date | null {
   const et = row.endTimeRaw;
   if (et != null && et !== "") {
@@ -311,6 +414,7 @@ function mapReservationRow(item: any): ReservationRow {
 
   return {
     id: item.id,
+    reservationId: item.reservationId ?? item.id,
     date: formatDateCol(date),
     weekDay: item.weekDay ?? getWeekDay(date),
     location: item.locationName ?? item.categoryName ?? item.location ?? "—",
@@ -332,8 +436,16 @@ function mapReservationRow(item: any): ReservationRow {
     reservationDateRaw: String(date).trim(),
     timeSlotRaw,
     endTimeRaw: item.endTime ?? item.bookingEndTime ?? item.reservationEndTime,
+    /** scheduledInfo:orderStatus==4 时 reason 空=用户取消,非空=商家取消 */
     statusText:
-      (item.orderStatusText && String(item.orderStatusText).trim()) || ORDER_STATUS_TEXT[orderStatus] || item.statusText || "—",
+      orderStatus === 4
+        ? reason === ""
+          ? "用户取消"
+          : "商家取消"
+        : (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,
@@ -354,9 +466,16 @@ async function getTableList(params: any) {
     pageNum,
     pageSize
   };
-  if (listFilter.name?.trim()) req.name = listFilter.name.trim();
-  if (listFilter.status !== undefined && listFilter.status !== "" && listFilter.status !== null) {
-    req.status = listFilter.status;
+  if (listFilter.reservationUserName?.trim()) {
+    req.reservationUserName = listFilter.reservationUserName.trim();
+  }
+  const sf = listFilter.orderStatus;
+  if (sf !== undefined && sf !== "" && sf !== null) {
+    if (sf === STATUS_FILTER_USER_CANCEL || sf === STATUS_FILTER_MERCHANT_CANCEL) {
+      req.orderStatus = 4;
+    } else {
+      req.orderStatus = sf;
+    }
   }
   if (listFilter.startDate && listFilter.endDate) {
     req.dateFrom = listFilter.startDate;
@@ -368,12 +487,19 @@ async function getTableList(params: any) {
     const inner = body?.data ?? body;
     const list = inner?.list ?? inner?.records ?? (Array.isArray(inner) ? inner : []);
     const arr = Array.isArray(list) ? list : [];
-    const total = inner?.total ?? body?.total ?? res?.total ?? 0;
-    const rows = arr.map((item: any, i: number) => ({
-      ...mapReservationRow(item),
+    const apiTotal = Number(inner?.total ?? body?.total ?? res?.total ?? 0) || 0;
+    let rows = arr.map((item: any) => mapReservationRow(item));
+    if (sf === STATUS_FILTER_USER_CANCEL) {
+      rows = rows.filter(r => r.orderStatus === 4 && cancelReasonIsEmpty(r));
+    } else if (sf === STATUS_FILTER_MERCHANT_CANCEL) {
+      rows = rows.filter(r => r.orderStatus === 4 && !cancelReasonIsEmpty(r));
+    }
+    const finalRows = rows.map((r, i) => ({
+      ...r,
       _seq: (pageNum - 1) * pageSize + i + 1
     }));
-    return { data: { list: rows, total: Number(total) || 0 } };
+    const total = sf === STATUS_FILTER_USER_CANCEL || sf === STATUS_FILTER_MERCHANT_CANCEL ? finalRows.length : apiTotal;
+    return { data: { list: finalRows, total } };
   } catch (e: any) {
     ElMessage.error(e?.message || "加载失败");
     return { data: { list: [] as ReservationRow[], total: 0 } };
@@ -381,8 +507,8 @@ async function getTableList(params: any) {
 }
 
 function handleSearch() {
-  listFilter.name = searchForm.name;
-  listFilter.status = searchForm.status;
+  listFilter.reservationUserName = searchForm.reservationUserName;
+  listFilter.orderStatus = searchForm.orderStatus;
   if (searchForm.dateRange?.length === 2) {
     listFilter.startDate = searchForm.dateRange[0];
     listFilter.endDate = searchForm.dateRange[1];
@@ -394,11 +520,11 @@ function handleSearch() {
 }
 
 function handleReset() {
-  searchForm.name = "";
-  searchForm.status = "";
+  searchForm.reservationUserName = "";
+  searchForm.orderStatus = "";
   searchForm.dateRange = [];
-  listFilter.name = "";
-  listFilter.status = "";
+  listFilter.reservationUserName = "";
+  listFilter.orderStatus = "";
   listFilter.startDate = "";
   listFilter.endDate = "";
   proTable.value?.getTableList();
@@ -409,40 +535,104 @@ function handleNew() {
 }
 
 function handleCancel(row: ReservationRow) {
-  ElMessageBox.confirm("确认取消该预约?", "提示", {
-    confirmButtonText: "确定",
-    cancelButtonText: "取消",
-    type: "warning"
-  })
-    .then(async () => {
-      try {
-        await reservationCancel({ id: row.id });
-        ElMessage.success("已取消");
-        proTable.value?.getTableList();
-      } catch (e: any) {
-        ElMessage.error(e?.message || "取消失败");
-      }
-    })
-    .catch(() => {});
+  cancelCurrentRow.value = row;
+  cancelReasonText.value = "";
+  cancelReasonVisible.value = true;
+}
+
+function resetCancelReasonDialog() {
+  cancelCurrentRow.value = null;
+  cancelReasonText.value = "";
+}
+
+async function confirmCancelReason() {
+  const reason = (cancelReasonText.value || "").trim();
+  if (!reason) {
+    ElMessage.warning("请输入取消原因");
+    return;
+  }
+  const row = cancelCurrentRow.value;
+  const reservationId = row?.reservationId ?? row?.id;
+  if (reservationId == null || reservationId === "") {
+    ElMessage.warning("缺少预订ID");
+    return;
+  }
+  cancelReasonLoading.value = true;
+  try {
+    const apiRes: any = await reservationCancel({ reservationId, cancelReason: reason });
+    ElMessage.success(apiRes?.msg || "已取消");
+    cancelReasonVisible.value = false;
+    resetCancelReasonDialog();
+    proTable.value?.getTableList();
+  } catch (e: any) {
+    if (typeof e?.code !== "number") {
+      ElMessage.error(e?.msg || e?.data?.msg || e?.message || "取消失败");
+    }
+  } finally {
+    cancelReasonLoading.value = false;
+  }
+}
+
+/** 与 scheduledInfo confirmAddTime:当前时间 yyyy-MM-dd HH:mm */
+function formatAddTimeStartFromNow(): string {
+  const now = new Date();
+  return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")} ${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
+}
+
+/** 与 scheduledInfo onAddTimeMinutesInput:最多 2 位数字 1-99 */
+function onAddTimeMinutesInput(val: string | number) {
+  const v = String(val ?? "")
+    .replace(/\D/g, "")
+    .slice(0, 2);
+  addTimeForm.minutesInput = v;
+}
+
+function resetAddTimeDialog() {
+  currentAddTimeRow.value = null;
+  addTimeForm.minutesInput = "";
 }
 
 function handleAddTime(row: ReservationRow) {
   currentAddTimeRow.value = row;
-  addTimeForm.minutes = 30;
+  addTimeForm.minutesInput = "";
   addTimeVisible.value = true;
 }
 
 async function confirmAddTime() {
   const row = currentAddTimeRow.value;
   if (!row) return;
+  const raw = String(addTimeForm.minutesInput ?? "").trim();
+  if (!raw) {
+    ElMessage.warning("请输入加时时长");
+    return;
+  }
+  const addTimeMinutesNum = parseInt(raw, 10);
+  if (isNaN(addTimeMinutesNum) || addTimeMinutesNum < 1 || addTimeMinutesNum > 99) {
+    ElMessage.warning("加时时长为1-99的整数");
+    return;
+  }
+  const reservationId = row.reservationId ?? row.id;
+  if (reservationId == null || reservationId === "") {
+    ElMessage.warning("缺少预订ID");
+    return;
+  }
+  const addTimeStart = formatAddTimeStartFromNow();
   addTimeLoading.value = true;
   try {
-    await reservationAddTime({ id: row.id, addMinutes: addTimeForm.minutes });
-    ElMessage.success("加时成功");
+    const apiRes: any = await reservationAddTime({
+      reservationId,
+      addTimeMinutes: addTimeMinutesNum,
+      addTimeStart
+    });
+    ElMessage.success(apiRes?.msg || "加时成功");
     addTimeVisible.value = false;
+    resetAddTimeDialog();
     proTable.value?.getTableList();
   } catch (e: any) {
-    ElMessage.error(e?.message || "加时失败");
+    // 业务 code≠200 时 indexApi 响应拦截器已提示 msg,避免重复 Toast
+    if (typeof e?.code !== "number") {
+      ElMessage.error(e?.msg || e?.data?.msg || e?.message || "加时失败");
+    }
   } finally {
     addTimeLoading.value = false;
   }
@@ -523,4 +713,57 @@ function handleRefund(row: ReservationRow) {
   white-space: nowrap;
   cursor: default;
 }
+.op-actions {
+  display: inline-flex;
+  flex-wrap: wrap;
+  gap: 4px 8px;
+  align-items: center;
+  justify-content: center;
+}
+.reason-dialog-body {
+  padding: 8px 4px;
+  font-size: 14px;
+  line-height: 1.6;
+  color: #303133;
+  word-break: break-word;
+  white-space: pre-wrap;
+}
+</style>
+
+<!-- append-to-body 时弹窗挂到 body,需非 scoped 命中 -->
+<style lang="scss">
+.add-time-dialog.el-dialog .el-dialog__body {
+  padding-top: 8px;
+}
+.add-time-form {
+  width: 100%;
+}
+.add-time-form .el-form-item {
+  margin-bottom: 8px;
+}
+.add-time-form .el-form-item__label {
+  font-weight: 500;
+  color: #303133;
+}
+.add-time-form .el-input-group {
+  width: 100%;
+}
+.add-time-form .el-input__wrapper {
+  flex: 1;
+  min-width: 0;
+}
+.add-time-append-unit {
+  padding: 0 4px;
+  font-size: 14px;
+  color: #606266;
+}
+.cancel-reason-dialog.el-dialog .el-dialog__body {
+  padding-top: 8px;
+}
+.cancel-reason-form {
+  width: 100%;
+}
+.cancel-reason-form .el-form-item {
+  margin-bottom: 0;
+}
 </style>

+ 9 - 1
src/views/appoinmentManagement/classifyManagement.vue

@@ -37,7 +37,14 @@
       destroy-on-close
       @close="onDialogClose"
     >
-      <el-form ref="formRef" :model="form" :rules="rules" label-width="160px" v-loading="detailLoading">
+      <el-form
+        ref="formRef"
+        :model="form"
+        :rules="rules"
+        label-width="160px"
+        require-asterisk-position="right"
+        v-loading="detailLoading"
+      >
         <el-form-item label="分类名称" prop="name" required>
           <el-input v-model="form.name" placeholder="请输入" clearable maxlength="20" show-word-limit />
         </el-form-item>
@@ -435,6 +442,7 @@ async function submitForm() {
 <style scoped lang="scss">
 :deep(.el-dialog__body) {
   height: 400px;
+  border: 1px solid red;
 }
 .classify-management {
   padding: 0;

+ 72 - 29
src/views/appoinmentManagement/infoManagement.vue

@@ -421,19 +421,57 @@ function minutesToTimeStr(total: number): string {
   return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
 }
 
-/** 与 merchant:填写了「结束前不可预订」则结束时间可选上限 = 营业 end − 该分钟数 */
+/**
+ * 与 group_merchant infoManagement:只要填写了「结束前不可预订」分钟数,
+ * 正常/特殊营业的结束时间可选上限 = 营业 endTime − 该分钟数(picker 用)
+ */
 function getNoBookMinutesForPicker(): number {
   const v = form.booking.noBookMinutesBeforeClose;
   if (v == null) return 0;
-  const n = Number(String(v).trim());
+  const n = Number(v);
   return n > 0 && !isNaN(n) ? n : 0;
 }
 
+/**
+ * 门店营业结束时刻 → 回显「可预约结束时间」:营业 endTime − 「结束前不可预订」分钟(与 computePickerEndMinutes 一致)。
+ * 仅用于从 getStoreInfoBusinessHours 回填;预约接口已存的 endTime 勿再调用,避免重复相减。
+ */
+function echoBookableEndFromStoreEnd(storeEndTime: string | undefined | null): string {
+  const raw = String(storeEndTime ?? "23:59").trim() || "23:59";
+  const noBook = getNoBookMinutesForPicker();
+  if (!noBook) return raw;
+  return subtractMinutesFromTime(raw, noBook);
+}
+
+/** 正常营业「全天/非全天」仅按门店 getStoreInfoBusinessHours 中 businessType=1 或 0 的 startTime、endTime */
+function applyNormalTimeTypeFromStore() {
+  const storeInfoList = Array.isArray(listFromStoreInfo.value) ? listFromStoreInfo.value : [];
+  const normal = storeInfoList.find((item: any) => item.businessType === 1 || item.businessType === 0);
+  if (!normal) return;
+  const st = String(normal.startTime ?? "").trim();
+  const et = String(normal.endTime ?? "").trim();
+  form.normalBook.timeType = st === "00:00" && et === "00:00" ? "allDay" : "notAllDay";
+}
+
+/** 无预约数据时,修改「结束前不可预订」需同步更新「可预约结束时间」回显(与 echoBookableEndFromStoreEnd 一致) */
+watch(
+  () => form.booking.noBookMinutesBeforeClose,
+  () => {
+    const list = listBooking.value;
+    if (Array.isArray(list) && list.length) return;
+    const storeInfoList = listFromStoreInfo.value;
+    const storeNormal = storeInfoList.find((item: any) => item.businessType === 1 || item.businessType === 0);
+    if (!storeNormal) return;
+    form.normalBook.endTime = echoBookableEndFromStoreEnd(storeNormal.endTime);
+  }
+);
+
 function computePickerEndMinutes(endTimeRaw: string): number {
   const noBook = getNoBookMinutesForPicker();
   return timeStrToMinutes(subtractMinutesFromTime(endTimeRaw || "23:59", noBook));
 }
 
+/** 与商家端:正常营业时间选择器仅以 getStoreInfoBusinessHours(businessType=1)的 startTime、endTime 为可选范围 */
 function getNormalWindowForPicker() {
   const store = (Array.isArray(listFromStoreInfo.value) ? listFromStoreInfo.value : []).find((x: any) => x.businessType === 1);
   let startTime = store?.startTime != null && String(store.startTime).trim() !== "" ? String(store.startTime).trim() : "00:00";
@@ -643,58 +681,64 @@ async function fetchStoreInfoBusinessHours() {
     const res: any = await getStoreInfoBusinessHours({ id: storeId });
     const list = Array.isArray(res?.data) ? res.data : [];
     listFromStoreInfo.value = list;
-    const normal = list.find((item: any) => item.businessType === 1);
-    /** 与商家端:特殊营业由 getBookingBusinessHours + applyListBookingToForm 回填,此处不占位 */
-    form.specialList = [];
-    if (normal && !form.normalBook.startTime && !form.normalBook.endTime) {
-      form.normalBook.startTime = normal.startTime || "";
-      form.normalBook.endTime = normal.endTime || "23:59";
-      const isAllDay = form.normalBook.startTime === "00:00" && form.normalBook.endTime === "00:00";
-      form.normalBook.timeType = isAllDay ? "allDay" : "notAllDay";
+    const normalStore = list.find((item: any) => item.businessType === 1 || item.businessType === 0);
+    if (normalStore) {
+      form.normalBook.normalId = Number(normalStore.id) || 0;
     }
+    /** 与商家端:正常/特殊营业时间由 getBookingBusinessHours + applyListBookingToForm 统一回显 */
+    form.specialList = [];
   } catch {
     listFromStoreInfo.value = [];
   }
 }
 
+/** 与 group_merchant scheduledService/infoManagement:listBooking + listFromStoreInfo 回显正常/特殊营业 */
 function applyListBookingToForm() {
   const list = Array.isArray(listBooking.value) ? listBooking.value : [];
-  const noBookMin = Number(form.booking.noBookMinutesBeforeClose) || 0;
-
-  if (list.length > 0) {
-    const normal = list.find((item: any) => item.businessType === 1 || item.businessType === 0);
-    if (normal) {
-      const st = normal.startTime || "";
-      const et = normal.endTime || "23:59";
-      form.normalBook.timeType = st === "00:00" && et === "00:00" ? "allDay" : "notAllDay";
-      form.normalBook.startTime = st;
-      form.normalBook.endTime = subtractMinutesFromTime(et, noBookMin);
-      form.normalBook.normalId = Number(normal.id) || 0;
-    }
-  }
-
   const storeInfoList = Array.isArray(listFromStoreInfo.value) ? listFromStoreInfo.value : [];
   const specialFromStore = storeInfoList.filter((item: any) => item.businessType === 2);
   const specialFromBooking = list.filter((item: any) => item.businessType === 2);
   const bookingEmpty = !list.length;
   const useStoreInfoForSpecial = !list.length || !specialFromBooking.length;
 
-  /** 预约接口返回的 holidayType 集合;仅在此集合内的门店节日才回显预订时间(与商家端 applyListBookingToForm) */
   const bookingHolidayTypeSet = new Set(
     specialFromBooking.map((b: any) => (b.holidayType != null ? String(b.holidayType).trim() : "")).filter(Boolean)
   );
 
+  const storeNormal = storeInfoList.find((item: any) => item.businessType === 1 || item.businessType === 0);
+
+  /** 无预约配置:正常营业从门店营业时间回显,结束时间用 echoBookableEndFromStoreEnd(可预约结束) */
+  if (!list.length) {
+    if (storeNormal) {
+      form.normalBook.startTime = String(storeNormal.startTime ?? "").trim();
+      form.normalBook.endTime = echoBookableEndFromStoreEnd(storeNormal.endTime);
+      form.normalBook.normalId = Number(storeNormal.id) || 0;
+    } else {
+      form.normalBook.startTime = "";
+      form.normalBook.endTime = "";
+    }
+  } else {
+    const normal = list.find((item: any) => item.businessType === 1 || item.businessType === 0);
+    if (normal) {
+      const st = normal.startTime || "";
+      const et = normal.endTime || "23:59";
+      form.normalBook.startTime = st;
+      form.normalBook.endTime = et;
+      form.normalBook.normalId = Number(normal.id) || 0;
+    }
+  }
+  applyNormalTimeTypeFromStore();
+
   if (useStoreInfoForSpecial) {
     if (bookingEmpty) {
       form.specialList = [];
       return;
     }
-    /** 有预约数据但未配置特殊营业行:不占位,用户先选节日 */
     form.specialList = [];
     return;
   }
 
-  /** 仅 getBookingBusinessHours 中 holidayType 与门店节日匹配时回显时间;allDay 以门店营业时间为准 */
+  /** 仅 getBookingBusinessHours 返回的 holidayType 对应项回显;特殊营业结束时间按接口原样,不减「结束前不可预订」 */
   form.specialList = specialFromStore.map((s: any, index: number) => {
     const name = (s.holidayInfo && s.holidayInfo.festivalName) || s.businessDate || s.holidayType || `特殊营业${index + 1}`;
     const storeFestivalName = (
@@ -720,12 +764,11 @@ function applyListBookingToForm() {
     }
     const startTime = match.startTime || "";
     const endTimeRaw = match.endTime || "23:59";
-    const endTime = endTimeRaw;
     return {
       name,
       allDay: allDayFromStoreSpecialRow(s),
       startTime,
-      endTime,
+      endTime: endTimeRaw,
       id: s.id,
       essentialId: s.essentialId,
       userPickedSpecial: true

+ 264 - 43
src/views/appoinmentManagement/tableManagement.vue

@@ -27,34 +27,79 @@
     <!-- 新建/编辑弹窗 -->
     <el-dialog
       v-model="dialogVisible"
+      class="table-management-dialog"
       :title="editId == null ? '新建桌位' : '编辑桌位'"
       width="480px"
       append-to-body
       destroy-on-close
       @close="onDialogClose"
     >
-      <el-form ref="formRef" :model="form" :rules="rules" label-width="80px" v-loading="detailLoading">
-        <el-form-item label="桌号" prop="tableNo" required>
-          <el-input v-model="form.tableNo" placeholder="请输入桌号" clearable maxlength="20" />
-        </el-form-item>
-        <el-form-item label="座位数" prop="seatCount" required>
-          <el-input
-            :model-value="String(form.seatCount || '')"
-            placeholder="请输入"
-            style="width: 100%"
-            @update:model-value="
-              (val: string) => {
-                const n = parseInt(val, 10);
-                form.seatCount = isNaN(n) ? 0 : n;
-              }
-            "
-          />
-        </el-form-item>
-        <el-form-item label="位置" prop="categoryId" required>
-          <el-select v-model="form.categoryId" placeholder="请选择位置" clearable style="width: 100%">
+      <el-form
+        ref="formRef"
+        class="table-dialog-form"
+        :model="form"
+        :rules="formRules"
+        label-width="80px"
+        v-loading="detailLoading"
+        require-asterisk-position="right"
+      >
+        <el-form-item label="选择分类" prop="categoryId" required>
+          <el-select v-model="form.categoryId" placeholder="请选择分类" clearable>
             <el-option v-for="item in categoryOptions" :key="item.id" :label="item.name" :value="item.id" />
           </el-select>
         </el-form-item>
+
+        <!-- 新建:可添加多组桌号/座位数(表单项与编辑态一致:作为 el-form 直接子节点,避免包一层 div 导致输入区宽度异常) -->
+        <template v-if="editId == null">
+          <div class="add-table-toolbar">
+            <el-button type="primary" link @click="addTableRow"> 添加 </el-button>
+          </div>
+          <template v-for="(row, index) in form.tableRows" :key="index">
+            <el-form-item label="桌号" :prop="'tableRows.' + index + '.tableNo'" :rules="tableNoFieldRules" required>
+              <el-input
+                :model-value="row.tableNo"
+                placeholder="如 A01"
+                maxlength="3"
+                @update:model-value="(val: string) => onTableNumberInput(row, val)"
+              />
+            </el-form-item>
+            <el-form-item label="座位数" :prop="'tableRows.' + index + '.seatCount'" :rules="seatCountFieldRulesRow" required>
+              <el-input
+                :model-value="row.seatCount"
+                placeholder="请输入"
+                maxlength="2"
+                inputmode="numeric"
+                @update:model-value="(val: string) => onSeatCountInput(row, val)"
+              />
+            </el-form-item>
+            <div v-if="form.tableRows.length > 1" class="table-row-delete">
+              <el-button type="danger" link @click="removeTableRow(index)"> 删除 </el-button>
+            </div>
+            <div v-if="index < form.tableRows.length - 1" class="table-row-sep" aria-hidden="true" />
+          </template>
+        </template>
+
+        <!-- 编辑:单条 -->
+        <template v-else>
+          <el-form-item label="桌号" prop="tableNo" required>
+            <el-input
+              :model-value="form.tableNo"
+              placeholder="如 A01"
+              clearable
+              maxlength="3"
+              @update:model-value="onTableNumberInputEdit"
+            />
+          </el-form-item>
+          <el-form-item label="座位数" prop="seatCount" required>
+            <el-input
+              :model-value="seatCountEditDisplay"
+              placeholder="请输入"
+              maxlength="2"
+              inputmode="numeric"
+              @update:model-value="onSeatCountInputEdit"
+            />
+          </el-form-item>
+        </template>
       </el-form>
       <template #footer>
         <el-button @click="dialogVisible = false"> 取消 </el-button>
@@ -65,9 +110,9 @@
 </template>
 
 <script setup lang="ts">
-import { ref, reactive, onMounted } from "vue";
+import { ref, reactive, computed, onMounted } from "vue";
 import { ElMessage, ElMessageBox } from "element-plus";
-import type { FormInstance, FormRules } from "element-plus";
+import type { FormInstance, FormRules, FormItemRule } from "element-plus";
 import ProTable from "@/components/ProTable/index.vue";
 import type { ProTableInstance, ColumnProps } from "@/components/ProTable/interface";
 import {
@@ -85,7 +130,8 @@ export interface TableRow {
   id: number | string;
   _seq?: number;
   tableNo: string;
-  seatCount: number;
+  /** 列表接口返回的座位数字段 */
+  seatingCapacity: number;
   categoryId?: number | string;
   locationName?: string;
 }
@@ -95,7 +141,7 @@ const proTable = ref<ProTableInstance>();
 const columns: ColumnProps<TableRow>[] = [
   { prop: "_seq", label: "序号", width: 60, align: "center" },
   { prop: "tableNo", label: "桌号" },
-  { prop: "seatCount", label: "座位数", align: "center" },
+  { prop: "seatingCapacity", label: "座位数", align: "center" },
   { prop: "locationName", label: "位置" },
   { prop: "operation", label: "操作", fixed: "right", width: 140, align: "center" }
 ];
@@ -149,10 +195,11 @@ async function loadCategories() {
 }
 
 function mapTableRow(item: any, categoryMap: Record<string, string>): TableRow {
+  const cap = item.seatingCapacity ?? item.seatCount ?? item.seats;
   return {
     id: item.id,
     tableNo: item.tableNo ?? item.tableNumber ?? item.deskNo ?? "",
-    seatCount: item.seatCount ?? item.seats ?? 0,
+    seatingCapacity: cap != null && cap !== "" ? Number(cap) || 0 : 0,
     categoryId: item.categoryId ?? item.category_id,
     locationName: item.locationName ?? item.categoryName ?? categoryMap[String(item.categoryId ?? item.category_id)] ?? ""
   };
@@ -222,33 +269,152 @@ const form = reactive<{
   tableNo: string;
   seatCount: number;
   categoryId: number | string | undefined;
+  /** 与 App addTableNumber 一致:座位为字符串便于输入过滤 */
+  tableRows: { tableNo: string; seatCount: string }[];
 }>({
   tableNo: "",
   seatCount: 0,
-  categoryId: undefined
+  categoryId: undefined,
+  tableRows: [{ tableNo: "", seatCount: "" }]
 });
 
-const rules: FormRules = {
-  tableNo: [
-    { required: true, message: "请输入桌号", trigger: "blur" },
-    {
-      pattern: /^[A-Z][0-9]{2}$/,
-      message: "桌号格式为:大写字母+2位数字,如 A01",
-      trigger: "blur"
-    }
-  ],
-  seatCount: [
-    { required: true, message: "请输入座位数", trigger: "blur" },
-    { type: "number", min: 1, max: 99, message: "请输入1-99的整数", trigger: "blur" }
-  ],
-  categoryId: [{ required: true, message: "请选择位置", trigger: "change" }]
-};
+/** 与 group_merchant/pages/scheduledService/addTableNumber.vue 一致 */
+const TABLE_NUMBER_REG = /^[A-Z][0-9]{2}$/;
+
+const tableNoFieldRules: FormItemRule[] = [
+  {
+    validator: (_rule, value, callback) => {
+      const num = String(value ?? "").trim();
+      if (!num) {
+        callback(new Error("请输入桌号"));
+        return;
+      }
+      if (!TABLE_NUMBER_REG.test(num)) {
+        callback(new Error("桌号格式为:大写字母+2位数字,如 A01"));
+        return;
+      }
+      callback();
+    },
+    trigger: "blur"
+  }
+];
+
+const seatCountFieldRulesRow: FormItemRule[] = [
+  {
+    validator: (_rule, value, callback) => {
+      const seat = String(value ?? "").trim();
+      if (!seat) {
+        callback(new Error("请输入座位数"));
+        return;
+      }
+      const n = parseInt(seat, 10);
+      if (isNaN(n) || n < 1 || n > 99) {
+        callback(new Error("请输入1-99的整数"));
+        return;
+      }
+      callback();
+    },
+    trigger: "blur"
+  }
+];
+
+const seatCountFieldRulesEdit: FormItemRule[] = [
+  {
+    validator: (_rule, value, callback) => {
+      const n = Number(value);
+      if (value === undefined || value === null || value === "" || Number.isNaN(n) || n === 0) {
+        callback(new Error("请输入座位数"));
+        return;
+      }
+      if (n < 1 || n > 99) {
+        callback(new Error("请输入1-99的整数"));
+        return;
+      }
+      callback();
+    },
+    trigger: "blur"
+  }
+];
+
+/** 编辑座位展示:0 视为空(与 App 输入行为一致) */
+const seatCountEditDisplay = computed(() => {
+  const s = form.seatCount;
+  if (s === 0 || s === undefined || s === null || Number.isNaN(Number(s))) return "";
+  return String(s);
+});
+
+function onTableNumberInput(row: { tableNo: string }, val: string) {
+  let v = String(val ?? "");
+  v = v.toUpperCase().replace(/[^A-Z0-9]/g, "");
+  const letter = /[A-Z]/.test(v[0]) ? v[0] : "";
+  const digits = v
+    .slice(letter ? 1 : 0)
+    .replace(/[^0-9]/g, "")
+    .slice(0, 2);
+  row.tableNo = letter + digits;
+}
+
+function onSeatCountInput(row: { seatCount: string }, val: string) {
+  let v = String(val ?? "")
+    .replace(/\D/g, "")
+    .slice(0, 2);
+  const num = parseInt(v, 10);
+  if (v !== "" && !isNaN(num) && num > 99) v = "99";
+  row.seatCount = v;
+}
+
+function onTableNumberInputEdit(val: string) {
+  let v = String(val ?? "");
+  v = v.toUpperCase().replace(/[^A-Z0-9]/g, "");
+  const letter = /[A-Z]/.test(v[0]) ? v[0] : "";
+  const digits = v
+    .slice(letter ? 1 : 0)
+    .replace(/[^0-9]/g, "")
+    .slice(0, 2);
+  form.tableNo = letter + digits;
+}
+
+function onSeatCountInputEdit(val: string) {
+  let v = String(val ?? "")
+    .replace(/\D/g, "")
+    .slice(0, 2);
+  const num = parseInt(v, 10);
+  if (v !== "" && !isNaN(num) && num > 99) v = "99";
+  if (v === "") {
+    form.seatCount = 0;
+  } else {
+    const n = parseInt(v, 10);
+    form.seatCount = isNaN(n) ? 0 : n;
+  }
+}
+
+/** 新建只校验分类 + 各行(行规则在 el-form-item 上);编辑校验分类 + 单条桌号/座位数 */
+const formRules = computed<FormRules>(() => {
+  const base: FormRules = {
+    categoryId: [{ required: true, message: "请选择位置", trigger: "change" }]
+  };
+  if (editId.value != null) {
+    base.tableNo = tableNoFieldRules;
+    base.seatCount = seatCountFieldRulesEdit;
+  }
+  return base;
+});
+
+function addTableRow() {
+  form.tableRows.push({ tableNo: "", seatCount: "" });
+}
+
+function removeTableRow(index: number) {
+  if (form.tableRows.length <= 1) return;
+  form.tableRows.splice(index, 1);
+}
 
 function handleNew() {
   editId.value = null;
   form.tableNo = "";
   form.seatCount = 0;
   form.categoryId = undefined;
+  form.tableRows = [{ tableNo: "", seatCount: "" }];
   dialogVisible.value = true;
 }
 
@@ -264,6 +430,7 @@ async function handleEdit(row: TableRow) {
     return;
   }
   editId.value = row.id;
+  form.tableRows = [{ tableNo: "", seatCount: "" }];
   dialogVisible.value = true;
   detailLoading.value = true;
   try {
@@ -272,7 +439,7 @@ async function handleEdit(row: TableRow) {
     const categoryId = d.categoryId ?? d.classifyId ?? undefined;
     form.categoryId = categoryId != null ? categoryId : (row.categoryId ?? undefined);
     form.tableNo = d.tableNumber ?? d.tableNo ?? row.tableNo ?? "";
-    form.seatCount = d.seatingCapacity ?? d.seatCount ?? row.seatCount ?? 0;
+    form.seatCount = d.seatingCapacity ?? d.seatCount ?? row.seatingCapacity ?? 0;
     if (typeof form.seatCount === "string") form.seatCount = parseInt(form.seatCount, 10) || 0;
     if (
       categoryId != null &&
@@ -284,7 +451,7 @@ async function handleEdit(row: TableRow) {
   } catch (e: any) {
     ElMessage.error(e?.message || "获取详情失败");
     form.tableNo = row.tableNo;
-    form.seatCount = row.seatCount;
+    form.seatCount = row.seatingCapacity ?? 0;
     form.categoryId = row.categoryId ?? undefined;
   } finally {
     detailLoading.value = false;
@@ -324,6 +491,7 @@ function onDialogClose() {
   form.tableNo = "";
   form.seatCount = 0;
   form.categoryId = undefined;
+  form.tableRows = [{ tableNo: "", seatCount: "" }];
 }
 
 async function submitForm() {
@@ -351,10 +519,14 @@ async function submitForm() {
         seatingCapacity
       });
     } else {
+      const tables = form.tableRows.map(r => ({
+        tableNumber: r.tableNo.trim(),
+        seatingCapacity: parseInt(String(r.seatCount).trim(), 10) || 0
+      }));
       await tableAdd({
         storeId,
         categoryId: form.categoryId,
-        tables: [{ tableNumber, seatingCapacity }]
+        tables
       });
     }
     ElMessage.success(editId.value == null ? "添加成功" : "修改成功");
@@ -399,4 +571,53 @@ onMounted(() => {
   width: 100%;
   padding: 12px 0;
 }
+.add-table-toolbar {
+  position: relative;
+  bottom: 5px;
+  margin: 0 0 8px 80px;
+}
+.table-row-sep {
+  width: 100%;
+  margin: 0 0 12px;
+  border: none;
+  border-bottom: 1px solid var(--el-border-color-lighter);
+}
+.table-row-delete {
+  position: relative;
+  bottom: 5px;
+  margin: 0 0 4px 80px;
+  text-align: right;
+}
+</style>
+
+<!-- append-to-body 时弹窗挂到 body,需非 scoped 才能稳定命中 -->
+<style lang="scss">
+.table-management-dialog.el-dialog .el-dialog__body {
+  box-sizing: border-box;
+  max-height: min(420px, 65vh);
+  overflow: hidden auto;
+}
+
+/* 弹窗内表单:与 .table-search 一致,控件占满标签右侧(scoped 对 teleport 不生效) */
+.table-management-dialog .table-dialog-form {
+  width: 100%;
+  .el-form-item {
+    width: 100%;
+  }
+  .el-form-item__content {
+    flex: 1;
+    min-width: 0;
+  }
+  .el-form-item__content > .el-input,
+  .el-form-item__content > .el-select {
+    width: 100%;
+    max-width: 100%;
+  }
+  .el-input {
+    width: 100%;
+  }
+  .el-select {
+    width: 100%;
+  }
+}
 </style>