From e99560ba879cb39238d1504a6eccd98c6a8ac2d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E7=90=A6?= Date: Fri, 16 Jan 2026 08:29:57 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=88=E4=BD=9C=E9=A1=B5=E9=9D=A2=E7=9A=84?= =?UTF-8?q?=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/InquiryModal.vue | 28 ++- client/src/components/admin/AdminLayout.vue | 14 +- client/src/pages/Services.vue | 61 ++--- client/src/pages/admin/PartnerForm.vue | 196 +++++++++++++++++ client/src/pages/admin/Partners.vue | 232 ++++++++++++++++++++ client/src/pages/admin/TestimonialForm.vue | 205 +++++++++++++++++ client/src/pages/admin/Testimonials.vue | 226 +++++++++++++++++++ client/src/router.ts | 10 + client/src/services/api.ts | 153 +++++++++++++ server/handlers/services.go | 144 ++++++++++++ server/main.go | 14 ++ server/models/services.go | 30 +++ server/repositories/services_repository.go | 183 +++++++++++++++ server/scripts/run_services_migration.go | 35 +++ 14 files changed, 1474 insertions(+), 57 deletions(-) create mode 100644 client/src/pages/admin/PartnerForm.vue create mode 100644 client/src/pages/admin/Partners.vue create mode 100644 client/src/pages/admin/TestimonialForm.vue create mode 100644 client/src/pages/admin/Testimonials.vue create mode 100644 server/handlers/services.go create mode 100644 server/models/services.go create mode 100644 server/repositories/services_repository.go create mode 100644 server/scripts/run_services_migration.go diff --git a/client/src/components/InquiryModal.vue b/client/src/components/InquiryModal.vue index 22c63a6..d1172ed 100644 --- a/client/src/components/InquiryModal.vue +++ b/client/src/components/InquiryModal.vue @@ -6,7 +6,7 @@

- Let's build
something
unreal. + 共创
无限
可能

告诉我你的构想。
@@ -36,7 +36,10 @@ placeholder="Name" v-model="formData.name" > - +

