From 3a898e8aa02da3f5682626a77d2cd7312093892e Mon Sep 17 00:00:00 2001 From: admin Date: Wed, 6 May 2026 09:16:34 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=85=A8=E9=9D=A2=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端修复: - 验证码存储与校验机制 - 订单创建事务+库存扣减 - GetStats字段名错误 - VerifyEmail改为POST - 供应商更新字段白名单 - 文件删除安全检查 - 抽奖安全随机数 - 用户管理CRUD - 订单取消/确认收货 - 工单回复 - Toggle返回新数据 - 移除死代码 前端修复: - 404兜底路由 - 401软跳转 - API层统一 - 面包屑补充banners - 国际化完善 - 购物车并行删除 - 退出清理购物车 - 供应商Dashboard数据 - 工单详情页 - 订单取消/确认收货 --- .gitignore | 2 + backend/cmd/server/sale.db | Bin 233472 -> 0 bytes backend/internal/api/handlers/article.go | 1 + backend/internal/api/handlers/auth.go | 111 +++ backend/internal/api/handlers/banner.go | 141 +++ backend/internal/api/handlers/lottery.go | 11 +- backend/internal/api/handlers/order.go | 73 +- backend/internal/api/handlers/system.go | 24 +- backend/internal/api/handlers/ticket.go | 33 + backend/internal/api/handlers/upload.go | 52 +- backend/internal/api/routes/routes.go | 27 +- backend/internal/models/banner.go | 26 + backend/internal/models/lottery.go | 1 + backend/internal/utils/database.go | 1 + frontend/src/api/index.ts | 29 +- frontend/src/i18n/locales/en.json | 2 + frontend/src/i18n/locales/ja.json | 2 + frontend/src/i18n/locales/zh.json | 2 + frontend/src/layouts/AdminLayout.vue | 181 +++- frontend/src/layouts/SupplierLayout.vue | 4 +- .../src/layouts/{scheme4 => }/UserLayout.vue | 22 +- frontend/src/router/index.ts | 11 +- frontend/src/store/cart.ts | 30 +- frontend/src/store/user.ts | 12 +- frontend/src/utils/request.ts | 4 +- frontend/src/views/NotFound.vue | 43 + frontend/src/views/admin/Articles.vue | 84 +- frontend/src/views/admin/Banners.vue | 331 +++++++ frontend/src/views/admin/Categories.vue | 2 - frontend/src/views/admin/Dashboard.vue | 105 +- frontend/src/views/admin/Lotteries.vue | 87 +- frontend/src/views/admin/Orders.vue | 2 - frontend/src/views/admin/Products.vue | 2 - frontend/src/views/admin/Settings.vue | 5 - frontend/src/views/admin/Suppliers.vue | 2 - frontend/src/views/admin/Tickets.vue | 5 - frontend/src/views/admin/Users.vue | 95 +- frontend/src/views/scheme4/Home.vue | 523 ---------- frontend/src/views/supplier/Dashboard.vue | 81 +- frontend/src/views/supplier/Inventory.vue | 23 +- frontend/src/views/supplier/Orders.vue | 45 +- frontend/src/views/user/Cart.vue | 2 - frontend/src/views/user/Home.vue | 902 ++++++++++++++---- frontend/src/views/user/Orders.vue | 37 +- frontend/src/views/user/Products.vue | 3 +- frontend/src/views/user/TicketDetail.vue | 166 ++++ frontend/src/views/user/Tickets.vue | 7 +- scripts/import_products.py | 2 +- scripts/insert_banners.py | 81 ++ scripts/insert_lotteries_articles.py | 207 ++++ scripts/insert_test_data.py | 53 + scripts/reset_admin_password.py | 30 + 52 files changed, 2807 insertions(+), 920 deletions(-) delete mode 100644 backend/cmd/server/sale.db create mode 100644 backend/internal/api/handlers/banner.go create mode 100644 backend/internal/models/banner.go rename frontend/src/layouts/{scheme4 => }/UserLayout.vue (96%) create mode 100644 frontend/src/views/NotFound.vue create mode 100644 frontend/src/views/admin/Banners.vue delete mode 100644 frontend/src/views/scheme4/Home.vue create mode 100644 frontend/src/views/user/TicketDetail.vue create mode 100644 scripts/insert_banners.py create mode 100644 scripts/insert_lotteries_articles.py create mode 100644 scripts/insert_test_data.py create mode 100644 scripts/reset_admin_password.py diff --git a/.gitignore b/.gitignore index 7ec6a6e..5cf62a3 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,9 @@ vendor/ # Build outputs frontend/dist/ backend/sale.db +backend/cmd/server/sale.db backend/sale +backend/cmd/server/uploads/ # IDE .idea/ diff --git a/backend/cmd/server/sale.db b/backend/cmd/server/sale.db deleted file mode 100644 index 1d935886066e0ff8ffa622c5d9447b4f0fbbad3c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 233472 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 0 && len(selected) < len(selectedParticipants) { - r := rng.Intn(totalWeight) + r, err := rand.Int(rand.Reader, big.NewInt(int64(totalWeight))) + if err != nil { + break + } cumWeight := 0 for _, p := range selectedParticipants { if selected[p.UserID] { @@ -234,7 +237,7 @@ func (h *LotteryHandler) Draw(c *gin.Context) { w = 1 } cumWeight += w - if cumWeight > r { + if cumWeight > int(r.Int64()) { winner := models.LotteryWinner{ LotteryID: lottery.ID, PrizeID: prize.ID, diff --git a/backend/internal/api/handlers/order.go b/backend/internal/api/handlers/order.go index f4105de..7ec608f 100644 --- a/backend/internal/api/handlers/order.go +++ b/backend/internal/api/handlers/order.go @@ -230,7 +230,9 @@ func (h *OrderHandler) Create(c *gin.Context) { PaymentMethod: req.PaymentMethod, } - if err := utils.DB.Create(&order).Error; err != nil { + tx := utils.DB.Begin() + if err := tx.Create(&order).Error; err != nil { + tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create order"}) return } @@ -238,20 +240,41 @@ func (h *OrderHandler) Create(c *gin.Context) { for i := range orderItems { orderItems[i].OrderID = order.ID } - utils.DB.Create(&orderItems) + if err := tx.Create(&orderItems).Error; err != nil { + tx.Rollback() + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create order items"}) + return + } for _, cart := range carts { if cart.Product.RequireCredit { - utils.DB.Model(&models.User{}).Where("id = ?", userID). - UpdateColumn("purchase_credits", utils.DB.Raw("purchase_credits - ?", cart.Product.CreditCost*cart.Quantity)) + if err := tx.Model(&models.User{}).Where("id = ?", userID). + UpdateColumn("purchase_credits", utils.DB.Raw("purchase_credits - ?", cart.Product.CreditCost*cart.Quantity)).Error; err != nil { + tx.Rollback() + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to deduct credits"}) + return + } } if cart.Product.CreditReward > 0 { - utils.DB.Model(&models.User{}).Where("id = ?", userID). + tx.Model(&models.User{}).Where("id = ?", userID). UpdateColumn("purchase_credits", utils.DB.Raw("purchase_credits + ?", cart.Product.CreditReward*cart.Quantity)) } + + var inventory models.Inventory + if err := tx.Where("product_id = ?", cart.ProductID).First(&inventory).Error; err == nil { + if inventory.Quantity >= cart.Quantity { + tx.Model(&inventory).UpdateColumn("quantity", inventory.Quantity-cart.Quantity) + } + } } - utils.DB.Where("user_id = ?", userID).Delete(&models.Cart{}) + if err := tx.Where("user_id = ?", userID).Delete(&models.Cart{}).Error; err != nil { + tx.Rollback() + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to clear cart"}) + return + } + + tx.Commit() utils.DB.Preload("OrderItems.Product").Preload("ShippingAddress").First(&order, order.ID) c.JSON(http.StatusCreated, gin.H{"data": order}) @@ -412,3 +435,41 @@ func (h *OrderHandler) Export(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"data": orders}) } + +func (h *OrderHandler) CancelOrder(c *gin.Context) { + userID := c.GetUint("user_id") + id, _ := strconv.Atoi(c.Param("id")) + + var order models.Order + if err := utils.DB.Where("id = ? AND user_id = ?", id, userID).First(&order).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Order not found"}) + return + } + + if order.Status != models.OrderStatusPendingPayment && order.Status != models.OrderStatusPendingConfirm { + c.JSON(http.StatusBadRequest, gin.H{"error": "Order cannot be cancelled"}) + return + } + + utils.DB.Model(&order).Update("status", models.OrderStatusCancelled) + c.JSON(http.StatusOK, gin.H{"message": "Order cancelled successfully"}) +} + +func (h *OrderHandler) ConfirmReceipt(c *gin.Context) { + userID := c.GetUint("user_id") + id, _ := strconv.Atoi(c.Param("id")) + + var order models.Order + if err := utils.DB.Where("id = ? AND user_id = ?", id, userID).First(&order).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Order not found"}) + return + } + + if order.Status != models.OrderStatusShipped { + c.JSON(http.StatusBadRequest, gin.H{"error": "Order cannot be confirmed"}) + return + } + + utils.DB.Model(&order).Update("status", models.OrderStatusCompleted) + c.JSON(http.StatusOK, gin.H{"message": "Order confirmed successfully"}) +} diff --git a/backend/internal/api/handlers/system.go b/backend/internal/api/handlers/system.go index 7c70287..13dd9a1 100644 --- a/backend/internal/api/handlers/system.go +++ b/backend/internal/api/handlers/system.go @@ -57,7 +57,7 @@ func (h *SystemHandler) GetStats(c *gin.Context) { utils.DB.Model(&models.User{}).Count(&userCount) utils.DB.Model(&models.Product{}).Count(&productCount) utils.DB.Model(&models.Order{}).Count(&orderCount) - utils.DB.Model(&models.Order{}).Where("status = ?", "completed").Select("COALESCE(SUM(total), 0)").Scan(&totalRevenue) + utils.DB.Model(&models.Order{}).Where("status = ?", "completed").Select("COALESCE(SUM(total_amount), 0)").Scan(&totalRevenue) c.JSON(http.StatusOK, gin.H{ "data": gin.H{ @@ -173,13 +173,29 @@ func (h *SupplierHandler) Update(c *gin.Context) { return } - var updateData map[string]interface{} - if err := c.ShouldBindJSON(&updateData); err != nil { + var req struct { + Username *string `json:"username"` + Email *string `json:"email"` + IsActive *bool `json:"is_active"` + } + if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - utils.DB.Model(&supplier).Updates(updateData) + updates := make(map[string]interface{}) + if req.Username != nil { + updates["username"] = *req.Username + } + if req.Email != nil { + updates["email"] = *req.Email + } + if req.IsActive != nil { + updates["is_active"] = *req.IsActive + } + + utils.DB.Model(&supplier).Updates(updates) + utils.DB.Where("id = ?", id).First(&supplier) c.JSON(http.StatusOK, gin.H{"data": supplier}) } diff --git a/backend/internal/api/handlers/ticket.go b/backend/internal/api/handlers/ticket.go index 3d4c911..9d961d6 100644 --- a/backend/internal/api/handlers/ticket.go +++ b/backend/internal/api/handlers/ticket.go @@ -164,3 +164,36 @@ func (h *TicketHandler) Assign(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "Ticket assigned successfully"}) } + +func (h *TicketHandler) Reply(c *gin.Context) { + userID := c.GetUint("user_id") + id, _ := strconv.Atoi(c.Param("id")) + + var ticket models.Ticket + if err := utils.DB.Where("id = ? AND user_id = ?", id, userID).First(&ticket).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Ticket not found"}) + return + } + + var req struct { + Content string `json:"content" binding:"required"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + reply := ticket.Reply + if reply != "" { + reply += "\n\n---\n\n" + } + reply += req.Content + + utils.DB.Model(&ticket).Updates(map[string]interface{}{ + "reply": reply, + "status": models.TicketStatusPending, + }) + + utils.DB.First(&ticket, id) + c.JSON(http.StatusOK, gin.H{"data": ticket}) +} diff --git a/backend/internal/api/handlers/upload.go b/backend/internal/api/handlers/upload.go index 46a2557..1268fc1 100644 --- a/backend/internal/api/handlers/upload.go +++ b/backend/internal/api/handlers/upload.go @@ -2,12 +2,10 @@ package handlers import ( "fmt" - "io" "net/http" "os" "path/filepath" "strings" - "time" "github.com/gin-gonic/gin" "github.com/google/uuid" @@ -104,35 +102,33 @@ func (h *UploadHandler) DeleteImage(c *gin.Context) { return } - filepath := filepath.Join("uploads/images", filename) - if err := os.Remove(filepath); err != nil { + if strings.Contains(filename, "..") || strings.Contains(filename, "/") || strings.Contains(filename, "\\") { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid filename"}) + return + } + + filePath := filepath.Join("uploads/images", filename) + filePath, err := filepath.Abs(filePath) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid file path"}) + return + } + + absUploadDir, _ := filepath.Abs("uploads/images") + if !strings.HasPrefix(filePath, absUploadDir) { + c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"}) + return + } + + if _, err := os.Stat(filePath); os.IsNotExist(err) { c.JSON(http.StatusNotFound, gin.H{"error": "File not found"}) return } + if err := os.Remove(filePath); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete file"}) + return + } + c.JSON(http.StatusOK, gin.H{"message": "File deleted successfully"}) } - -func (h *UploadHandler) ExportOrders(c *gin.Context) { - filename := fmt.Sprintf("orders_%s.csv", time.Now().Format("20060102150405")) - filepath := filepath.Join("uploads", filename) - - file, err := os.Create(filepath) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create file"}) - return - } - defer file.Close() - - c.Header("Content-Description", "File Transfer") - c.Header("Content-Transfer-Encoding", "binary") - c.Header("Content-Disposition", "attachment; filename="+filename) - c.Header("Content-Type", "text/csv") - - file.Seek(0, 0) - _, err = io.Copy(c.Writer, file) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to send file"}) - return - } -} diff --git a/backend/internal/api/routes/routes.go b/backend/internal/api/routes/routes.go index cfc33af..eacf057 100644 --- a/backend/internal/api/routes/routes.go +++ b/backend/internal/api/routes/routes.go @@ -5,8 +5,6 @@ import ( "sale/internal/api/handlers" "sale/internal/api/middlewares" - "sale/internal/models" - "sale/internal/utils" "github.com/gin-gonic/gin" ) @@ -26,6 +24,7 @@ func SetupRoutes(r *gin.Engine) { inventoryHandler := handlers.NewInventoryHandler() articleHandler := handlers.NewArticleHandler() uploadHandler := handlers.NewUploadHandler() + bannerHandler := handlers.NewBannerHandler() r.Use(middlewares.CORSMiddleware()) @@ -40,7 +39,7 @@ func SetupRoutes(r *gin.Engine) { auth.POST("/login", authHandler.Login) auth.POST("/forgot-password", authHandler.ForgotPassword) auth.POST("/reset-password", authHandler.ResetPassword) - auth.GET("/verify-email", authHandler.VerifyEmail) + auth.POST("/verify-email", authHandler.VerifyEmail) } articles := api.Group("/articles") @@ -51,6 +50,8 @@ func SetupRoutes(r *gin.Engine) { articles.POST("/crawl-ribenyan", articleHandler.CrawlRibenyan) } + api.GET("/banners", bannerHandler.List) + api.GET("/categories", categoryHandler.List) api.GET("/brands", brandHandler.List) api.GET("/products", productHandler.List) @@ -97,6 +98,8 @@ func SetupRoutes(r *gin.Engine) { orders.GET("/:id", orderHandler.GetByID) orders.POST("", orderHandler.Create) orders.POST("/:id/refund", orderHandler.Refund) + orders.PUT("/:id/cancel", orderHandler.CancelOrder) + orders.PUT("/:id/confirm-receipt", orderHandler.ConfirmReceipt) } lotteries := authed.Group("/lotteries") @@ -109,6 +112,7 @@ func SetupRoutes(r *gin.Engine) { tickets.GET("", ticketHandler.List) tickets.POST("", ticketHandler.Create) tickets.GET("/:id", ticketHandler.GetByID) + tickets.POST("/:id/reply", ticketHandler.Reply) } supplier := authed.Group("/supplier") @@ -126,11 +130,9 @@ func SetupRoutes(r *gin.Engine) { admin.Use(middlewares.RoleMiddleware("admin")) { admin.GET("/stats", systemHandler.GetStats) - admin.GET("/users", func(c *gin.Context) { - var users []models.User - utils.DB.Select("id, username, email, role, purchase_credits, is_active, created_at").Find(&users) - c.JSON(200, gin.H{"data": users}) - }) + admin.GET("/users", authHandler.AdminGetUsers) + admin.PUT("/users/:id", authHandler.AdminUpdateUser) + admin.DELETE("/users/:id", authHandler.AdminDeleteUser) adminCategories := admin.Group("/categories") { @@ -203,6 +205,15 @@ func SetupRoutes(r *gin.Engine) { adminArticles.DELETE("/:id", articleHandler.Delete) adminArticles.PUT("/:id/pin", articleHandler.TogglePin) } + + adminBanners := admin.Group("/banners") + { + adminBanners.GET("", bannerHandler.AdminList) + adminBanners.POST("", bannerHandler.Create) + adminBanners.PUT("/:id", bannerHandler.Update) + adminBanners.DELETE("/:id", bannerHandler.Delete) + adminBanners.PUT("/:id/toggle", bannerHandler.ToggleActive) + } } } } diff --git a/backend/internal/models/banner.go b/backend/internal/models/banner.go new file mode 100644 index 0000000..10c54c8 --- /dev/null +++ b/backend/internal/models/banner.go @@ -0,0 +1,26 @@ +package models + +import ( + "time" + + "gorm.io/gorm" +) + +type Banner struct { + ID uint `gorm:"primaryKey" json:"id"` + Title string `gorm:"size:100;not null" json:"title"` + Desc string `gorm:"size:200" json:"desc"` + Image string `gorm:"size:500;not null" json:"image"` + Link string `gorm:"size:200" json:"link"` + Button string `gorm:"size:50" json:"button"` + BgColor string `gorm:"size:50" json:"bg_color"` + 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 (Banner) TableName() string { + return "banners" +} diff --git a/backend/internal/models/lottery.go b/backend/internal/models/lottery.go index ecdfd09..3c69e71 100644 --- a/backend/internal/models/lottery.go +++ b/backend/internal/models/lottery.go @@ -10,6 +10,7 @@ type Lottery struct { ID uint `gorm:"primaryKey" json:"id"` Name string `gorm:"size:100;not null" json:"name"` Description string `json:"description"` + Image string `gorm:"size:500" json:"image"` StartTime time.Time `json:"start_time"` EndTime time.Time `json:"end_time"` Cycle string `gorm:"size:20" json:"cycle"` diff --git a/backend/internal/utils/database.go b/backend/internal/utils/database.go index dbb73a5..6d83ad4 100644 --- a/backend/internal/utils/database.go +++ b/backend/internal/utils/database.go @@ -70,6 +70,7 @@ func AutoMigrate() { &models.SystemSetting{}, &models.SupplierAuthorization{}, &models.Article{}, + &models.Banner{}, ) 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 4be5c4d..2151d5c 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -29,7 +29,7 @@ export const authApi = { login: (data: any) => api.post('/auth/login', data), forgotPassword: (data: any) => api.post('/auth/forgot-password', data), resetPassword: (data: any) => api.post('/auth/reset-password', data), - verifyEmail: (params: any) => api.get('/auth/verify-email', { params }), + verifyEmail: (data: any) => api.post('/auth/verify-email', data), sendVerifyCode: () => api.post('/users/send-verify-code'), } @@ -72,6 +72,8 @@ export const orderApi = { getById: (id: number) => api.get(`/orders/${id}`), create: (data: any) => api.post('/orders', data), refund: (id: number, data: any) => api.post(`/orders/${id}/refund`, data), + cancel: (id: number) => api.put(`/orders/${id}/cancel`), + confirmReceipt: (id: number) => api.put(`/orders/${id}/confirm-receipt`), } export const articleApi = { @@ -79,6 +81,10 @@ export const articleApi = { getById: (id: number) => api.get(`/articles/${id}`), } +export const bannerApi = { + list: () => api.get('/banners'), +} + export const lotteryApi = { list: () => api.get('/lotteries'), getById: (id: number) => api.get(`/lotteries/${id}`), @@ -89,10 +95,7 @@ export const ticketApi = { list: (params?: any) => api.get('/tickets', { params }), create: (data: any) => api.post('/tickets', data), getById: (id: number) => api.get(`/tickets/${id}`), -} - -export const menuApi = { - list: () => api.get('/menus'), + reply: (id: number, data: any) => api.post(`/tickets/${id}/reply`, data), } export const supplierApi = { @@ -105,6 +108,11 @@ export const supplierApi = { } export const adminApi = { + getStats: () => api.get('/admin/stats'), + getUsers: () => api.get('/admin/users'), + updateUser: (id: number, data: any) => api.put(`/admin/users/${id}`, data), + deleteUser: (id: number) => api.delete(`/admin/users/${id}`), + getCategories: () => api.get('/categories'), createCategory: (data: any) => api.post('/admin/categories', data), updateCategory: (id: number, data: any) => api.put(`/admin/categories/${id}`, data), @@ -147,11 +155,6 @@ export const adminApi = { updateSMTP: (data: any) => api.put('/admin/settings/smtp', data), updatePayment: (data: any) => api.put('/admin/settings/payment', data), - getMenus: () => api.get('/admin/menus'), - createMenu: (data: any) => api.post('/admin/menus', data), - updateMenu: (id: number, data: any) => api.put(`/admin/menus/${id}`, data), - deleteMenu: (id: number) => api.delete(`/admin/menus/${id}`), - getInventory: () => api.get('/admin/inventory'), getArticles: (params?: any) => api.get('/admin/articles', { params }), @@ -159,4 +162,10 @@ export const adminApi = { updateArticle: (id: number, data: any) => api.put(`/admin/articles/${id}`, data), deleteArticle: (id: number) => api.delete(`/admin/articles/${id}`), togglePinArticle: (id: number) => api.put(`/admin/articles/${id}/pin`), + + getBanners: () => api.get('/admin/banners'), + createBanner: (data: any) => api.post('/admin/banners', data), + updateBanner: (id: number, data: any) => api.put(`/admin/banners/${id}`, data), + deleteBanner: (id: number) => api.delete(`/admin/banners/${id}`), + toggleBanner: (id: number) => api.put(`/admin/banners/${id}/toggle`), } diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 20dd1a9..05987b1 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -123,6 +123,8 @@ "supplierManagement": "Supplier Management", "lotteryManagement": "Lottery Management", "ticketManagement": "Ticket Management", + "articleManagement": "Article Management", + "bannerManagement": "Banner Management", "systemSettings": "System Settings", "menuManagement": "Menu Management", "smtpSettings": "SMTP Settings", diff --git a/frontend/src/i18n/locales/ja.json b/frontend/src/i18n/locales/ja.json index 87edea5..8e8c9ae 100644 --- a/frontend/src/i18n/locales/ja.json +++ b/frontend/src/i18n/locales/ja.json @@ -123,6 +123,8 @@ "supplierManagement": "サプライヤー管理", "lotteryManagement": "抽選管理", "ticketManagement": "チケット管理", + "articleManagement": "記事管理", + "bannerManagement": "バナー管理", "systemSettings": "システム設定", "menuManagement": "メニュー管理", "smtpSettings": "SMTP設定", diff --git a/frontend/src/i18n/locales/zh.json b/frontend/src/i18n/locales/zh.json index 683a219..aa62e0f 100644 --- a/frontend/src/i18n/locales/zh.json +++ b/frontend/src/i18n/locales/zh.json @@ -123,6 +123,8 @@ "supplierManagement": "供货商管理", "lotteryManagement": "抽奖管理", "ticketManagement": "工单管理", + "articleManagement": "资讯管理", + "bannerManagement": "轮播图管理", "systemSettings": "系统设置", "menuManagement": "菜单管理", "smtpSettings": "SMTP设置", diff --git a/frontend/src/layouts/AdminLayout.vue b/frontend/src/layouts/AdminLayout.vue index a11509e..466c942 100644 --- a/frontend/src/layouts/AdminLayout.vue +++ b/frontend/src/layouts/AdminLayout.vue @@ -47,7 +47,11 @@ - 资讯管理 + {{ $t('admin.articleManagement') }} + + + + {{ $t('admin.bannerManagement') }} @@ -95,7 +99,45 @@ -

