diff --git a/backend/internal/api/handlers/announcement.go b/backend/internal/api/handlers/announcement.go new file mode 100644 index 0000000..bf0cfbc --- /dev/null +++ b/backend/internal/api/handlers/announcement.go @@ -0,0 +1,123 @@ +package handlers + +import ( + "net/http" + "strconv" + + "sale/internal/models" + "sale/internal/utils" + + "github.com/gin-gonic/gin" +) + +type AnnouncementHandler struct{} + +func NewAnnouncementHandler() *AnnouncementHandler { + return &AnnouncementHandler{} +} + +// List 用户端接口 - 只返回激活的公告 +func (h *AnnouncementHandler) List(c *gin.Context) { + var announcements []models.Announcement + utils.DB.Where("is_active = ?", true). + Order("sort_order ASC, created_at DESC"). + Find(&announcements) + c.JSON(http.StatusOK, gin.H{"data": announcements}) +} + +// AdminList 管理端接口 - 返回所有公告 +func (h *AnnouncementHandler) AdminList(c *gin.Context) { + var announcements []models.Announcement + utils.DB.Order("sort_order ASC, created_at DESC").Find(&announcements) + c.JSON(http.StatusOK, gin.H{"data": announcements}) +} + +// Create 创建公告 +func (h *AnnouncementHandler) Create(c *gin.Context) { + var req struct { + Title string `json:"title" binding:"required"` + Content string `json:"content"` + SortOrder int `json:"sort_order"` + IsActive bool `json:"is_active"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "标题不能为空"}) + return + } + + announcement := models.Announcement{ + Title: req.Title, + Content: req.Content, + SortOrder: req.SortOrder, + IsActive: req.IsActive, + } + utils.DB.Create(&announcement) + c.JSON(http.StatusCreated, gin.H{"data": announcement}) +} + +// Update 更新公告 +func (h *AnnouncementHandler) Update(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"}) + return + } + + var announcement models.Announcement + if err := utils.DB.First(&announcement, id).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "公告不存在"}) + return + } + + var req struct { + Title string `json:"title"` + Content string `json:"content"` + SortOrder int `json:"sort_order"` + IsActive bool `json:"is_active"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) + return + } + + announcement.Title = req.Title + announcement.Content = req.Content + announcement.SortOrder = req.SortOrder + announcement.IsActive = req.IsActive + utils.DB.Save(&announcement) + c.JSON(http.StatusOK, gin.H{"data": announcement}) +} + +// Delete 删除公告 +func (h *AnnouncementHandler) Delete(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"}) + return + } + + if err := utils.DB.Delete(&models.Announcement{}, id).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "删除失败"}) + return + } + c.JSON(http.StatusOK, gin.H{"message": "删除成功"}) +} + +// ToggleActive 切换激活状态 +func (h *AnnouncementHandler) ToggleActive(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"}) + return + } + + var announcement models.Announcement + if err := utils.DB.First(&announcement, id).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "公告不存在"}) + return + } + + announcement.IsActive = !announcement.IsActive + utils.DB.Save(&announcement) + c.JSON(http.StatusOK, gin.H{"data": announcement}) +} \ No newline at end of file diff --git a/backend/internal/api/routes/routes.go b/backend/internal/api/routes/routes.go index 5a640a1..aa09a0a 100644 --- a/backend/internal/api/routes/routes.go +++ b/backend/internal/api/routes/routes.go @@ -28,6 +28,7 @@ func SetupRoutes(r *gin.Engine) { logHandler := handlers.NewLogHandler() purchaseGroupHandler := handlers.NewPurchaseGroupHandler() shippingTemplateHandler := handlers.NewShippingTemplateHandler() + announcementHandler := handlers.NewAnnouncementHandler() r.Use(middlewares.CORSMiddleware()) @@ -62,6 +63,7 @@ func SetupRoutes(r *gin.Engine) { } api.GET("/banners", bannerHandler.List) + api.GET("/announcements", announcementHandler.List) api.GET("/categories", categoryHandler.List) api.GET("/brands", brandHandler.List) @@ -262,6 +264,15 @@ func SetupRoutes(r *gin.Engine) { adminShippingTemplates.PUT("/:id", shippingTemplateHandler.Update) adminShippingTemplates.DELETE("/:id", shippingTemplateHandler.Delete) } + + adminAnnouncements := admin.Group("/announcements") + { + adminAnnouncements.GET("", announcementHandler.AdminList) + adminAnnouncements.POST("", announcementHandler.Create) + adminAnnouncements.PUT("/:id", announcementHandler.Update) + adminAnnouncements.DELETE("/:id", announcementHandler.Delete) + adminAnnouncements.PUT("/:id/toggle", announcementHandler.ToggleActive) + } } } } diff --git a/backend/internal/models/announcement.go b/backend/internal/models/announcement.go new file mode 100644 index 0000000..55acaa2 --- /dev/null +++ b/backend/internal/models/announcement.go @@ -0,0 +1,22 @@ +package models + +import ( + "time" + + "gorm.io/gorm" +) + +type Announcement struct { + ID uint `gorm:"primaryKey" json:"id"` + Title string `gorm:"size:255;not null" json:"title"` + Content string `gorm:"type:text" json:"content"` + SortOrder int `gorm:"default:0" json:"sort_order"` + IsActive bool `gorm:"default:true" json:"is_active"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` +} + +func (Announcement) TableName() string { + return "announcements" +} \ No newline at end of file diff --git a/backend/internal/utils/database.go b/backend/internal/utils/database.go index 9433d41..38216dc 100644 --- a/backend/internal/utils/database.go +++ b/backend/internal/utils/database.go @@ -82,6 +82,7 @@ func AutoMigrate() { &models.Article{}, &models.Banner{}, &models.PaymentChannel{}, + &models.Announcement{}, ) if err != nil { log.Fatalf("Failed to migrate database: %v", err) diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 51177f0..baaebdf 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -91,6 +91,10 @@ export const bannerApi = { list: () => api.get('/banners'), } +export const announcementApi = { + list: () => api.get('/announcements'), +} + export const lotteryApi = { list: () => api.get('/lotteries'), getById: (id: number) => api.get(`/lotteries/${id}`), @@ -201,4 +205,11 @@ export const adminApi = { createShippingTemplate: (data: any) => api.post('/admin/shipping-templates', data), updateShippingTemplate: (id: number, data: any) => api.put(`/admin/shipping-templates/${id}`, data), deleteShippingTemplate: (id: number) => api.delete(`/admin/shipping-templates/${id}`), + + // 公告 + getAnnouncements: () => api.get('/admin/announcements'), + createAnnouncement: (data: any) => api.post('/admin/announcements', data), + updateAnnouncement: (id: number, data: any) => api.put(`/admin/announcements/${id}`, data), + deleteAnnouncement: (id: number) => api.delete(`/admin/announcements/${id}`), + toggleAnnouncement: (id: number) => api.put(`/admin/announcements/${id}/toggle`), } diff --git a/frontend/src/layouts/AdminLayout.vue b/frontend/src/layouts/AdminLayout.vue index e50662d..2316326 100644 --- a/frontend/src/layouts/AdminLayout.vue +++ b/frontend/src/layouts/AdminLayout.vue @@ -57,6 +57,10 @@ {{ $t('admin.bannerManagement') }} + + + 公告管理 + {{ $t('admin.paymentManagement') }} @@ -169,7 +173,7 @@ import { ref, computed } from 'vue' import { useRouter, useRoute } from 'vue-router' import { useI18n } from 'vue-i18n' import logoImg from '../assets/logo.png' -import { DataAnalysis, User, Folder, Grid, Stamp, Goods, List, Van, Trophy, ChatDotSquare, Setting, Document, Picture, Compass, Fold, Expand, HomeFilled, FullScreen, ArrowDown, SwitchButton, ArrowRight, Wallet, Notebook, Box } from '@element-plus/icons-vue' +import { DataAnalysis, User, Folder, Grid, Stamp, Goods, List, Van, Trophy, ChatDotSquare, Setting, Document, Picture, Compass, Fold, Expand, HomeFilled, FullScreen, ArrowDown, SwitchButton, ArrowRight, Wallet, Notebook, Box, Bell } from '@element-plus/icons-vue' import { useUserStore } from '../store/user' import { useCartStore } from '../store/cart' diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 6fe66c4..ca5fc95 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -51,6 +51,7 @@ const routes = [ { path: 'settings', name: 'AdminSettings', component: () => import('../views/admin/Settings.vue') }, { path: 'articles', name: 'AdminArticles', component: () => import('../views/admin/Articles.vue') }, { path: 'banners', name: 'AdminBanners', component: () => import('../views/admin/Banners.vue') }, + { path: 'announcements', name: 'AdminAnnouncements', component: () => import('../views/admin/Announcements.vue') }, { path: 'payment', name: 'AdminPayment', component: () => import('../views/admin/PaymentChannels.vue') }, { path: 'logs', name: 'AdminLogs', component: () => import('../views/admin/Logs.vue') }, { path: 'shipping-templates', name: 'AdminShippingTemplates', component: () => import('../views/admin/ShippingTemplates.vue') }, diff --git a/frontend/src/views/admin/Announcements.vue b/frontend/src/views/admin/Announcements.vue new file mode 100644 index 0000000..602eecb --- /dev/null +++ b/frontend/src/views/admin/Announcements.vue @@ -0,0 +1,184 @@ + + + + + diff --git a/frontend/src/views/user/Home.vue b/frontend/src/views/user/Home.vue index d499d78..98bf8ad 100644 --- a/frontend/src/views/user/Home.vue +++ b/frontend/src/views/user/Home.vue @@ -1,5 +1,19 @@