From e92cd56bb56b111c1afe9922191f9897215ff6b0 Mon Sep 17 00:00:00 2001
From: puzhibing <393733352@qq.com>
Date: 星期五, 28 八月 2026 17:55:27 +0800
Subject: [PATCH] 提交数据库和服务器部署说明文档

---
 frontend-admin/src/views/BookingSettingsView.vue |  215 ++++++++++++++++++++++++++++++++++++++++-------------
 1 files changed, 160 insertions(+), 55 deletions(-)

diff --git a/frontend-admin/src/views/BookingSettingsView.vue b/frontend-admin/src/views/BookingSettingsView.vue
index 668cac2..e8cb048 100644
--- a/frontend-admin/src/views/BookingSettingsView.vue
+++ b/frontend-admin/src/views/BookingSettingsView.vue
@@ -29,7 +29,7 @@
     <p class="fields-hint">预约字段变更即时生效</p>
 
     <h3 class="section-title section-gap">可预约范围</h3>
-    <el-form label-width="120px" class="block-form">
+    <el-form label-width="140px" class="block-form">
       <el-form-item label="预约时段">
         <el-radio-group v-model="rangeMode">
           <el-radio-button value="SINGLE">单日</el-radio-button>
@@ -48,27 +48,33 @@
         />
       </el-form-item>
     </el-form>
+    <p class="fields-hint">选择多日时单日亦会生效</p>
 
     <h3 class="section-title section-gap">节假日配置</h3>
     <div class="holiday-toolbar">
-      <span class="lbl">节假日/高温假:</span>
-      <el-date-picker
-        v-model="pickDate"
-        type="date"
-        value-format="YYYY-MM-DD"
-        placeholder="选择日期"
-        clearable
-        style="width: 200px"
-      />
-      <el-button v-permission="'config:booking-settings:update'" type="primary" @click="addHoliday">添加</el-button>
-      <el-button v-permission="'config:booking-settings:update'" @click="resetHolidays">重置</el-button>
+      <el-button @click="switchMonth(-1)">上月</el-button>
+      <div class="month-label">{{ currentMonthLabel }}</div>
+      <el-button @click="switchMonth(1)">下月</el-button>
+      <el-button v-permission="'config:booking-settings:update'" @click="pickCurrentMonthWeekends">快速选择本月周末</el-button>
     </div>
-    <div class="holiday-tags">
-      <el-tag v-for="d in holidayDates" :key="d" closable class="tag-item" @close="removeHoliday(d)">
-        {{ d }}
-      </el-tag>
-      <span v-if="holidayDates.length === 0" class="empty-hint">暂无已选节假日</span>
+    <div class="holiday-calendar">
+      <div class="week-head" v-for="w in weekNames" :key="w">{{ w }}</div>
+      <div
+        v-for="cell in calendarCells"
+        :key="cell.date"
+        class="day-cell"
+        :class="{
+          'cell-out': !cell.inMonth,
+          'cell-holiday': cell.inMonth && isHoliday(cell.date),
+          'cell-workday': cell.inMonth && !isHoliday(cell.date),
+        }"
+        @click="toggleHoliday(cell)"
+      >
+        <div v-if="cell.inMonth" class="day-num">{{ cell.day }}</div>
+        <div v-if="cell.inMonth" class="day-flag">{{ isHoliday(cell.date) ? "休" : "班" }}</div>
+      </div>
     </div>
+    <p class="fields-hint">点击日期会实时保存,无需再点“保存预约策略”</p>
 
     <h3 class="section-title section-gap">车辆道闸权限</h3>
     <div class="barrier-block">
@@ -86,9 +92,10 @@
 </template>
 
 <script setup lang="ts">
-import { onMounted, ref } from "vue";
+import { computed, onMounted, ref } from "vue";
 import { ElMessage } from "element-plus";
 import http from "../api/http";
+import { normalizeFormData } from "../utils/formHelper";
 
 const LOCKED_CODES = new Set(["respondent", "visit_start_time"]);
 
@@ -148,9 +155,35 @@
 const rangeMode = ref<"SINGLE" | "MULTI">("SINGLE");
 const maxConsecutiveDays = ref(7);
 const holidayDates = ref<string[]>([]);
-const pickDate = ref<string | null>(null);
 const vehicleBarrierEnabled = ref(false);
 const extrasSaving = ref(false);
+const holidaySaving = ref(false);
+const currentMonth = ref(new Date(new Date().getFullYear(), new Date().getMonth(), 1));
+
+const weekNames = ["日", "一", "二", "三", "四", "五", "六"];
+
+const currentMonthLabel = computed(() => {
+  const y = currentMonth.value.getFullYear();
+  const m = `${currentMonth.value.getMonth() + 1}`.padStart(2, "0");
+  return `${y}-${m}`;
+});
+
+const calendarCells = computed(() => {
+  const first = new Date(currentMonth.value.getFullYear(), currentMonth.value.getMonth(), 1);
+  const start = new Date(first);
+  start.setDate(1 - first.getDay());
+  const cells: Array<{ date: string; day: number; inMonth: boolean }> = [];
+  for (let i = 0; i < 42; i++) {
+    const d = new Date(start);
+    d.setDate(start.getDate() + i);
+    cells.push({
+      date: formatLocalDate(d),
+      day: d.getDate(),
+      inMonth: d.getMonth() === currentMonth.value.getMonth(),
+    });
+  }
+  return cells;
+});
 
 async function loadExtras() {
   try {
@@ -172,33 +205,59 @@
   }
 }
 
