<template>
|
<div class="page">
|
<h3 class="section-title">预约字段</h3>
|
<el-table :data="fields" v-loading="fieldsLoading" stripe border class="wide-table booking-fields-table">
|
<el-table-column prop="fieldName" label="字段" min-width="160" show-overflow-tooltip />
|
<el-table-column label="是否启用" width="120" align="center">
|
<template #default="{ row }">
|
<el-switch
|
v-permission="'config:booking-settings:update'"
|
:model-value="row.visible === 1"
|
:disabled="isFieldLocked(row)"
|
:class="{ 'switch-locked': isFieldLocked(row) }"
|
@change="(v: boolean) => patchField(row, 'visible', v)"
|
/>
|
</template>
|
</el-table-column>
|
<el-table-column label="是否必填" width="120" align="center">
|
<template #default="{ row }">
|
<el-switch
|
v-permission="'config:booking-settings:update'"
|
:model-value="row.requiredFlag === 1"
|
:disabled="isFieldLocked(row) || row.visible !== 1"
|
:class="{ 'switch-locked': isFieldLocked(row) }"
|
@change="(v: boolean) => patchField(row, 'required', v)"
|
/>
|
</template>
|
</el-table-column>
|
</el-table>
|
<p class="fields-hint">预约字段变更即时生效</p>
|
|
<h3 class="section-title section-gap">可预约范围</h3>
|
<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-radio-button value="MULTI">多日</el-radio-button>
|
</el-radio-group>
|
</el-form-item>
|
<el-form-item v-if="rangeMode === 'MULTI'" label="最长连续预约天数">
|
<el-input-number
|
v-model="maxConsecutiveDays"
|
:min="1"
|
:max="999"
|
:precision="0"
|
:step="1"
|
controls-position="right"
|
style="width: 200px"
|
/>
|
</el-form-item>
|
</el-form>
|
<p class="fields-hint">选择多日时单日亦会生效</p>
|
|
<h3 class="section-title section-gap">节假日配置</h3>
|
<div class="holiday-toolbar">
|
<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-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">
|
<div class="barrier-row">
|
<span class="barrier-label">车辆道闸下放权限:</span>
|
<el-switch v-permission="'config:booking-settings:update'" v-model="vehicleBarrierEnabled" />
|
</div>
|
<p class="barrier-hint">开启后道闸可识别预约车辆,自动开闸</p>
|
</div>
|
|
<div class="save-bar">
|
<el-button v-permission="'config:booking-settings:update'" type="primary" :loading="extrasSaving" @click="saveExtras">保存预约策略</el-button>
|
</div>
|
</div>
|
</template>
|
|
<script setup lang="ts">
|
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"]);
|
|
type FieldRow = {
|
id: number;
|
uuid: string;
|
fieldCode: string;
|
fieldName: string;
|
visible: number;
|
requiredFlag: number;
|
};
|
|
const fieldsLoading = ref(false);
|
const fields = ref<FieldRow[]>([]);
|
|
function isFieldLocked(row: FieldRow) {
|
return LOCKED_CODES.has(row.fieldCode);
|
}
|
|
async function loadFields() {
|
fieldsLoading.value = true;
|
try {
|
const { data } = await http.get("/config/fields");
|
fields.value = (data.result || []) as FieldRow[];
|
} catch (e: unknown) {
|
ElMessage.error(e instanceof Error ? e.message : "加载字段失败");
|
} finally {
|
fieldsLoading.value = false;
|
}
|
}
|
|
async function patchField(row: FieldRow, kind: "visible" | "required", val: boolean) {
|
const prevV = row.visible;
|
const prevR = row.requiredFlag;
|
const body: { uuid: string; visible?: number; requiredFlag?: number } = { uuid: row.uuid };
|
if (kind === "visible") {
|
body.visible = val ? 1 : 0;
|
if (!val) body.requiredFlag = 0;
|
} else {
|
body.requiredFlag = val ? 1 : 0;
|
}
|
try {
|
await http.patch(`/config/fields/${row.id}`, body);
|
if (kind === "visible") {
|
row.visible = val ? 1 : 0;
|
if (!val) row.requiredFlag = 0;
|
} else {
|
row.requiredFlag = val ? 1 : 0;
|
}
|
} catch (e: unknown) {
|
row.visible = prevV;
|
row.requiredFlag = prevR;
|
ElMessage.error(e instanceof Error ? e.message : "更新失败");
|
}
|
}
|
|
const rangeMode = ref<"SINGLE" | "MULTI">("SINGLE");
|
const maxConsecutiveDays = ref(7);
|
const holidayDates = ref<string[]>([]);
|
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 {
|
const { data } = await http.get("/config/booking-settings");
|
const r = data.result as {
|
rangeMode?: string;
|
maxConsecutiveDays?: number;
|
holidayDates?: string[];
|
vehicleBarrierEnabled?: boolean;
|
};
|
const m = (r.rangeMode || "SINGLE").toUpperCase();
|
rangeMode.value = m === "MULTI" ? "MULTI" : "SINGLE";
|
maxConsecutiveDays.value =
|
r.maxConsecutiveDays != null && r.maxConsecutiveDays >= 1 ? r.maxConsecutiveDays : 7;
|
holidayDates.value = Array.isArray(r.holidayDates) ? [...r.holidayDates] : [];
|
vehicleBarrierEnabled.value = !!r.vehicleBarrierEnabled;
|
} catch {
|
/* ignore */
|
}
|
}
|
|
function isHoliday(date: string) {
|
return holidayDates.value.includes(date);
|
}
|
|
function switchMonth(offset: number) {
|
const d = new Date(currentMonth.value);
|
d.setMonth(d.getMonth() + offset, 1);
|
currentMonth.value = d;
|
}
|
|
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 {
|
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) {
|
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;
|
}
|
}
|
|
async function saveExtras() {
|
if (rangeMode.value === "MULTI") {
|
const n = maxConsecutiveDays.value;
|
if (n == null || !Number.isInteger(n) || n < 1) {
|
ElMessage.warning("最长连续预约天数须为正整数");
|
return;
|
}
|
}
|
extrasSaving.value = true;
|
try {
|
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) {
|
ElMessage.error(e instanceof Error ? e.message : "保存失败");
|
} finally {
|
extrasSaving.value = false;
|
}
|
}
|
|
onMounted(async () => {
|
await Promise.all([loadFields(), loadExtras()]);
|
});
|
</script>
|
|
<style scoped>
|
.page {
|
padding: 0;
|
}
|
.section-title {
|
font-size: 16px;
|
font-weight: 600;
|
margin: 0 0 12px;
|
padding-left: 10px;
|
border-left: 3px solid #409eff;
|
color: #303133;
|
}
|
.section-gap {
|
margin-top: 28px;
|
}
|
.wide-table {
|
max-width: 720px;
|
}
|
.fields-hint {
|
margin: 8px 0 0;
|
font-size: 12px;
|
color: #909399;
|
line-height: 1.4;
|
}
|
:deep(.booking-fields-table .el-table__cell) {
|
padding-top: 6px;
|
padding-bottom: 6px;
|
}
|
:deep(.booking-fields-table .el-table__cell .cell) {
|
line-height: 18px;
|
}
|
:deep(.switch-locked .el-switch__core) {
|
opacity: 0.85;
|
}
|
.block-form {
|
max-width: 640px;
|
}
|
.holiday-toolbar {
|
display: flex;
|
align-items: center;
|
gap: 12px;
|
margin-bottom: 12px;
|
}
|
.month-label {
|
width: 120px;
|
height: 32px;
|
border: 1px solid #dcdfe6;
|
border-radius: 4px;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
color: #606266;
|
font-size: 14px;
|
}
|
.holiday-calendar {
|
display: grid;
|
grid-template-columns: repeat(7, 1fr);
|
gap: 8px;
|
border: 1px solid #d9ecff;
|
border-radius: 10px;
|
padding: 12px;
|
max-width: 760px;
|
}
|
.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;
|
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;
|
}
|
.barrier-row {
|
display: flex;
|
align-items: center;
|
gap: 10px;
|
}
|
.barrier-label {
|
font-size: 14px;
|
color: #606266;
|
}
|
.barrier-hint {
|
margin: 8px 0 0;
|
font-size: 12px;
|
color: #909399;
|
line-height: 1.5;
|
}
|
.save-bar {
|
margin-top: 28px;
|
}
|
</style>
|