diff --git a/.workbuddy/memory/2026-05-06.md b/.workbuddy/memory/2026-05-06.md new file mode 100644 index 0000000..dc4afb5 --- /dev/null +++ b/.workbuddy/memory/2026-05-06.md @@ -0,0 +1,147 @@ +# 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 会自动添加新字段 +- 前端颜色选择与日程类型联动 + +## 修复:calendar/events jsonErr + +**问题**:`fromGetCalendarEvents` 的 `start/end` 是 `*time.Time` 类型,无法直接解析字符串格式的日期。 + +**修复**:改为直接用类型断言解析字符串,用 `time.Parse("2006-01-02", ...)` 解析。 + +## 优化:BgColor 弃用,前端根据 ScheduleType 渲染颜色 + +**前端**:添加 `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 英文版 + +## 新增日历管理页面 /calendars/admin + +**功能**:系统管理员查看所有日历列表,包含日程数量、创建者、创建时间。 + +**新增文件**: +- `src/views/calendar/CalendarAdminList.vue` - 日历管理列表组件 + +**路由修改** `src/router/index.js`: +- 新增 `/calendars/admin` 路由,指向 `CalendarAdminList.vue` +- 设置 `meta: { requireSysAdmin: true }` 要求管理员权限 + +**SysAdminView.vue**: +- `tabs` 数组新增 `{ id: 'calendar', label: t('calendar.admin_title'), to: '/calendars/admin' }` + +**i18n 新增**: +- `zh-CN.json`: `calendar.admin_title = "日历管理"`, `calendar.event_count = "日程数量"` +- `en.json`: `calendar.admin_title = "Calendar Admin"`, `calendar.event_count = "Event Count"` + +## 修复:CalendarAdminList eventCounts 未定义 + +**问题**:模板访问 `eventCounts[calendar.ID]` 但 `eventCounts` 从未声明,且 `fetchEventCounts` 未被调用。 + +**修复**:后端已直接返回 `event_count` 字段,改为模板直接用 `calendar.event_count ?? 0`,删除多余的 `eventCounts` ref 和 `fetchEventCounts` 函数。 + +## 新增:后端 TabCalendarEventUserBind 绑定表 + +**文件**:`routers/binds.go` +- 新增 `TabCalendarEventUserBind` 结构体(EventID/UserID/CreatorID/CreatedAt) +- 在 `BindsInit` AutoMigrate 中注册 + +## 新增:日历右键菜单(复制/粘贴日程) + +**文件**:`CalendarDetail.vue` + +**功能**: +- 右键日程事件弹出上下文菜单,显示「复制日程」「粘贴日程」 +- 复制:将日程数据存入 `clipboard` ref +- 粘贴:以 clipboard 数据调用 `addEvent` API 创建新日程 +- 点击任意位置关闭菜单 + +**实现**: +- `contextMenu` ref 管理菜单显示/位置/事件数据 +- `clipboard` ref 存储复制的日程 +- `eventDidMount` 中为每个事件绑定 `contextmenu` 事件 +- `onMounted` 注册全局 click 关闭菜单,`onBeforeUnmount` 移除 +- i18n 新增 `calendar.copy_event/paste_event/copy_success/paste_success/no_event_to_paste` + +## 修复:粘贴多天日程时结束日期少1天 + +**问题**:Ctrl+V 粘贴大于1天的日程时,目标结束日期少1天。 + +**原因**:`pasteToDate` 中用 `new Date(targetEndMs).toISOString().split('T')[0]` 计算目标结束日期,`toISOString()` 输出 UTC 时间,在 UTC+8 时区下日期会往前偏移1天。 + +**修复**(`CalendarDetail.vue` `pasteToDate` 函数): +- 改用天数差 `Math.round((origEndDate - origStartDate) / 86400000)` 替代毫秒差 +- 用本地日期方法 `targetEndDate.setDate(targetEndDate.getDate() + diffDays)` + 手动拼接 `YYYY-MM-DD`,替代 `toISOString().split('T')[0]` + +## 修复:Ctrl+C 无法复制大于1天的日程 + +**问题**:Ctrl+C 复制多天日程后粘贴无效果。 + +**根因**:`calendarOptions.value.events` 中多天事件的 `end` 是 `DateUtils.toCalendarEnd()` 返回的 **Date 对象**(非字符串)。Ctrl+C 直接 `selectedEvent.end || selectedEvent.start` 存入 clipboard,导致 `pasteToDate` 调用 `clipboard.value.end.split('T')[0]` 时 Date 对象没有 `split` 方法而报错。单天事件因走 `isSameDay` 分支不解析 `end`,所以不受影响。 + +**修复**(`CalendarDetail.vue` `handleKeyDown` Ctrl+C 分支): +- 检测 `end` 是否为 Date 实例,如果是则用本地日期方法转为 `YYYY-MM-DD` 字符串再存入 clipboard + +## 新增:编辑事件窗口显示创建者 + +**功能**:编辑事件模态框标题居中显示「xxx 创建」 + +**实现**(参考 ScheduleView.vue): +- 导入 `useUsersStore`,添加 `usersStore` +- `pageData.eventBindUserID`:数组,每项 `{ eventID, userID }` +- `getUserIdFromEventID(eventID)`:通过事件ID查创建者用户ID +- `getUsernameFromUserID(userID)`:调用 `usersStore.getUsernameFromUserID(userID)`(有缓存) +- `getEvents` 中遍历事件时 `pageData.value.eventBindUserID.push({ eventID: item.ID, userID: item.UserID })` +- 模态框标题新增 `
` 居中显示 +- i18n:`calendar.created_by` = "{name} 创建" / "Created by {name}" 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..d63a822 --- /dev/null +++ b/backend/my_work/routers/apiCalendar.go @@ -0,0 +1,677 @@ +package routers + +import ( + "encoding/json" + "ops/models" + "slices" + "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 *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:最后修改时间"` + 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"` + Is_public 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"` + Is_public bool `json:"is_public"` +} + +type fromDeleteCalendar struct { + ID uint `json:"id" binding:"required"` +} + +type fromGetCalendarEvents struct { + 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"` + 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 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 { + ID uint `json:"id" binding:"required"` +} + +type fromRestoreCalendar struct { + ID uint `json:"id" binding:"required"` +} + +var ( + calendarUserGroup TabUserGroups + calendarAdmins []uint +) + +// CalendarUpdateAdminsCash +func CalendarUpdateAdminsCash() { + calendarAdmins = nil + calendarAdmins = append(calendarAdmins, 1) // id=1 系统管理员默认拥有所有权限 + var binds []TabUserGroupBinds + models.DB.Where("group_id = ?", calendarUserGroup.ID).Find(&binds) + for _, item := range binds { + if !slices.Contains(calendarAdmins, item.UserID) { + calendarAdmins = append(calendarAdmins, item.UserID) + } + } +} + +// canModifyCustomer 判断是否有权限修改/删除客户(创建者或管理员) +func canModifyCalendar(userID, creatorUserID uint) bool { + if slices.Contains(calendarAdmins, userID) { + return true + } + return userID == creatorUserID +} + +func ApiCalendarInit() { + // 初始化数据表 + models.DB.AutoMigrate(&TabCalendar{}) + models.DB.AutoMigrate(&TabCalendarEvent{}) + models.DB.AutoMigrate(&TabCalendarLog{}) + + // 自动创建 calendar_admin 用户组 + models.DB.Where("name = ?", "calendar_admin").FirstOrCreate(&calendarUserGroup, TabUserGroups{ + Name: "calendar_admin", + Type: "usergroup", + }) + + CalendarUpdateAdminsCash() +} + +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.Is_public, + } + 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, user, _ := AuthenticationAuthority(ctx) + + var calendars []TabCalendar + models.DB.Where("deleted_at IS NULL").Order("created_at DESC").Find(&calendars) + + type CalendarWithEdit struct { + TabCalendar + CanEdit bool `json:"canEdit"` + } + var result []CalendarWithEdit + for _, cal := range calendars { + // 私有日历:只有创建者可见 + if !cal.IsPublic { + if !isAuth || cal.UserID != user.ID { + continue + } + result = append(result, CalendarWithEdit{ + TabCalendar: cal, + CanEdit: true, + }) + continue + } + // 公开日历 + canEdit := false + if isAuth { + canEdit = canModifyCalendar(user.ID, cal.UserID) + } + result = append(result, CalendarWithEdit{ + TabCalendar: cal, + CanEdit: canEdit, + }) + } + ReturnJson(ctx, "apiOK", gin.H{"list": result}) + + }) + + // 更新日历 + 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.Is_public, + } + 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/list_all", func(ctx *gin.Context) { + isAuth, user, _ := AuthenticationAuthority(ctx) + if !isAuth { + ReturnJson(ctx, "userCookieError", nil) + return + } + + // 限制只有日历管理员可访问 + if !slices.Contains(calendarAdmins, user.ID) { + ReturnJson(ctx, "permission_denied", nil) + return + } + + // 使用 Unscoped 查询所有日历(包括软删除的) + var calendars []TabCalendar + models.DB.Unscoped().Order("created_at DESC").Find(&calendars) + + // 一次性查询所有日历的事件数量(仅统计未删除的事件) + type calendarEventCount struct { + CalendarID uint `gorm:"column:calendar_id"` + Cnt int `gorm:"column:cnt"` + } + var rows []calendarEventCount + models.DB.Model(&TabCalendarEvent{}). + Select("calendar_id, COUNT(*) as cnt"). + Where("deleted_at IS NULL"). + Group("calendar_id"). + Scan(&rows) + eventCountMap := make(map[uint]int) + for _, row := range rows { + eventCountMap[row.CalendarID] = row.Cnt + } + + type CalendarWithEdit struct { + TabCalendar + CanEdit bool `json:"canEdit"` + EventCount int `json:"event_count"` + } + var result []CalendarWithEdit + for _, cal := range calendars { + result = append(result, CalendarWithEdit{ + TabCalendar: cal, + CanEdit: true, + EventCount: eventCountMap[cal.ID], + }) + } + ReturnJson(ctx, "apiOK", gin.H{"list": result}) + }) + + // 恢复已删除的日历 + r.POST("/calendar/restore", func(ctx *gin.Context) { + isAuth, user, data := AuthenticationAuthority(ctx) + if !isAuth { + ReturnJson(ctx, "userCookieError", nil) + return + } + + // 限制只有日历管理员可操作 + if !slices.Contains(calendarAdmins, user.ID) { + ReturnJson(ctx, "permission_denied", nil) + return + } + + var from fromRestoreCalendar + if err := mapstructure.Decode(data, &from); err != nil { + ReturnJson(ctx, "jsonErr", nil) + return + } + + // 使用 Unscoped 查询(包括软删除的) + var calendar TabCalendar + if models.DB.Unscoped().Where("id = ?", from.ID).First(&calendar).Error != nil { + ReturnJson(ctx, "calendar_not_find", nil) + return + } + + // 恢复软删除(将 deleted_at 设为 NULL) + if models.DB.Unscoped().Model(&calendar).Update("deleted_at", nil).Error != nil { + ReturnJson(ctx, "apiErr", nil) + return + } + + // 记录日志 + newContent, _ := json.Marshal(calendar) + log := TabCalendarLog{ + CalendarID: calendar.ID, + UserID: user.ID, + ActionType: "restore", + NewContent: string(newContent), + IP: ctx.ClientIP(), + } + models.DB.Create(&log) + ReturnJson(ctx, "apiOK", 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) { + data, cookieval := SeparateData(ctx) + + 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 calendarCreatorID uint + var calendar TabCalendar + if models.DB.Where("id = ?", calendarID).First(&calendar).Error == nil { + calendarCreatorID = calendar.UserID + } + + 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 || calendarCreatorID == currentUserID || slices.Contains(calendarAdmins, currentUserID) { + canEdit = true + } + } + item["canEdit"] = canEdit + relist = append(relist, item) + } + //fmt.Println(calendarAdmins) + //fmt.Println(calendarUserGroup) + + ReturnJson(ctx, "apiOK", gin.H{"list": relist}) + }) + + // 添加日历事件 + r.POST("/calendar/addevent", func(ctx *gin.Context) { + isAuth, user, data := AuthenticationAuthority(ctx) + if isAuth { + // 先检查必需字段 + 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) + 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, + ScheduleType: scheduleType, + IsPublic: isPublic, + Remark: remark, + } + + 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) + } + }) + + // 更新日历事件 + r.POST("/calendar/updateevent", func(ctx *gin.Context) { + isAuth, user, data := AuthenticationAuthority(ctx) + if isAuth { + // 先检查必需字段 + idRaw, ok := data["id"].(float64) + if !ok || idRaw == 0 { + ReturnJson(ctx, "jsonErr", nil) + return + } + eventID := uint(idRaw) + + oldEvent := TabCalendarEvent{} + if models.DB.Where("id = ?", eventID).First(&oldEvent).Error == nil { + // 检查权限(事件创建人、日历创建人或管理员可修改) + var calendarCreatorID uint + var calendar TabCalendar + if models.DB.Where("id = ?", oldEvent.CalendarID).First(&calendar).Error == nil { + calendarCreatorID = calendar.UserID + } + if !canModifyCalendar(user.ID, oldEvent.UserID) && calendarCreatorID != user.ID { + ReturnJson(ctx, "permission_denied", nil) + return + } + + // 解析字段 + startStr, _ := data["start"].(string) + endStr, _ := data["end"].(string) + title, _ := data["title"].(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, + ScheduleType: scheduleType, + IsPublic: isPublic, + Remark: remark, + } + + 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, "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 { + // 检查权限(事件创建人、日历创建人或管理员可删除) + var calendarCreatorID uint + var calendar TabCalendar + if models.DB.Where("id = ?", oldEvent.CalendarID).First(&calendar).Error == nil { + calendarCreatorID = calendar.UserID + } + if !canModifyCalendar(user.ID, oldEvent.UserID) && calendarCreatorID != 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/backend/my_work/routers/apiSchedule.go b/backend/my_work/routers/apiSchedule.go index 2fbb7f4..5a75375 100644 --- a/backend/my_work/routers/apiSchedule.go +++ b/backend/my_work/routers/apiSchedule.go @@ -81,17 +81,17 @@ func ScheduleUpdateAdminsCash() { func ApiScheduleInit() { //先初始化数据表 - models.DB.AutoMigrate(&TabSchedule{}) - models.DB.AutoMigrate(&TabScheduleLog{}) + // models.DB.AutoMigrate(&TabSchedule{}) + // models.DB.AutoMigrate(&TabScheduleLog{}) - //先检查用户组有没有这个key - userGroup.Name = "schedule_admin" - if models.DB.Where(&userGroup).First(&userGroup).Error == nil { - ScheduleUpdateAdminsCash() - } else { - userGroup.Type = "usergroup" - models.DB.Create(&userGroup) - } + // //先检查用户组有没有这个key + // userGroup.Name = "schedule_admin" + // if models.DB.Where(&userGroup).First(&userGroup).Error == nil { + // ScheduleUpdateAdminsCash() + // } else { + // userGroup.Type = "usergroup" + // models.DB.Create(&userGroup) + // } } func ApiSchedule(r *gin.RouterGroup) { diff --git a/backend/my_work/routers/apiSysAdmin.go b/backend/my_work/routers/apiSysAdmin.go index 30a328e..ec5f4ad 100644 --- a/backend/my_work/routers/apiSysAdmin.go +++ b/backend/my_work/routers/apiSysAdmin.go @@ -164,7 +164,7 @@ func ApiSysAdmin(r *gin.RouterGroup) { return } - var params struct { + var params struct { GroupID float64 `json:"group_id" mapstructure:"group_id"` Page float64 `json:"page" mapstructure:"page"` PageSize float64 `json:"page_size" mapstructure:"page_size"` @@ -407,6 +407,8 @@ func ApiSysAdmin(r *gin.RouterGroup) { WarehouseUpdateAdminsCash() case "customer_admin": CustomerUpdateAdminsCash() + case "calendar_admin": + CalendarUpdateAdminsCash() } ReturnJson(ctx, "apiOK", nil) @@ -462,6 +464,8 @@ func ApiSysAdmin(r *gin.RouterGroup) { WarehouseUpdateAdminsCash() case "customer_admin": CustomerUpdateAdminsCash() + case "calendar_admin": + CalendarUpdateAdminsCash() } ReturnJson(ctx, "apiOK", nil) @@ -579,6 +583,28 @@ func ApiSysAdmin(r *gin.RouterGroup) { } } + if params.Module == "all" || params.Module == "calendar" { + var logs []TabCalendarLog + query := models.DB.Model(&TabCalendarLog{}) + if params.Module == "calendar" { + query.Order("created_at DESC").Find(&logs) + } else { + query.Order("created_at DESC").Limit(1000).Find(&logs) + } + for _, log := range logs { + allLogs = append(allLogs, LogEntry{ + ID: log.ID, + Module: "calendar", + EntityID: log.CalendarID, + UserID: log.UserID, + ActionType: log.ActionType, + IP: log.IP, + Remark: log.Remark, + CreatedAt: log.CreatedAt, + }) + } + } + if params.Module == "all" || params.Module == "warehouse" { var logs []TabWarehouseLog query := models.DB.Model(&TabWarehouseLog{}) diff --git a/backend/my_work/routers/apiUsers.go b/backend/my_work/routers/apiUsers.go index f7274a4..9e26ef9 100644 --- a/backend/my_work/routers/apiUsers.go +++ b/backend/my_work/routers/apiUsers.go @@ -339,7 +339,7 @@ func AuthenticationAuthority(ctx *gin.Context) (bool, TabUser, map[string]interf } } else { - ReturnJson(ctx, "userCookieError", nil) + //ReturnJson(ctx, "userCookieError", nil) return false, user, nil } @@ -594,6 +594,23 @@ func ApiUser(r *gin.RouterGroup) { } redata["isSysAdmin"] = isSysAdmin + // 获取用户加入的群组列表 + var binds []TabUserGroupBinds + models.DB.Where("user_id = ?", user.ID).Find(&binds) + + var groups []map[string]interface{} + for _, bind := range binds { + var group TabUserGroups + if models.DB.Where("id = ?", bind.GroupID).First(&group).Error == nil { + groups = append(groups, map[string]interface{}{ + "id": group.ID, + "name": group.Name, + "type": group.Type, + }) + } + } + redata["groups"] = groups + ReturnJson(ctx, "apiOK", redata) } diff --git a/backend/my_work/routers/binds.go b/backend/my_work/routers/binds.go index e5199b3..86f6976 100644 --- a/backend/my_work/routers/binds.go +++ b/backend/my_work/routers/binds.go @@ -82,6 +82,15 @@ type TabWarehouseItemCustomerBind struct { CreatedAt *time.Time `gorm:"type:datetime;autoCreateTime"` } +// TabCalendarEventUserBind 日程与用户关联表(日程分配给的用户) +type TabCalendarEventUserBind struct { + ID uint `gorm:"primarykey"` + EventID uint `gorm:"not null;index;comment:关联日程ID"` + UserID uint `gorm:"not null;index;comment:关联用户ID"` + CreatorID uint `gorm:"not null;comment:分配人id"` + CreatedAt *time.Time `gorm:"type:datetime;autoCreateTime"` +} + func BindsInit() { models.DB.AutoMigrate( &TabPurchaseFileBind{}, @@ -93,6 +102,7 @@ func BindsInit() { &TabWorkOrderPurchaseOrderBind{}, &TabWorkOrderCustomerBind{}, &TabWarehouseItemCustomerBind{}, + &TabCalendarEventUserBind{}, ) } diff --git a/frontend/ops_vue_js/package.json b/frontend/ops_vue_js/package.json index b742121..eaf6ece 100644 --- a/frontend/ops_vue_js/package.json +++ b/frontend/ops_vue_js/package.json @@ -1,6 +1,6 @@ { "name": "ops_vue_js", - "version": "1.3.5", + "version": "1.4.7", "private": true, "type": "module", "engines": { 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..796b34d --- /dev/null +++ b/frontend/ops_vue_js/src/api/calendar.js @@ -0,0 +1,53 @@ +import { api } from './index' + +export const calendarApi = { + // 创建日历 + createCalendar(data) { + return api.post('/calendar/calendar/create', data) + }, + + // 获取日历列表 + getCalendars() { + return api.post('/calendar/calendar/list', {}) + }, + + // 获取所有日历(包括已删除的,系统管理员专用) + getAllCalendars() { + return api.post('/calendar/calendar/list_all', {}) + }, + + // 更新日历 + updateCalendar(data) { + return api.post('/calendar/calendar/update', data) + }, + + // 删除日历 + deleteCalendar(id) { + return api.post('/calendar/calendar/delete', { id }) + }, + + // 恢复已删除的日历 + restoreCalendar(id) { + return api.post('/calendar/calendar/restore', { 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', { id }) + } +} diff --git a/frontend/ops_vue_js/src/components/AppHeader.vue b/frontend/ops_vue_js/src/components/AppHeader.vue index 504a485..bb00897 100644 --- a/frontend/ops_vue_js/src/components/AppHeader.vue +++ b/frontend/ops_vue_js/src/components/AppHeader.vue @@ -53,7 +53,8 @@ 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.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" }, @@ -143,6 +144,14 @@ const navItems = computed(() => [ v-if="userDropdownOpen" class="absolute right-0 z-50 mt-1 w-48 rounded-lg border border-gray-200 bg-white py-1 shadow-lg dark:border-dk-muted dark:bg-dk-card" > + + + {{ t("message.profile_information") }} + [
+ + + {{ t("message.profile_information") }} + */ -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 ef99ab5..30b61b6 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", @@ -33,7 +35,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", @@ -311,6 +314,7 @@ "standby": "Standby", "personal_holiday": "Personal Holiday", "public_holiday": "Public Holiday", + "event_type": "Event Type", "to": "To", "close": "Close", "copy": "Copy", @@ -394,7 +398,8 @@ "save_success": "Saved successfully", "submit": "Submit", "submitting": "Submitting...", - "loading": "Loading..." + "loading": "Loading...", + "profile_information": "Profile Information" }, "settings": { "cancel": "Cancel", @@ -434,7 +439,13 @@ "placeholder_name": "Enter your name", "placeholder_remark": "Personal introduction or remark", "name_hint": "Display only, login name unchanged.", - "remark_hint": "Remark only" + "remark_hint": "Remark only", + "edit_profile": "Edit Profile", + "security": "Security", + "security_description": "Manage your account security settings", + "my_groups": "My Groups", + "no_groups": "Not joined any groups yet", + "admin_panel": "Admin" }, "button": { "submit": "Submit", @@ -601,5 +612,58 @@ "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?", + "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", + "view_event": "View 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?", + "is_public_event": "Public Event", + "admin_title": "Calendar Admin", + "event_count": "Event Count", + "restore": "Restore", + "restore_success": "Restored successfully", + "deleted": "Deleted", + "confirm_restore": "Are you sure you want to restore this calendar?", + "confirm_restore_message": "Are you sure you want to restore calendar '{name}'?", + "admin_panel": "Admin", + "copy_event": "Copy Event", + "paste_event": "Paste Event", + "copy_success": "Event copied", + "paste_success": "Event pasted", + "no_event_to_paste": "No event to paste", + "created_by": "Created by {name}" } } diff --git a/frontend/ops_vue_js/src/i18n/zh-CN.json b/frontend/ops_vue_js/src/i18n/zh-CN.json index 4e27e17..6d1562b 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": "搜索", @@ -33,7 +35,8 @@ "purchase": "采购", "warehouse": "仓库", "warehouse_items": "物品总览", - "work_order": "工单" + "work_order": "工单", + "calendar": "日历" }, "tagadder": { "not_fund_item": "没有找到匹配项", @@ -311,6 +314,7 @@ "standby": "备用", "personal_holiday": "个人假期", "public_holiday": "公众假期", + "event_type": "日程类型", "to": "至", "close": "关闭", "copy": "复制", @@ -394,7 +398,8 @@ "save_success": "保存成功", "submit": "提交", "submitting": "提交中...", - "loading": "加载中..." + "loading": "加载中...", + "profile_information": "个人信息" }, "settings": { "cancel": "取消", @@ -434,7 +439,13 @@ "placeholder_name": "请输入您的姓名", "placeholder_remark": "个人简介或备注", "name_hint": "仅用于显示,不影响登录名", - "remark_hint": "仅备注" + "remark_hint": "仅备注", + "edit_profile": "编辑资料", + "security": "安全设置", + "security_description": "管理您的账户安全设置", + "my_groups": "我的群组", + "no_groups": "暂未加入任何群组", + "admin_panel": "管理" }, "button": { "submit": "提交", @@ -549,6 +560,7 @@ "module_purchase": "采购", "module_schedule": "日程", "module_warehouse": "仓库", + "module_calendar": "日历", "module_work_order": "工单", "action_create": "创建", "action_update": "更新", @@ -601,5 +613,58 @@ "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": "确定要删除此日历吗?", + "confirm_delete_message": "确定要删除日历「{name}」吗?此操作不可撤销。", + "loading": "加载中...", + "add_event": "添加事件", + "edit_event": "编辑事件", + "view_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": "确定要删除此事件吗?", + "is_public_event": "公共日程", + "admin_title": "日历管理", + "event_count": "日程数量", + "restore": "恢复", + "restore_success": "恢复成功", + "deleted": "已删除", + "confirm_restore": "确定要恢复此日历吗?", + "confirm_restore_message": "确定要恢复日历「{name}」吗?", + "admin_panel": "管理", + "copy_event": "复制日程", + "paste_event": "粘贴日程", + "copy_success": "日程已复制", + "paste_success": "日程已粘贴", + "no_event_to_paste": "没有可粘贴的日程", + "created_by": "{name} 创建" } } diff --git a/frontend/ops_vue_js/src/router/index.js b/frontend/ops_vue_js/src/router/index.js index 52c21f2..f791676 100644 --- a/frontend/ops_vue_js/src/router/index.js +++ b/frontend/ops_vue_js/src/router/index.js @@ -129,6 +129,27 @@ const router = createRouter({ name: 'customer-edit', component: () => import('@/views/customer/CustomerFormPage.vue'), }, + { + path: 'calendars/admin', + name: 'calendars-admin', + component: () => import('@/views/calendar/CalendarAdminList.vue'), + meta: { requireSysAdmin: true }, + }, + { + path: 'calendars', + name: 'calendars', + component: () => import('@/views/calendar/CalendarList.vue'), + }, + { + path: 'calendar/:id', + name: 'calendar-detail', + component: () => import('@/views/calendar/CalendarDetail.vue'), + }, + { + path: 'user/my', + name: 'user-my', + component: () => import('@/views/user/MyProfile.vue'), + }, ], }, @@ -184,9 +205,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/stores/user.js b/frontend/ops_vue_js/src/stores/user.js index d035ad7..7c5fd6a 100644 --- a/frontend/ops_vue_js/src/stores/user.js +++ b/frontend/ops_vue_js/src/stores/user.js @@ -41,6 +41,7 @@ export const useUserStore = defineStore('user', () => { const userInfo = ref(null) // TabUserInfo_ 详情 const userCookie = ref(null) // Cookie session const isLoggedIn = ref(false) + const groups = ref([]) // 用户加入的群组列表 // ── Getters ── const cookieValue = computed(() => userCookie.value?.Value ?? '') @@ -63,6 +64,14 @@ export const useUserStore = defineStore('user', () => { // 是否系统管理员(后端直接返回) const isSysAdmin = ref(false) + // 是否为日历管理员(在 calendar_admin 群组中) + const isCalendarAdmin = computed(() => + groups.value.some(g => g.name === 'calendar_admin') + ) + + // 用户加入的群组名称列表(计算属性) + const groupNames = computed(() => groups.value.map(g => g.name)) + // ── Actions ── function login(cookie) { userCookie.value = cookie @@ -86,6 +95,7 @@ export const useUserStore = defineStore('user', () => { user.value = null userInfo.value = null isSysAdmin.value = false + groups.value = [] isLoggedIn.value = false removeStorage(STORAGE_KEY_COOKIE) } @@ -98,6 +108,8 @@ export const useUserStore = defineStore('user', () => { userInfo.value = data.userInfo ?? null // 存储系统管理员状态 isSysAdmin.value = data.isSysAdmin === true + // 存储用户群组列表 + groups.value = data.groups ?? [] } } catch { // 拦截器已处理错误提示 @@ -123,6 +135,9 @@ export const useUserStore = defineStore('user', () => { userCookie, isLoggedIn, isSysAdmin, + isCalendarAdmin, + groups, + groupNames, cookieValue, avatarUrl, birthday, diff --git a/frontend/ops_vue_js/src/views/HomeView.vue b/frontend/ops_vue_js/src/views/HomeView.vue index e9a13c4..d07f034 100644 --- a/frontend/ops_vue_js/src/views/HomeView.vue +++ b/frontend/ops_vue_js/src/views/HomeView.vue @@ -131,19 +131,19 @@ onMounted(() => {

