From cb363c93a0b670cb93e463078217416a692ed3f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E6=96=87=E5=B3=B0?= Date: Thu, 30 Apr 2026 21:44:22 +0800 Subject: [PATCH 01/21] =?UTF-8?q?Signed-off-by:=20=E5=90=B4=E6=96=87?= =?UTF-8?q?=E5=B3=B0=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/my_work/main.go | 1 + backend/my_work/routers/api.go | 1 + backend/my_work/routers/apiCalendar.go | 434 +++++++++++++++ frontend/ops_vue_js/src/api/calendar.js | 43 ++ .../ops_vue_js/src/components/AppHeader.vue | 1 + frontend/ops_vue_js/src/i18n/en.json | 39 +- frontend/ops_vue_js/src/i18n/zh-CN.json | 39 +- frontend/ops_vue_js/src/router/index.js | 10 + .../src/views/calendar/CalendarDetail.vue | 492 ++++++++++++++++++ .../src/views/calendar/CalendarList.vue | 376 +++++++++++++ 10 files changed, 1434 insertions(+), 2 deletions(-) create mode 100644 backend/my_work/routers/apiCalendar.go create mode 100644 frontend/ops_vue_js/src/api/calendar.js create mode 100644 frontend/ops_vue_js/src/views/calendar/CalendarDetail.vue create mode 100644 frontend/ops_vue_js/src/views/calendar/CalendarList.vue diff --git a/backend/my_work/main.go b/backend/my_work/main.go index a8e0393..62d3124 100644 --- a/backend/my_work/main.go +++ b/backend/my_work/main.go @@ -81,6 +81,7 @@ func main() { routers.ApiWorkOrderInit() routers.ApiWarehouseInit() routers.ApiCustomerInit() + routers.ApiCalendarInit() routers.BindsInit() //最后初始化绑定数据表 diff --git a/backend/my_work/routers/api.go b/backend/my_work/routers/api.go index 4a202df..922c63c 100644 --- a/backend/my_work/routers/api.go +++ b/backend/my_work/routers/api.go @@ -45,6 +45,7 @@ func ApiRoot(r *gin.RouterGroup) { ApiWarehouse(r.Group("/warehouse")) ApiSysAdmin(r.Group("/admin")) ApiCustomer(r.Group("/customer")) + ApiCalendar(r.Group("/calendar")) r.GET("/", func(ctx *gin.Context) { ReturnJson(ctx, "apiOK", gin.H{ "isOpsApiRoot": true, diff --git a/backend/my_work/routers/apiCalendar.go b/backend/my_work/routers/apiCalendar.go new file mode 100644 index 0000000..59a3479 --- /dev/null +++ b/backend/my_work/routers/apiCalendar.go @@ -0,0 +1,434 @@ +package routers + +import ( + "encoding/json" + "ops/models" + "time" + + "github.com/gin-gonic/gin" + "github.com/mitchellh/mapstructure" + "gorm.io/gorm" +) + +// TabCalendar 日历表 +type TabCalendar struct { + ID uint `gorm:"primarykey"` + UserID uint `gorm:"not null;comment:创建人ID"` + Name string `gorm:"size:100;not null;comment:日历名称"` + Description string `gorm:"size:500;comment:日历描述"` + Color string `gorm:"size:50;default:#3788d9;comment:日历颜色"` + IsPublic bool `gorm:"default:false;comment:是否公开"` + + CreatedAt *time.Time `gorm:"type:datetime;autoCreateTime;comment:创建时间"` + UpdatedAt *time.Time `gorm:"type:datetime;autoUpdateTime;comment:最后修改时间"` + DeletedAt gorm.DeletedAt `gorm:"index"` +} + +// 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:备注"` + + CreatedAt *time.Time `gorm:"type:datetime;autoCreateTime;comment:创建时间"` + UpdatedAt *time.Time `gorm:"type:datetime;autoUpdateTime;comment:最后修改时间"` + DeletedAt gorm.DeletedAt `gorm:"index"` +} + +// TabCalendarLog 日历操作日志表 +type TabCalendarLog struct { + ID uint `gorm:"primarykey"` + CalendarID uint `gorm:"not null;index;comment:关联日历ID"` + EventID uint `gorm:"not null;index;comment:关联事件ID(可选)"` + UserID uint `gorm:"not null;comment:操作人ID"` + ActionType string `gorm:"size:50;not null;comment:操作类型: create-创建 update-修改 delete-删除"` + OldContent string `gorm:"type:text;comment:修改前内容(JSON)"` + NewContent string `gorm:"type:text;comment:修改后内容(JSON)"` + IP string `gorm:"size:50;comment:操作IP"` + Remark string `gorm:"size:500;comment:备注/操作描述"` + + CreatedAt *time.Time `gorm:"type:datetime;autoCreateTime;comment:操作时间"` +} + +// 请求结构体 +type fromCreateCalendar struct { + Name string `json:"name" binding:"required"` + Description string `json:"description"` + Color string `json:"color"` + IsPublic bool `json:"is_public"` +} + +type fromUpdateCalendar struct { + ID uint `json:"id" binding:"required"` + Name string `json:"name" binding:"required"` + Description string `json:"description"` + Color string `json:"color"` + IsPublic bool `json:"is_public"` +} + +type fromDeleteCalendar struct { + ID uint `json:"id" binding:"required"` +} + +type fromGetCalendarEvents struct { + CalendarID uint `json:"calendar_id" binding:"required"` + Start string `json:"start" binding:"required"` + End string `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"` +} + +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"` +} + +type fromDeleteCalendarEvent struct { + ID uint `json:"id" binding:"required"` +} + +func ApiCalendarInit() { + // 初始化数据表 + models.DB.AutoMigrate(&TabCalendar{}) + models.DB.AutoMigrate(&TabCalendarEvent{}) + models.DB.AutoMigrate(&TabCalendarLog{}) +} + +func ApiCalendar(r *gin.RouterGroup) { + // 创建日历 + r.POST("/calendar/create", func(ctx *gin.Context) { + isAuth, user, data := AuthenticationAuthority(ctx) + if isAuth { + var from fromCreateCalendar + if err := mapstructure.Decode(data, &from); err == nil { + calendar := TabCalendar{ + UserID: user.ID, + Name: from.Name, + Description: from.Description, + Color: from.Color, + IsPublic: from.IsPublic, + } + if calendar.Color == "" { + calendar.Color = "#3788d9" + } + if models.DB.Create(&calendar).Error == nil { + // 记录日志 + newContent, _ := json.Marshal(calendar) + log := TabCalendarLog{ + CalendarID: calendar.ID, + UserID: user.ID, + ActionType: "create", + NewContent: string(newContent), + IP: ctx.ClientIP(), + } + models.DB.Create(&log) + ReturnJson(ctx, "apiOK", gin.H{"id": calendar.ID}) + } else { + ReturnJson(ctx, "apiErr", nil) + } + } else { + ReturnJson(ctx, "jsonErr", nil) + } + } else { + ReturnJson(ctx, "userCookieError", nil) + } + }) + + // 获取日历列表 + 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) + } + }) + + // 更新日历 + r.POST("/calendar/update", func(ctx *gin.Context) { + isAuth, user, data := AuthenticationAuthority(ctx) + if isAuth { + var from fromUpdateCalendar + if err := mapstructure.Decode(data, &from); err == nil { + oldCalendar := TabCalendar{} + if models.DB.Where("id = ?", from.ID).First(&oldCalendar).Error == nil { + // 检查权限(只有创建人可以修改) + if oldCalendar.UserID != user.ID { + ReturnJson(ctx, "permission_denied", nil) + return + } + + newCalendar := TabCalendar{ + Name: from.Name, + Description: from.Description, + Color: from.Color, + IsPublic: from.IsPublic, + } + if newCalendar.Color == "" { + newCalendar.Color = "#3788d9" + } + + if models.DB.Model(&oldCalendar).Updates(&newCalendar).Error == nil { + // 记录日志 + newContent, _ := json.Marshal(newCalendar) + oldContent, _ := json.Marshal(oldCalendar) + log := TabCalendarLog{ + CalendarID: oldCalendar.ID, + UserID: user.ID, + ActionType: "update", + OldContent: string(oldContent), + NewContent: string(newContent), + IP: ctx.ClientIP(), + } + models.DB.Create(&log) + ReturnJson(ctx, "apiOK", nil) + } else { + ReturnJson(ctx, "apiErr", nil) + } + } else { + ReturnJson(ctx, "calendar_not_find", nil) + } + } else { + ReturnJson(ctx, "jsonErr", nil) + } + } else { + ReturnJson(ctx, "userCookieError", nil) + } + }) + + // 删除日历 + r.POST("/calendar/delete", func(ctx *gin.Context) { + isAuth, user, data := AuthenticationAuthority(ctx) + if isAuth { + var from fromDeleteCalendar + if err := mapstructure.Decode(data, &from); err == nil { + oldCalendar := TabCalendar{} + if models.DB.Where("id = ?", from.ID).First(&oldCalendar).Error == nil { + // 检查权限(只有创建人可以删除) + if oldCalendar.UserID != user.ID { + ReturnJson(ctx, "permission_denied", nil) + return + } + + // 软删除日历 + if models.DB.Delete(&oldCalendar).Error == nil { + // 记录日志 + oldContent, _ := json.Marshal(oldCalendar) + log := TabCalendarLog{ + CalendarID: oldCalendar.ID, + UserID: user.ID, + ActionType: "delete", + OldContent: string(oldContent), + IP: ctx.ClientIP(), + } + models.DB.Create(&log) + ReturnJson(ctx, "apiOK", nil) + } else { + ReturnJson(ctx, "apiErr", nil) + } + } else { + ReturnJson(ctx, "calendar_not_find", nil) + } + } else { + ReturnJson(ctx, "jsonErr", nil) + } + } else { + ReturnJson(ctx, "userCookieError", nil) + } + }) + + // 获取日历事件 + r.POST("/calendar/events", func(ctx *gin.Context) { + isAuth, _, data := AuthenticationAuthority(ctx) + if isAuth { + var from fromGetCalendarEvents + if err := mapstructure.Decode(data, &from); err == nil { + var events []TabCalendarEvent + models.DB.Where("calendar_id = ? AND start_date <= ? AND end_date >= ? AND deleted_at IS NULL", + from.CalendarID, from.End, from.Start).Find(&events) + + // 为事件添加编辑权限标识 + var relist []map[string]interface{} + for _, event := range events { + data, _ := json.Marshal(event) + var temp map[string]interface{} + json.Unmarshal(data, &temp) + // 这里可以根据需要添加 edit 字段 + relist = append(relist, temp) + } + + ReturnJson(ctx, "apiOK", gin.H{"list": relist}) + } else { + ReturnJson(ctx, "jsonErr", nil) + } + } else { + ReturnJson(ctx, "userCookieError", nil) + } + }) + + // 添加日历事件 + r.POST("/calendar/addevent", func(ctx *gin.Context) { + isAuth, user, data := AuthenticationAuthority(ctx) + if isAuth { + var from fromAddCalendarEvent + if err := mapstructure.Decode(data, &from); err == nil { + // 检查日历是否存在 + var calendar TabCalendar + if models.DB.Where("id = ? AND deleted_at IS NULL", from.CalendarID).First(&calendar).Error != nil { + ReturnJson(ctx, "calendar_not_find", nil) + return + } + + event := TabCalendarEvent{ + CalendarID: from.CalendarID, + UserID: user.ID, + Title: from.Title, + StartDate: from.Start, + EndDate: from.End, + BgColor: from.Color, + Remark: from.Remark, + } + if event.BgColor == "" { + event.BgColor = calendar.Color + } + + if models.DB.Create(&event).Error == nil { + // 记录日志 + newContent, _ := json.Marshal(event) + log := TabCalendarLog{ + CalendarID: event.CalendarID, + EventID: event.ID, + UserID: user.ID, + ActionType: "create_event", + NewContent: string(newContent), + IP: ctx.ClientIP(), + } + models.DB.Create(&log) + ReturnJson(ctx, "apiOK", gin.H{"id": event.ID}) + } else { + ReturnJson(ctx, "apiErr", nil) + } + } else { + ReturnJson(ctx, "jsonErr", nil) + } + } else { + ReturnJson(ctx, "userCookieError", nil) + } + }) + + // 更新日历事件 + r.POST("/calendar/updateevent", func(ctx *gin.Context) { + isAuth, user, data := AuthenticationAuthority(ctx) + if isAuth { + var from fromUpdateCalendarEvent + if err := mapstructure.Decode(data, &from); err == nil { + oldEvent := TabCalendarEvent{} + if models.DB.Where("id = ?", from.ID).First(&oldEvent).Error == nil { + // 检查权限(只有创建人可以修改) + if oldEvent.UserID != user.ID { + ReturnJson(ctx, "permission_denied", nil) + return + } + + newEvent := TabCalendarEvent{ + Title: from.Title, + StartDate: from.Start, + EndDate: from.End, + BgColor: from.Color, + Remark: from.Remark, + } + if newEvent.BgColor == "" { + // 获取日历颜色 + var calendar TabCalendar + models.DB.Where("id = ?", oldEvent.CalendarID).First(&calendar) + newEvent.BgColor = calendar.Color + } + + if models.DB.Model(&oldEvent).Updates(&newEvent).Error == nil { + // 记录日志 + newContent, _ := json.Marshal(newEvent) + oldContent, _ := json.Marshal(oldEvent) + log := TabCalendarLog{ + CalendarID: oldEvent.CalendarID, + EventID: oldEvent.ID, + UserID: user.ID, + ActionType: "update_event", + OldContent: string(oldContent), + NewContent: string(newContent), + IP: ctx.ClientIP(), + } + models.DB.Create(&log) + ReturnJson(ctx, "apiOK", nil) + } else { + ReturnJson(ctx, "apiErr", nil) + } + } else { + ReturnJson(ctx, "event_not_find", nil) + } + } else { + ReturnJson(ctx, "jsonErr", nil) + } + } else { + ReturnJson(ctx, "userCookieError", nil) + } + }) + + // 删除日历事件 + r.POST("/calendar/deleteevent", func(ctx *gin.Context) { + isAuth, user, data := AuthenticationAuthority(ctx) + if isAuth { + var from fromDeleteCalendarEvent + if err := mapstructure.Decode(data, &from); err == nil { + oldEvent := TabCalendarEvent{} + if models.DB.Where("id = ?", from.ID).First(&oldEvent).Error == nil { + // 检查权限(只有创建人可以删除) + if oldEvent.UserID != user.ID { + ReturnJson(ctx, "permission_denied", nil) + return + } + + if models.DB.Delete(&oldEvent).Error == nil { + // 记录日志 + oldContent, _ := json.Marshal(oldEvent) + log := TabCalendarLog{ + CalendarID: oldEvent.CalendarID, + EventID: oldEvent.ID, + UserID: user.ID, + ActionType: "delete_event", + OldContent: string(oldContent), + IP: ctx.ClientIP(), + } + models.DB.Create(&log) + ReturnJson(ctx, "apiOK", nil) + } else { + ReturnJson(ctx, "apiErr", nil) + } + } else { + ReturnJson(ctx, "event_not_find", nil) + } + } else { + ReturnJson(ctx, "jsonErr", nil) + } + } else { + ReturnJson(ctx, "userCookieError", nil) + } + }) +} diff --git a/frontend/ops_vue_js/src/api/calendar.js b/frontend/ops_vue_js/src/api/calendar.js new file mode 100644 index 0000000..26fb8fb --- /dev/null +++ b/frontend/ops_vue_js/src/api/calendar.js @@ -0,0 +1,43 @@ +import { api } from './index' + +export const calendarApi = { + // 创建日历 + createCalendar(data) { + return api.post('/calendar/calendar/create', { data }) + }, + + // 获取日历列表 + getCalendars() { + return api.post('/calendar/calendar/list', { data: {} }) + }, + + // 更新日历 + updateCalendar(data) { + return api.post('/calendar/calendar/update', { data }) + }, + + // 删除日历 + deleteCalendar(id) { + return api.post('/calendar/calendar/delete', { data: { id } }) + }, + + // 获取日历事件 + getEvents(data) { + return api.post('/calendar/calendar/events', { data }) + }, + + // 添加日历事件 + addEvent(data) { + return api.post('/calendar/calendar/addevent', { data }) + }, + + // 更新日历事件 + updateEvent(data) { + return api.post('/calendar/calendar/updateevent', { data }) + }, + + // 删除日历事件 + deleteEvent(id) { + return api.post('/calendar/calendar/deleteevent', { data: { id } }) + } +} diff --git a/frontend/ops_vue_js/src/components/AppHeader.vue b/frontend/ops_vue_js/src/components/AppHeader.vue index 504a485..e1d632c 100644 --- a/frontend/ops_vue_js/src/components/AppHeader.vue +++ b/frontend/ops_vue_js/src/components/AppHeader.vue @@ -54,6 +54,7 @@ const normalClass = "rounded-md px-3 py-2 text-sm font-medium text-gray-600 tran const navItems = computed(() => [ { label: t("appname.home"), to: "/" }, { label: t("appname.schedule"), to: "/schedule" }, + { label: t("appname.calendar"), to: "/calendars" }, { label: t("appname.purchase"), to: "/purchase" }, { label: t("appname.work_order"), to: "/work_order" }, { label: t("appname.warehouse"), to: "/warehouse" }, diff --git a/frontend/ops_vue_js/src/i18n/en.json b/frontend/ops_vue_js/src/i18n/en.json index ef99ab5..ec6d7f7 100644 --- a/frontend/ops_vue_js/src/i18n/en.json +++ b/frontend/ops_vue_js/src/i18n/en.json @@ -33,7 +33,8 @@ "purchase": "Purchase", "warehouse": "Warehouse", "warehouse_items": "Items Overview", - "work_order": "Work Order" + "work_order": "Work Order", + "calendar": "Calendar" }, "tagadder": { "not_fund_item": "No matching items found", @@ -601,5 +602,41 @@ "created_by": "Created By", "not_found": "Customer not found", "related_customers": "Related Customers" + }, + "calendar": { + "calendars": "Calendar List", + "calendar_detail": "Calendar Detail", + "create_calendar": "Create Calendar", + "edit_calendar": "Edit Calendar", + "delete_calendar": "Delete Calendar", + "calendar_not_found": "Calendar not found", + "no_calendars": "No calendars yet", + "name": "Name", + "name_required": "Calendar name is required", + "name_placeholder": "Enter calendar name", + "description": "Description", + "description_placeholder": "Enter calendar description", + "color": "Color", + "is_public": "Public Calendar", + "public": "Public", + "private": "Private", + "create_success": "Calendar created successfully", + "update_success": "Calendar updated successfully", + "delete_success": "Calendar deleted successfully", + "confirm_delete": "Are you sure you want to delete this calendar?", + "loading": "Loading...", + "add_event": "Add Event", + "edit_event": "Edit Event", + "delete_event": "Delete Event", + "event_title": "Event Title", + "event_title_required": "Event title is required", + "event_title_placeholder": "Enter event title", + "start_date": "Start Date", + "end_date": "End Date", + "remark": "Remark", + "remark_placeholder": "Enter remark", + "event_save_success": "Event saved successfully", + "event_delete_success": "Event deleted successfully", + "confirm_delete_event": "Are you sure you want to delete this event?" } } diff --git a/frontend/ops_vue_js/src/i18n/zh-CN.json b/frontend/ops_vue_js/src/i18n/zh-CN.json index 4e27e17..e14f6f3 100644 --- a/frontend/ops_vue_js/src/i18n/zh-CN.json +++ b/frontend/ops_vue_js/src/i18n/zh-CN.json @@ -33,7 +33,8 @@ "purchase": "采购", "warehouse": "仓库", "warehouse_items": "物品总览", - "work_order": "工单" + "work_order": "工单", + "calendar": "日历" }, "tagadder": { "not_fund_item": "没有找到匹配项", @@ -601,5 +602,41 @@ "created_by": "创建者", "not_found": "客户不存在", "related_customers": "关联客户" + }, + "calendar": { + "calendars": "日历列表", + "calendar_detail": "日历详情", + "create_calendar": "新建日历", + "edit_calendar": "编辑日历", + "delete_calendar": "删除日历", + "calendar_not_found": "日历未找到", + "no_calendars": "暂无日历", + "name": "名称", + "name_required": "请输入日历名称", + "name_placeholder": "请输入日历名称", + "description": "描述", + "description_placeholder": "请输入日历描述", + "color": "颜色", + "is_public": "公开日历", + "public": "公开", + "private": "私有", + "create_success": "创建成功", + "update_success": "更新成功", + "delete_success": "删除成功", + "confirm_delete": "确定要删除此日历吗?", + "loading": "加载中...", + "add_event": "添加事件", + "edit_event": "编辑事件", + "delete_event": "删除事件", + "event_title": "事件标题", + "event_title_required": "请输入事件标题", + "event_title_placeholder": "请输入事件标题", + "start_date": "开始日期", + "end_date": "结束日期", + "remark": "备注", + "remark_placeholder": "请输入备注", + "event_save_success": "事件保存成功", + "event_delete_success": "事件删除成功", + "confirm_delete_event": "确定要删除此事件吗?" } } diff --git a/frontend/ops_vue_js/src/router/index.js b/frontend/ops_vue_js/src/router/index.js index 52c21f2..f30419b 100644 --- a/frontend/ops_vue_js/src/router/index.js +++ b/frontend/ops_vue_js/src/router/index.js @@ -129,6 +129,16 @@ const router = createRouter({ name: 'customer-edit', component: () => import('@/views/customer/CustomerFormPage.vue'), }, + { + path: 'calendars', + name: 'calendars', + component: () => import('@/views/calendar/CalendarList.vue'), + }, + { + path: 'calendar/:id', + name: 'calendar-detail', + component: () => import('@/views/calendar/CalendarDetail.vue'), + }, ], }, diff --git a/frontend/ops_vue_js/src/views/calendar/CalendarDetail.vue b/frontend/ops_vue_js/src/views/calendar/CalendarDetail.vue new file mode 100644 index 0000000..0657f2c --- /dev/null +++ b/frontend/ops_vue_js/src/views/calendar/CalendarDetail.vue @@ -0,0 +1,492 @@ + + + diff --git a/frontend/ops_vue_js/src/views/calendar/CalendarList.vue b/frontend/ops_vue_js/src/views/calendar/CalendarList.vue new file mode 100644 index 0000000..a0374fe --- /dev/null +++ b/frontend/ops_vue_js/src/views/calendar/CalendarList.vue @@ -0,0 +1,376 @@ + + + From 49785004aaa149134c7485a6d038c20ad77ac1bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E6=96=87=E5=B3=B0?= Date: Wed, 6 May 2026 15:47:26 +0800 Subject: [PATCH 02/21] up --- backend/my_work/routers/apiCalendar.go | 77 ++- frontend/ops_vue_js/src/api/calendar.js | 16 +- frontend/ops_vue_js/src/i18n/en.json | 1 + frontend/ops_vue_js/src/i18n/zh-CN.json | 1 + frontend/ops_vue_js/src/router/index.js | 7 +- .../src/views/calendar/CalendarDetail.vue | 618 +++++++++++------- .../src/views/calendar/CalendarList.vue | 4 +- 7 files changed, 438 insertions(+), 286 deletions(-) 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)
+ + +
+ {{ t('calendar.is_public_event') }} + +
From 7369fe4117b0d15fc6b0ca0995dc7563b468fb9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E6=96=87=E5=B3=B0?= Date: Wed, 6 May 2026 17:03:57 +0800 Subject: [PATCH 04/21] up --- backend/my_work/routers/apiCalendar.go | 244 ++++++++++++++----------- 1 file changed, 137 insertions(+), 107 deletions(-) diff --git a/backend/my_work/routers/apiCalendar.go b/backend/my_work/routers/apiCalendar.go index 746de6c..c26fbc1 100644 --- a/backend/my_work/routers/apiCalendar.go +++ b/backend/my_work/routers/apiCalendar.go @@ -27,17 +27,17 @@ 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"` - 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:背景颜色"` - IsPublic bool `gorm:"default:false;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:背景颜色"` + IsPublic bool `gorm:"default:false;comment:是否为公共日程"` + Remark string `gorm:"type:text;comment:备注"` CreatedAt *time.Time `gorm:"type:datetime;autoCreateTime;comment:创建时间"` UpdatedAt *time.Time `gorm:"type:datetime;autoUpdateTime;comment:最后修改时间"` @@ -86,23 +86,25 @@ type fromGetCalendarEvents struct { } type fromAddCalendarEvent struct { - 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"` - Is_public bool `json:"is_public"` - Remark string `json:"remark"` + 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"` + ScheduleType string `json:"schedule_type"` + Is_public bool `json:"is_public"` + Remark string `json:"remark"` } type fromUpdateCalendarEvent struct { - 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"` - Is_public bool `json:"is_public"` - Remark string `json:"remark"` + 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"` + ScheduleType string `json:"schedule_type"` + Is_public bool `json:"is_public"` + Remark string `json:"remark"` } type fromDeleteCalendarEvent struct { @@ -352,47 +354,61 @@ func ApiCalendar(r *gin.RouterGroup) { r.POST("/calendar/addevent", func(ctx *gin.Context) { isAuth, user, data := AuthenticationAuthority(ctx) if isAuth { - var from fromAddCalendarEvent - if err := mapstructure.Decode(data, &from); err == nil { - // 检查日历是否存在 - var calendar TabCalendar - if models.DB.Where("id = ? AND deleted_at IS NULL", from.CalendarID).First(&calendar).Error != nil { - ReturnJson(ctx, "calendar_not_find", nil) - return - } - - event := TabCalendarEvent{ - CalendarID: from.CalendarID, - UserID: user.ID, - Title: from.Title, - StartDate: from.Start, - EndDate: from.End, - BgColor: from.Color, - IsPublic: from.Is_public, - Remark: from.Remark, - } - if event.BgColor == "" { - event.BgColor = calendar.Color - } - - if models.DB.Create(&event).Error == nil { - // 记录日志 - newContent, _ := json.Marshal(event) - log := TabCalendarLog{ - CalendarID: event.CalendarID, - EventID: event.ID, - UserID: user.ID, - ActionType: "create_event", - NewContent: string(newContent), - IP: ctx.ClientIP(), - } - models.DB.Create(&log) - ReturnJson(ctx, "apiOK", gin.H{"id": event.ID}) - } else { - ReturnJson(ctx, "apiErr", nil) - } - } else { + // 先检查必需字段 + calendarIDRaw, ok := data["calendar_id"].(float64) + if !ok || calendarIDRaw == 0 { ReturnJson(ctx, "jsonErr", nil) + return + } + calendarID := uint(calendarIDRaw) + + // 检查日历是否存在 + var calendar TabCalendar + if models.DB.Where("id = ? AND deleted_at IS NULL", calendarID).First(&calendar).Error != nil { + ReturnJson(ctx, "calendar_not_find", nil) + return + } + + // 解析日期 + startStr, _ := data["start"].(string) + endStr, _ := data["end"].(string) + title, _ := data["title"].(string) + color, _ := data["color"].(string) + remark, _ := data["remark"].(string) + isPublic, _ := data["is_public"].(bool) + + startDate, _ := time.Parse("2006-01-02 15:04:05", startStr) + endDate, _ := time.Parse("2006-01-02 15:04:05", endStr) + + event := TabCalendarEvent{ + CalendarID: calendarID, + UserID: user.ID, + Title: title, + StartDate: &startDate, + EndDate: &endDate, + BgColor: color, + IsPublic: isPublic, + Remark: remark, + } + if event.BgColor == "" { + event.BgColor = calendar.Color + } + + if models.DB.Create(&event).Error == nil { + // 记录日志 + newContent, _ := json.Marshal(event) + log := TabCalendarLog{ + CalendarID: event.CalendarID, + EventID: event.ID, + UserID: user.ID, + ActionType: "create_event", + NewContent: string(newContent), + IP: ctx.ClientIP(), + } + models.DB.Create(&log) + ReturnJson(ctx, "apiOK", gin.H{"id": event.ID}) + } else { + ReturnJson(ctx, "apiErr", nil) } } else { ReturnJson(ctx, "userCookieError", nil) @@ -403,54 +419,68 @@ func ApiCalendar(r *gin.RouterGroup) { r.POST("/calendar/updateevent", func(ctx *gin.Context) { isAuth, user, data := AuthenticationAuthority(ctx) if isAuth { - var from fromUpdateCalendarEvent - if err := mapstructure.Decode(data, &from); err == nil { - oldEvent := TabCalendarEvent{} - if models.DB.Where("id = ?", from.ID).First(&oldEvent).Error == nil { - // 检查权限(只有创建人可以修改) - if oldEvent.UserID != user.ID { - ReturnJson(ctx, "permission_denied", nil) - return - } + // 先检查必需字段 + idRaw, ok := data["id"].(float64) + if !ok || idRaw == 0 { + ReturnJson(ctx, "jsonErr", nil) + return + } + eventID := uint(idRaw) - newEvent := TabCalendarEvent{ - Title: from.Title, - StartDate: from.Start, - EndDate: from.End, - BgColor: from.Color, - IsPublic: from.Is_public, - Remark: from.Remark, - } - if newEvent.BgColor == "" { - // 获取日历颜色 - var calendar TabCalendar - models.DB.Where("id = ?", oldEvent.CalendarID).First(&calendar) - newEvent.BgColor = calendar.Color - } + oldEvent := TabCalendarEvent{} + if models.DB.Where("id = ?", eventID).First(&oldEvent).Error == nil { + // 检查权限(只有创建人可以修改) + if oldEvent.UserID != user.ID { + ReturnJson(ctx, "permission_denied", nil) + return + } - if models.DB.Model(&oldEvent).Updates(&newEvent).Error == nil { - // 记录日志 - newContent, _ := json.Marshal(newEvent) - oldContent, _ := json.Marshal(oldEvent) - log := TabCalendarLog{ - CalendarID: oldEvent.CalendarID, - EventID: oldEvent.ID, - UserID: user.ID, - ActionType: "update_event", - OldContent: string(oldContent), - NewContent: string(newContent), - IP: ctx.ClientIP(), - } - models.DB.Create(&log) - ReturnJson(ctx, "apiOK", nil) - } else { - ReturnJson(ctx, "apiErr", nil) + // 解析字段 + startStr, _ := data["start"].(string) + endStr, _ := data["end"].(string) + title, _ := data["title"].(string) + color, _ := data["color"].(string) + remark, _ := data["remark"].(string) + isPublic, _ := data["is_public"].(bool) + + startDate, _ := time.Parse("2006-01-02 15:04:05", startStr) + endDate, _ := time.Parse("2006-01-02 15:04:05", endStr) + + newEvent := TabCalendarEvent{ + Title: title, + StartDate: &startDate, + EndDate: &endDate, + BgColor: color, + IsPublic: isPublic, + Remark: remark, + } + if newEvent.BgColor == "" { + // 获取日历颜色 + var calendar TabCalendar + models.DB.Where("id = ?", oldEvent.CalendarID).First(&calendar) + newEvent.BgColor = calendar.Color + } + + if models.DB.Model(&oldEvent).Updates(&newEvent).Error == nil { + // 记录日志 + newContent, _ := json.Marshal(newEvent) + oldContent, _ := json.Marshal(oldEvent) + log := TabCalendarLog{ + CalendarID: oldEvent.CalendarID, + EventID: oldEvent.ID, + UserID: user.ID, + ActionType: "update_event", + OldContent: string(oldContent), + NewContent: string(newContent), + IP: ctx.ClientIP(), } + models.DB.Create(&log) + ReturnJson(ctx, "apiOK", nil) } else { - ReturnJson(ctx, "event_not_find", nil) + ReturnJson(ctx, "apiErr", nil) } } else { - ReturnJson(ctx, "jsonErr", nil) + ReturnJson(ctx, "event_not_find", nil) } } else { ReturnJson(ctx, "userCookieError", nil) From fcc6acbcee5ea27a868ede82f528f7991025bf82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E6=96=87=E5=B3=B0?= Date: Wed, 6 May 2026 18:55:17 +0800 Subject: [PATCH 05/21] up --- .workbuddy/memory/2026-05-06.md | 32 ++++++++++ backend/my_work/routers/apiCalendar.go | 60 +++++++++++-------- frontend/ops_vue_js/src/i18n/en.json | 1 + frontend/ops_vue_js/src/i18n/zh-CN.json | 1 + .../src/views/calendar/CalendarDetail.vue | 31 +++++++--- 5 files changed, 91 insertions(+), 34 deletions(-) create mode 100644 .workbuddy/memory/2026-05-06.md diff --git a/.workbuddy/memory/2026-05-06.md b/.workbuddy/memory/2026-05-06.md new file mode 100644 index 0000000..3487e3b --- /dev/null +++ b/.workbuddy/memory/2026-05-06.md @@ -0,0 +1,32 @@ +# 2026-05-06 日志 + +## 日历事件日程类型功能 + +### 修改内容 +- **后端** `routers/apiCalendar.go`: + - `TabCalendarEvent` 结构体新增 `ScheduleType string` 字段(默认值 work) + - `addevent`/`updateevent` 接口解析并保存 `schedule_type` 参数 + +- **前端** `CalendarDetail.vue`: + - `eventData` 新增 `scheduleType` 字段 + - `openEventModal`/`editEvent` 传递 `scheduleType` + - `selectColor` 函数联动更新 `scheduleType` + - `saveEvent`/`eventDrop` 提交 `schedule_type` + - `getEvents` 返回数据附加 `extendedProps.scheduleType` + - 模态框显示日程类型标签 + +- **i18n**: + - `zh-CN.json`: 新增 `event_type: "日程类型"` + - `en.json`: 新增 `event_type: "Event Type"` + +### 日程类型选项 +- work - 工作 +- duty - 值班 +- exam - 考试 +- standby - 备用 +- personal_holiday - 个人假期 +- public_holiday - 公众假期 + +### 注意事项 +- GORM AutoMigrate 会自动添加新字段 +- 前端颜色选择与日程类型联动 diff --git a/backend/my_work/routers/apiCalendar.go b/backend/my_work/routers/apiCalendar.go index c26fbc1..e87163b 100644 --- a/backend/my_work/routers/apiCalendar.go +++ b/backend/my_work/routers/apiCalendar.go @@ -27,17 +27,17 @@ 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"` - //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:背景颜色"` - IsPublic bool `gorm:"default:false;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"` + 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:是否全日事件"` + ScheduleType string `gorm:"size:50;default:work;comment:日程类型: work-工作 duty-值班 exam-考试 standby-待命 personal_holiday-调休 personal_holiday-公假"` + BgColor string `gorm:"size:50;default:#3788d9;comment:背景颜色"` + IsPublic bool `gorm:"default:false;comment:是否为公共日程"` + Remark string `gorm:"type:text;comment:备注"` CreatedAt *time.Time `gorm:"type:datetime;autoCreateTime;comment:创建时间"` UpdatedAt *time.Time `gorm:"type:datetime;autoUpdateTime;comment:最后修改时间"` @@ -376,19 +376,24 @@ func ApiCalendar(r *gin.RouterGroup) { color, _ := data["color"].(string) remark, _ := data["remark"].(string) isPublic, _ := data["is_public"].(bool) + scheduleType, _ := data["schedule_type"].(string) + if scheduleType == "" { + scheduleType = "work" + } startDate, _ := time.Parse("2006-01-02 15:04:05", startStr) endDate, _ := time.Parse("2006-01-02 15:04:05", endStr) event := TabCalendarEvent{ - CalendarID: calendarID, - UserID: user.ID, - Title: title, - StartDate: &startDate, - EndDate: &endDate, - BgColor: color, - IsPublic: isPublic, - Remark: remark, + CalendarID: calendarID, + UserID: user.ID, + Title: title, + StartDate: &startDate, + EndDate: &endDate, + ScheduleType: scheduleType, + BgColor: color, + IsPublic: isPublic, + Remark: remark, } if event.BgColor == "" { event.BgColor = calendar.Color @@ -442,17 +447,22 @@ func ApiCalendar(r *gin.RouterGroup) { color, _ := data["color"].(string) remark, _ := data["remark"].(string) isPublic, _ := data["is_public"].(bool) + scheduleType, _ := data["schedule_type"].(string) + if scheduleType == "" { + scheduleType = "work" + } startDate, _ := time.Parse("2006-01-02 15:04:05", startStr) endDate, _ := time.Parse("2006-01-02 15:04:05", endStr) newEvent := TabCalendarEvent{ - Title: title, - StartDate: &startDate, - EndDate: &endDate, - BgColor: color, - IsPublic: isPublic, - Remark: remark, + Title: title, + StartDate: &startDate, + EndDate: &endDate, + ScheduleType: scheduleType, + BgColor: color, + IsPublic: isPublic, + Remark: remark, } if newEvent.BgColor == "" { // 获取日历颜色 diff --git a/frontend/ops_vue_js/src/i18n/en.json b/frontend/ops_vue_js/src/i18n/en.json index 0e6d2df..017c91e 100644 --- a/frontend/ops_vue_js/src/i18n/en.json +++ b/frontend/ops_vue_js/src/i18n/en.json @@ -312,6 +312,7 @@ "standby": "Standby", "personal_holiday": "Personal Holiday", "public_holiday": "Public Holiday", + "event_type": "Event Type", "to": "To", "close": "Close", "copy": "Copy", diff --git a/frontend/ops_vue_js/src/i18n/zh-CN.json b/frontend/ops_vue_js/src/i18n/zh-CN.json index b5b0974..d7e8bea 100644 --- a/frontend/ops_vue_js/src/i18n/zh-CN.json +++ b/frontend/ops_vue_js/src/i18n/zh-CN.json @@ -312,6 +312,7 @@ "standby": "备用", "personal_holiday": "个人假期", "public_holiday": "公众假期", + "event_type": "日程类型", "to": "至", "close": "关闭", "copy": "复制", diff --git a/frontend/ops_vue_js/src/views/calendar/CalendarDetail.vue b/frontend/ops_vue_js/src/views/calendar/CalendarDetail.vue index c5e578d..c8dbe69 100644 --- a/frontend/ops_vue_js/src/views/calendar/CalendarDetail.vue +++ b/frontend/ops_vue_js/src/views/calendar/CalendarDetail.vue @@ -37,6 +37,7 @@ const eventData = ref({ startDate: "", endDate: "", color: "#3788d9", + scheduleType: "work", isPublic: false, isEditing: false, isEditable: false, @@ -78,13 +79,14 @@ function closeEventModal() { showModal.value = false } -function openEventModal(dateStr, dataEnd, id = 0, title = "", color = "#3788d9", isPublic = false, isEditing = false, isEditable = true) { +function openEventModal(dateStr, dataEnd, id = 0, title = "", color = "#3788d9", scheduleType = "work", isPublic = false, isEditing = false, isEditable = true) { eventData.value = { id: id, title: title, startDate: dateStr, endDate: dataEnd, color: color, + scheduleType: scheduleType, isPublic: isPublic, isEditing: isEditing, isEditable: isEditable, @@ -99,6 +101,7 @@ function editEvent(info) { parseInt(info.event.id), info.event.title, info.event.backgroundColor, + info.event.extendedProps?.scheduleType || "work", info.event.extendedProps?.isPublic || false, true, info.event.durationEditable, @@ -108,6 +111,11 @@ function editEvent(info) { function selectColor(colorValue) { if (eventData.value.isEditable) { eventData.value.color = colorValue + // 根据颜色更新日程类型 + const selectedColor = colorOptions.value.find(c => c.value === colorValue) + if (selectedColor) { + eventData.value.scheduleType = selectedColor.type + } } } @@ -129,9 +137,6 @@ async function saveEvent() { return } - const selectedColor = colorOptions.value.find(c => c.value === eventData.value.color) - const scheduleType = selectedColor ? selectedColor.type : "work" - try { let result if (eventData.value.isEditing) { @@ -144,7 +149,7 @@ async function saveEvent() { ? eventData.value.endDate : DateUtils.toRealEnd(eventData.value.endDate), ), - schedule_type: scheduleType, + schedule_type: eventData.value.scheduleType, is_public: eventData.value.isPublic, }) } else { @@ -157,7 +162,7 @@ async function saveEvent() { ? eventData.value.endDate : DateUtils.toRealEnd(eventData.value.endDate), ), - schedule_type: scheduleType, + schedule_type: eventData.value.scheduleType, is_public: eventData.value.isPublic, }) } @@ -218,6 +223,10 @@ async function getEvents() { backgroundColor: item.BgColor, borderColor: item.ID === pageData.value.seleEventID ? "#000000" : "#F7F7F7", allDay: true, + extendedProps: { + scheduleType: item.ScheduleType || "work", + isPublic: item.IsPublic || false, + }, }) }) } @@ -391,8 +400,6 @@ const calendarOptions = ref({ 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({ @@ -400,7 +407,7 @@ const calendarOptions = ref({ title: info.event.title, start: toDatetime(startStr), end: toDatetime(startStr === endStr ? endStr : DateUtils.toRealEnd(endStr)), - schedule_type: scheduleType, + schedule_type: info.event.extendedProps?.scheduleType || "work", }).then(r => { if (r.errCode !== 0) toast.error(t('message.server_error')) else getEvents() @@ -569,6 +576,12 @@ onMounted(() => { + +
+ {{ t('schedule.event_type') }}: + {{ t('schedule.' + eventData.scheduleType) || eventData.scheduleType }} +
+
{{ t('calendar.is_public_event') }} From 0a85cd4c49515ad7057d88d6004eb11bcfed6b28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E6=96=87=E5=B3=B0?= Date: Wed, 6 May 2026 19:07:54 +0800 Subject: [PATCH 06/21] up --- .workbuddy/memory/2026-05-06.md | 12 ++++ backend/my_work/routers/apiCalendar.go | 70 +++++++++---------- .../src/views/calendar/CalendarDetail.vue | 8 ++- 3 files changed, 52 insertions(+), 38 deletions(-) diff --git a/.workbuddy/memory/2026-05-06.md b/.workbuddy/memory/2026-05-06.md index 3487e3b..4395f28 100644 --- a/.workbuddy/memory/2026-05-06.md +++ b/.workbuddy/memory/2026-05-06.md @@ -30,3 +30,15 @@ ### 注意事项 - GORM AutoMigrate 会自动添加新字段 - 前端颜色选择与日程类型联动 + +## 修复:calendar/events jsonErr + +**问题**:`fromGetCalendarEvents` 的 `start/end` 是 `*time.Time` 类型,无法直接解析字符串格式的日期。 + +**修复**:改为直接用类型断言解析字符串,用 `time.Parse("2006-01-02", ...)` 解析。 + +## 优化:BgColor 弃用,前端根据 ScheduleType 渲染颜色 + +**前端**:添加 `getColorByScheduleType()` 函数,`getEvents` 中使用 scheduleType 映射颜色。 + +**后端**:只存储 ScheduleType,不处理颜色逻辑。颜色完全由前端 `colorOptions` 控制。 diff --git a/backend/my_work/routers/apiCalendar.go b/backend/my_work/routers/apiCalendar.go index e87163b..007130f 100644 --- a/backend/my_work/routers/apiCalendar.go +++ b/backend/my_work/routers/apiCalendar.go @@ -325,26 +325,35 @@ func ApiCalendar(r *gin.RouterGroup) { r.POST("/calendar/events", func(ctx *gin.Context) { isAuth, _, data := AuthenticationAuthority(ctx) if isAuth { - var from fromGetCalendarEvents - if err := mapstructure.Decode(data, &from); err == nil { - var events []TabCalendarEvent - models.DB.Where("calendar_id = ? AND start_date <= ? AND end_date >= ? AND deleted_at IS NULL", - from.CalendarID, from.End, from.Start).Find(&events) - - // 为事件添加编辑权限标识 - var relist []map[string]interface{} - for _, event := range events { - data, _ := json.Marshal(event) - var temp map[string]interface{} - json.Unmarshal(data, &temp) - // 这里可以根据需要添加 edit 字段 - relist = append(relist, temp) - } - - ReturnJson(ctx, "apiOK", gin.H{"list": relist}) - } else { + // 直接从 data 中解析,避免 float64 → uint 类型问题 + calendarIDRaw, ok := data["calendar_id"].(float64) + if !ok || calendarIDRaw == 0 { ReturnJson(ctx, "jsonErr", nil) + return } + calendarID := uint(calendarIDRaw) + + // 解析日期字符串 + startStr, _ := data["start"].(string) + endStr, _ := data["end"].(string) + startDate, _ := time.Parse("2006-01-02", startStr) + endDate, _ := time.Parse("2006-01-02", endStr) + + var events []TabCalendarEvent + models.DB.Where("calendar_id = ? AND start_date <= ? AND end_date >= ? AND deleted_at IS NULL", + calendarID, &endDate, &startDate).Find(&events) + + // 为事件添加编辑权限标识 + var relist []map[string]interface{} + for _, event := range events { + data, _ := json.Marshal(event) + var temp map[string]interface{} + json.Unmarshal(data, &temp) + // 这里可以根据需要添加 edit 字段 + relist = append(relist, temp) + } + + ReturnJson(ctx, "apiOK", gin.H{"list": relist}) } else { ReturnJson(ctx, "userCookieError", nil) } @@ -373,7 +382,6 @@ func ApiCalendar(r *gin.RouterGroup) { startStr, _ := data["start"].(string) endStr, _ := data["end"].(string) title, _ := data["title"].(string) - color, _ := data["color"].(string) remark, _ := data["remark"].(string) isPublic, _ := data["is_public"].(bool) scheduleType, _ := data["schedule_type"].(string) @@ -386,17 +394,13 @@ func ApiCalendar(r *gin.RouterGroup) { event := TabCalendarEvent{ CalendarID: calendarID, - UserID: user.ID, - Title: title, - StartDate: &startDate, - EndDate: &endDate, + UserID: user.ID, + Title: title, + StartDate: &startDate, + EndDate: &endDate, ScheduleType: scheduleType, - BgColor: color, - IsPublic: isPublic, - Remark: remark, - } - if event.BgColor == "" { - event.BgColor = calendar.Color + IsPublic: isPublic, + Remark: remark, } if models.DB.Create(&event).Error == nil { @@ -444,7 +448,6 @@ func ApiCalendar(r *gin.RouterGroup) { startStr, _ := data["start"].(string) endStr, _ := data["end"].(string) title, _ := data["title"].(string) - color, _ := data["color"].(string) remark, _ := data["remark"].(string) isPublic, _ := data["is_public"].(bool) scheduleType, _ := data["schedule_type"].(string) @@ -460,16 +463,9 @@ func ApiCalendar(r *gin.RouterGroup) { StartDate: &startDate, EndDate: &endDate, ScheduleType: scheduleType, - BgColor: color, IsPublic: isPublic, Remark: remark, } - if newEvent.BgColor == "" { - // 获取日历颜色 - var calendar TabCalendar - models.DB.Where("id = ?", oldEvent.CalendarID).First(&calendar) - newEvent.BgColor = calendar.Color - } if models.DB.Model(&oldEvent).Updates(&newEvent).Error == nil { // 记录日志 diff --git a/frontend/ops_vue_js/src/views/calendar/CalendarDetail.vue b/frontend/ops_vue_js/src/views/calendar/CalendarDetail.vue index c8dbe69..8ea89dc 100644 --- a/frontend/ops_vue_js/src/views/calendar/CalendarDetail.vue +++ b/frontend/ops_vue_js/src/views/calendar/CalendarDetail.vue @@ -119,6 +119,12 @@ function selectColor(colorValue) { } } +// 根据日程类型获取颜色 +function getColorByScheduleType(scheduleType) { + const found = colorOptions.value.find(c => c.type === scheduleType) + return found ? found.value : "#3788d9" // 默认蓝色 +} + // 日期转后端格式:YYYY-MM-DD 00:00:00 function toDatetime(dateStr) { return dateStr ? dateStr + " 00:00:00" : "" @@ -220,7 +226,7 @@ async function getEvents() { end: item.StartDate === item.EndDate ? item.EndDate : DateUtils.toCalendarEnd(item.EndDate), - backgroundColor: item.BgColor, + backgroundColor: getColorByScheduleType(item.ScheduleType), borderColor: item.ID === pageData.value.seleEventID ? "#000000" : "#F7F7F7", allDay: true, extendedProps: { From 8bbde00594a26c5686db02dfd87a8f4ee331a939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E6=96=87=E5=B3=B0?= Date: Wed, 6 May 2026 19:39:41 +0800 Subject: [PATCH 07/21] up --- backend/my_work/routers/apiCalendar.go | 104 +++++++++++++++---------- 1 file changed, 62 insertions(+), 42 deletions(-) diff --git a/backend/my_work/routers/apiCalendar.go b/backend/my_work/routers/apiCalendar.go index 007130f..469b74e 100644 --- a/backend/my_work/routers/apiCalendar.go +++ b/backend/my_work/routers/apiCalendar.go @@ -27,9 +27,9 @@ 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"` + 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 *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"` @@ -321,42 +321,62 @@ func ApiCalendar(r *gin.RouterGroup) { } }) - // 获取日历事件 + // 获取日历事件(公开接口,无需登录) r.POST("/calendar/events", func(ctx *gin.Context) { - isAuth, _, data := AuthenticationAuthority(ctx) - if isAuth { - // 直接从 data 中解析,避免 float64 → uint 类型问题 - calendarIDRaw, ok := data["calendar_id"].(float64) - if !ok || calendarIDRaw == 0 { - ReturnJson(ctx, "jsonErr", nil) - return - } - calendarID := uint(calendarIDRaw) + data, cookieval := SeparateData(ctx) - // 解析日期字符串 - startStr, _ := data["start"].(string) - endStr, _ := data["end"].(string) - startDate, _ := time.Parse("2006-01-02", startStr) - endDate, _ := time.Parse("2006-01-02", endStr) - - var events []TabCalendarEvent - models.DB.Where("calendar_id = ? AND start_date <= ? AND end_date >= ? AND deleted_at IS NULL", - calendarID, &endDate, &startDate).Find(&events) - - // 为事件添加编辑权限标识 - var relist []map[string]interface{} - for _, event := range events { - data, _ := json.Marshal(event) - var temp map[string]interface{} - json.Unmarshal(data, &temp) - // 这里可以根据需要添加 edit 字段 - relist = append(relist, temp) - } - - ReturnJson(ctx, "apiOK", gin.H{"list": relist}) - } else { - ReturnJson(ctx, "userCookieError", nil) + if data == nil { + ReturnJson(ctx, "jsonErr", nil) + return } + calendarIDRaw, ok := data["calendar_id"].(float64) + if !ok || calendarIDRaw == 0 { + ReturnJson(ctx, "jsonErr", nil) + return + } + calendarID := uint(calendarIDRaw) + + startStr, _ := data["start"].(string) + endStr, _ := data["end"].(string) + startDate, _ := time.Parse("2006-01-02", startStr) + endDate, _ := time.Parse("2006-01-02", endStr) + + // 查询:当前日历的事件 + 所有公共日程 + var events []TabCalendarEvent + models.DB.Where( + "(calendar_id = ? OR is_public = ?) AND start_date <= ? AND end_date >= ? AND deleted_at IS NULL", + calendarID, true, &endDate, &startDate, + ).Find(&events) + + // 判断是否已登录 + var currentUserID uint + isLogin := false + if cookieval != "" { + user, err := AuthenticationAuthorityFromCookie(cookieval) + if err == nil { + isLogin = true + currentUserID = user.ID + } + } + + var relist []map[string]interface{} + for _, event := range events { + eventMap, _ := json.Marshal(event) + var item map[string]interface{} + json.Unmarshal(eventMap, &item) + + // 可编辑条件:事件创建者 或 日历管理员 + canEdit := false + if isLogin { + if event.UserID == currentUserID || slices.Contains(calendarAdmins, currentUserID) { + canEdit = true + } + } + item["canEdit"] = canEdit + relist = append(relist, item) + } + + ReturnJson(ctx, "apiOK", gin.H{"list": relist}) }) // 添加日历事件 @@ -394,13 +414,13 @@ func ApiCalendar(r *gin.RouterGroup) { event := TabCalendarEvent{ CalendarID: calendarID, - UserID: user.ID, - Title: title, - StartDate: &startDate, - EndDate: &endDate, + UserID: user.ID, + Title: title, + StartDate: &startDate, + EndDate: &endDate, ScheduleType: scheduleType, - IsPublic: isPublic, - Remark: remark, + IsPublic: isPublic, + Remark: remark, } if models.DB.Create(&event).Error == nil { From 056eb4798d8ee07b311ff7e19bed25f7d7d167c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E6=96=87=E5=B3=B0?= Date: Wed, 6 May 2026 20:01:58 +0800 Subject: [PATCH 08/21] up --- .../src/views/calendar/CalendarDetail.vue | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/frontend/ops_vue_js/src/views/calendar/CalendarDetail.vue b/frontend/ops_vue_js/src/views/calendar/CalendarDetail.vue index 8ea89dc..ae14464 100644 --- a/frontend/ops_vue_js/src/views/calendar/CalendarDetail.vue +++ b/frontend/ops_vue_js/src/views/calendar/CalendarDetail.vue @@ -95,6 +95,7 @@ function openEventModal(dateStr, dataEnd, id = 0, title = "", color = "#3788d9", } function editEvent(info) { + const canEdit = info.event.extendedProps?.canEdit ?? false openEventModal( info.event.startStr, info.event.end ? info.event.endStr : info.event.startStr, @@ -104,7 +105,7 @@ function editEvent(info) { info.event.extendedProps?.scheduleType || "work", info.event.extendedProps?.isPublic || false, true, - info.event.durationEditable, + canEdit, ) } @@ -219,6 +220,7 @@ async function getEvents() { if (errCode === 0) { calendarOptions.value.events = [] ;(data.list || []).forEach(item => { + const canEdit = item.canEdit === true calendarOptions.value.events.push({ id: item.ID, title: item.Title, @@ -229,12 +231,22 @@ async function getEvents() { backgroundColor: getColorByScheduleType(item.ScheduleType), borderColor: item.ID === pageData.value.seleEventID ? "#000000" : "#F7F7F7", allDay: true, + editable: canEdit, + durationEditable: canEdit, extendedProps: { scheduleType: item.ScheduleType || "work", isPublic: item.IsPublic || false, + canEdit: canEdit, }, }) }) + + // 检测数据是否变化(多浏览器同步场景),变了就重新计算滚动 + const newSnapshot = JSON.stringify(data.list) + if (newSnapshot !== pageData.value.lastEventsSnapshot) { + pageData.value.lastEventsSnapshot = newSnapshot + setTimeout(recalcScrollTitles, 150) + } } } catch { // 拦截器已处理 @@ -405,6 +417,12 @@ const calendarOptions = ref({ }, eventDrop(info) { + const canEdit = info.event.extendedProps?.canEdit ?? false + if (!canEdit) { + info.revert() + toast.warning(t("message.no_permission")) + return + } // 拖拽后直接更新 const startStr = info.event.startStr const endStr = info.event.end ? info.event.endStr : startStr @@ -441,9 +459,14 @@ watch(locale, () => { }) let resizeObserver = null +let refreshTimer = null onMounted(() => { fetchCalendarInfo() + // 每 5 秒刷新一次数据 + refreshTimer = setInterval(() => { + getEvents() + }, 5000) // 监听日历容器宽度变化 let resizeTimer = null resizeObserver = new ResizeObserver(() => { @@ -454,6 +477,10 @@ onMounted(() => { resizeObserver.observe(calendarRef.value.$el) } onBeforeUnmount(() => { + if (refreshTimer) { + clearInterval(refreshTimer) + refreshTimer = null + } if (resizeObserver) { resizeObserver.disconnect() resizeObserver = null From 1d5f0b4bc086ea13e8e9ed14227d637a93c6a306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E6=96=87=E5=B3=B0?= Date: Wed, 6 May 2026 20:36:52 +0800 Subject: [PATCH 09/21] up --- .workbuddy/memory/2026-05-06.md | 24 ++++++++ .../src/components/ConfirmDialog.vue | 8 ++- frontend/ops_vue_js/src/i18n/en.json | 3 + frontend/ops_vue_js/src/i18n/zh-CN.json | 3 + .../src/views/calendar/CalendarDetail.vue | 59 +++++++++++++++++-- .../src/views/calendar/CalendarList.vue | 33 ++++++++--- 6 files changed, 114 insertions(+), 16 deletions(-) diff --git a/.workbuddy/memory/2026-05-06.md b/.workbuddy/memory/2026-05-06.md index 4395f28..50d9e60 100644 --- a/.workbuddy/memory/2026-05-06.md +++ b/.workbuddy/memory/2026-05-06.md @@ -42,3 +42,27 @@ **前端**:添加 `getColorByScheduleType()` 函数,`getEvents` 中使用 scheduleType 映射颜色。 **后端**:只存储 ScheduleType,不处理颜色逻辑。颜色完全由前端 `colorOptions` 控制。 + +## CalendarDetail 滚动标题快照对比 + +**功能**:每次 getEvents 存快照,数据变化或宽度变化时重新计算标题滚动。 + +**实现**: +- `pageData.lastEventsSnapshot`:存储上一次的 JSON 快照 +- `getEvents()` 末尾对比快照,变化则 `setTimeout(recalcScrollTitles, 150)` +- `ResizeObserver` 监听日历容器宽度变化,防抖 150ms 后重算 +- `applyScrollToTitle()`:清除旧状态 → 测量 overflow → 设置 `--scroll-distance` 和 `data-truncated` 属性 + +## CalendarList 编辑/删除按钮改用 canEdit + +**改动**: +- `CalendarList.vue`:编辑/删除按钮 `v-if` 条件从 `calendar.UserID === userStore.userInfo?.ID` 改为 `calendar.canEdit` +- 移除废弃的 `useUserStore` 导入 + +## CalendarList 删除改用 ConfirmDialog 组件 + +**改动**: +- `CalendarList.vue` 导入并使用 `ConfirmDialog` 组件 +- 新增 `showDeleteModal` + `deletingCalendar` 状态 +- `deleteCalendar()` 改为打开确认弹窗,`confirmDelete()` 执行实际删除 API +- i18n 新增 `calendar.confirm_delete_message`:zh-CN「确定要删除日历「{name}」吗?此操作不可撤销。」,en 英文版 diff --git a/frontend/ops_vue_js/src/components/ConfirmDialog.vue b/frontend/ops_vue_js/src/components/ConfirmDialog.vue index 704bae7..31c2289 100644 --- a/frontend/ops_vue_js/src/components/ConfirmDialog.vue +++ b/frontend/ops_vue_js/src/components/ConfirmDialog.vue @@ -15,7 +15,7 @@ * 或者作为组件使用 v-model: * */ -import { ref, watch } from "vue"; +import { ref, watch, onMounted, getCurrentInstance } from "vue"; import { useI18n } from "vue-i18n"; const props = defineProps({ @@ -47,7 +47,11 @@ const props = defineProps({ const emit = defineEmits(["update:modelValue", "confirm", "cancel"]); -const { t } = useI18n(); +// 优先用组件内 i18n,Teleport 场景下可能为空,从全局补足 +const instance = getCurrentInstance(); +const { t: componentT } = useI18n(); +const globalT = instance?.appContext?.config?.globalProperties?.$i18n?.t; +const t = (key, ...args) => componentT(key, ...args) || globalT?.(key, ...args) || key; function close() { emit("update:modelValue", false); diff --git a/frontend/ops_vue_js/src/i18n/en.json b/frontend/ops_vue_js/src/i18n/en.json index 017c91e..45e1c1c 100644 --- a/frontend/ops_vue_js/src/i18n/en.json +++ b/frontend/ops_vue_js/src/i18n/en.json @@ -1,4 +1,6 @@ { + "delete": "Delete", + "cancel": "Cancel", "common": { "actions": "Actions", "search": "Search", @@ -625,6 +627,7 @@ "update_success": "Calendar updated successfully", "delete_success": "Calendar deleted successfully", "confirm_delete": "Are you sure you want to delete this calendar?", + "confirm_delete_message": "Are you sure you want to delete calendar '{name}'? This action cannot be undone.", "loading": "Loading...", "add_event": "Add Event", "edit_event": "Edit Event", diff --git a/frontend/ops_vue_js/src/i18n/zh-CN.json b/frontend/ops_vue_js/src/i18n/zh-CN.json index d7e8bea..7c28b72 100644 --- a/frontend/ops_vue_js/src/i18n/zh-CN.json +++ b/frontend/ops_vue_js/src/i18n/zh-CN.json @@ -1,4 +1,6 @@ { + "delete": "删除", + "cancel": "取消", "common": { "actions": "操作", "search": "搜索", @@ -625,6 +627,7 @@ "update_success": "更新成功", "delete_success": "删除成功", "confirm_delete": "确定要删除此日历吗?", + "confirm_delete_message": "确定要删除日历「{name}」吗?此操作不可撤销。", "loading": "加载中...", "add_event": "添加事件", "edit_event": "编辑事件", diff --git a/frontend/ops_vue_js/src/views/calendar/CalendarDetail.vue b/frontend/ops_vue_js/src/views/calendar/CalendarDetail.vue index ae14464..8d26126 100644 --- a/frontend/ops_vue_js/src/views/calendar/CalendarDetail.vue +++ b/frontend/ops_vue_js/src/views/calendar/CalendarDetail.vue @@ -13,6 +13,7 @@ import { useUserStore } from "@/stores/user" import { calendarApi } from "@/api/calendar" import { useDateUtils } from "@/composables/useDateUtils" import DatatimePickerForFullCalendar from "@/components/datatimePickerForFullCalendar.vue" +import ConfirmDialog from "@/components/ConfirmDialog.vue" const route = useRoute() const router = useRouter() @@ -62,6 +63,8 @@ const pageData = ref({ lastEventsSnapshot: null, }) +const showDeleteModal = ref(false) + // 选中/取消选中事件 function unseleEvent(eventID) { const target = calendarOptions.value.events.find(item => item.id === eventID) @@ -187,19 +190,23 @@ async function saveEvent() { } async function deleteEvent() { - if (!confirm(t('calendar.confirm_delete_event'))) return + showDeleteModal.value = true +} +async function confirmDeleteEvent() { try { const result = await calendarApi.deleteEvent(eventData.value.id) if (result.errCode === 0) { - toast.success(t('calendar.event_delete_success')) + toast.success(t("calendar.event_delete_success")) closeEventModal() getEvents() } else { - toast.error(t('message.server_error')) + toast.error(t("message.server_error")) } } catch { // 拦截器已处理 + } finally { + showDeleteModal.value = false } } @@ -319,12 +326,20 @@ const calendarOptions = ref({ }, headerToolbar: { - left: "prevYear,prev,today,next,nextYear", - center: "title", - right: "", + left: "backToList,prevYear,prev,today,next,nextYear", + center: "myTitle", + right: "title", }, customButtons: { + backToList: { + text: t("calendar.calendars"), + click() { router.push("/calendars") }, + }, + myTitle: { + text: calendarInfo.value?.Name || "", + disabled: true, + }, prevYear: { text: t("schedule.previous_year"), click() { calendarRef.value.getApi().prevYear(); getEvents() }, @@ -439,9 +454,15 @@ const calendarOptions = ref({ }, }) +// 监听日历信息变化 +watch(calendarInfo, () => { + calendarOptions.value.customButtons.myTitle.text = calendarInfo.value?.Name || "" +}) + // 监听语言变化 watch(locale, () => { calendarOptions.value.locale = locale.value + calendarOptions.value.customButtons.backToList.text = t("calendar.calendars") 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") @@ -674,9 +695,35 @@ onMounted(() => {
+ + +