更新日历功能

This commit is contained in:
2026-09-24 15:14:30 +08:00
parent c5ed596008
commit bc9e32d62a
13 changed files with 1446 additions and 6 deletions
+12
View File
@@ -83,3 +83,15 @@ export function carryPendingSchedules(data) {
data
})
}
// 中国法定节假日/调休日
// 返回 { list: [{date, name, type, year, wage}], builtin_years: [..] }
// type: 0 调休上班(周末补班,显示「班」);1 法定放假(显示「休」)
// 传 start_date + end_date 查区间(日历按 42 格查询),或传 year 查整年。
export function getScheduleHolidays(params) {
return request({
url: '/backend/oa/holiday',
method: 'get',
params
})
}
@@ -176,6 +176,11 @@ const props = defineProps({
draft: {
type: Object,
default: null
},
// 新建时预填的标题(如从节假日快捷新建,预填节日名称)
prefillTitle: {
type: String,
default: ""
}
});
@@ -367,7 +372,7 @@ function initForm() {
}
} else {
editingId.value = 0;
form.title = "";
form.title = props.prefillTitle || "";
form.schedule_date = props.defaultDate || "";
form.all_day = true;
form.start_time = "";
+346 -5
View File
@@ -92,18 +92,27 @@
'other-month': !cell.inMonth,
today: cell.isToday,
selected: cell.date === selectedDate,
'has-items': cell.items.length > 0
'has-items': cell.items.length > 0,
'is-off': cell.tag === 'off',
'is-work': cell.tag === 'work'
}"
@click="selectedDate = cell.date"
@dblclick="openCreate(cell.date)"
>
<div class="cell-date">
<span class="day-num">{{ cell.day }}</span>
<span v-if="cell.tag === 'off'" class="holiday-tag is-off">休</span>
<span v-else-if="cell.tag === 'work'" class="holiday-tag is-work"
>班</span
>
<span v-if="cell.isToday" class="today-tag">今</span>
<span v-else-if="cell.day === 1" class="month-tag"
>{{ cell.month }}月</span
>
</div>
<div v-if="cell.holidayName" class="cell-holiday" :title="cell.holidayName">
{{ cell.holidayName }}
</div>
<div class="cell-items">
<div
v-for="item in cell.items.slice(0, 3)"
@@ -142,7 +151,20 @@
<div class="day-panel">
<div class="day-panel-header">
<div>
<div class="day-title">{{ selectedDateLabel }}</div>
<div class="day-title">
{{ selectedDateLabel }}
<span v-if="selectedHoliday" class="day-title-holiday">
<span
class="holiday-tag"
:class="selectedHoliday.type === 1 ? 'is-off' : 'is-work'"
>{{ selectedHoliday.type === 1 ? "休" : "班" }}</span
>
{{ selectedHoliday.type === 1 ? selectedHoliday.name : "调休上班" }}
</span>
<span v-else-if="selectedIsRestDay" class="day-title-holiday"
>周末</span
>
</div>
<div class="day-sub">
{{ selectedItems.length }} 项日程<template v-if="pendingCount > 0"
>,{{ pendingCount }} 项未完成</template
@@ -152,6 +174,14 @@
</div>
</div>
<div class="panel-actions">
<el-button
v-if="selectedHoliday && selectedHoliday.type === 1"
size="small"
:icon="Plus"
:title="`以「${selectedHoliday.name}」为标题新建日程`"
@click="openHolidayCreate"
>节日日程</el-button
>
<el-button
v-if="pendingCount > 0"
size="small"
@@ -366,6 +396,7 @@
:schedule="editingItem"
:default-date="selectedDate"
:draft="pendingDraft"
:prefill-title="prefillTitle"
@saved="reloadAll"
@draft-applied="pendingDraft = null"
/>
@@ -410,6 +441,7 @@ import {
carrySchedule,
deleteSchedule,
finishSchedule,
getScheduleHolidays,
getScheduleList,
getScheduleStats
} from "@/api/oaSchedule";
@@ -484,6 +516,120 @@ const calendarMonth = ref(now.getMonth() + 1);
const calendarItems = ref([]);
const selectedDate = ref(todayStr());
// ---------- 中国法定节假日 / 调休 ----------
// 后端返回 { 'YYYY-MM-DD': {name, type, wage} },type: 0 调休上班 / 1 法定放假。
// 普通周末不在此表内,由 isWeekend 按星期判定,避免与节假日数据重复。
const holidayMap = ref({});
// 已成功加载节假日的年份(只有真正拿到该年数据才记入),避免重复请求。
// 用 ref 而非裸 Set:翻月时若命中缓存可跳过请求,但 UI 仍需感知变化。
// 这里用普通 Set 即可——是否要发请求由 loadHolidays 判断,不参与模板渲染。
const loadedHolidayYears = new Set();
// 正在请求中的年份,防止同一年的并发重复请求(快速连点翻月时会出现)
const loadingHolidayYears = new Set();
// 观察日历当前可见月份,月份一变就确保该月所需年份的节假日已加载。
// 这样即使某次请求失败、或用户先翻到别处再翻回来,也能自动补齐,
// 不依赖"上一次翻月恰好成功"这一脆弱前提。
watch(
[calendarYear, calendarMonth],
() => {
const start = calendarRangeStart();
const end = new Date(
start.getFullYear(),
start.getMonth(),
start.getDate() + 41
);
loadHolidays(fmtDate(start), fmtDate(end));
},
{ immediate: true }
);
async function loadHolidays(start, end) {
// 42 格窗口最多横跨两个年份,取首尾年份去重即可
const years = [start.slice(0, 4), end.slice(0, 4)].filter(
(y, i, arr) => arr.indexOf(y) === i
);
// 整年拉取而不是只拉 42 格窗口:一次请求即可覆盖该年 12 个月的翻页。
const pending = years.filter(
y => !loadedHolidayYears.has(y) && !loadingHolidayYears.has(y)
);
if (pending.length === 0) {
return;
}
pending.forEach(y => loadingHolidayYears.add(y));
try {
const results = await Promise.all(
pending.map(y => getScheduleHolidays({ year: y }))
);
// 注意:即使接口返回空 list,只要 code===200 也视为该年"已加载"。
// 否则未收录的年份(后端也查不到)会被反复请求,且空年份永远无法标记。
const map = { ...holidayMap.value };
const okYears = [];
for (let i = 0; i < results.length; i++) {
const res = results[i];
if (res?.code !== 200) {
continue;
}
okYears.push(pending[i]);
for (const h of res.data?.list || []) {
map[h.date] = { name: h.name, type: h.type, wage: h.wage };
}
}
// 只要有任一年成功就落一次地图,保证 UI 拿到最新数据
if (okYears.length > 0) {
holidayMap.value = map;
okYears.forEach(y => loadedHolidayYears.add(y));
}
} catch {
// 节假日属于辅助信息,接口失败时静默降级:日历仍可用,只是不显示休/班标记
} finally {
// 无论成败都解除加载中标记:失败时下次翻月会重试,避免永久卡住不再请求
pending.forEach(y => loadingHolidayYears.delete(y));
}
}
// 是否周末(周六/周日)。节假日数据里不含普通周末,故需单独判断。
function isWeekend(date) {
const parts = String(date || "")
.slice(0, 10)
.split("-")
.map(Number);
if (parts.length !== 3 || parts.some(Number.isNaN)) {
return false;
}
const day = new Date(parts[0], parts[1] - 1, parts[2]).getDay();
return day === 0 || day === 6;
}
// 某天的日期类型标记:off(放假)/ work(调休上班)/ weekend(普通周末)/ ''
// 调休上班优先于周末——补班的周六实际是工作日。
function dayTag(date) {
const h = holidayMap.value[date];
if (h) {
return h.type === 1 ? "off" : "work";
}
return isWeekend(date) ? "weekend" : "";
}
// 某天的节日名称:调休上班日不显示名称(否则整月周末补班都挂"调休上班",过于噪音)
function holidayName(date) {
const h = holidayMap.value[date];
if (!h || h.type !== 1) {
return "";
}
return h.name || "";
}
// 选中日期的节日信息,供右侧面板展示
const selectedHoliday = computed(() => holidayMap.value[selectedDate.value] || null);
// 选中日期是否休息日(法定放假或普通周末),用于侧栏文案与配色
const selectedIsRestDay = computed(() => {
const tag = dayTag(selectedDate.value);
return tag === "off" || tag === "weekend";
});
// 日历 42 格(6 行 x 7 列,周一起始)
const calendarCells = computed(() => {
const map = calendarMap.value;
@@ -503,7 +649,10 @@ const calendarCells = computed(() => {
month: d.getMonth() + 1,
inMonth: d.getMonth() + 1 === calendarMonth.value,
isToday: date === today,
items: map[date] || []
items: map[date] || [],
// 节假日相关:tag 用于格内角标与整格底色,holidayName 用于节日名文案
tag: dayTag(date),
holidayName: holidayName(date)
});
}
return cells;
@@ -581,9 +730,13 @@ async function loadCalendarData() {
start.getMonth(),
start.getDate() + 41
);
const startStr = fmtDate(start);
const endStr = fmtDate(end);
// 节假日不在这里加载:由上方 watch([calendarYear, calendarMonth]) 统一驱动,
// 保证"翻月即加载对应月份所需年份",且首次挂载、二次进入都能自动补齐。
const res = await getScheduleList({
start_date: fmtDate(start),
end_date: fmtDate(end),
start_date: startStr,
end_date: endStr,
page: 1,
pageSize: 500,
category: category.value
@@ -684,6 +837,8 @@ watch(viewMode, mode => {
// ---------- 详情/新增/编辑/操作 ----------
const editVisible = ref(false);
const editingItem = ref(null);
// 新建时预填的标题(节假日快捷新建使用)
const prefillTitle = ref("");
const detailVisible = ref(false);
const detailItem = ref(null);
// 从通知设置页返回时恢复的草稿(含提醒设置)
@@ -691,12 +846,24 @@ const pendingDraft = ref(null);
function openCreate(date) {
editingItem.value = null;
prefillTitle.value = "";
if (date) {
selectedDate.value = date;
}
editVisible.value = true;
}
// 从节假日快捷新建:以节日名预填标题,日期取当前选中日
function openHolidayCreate() {
const h = selectedHoliday.value;
if (!h || h.type !== 1) {
return;
}
editingItem.value = null;
prefillTitle.value = h.name || "节假日";
editVisible.value = true;
}
function openEdit(item) {
editingItem.value = item;
editVisible.value = true;
@@ -1174,6 +1341,96 @@ onMounted(() => {
color: #409eff;
}
/* 节假日:休/班 角标。休=红(放假),班=灰(调休补班),与国内日历习惯一致 */
.holiday-tag {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 16px;
height: 16px;
padding: 0 3px;
border-radius: 3px;
font-size: 10px;
line-height: 1;
flex-shrink: 0;
}
.holiday-tag.is-off {
background: #fef0f0;
color: #f56c6c;
border: 1px solid #fde2e2;
}
.holiday-tag.is-work {
background: #f4f4f5;
color: #909399;
border: 1px solid #e9e9eb;
}
/* 放假日整格浅红底,一眼区分连续假期 */
.calendar-cell.is-off {
background: #fffbfb;
}
.calendar-cell.is-off:hover {
background: #fff5f5;
}
/* 调休上班日的周六日按工作日处理,弱化底色避免误判为休息 */
.calendar-cell.is-work {
background: #fff;
}
/* 今天:整格实蓝底,最醒目。放在 is-off / is-work 之后,
保证今天即使逢节假日也保留蓝色标识。
实底较深,格内文字统一转白(或白底反白)以保证对比度。 */
.calendar-cell.today {
background: #409eff;
}
.calendar-cell.today:hover {
background: #66b1ff;
}
/* 日期数字改为白底蓝字,避免在蓝底上"蓝圆叠蓝底"糊成一片 */
.calendar-cell.today .day-num {
background: #fff;
color: #409eff;
}
.calendar-cell.today .cell-holiday,
.calendar-cell.today .today-tag {
color: #fff;
}
/* 休/班角标改为白底,保持原有红/灰字色可辨识 */
.calendar-cell.today .holiday-tag.is-off {
background: #fff;
color: #f56c6c;
border-color: #fff;
}
.calendar-cell.today .holiday-tag.is-work {
background: #fff;
color: #909399;
border-color: #fff;
}
/* 格内节日名称 */
.cell-holiday {
font-size: 11px;
line-height: 16px;
margin-bottom: 2px;
color: #f56c6c;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.calendar-cell.other-month .cell-holiday {
color: #e0a0a0;
}
.cell-items {
display: flex;
flex-direction: column;
@@ -1260,6 +1517,22 @@ onMounted(() => {
font-weight: 600;
}
/* 标题内的节假日标记(休/班 角标 + 名称) */
.day-title-holiday {
display: inline-flex;
align-items: center;
gap: 4px;
margin-left: 6px;
font-size: 12px;
font-weight: 400;
color: #f56c6c;
vertical-align: middle;
}
.day-title-holiday .holiday-tag.is-work {
color: #909399;
}
.day-sub {
font-size: 12px;
color: #909399;
@@ -1381,6 +1654,11 @@ onMounted(() => {
display: none;
}
/* 窄屏下节日名与休/班角标可能挤压日期数字,仅保留角标 */
.cell-holiday {
display: none;
}
.calendar-cell {
position: relative;
}
@@ -1454,6 +1732,69 @@ html.dark & {
.month-tag {
color: var(--el-color-primary);
}
/* 节假日标记暗色适配:浅色底改为半透明色块,保证深底上仍可读 */
.holiday-tag.is-off {
background: rgba(245, 108, 108, 0.16);
color: #f89898;
border-color: rgba(245, 108, 108, 0.3);
}
.holiday-tag.is-work {
background: var(--el-fill-color);
color: var(--el-text-color-secondary);
border-color: var(--el-border-color-lighter);
}
.calendar-cell.is-off {
background: rgba(245, 108, 108, 0.07);
&:hover {
background: rgba(245, 108, 108, 0.12);
}
}
.calendar-cell.is-work {
background: var(--el-bg-color);
}
/* 今天:整格主色实底,与浅色主题保持一致 */
.calendar-cell.today {
background: var(--el-color-primary);
&:hover {
background: var(--el-color-primary-light-3);
}
.day-num {
background: #fff;
color: var(--el-color-primary);
}
.cell-holiday,
.today-tag {
color: #fff;
}
.holiday-tag.is-off {
background: #fff;
color: #f56c6c;
border-color: #fff;
}
.holiday-tag.is-work {
background: #fff;
color: #909399;
border-color: #fff;
}
}
.cell-holiday {
color: #f89898;
}
.calendar-cell.other-month .cell-holiday {
color: rgba(248, 152, 152, 0.55);
}
.day-title-holiday {
color: #f89898;
}
.day-title-holiday .holiday-tag.is-work {
color: var(--el-text-color-secondary);
}
.cell-item {
background: var(--el-fill-color-light);