puzhibing
2026-08-28 e92cd56bb56b111c1afe9922191f9897215ff6b0
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;