|
|
@@ -0,0 +1,881 @@
|
|
|
+<template>
|
|
|
+ <div class="info-management">
|
|
|
+ <div class="page-title">信息设置</div>
|
|
|
+
|
|
|
+ <!-- 基础信息 -->
|
|
|
+ <div class="form-card">
|
|
|
+ <div class="section-title">
|
|
|
+ <span class="section-bar" />
|
|
|
+ <span>基础信息</span>
|
|
|
+ </div>
|
|
|
+ <el-form :model="form.base" label-width="450px" class="section-form">
|
|
|
+ <el-form-item label="未按时到店">
|
|
|
+ <el-radio-group v-model="form.base.keepPosition">
|
|
|
+ <el-radio label="keep"> 保留位置 </el-radio>
|
|
|
+ <el-radio label="notKeep"> 不保留位置 </el-radio>
|
|
|
+ </el-radio-group>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item v-if="form.base.keepPosition === 'keep'" label="保留时长(分钟)" required>
|
|
|
+ <el-input
|
|
|
+ :model-value="String(form.base.retentionMinutes ?? '')"
|
|
|
+ placeholder="请输入"
|
|
|
+ clearable
|
|
|
+ maxlength="2"
|
|
|
+ class="form-input"
|
|
|
+ @update:model-value="(v: string) => onIntInput(v, 2, n => (form.base.retentionMinutes = n))"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="预订日期显示(天)" required>
|
|
|
+ <el-input
|
|
|
+ :model-value="String(form.base.bookDateDays ?? '')"
|
|
|
+ placeholder="请输入"
|
|
|
+ clearable
|
|
|
+ maxlength="2"
|
|
|
+ class="form-input"
|
|
|
+ @update:model-value="(v: string) => onIntInput(v, 2, n => (form.base.bookDateDays = n))"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="单时段最大容纳人数" required>
|
|
|
+ <el-input
|
|
|
+ :model-value="String(form.base.maxCapacityPerSlot ?? '')"
|
|
|
+ placeholder="请输入"
|
|
|
+ clearable
|
|
|
+ maxlength="4"
|
|
|
+ class="form-input"
|
|
|
+ @update:model-value="(v: string) => onIntInput(v, 4, n => (form.base.maxCapacityPerSlot = n))"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 预订设置 -->
|
|
|
+ <div class="form-card">
|
|
|
+ <div class="section-title">
|
|
|
+ <span class="section-bar" />
|
|
|
+ <span>预订设置</span>
|
|
|
+ </div>
|
|
|
+ <el-form :model="form.booking" label-width="450px" class="section-form">
|
|
|
+ <el-form-item label="预订">
|
|
|
+ <el-radio-group v-model="form.booking.feeType">
|
|
|
+ <el-radio label="free"> 免费 </el-radio>
|
|
|
+ <el-radio label="paid"> 付费 </el-radio>
|
|
|
+ </el-radio-group>
|
|
|
+ </el-form-item>
|
|
|
+ <template v-if="form.booking.feeType === 'paid'">
|
|
|
+ <el-form-item label="预订金额(元)" required>
|
|
|
+ <el-input
|
|
|
+ :model-value="String(form.booking.bookAmount ?? '')"
|
|
|
+ placeholder="请输入"
|
|
|
+ clearable
|
|
|
+ maxlength="3"
|
|
|
+ class="form-input"
|
|
|
+ @update:model-value="(v: string) => onIntInput(v, 3, n => (form.booking.bookAmount = n))"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="取消预订退费时长设置,需提前(小时)" required>
|
|
|
+ <el-input
|
|
|
+ :model-value="String(form.booking.refundAdvanceHours ?? '')"
|
|
|
+ placeholder="请输入"
|
|
|
+ clearable
|
|
|
+ maxlength="3"
|
|
|
+ class="form-input"
|
|
|
+ @update:model-value="(v: string) => onIntInput(v, 3, n => (form.booking.refundAdvanceHours = n))"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ </template>
|
|
|
+ <el-form-item label="营业时间结束前多少分钟不可预订" required>
|
|
|
+ <el-input
|
|
|
+ :model-value="String(form.booking.noBookMinutesBeforeClose ?? '')"
|
|
|
+ placeholder="请输入"
|
|
|
+ clearable
|
|
|
+ maxlength="3"
|
|
|
+ class="form-input"
|
|
|
+ @update:model-value="(v: string) => onIntInput(v, 3, n => (form.booking.noBookMinutesBeforeClose = n))"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 正常营业 -->
|
|
|
+ <div class="form-card">
|
|
|
+ <div class="section-title">
|
|
|
+ <span class="section-bar" />
|
|
|
+ <span>正常营业</span>
|
|
|
+ </div>
|
|
|
+ <el-form :model="form.normalBook" label-width="450px" class="section-form">
|
|
|
+ <el-form-item label="预订时间">
|
|
|
+ <el-radio-group v-model="form.normalBook.timeType">
|
|
|
+ <el-radio label="allDay" :disabled="normalAllDayDisabled"> 全天 </el-radio>
|
|
|
+ <el-radio label="notAllDay"> 非全天 </el-radio>
|
|
|
+ </el-radio-group>
|
|
|
+ </el-form-item>
|
|
|
+ <template v-if="form.normalBook.timeType === 'notAllDay'">
|
|
|
+ <el-form-item label="时间设置">
|
|
|
+ <div class="time-row">
|
|
|
+ <el-time-picker
|
|
|
+ v-model="normalStartTimeValue"
|
|
|
+ format="HH:mm"
|
|
|
+ value-format="HH:mm"
|
|
|
+ placeholder="选择开始时间"
|
|
|
+ style="width: 160px"
|
|
|
+ :disabled-hours="normalStartDisabledHours"
|
|
|
+ :disabled-minutes="normalStartDisabledMinutes"
|
|
|
+ @visible-change="v => v && initNormalStartPickerRange()"
|
|
|
+ @change="onNormalStartTimeChange"
|
|
|
+ />
|
|
|
+ <el-time-picker
|
|
|
+ v-model="normalEndTimeValue"
|
|
|
+ format="HH:mm"
|
|
|
+ value-format="HH:mm"
|
|
|
+ placeholder="选择结束时间(次日)"
|
|
|
+ style="width: 160px"
|
|
|
+ :disabled-hours="normalEndDisabledHours"
|
|
|
+ :disabled-minutes="normalEndDisabledMinutes"
|
|
|
+ @visible-change="v => v && initNormalEndPickerRange()"
|
|
|
+ @change="onNormalEndTimeChange"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+ </template>
|
|
|
+ </el-form>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 特殊营业 -->
|
|
|
+ <div class="form-card">
|
|
|
+ <div class="section-title">
|
|
|
+ <span class="section-bar" />
|
|
|
+ <span>特殊营业</span>
|
|
|
+ </div>
|
|
|
+ <el-form label-width="450px" class="section-form">
|
|
|
+ <el-form-item label="选择节日">
|
|
|
+ <el-select
|
|
|
+ v-model="selectedHolidayNames"
|
|
|
+ multiple
|
|
|
+ collapse-tags
|
|
|
+ collapse-tags-tooltip
|
|
|
+ placeholder="下拉菜单"
|
|
|
+ style="width: 40%"
|
|
|
+ :disabled="holidayOptions.length === 0"
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="item in holidayOptions"
|
|
|
+ :key="item.name"
|
|
|
+ :label="item.name"
|
|
|
+ :value="item.name"
|
|
|
+ :disabled="!allowedFestivalSet.has(item.name)"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <template v-for="(item, idx) in form.specialList" :key="item.name + '-' + idx">
|
|
|
+ <el-form-item :label="item.name">
|
|
|
+ <el-radio-group v-model="item.allDay">
|
|
|
+ <el-radio label="allDay" :disabled="!!specialAllDayDisabledMap[item.name]"> 全天 </el-radio>
|
|
|
+ <el-radio label="notAllDay"> 非全天 </el-radio>
|
|
|
+ </el-radio-group>
|
|
|
+ </el-form-item>
|
|
|
+ <div v-if="item.allDay === 'notAllDay'" class="special-time-row">
|
|
|
+ <el-form-item label="开始时间" label-width="80px">
|
|
|
+ <el-time-picker
|
|
|
+ v-model="item.startTime"
|
|
|
+ format="HH:mm"
|
|
|
+ value-format="HH:mm"
|
|
|
+ placeholder="选择开始时间"
|
|
|
+ style="width: 160px"
|
|
|
+ :disabled-hours="() => specialDisabledHours()"
|
|
|
+ :disabled-minutes="(h: number) => specialDisabledMinutes(h)"
|
|
|
+ @visible-change="v => v && initSpecialPickerRange(item.name, 'start')"
|
|
|
+ @change="() => onSpecialTimeChange(item.name, 'start')"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="结束时间" label-width="80px">
|
|
|
+ <el-time-picker
|
|
|
+ v-model="item.endTime"
|
|
|
+ format="HH:mm"
|
|
|
+ value-format="HH:mm"
|
|
|
+ placeholder="选择结束时间(次日)"
|
|
|
+ style="width: 160px"
|
|
|
+ :disabled-hours="() => specialDisabledHours()"
|
|
|
+ :disabled-minutes="(h: number) => specialDisabledMinutes(h)"
|
|
|
+ @visible-change="v => v && initSpecialPickerRange(item.name, 'end')"
|
|
|
+ @change="() => onSpecialTimeChange(item.name, 'end')"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-form>
|
|
|
+ </div>
|
|
|
+ <div style=" display: flex; justify-content: center;width: 100%">
|
|
|
+ <el-button type="primary" :loading="saveLoading" @click="onSave"> 保存 </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref, reactive, computed, onMounted, watch } from "vue";
|
|
|
+import { ElMessage } from "element-plus";
|
|
|
+import { InfoFilled } from "@element-plus/icons-vue";
|
|
|
+import { bookingSettingsDetail, bookingSettingsSave, getBookingBusinessHours } from "@/api/modules/scheduledService";
|
|
|
+import { getStoreInfoBusinessHours, getHolidayList } from "@/api/modules/storeDecoration";
|
|
|
+import { localGet } from "@/utils";
|
|
|
+
|
|
|
+const saveLoading = ref(false);
|
|
|
+
|
|
|
+/** 数字输入:仅保留数字、限制位数,清空为 0 */
|
|
|
+function onIntInput(raw: string, maxLen: number, set: (n: number) => void) {
|
|
|
+ const d = String(raw).replace(/\D/g, "").slice(0, maxLen);
|
|
|
+ set(d === "" ? 0 : parseInt(d, 10));
|
|
|
+}
|
|
|
+
|
|
|
+const form = reactive({
|
|
|
+ base: {
|
|
|
+ id: undefined as number | string | undefined,
|
|
|
+ keepPosition: "keep" as "keep" | "notKeep",
|
|
|
+ retentionMinutes: 30 as number,
|
|
|
+ bookDateDays: 7 as number,
|
|
|
+ maxCapacityPerSlot: 10 as number
|
|
|
+ },
|
|
|
+ booking: {
|
|
|
+ feeType: "free" as "free" | "paid",
|
|
|
+ bookAmount: 0 as number,
|
|
|
+ refundAdvanceHours: 24 as number,
|
|
|
+ noBookMinutesBeforeClose: 30 as number
|
|
|
+ },
|
|
|
+ normalBook: {
|
|
|
+ timeType: "allDay" as "allDay" | "notAllDay",
|
|
|
+ startTime: "" as string,
|
|
|
+ endTime: "" as string,
|
|
|
+ normalId: 0 as number
|
|
|
+ },
|
|
|
+ specialList: [] as {
|
|
|
+ name: string;
|
|
|
+ allDay: "allDay" | "notAllDay";
|
|
|
+ startTime: string;
|
|
|
+ endTime: string;
|
|
|
+ id?: number;
|
|
|
+ essentialId?: number;
|
|
|
+ }[]
|
|
|
+});
|
|
|
+
|
|
|
+const listFromStoreInfo = ref<any[]>([]);
|
|
|
+const listBooking = ref<any[]>([]);
|
|
|
+const holidayOptions = ref<{ name: string }[]>([]);
|
|
|
+
|
|
|
+const normalStartTimeValue = ref<string>("");
|
|
|
+const normalEndTimeValue = ref<string>("");
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => form.normalBook.startTime,
|
|
|
+ v => {
|
|
|
+ normalStartTimeValue.value = v || "";
|
|
|
+ },
|
|
|
+ { immediate: true }
|
|
|
+);
|
|
|
+watch(
|
|
|
+ () => form.normalBook.endTime,
|
|
|
+ v => {
|
|
|
+ normalEndTimeValue.value = v || "";
|
|
|
+ },
|
|
|
+ { immediate: true }
|
|
|
+);
|
|
|
+watch(normalStartTimeValue, v => {
|
|
|
+ form.normalBook.startTime = v || "";
|
|
|
+});
|
|
|
+watch(normalEndTimeValue, v => {
|
|
|
+ form.normalBook.endTime = v || "";
|
|
|
+});
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => form.normalBook.startTime,
|
|
|
+ v => {
|
|
|
+ if (form.normalBook.timeType !== "notAllDay" || !v || !form.normalBook.endTime) return;
|
|
|
+ const st = timeStrToMinutes(v);
|
|
|
+ const et = timeStrToMinutes(form.normalBook.endTime);
|
|
|
+ if (et >= st) return;
|
|
|
+ const s = minutesToTimeStr(st);
|
|
|
+ normalEndTimeValue.value = s;
|
|
|
+ form.normalBook.endTime = s;
|
|
|
+ }
|
|
|
+);
|
|
|
+
|
|
|
+const allowedFestivalSet = computed(() => {
|
|
|
+ const set = new Set<string>();
|
|
|
+ listFromStoreInfo.value
|
|
|
+ .filter(item => item.businessType === 2)
|
|
|
+ .forEach(item => {
|
|
|
+ const name =
|
|
|
+ (item.holidayInfo && item.holidayInfo.festivalName != null ? String(item.holidayInfo.festivalName).trim() : "") ||
|
|
|
+ (item.festivalName != null ? String(item.festivalName).trim() : "") ||
|
|
|
+ (item.businessDate != null ? String(item.businessDate).trim() : "") ||
|
|
|
+ (item.holidayType != null ? String(item.holidayType).trim() : "");
|
|
|
+ if (name) set.add(name);
|
|
|
+ });
|
|
|
+ return set;
|
|
|
+});
|
|
|
+
|
|
|
+const selectedHolidayNames = computed({
|
|
|
+ get: () => form.specialList.map(s => s.name),
|
|
|
+ set: (val: string[]) => {
|
|
|
+ const existingMap: Record<string, (typeof form.specialList)[0]> = {};
|
|
|
+ form.specialList.forEach(s => {
|
|
|
+ existingMap[s.name] = s;
|
|
|
+ });
|
|
|
+ form.specialList = val.map(name => existingMap[name] || { name, allDay: "allDay", startTime: "", endTime: "" });
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+function getStoreId(): number | string | null {
|
|
|
+ return localGet("geeker-user")?.userInfo?.storeId ?? localGet("createdId") ?? null;
|
|
|
+}
|
|
|
+
|
|
|
+function subtractMinutesFromTime(timeStr: string, minutes: number): string {
|
|
|
+ const [h, m] = (timeStr || "00:00").split(":").map(Number);
|
|
|
+ let total = (h || 0) * 60 + (m || 0);
|
|
|
+ total = Math.max(0, total - (Number(minutes) || 0));
|
|
|
+ const nh = Math.floor(total / 60) % 24;
|
|
|
+ const nm = total % 60;
|
|
|
+ return `${String(nh).padStart(2, "0")}:${String(nm).padStart(2, "0")}`;
|
|
|
+}
|
|
|
+
|
|
|
+function timeStrToMinutes(timeStr: string): number {
|
|
|
+ const [h, m] = String(timeStr || "00:00")
|
|
|
+ .split(":")
|
|
|
+ .map(Number);
|
|
|
+ return Math.max(0, Math.min(23 * 60 + 59, (h || 0) * 60 + (m || 0)));
|
|
|
+}
|
|
|
+
|
|
|
+function minutesToTimeStr(total: number): string {
|
|
|
+ const t = Math.max(0, Math.min(23 * 60 + 59, total));
|
|
|
+ const h = Math.floor(t / 60);
|
|
|
+ const m = t % 60;
|
|
|
+ return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
|
|
|
+}
|
|
|
+
|
|
|
+/** 与 merchant:填写了「结束前不可预订」则结束时间可选上限 = 营业 end − 该分钟数 */
|
|
|
+function getNoBookMinutesForPicker(): number {
|
|
|
+ const v = form.booking.noBookMinutesBeforeClose;
|
|
|
+ if (v == null) return 0;
|
|
|
+ const n = Number(String(v).trim());
|
|
|
+ return n > 0 && !isNaN(n) ? n : 0;
|
|
|
+}
|
|
|
+
|
|
|
+function computePickerEndMinutes(endTimeRaw: string): number {
|
|
|
+ const noBook = getNoBookMinutesForPicker();
|
|
|
+ return timeStrToMinutes(subtractMinutesFromTime(endTimeRaw || "23:59", noBook));
|
|
|
+}
|
|
|
+
|
|
|
+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";
|
|
|
+ let endTimeRaw = store?.endTime != null && String(store.endTime).trim() !== "" ? String(store.endTime).trim() : "23:59";
|
|
|
+ if (startTime === "00:00" && endTimeRaw === "00:00") {
|
|
|
+ endTimeRaw = "23:59";
|
|
|
+ }
|
|
|
+ return { startTime, endTimeRaw };
|
|
|
+}
|
|
|
+
|
|
|
+function getSpecialWindowForPicker(holidayName: string) {
|
|
|
+ const n = String(holidayName || "").trim();
|
|
|
+ const storeList = Array.isArray(listFromStoreInfo.value) ? listFromStoreInfo.value : [];
|
|
|
+ const storeRow = storeList.find(
|
|
|
+ (s: any) =>
|
|
|
+ s.businessType === 2 &&
|
|
|
+ ((s.holidayInfo && String(s.holidayInfo.festivalName || "").trim()) === n ||
|
|
|
+ String(s.businessDate || "").trim() === n ||
|
|
|
+ String(s.holidayType || "").trim() === n)
|
|
|
+ );
|
|
|
+ let startTime =
|
|
|
+ storeRow?.startTime != null && String(storeRow.startTime).trim() !== "" ? String(storeRow.startTime).trim() : "00:00";
|
|
|
+ let endTimeRaw =
|
|
|
+ storeRow?.endTime != null && String(storeRow.endTime).trim() !== "" ? String(storeRow.endTime).trim() : "23:59";
|
|
|
+ if (startTime === "00:00" && endTimeRaw === "00:00") {
|
|
|
+ endTimeRaw = "23:59";
|
|
|
+ }
|
|
|
+ return { startTime, endTimeRaw };
|
|
|
+}
|
|
|
+
|
|
|
+function buildDisabledHours(sm: number, em: number): number[] {
|
|
|
+ const arr: number[] = [];
|
|
|
+ for (let h = 0; h < 24; h++) {
|
|
|
+ const lo = h * 60;
|
|
|
+ const hi = h * 60 + 59;
|
|
|
+ if (hi < sm || lo > em) arr.push(h);
|
|
|
+ }
|
|
|
+ return arr;
|
|
|
+}
|
|
|
+
|
|
|
+function buildDisabledMinutes(hour: number, sm: number, em: number): number[] {
|
|
|
+ const arr: number[] = [];
|
|
|
+ const startH = Math.floor(sm / 60);
|
|
|
+ const endH = Math.floor(em / 60);
|
|
|
+ if (hour < startH || hour > endH) {
|
|
|
+ return Array.from({ length: 60 }, (_, i) => i);
|
|
|
+ }
|
|
|
+ let minM = 0;
|
|
|
+ let maxM = 59;
|
|
|
+ if (startH === endH) {
|
|
|
+ minM = sm % 60;
|
|
|
+ maxM = em % 60;
|
|
|
+ } else if (hour === startH) {
|
|
|
+ minM = sm % 60;
|
|
|
+ } else if (hour === endH) {
|
|
|
+ maxM = em % 60;
|
|
|
+ }
|
|
|
+ for (let m = 0; m < 60; m++) {
|
|
|
+ if (m < minM || m > maxM) arr.push(m);
|
|
|
+ }
|
|
|
+ return arr;
|
|
|
+}
|
|
|
+
|
|
|
+const normalStartPickSm = ref(0);
|
|
|
+const normalStartPickEm = ref(23 * 60 + 59);
|
|
|
+const normalEndPickSm = ref(0);
|
|
|
+const normalEndPickEm = ref(23 * 60 + 59);
|
|
|
+const specialPickSm = ref(0);
|
|
|
+const specialPickEm = ref(23 * 60 + 59);
|
|
|
+
|
|
|
+function initNormalStartPickerRange() {
|
|
|
+ const { startTime, endTimeRaw } = getNormalWindowForPicker();
|
|
|
+ let sm = timeStrToMinutes(startTime || "00:00");
|
|
|
+ let em = computePickerEndMinutes(endTimeRaw);
|
|
|
+ if (em < sm) em = 23 * 60 + 59;
|
|
|
+ normalStartPickSm.value = sm;
|
|
|
+ normalStartPickEm.value = em;
|
|
|
+}
|
|
|
+
|
|
|
+function initNormalEndPickerRange() {
|
|
|
+ const { startTime, endTimeRaw } = getNormalWindowForPicker();
|
|
|
+ let sm = timeStrToMinutes(startTime || "00:00");
|
|
|
+ let em = computePickerEndMinutes(endTimeRaw);
|
|
|
+ if (em < sm) em = 23 * 60 + 59;
|
|
|
+ const stSel = timeStrToMinutes(form.normalBook.startTime || "00:00");
|
|
|
+ sm = Math.max(sm, stSel);
|
|
|
+ if (sm > em) em = sm;
|
|
|
+ normalEndPickSm.value = sm;
|
|
|
+ normalEndPickEm.value = em;
|
|
|
+}
|
|
|
+
|
|
|
+function initSpecialPickerRange(holidayName: string, key: "start" | "end") {
|
|
|
+ const { startTime, endTimeRaw } = getSpecialWindowForPicker(holidayName);
|
|
|
+ let sm = timeStrToMinutes(startTime || "00:00");
|
|
|
+ let em = computePickerEndMinutes(endTimeRaw);
|
|
|
+ if (em < sm) em = 23 * 60 + 59;
|
|
|
+ if (key === "end") {
|
|
|
+ const item = form.specialList.find(s => s.name === holidayName);
|
|
|
+ const stSel = timeStrToMinutes(item?.startTime || "00:00");
|
|
|
+ sm = Math.max(sm, stSel);
|
|
|
+ if (sm > em) em = sm;
|
|
|
+ }
|
|
|
+ specialPickSm.value = sm;
|
|
|
+ specialPickEm.value = em;
|
|
|
+}
|
|
|
+
|
|
|
+function normalStartDisabledHours() {
|
|
|
+ return buildDisabledHours(normalStartPickSm.value, normalStartPickEm.value);
|
|
|
+}
|
|
|
+function normalStartDisabledMinutes(hour: number) {
|
|
|
+ return buildDisabledMinutes(hour, normalStartPickSm.value, normalStartPickEm.value);
|
|
|
+}
|
|
|
+function normalEndDisabledHours() {
|
|
|
+ return buildDisabledHours(normalEndPickSm.value, normalEndPickEm.value);
|
|
|
+}
|
|
|
+function normalEndDisabledMinutes(hour: number) {
|
|
|
+ return buildDisabledMinutes(hour, normalEndPickSm.value, normalEndPickEm.value);
|
|
|
+}
|
|
|
+function specialDisabledHours() {
|
|
|
+ return buildDisabledHours(specialPickSm.value, specialPickEm.value);
|
|
|
+}
|
|
|
+function specialDisabledMinutes(hour: number) {
|
|
|
+ return buildDisabledMinutes(hour, specialPickSm.value, specialPickEm.value);
|
|
|
+}
|
|
|
+
|
|
|
+function onNormalStartTimeChange(val: string | null) {
|
|
|
+ if (val == null || val === "") return;
|
|
|
+ let vm = timeStrToMinutes(val);
|
|
|
+ vm = Math.max(normalStartPickSm.value, Math.min(normalStartPickEm.value, vm));
|
|
|
+ const s = minutesToTimeStr(vm);
|
|
|
+ normalStartTimeValue.value = s;
|
|
|
+ form.normalBook.startTime = s;
|
|
|
+}
|
|
|
+
|
|
|
+function onNormalEndTimeChange(val: string | null) {
|
|
|
+ if (val == null || val === "") return;
|
|
|
+ let vm = timeStrToMinutes(val);
|
|
|
+ vm = Math.max(normalEndPickSm.value, Math.min(normalEndPickEm.value, vm));
|
|
|
+ const s = minutesToTimeStr(vm);
|
|
|
+ normalEndTimeValue.value = s;
|
|
|
+ form.normalBook.endTime = s;
|
|
|
+}
|
|
|
+
|
|
|
+function onSpecialTimeChange(holidayName: string, key: "start" | "end") {
|
|
|
+ const item = form.specialList.find(s => s.name === holidayName);
|
|
|
+ if (!item) return;
|
|
|
+ const field = key === "start" ? "startTime" : "endTime";
|
|
|
+ const val = item[field];
|
|
|
+ if (val == null || val === "") return;
|
|
|
+ let vm = timeStrToMinutes(val);
|
|
|
+ vm = Math.max(specialPickSm.value, Math.min(specialPickEm.value, vm));
|
|
|
+ item[field] = minutesToTimeStr(vm);
|
|
|
+}
|
|
|
+
|
|
|
+/** 门店正常营业非 00:00–00:00 时不可选「全天」(与 merchant) */
|
|
|
+const normalAllDayDisabled = computed(() => {
|
|
|
+ const list = Array.isArray(listFromStoreInfo.value) ? listFromStoreInfo.value : [];
|
|
|
+ const normal = list.find((item: any) => item.businessType === 1 || item.businessType === 0);
|
|
|
+ if (!normal) return false;
|
|
|
+ const st = String(normal.startTime || "").trim();
|
|
|
+ const et = String(normal.endTime || "").trim();
|
|
|
+ return st !== "00:00" || et !== "00:00";
|
|
|
+});
|
|
|
+
|
|
|
+/** 各节日门店配置非 00:00–00:00 时该行不可选「全天」 */
|
|
|
+const specialAllDayDisabledMap = computed(() => {
|
|
|
+ const list = Array.isArray(listFromStoreInfo.value) ? listFromStoreInfo.value : [];
|
|
|
+ const map: Record<string, boolean> = {};
|
|
|
+ list
|
|
|
+ .filter((item: any) => item.businessType === 2)
|
|
|
+ .forEach((s: any) => {
|
|
|
+ const name = (s.holidayInfo && s.holidayInfo.festivalName) || s.businessDate || s.holidayType || "";
|
|
|
+ if (name) {
|
|
|
+ const st = String(s.startTime || "").trim();
|
|
|
+ const et = String(s.endTime || "").trim();
|
|
|
+ map[String(name)] = st !== "00:00" || et !== "00:00";
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return map;
|
|
|
+});
|
|
|
+
|
|
|
+async function loadHolidayOptions() {
|
|
|
+ try {
|
|
|
+ const year = new Date().getFullYear();
|
|
|
+ const res: any = await getHolidayList({
|
|
|
+ holidayName: "",
|
|
|
+ openFlag: 1,
|
|
|
+ year,
|
|
|
+ page: 1,
|
|
|
+ size: 100
|
|
|
+ });
|
|
|
+ const records = res?.data?.records ?? res?.records ?? [];
|
|
|
+ holidayOptions.value = records
|
|
|
+ .map((record: any) => ({
|
|
|
+ name: record.festivalName ?? record.holidayName ?? record.name ?? ""
|
|
|
+ }))
|
|
|
+ .filter((x: { name: string }) => x.name);
|
|
|
+ } catch {
|
|
|
+ holidayOptions.value = [];
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function fetchStoreInfoBusinessHours() {
|
|
|
+ const storeId = getStoreId();
|
|
|
+ if (!storeId) return;
|
|
|
+ try {
|
|
|
+ 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);
|
|
|
+ const specialItems = list.filter((item: any) => item.businessType === 2);
|
|
|
+ if (specialItems.length && !form.specialList.length) {
|
|
|
+ form.specialList = specialItems.map((s: any, index: number) => {
|
|
|
+ const name = (s.holidayInfo && s.holidayInfo.festivalName) || s.businessDate || s.holidayType || `特殊营业${index + 1}`;
|
|
|
+ return {
|
|
|
+ name,
|
|
|
+ allDay: "allDay" as const,
|
|
|
+ startTime: "",
|
|
|
+ endTime: "",
|
|
|
+ id: s.id,
|
|
|
+ essentialId: s.essentialId
|
|
|
+ };
|
|
|
+ });
|
|
|
+ }
|
|
|
+ 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";
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ listFromStoreInfo.value = [];
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+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 specialFromBooking = list.filter((item: any) => item.businessType === 2);
|
|
|
+ const specialFromStore = listFromStoreInfo.value.filter((item: any) => item.businessType === 2);
|
|
|
+ if (specialFromStore.length && form.specialList.length) {
|
|
|
+ form.specialList = specialFromStore.map((s: any, index: number) => {
|
|
|
+ const name = (s.holidayInfo && s.holidayInfo.festivalName) || s.businessDate || s.holidayType || `特殊营业${index + 1}`;
|
|
|
+ const match = specialFromBooking.find(
|
|
|
+ (b: any) =>
|
|
|
+ String(b.id) === String(s.id) ||
|
|
|
+ String(b.essentialId) === String(s.essentialId) ||
|
|
|
+ (b.holidayType != null && String(b.holidayType).trim() === String(name).trim())
|
|
|
+ );
|
|
|
+ if (!match) {
|
|
|
+ return {
|
|
|
+ name,
|
|
|
+ allDay: "allDay" as const,
|
|
|
+ startTime: "",
|
|
|
+ endTime: "",
|
|
|
+ id: s.id,
|
|
|
+ essentialId: s.essentialId
|
|
|
+ };
|
|
|
+ }
|
|
|
+ const startTime = match.startTime || "";
|
|
|
+ const endTimeRaw = match.endTime || "23:59";
|
|
|
+ /** 与 merchant 一致:特殊营业结束时间回显不扣「结束前不可预订」 */
|
|
|
+ const endTime = endTimeRaw;
|
|
|
+ const isAllDay = match.bookingTimeType === 1 || (startTime === "00:00" && endTimeRaw === "00:00");
|
|
|
+ return {
|
|
|
+ name,
|
|
|
+ allDay: isAllDay ? "allDay" : "notAllDay",
|
|
|
+ startTime,
|
|
|
+ endTime,
|
|
|
+ id: s.id,
|
|
|
+ essentialId: s.essentialId
|
|
|
+ };
|
|
|
+ });
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function fetchBookingBusinessHours() {
|
|
|
+ const settingsId = form.base.id;
|
|
|
+ if (settingsId == null || settingsId === undefined) return;
|
|
|
+ try {
|
|
|
+ const res: any = await getBookingBusinessHours({ settingsId });
|
|
|
+ const list = Array.isArray(res?.data) ? res.data : [];
|
|
|
+ listBooking.value = list;
|
|
|
+ applyListBookingToForm();
|
|
|
+ } catch {
|
|
|
+ listBooking.value = [];
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function getInfoSettings() {
|
|
|
+ const storeId = getStoreId();
|
|
|
+ if (!storeId) return;
|
|
|
+ try {
|
|
|
+ const res: any = await bookingSettingsDetail({ storeId: Number(storeId) });
|
|
|
+ const d = res?.data ?? res ?? {};
|
|
|
+ form.base.id = d.id;
|
|
|
+ form.base.keepPosition = d.retainPositionFlag === 1 ? "keep" : "notKeep";
|
|
|
+ form.base.retentionMinutes = Number(d.retentionDuration) || 30;
|
|
|
+ form.base.bookDateDays = Number(d.bookingDateDisplayDays) || 7;
|
|
|
+ form.base.maxCapacityPerSlot = Number(d.maxCapacityPerSlot) || 10;
|
|
|
+
|
|
|
+ form.booking.feeType = d.reservation === "1" ? "paid" : "free";
|
|
|
+ form.booking.bookAmount = Number(d.reservationMoney) || 0;
|
|
|
+ form.booking.refundAdvanceHours = Number(d.offUnsubscribeHours) || 24;
|
|
|
+ form.booking.noBookMinutesBeforeClose = Number(d.bookingNotAvailableTime) || 30;
|
|
|
+ } catch {
|
|
|
+ // 无数据时使用默认值
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function validate(): boolean {
|
|
|
+ if (form.base.keepPosition === "keep") {
|
|
|
+ const n = Number(form.base.retentionMinutes);
|
|
|
+ if (!n || n < 1 || n > 99) {
|
|
|
+ ElMessage.warning("保留时长(分钟)为1-99的整数");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const bookDateNum = Number(form.base.bookDateDays);
|
|
|
+ if (!bookDateNum || bookDateNum < 1 || bookDateNum > 99) {
|
|
|
+ ElMessage.warning("预订日期显示(天)为1-99的整数");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ const maxCap = Number(form.base.maxCapacityPerSlot);
|
|
|
+ if (!maxCap || maxCap < 1 || maxCap > 9999) {
|
|
|
+ ElMessage.warning("单时段最大容纳人数为1-9999的整数");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (form.booking.feeType === "paid") {
|
|
|
+ if (!Number(form.booking.bookAmount) || Number(form.booking.bookAmount) < 1 || Number(form.booking.bookAmount) > 999) {
|
|
|
+ ElMessage.warning("预订金额(元)为1-999的整数");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (
|
|
|
+ !Number(form.booking.refundAdvanceHours) ||
|
|
|
+ Number(form.booking.refundAdvanceHours) < 1 ||
|
|
|
+ Number(form.booking.refundAdvanceHours) > 999
|
|
|
+ ) {
|
|
|
+ ElMessage.warning("取消预订退费需提前(小时)为1-999的整数");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (
|
|
|
+ !Number(form.booking.noBookMinutesBeforeClose) ||
|
|
|
+ Number(form.booking.noBookMinutesBeforeClose) < 1 ||
|
|
|
+ Number(form.booking.noBookMinutesBeforeClose) > 999
|
|
|
+ ) {
|
|
|
+ ElMessage.warning("营业时间结束前多少分钟不可预订为1-999的整数");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (form.normalBook.timeType === "notAllDay") {
|
|
|
+ if (!form.normalBook.startTime || !form.normalBook.endTime) {
|
|
|
+ ElMessage.warning("请设置正常营业时间");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+async function onSave() {
|
|
|
+ if (!validate()) return;
|
|
|
+ const storeId = getStoreId();
|
|
|
+ if (!storeId) {
|
|
|
+ ElMessage.warning("未找到门店信息");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ saveLoading.value = true;
|
|
|
+ try {
|
|
|
+ const businessDate = listFromStoreInfo.value.find((item: any) => item.businessType === 1)?.businessDate;
|
|
|
+ const params: Record<string, any> = {
|
|
|
+ storeId: Number(storeId),
|
|
|
+ retainPositionFlag: form.base.keepPosition === "keep" ? 1 : 0,
|
|
|
+ retentionDuration: Number(form.base.retentionMinutes) || 0,
|
|
|
+ bookingDateDisplayDays: Number(form.base.bookDateDays) || 0,
|
|
|
+ maxCapacityPerSlot: Number(form.base.maxCapacityPerSlot) || 0,
|
|
|
+ reservation: form.booking.feeType === "free" ? "0" : "1",
|
|
|
+ reservationMoney: form.booking.feeType === "paid" ? Number(form.booking.bookAmount) || 0 : 0,
|
|
|
+ offUnsubscribeHours: Number(form.booking.refundAdvanceHours) || 0,
|
|
|
+ bookingNotAvailableTime: Number(form.booking.noBookMinutesBeforeClose) || 0,
|
|
|
+ normalBusinessHours: {
|
|
|
+ businessType: 1,
|
|
|
+ holidayType: businessDate,
|
|
|
+ bookingTimeType: form.normalBook.timeType === "allDay" ? 1 : 0,
|
|
|
+ startTime: form.normalBook.startTime || "",
|
|
|
+ endTime: form.normalBook.endTime || ""
|
|
|
+ },
|
|
|
+ specialBusinessHoursList: form.specialList.map((cur, i) => ({
|
|
|
+ bookingTimeType: cur.allDay === "allDay" ? 1 : 0,
|
|
|
+ businessType: 2,
|
|
|
+ holidayType: cur.name || "",
|
|
|
+ startTime: cur.startTime || "",
|
|
|
+ endTime: cur.endTime || "",
|
|
|
+ sort: i,
|
|
|
+ essentialId: cur.essentialId
|
|
|
+ }))
|
|
|
+ };
|
|
|
+ await bookingSettingsSave(params);
|
|
|
+ ElMessage.success("保存成功");
|
|
|
+ } catch (e: any) {
|
|
|
+ ElMessage.error(e?.message || "保存失败");
|
|
|
+ } finally {
|
|
|
+ saveLoading.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(async () => {
|
|
|
+ await loadHolidayOptions();
|
|
|
+ await getInfoSettings();
|
|
|
+ await fetchStoreInfoBusinessHours();
|
|
|
+ await fetchBookingBusinessHours();
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.info-management {
|
|
|
+ max-width: 100%;
|
|
|
+ padding: 16px 20px 80px;
|
|
|
+ margin: 0 auto;
|
|
|
+}
|
|
|
+.page-title {
|
|
|
+ margin-bottom: 20px;
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+.form-card {
|
|
|
+ padding: 20px 24px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ background: #ffffff;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 1px 4px rgb(0 0 0 / 6%);
|
|
|
+}
|
|
|
+.section-title {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+ .section-bar {
|
|
|
+ width: 4px;
|
|
|
+ height: 16px;
|
|
|
+ margin-right: 10px;
|
|
|
+ background: #6c8ff8;
|
|
|
+ border-radius: 2px;
|
|
|
+ }
|
|
|
+}
|
|
|
+.section-form {
|
|
|
+ :deep(.el-form-item) {
|
|
|
+ margin-bottom: 18px;
|
|
|
+ }
|
|
|
+ :deep(.el-form-item__label) {
|
|
|
+ font-weight: 400;
|
|
|
+ line-height: 20px;
|
|
|
+ color: #606266;
|
|
|
+ }
|
|
|
+}
|
|
|
+.form-input {
|
|
|
+ width: 40%;
|
|
|
+ max-width: 100%;
|
|
|
+}
|
|
|
+.time-row {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 12px;
|
|
|
+ align-items: center;
|
|
|
+ .time-tip {
|
|
|
+ margin-left: 4px;
|
|
|
+ color: #909399;
|
|
|
+ }
|
|
|
+}
|
|
|
+.special-time-row {
|
|
|
+ padding-left: 0;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ margin-left: 0;
|
|
|
+ .info-icon {
|
|
|
+ margin-left: 6px;
|
|
|
+ color: #909399;
|
|
|
+ vertical-align: middle;
|
|
|
+ }
|
|
|
+ .next-day-tag {
|
|
|
+ margin-left: 8px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+ }
|
|
|
+}
|
|
|
+.footer-actions {
|
|
|
+ position: fixed;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ left: 0;
|
|
|
+ padding: 12px 20px;
|
|
|
+ background: #ffffff;
|
|
|
+ box-shadow: 0 -1px 4px rgb(0 0 0 / 6%);
|
|
|
+ .el-button {
|
|
|
+ display: block;
|
|
|
+ width: 100%;
|
|
|
+ max-width: 720px;
|
|
|
+ margin: 0 auto;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|