-function addHoliday() {
-  const d = pickDate.value?.trim();
-  if (!d) {
-    ElMessage.warning("请先选择日期");
-    return;
-  }
-  if (holidayDates.value.includes(d)) {
-    ElMessage.info("该日期已在列表中");
-    return;
-  }
-  holidayDates.value = [...holidayDates.value, d].sort();
-  pickDate.value = null;
+function isHoliday(date: string) {
+  return holidayDates.value.includes(date);
 }
 
-function removeHoliday(d: string) {
-  holidayDates.value = holidayDates.value.filter((x) => x !== d);
+function switchMonth(offset: number) {
+  const d = new Date(currentMonth.value);
+  d.setMonth(d.getMonth() + offset, 1);
+  currentMonth.value = d;
 }
 
-async function resetHolidays() {
-  pickDate.value = null;
+async function toggleHoliday(cell: { date: string; inMonth: boolean }) {
+  if (!cell.inMonth) return;
+  if (holidaySaving.value) return;
+  const nextHoliday = !isHoliday(cell.date);
+  holidaySaving.value = true;
   try {
-    const { data } = await http.get("/config/booking-settings");
-    const hol = data.result?.holidayDates as string[] | undefined;
-    holidayDates.value = Array.isArray(hol) ? [...hol] : [];
+    await http.patch("/config/booking-settings/holiday-date", {
+      date: cell.date,
+      holiday: nextHoliday,
+    });
+    if (nextHoliday) {
+      holidayDates.value = [...holidayDates.value, cell.date].sort();
+    } else {
+      holidayDates.value = holidayDates.value.filter((x) => x !== cell.date);
+    }
+    ElMessage.success(nextHoliday ? "已设为休息日" : "已取消休息日");
   } catch (e: unknown) {
-    holidayDates.value = [];
-    ElMessage.error(e instanceof Error ? e.message : "重置失败");
+    ElMessage.error(e instanceof Error ? e.message : "操作失败");
+  } finally {
+    holidaySaving.value = false;
+  }
+}
+
+function formatLocalDate(date: Date) {
+  const y = date.getFullYear();
+  const m = `${date.getMonth() + 1}`.padStart(2, "0");
+  const d = `${date.getDate()}`.padStart(2, "0");
+  return `${y}-${m}-${d}`;
+}
+
+async function pickCurrentMonthWeekends() {
+  if (holidaySaving.value) return;
+  holidaySaving.value = true;
+  try {
+    await http.post("/config/booking-settings/holiday-month-weekends", {
+      month: currentMonthLabel.value,
+    });
+    await loadExtras();
+    ElMessage.success("已选择本月周末");
+  } catch (e: unknown) {
+    ElMessage.error(e instanceof Error ? e.message : "操作失败");
+  } finally {
+    holidaySaving.value = false;
   }
 }
 
@@ -212,12 +271,13 @@
   }
   extrasSaving.value = true;
   try {
-    await http.put("/config/booking-settings", {
+    const payload = normalizeFormData({
       rangeMode: rangeMode.value,
       maxConsecutiveDays: rangeMode.value === "MULTI" ? maxConsecutiveDays.value : maxConsecutiveDays.value ?? 7,
       holidayDates: holidayDates.value,
       vehicleBarrierEnabled: vehicleBarrierEnabled.value,
     });
+    await http.put("/config/booking-settings", payload);
     ElMessage.success("已保存");
     await loadExtras();
   } catch (e: unknown) {
@@ -271,28 +331,73 @@
 }
 .holiday-toolbar {
   display: flex;
-  flex-wrap: wrap;
   align-items: center;
-  gap: 10px;
+  gap: 12px;
   margin-bottom: 12px;
 }
-.lbl {
-  font-size: 14px;
-  color: #606266;
-}
-.holiday-tags {
+.month-label {
+  width: 120px;
+  height: 32px;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
   display: flex;
-  flex-wrap: wrap;
-  gap: 8px;
   align-items: center;
-  min-height: 32px;
+  justify-content: center;
+  color: #606266;
+  font-size: 14px;
 }
-.tag-item {
-  margin: 0;
+.holiday-calendar {
+  display: grid;
+  grid-template-columns: repeat(7, 1fr);
+  gap: 8px;
+  border: 1px solid #d9ecff;
+  border-radius: 10px;
+  padding: 12px;
+  max-width: 760px;
 }
-.empty-hint {
+.week-head {
+  text-align: center;
+  font-size: 14px;
+  color: #1f2937;
+}
+.day-cell {
+  min-height: 58px;
+  border-radius: 8px;
+  padding: 6px;
+  cursor: pointer;
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+}
+.cell-out {
+  cursor: default;
+  background: transparent;
+}
+.cell-holiday {
+  background: #fdecec;
+}
+.cell-workday {
+  background: #ffffff;
+}
+.day-num {
+  font-size: 22px;
+  line-height: 1;
+  color: #111827;
+}
+.day-flag {
+  align-self: flex-end;
   font-size: 13px;
-  color: #c0c4cc;
+  line-height: 18px;
+  padding: 0 4px;
+  border-radius: 4px;
+}
+.cell-holiday .day-flag {
+  color: #fff;
+  background: #ff4d4f;
+}
+.cell-workday .day-flag {
+  color: #fff;
+  background: #409eff;
 }
 .barrier-block {
   max-width: 560px;

--
Gitblit v1.7.1