- +
@@ -57,7 +63,10 @@ placeholder="Contact" v-model="formData.contact" > - +
@@ -73,7 +82,7 @@ class="peer hidden" v-model="formData.budget" > - 10k - 50k + 1w - 5w
@@ -106,7 +115,10 @@ v-model="formData.description" @input="autoResizeTextarea" > - +
diff --git a/client/src/components/admin/AdminLayout.vue b/client/src/components/admin/AdminLayout.vue index c517e4c..4092960 100644 --- a/client/src/components/admin/AdminLayout.vue +++ b/client/src/components/admin/AdminLayout.vue @@ -45,10 +45,22 @@
  • - + 👤 关于页面
  • +
  • + + 💬 + 客户评价 + +
  • +
  • + + 🤝 + 合作伙伴 + +
  • 💻 diff --git a/client/src/pages/Services.vue b/client/src/pages/Services.vue index b64a072..640f345 100644 --- a/client/src/pages/Services.vue +++ b/client/src/pages/Services.vue @@ -157,19 +157,12 @@ + + \ No newline at end of file diff --git a/client/src/pages/admin/Partners.vue b/client/src/pages/admin/Partners.vue new file mode 100644 index 0000000..31a7a1e --- /dev/null +++ b/client/src/pages/admin/Partners.vue @@ -0,0 +1,232 @@ + + + + + \ No newline at end of file diff --git a/client/src/pages/admin/TestimonialForm.vue b/client/src/pages/admin/TestimonialForm.vue new file mode 100644 index 0000000..50f4581 --- /dev/null +++ b/client/src/pages/admin/TestimonialForm.vue @@ -0,0 +1,205 @@ + + + + + \ No newline at end of file diff --git a/client/src/pages/admin/Testimonials.vue b/client/src/pages/admin/Testimonials.vue new file mode 100644 index 0000000..8322591 --- /dev/null +++ b/client/src/pages/admin/Testimonials.vue @@ -0,0 +1,226 @@ + + + + + \ No newline at end of file diff --git a/client/src/router.ts b/client/src/router.ts index e1827b1..bac9d7e 100644 --- a/client/src/router.ts +++ b/client/src/router.ts @@ -58,6 +58,16 @@ const routes = [ { path: 'about/create', name: 'admin-about-create', component: () => import('./pages/admin/AboutForm.vue') }, { path: 'about/edit/:id', name: 'admin-about-edit', component: () => import('./pages/admin/AboutForm.vue') }, + // 客户评价管理 + { path: 'testimonials', name: 'admin-testimonials', component: () => import('./pages/admin/Testimonials.vue') }, + { path: 'testimonials/create', name: 'admin-testimonials-create', component: () => import('./pages/admin/TestimonialForm.vue') }, + { path: 'testimonials/:id/edit', name: 'admin-testimonials-edit', component: () => import('./pages/admin/TestimonialForm.vue') }, + + // 合作伙伴管理 + { path: 'partners', name: 'admin-partners', component: () => import('./pages/admin/Partners.vue') }, + { path: 'partners/create', name: 'admin-partners-create', component: () => import('./pages/admin/PartnerForm.vue') }, + { path: 'partners/:id/edit', name: 'admin-partners-edit', component: () => import('./pages/admin/PartnerForm.vue') }, + // 系统配置 { path: 'settings', name: 'admin-settings', component: () => import('./pages/admin/Settings.vue') }, diff --git a/client/src/services/api.ts b/client/src/services/api.ts index e326bb6..afabb13 100644 --- a/client/src/services/api.ts +++ b/client/src/services/api.ts @@ -961,3 +961,156 @@ export const deleteAboutProfile = async (id: number): Promise => { throw error } } + +// 客户评价相关类型 +export interface Testimonial { + id: number + content: string + author: string + role: string + avatar: string + rating: number + createdAt: string + updatedAt: string +} + +// 合作伙伴相关类型 +export interface Partner { + id: number + name: string + logo: string + description: string + website: string + createdAt: string + updatedAt: string +} + +// 客户评价API +export const fetchTestimonials = async (): Promise => { + try { + const response = await fetch(`${API_BASE}/testimonials`) + if (!response.ok) { + const errorData = await response.json() + throw new Error(errorData.error || '获取客户评价失败') + } + return await response.json() + } catch (error) { + console.error('Fetch testimonials error:', error) + throw error + } +} + +export const createTestimonial = async (data: Omit): Promise => { + try { + const response = await fetch(`${API_BASE}/admin/testimonials`, { + method: 'POST', + headers: getAuthHeaders(), + body: JSON.stringify(data) + }) + if (!response.ok) { + const errorData = await response.json() + throw new Error(errorData.error || '创建客户评价失败') + } + } catch (error) { + console.error('Create testimonial error:', error) + throw error + } +} + +export const updateTestimonial = async (id: number, data: Omit): Promise => { + try { + const response = await fetch(`${API_BASE}/admin/testimonials/${id}`, { + method: 'PUT', + headers: getAuthHeaders(), + body: JSON.stringify(data) + }) + if (!response.ok) { + const errorData = await response.json() + throw new Error(errorData.error || '更新客户评价失败') + } + } catch (error) { + console.error('Update testimonial error:', error) + throw error + } +} + +export const deleteTestimonial = async (id: number): Promise => { + try { + const response = await fetch(`${API_BASE}/admin/testimonials/${id}`, { + method: 'DELETE', + headers: getAuthHeaders() + }) + if (!response.ok) { + const errorData = await response.json() + throw new Error(errorData.error || '删除客户评价失败') + } + } catch (error) { + console.error('Delete testimonial error:', error) + throw error + } +} + +// 合作伙伴API +export const fetchPartners = async (): Promise => { + try { + const response = await fetch(`${API_BASE}/partners`) + if (!response.ok) { + const errorData = await response.json() + throw new Error(errorData.error || '获取合作伙伴失败') + } + return await response.json() + } catch (error) { + console.error('Fetch partners error:', error) + throw error + } +} + +export const createPartner = async (data: Omit): Promise => { + try { + const response = await fetch(`${API_BASE}/admin/partners`, { + method: 'POST', + headers: getAuthHeaders(), + body: JSON.stringify(data) + }) + if (!response.ok) { + const errorData = await response.json() + throw new Error(errorData.error || '创建合作伙伴失败') + } + } catch (error) { + console.error('Create partner error:', error) + throw error + } +} + +export const updatePartner = async (id: number, data: Omit): Promise => { + try { + const response = await fetch(`${API_BASE}/admin/partners/${id}`, { + method: 'PUT', + headers: getAuthHeaders(), + body: JSON.stringify(data) + }) + if (!response.ok) { + const errorData = await response.json() + throw new Error(errorData.error || '更新合作伙伴失败') + } + } catch (error) { + console.error('Update partner error:', error) + throw error + } +} + +export const deletePartner = async (id: number): Promise => { + try { + const response = await fetch(`${API_BASE}/admin/partners/${id}`, { + method: 'DELETE', + headers: getAuthHeaders() + }) + if (!response.ok) { + const errorData = await response.json() + throw new Error(errorData.error || '删除合作伙伴失败') + } + } catch (error) { + console.error('Delete partner error:', error) + throw error + } +} diff --git a/server/handlers/services.go b/server/handlers/services.go new file mode 100644 index 0000000..ecf854a --- /dev/null +++ b/server/handlers/services.go @@ -0,0 +1,144 @@ +package handlers + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/niangaodev/art-code/models" + "github.com/niangaodev/art-code/repositories" +) + +// GetTestimonials 获取客户评价 +func GetTestimonials(c *gin.Context) { + testimonials, err := repositories.GetTestimonials() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch testimonials"}) + return + } + c.JSON(http.StatusOK, testimonials) +} + +// GetPartners 获取合作伙伴 +func GetPartners(c *gin.Context) { + partners, err := repositories.GetPartners() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch partners"}) + return + } + c.JSON(http.StatusOK, partners) +} + +// AdminCreateTestimonial 创建客户评价 +func AdminCreateTestimonial(c *gin.Context) { + var t models.Testimonial + if err := c.ShouldBindJSON(&t); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) + return + } + + if err := repositories.CreateTestimonial(&t); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create testimonial"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Testimonial created successfully"}) +} + +// AdminUpdateTestimonial 更新客户评价 +func AdminUpdateTestimonial(c *gin.Context) { + idStr := c.Param("id") + var id uint + if _, err := fmt.Sscanf(idStr, "%d", &id); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"}) + return + } + + var t models.Testimonial + if err := c.ShouldBindJSON(&t); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) + return + } + t.ID = id + + if err := repositories.UpdateTestimonial(&t); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update testimonial"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Testimonial updated successfully"}) +} + +// AdminDeleteTestimonial 删除客户评价 +func AdminDeleteTestimonial(c *gin.Context) { + idStr := c.Param("id") + var id uint + if _, err := fmt.Sscanf(idStr, "%d", &id); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"}) + return + } + + if err := repositories.DeleteTestimonial(id); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete testimonial"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Testimonial deleted successfully"}) +} + +// AdminCreatePartner 创建合作伙伴 +func AdminCreatePartner(c *gin.Context) { + var p models.Partner + if err := c.ShouldBindJSON(&p); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) + return + } + + if err := repositories.CreatePartner(&p); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create partner"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Partner created successfully"}) +} + +// AdminUpdatePartner 更新合作伙伴 +func AdminUpdatePartner(c *gin.Context) { + idStr := c.Param("id") + var id uint + if _, err := fmt.Sscanf(idStr, "%d", &id); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"}) + return + } + + var p models.Partner + if err := c.ShouldBindJSON(&p); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) + return + } + p.ID = id + + if err := repositories.UpdatePartner(&p); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update partner"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Partner updated successfully"}) +} + +// AdminDeletePartner 删除合作伙伴 +func AdminDeletePartner(c *gin.Context) { + idStr := c.Param("id") + var id uint + if _, err := fmt.Sscanf(idStr, "%d", &id); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"}) + return + } + + if err := repositories.DeletePartner(id); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete partner"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Partner deleted successfully"}) +} diff --git a/server/main.go b/server/main.go index 78984ed..1085913 100644 --- a/server/main.go +++ b/server/main.go @@ -77,6 +77,10 @@ func main() { // 关于页面路由 api.GET("/about", handlers.GetAboutProfile) + + // 合作页面路由 + api.GET("/testimonials", handlers.GetTestimonials) + api.GET("/partners", handlers.GetPartners) } // 管理员API路由组 @@ -152,6 +156,16 @@ func main() { authAdmin.POST("/about", middleware.PermissionMiddleware("settings", "create"), handlers.AdminCreateAboutProfile) authAdmin.PUT("/about/:id", middleware.PermissionMiddleware("settings", "update"), handlers.AdminUpdateAboutProfile) authAdmin.DELETE("/about/:id", middleware.PermissionMiddleware("settings", "delete"), handlers.AdminDeleteAboutProfile) + + // 客户评价管理 (复用 settings 权限) + authAdmin.POST("/testimonials", middleware.PermissionMiddleware("settings", "create"), handlers.AdminCreateTestimonial) + authAdmin.PUT("/testimonials/:id", middleware.PermissionMiddleware("settings", "update"), handlers.AdminUpdateTestimonial) + authAdmin.DELETE("/testimonials/:id", middleware.PermissionMiddleware("settings", "delete"), handlers.AdminDeleteTestimonial) + + // 合作伙伴管理 (复用 settings 权限) + authAdmin.POST("/partners", middleware.PermissionMiddleware("settings", "create"), handlers.AdminCreatePartner) + authAdmin.PUT("/partners/:id", middleware.PermissionMiddleware("settings", "update"), handlers.AdminUpdatePartner) + authAdmin.DELETE("/partners/:id", middleware.PermissionMiddleware("settings", "delete"), handlers.AdminDeletePartner) } } diff --git a/server/models/services.go b/server/models/services.go new file mode 100644 index 0000000..8675be3 --- /dev/null +++ b/server/models/services.go @@ -0,0 +1,30 @@ +package models + +import ( + "time" +) + +// Testimonial 客户评价模型 +type Testimonial struct { + ID uint `json:"id"` + Name string `json:"name"` + Role string `json:"role"` + Content string `json:"content"` + Avatar string `json:"avatar"` + Rating uint8 `json:"rating"` + SortOrder uint `json:"sortOrder"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +// Partner 合作伙伴模型 +type Partner struct { + ID uint `json:"id"` + Name string `json:"name"` + Logo string `json:"logo"` + Description string `json:"description"` + URL string `json:"url"` + SortOrder uint `json:"sortOrder"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} diff --git a/server/repositories/services_repository.go b/server/repositories/services_repository.go new file mode 100644 index 0000000..8ca3fe1 --- /dev/null +++ b/server/repositories/services_repository.go @@ -0,0 +1,183 @@ +package repositories + +import ( + "log" + + "github.com/niangaodev/art-code/config" + "github.com/niangaodev/art-code/models" +) + +// GetTestimonials 获取所有客户评价 +func GetTestimonials() ([]models.Testimonial, error) { + query := "SELECT id, name, role, content, avatar, rating, sort_order, created_at, updated_at FROM testimonials ORDER BY sort_order ASC, created_at DESC" + rows, err := config.DB.Query(query) + if err != nil { + log.Printf("Error querying testimonials: %v", err) + return nil, err + } + defer rows.Close() + + var testimonials []models.Testimonial + for rows.Next() { + var t models.Testimonial + if err := rows.Scan( + &t.ID, + &t.Name, + &t.Role, + &t.Content, + &t.Avatar, + &t.Rating, + &t.SortOrder, + &t.CreatedAt, + &t.UpdatedAt, + ); err != nil { + log.Printf("Error scanning testimonial: %v", err) + continue + } + testimonials = append(testimonials, t) + } + + return testimonials, nil +} + +// CreateTestimonial 创建客户评价 +func CreateTestimonial(t *models.Testimonial) error { + query := ` + INSERT INTO testimonials (name, role, content, avatar, rating, sort_order, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, NOW(), NOW()) + ` + _, err := config.DB.Exec( + query, + t.Name, + t.Role, + t.Content, + t.Avatar, + t.Rating, + t.SortOrder, + ) + if err != nil { + log.Printf("Error creating testimonial: %v", err) + return err + } + return nil +} + +// UpdateTestimonial 更新客户评价 +func UpdateTestimonial(t *models.Testimonial) error { + query := ` + UPDATE testimonials SET name = ?, role = ?, content = ?, avatar = ?, rating = ?, sort_order = ?, updated_at = NOW() + WHERE id = ? + ` + _, err := config.DB.Exec( + query, + t.Name, + t.Role, + t.Content, + t.Avatar, + t.Rating, + t.SortOrder, + t.ID, + ) + if err != nil { + log.Printf("Error updating testimonial: %v", err) + return err + } + return nil +} + +// DeleteTestimonial 删除客户评价 +func DeleteTestimonial(id uint) error { + query := "DELETE FROM testimonials WHERE id = ?" + _, err := config.DB.Exec(query, id) + if err != nil { + log.Printf("Error deleting testimonial: %v", err) + return err + } + return nil +} + +// GetPartners 获取所有合作伙伴 +func GetPartners() ([]models.Partner, error) { + query := "SELECT id, name, logo, description, url, sort_order, created_at, updated_at FROM partners ORDER BY sort_order ASC, created_at DESC" + rows, err := config.DB.Query(query) + if err != nil { + log.Printf("Error querying partners: %v", err) + return nil, err + } + defer rows.Close() + + var partners []models.Partner + for rows.Next() { + var p models.Partner + if err := rows.Scan( + &p.ID, + &p.Name, + &p.Logo, + &p.Description, + &p.URL, + &p.SortOrder, + &p.CreatedAt, + &p.UpdatedAt, + ); err != nil { + log.Printf("Error scanning partner: %v", err) + continue + } + partners = append(partners, p) + } + + return partners, nil +} + +// CreatePartner 创建合作伙伴 +func CreatePartner(p *models.Partner) error { + query := ` + INSERT INTO partners (name, logo, description, url, sort_order, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, NOW(), NOW()) + ` + _, err := config.DB.Exec( + query, + p.Name, + p.Logo, + p.Description, + p.URL, + p.SortOrder, + ) + if err != nil { + log.Printf("Error creating partner: %v", err) + return err + } + return nil +} + +// UpdatePartner 更新合作伙伴 +func UpdatePartner(p *models.Partner) error { + query := ` + UPDATE partners SET name = ?, logo = ?, description = ?, url = ?, sort_order = ?, updated_at = NOW() + WHERE id = ? + ` + _, err := config.DB.Exec( + query, + p.Name, + p.Logo, + p.Description, + p.URL, + p.SortOrder, + p.ID, + ) + if err != nil { + log.Printf("Error updating partner: %v", err) + return err + } + return nil +} + +// DeletePartner 删除合作伙伴 +func DeletePartner(id uint) error { + query := "DELETE FROM partners WHERE id = ?" + _, err := config.DB.Exec(query, id) + if err != nil { + log.Printf("Error deleting partner: %v", err) + return err + } + return nil +} diff --git a/server/scripts/run_services_migration.go b/server/scripts/run_services_migration.go new file mode 100644 index 0000000..0a92f60 --- /dev/null +++ b/server/scripts/run_services_migration.go @@ -0,0 +1,35 @@ +package main + +import ( + "io/ioutil" + "log" + "strings" + + "github.com/niangaodev/art-code/config" +) + +func main() { + config.InitDB() + defer config.CloseDB() + + // Read SQL file + content, err := ioutil.ReadFile("scripts/create_services_tables.sql") + if err != nil { + log.Fatalf("Failed to read SQL file: %v", err) + } + + requests := strings.Split(string(content), ";") + for _, request := range requests { + request = strings.TrimSpace(request) + if request == "" { + continue + } + _, err := config.DB.Exec(request) + if err != nil { + log.Printf("Error executing query: %s\nError: %v", request, err) + } else { + log.Println("Executed query successfully") + } + } + log.Println("Services tables migration completed.") +}