{{ t('message.welcome') }}

-
-
+ + -
+ -
+ -
+ +
diff --git a/frontend/ops_vue_js/src/views/calendar/CalendarAdminList.vue b/frontend/ops_vue_js/src/views/calendar/CalendarAdminList.vue new file mode 100644 index 0000000..7fc16e6 --- /dev/null +++ b/frontend/ops_vue_js/src/views/calendar/CalendarAdminList.vue @@ -0,0 +1,432 @@ + + + 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..9eb6186 --- /dev/null +++ b/frontend/ops_vue_js/src/views/calendar/CalendarDetail.vue @@ -0,0 +1,1008 @@ + + + + + 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..64826e9 --- /dev/null +++ b/frontend/ops_vue_js/src/views/calendar/CalendarList.vue @@ -0,0 +1,406 @@ + + + diff --git a/frontend/ops_vue_js/src/views/sysadmin/OperationLogsTab.vue b/frontend/ops_vue_js/src/views/sysadmin/OperationLogsTab.vue index 1c4e493..a8ad707 100644 --- a/frontend/ops_vue_js/src/views/sysadmin/OperationLogsTab.vue +++ b/frontend/ops_vue_js/src/views/sysadmin/OperationLogsTab.vue @@ -19,11 +19,13 @@ const totalPages = computed(() => Math.ceil(total.value / pageSize.value)) // 模块列表 const modules = [ { id: 'all', label: 'operation_logs.all' }, + { id: 'calendar', label: 'calendar.calendars' }, { id: 'customer', label: 'customer.title' }, { id: 'purchase', label: 'purchase.title' }, - { id: 'schedule', label: 'schedule.title' }, + // { id: 'schedule', label: 'schedule.title' }, { id: 'warehouse', label: 'warehouse.title' }, { id: 'work_order', label: 'work_order.title' }, + ] const activeModule = ref('all') @@ -83,6 +85,7 @@ function getModuleClass(module) { purchase: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400', schedule: 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-400', warehouse: 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-400', + calendar: 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-400', work_order: 'bg-cyan-100 text-cyan-800 dark:bg-cyan-900/30 dark:text-cyan-400', } return map[module] || 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400' diff --git a/frontend/ops_vue_js/src/views/sysadmin/SysAdminView.vue b/frontend/ops_vue_js/src/views/sysadmin/SysAdminView.vue index 4670c0d..617e9b3 100644 --- a/frontend/ops_vue_js/src/views/sysadmin/SysAdminView.vue +++ b/frontend/ops_vue_js/src/views/sysadmin/SysAdminView.vue @@ -31,6 +31,7 @@ const tabs = [ { id: 'logs', label: t('sysadmin.tab_logs') }, { id: 'operation_logs', label: t('sysadmin.tab_operation_logs') }, { id: 'customer', label: t('customer.title'), to: '/customer' }, + { id: 'calendar', label: t('calendar.admin_title'), to: '/calendars/admin' }, ] async function fetchSysAdmins() { diff --git a/frontend/ops_vue_js/src/views/user/MyProfile.vue b/frontend/ops_vue_js/src/views/user/MyProfile.vue new file mode 100644 index 0000000..187f1ad --- /dev/null +++ b/frontend/ops_vue_js/src/views/user/MyProfile.vue @@ -0,0 +1,122 @@ + + +