diff --git a/backend/my_work/routers/apiCalendar.go b/backend/my_work/routers/apiCalendar.go index 59a3479..a93a141 100644 --- a/backend/my_work/routers/apiCalendar.go +++ b/backend/my_work/routers/apiCalendar.go @@ -26,14 +26,16 @@ type TabCalendar struct { // TabCalendarEvent 日历事件表 type TabCalendarEvent struct { - ID uint `gorm:"primarykey"` - CalendarID uint `gorm:"not null;index;comment:关联日历ID"` - UserID uint `gorm:"not null;comment:创建人ID"` - Title string `gorm:"size:200;not null;comment:事件标题"` - StartDate string `gorm:"size:10;not null;index;comment:开始日期 YYYY-MM-DD"` - EndDate string `gorm:"size:10;not null;index;comment:结束日期 YYYY-MM-DD"` - BgColor string `gorm:"size:50;default:#3788d9;comment:背景颜色"` - Remark string `gorm:"type:text;comment:备注"` + ID uint `gorm:"primarykey"` + CalendarID uint `gorm:"not null;index;comment:关联日历ID"` + UserID uint `gorm:"not null;comment:创建人ID"` + UsersID []uint `gorm:"type:json; null;comment:其他关联用户ID"` + Title string `gorm:"size:200;not null;comment:事件标题"` + StartDate *time.Time `gorm:"size:10;not null;index;comment:开始日期 YYYY-MM-DD"` + EndDate *time.Time `gorm:"size:10;not null;index;comment:结束日期 YYYY-MM-DD"` + IsAllDay bool `gorm:"default:true;comment:是否全日事件"` + BgColor string `gorm:"size:50;default:#3788d9;comment:背景颜色"` + Remark string `gorm:"type:text;comment:备注"` CreatedAt *time.Time `gorm:"type:datetime;autoCreateTime;comment:创建时间"` UpdatedAt *time.Time `gorm:"type:datetime;autoUpdateTime;comment:最后修改时间"` @@ -60,7 +62,7 @@ type fromCreateCalendar struct { Name string `json:"name" binding:"required"` Description string `json:"description"` Color string `json:"color"` - IsPublic bool `json:"is_public"` + Is_public bool `json:"is_public"` } type fromUpdateCalendar struct { @@ -68,7 +70,7 @@ type fromUpdateCalendar struct { Name string `json:"name" binding:"required"` Description string `json:"description"` Color string `json:"color"` - IsPublic bool `json:"is_public"` + Is_public bool `json:"is_public"` } type fromDeleteCalendar struct { @@ -76,27 +78,27 @@ type fromDeleteCalendar struct { } type fromGetCalendarEvents struct { - CalendarID uint `json:"calendar_id" binding:"required"` - Start string `json:"start" binding:"required"` - End string `json:"end" binding:"required"` + CalendarID uint `json:"calendar_id" binding:"required"` + Start *time.Time `json:"start" binding:"required"` + End *time.Time `json:"end" binding:"required"` } type fromAddCalendarEvent struct { - CalendarID uint `json:"calendar_id" binding:"required"` - Title string `json:"title" binding:"required"` - Start string `json:"start" binding:"required"` - End string `json:"end" binding:"required"` - Color string `json:"color"` - Remark string `json:"remark"` + CalendarID uint `json:"calendar_id" binding:"required"` + Title string `json:"title" binding:"required"` + Start *time.Time `json:"start" binding:"required"` + End *time.Time `json:"end" binding:"required"` + Color string `json:"color"` + Remark string `json:"remark"` } type fromUpdateCalendarEvent struct { - ID uint `json:"id" binding:"required"` - Title string `json:"title" binding:"required"` - Start string `json:"start" binding:"required"` - End string `json:"end" binding:"required"` - Color string `json:"color"` - Remark string `json:"remark"` + ID uint `json:"id" binding:"required"` + Title string `json:"title" binding:"required"` + Start *time.Time `json:"start" binding:"required"` + End *time.Time `json:"end" binding:"required"` + Color string `json:"color"` + Remark string `json:"remark"` } type fromDeleteCalendarEvent struct { @@ -122,7 +124,7 @@ func ApiCalendar(r *gin.RouterGroup) { Name: from.Name, Description: from.Description, Color: from.Color, - IsPublic: from.IsPublic, + IsPublic: from.Is_public, } if calendar.Color == "" { calendar.Color = "#3788d9" @@ -152,14 +154,11 @@ func ApiCalendar(r *gin.RouterGroup) { // 获取日历列表 r.POST("/calendar/list", func(ctx *gin.Context) { - isAuth, _, _ := AuthenticationAuthority(ctx) - if isAuth { - var calendars []TabCalendar - models.DB.Where("deleted_at IS NULL").Order("created_at DESC").Find(&calendars) - ReturnJson(ctx, "apiOK", gin.H{"list": calendars}) - } else { - ReturnJson(ctx, "userCookieError", nil) - } + + var calendars []TabCalendar + models.DB.Where("deleted_at IS NULL").Order("created_at DESC").Find(&calendars) + ReturnJson(ctx, "apiOK", gin.H{"list": calendars}) + }) // 更新日历 @@ -180,7 +179,7 @@ func ApiCalendar(r *gin.RouterGroup) { Name: from.Name, Description: from.Description, Color: from.Color, - IsPublic: from.IsPublic, + IsPublic: from.Is_public, } if newCalendar.Color == "" { newCalendar.Color = "#3788d9" @@ -349,11 +348,11 @@ func ApiCalendar(r *gin.RouterGroup) { } newEvent := TabCalendarEvent{ - Title: from.Title, + Title: from.Title, StartDate: from.Start, - EndDate: from.End, - BgColor: from.Color, - Remark: from.Remark, + EndDate: from.End, + BgColor: from.Color, + Remark: from.Remark, } if newEvent.BgColor == "" { // 获取日历颜色 diff --git a/frontend/ops_vue_js/src/api/calendar.js b/frontend/ops_vue_js/src/api/calendar.js index 26fb8fb..4d23479 100644 --- a/frontend/ops_vue_js/src/api/calendar.js +++ b/frontend/ops_vue_js/src/api/calendar.js @@ -3,41 +3,41 @@ import { api } from './index' export const calendarApi = { // 创建日历 createCalendar(data) { - return api.post('/calendar/calendar/create', { data }) + return api.post('/calendar/calendar/create', data) }, // 获取日历列表 getCalendars() { - return api.post('/calendar/calendar/list', { data: {} }) + return api.post('/calendar/calendar/list', {}) }, // 更新日历 updateCalendar(data) { - return api.post('/calendar/calendar/update', { data }) + return api.post('/calendar/calendar/update', data) }, // 删除日历 deleteCalendar(id) { - return api.post('/calendar/calendar/delete', { data: { id } }) + return api.post('/calendar/calendar/delete', { id }) }, // 获取日历事件 getEvents(data) { - return api.post('/calendar/calendar/events', { data }) + return api.post('/calendar/calendar/events', data) }, // 添加日历事件 addEvent(data) { - return api.post('/calendar/calendar/addevent', { data }) + return api.post('/calendar/calendar/addevent', data) }, // 更新日历事件 updateEvent(data) { - return api.post('/calendar/calendar/updateevent', { data }) + return api.post('/calendar/calendar/updateevent', data) }, // 删除日历事件 deleteEvent(id) { - return api.post('/calendar/calendar/deleteevent', { data: { id } }) + return api.post('/calendar/calendar/deleteevent', { id }) } } diff --git a/frontend/ops_vue_js/src/i18n/en.json b/frontend/ops_vue_js/src/i18n/en.json index ec6d7f7..2882dc7 100644 --- a/frontend/ops_vue_js/src/i18n/en.json +++ b/frontend/ops_vue_js/src/i18n/en.json @@ -627,6 +627,7 @@ "loading": "Loading...", "add_event": "Add Event", "edit_event": "Edit Event", + "view_event": "View Event", "delete_event": "Delete Event", "event_title": "Event Title", "event_title_required": "Event title is required", diff --git a/frontend/ops_vue_js/src/i18n/zh-CN.json b/frontend/ops_vue_js/src/i18n/zh-CN.json index e14f6f3..fac28c8 100644 --- a/frontend/ops_vue_js/src/i18n/zh-CN.json +++ b/frontend/ops_vue_js/src/i18n/zh-CN.json @@ -627,6 +627,7 @@ "loading": "加载中...", "add_event": "添加事件", "edit_event": "编辑事件", + "view_event": "查看事件", "delete_event": "删除事件", "event_title": "事件标题", "event_title_required": "请输入事件标题", diff --git a/frontend/ops_vue_js/src/router/index.js b/frontend/ops_vue_js/src/router/index.js index f30419b..cb69a49 100644 --- a/frontend/ops_vue_js/src/router/index.js +++ b/frontend/ops_vue_js/src/router/index.js @@ -194,9 +194,10 @@ const router = createRouter({ router.beforeEach((to) => { const userStore = useUserStore() - // 不需要登录的页面 - const publicPages = ['/', '/login', '/register', '/forgot_password', '/schedule', '/404'] - if (publicPages.includes(to.path)) return true + // 不需要登录的页面(精确匹配或前缀匹配) + const publicPages = ['/', '/login', '/register', '/forgot_password', '/schedule', '/calendars', '/404'] + const publicPrefixes = ['/calendar/'] + if (publicPages.includes(to.path) || publicPrefixes.some(p => to.path.startsWith(p))) return true // 未登录 → 跳转登录 if (!userStore.isLoggedIn) { diff --git a/frontend/ops_vue_js/src/views/calendar/CalendarDetail.vue b/frontend/ops_vue_js/src/views/calendar/CalendarDetail.vue index 0657f2c..08b4d78 100644 --- a/frontend/ops_vue_js/src/views/calendar/CalendarDetail.vue +++ b/frontend/ops_vue_js/src/views/calendar/CalendarDetail.vue @@ -12,7 +12,7 @@ import { useToastStore } from "@/stores/toast" import { useUserStore } from "@/stores/user" import { calendarApi } from "@/api/calendar" import { useDateUtils } from "@/composables/useDateUtils" -import { IconPlus, IconTrash, IconEdit, IconCalendar } from "@tabler/icons-vue" +import DatatimePickerForFullCalendar from "@/components/datatimePickerForFullCalendar.vue" const route = useRoute() const router = useRouter() @@ -37,16 +37,17 @@ const eventData = ref({ startDate: "", endDate: "", color: "#3788d9", - remark: "", isEditing: false, - isEditable: false + isEditable: false, }) const colorOptions = ref([ - { value: "#3788d9", label: t("schedule.work") }, - { value: "#06d6a0", label: t("schedule.duty") }, - { value: "#ff595e", label: t("schedule.exam") }, - { value: "#ffca3a", label: t("schedule.standby") }, + { value: "#3788d9", label: t("schedule.work"), name: t("schedule.work"), type: "work" }, + { value: "#06d6a0", label: t("schedule.duty"), name: t("schedule.duty"), type: "duty" }, + { value: "#ff595e", label: t("schedule.exam"), name: t("schedule.exam"), type: "exam" }, + { value: "#ffca3a", label: t("schedule.standby"), name: t("schedule.standby"), type: "standby" }, + { value: "#D16C13", label: t("schedule.personal_holiday"), name: t("schedule.personal_holiday"), type: "personal_holiday" }, + { value: "#D10D21", label: t("schedule.public_holiday"), name: t("schedule.public_holiday"), type: "public_holiday" }, ]) const pageData = ref({ @@ -55,91 +56,127 @@ const pageData = ref({ lastClickTimeStr: "", lastEventClickTime: 0, lastEventClickID: 0, - submitChecked: false + submitChecked: false, + lastEventsSnapshot: null, }) -function openEventModal(startDate, endDate) { +// 选中/取消选中事件 +function unseleEvent(eventID) { + const target = calendarOptions.value.events.find(item => item.id === eventID) + if (target) { + target.borderColor = "#F7F7F7" + } +} + +function unseleEventAll() { + unseleEvent(pageData.value.seleEventID) + pageData.value.seleEventID = 0 +} + +function closeEventModal() { + showModal.value = false +} + +function openEventModal(dateStr, dataEnd, id = 0, title = "", color = "#3788d9", isEditing = false, isEditable = true) { eventData.value = { - id: 0, - title: "", - startDate: startDate || "", - endDate: endDate || "", - color: calendarInfo.value.Color || "#3788d9", - remark: "", - isEditing: false, - isEditable: true + id: id, + title: title, + startDate: dateStr, + endDate: dataEnd, + color: color, + isEditing: isEditing, + isEditable: isEditable, } showModal.value = true } -function editEvent(event) { - eventData.value = { - id: event.id, - title: event.title, - startDate: event.start, - endDate: event.end || event.start, - color: event.backgroundColor, - remark: event.extendedProps?.remark || "", - isEditing: true, - isEditable: true +function editEvent(info) { + openEventModal( + info.event.startStr, + info.event.end ? info.event.endStr : info.event.startStr, + parseInt(info.event.id), + info.event.title, + info.event.backgroundColor, + true, + info.event.durationEditable, + ) +} + +function selectColor(colorValue) { + if (eventData.value.isEditable) { + eventData.value.color = colorValue } - showModal.value = true +} + +// 日期转后端格式:YYYY-MM-DD 00:00:00 +function toDatetime(dateStr) { + return dateStr ? dateStr + " 00:00:00" : "" } async function saveEvent() { if (!eventData.value.title.trim()) { - toast.error(t('calendar.event_title_required')) + pageData.value.submitChecked = true + toast.warning(t('calendar.event_title_required')) + return + } + pageData.value.submitChecked = false + + if (!eventData.value.startDate || !eventData.value.endDate) { + toast.warning(t('schedule.date_required')) return } - pageData.value.submitChecked = true + const selectedColor = colorOptions.value.find(c => c.value === eventData.value.color) + const scheduleType = selectedColor ? selectedColor.type : "work" try { let result if (eventData.value.isEditing) { result = await calendarApi.updateEvent({ id: eventData.value.id, - title: eventData.value.title, - start: eventData.value.startDate, - end: eventData.value.endDate, - color: eventData.value.color, - remark: eventData.value.remark + title: eventData.value.title.trim(), + start: toDatetime(eventData.value.startDate), + end: toDatetime( + eventData.value.startDate === eventData.value.endDate + ? eventData.value.endDate + : DateUtils.toRealEnd(eventData.value.endDate), + ), + schedule_type: scheduleType, }) } else { result = await calendarApi.addEvent({ calendar_id: calendarId.value, - title: eventData.value.title, - start: eventData.value.startDate, - end: eventData.value.endDate, - color: eventData.value.color, - remark: eventData.value.remark + title: eventData.value.title.trim(), + start: toDatetime(eventData.value.startDate), + end: toDatetime( + eventData.value.startDate === eventData.value.endDate + ? eventData.value.endDate + : DateUtils.toRealEnd(eventData.value.endDate), + ), + schedule_type: scheduleType, }) } if (result.errCode === 0) { toast.success(t('calendar.event_save_success')) - showModal.value = false + closeEventModal() getEvents() } else { toast.error(t('message.server_error')) } } catch { // 拦截器已处理 - } finally { - pageData.value.submitChecked = false } } async function deleteEvent() { - if (!confirm(t('calendar.confirm_delete_event'))) { - return - } + if (!confirm(t('calendar.confirm_delete_event'))) return try { const result = await calendarApi.deleteEvent(eventData.value.id) if (result.errCode === 0) { toast.success(t('calendar.event_delete_success')) - showModal.value = false + closeEventModal() getEvents() } else { toast.error(t('message.server_error')) @@ -152,31 +189,32 @@ async function deleteEvent() { async function getEvents() { if (!calendarRef.value) return - const calendarApi2 = calendarRef.value.getApi() - const view = calendarApi2.view - const start = DateUtils.formatDate(view.activeStart) - const end = DateUtils.formatDate(view.activeEnd) + const calApi = calendarRef.value.getApi() + const start = DateUtils.dateToStr(calendarNowShow.value.start) + const end = DateUtils.toRealEnd(calendarNowShow.value.end) try { const { errCode, data } = await calendarApi.getEvents({ calendar_id: calendarId.value, start: start, - end: end + end: end, }) if (errCode === 0) { - const events = (data.list || []).map(event => ({ - id: event.ID, - title: event.Title, - start: event.StartDate, - end: event.EndDate, - backgroundColor: event.BgColor, - borderColor: event.BgColor, - extendedProps: { - remark: event.Remark - } - })) - calendarOptions.value.events = events + calendarOptions.value.events = [] + ;(data.list || []).forEach(item => { + calendarOptions.value.events.push({ + id: item.ID, + title: item.Title, + start: item.StartDate, + end: item.StartDate === item.EndDate + ? item.EndDate + : DateUtils.toCalendarEnd(item.EndDate), + backgroundColor: item.BgColor, + borderColor: item.ID === pageData.value.seleEventID ? "#000000" : "#F7F7F7", + allDay: true, + }) + }) } } catch { // 拦截器已处理 @@ -203,18 +241,29 @@ async function fetchCalendarInfo() { } } -function unseleEvent(eventID) { - const target = calendarOptions.value.events.find(item => item.id === eventID) - if (target) { - target.borderColor = target.backgroundColor +// ─── 滚动标题工具函数 ─────────────────────────────────────────────────────── +function applyScrollToTitle(titleEl) { + titleEl.removeAttribute("data-truncated") + titleEl.style.removeProperty("--scroll-distance") + const overflow = titleEl.scrollWidth - titleEl.clientWidth + if (overflow > 0) { + titleEl.style.setProperty("--scroll-distance", `-${overflow}px`) + titleEl.setAttribute("data-truncated", "true") } } -function unseleEventAll() { - unseleEvent(pageData.value.seleEventID) - pageData.value.seleEventID = 0 +function recalcScrollTitles() { + nextTick(() => { + requestAnimationFrame(() => { + const calendarEl = calendarRef.value?.$el + if (!calendarEl) return + calendarEl.querySelectorAll(".fc-event-title").forEach(applyScrollToTitle) + }) + }) } +// ───────────────────────────────────────────────────────────────────────────── + const calendarOptions = ref({ height: "100%", contentHeight: "auto", @@ -230,52 +279,55 @@ const calendarOptions = ref({ 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", - right: "" + right: "", }, customButtons: { prevYear: { text: t("schedule.previous_year"), - click() { - calendarRef.value.getApi().prevYear() - getEvents() - } + click() { calendarRef.value.getApi().prevYear(); getEvents() }, }, nextYear: { text: t("schedule.next_year"), - click() { - calendarRef.value.getApi().nextYear() - getEvents() - } + click() { calendarRef.value.getApi().nextYear(); getEvents() }, }, prev: { text: t("schedule.previous_month"), - click() { - calendarRef.value.getApi().prev() - getEvents() - } + click() { calendarRef.value.getApi().prev(); getEvents() }, }, next: { text: t("schedule.next_month"), - click() { - calendarRef.value.getApi().next() - getEvents() - } + click() { calendarRef.value.getApi().next(); getEvents() }, }, today: { text: t("schedule.today"), - click() { - calendarRef.value.getApi().today() - getEvents() - } - } + click() { calendarRef.value.getApi().today(); getEvents() }, + }, + week: { + text: t("schedule.week"), + click() { calendarRef.value.getApi().changeView("timeGridWeek") }, + }, }, events: [], + eventDidMount(info) { + const titleEl = info.el.querySelector(".fc-event-title") + if (titleEl) { + requestAnimationFrame(() => applyScrollToTitle(titleEl)) + } + }, + datesSet(info) { calendarNowShow.value = info getEvents() @@ -284,7 +336,6 @@ const calendarOptions = ref({ dateClick(info) { const nowTime = new Date().getTime() const timeDifference = nowTime - pageData.value.lastClickTime - unseleEventAll() if (info.dateStr === pageData.value.lastClickTimeStr) { @@ -314,179 +365,278 @@ const calendarOptions = ref({ eventClick(info) { const nowTime = new Date().getTime() const timeDifference = nowTime - pageData.value.lastEventClickTime + const eventid = parseInt(info.event.id) - if (info.event.id === pageData.value.lastEventClickID) { - if (timeDifference < 400 && timeDifference > 0) { - editEvent({ - id: info.event.id, - title: info.event.title, - start: info.event.startStr?.split('T')[0] || info.event.start?.toISOString().split('T')[0], - end: info.event.endStr?.split('T')[0] || info.event.end?.toISOString().split('T')[0], - backgroundColor: info.event.backgroundColor - }) - } - } - pageData.value.lastEventClickID = info.event.id - pageData.value.lastEventClickTime = nowTime - - // 选中效果 unseleEventAll() const target = calendarOptions.value.events.find(item => String(item.id) === String(info.event.id)) if (target) { target.borderColor = "#000000" pageData.value.seleEventID = target.id } - } + + if (eventid === pageData.value.lastEventClickID) { + if (timeDifference < 400 && timeDifference > 0) { + editEvent(info) + unseleEventAll() + } + } + pageData.value.lastEventClickID = eventid + pageData.value.lastEventClickTime = nowTime + }, + + eventDrop(info) { + // 拖拽后直接更新 + const selectedColor = colorOptions.value.find(c => c.value === info.event.backgroundColor) + const scheduleType = selectedColor ? selectedColor.type : "work" + const startStr = info.event.startStr + const endStr = info.event.end ? info.event.endStr : startStr + calendarApi.updateEvent({ + id: parseInt(info.event.id), + title: info.event.title, + start: toDatetime(startStr), + end: toDatetime(startStr === endStr ? endStr : DateUtils.toRealEnd(endStr)), + schedule_type: scheduleType, + }).then(r => { + if (r.errCode !== 0) toast.error(t('message.server_error')) + else getEvents() + }) + }, }) +// 监听语言变化 +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: "#3788d9", label: t("schedule.work"), name: t("schedule.work"), type: "work" }, + { value: "#06d6a0", label: t("schedule.duty"), name: t("schedule.duty"), type: "duty" }, + { value: "#ff595e", label: t("schedule.exam"), name: t("schedule.exam"), type: "exam" }, + { value: "#ffca3a", label: t("schedule.standby"), name: t("schedule.standby"), type: "standby" }, + { value: "#D16C13", label: t("schedule.personal_holiday"), name: t("schedule.personal_holiday"), type: "personal_holiday" }, + { value: "#D10D21", label: t("schedule.public_holiday"), name: t("schedule.public_holiday"), type: "public_holiday" }, + ] +}) + +let resizeObserver = null + onMounted(() => { fetchCalendarInfo() + // 监听日历容器宽度变化 + let resizeTimer = null + resizeObserver = new ResizeObserver(() => { + clearTimeout(resizeTimer) + resizeTimer = setTimeout(() => recalcScrollTitles(), 150) + }) + if (calendarRef.value?.$el) { + resizeObserver.observe(calendarRef.value.$el) + } + onBeforeUnmount(() => { + if (resizeObserver) { + resizeObserver.disconnect() + resizeObserver = null + } + clearTimeout(resizeTimer) + }) }) + + diff --git a/frontend/ops_vue_js/src/views/calendar/CalendarList.vue b/frontend/ops_vue_js/src/views/calendar/CalendarList.vue index a0374fe..6e20c71 100644 --- a/frontend/ops_vue_js/src/views/calendar/CalendarList.vue +++ b/frontend/ops_vue_js/src/views/calendar/CalendarList.vue @@ -188,14 +188,14 @@ onMounted(fetchCalendars)