| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628 |
- <template>
- <div class="table-box table-management">
- <!-- 筛选(与原先一致:搜索/重置后再请求列表) -->
- <div class="filter-bar">
- <div class="filter-row">
- <span class="filter-label">位置</span>
- <el-select v-model="filterLocation" placeholder="请选择" clearable style="width: 200px">
- <el-option v-for="item in categoryOptions" :key="item.id" :label="item.name" :value="item.id" />
- </el-select>
- <el-button type="primary" @click="handleSearch"> 搜索 </el-button>
- <el-button @click="handleReset"> 重置 </el-button>
- </div>
- </div>
- <ProTable ref="proTable" :columns="columns" :request-api="getTableList" :init-param="initParam">
- <template #tableHeader>
- <div class="table-header">
- <el-button type="primary" @click="handleNew"> 新建 </el-button>
- </div>
- </template>
- <template #operation="scope">
- <el-button link type="primary" @click="handleEdit(scope.row)"> 编辑 </el-button>
- <el-button link type="primary" @click="handleDelete(scope.row)"> 删除 </el-button>
- </template>
- </ProTable>
- <!-- 新建/编辑弹窗 -->
- <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"
- 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>
- <el-button type="primary" :loading="submitLoading" @click="submitForm"> 确定 </el-button>
- </template>
- </el-dialog>
- </div>
- </template>
- <script setup lang="ts">
- import { ref, reactive, computed, onMounted } from "vue";
- import { ElMessage, ElMessageBox } 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 {
- scheduleList,
- tableList,
- tableDetail,
- tableAdd,
- tableEdit,
- tableDel,
- getTableHasReservation
- } from "@/api/modules/scheduledService";
- import { localGet } from "@/utils";
- export interface TableRow {
- id: number | string;
- _seq?: number;
- tableNo: string;
- /** 列表接口返回的座位数字段 */
- seatingCapacity: number;
- categoryId?: number | string;
- locationName?: string;
- }
- const proTable = ref<ProTableInstance>();
- const columns: ColumnProps<TableRow>[] = [
- { prop: "_seq", label: "序号", width: 60, align: "center" },
- { prop: "tableNo", label: "桌号" },
- { prop: "seatingCapacity", label: "座位数", align: "center" },
- { prop: "locationName", label: "位置" },
- { prop: "operation", label: "操作", fixed: "right", width: 140, align: "center" }
- ];
- const initParam = reactive({
- storeId: localGet("geeker-user")?.userInfo?.storeId ?? localGet("createdId") ?? ""
- });
- /** 列表请求时使用的位置筛选(仅点击「搜索」后生效) */
- const listFilterCategoryId = ref<number | string | undefined>(undefined);
- const filterLocation = ref<number | string | undefined>(undefined);
- const categoryOptions = ref<{ id: number | string; name: string }[]>([]);
- 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, storeId: number | string): Promise<boolean> {
- const res: any = await getTableHasReservation({ tableId: tableId, storeId: getStoreId() });
- return parseHasReservationResponse(res);
- }
- async function loadCategories() {
- const storeId = getStoreId();
- if (!storeId) {
- categoryOptions.value = [];
- return;
- }
- try {
- const res: any = await scheduleList({ storeId: Number(storeId), pageNum: 1, pageSize: 500 });
- const raw = res?.data?.records ?? res?.data ?? res?.list ?? res ?? [];
- const arr = Array.isArray(raw) ? raw : [];
- categoryOptions.value = arr.map((item: any) => ({
- id: item.id,
- name: item.categoryName ?? item.name ?? ""
- }));
- } catch {
- categoryOptions.value = [];
- }
- }
- 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 ?? "",
- 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)] ?? ""
- };
- }
- /** ProTable / useTable 约定:返回 { data: { list, total } } */
- async function getTableList(params: any) {
- const storeId = params.storeId ?? initParam.storeId;
- if (!storeId) {
- return { data: { list: [] as TableRow[], total: 0 } };
- }
- if (categoryOptions.value.length === 0) {
- await loadCategories();
- }
- const pageNum = params.pageNum ?? 1;
- const pageSize = params.pageSize ?? 10;
- const categoryMap = Object.fromEntries(categoryOptions.value.map(c => [String(c.id), c.name]));
- const req: {
- storeId: number;
- pageNum: number;
- pageSize: number;
- categoryId?: number | string;
- } = {
- storeId: Number(storeId),
- pageNum,
- pageSize
- };
- if (listFilterCategoryId.value != null && listFilterCategoryId.value !== "") {
- req.categoryId = listFilterCategoryId.value;
- }
- try {
- const res: any = await tableList(req);
- const body = res?.data ?? res;
- const inner = body?.data ?? body;
- const raw = inner?.records ?? inner?.list ?? (Array.isArray(inner) ? inner : []);
- const arr = Array.isArray(raw) ? raw : [];
- const total = inner?.total ?? body?.total ?? inner?.totalCount ?? 0;
- const list = arr.map((item: any, i: number) => ({
- ...mapTableRow(item, categoryMap),
- _seq: (pageNum - 1) * pageSize + i + 1
- }));
- return { data: { list, total: Number(total) || 0 } };
- } catch (e: any) {
- ElMessage.error(e?.message || "加载失败");
- return { data: { list: [] as TableRow[], total: 0 } };
- }
- }
- function handleSearch() {
- listFilterCategoryId.value = filterLocation.value;
- proTable.value?.getTableList();
- }
- function handleReset() {
- filterLocation.value = undefined;
- listFilterCategoryId.value = undefined;
- proTable.value?.getTableList();
- }
- const dialogVisible = ref(false);
- const editId = ref<number | string | null>(null);
- const submitLoading = ref(false);
- const detailLoading = ref(false);
- const formRef = ref<FormInstance>();
- const form = reactive<{
- tableNo: string;
- seatCount: number;
- categoryId: number | string | undefined;
- /** 与 App addTableNumber 一致:座位为字符串便于输入过滤 */
- tableRows: { tableNo: string; seatCount: string }[];
- }>({
- tableNo: "",
- seatCount: 0,
- categoryId: undefined,
- tableRows: [{ tableNo: "", seatCount: "" }]
- });
- /** 与 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;
- }
- 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;
- form.tableRows = [{ tableNo: "", seatCount: "" }];
- dialogVisible.value = true;
- detailLoading.value = true;
- try {
- const res: any = await tableDetail({ id: row.id });
- const d = res?.data ?? res ?? {};
- 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.seatingCapacity ?? 0;
- if (typeof form.seatCount === "string") form.seatCount = parseInt(form.seatCount, 10) || 0;
- if (
- categoryId != null &&
- categoryOptions.value.length > 0 &&
- !categoryOptions.value.find(c => String(c.id) === String(categoryId))
- ) {
- await loadCategories();
- }
- } catch (e: any) {
- ElMessage.error(e?.message || "获取详情失败");
- form.tableNo = row.tableNo;
- form.seatCount = row.seatingCapacity ?? 0;
- form.categoryId = row.categoryId ?? undefined;
- } finally {
- detailLoading.value = false;
- }
- }
- 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: "取消",
- type: "warning"
- })
- .then(async () => {
- try {
- await tableDel({ id: row.id });
- ElMessage.success("删除成功");
- proTable.value?.getTableList();
- } catch (e: any) {
- ElMessage.error(e?.message || "删除失败");
- }
- })
- .catch(() => {});
- }
- function onDialogClose() {
- editId.value = null;
- form.tableNo = "";
- form.seatCount = 0;
- form.categoryId = undefined;
- form.tableRows = [{ tableNo: "", seatCount: "" }];
- }
- async function submitForm() {
- if (!formRef.value) return;
- try {
- await formRef.value.validate();
- } catch {
- return;
- }
- const storeId = getStoreId();
- if (!storeId) {
- ElMessage.warning("未找到门店信息");
- return;
- }
- submitLoading.value = true;
- try {
- const tableNumber = form.tableNo.trim();
- const seatingCapacity = Number(form.seatCount) || 0;
- if (editId.value != null) {
- await tableEdit({
- id: editId.value,
- storeId,
- categoryId: form.categoryId,
- tableNumber,
- 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
- });
- }
- ElMessage.success(editId.value == null ? "添加成功" : "修改成功");
- dialogVisible.value = false;
- onDialogClose();
- proTable.value?.getTableList();
- } catch (e: any) {
- ElMessage.error(e?.message || "保存失败");
- } finally {
- submitLoading.value = false;
- }
- }
- onMounted(() => {
- loadCategories();
- });
- </script>
- <style lang="scss">
- .table-management-dialog .el-dialog__body {
- box-sizing: border-box;
- height: 300px;
- }
- </style>
- <style scoped lang="scss">
- .table-management {
- padding: 0;
- }
- .filter-bar {
- padding: 0 4px;
- margin-bottom: 12px;
- }
- .filter-row {
- display: flex;
- flex-wrap: wrap;
- gap: 12px;
- align-items: center;
- }
- .filter-label {
- min-width: 32px;
- font-size: 14px;
- color: #606266;
- }
- .table-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- 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>
|