更新日历功能
This commit is contained in:
@@ -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 = "";
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user