530 lines
16 KiB
Vue
530 lines
16 KiB
Vue
<script setup>
|
||
// Vue 核心响应式 API
|
||
import { ref, watch, onMounted, onBeforeUnmount } from "vue";
|
||
// FullCalendar Vue 3 组件
|
||
import FullCalendar from "@fullcalendar/vue3";
|
||
// FullCalendar 插件:月视图
|
||
import dayGridPlugin from "@fullcalendar/daygrid";
|
||
// FullCalendar 插件:周视图(时间网格)
|
||
import timeGridPlugin from "@fullcalendar/timegrid";
|
||
// FullCalendar 插件:交互功能(拖拽、点击等)
|
||
import interactionPlugin from "@fullcalendar/interaction";
|
||
// FullCalendar 插件:列表视图
|
||
import listPlugin from "@fullcalendar/list";
|
||
// 国际化 hook
|
||
import { useI18n } from "vue-i18n";
|
||
// 页面标题 composable
|
||
import { usePageTitle } from "@/composables/usePageTitle";
|
||
|
||
import DatatimePickerForFullCalendar from "@/components/datatimePickerForFullCalendar.vue";
|
||
|
||
import { useToastStore } from "@/stores/toast";
|
||
|
||
const toast = useToastStore();
|
||
|
||
// 设置页面标题
|
||
usePageTitle("appname.schedule");
|
||
|
||
// 获取国际化翻译函数和当前语言
|
||
const { t, locale } = useI18n();
|
||
|
||
// FullCalendar 组件的引用,用于调用日历 API
|
||
const calendarRef = ref(null);
|
||
// 用于跟踪上次点击时间的响应式变量
|
||
const lastClickTime = ref(0);
|
||
// 用于跟踪上次点击event时间的响应式变量
|
||
const lastEventClickTime = ref(0);
|
||
|
||
// 模态框相关状态
|
||
const showModal = ref(false);
|
||
const modalTitle = ref("添加日程");
|
||
const eventData = ref({
|
||
title: "",
|
||
startDate: "",
|
||
endDate: "",
|
||
color: "#066FD1", // 默认蓝色工作事件
|
||
});
|
||
|
||
// 颜色选项
|
||
const colorOptions = ref([
|
||
{ value: "#066FD1", label: t("schedule.work"), name: t("schedule.work") },
|
||
{ value: "#09D119", label: t("schedule.duty"), name: t("schedule.duty") },
|
||
{ value: "#FF00FF", label: t("schedule.exam"), name: t("schedule.exam") },
|
||
{
|
||
value: "#FFFF00",
|
||
label: t("schedule.standby"),
|
||
name: t("schedule.standby"),
|
||
},
|
||
{
|
||
value: "#D16C13",
|
||
label: t("schedule.personal_holiday"),
|
||
name: t("schedule.personal_holiday"),
|
||
},
|
||
{
|
||
value: "#D10D21",
|
||
label: t("schedule.public_holiday"),
|
||
name: t("schedule.public_holiday"),
|
||
},
|
||
]);
|
||
|
||
// 日历配置选项
|
||
const calendarOptions = ref({
|
||
// 日历高度:占满可用空间
|
||
height: "100%",
|
||
// 内容高度自适应
|
||
contentHeight: "auto",
|
||
// 使用当前应用语言
|
||
locale: locale.value,
|
||
// 注册使用的插件
|
||
plugins: [dayGridPlugin, timeGridPlugin, interactionPlugin, listPlugin],
|
||
// 显示当前时间指示线
|
||
nowIndicator: true,
|
||
// 显示周末
|
||
weekends: true,
|
||
// 初始视图:月视图
|
||
initialView: "dayGridMonth",
|
||
// 允许选择日期/时间段
|
||
selectable: true,
|
||
// 允许拖拽调整事件
|
||
editable: true,
|
||
// 日期格中事件过多时显示"+N more"
|
||
dayMaxEvents: true,
|
||
|
||
// 日期标题可点击跳转 //不跳转
|
||
//navLinks: true,
|
||
|
||
// 一周的第一天:1 = 周一
|
||
firstDay: 1,
|
||
// 自动展开行高
|
||
expandRows: true,
|
||
// 固定头部日期
|
||
stickyHeaderDates: true,
|
||
|
||
// 日期格子挂载时的样式处理
|
||
dayCellDidMount(info) {
|
||
// 周六周日显示灰色背景
|
||
if (info.date.getDay() === 0 || info.date.getDay() === 6) {
|
||
info.el.style.backgroundColor = "#f5f5f5";
|
||
}
|
||
// 添加边框样式
|
||
info.el.style.border = "1px solid #e5e7eb";
|
||
},
|
||
|
||
// 顶部工具栏配置
|
||
headerToolbar: {
|
||
// 左侧:年份和月份导航按钮
|
||
left: "prevYear,prev,today,next,nextYear",
|
||
// 中间:标题(显示当前月份/年份)
|
||
center: "title",
|
||
// 右侧:留空(可通过 customButtons 扩展)
|
||
right: "",
|
||
},
|
||
|
||
// 自定义按钮:扩展工具栏功能
|
||
customButtons: {
|
||
// 上一年按钮
|
||
prevYear: {
|
||
text: t("schedule.previous_year"),
|
||
click() {
|
||
calendarRef.value.getApi().prevYear();
|
||
},
|
||
},
|
||
// 下一年按钮
|
||
nextYear: {
|
||
text: t("schedule.next_year"),
|
||
click() {
|
||
calendarRef.value.getApi().nextYear();
|
||
},
|
||
},
|
||
// 上一个月按钮
|
||
prev: {
|
||
text: t("schedule.previous_month"),
|
||
click() {
|
||
calendarRef.value.getApi().prev();
|
||
},
|
||
},
|
||
// 下一个月按钮
|
||
next: {
|
||
text: t("schedule.next_month"),
|
||
click() {
|
||
calendarRef.value.getApi().next();
|
||
},
|
||
},
|
||
// 今天按钮:跳转到今天
|
||
today: {
|
||
text: t("schedule.today"),
|
||
click() {
|
||
calendarRef.value.getApi().today();
|
||
},
|
||
},
|
||
// 周视图按钮:切换到周视图
|
||
week: {
|
||
text: t("schedule.week"),
|
||
click() {
|
||
calendarRef.value.getApi().changeView("timeGridWeek");
|
||
},
|
||
},
|
||
},
|
||
|
||
// 日历事件列表(目前为空,后续可接入数据源)
|
||
events: [],
|
||
|
||
// 日期点击事件处理函数
|
||
dateClick(info) {
|
||
const nowTime = new Date().getTime();
|
||
const timeDifference = nowTime - lastClickTime.value;
|
||
|
||
// 判断是否为双击(400ms 内连续点击)
|
||
if (timeDifference < 400 && timeDifference > 0) {
|
||
console.log("双击日期:", info.dateStr);
|
||
// 双击功能:快速添加事件
|
||
handleDoubleClick(info);
|
||
} else {
|
||
console.log("单击日期:", info.dateStr);
|
||
// 单击功能:显示日期详情
|
||
handleSingleClick(info);
|
||
}
|
||
|
||
// 更新上次点击时间
|
||
lastClickTime.value = nowTime;
|
||
},
|
||
|
||
//选择日期
|
||
select(info) {
|
||
if (info.end - info.start > 86400000) {
|
||
//选择了多日
|
||
console.log("选择了多日:", info);
|
||
} else {
|
||
//选择单日
|
||
console.log("选择单日:", info);
|
||
}
|
||
},
|
||
|
||
//事件event点击处理函数
|
||
eventClick(info) {
|
||
const nowTime = new Date().getTime();
|
||
const timeDifference = nowTime - lastEventClickTime.value;
|
||
|
||
// 判断是否为双击(400ms 内连续点击)
|
||
if (timeDifference < 400 && timeDifference > 0) {
|
||
console.log("双击事件:", info);
|
||
// 双击功能:快速添加事件
|
||
} else {
|
||
console.log("单击事件:", info);
|
||
// 单击功能:显示日期详情
|
||
}
|
||
// 更新上次点击时间
|
||
lastEventClickTime.value = nowTime;
|
||
},
|
||
|
||
//event拖动处理
|
||
eventDrop(info) {},
|
||
});
|
||
|
||
// 打开模态框
|
||
const openEventModal = (dateStr) => {
|
||
eventData.value = {
|
||
title: "",
|
||
startDate: dateStr,
|
||
endDate: dateStr,
|
||
color: "#066FD1",
|
||
};
|
||
|
||
showModal.value = true;
|
||
};
|
||
|
||
// 关闭模态框
|
||
const closeEventModal = () => {
|
||
showModal.value = false;
|
||
};
|
||
|
||
// 处理双击事件:打开模态框添加事件
|
||
const handleDoubleClick = (info) => {
|
||
openEventModal(info.dateStr);
|
||
};
|
||
|
||
// 处理单机事件:显示日期详情
|
||
const handleSingleClick = (info) => {
|
||
const dateEvents = calendarOptions.value.events.filter(
|
||
(event) =>
|
||
event.start === info.dateStr ||
|
||
(event.start <= info.dateStr && event.end > info.dateStr),
|
||
);
|
||
|
||
if (dateEvents.length > 0) {
|
||
//alert(`${info.dateStr} 有 ${dateEvents.length} 个事件`)
|
||
} else {
|
||
//alert(`${info.dateStr} 没有事件`)
|
||
}
|
||
};
|
||
|
||
// 保存日程事件
|
||
const saveEvent = () => {
|
||
if (!eventData.value.title.trim()) {
|
||
//alert("请输入日程内容");
|
||
toast.warning(t("schedule.event_title_required"));
|
||
return;
|
||
}
|
||
|
||
if (!eventData.value.startDate || !eventData.value.endDate) {
|
||
//alert("请选择日期");
|
||
toast.warning(t("schedule.date_required"));
|
||
return;
|
||
}
|
||
|
||
const selectedColor = colorOptions.value.find(
|
||
(color) => color.value === eventData.value.color,
|
||
);
|
||
const colorName = selectedColor ? selectedColor.name : eventData.value.color;
|
||
|
||
const newEvent = {
|
||
title: eventData.value.title.trim(),
|
||
start: eventData.value.startDate,
|
||
end: eventData.value.endDate,
|
||
allDay: true,
|
||
backgroundColor: eventData.value.color,
|
||
borderColor: eventData.value.color,
|
||
textColor: "#ffffff",
|
||
extendedProps: {
|
||
type: colorName,
|
||
description: eventData.value.title.trim(),
|
||
},
|
||
};
|
||
|
||
// 添加到日历事件列表
|
||
calendarOptions.value.events.push(newEvent);
|
||
|
||
console.log("事件添加成功:", newEvent);
|
||
toast.success(t("schedule.event_added_successfully"));
|
||
|
||
// 关闭模态框
|
||
closeEventModal();
|
||
};
|
||
|
||
// 清除日期选择
|
||
const clearDates = () => {
|
||
eventData.value.startDate = "";
|
||
eventData.value.endDate = "";
|
||
};
|
||
|
||
// 颜色选择处理
|
||
const selectColor = (colorValue) => {
|
||
eventData.value.color = colorValue;
|
||
};
|
||
|
||
// 监听语言变化,更新日历的本地化和按钮文字
|
||
watch(locale, () => {
|
||
// 更新日历语言
|
||
calendarOptions.value.locale = locale.value;
|
||
// 更新自定义按钮的文字
|
||
calendarOptions.value.customButtons.prevYear.text = t(
|
||
"schedule.previous_year",
|
||
);
|
||
calendarOptions.value.customButtons.nextYear.text = t("schedule.next_year");
|
||
calendarOptions.value.customButtons.prev.text = t("schedule.previous_month");
|
||
calendarOptions.value.customButtons.next.text = t("schedule.next_month");
|
||
calendarOptions.value.customButtons.today.text = t("schedule.today");
|
||
calendarOptions.value.customButtons.week.text = t("schedule.week");
|
||
|
||
colorOptions.value = [
|
||
{ value: "#066FD1", label: t("schedule.work"), name: t("schedule.work") },
|
||
{ value: "#09D119", label: t("schedule.duty"), name: t("schedule.duty") },
|
||
{ value: "#FF00FF", label: t("schedule.exam"), name: t("schedule.exam") },
|
||
{
|
||
value: "#FFFF00",
|
||
label: t("schedule.standby"),
|
||
name: t("schedule.standby"),
|
||
},
|
||
{
|
||
value: "#D16C13",
|
||
label: t("schedule.personal_holiday"),
|
||
name: t("schedule.personal_holiday"),
|
||
},
|
||
{
|
||
value: "#D10D21",
|
||
label: t("schedule.public_holiday"),
|
||
name: t("schedule.public_holiday"),
|
||
},
|
||
];
|
||
});
|
||
|
||
onMounted(() => {
|
||
const handleKeydown = (event) => {
|
||
// Ctrl+C 事件
|
||
if (event.ctrlKey && event.key === "c") {
|
||
event.preventDefault(); // 可选:阻止默认复制行为
|
||
console.log("Ctrl+C 被按下");
|
||
// 你的业务逻辑
|
||
}
|
||
|
||
// Ctrl+V 事件
|
||
if (event.ctrlKey && event.key === "v") {
|
||
event.preventDefault(); // 可选:阻止默认粘贴行为
|
||
console.log("Ctrl+V 被按下");
|
||
// 你的业务逻辑
|
||
}
|
||
};
|
||
|
||
document.addEventListener("keydown", handleKeydown);
|
||
|
||
// 清理事件监听器
|
||
onBeforeUnmount(() => {
|
||
document.removeEventListener("keydown", handleKeydown);
|
||
});
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<!-- 日历容器:占满视口高度减去顶部导航高度 -->
|
||
<div class="flex h-[calc(100vh-3.5rem)] w-full flex-col relative">
|
||
<!-- 事件编辑模态框 -->
|
||
<div
|
||
v-if="showModal"
|
||
class="fixed inset-0 z-50 flex items-center justify-center bg-gray-800/20"
|
||
>
|
||
<div class="modal-content bg-white rounded-lg shadow-lg w-full max-w-2xl">
|
||
<!-- 模态框头部 -->
|
||
<div
|
||
class="modal-header border-b p-4 flex justify-between items-center"
|
||
>
|
||
<h5 class="modal-title text-lg font-semibold">
|
||
{{ t("schedule.add_event") }}
|
||
</h5>
|
||
<button
|
||
@click="closeEventModal"
|
||
class="btn-close text-gray-500 hover:text-gray-700"
|
||
>
|
||
<svg
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
width="24"
|
||
height="24"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="2"
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
class="icon icon-tabler icons-tabler-outline icon-tabler-x"
|
||
>
|
||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||
<path d="M18 6l-12 12"></path>
|
||
<path d="M6 6l12 12"></path>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 模态框主体 -->
|
||
<div class="modal-body p-4">
|
||
<!-- 日期选择区域 -->
|
||
<DatatimePickerForFullCalendar
|
||
:start-date="eventData.startDate"
|
||
:end-date="eventData.endDate"
|
||
/>
|
||
|
||
<!-- 内容输入区域 -->
|
||
<div class="mb-4">
|
||
<div class="uni-easyinput input relative">
|
||
<div
|
||
class="uni-easyinput__content is-input-border border border-gray-300 rounded-md bg-white relative"
|
||
>
|
||
<input
|
||
v-model="eventData.title"
|
||
type="text"
|
||
maxlength="140"
|
||
class="uni-easyinput__content-input w-full px-3 py-2 outline-none"
|
||
:placeholder="t('schedule.event_title_placeholder')"
|
||
@keyup.enter="saveEvent"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 颜色选择区域 -->
|
||
<div class="mb-4">
|
||
<div class="color_box grid grid-cols-3 gap-2">
|
||
<div
|
||
v-for="color in colorOptions"
|
||
:key="color.value"
|
||
class="color_box_item"
|
||
>
|
||
<label
|
||
class="uni-label-pointer form-colorinput flex items-center gap-2 cursor-pointer"
|
||
@click="selectColor(color.value)"
|
||
>
|
||
<div class="uni-radio-wrapper">
|
||
<div
|
||
class="uni-radio-input flex items-center justify-center w-6 h-6 rounded-full transition-all"
|
||
:style="{
|
||
backgroundColor: color.value,
|
||
borderColor: color.value,
|
||
}"
|
||
>
|
||
<svg
|
||
v-if="eventData.color === color.value"
|
||
width="18"
|
||
height="18"
|
||
viewBox="0 0 32 32"
|
||
>
|
||
<path
|
||
d="M1.952 18.080q-0.32-0.352-0.416-0.88t0.128-0.976l0.16-0.352q0.224-0.416 0.64-0.528t0.8 0.176l6.496 4.704q0.384 0.288 0.912 0.272t0.88-0.336l17.312-14.272q0.352-0.288 0.848-0.256t0.848 0.352l-0.416-0.416q0.32 0.352 0.32 0.816t-0.32 0.816l-18.656 18.912q-0.32 0.352-0.8 0.352t-0.8-0.32l-7.936-8.064z"
|
||
fill="#ffffff"
|
||
></path>
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
<span class="text-gray-700">{{ color.label }}</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 模态框底部 -->
|
||
<div
|
||
class="modal-footer border-t p-4 flex justify-between items-center"
|
||
>
|
||
<button
|
||
@click="closeEventModal"
|
||
class="btn px-4 py-2 text-gray-700 hover:bg-gray-100 rounded-md"
|
||
>
|
||
{{ t("schedule.close") }}
|
||
</button>
|
||
<div class="flex gap-2">
|
||
<button
|
||
class="btn px-4 py-2 text-gray-700 hover:bg-gray-100 rounded-md"
|
||
disabled
|
||
>
|
||
{{ t("schedule.copy") }}
|
||
</button>
|
||
<button
|
||
class="btn px-4 py-2 text-gray-700 hover:bg-gray-100 rounded-md"
|
||
disabled
|
||
>
|
||
{{ t("schedule.paste") }}
|
||
</button>
|
||
<button
|
||
@click="saveEvent"
|
||
class="btn btn-primary px-4 py-2 bg-blue-600 text-white hover:bg-blue-700 rounded-md"
|
||
>
|
||
{{ t("schedule.add_event_button") }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 日历主体区域,带边框和阴影 -->
|
||
<div
|
||
class="flex-1 rounded-lg border border-gray-200 bg-white p-0.5 shadow dark:border-dk-muted dark:bg-dk-card"
|
||
>
|
||
<!-- 内层容器:隐藏溢出内容 -->
|
||
<div class="h-full w-full overflow-hidden rounded-md">
|
||
<!-- FullCalendar 日历组件 -->
|
||
<!-- ref="calendarRef" 用于获取组件实例调用日历 API -->
|
||
<!-- :options 绑定日历配置对象 -->
|
||
<FullCalendar ref="calendarRef" :options="calendarOptions" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|