Signed-off-by: 吴文峰 <kevin@lmve.net>
This commit is contained in:
@@ -81,6 +81,7 @@ func main() {
|
|||||||
routers.ApiWorkOrderInit()
|
routers.ApiWorkOrderInit()
|
||||||
routers.ApiWarehouseInit()
|
routers.ApiWarehouseInit()
|
||||||
routers.ApiCustomerInit()
|
routers.ApiCustomerInit()
|
||||||
|
routers.ApiCalendarInit()
|
||||||
|
|
||||||
routers.BindsInit() //最后初始化绑定数据表
|
routers.BindsInit() //最后初始化绑定数据表
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ func ApiRoot(r *gin.RouterGroup) {
|
|||||||
ApiWarehouse(r.Group("/warehouse"))
|
ApiWarehouse(r.Group("/warehouse"))
|
||||||
ApiSysAdmin(r.Group("/admin"))
|
ApiSysAdmin(r.Group("/admin"))
|
||||||
ApiCustomer(r.Group("/customer"))
|
ApiCustomer(r.Group("/customer"))
|
||||||
|
ApiCalendar(r.Group("/calendar"))
|
||||||
r.GET("/", func(ctx *gin.Context) {
|
r.GET("/", func(ctx *gin.Context) {
|
||||||
ReturnJson(ctx, "apiOK", gin.H{
|
ReturnJson(ctx, "apiOK", gin.H{
|
||||||
"isOpsApiRoot": true,
|
"isOpsApiRoot": true,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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 } })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,6 +54,7 @@ const normalClass = "rounded-md px-3 py-2 text-sm font-medium text-gray-600 tran
|
|||||||
const navItems = computed(() => [
|
const navItems = computed(() => [
|
||||||
{ label: t("appname.home"), to: "/" },
|
{ 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.purchase"), to: "/purchase" },
|
||||||
{ label: t("appname.work_order"), to: "/work_order" },
|
{ label: t("appname.work_order"), to: "/work_order" },
|
||||||
{ label: t("appname.warehouse"), to: "/warehouse" },
|
{ label: t("appname.warehouse"), to: "/warehouse" },
|
||||||
|
|||||||
@@ -33,7 +33,8 @@
|
|||||||
"purchase": "Purchase",
|
"purchase": "Purchase",
|
||||||
"warehouse": "Warehouse",
|
"warehouse": "Warehouse",
|
||||||
"warehouse_items": "Items Overview",
|
"warehouse_items": "Items Overview",
|
||||||
"work_order": "Work Order"
|
"work_order": "Work Order",
|
||||||
|
"calendar": "Calendar"
|
||||||
},
|
},
|
||||||
"tagadder": {
|
"tagadder": {
|
||||||
"not_fund_item": "No matching items found",
|
"not_fund_item": "No matching items found",
|
||||||
@@ -601,5 +602,41 @@
|
|||||||
"created_by": "Created By",
|
"created_by": "Created By",
|
||||||
"not_found": "Customer not found",
|
"not_found": "Customer not found",
|
||||||
"related_customers": "Related Customers"
|
"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?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,8 @@
|
|||||||
"purchase": "采购",
|
"purchase": "采购",
|
||||||
"warehouse": "仓库",
|
"warehouse": "仓库",
|
||||||
"warehouse_items": "物品总览",
|
"warehouse_items": "物品总览",
|
||||||
"work_order": "工单"
|
"work_order": "工单",
|
||||||
|
"calendar": "日历"
|
||||||
},
|
},
|
||||||
"tagadder": {
|
"tagadder": {
|
||||||
"not_fund_item": "没有找到匹配项",
|
"not_fund_item": "没有找到匹配项",
|
||||||
@@ -601,5 +602,41 @@
|
|||||||
"created_by": "创建者",
|
"created_by": "创建者",
|
||||||
"not_found": "客户不存在",
|
"not_found": "客户不存在",
|
||||||
"related_customers": "关联客户"
|
"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": "确定要删除此事件吗?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,6 +129,16 @@ const router = createRouter({
|
|||||||
name: 'customer-edit',
|
name: 'customer-edit',
|
||||||
component: () => import('@/views/customer/CustomerFormPage.vue'),
|
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'),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,492 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, watch, onMounted, onBeforeUnmount, nextTick } from "vue"
|
||||||
|
import { useRoute, useRouter } from "vue-router"
|
||||||
|
import FullCalendar from "@fullcalendar/vue3"
|
||||||
|
import dayGridPlugin from "@fullcalendar/daygrid"
|
||||||
|
import timeGridPlugin from "@fullcalendar/timegrid"
|
||||||
|
import interactionPlugin from "@fullcalendar/interaction"
|
||||||
|
import listPlugin from "@fullcalendar/list"
|
||||||
|
import { useI18n } from "vue-i18n"
|
||||||
|
import { usePageTitle } from "@/composables/usePageTitle"
|
||||||
|
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"
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const { t, locale } = useI18n()
|
||||||
|
const toast = useToastStore()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const DateUtils = useDateUtils()
|
||||||
|
|
||||||
|
const calendarId = ref(parseInt(route.params.id))
|
||||||
|
const calendarInfo = ref({})
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
usePageTitle('appname.calendar_detail')
|
||||||
|
|
||||||
|
const calendarRef = ref(null)
|
||||||
|
const calendarNowShow = ref()
|
||||||
|
|
||||||
|
const showModal = ref(false)
|
||||||
|
const eventData = ref({
|
||||||
|
id: 0,
|
||||||
|
title: "",
|
||||||
|
startDate: "",
|
||||||
|
endDate: "",
|
||||||
|
color: "#3788d9",
|
||||||
|
remark: "",
|
||||||
|
isEditing: 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") },
|
||||||
|
])
|
||||||
|
|
||||||
|
const pageData = ref({
|
||||||
|
seleEventID: 0,
|
||||||
|
lastClickTime: 0,
|
||||||
|
lastClickTimeStr: "",
|
||||||
|
lastEventClickTime: 0,
|
||||||
|
lastEventClickID: 0,
|
||||||
|
submitChecked: false
|
||||||
|
})
|
||||||
|
|
||||||
|
function openEventModal(startDate, endDate) {
|
||||||
|
eventData.value = {
|
||||||
|
id: 0,
|
||||||
|
title: "",
|
||||||
|
startDate: startDate || "",
|
||||||
|
endDate: endDate || "",
|
||||||
|
color: calendarInfo.value.Color || "#3788d9",
|
||||||
|
remark: "",
|
||||||
|
isEditing: false,
|
||||||
|
isEditable: true
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
showModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveEvent() {
|
||||||
|
if (!eventData.value.title.trim()) {
|
||||||
|
toast.error(t('calendar.event_title_required'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pageData.value.submitChecked = true
|
||||||
|
|
||||||
|
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
|
||||||
|
})
|
||||||
|
} 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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.errCode === 0) {
|
||||||
|
toast.success(t('calendar.event_save_success'))
|
||||||
|
showModal.value = false
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await calendarApi.deleteEvent(eventData.value.id)
|
||||||
|
if (result.errCode === 0) {
|
||||||
|
toast.success(t('calendar.event_delete_success'))
|
||||||
|
showModal.value = false
|
||||||
|
getEvents()
|
||||||
|
} else {
|
||||||
|
toast.error(t('message.server_error'))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 拦截器已处理
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { errCode, data } = await calendarApi.getEvents({
|
||||||
|
calendar_id: calendarId.value,
|
||||||
|
start: start,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 拦截器已处理
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchCalendarInfo() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const { errCode, data } = await calendarApi.getCalendars()
|
||||||
|
if (errCode === 0) {
|
||||||
|
const calendar = (data.list || []).find(c => c.ID === calendarId.value)
|
||||||
|
if (calendar) {
|
||||||
|
calendarInfo.value = calendar
|
||||||
|
} else {
|
||||||
|
toast.error(t('calendar.calendar_not_found'))
|
||||||
|
router.push('/calendars')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 拦截器已处理
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unseleEvent(eventID) {
|
||||||
|
const target = calendarOptions.value.events.find(item => item.id === eventID)
|
||||||
|
if (target) {
|
||||||
|
target.borderColor = target.backgroundColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unseleEventAll() {
|
||||||
|
unseleEvent(pageData.value.seleEventID)
|
||||||
|
pageData.value.seleEventID = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const calendarOptions = ref({
|
||||||
|
height: "100%",
|
||||||
|
contentHeight: "auto",
|
||||||
|
locale: locale.value,
|
||||||
|
plugins: [dayGridPlugin, timeGridPlugin, interactionPlugin, listPlugin],
|
||||||
|
nowIndicator: true,
|
||||||
|
weekends: true,
|
||||||
|
initialView: "dayGridMonth",
|
||||||
|
selectable: true,
|
||||||
|
editable: true,
|
||||||
|
dayMaxEvents: true,
|
||||||
|
firstDay: 1,
|
||||||
|
expandRows: true,
|
||||||
|
stickyHeaderDates: true,
|
||||||
|
|
||||||
|
headerToolbar: {
|
||||||
|
left: "prevYear,prev,today,next,nextYear",
|
||||||
|
center: "title",
|
||||||
|
right: ""
|
||||||
|
},
|
||||||
|
|
||||||
|
customButtons: {
|
||||||
|
prevYear: {
|
||||||
|
text: t("schedule.previous_year"),
|
||||||
|
click() {
|
||||||
|
calendarRef.value.getApi().prevYear()
|
||||||
|
getEvents()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
nextYear: {
|
||||||
|
text: t("schedule.next_year"),
|
||||||
|
click() {
|
||||||
|
calendarRef.value.getApi().nextYear()
|
||||||
|
getEvents()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
prev: {
|
||||||
|
text: t("schedule.previous_month"),
|
||||||
|
click() {
|
||||||
|
calendarRef.value.getApi().prev()
|
||||||
|
getEvents()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
next: {
|
||||||
|
text: t("schedule.next_month"),
|
||||||
|
click() {
|
||||||
|
calendarRef.value.getApi().next()
|
||||||
|
getEvents()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
today: {
|
||||||
|
text: t("schedule.today"),
|
||||||
|
click() {
|
||||||
|
calendarRef.value.getApi().today()
|
||||||
|
getEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
events: [],
|
||||||
|
|
||||||
|
datesSet(info) {
|
||||||
|
calendarNowShow.value = info
|
||||||
|
getEvents()
|
||||||
|
},
|
||||||
|
|
||||||
|
dateClick(info) {
|
||||||
|
const nowTime = new Date().getTime()
|
||||||
|
const timeDifference = nowTime - pageData.value.lastClickTime
|
||||||
|
|
||||||
|
unseleEventAll()
|
||||||
|
|
||||||
|
if (info.dateStr === pageData.value.lastClickTimeStr) {
|
||||||
|
if (timeDifference < 400 && timeDifference > 0) {
|
||||||
|
if (userStore.isLoggedIn) {
|
||||||
|
openEventModal(info.dateStr, info.dateStr)
|
||||||
|
} else {
|
||||||
|
toast.warning(t("message.login_to_your_account"))
|
||||||
|
router.replace("/login?redirect=/calendar/" + calendarId.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pageData.value.lastClickTimeStr = info.dateStr
|
||||||
|
pageData.value.lastClickTime = nowTime
|
||||||
|
},
|
||||||
|
|
||||||
|
select(info) {
|
||||||
|
if (info.end - info.start > 86400000) {
|
||||||
|
if (userStore.isLoggedIn) {
|
||||||
|
openEventModal(info.startStr, info.endStr)
|
||||||
|
} else {
|
||||||
|
toast.warning(t("message.login_to_your_account"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
eventClick(info) {
|
||||||
|
const nowTime = new Date().getTime()
|
||||||
|
const timeDifference = nowTime - pageData.value.lastEventClickTime
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchCalendarInfo()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-6 px-6 py-6">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
@click="router.push('/calendars')"
|
||||||
|
class="rounded-lg p-2 text-gray-600 transition-colors hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-dk-muted"
|
||||||
|
>
|
||||||
|
←
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
{{ calendarInfo.Name || t('calendar.loading') }}
|
||||||
|
</h3>
|
||||||
|
<p v-if="calendarInfo.Description" class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{{ calendarInfo.Description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="userStore.isLoggedIn"
|
||||||
|
class="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
@click="openEventModal()"
|
||||||
|
class="inline-flex items-center gap-1.5 rounded-lg bg-blue-600 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
<IconPlus :size="16" />
|
||||||
|
{{ t('calendar.add_event') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-xl border border-gray-200 bg-white shadow-lg dark:border-dk-muted dark:bg-dk-card">
|
||||||
|
<FullCalendar
|
||||||
|
ref="calendarRef"
|
||||||
|
:options="calendarOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Event Modal -->
|
||||||
|
<div
|
||||||
|
v-if="showModal"
|
||||||
|
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
|
||||||
|
@click.self="showModal = false"
|
||||||
|
>
|
||||||
|
<div class="w-full max-w-md rounded-xl bg-white p-6 shadow-xl dark:bg-dk-card">
|
||||||
|
<h3 class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
{{ eventData.isEditing ? t('calendar.edit_event') : t('calendar.add_event') }}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
{{ t('calendar.event_title') }} *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model="eventData.title"
|
||||||
|
type="text"
|
||||||
|
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 outline-none transition-colors focus:border-blue-500 dark:border-dk-muted dark:bg-dk-base dark:text-white"
|
||||||
|
:placeholder="t('calendar.event_title_placeholder')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4 grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
{{ t('calendar.start_date') }} *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model="eventData.startDate"
|
||||||
|
type="date"
|
||||||
|
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 outline-none transition-colors focus:border-blue-500 dark:border-dk-muted dark:bg-dk-base dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
{{ t('calendar.end_date') }} *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model="eventData.endDate"
|
||||||
|
type="date"
|
||||||
|
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 outline-none transition-colors focus:border-blue-500 dark:border-dk-muted dark:bg-dk-base dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
{{ t('calendar.color') }}
|
||||||
|
</label>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button
|
||||||
|
v-for="color in colorOptions"
|
||||||
|
:key="color.value"
|
||||||
|
@click="eventData.color = color.value"
|
||||||
|
class="h-8 w-8 rounded-full transition-transform hover:scale-110"
|
||||||
|
:class="{ 'ring-2 ring-blue-500 ring-offset-2': eventData.color === color.value }"
|
||||||
|
:style="{ backgroundColor: color.value }"
|
||||||
|
:title="color.label"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
{{ t('calendar.remark') }}
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
v-model="eventData.remark"
|
||||||
|
rows="3"
|
||||||
|
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 outline-none transition-colors focus:border-blue-500 dark:border-dk-muted dark:bg-dk-base dark:text-white"
|
||||||
|
:placeholder="t('calendar.remark_placeholder')"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<button
|
||||||
|
v-if="eventData.isEditing"
|
||||||
|
@click="deleteEvent"
|
||||||
|
class="inline-flex items-center gap-1 rounded-lg bg-red-600 px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-red-700"
|
||||||
|
>
|
||||||
|
<IconTrash :size="16" />
|
||||||
|
{{ t('delete') }}
|
||||||
|
</button>
|
||||||
|
<div v-else></div>
|
||||||
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button
|
||||||
|
@click="showModal = false"
|
||||||
|
class="rounded-lg border border-gray-300 px-4 py-2 text-sm text-gray-600 transition-colors hover:bg-gray-50 dark:border-dk-muted dark:text-gray-300 dark:hover:bg-dk-muted"
|
||||||
|
>
|
||||||
|
{{ t('cancel') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="saveEvent"
|
||||||
|
:disabled="pageData.submitChecked"
|
||||||
|
class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{{ t('save') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,376 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useToastStore } from '@/stores/toast'
|
||||||
|
import { usePageTitle } from '@/composables/usePageTitle'
|
||||||
|
import { calendarApi } from '@/api/calendar'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import { IconPlus, IconCalendar, IconTrash, IconEdit } from '@tabler/icons-vue'
|
||||||
|
|
||||||
|
usePageTitle('appname.calendar')
|
||||||
|
const { t } = useI18n()
|
||||||
|
const router = useRouter()
|
||||||
|
const toast = useToastStore()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
const calendars = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const showCreateModal = ref(false)
|
||||||
|
const showEditModal = ref(false)
|
||||||
|
const editingCalendar = ref(null)
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
color: '#3788d9',
|
||||||
|
is_public: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const colorOptions = [
|
||||||
|
'#3788d9',
|
||||||
|
'#06d6a0',
|
||||||
|
'#ff595e',
|
||||||
|
'#ffca3a',
|
||||||
|
'#8b5cf6',
|
||||||
|
'#ec4899'
|
||||||
|
]
|
||||||
|
|
||||||
|
async function fetchCalendars() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const { errCode, data } = await calendarApi.getCalendars()
|
||||||
|
if (errCode === 0) {
|
||||||
|
calendars.value = data.list || []
|
||||||
|
} else {
|
||||||
|
toast.error(t('message.server_error'))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 拦截器已处理
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreateModal() {
|
||||||
|
form.value = { name: '', description: '', color: '#3788d9', is_public: false }
|
||||||
|
showCreateModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditModal(calendar) {
|
||||||
|
editingCalendar.value = calendar
|
||||||
|
form.value = {
|
||||||
|
name: calendar.Name,
|
||||||
|
description: calendar.Description || '',
|
||||||
|
color: calendar.Color,
|
||||||
|
is_public: calendar.IsPublic
|
||||||
|
}
|
||||||
|
showEditModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createCalendar() {
|
||||||
|
if (!form.value.name.trim()) {
|
||||||
|
toast.error(t('calendar.name_required'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { errCode, data } = await calendarApi.createCalendar(form.value)
|
||||||
|
if (errCode === 0) {
|
||||||
|
toast.success(t('calendar.create_success'))
|
||||||
|
showCreateModal.value = false
|
||||||
|
fetchCalendars()
|
||||||
|
} else {
|
||||||
|
toast.error(t('message.server_error'))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 拦截器已处理
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateCalendar() {
|
||||||
|
if (!form.value.name.trim()) {
|
||||||
|
toast.error(t('calendar.name_required'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { errCode } = await calendarApi.updateCalendar({
|
||||||
|
id: editingCalendar.value.ID,
|
||||||
|
...form.value
|
||||||
|
})
|
||||||
|
if (errCode === 0) {
|
||||||
|
toast.success(t('calendar.update_success'))
|
||||||
|
showEditModal.value = false
|
||||||
|
fetchCalendars()
|
||||||
|
} else {
|
||||||
|
toast.error(t('message.server_error'))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 拦截器已处理
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteCalendar(calendar) {
|
||||||
|
if (!confirm(t('calendar.confirm_delete'))) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { errCode } = await calendarApi.deleteCalendar(calendar.ID)
|
||||||
|
if (errCode === 0) {
|
||||||
|
toast.success(t('calendar.delete_success'))
|
||||||
|
fetchCalendars()
|
||||||
|
} else {
|
||||||
|
toast.error(t('message.server_error'))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 拦截器已处理
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToCalendar(id) {
|
||||||
|
router.push(`/calendar/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(fetchCalendars)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="mx-auto max-w-6xl px-6 py-6">
|
||||||
|
<div class="flex flex-col gap-6 rounded-xl border border-gray-200 bg-white shadow-lg dark:border-dk-muted dark:bg-dk-card">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex items-center justify-between border-b border-gray-100 px-6 py-4 dark:border-dk-muted">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">{{ t('calendar.calendars') }}</h3>
|
||||||
|
<button
|
||||||
|
@click="openCreateModal"
|
||||||
|
class="inline-flex items-center gap-1.5 rounded-lg bg-blue-600 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
<IconPlus :size="16" />
|
||||||
|
{{ t('calendar.create_calendar') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Calendar List -->
|
||||||
|
<div class="px-6 py-3">
|
||||||
|
<div v-if="loading" class="py-8 text-center text-gray-400">
|
||||||
|
<svg class="mx-auto mb-2 h-5 w-5 animate-spin text-gray-400" viewBox="0 0 24 24" fill="none">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||||
|
</svg>
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="calendars.length === 0" class="py-8 text-center text-gray-400">
|
||||||
|
{{ t('calendar.no_calendars') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
<div
|
||||||
|
v-for="calendar in calendars"
|
||||||
|
:key="calendar.ID"
|
||||||
|
class="cursor-pointer rounded-lg border border-gray-200 p-4 transition-colors hover:bg-gray-50 dark:border-dk-muted dark:hover:bg-dk-base"
|
||||||
|
@click="goToCalendar(calendar.ID)"
|
||||||
|
>
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
class="h-4 w-4 rounded-full"
|
||||||
|
:style="{ backgroundColor: calendar.Color }"
|
||||||
|
></div>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-medium text-gray-900 dark:text-white">{{ calendar.Name }}</h4>
|
||||||
|
<p v-if="calendar.Description" class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{{ calendar.Description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-1" @click.stop>
|
||||||
|
<button
|
||||||
|
v-if="calendar.UserID === userStore.userInfo.ID"
|
||||||
|
@click="openEditModal(calendar)"
|
||||||
|
class="rounded p-1 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-dk-muted"
|
||||||
|
>
|
||||||
|
<IconEdit :size="16" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="calendar.UserID === userStore.userInfo.ID"
|
||||||
|
@click="deleteCalendar(calendar)"
|
||||||
|
class="rounded p-1 text-gray-400 transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-dk-muted"
|
||||||
|
>
|
||||||
|
<IconTrash :size="16" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3 flex items-center gap-2 text-xs text-gray-400">
|
||||||
|
<IconCalendar :size="14" />
|
||||||
|
<span>{{ calendar.IsPublic ? t('calendar.public') : t('calendar.private') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Create Calendar Modal -->
|
||||||
|
<div
|
||||||
|
v-if="showCreateModal"
|
||||||
|
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
|
||||||
|
@click.self="showCreateModal = false"
|
||||||
|
>
|
||||||
|
<div class="w-full max-w-md rounded-xl bg-white p-6 shadow-xl dark:bg-dk-card">
|
||||||
|
<h3 class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
{{ t('calendar.create_calendar') }}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
{{ t('calendar.name') }} *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model="form.name"
|
||||||
|
type="text"
|
||||||
|
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 outline-none transition-colors focus:border-blue-500 dark:border-dk-muted dark:bg-dk-base dark:text-white"
|
||||||
|
:placeholder="t('calendar.name_placeholder')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
{{ t('calendar.description') }}
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
v-model="form.description"
|
||||||
|
rows="3"
|
||||||
|
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 outline-none transition-colors focus:border-blue-500 dark:border-dk-muted dark:bg-dk-base dark:text-white"
|
||||||
|
:placeholder="t('calendar.description_placeholder')"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
{{ t('calendar.color') }}
|
||||||
|
</label>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button
|
||||||
|
v-for="color in colorOptions"
|
||||||
|
:key="color"
|
||||||
|
@click="form.color = color"
|
||||||
|
class="h-8 w-8 rounded-full transition-transform hover:scale-110"
|
||||||
|
:class="{ 'ring-2 ring-blue-500 ring-offset-2': form.color === color }"
|
||||||
|
:style="{ backgroundColor: color }"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6 flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
id="is_public"
|
||||||
|
v-model="form.is_public"
|
||||||
|
type="checkbox"
|
||||||
|
class="rounded border-gray-300"
|
||||||
|
/>
|
||||||
|
<label for="is_public" class="text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
{{ t('calendar.is_public') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
@click="showCreateModal = false"
|
||||||
|
class="rounded-lg border border-gray-300 px-4 py-2 text-sm text-gray-600 transition-colors hover:bg-gray-50 dark:border-dk-muted dark:text-gray-300 dark:hover:bg-dk-muted"
|
||||||
|
>
|
||||||
|
{{ t('cancel') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="createCalendar"
|
||||||
|
class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
{{ t('create') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit Calendar Modal -->
|
||||||
|
<div
|
||||||
|
v-if="showEditModal"
|
||||||
|
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
|
||||||
|
@click.self="showEditModal = false"
|
||||||
|
>
|
||||||
|
<div class="w-full max-w-md rounded-xl bg-white p-6 shadow-xl dark:bg-dk-card">
|
||||||
|
<h3 class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
{{ t('calendar.edit_calendar') }}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
{{ t('calendar.name') }} *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model="form.name"
|
||||||
|
type="text"
|
||||||
|
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 outline-none transition-colors focus:border-blue-500 dark:border-dk-muted dark:bg-dk-base dark:text-white"
|
||||||
|
:placeholder="t('calendar.name_placeholder')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
{{ t('calendar.description') }}
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
v-model="form.description"
|
||||||
|
rows="3"
|
||||||
|
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 outline-none transition-colors focus:border-blue-500 dark:border-dk-muted dark:bg-dk-base dark:text-white"
|
||||||
|
:placeholder="t('calendar.description_placeholder')"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
{{ t('calendar.color') }}
|
||||||
|
</label>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button
|
||||||
|
v-for="color in colorOptions"
|
||||||
|
:key="color"
|
||||||
|
@click="form.color = color"
|
||||||
|
class="h-8 w-8 rounded-full transition-transform hover:scale-110"
|
||||||
|
:class="{ 'ring-2 ring-blue-500 ring-offset-2': form.color === color }"
|
||||||
|
:style="{ backgroundColor: color }"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6 flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
id="edit_is_public"
|
||||||
|
v-model="form.is_public"
|
||||||
|
type="checkbox"
|
||||||
|
class="rounded border-gray-300"
|
||||||
|
/>
|
||||||
|
<label for="edit_is_public" class="text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
{{ t('calendar.is_public') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
@click="showEditModal = false"
|
||||||
|
class="rounded-lg border border-gray-300 px-4 py-2 text-sm text-gray-600 transition-colors hover:bg-gray-50 dark:border-dk-muted dark:text-gray-300 dark:hover:bg-dk-muted"
|
||||||
|
>
|
||||||
|
{{ t('cancel') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="updateCalendar"
|
||||||
|
class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
{{ t('save') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user