| | |
| | | <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> |
| | |
| | | /> |
| | | </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"> |
| | |
| | | </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"]); |
| | | |
| | |
| | | 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 { |
| | |
| | | } |
| | | } |
| | | |
| | | 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; |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | 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) { |
| | |
| | | } |
| | | .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; |