日期7788
This commit is contained in:
@@ -1,94 +1,527 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import FullCalendar from '@fullcalendar/vue3'
|
||||
import dayGridPlugin from '@fullcalendar/daygrid'
|
||||
import timeGridPlugin from '@fullcalendar/timegrid'
|
||||
import interactionPlugin from '@fullcalendar/interaction'
|
||||
import listPlugin from '@fullcalendar/list'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { usePageTitle } from '@/composables/usePageTitle'
|
||||
<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";
|
||||
|
||||
usePageTitle('appname.schedule')
|
||||
const { t, locale } = useI18n()
|
||||
import DatatimePickerForFullCalendar from "@/components/datatimePickerForFullCalendar.vue";
|
||||
|
||||
const calendarRef = ref(null)
|
||||
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',
|
||||
// 日历高度:占满可用空间
|
||||
height: "100%",
|
||||
// 内容高度自适应
|
||||
contentHeight: "auto",
|
||||
// 使用当前应用语言
|
||||
locale: locale.value,
|
||||
// 注册使用的插件
|
||||
plugins: [dayGridPlugin, timeGridPlugin, interactionPlugin, listPlugin],
|
||||
// 显示当前时间指示线
|
||||
nowIndicator: true,
|
||||
// 显示周末
|
||||
weekends: true,
|
||||
initialView: 'dayGridMonth',
|
||||
// 初始视图:月视图
|
||||
initialView: "dayGridMonth",
|
||||
// 允许选择日期/时间段
|
||||
selectable: true,
|
||||
// 允许拖拽调整事件
|
||||
editable: true,
|
||||
// 日期格中事件过多时显示"+N more"
|
||||
dayMaxEvents: true,
|
||||
navLinks: 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.backgroundColor = "#f5f5f5";
|
||||
}
|
||||
info.el.style.border = '1px solid #e5e7eb'
|
||||
// 添加边框样式
|
||||
info.el.style.border = "1px solid #e5e7eb";
|
||||
},
|
||||
|
||||
// 顶部工具栏配置
|
||||
headerToolbar: {
|
||||
left: 'prevYear,prev,today,next,nextYear',
|
||||
center: 'title',
|
||||
right: '',
|
||||
// 左侧:年份和月份导航按钮
|
||||
left: "prevYear,prev,today,next,nextYear",
|
||||
// 中间:标题(显示当前月份/年份)
|
||||
center: "title",
|
||||
// 右侧:留空(可通过 customButtons 扩展)
|
||||
right: "",
|
||||
},
|
||||
|
||||
// 自定义按钮:扩展工具栏功能
|
||||
customButtons: {
|
||||
// 上一年按钮
|
||||
prevYear: {
|
||||
text: t('schedule.previous_year'),
|
||||
click() { calendarRef.value.getApi().prevYear() },
|
||||
text: t("schedule.previous_year"),
|
||||
click() {
|
||||
calendarRef.value.getApi().prevYear();
|
||||
},
|
||||
},
|
||||
// 下一年按钮
|
||||
nextYear: {
|
||||
text: t('schedule.next_year'),
|
||||
click() { calendarRef.value.getApi().nextYear() },
|
||||
text: t("schedule.next_year"),
|
||||
click() {
|
||||
calendarRef.value.getApi().nextYear();
|
||||
},
|
||||
},
|
||||
// 上一个月按钮
|
||||
prev: {
|
||||
text: t('schedule.previous_month'),
|
||||
click() { calendarRef.value.getApi().prev() },
|
||||
text: t("schedule.previous_month"),
|
||||
click() {
|
||||
calendarRef.value.getApi().prev();
|
||||
},
|
||||
},
|
||||
// 下一个月按钮
|
||||
next: {
|
||||
text: t('schedule.next_month'),
|
||||
click() { calendarRef.value.getApi().next() },
|
||||
text: t("schedule.next_month"),
|
||||
click() {
|
||||
calendarRef.value.getApi().next();
|
||||
},
|
||||
},
|
||||
// 今天按钮:跳转到今天
|
||||
today: {
|
||||
text: t('schedule.today'),
|
||||
click() { calendarRef.value.getApi().today() },
|
||||
text: t("schedule.today"),
|
||||
click() {
|
||||
calendarRef.value.getApi().today();
|
||||
},
|
||||
},
|
||||
// 周视图按钮:切换到周视图
|
||||
week: {
|
||||
text: t('schedule.week'),
|
||||
click() { calendarRef.value.getApi().changeView('timeGridWeek') },
|
||||
text: t("schedule.week"),
|
||||
click() {
|
||||
calendarRef.value.getApi().changeView("timeGridWeek");
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
events: [
|
||||
// 日历事件列表(目前为空,后续可接入数据源)
|
||||
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')
|
||||
})
|
||||
// 更新日历语言
|
||||
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">
|
||||
<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="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>
|
||||
|
||||
Reference in New Issue
Block a user