{{ currentPageTitle }}

+ +
+ + + + + + + +
+ {{ user?.username?.charAt(0).toUpperCase() }} + {{ user?.username }} + +
+ +
+
@@ -110,13 +152,15 @@ import { ref, computed } from 'vue' import { useRouter, useRoute } from 'vue-router' import { useI18n } from 'vue-i18n' -import { DataAnalysis, User, Folder, Goods, List, Van, Trophy, ChatDotSquare, Setting, Document, Compass, Fold, Expand } from '@element-plus/icons-vue' +import { DataAnalysis, User, Folder, Goods, List, Van, Trophy, ChatDotSquare, Setting, Document, Picture, Compass, Fold, Expand, HomeFilled, FullScreen, ArrowDown, SwitchButton, ArrowRight } from '@element-plus/icons-vue' import { useUserStore } from '../store/user' +import { useCartStore } from '../store/cart' const router = useRouter() const route = useRoute() const { locale, t } = useI18n() const userStore = useUserStore() +const cartStore = useCartStore() const sidebarCollapsed = ref(false) const mobileMenuOpen = ref(false) @@ -132,6 +176,7 @@ const pageTitles: Record = { '/admin/lotteries': '抽奖管理', '/admin/tickets': '工单管理', '/admin/articles': '资讯管理', + '/admin/banners': '轮播图管理', '/admin/settings': '系统设置' } @@ -147,7 +192,15 @@ function changeLocale(lang: string) { function handleCommand(command: string) { switch (command) { case 'home': router.push('/'); break - case 'logout': userStore.logout(); router.push('/login'); break + case 'logout': userStore.logout(); cartStore.clearCart(); router.push('/login'); break + } +} + +function toggleFullscreen() { + if (!document.fullscreenElement) { + document.documentElement.requestFullscreen() + } else { + document.exitFullscreen() } } @@ -328,11 +381,99 @@ function handleCommand(command: string) { background: rgba(255, 255, 255, 0.06); } -.page-title { - font-size: 18px; - font-weight: 600; +.breadcrumb { + display: flex; + align-items: center; + gap: 4px; +} + +.breadcrumb-item { + display: inline-flex; + align-items: center; + gap: 4px; + color: rgba(255, 255, 255, 0.5); + font-size: 13px; + cursor: pointer; + padding: 4px 8px; + border-radius: 6px; + transition: all 0.15s; +} + +.breadcrumb-item:hover { + color: rgba(255, 255, 255, 0.9); + background: rgba(255, 255, 255, 0.06); +} + +.breadcrumb-sep { + font-size: 12px; + color: rgba(255, 255, 255, 0.2); +} + +.breadcrumb-current { + color: rgba(255, 255, 255, 0.9); + font-size: 13px; + font-weight: 500; + padding: 4px 8px; + background: rgba(255, 255, 255, 0.06); + border-radius: 6px; +} + +.topbar-actions { + margin-left: auto; + display: flex; + align-items: center; + gap: 8px; +} + +.action-btn { + width: 36px; + height: 36px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + color: rgba(255, 255, 255, 0.6); + background: none; + border: none; + cursor: pointer; + transition: all 0.15s; +} + +.action-btn:hover { + background: rgba(255, 255, 255, 0.06); color: #fff; - margin: 0; +} + +.user-dropdown { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + border-radius: 8px; + cursor: pointer; + transition: background 0.15s; +} + +.user-dropdown:hover { + background: rgba(255, 255, 255, 0.06); +} + +.user-avatar { + background: linear-gradient(135deg, #4e6ef2, #7c5cfc); + color: #fff; + font-size: 14px; + font-weight: 600; +} + +.user-name { + color: rgba(255, 255, 255, 0.9); + font-size: 14px; + font-weight: 500; +} + +.dropdown-arrow { + color: rgba(255, 255, 255, 0.4); + font-size: 12px; } .page-content { @@ -364,10 +505,30 @@ function handleCommand(command: string) { margin-left: 0; } + .topbar { + padding: 0 12px; + } + .menu-btn { display: flex; } + .breadcrumb-item span { + display: none; + } + + .user-name { + display: none; + } + + .dropdown-arrow { + display: none; + } + + .user-dropdown { + padding: 4px; + } + .collapse-btn { display: none; } @@ -379,5 +540,9 @@ function handleCommand(command: string) { background: rgba(0, 0, 0, 0.5); z-index: 99; } + + .page-content { + padding: 16px 12px; + } } diff --git a/frontend/src/layouts/SupplierLayout.vue b/frontend/src/layouts/SupplierLayout.vue index 318cb3c..315dc8e 100644 --- a/frontend/src/layouts/SupplierLayout.vue +++ b/frontend/src/layouts/SupplierLayout.vue @@ -70,11 +70,13 @@ import { useRouter, useRoute } from 'vue-router' import { useI18n } from 'vue-i18n' import { DataAnalysis, List, Box, Fold, Expand } from '@element-plus/icons-vue' import { useUserStore } from '../store/user' +import { useCartStore } from '../store/cart' const router = useRouter() const route = useRoute() const { t } = useI18n() const userStore = useUserStore() +const cartStore = useCartStore() const sidebarCollapsed = ref(false) const mobileMenuOpen = ref(false) @@ -93,7 +95,7 @@ const currentPageTitle = computed(() => { function handleCommand(command: string) { switch (command) { case 'home': router.push('/'); break - case 'logout': userStore.logout(); router.push('/login'); break + case 'logout': userStore.logout(); cartStore.clearCart(); router.push('/login'); break } } diff --git a/frontend/src/layouts/scheme4/UserLayout.vue b/frontend/src/layouts/UserLayout.vue similarity index 96% rename from frontend/src/layouts/scheme4/UserLayout.vue rename to frontend/src/layouts/UserLayout.vue index b7b294e..7210b78 100644 --- a/frontend/src/layouts/scheme4/UserLayout.vue +++ b/frontend/src/layouts/UserLayout.vue @@ -117,8 +117,8 @@ import { ref, computed, watch } from 'vue' import { useRouter, useRoute } from 'vue-router' import { useI18n } from 'vue-i18n' import { HomeFilled, Goods, Document, Trophy, ShoppingCart, Compass, User, Fold, Expand, Search } from '@element-plus/icons-vue' -import { useUserStore } from '../../store/user' -import { useCartStore } from '../../store/cart' +import { useUserStore } from '../store/user' +import { useCartStore } from '../store/cart' const router = useRouter() const route = useRoute() @@ -147,7 +147,7 @@ function handleCommand(command: string) { case 'orders': router.push('/orders'); break case 'admin': router.push('/admin'); break case 'supplier': router.push('/supplier'); break - case 'logout': userStore.logout(); router.push('/login'); break + case 'logout': userStore.logout(); cartStore.clearCart(); router.push('/login'); break } } @@ -442,6 +442,11 @@ if (isLoggedIn.value) cartStore.fetchCart() } @media (max-width: 768px) { + .layout-wrapper { + width: 100%; + overflow-x: hidden; + } + .sidebar { transform: translateX(-100%); width: 220px !important; @@ -458,6 +463,9 @@ if (isLoggedIn.value) cartStore.fetchCart() .main-container, .main-container.sidebar-collapsed { margin-left: 0; + width: 100%; + max-width: 100vw; + overflow-x: hidden; } .menu-btn { @@ -468,6 +476,14 @@ if (isLoggedIn.value) cartStore.fetchCart() display: none; } + .page-content { + padding: 16px; + width: 100%; + max-width: 100vw; + overflow-x: hidden; + box-sizing: border-box; + } + .overlay { display: block; position: fixed; diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index f2d027a..4bc49a9 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -10,9 +10,9 @@ const routes = [ }, { path: '/', - component: () => import('../layouts/scheme4/UserLayout.vue'), + component: () => import('../layouts/UserLayout.vue'), children: [ - { path: '', name: 'Home', component: () => import('../views/scheme4/Home.vue') }, + { path: '', name: 'Home', component: () => import('../views/user/Home.vue') }, { path: 'products', name: 'Products', component: () => import('../views/user/Products.vue') }, { path: 'products/:id', name: 'ProductDetail', component: () => import('../views/user/ProductDetail.vue') }, { path: 'cart', name: 'Cart', component: () => import('../views/user/Cart.vue') }, @@ -24,6 +24,7 @@ const routes = [ { path: 'lotteries/:id', name: 'LotteryDetail', component: () => import('../views/user/LotteryDetail.vue') }, { path: 'tickets', name: 'Tickets', component: () => import('../views/user/Tickets.vue') }, { path: 'tickets/create', name: 'CreateTicket', component: () => import('../views/user/CreateTicket.vue') }, + { path: 'tickets/:id', name: 'TicketDetail', component: () => import('../views/user/TicketDetail.vue') }, { path: 'articles', name: 'Articles', component: () => import('../views/user/Articles.vue') }, { path: 'articles/:id', name: 'ArticleDetail', component: () => import('../views/user/ArticleDetail.vue') }, { path: 'login', name: 'Login', component: () => import('../views/auth/Login.vue'), meta: { guest: true, hideSidebar: true } }, @@ -46,6 +47,7 @@ const routes = [ { path: 'tickets', name: 'AdminTickets', component: () => import('../views/admin/Tickets.vue') }, { 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') }, ], }, { @@ -58,6 +60,11 @@ const routes = [ { path: 'inventory', name: 'SupplierInventory', component: () => import('../views/supplier/Inventory.vue') }, ], }, + { + path: '/:pathMatch(.*)*', + name: 'NotFound', + component: () => import('../views/NotFound.vue'), + }, ] const router = createRouter({ diff --git a/frontend/src/store/cart.ts b/frontend/src/store/cart.ts index 96941a9..537e32d 100644 --- a/frontend/src/store/cart.ts +++ b/frontend/src/store/cart.ts @@ -13,7 +13,6 @@ export const useCartStore = defineStore('cart', () => { try { const res: any = await cartApi.list() items.value = res.data || [] - calcTotals() } finally { loading.value = false } @@ -35,12 +34,16 @@ export const useCartStore = defineStore('cart', () => { } async function removeItems(ids: number[]) { - for (const id of ids) { - await cartApi.delete(id) - } + await Promise.all(ids.map(id => cartApi.delete(id))) await fetchCart() } + function clearCart() { + items.value = [] + totalAmount.value = 0 + totalCount.value = 0 + } + function calcTotals() { totalAmount.value = items.value.reduce((sum, item) => sum + (item.product?.price || 0) * item.quantity, 0) totalCount.value = items.value.reduce((sum, item) => sum + item.quantity, 0) @@ -48,15 +51,16 @@ export const useCartStore = defineStore('cart', () => { watch(items, calcTotals, { deep: true }) - return { - items, - loading, - totalAmount, - totalCount, - fetchCart, - addItem, - updateItem, + return { + items, + loading, + totalAmount, + totalCount, + fetchCart, + addItem, + updateItem, removeItem, - removeItems + removeItems, + clearCart } }) diff --git a/frontend/src/store/user.ts b/frontend/src/store/user.ts index 3e75831..395e9a4 100644 --- a/frontend/src/store/user.ts +++ b/frontend/src/store/user.ts @@ -2,10 +2,18 @@ import { defineStore } from 'pinia' import { ref, computed } from 'vue' import { authApi, userApi } from '../api' +function safeParseUser(str: string | null): any { + if (!str || str === 'undefined' || str === 'null') return null + try { + return JSON.parse(str) + } catch { + return null + } +} + export const useUserStore = defineStore('user', () => { const token = ref(localStorage.getItem('token') || '') - const userStr = localStorage.getItem('user') - const user = ref(userStr && userStr !== 'undefined' ? JSON.parse(userStr) : null) + const user = ref(safeParseUser(localStorage.getItem('user'))) const isLoggedIn = computed(() => !!token.value) const isAdmin = computed(() => user.value?.role === 'admin') diff --git a/frontend/src/utils/request.ts b/frontend/src/utils/request.ts index adf5e7e..47902e3 100644 --- a/frontend/src/utils/request.ts +++ b/frontend/src/utils/request.ts @@ -1,5 +1,6 @@ import axios from 'axios' import { ElMessage } from 'element-plus' +import router from '../router' const api = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080/api', @@ -29,7 +30,8 @@ api.interceptors.response.use( localStorage.removeItem('token') localStorage.removeItem('user') if (hadToken) { - window.location.href = '/login' + ElMessage.error('登录已过期,请重新登录') + router.push('/login') } } else { ElMessage.error(message) diff --git a/frontend/src/views/NotFound.vue b/frontend/src/views/NotFound.vue new file mode 100644 index 0000000..0812daf --- /dev/null +++ b/frontend/src/views/NotFound.vue @@ -0,0 +1,43 @@ + + + + + diff --git a/frontend/src/views/admin/Articles.vue b/frontend/src/views/admin/Articles.vue index f6c10a5..a45c236 100644 --- a/frontend/src/views/admin/Articles.vue +++ b/frontend/src/views/admin/Articles.vue @@ -1,7 +1,6 @@