feat: add announcement feature with admin management and home page display
This commit is contained in:
@@ -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})
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ func SetupRoutes(r *gin.Engine) {
|
|||||||
logHandler := handlers.NewLogHandler()
|
logHandler := handlers.NewLogHandler()
|
||||||
purchaseGroupHandler := handlers.NewPurchaseGroupHandler()
|
purchaseGroupHandler := handlers.NewPurchaseGroupHandler()
|
||||||
shippingTemplateHandler := handlers.NewShippingTemplateHandler()
|
shippingTemplateHandler := handlers.NewShippingTemplateHandler()
|
||||||
|
announcementHandler := handlers.NewAnnouncementHandler()
|
||||||
|
|
||||||
r.Use(middlewares.CORSMiddleware())
|
r.Use(middlewares.CORSMiddleware())
|
||||||
|
|
||||||
@@ -62,6 +63,7 @@ func SetupRoutes(r *gin.Engine) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
api.GET("/banners", bannerHandler.List)
|
api.GET("/banners", bannerHandler.List)
|
||||||
|
api.GET("/announcements", announcementHandler.List)
|
||||||
|
|
||||||
api.GET("/categories", categoryHandler.List)
|
api.GET("/categories", categoryHandler.List)
|
||||||
api.GET("/brands", brandHandler.List)
|
api.GET("/brands", brandHandler.List)
|
||||||
@@ -262,6 +264,15 @@ func SetupRoutes(r *gin.Engine) {
|
|||||||
adminShippingTemplates.PUT("/:id", shippingTemplateHandler.Update)
|
adminShippingTemplates.PUT("/:id", shippingTemplateHandler.Update)
|
||||||
adminShippingTemplates.DELETE("/:id", shippingTemplateHandler.Delete)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -82,6 +82,7 @@ func AutoMigrate() {
|
|||||||
&models.Article{},
|
&models.Article{},
|
||||||
&models.Banner{},
|
&models.Banner{},
|
||||||
&models.PaymentChannel{},
|
&models.PaymentChannel{},
|
||||||
|
&models.Announcement{},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to migrate database: %v", err)
|
log.Fatalf("Failed to migrate database: %v", err)
|
||||||
|
|||||||
@@ -91,6 +91,10 @@ export const bannerApi = {
|
|||||||
list: () => api.get('/banners'),
|
list: () => api.get('/banners'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const announcementApi = {
|
||||||
|
list: () => api.get('/announcements'),
|
||||||
|
}
|
||||||
|
|
||||||
export const lotteryApi = {
|
export const lotteryApi = {
|
||||||
list: () => api.get('/lotteries'),
|
list: () => api.get('/lotteries'),
|
||||||
getById: (id: number) => api.get(`/lotteries/${id}`),
|
getById: (id: number) => api.get(`/lotteries/${id}`),
|
||||||
@@ -201,4 +205,11 @@ export const adminApi = {
|
|||||||
createShippingTemplate: (data: any) => api.post('/admin/shipping-templates', data),
|
createShippingTemplate: (data: any) => api.post('/admin/shipping-templates', data),
|
||||||
updateShippingTemplate: (id: number, data: any) => api.put(`/admin/shipping-templates/${id}`, data),
|
updateShippingTemplate: (id: number, data: any) => api.put(`/admin/shipping-templates/${id}`, data),
|
||||||
deleteShippingTemplate: (id: number) => api.delete(`/admin/shipping-templates/${id}`),
|
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`),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,10 @@
|
|||||||
<el-icon><Picture /></el-icon>
|
<el-icon><Picture /></el-icon>
|
||||||
<span class="nav-text">{{ $t('admin.bannerManagement') }}</span>
|
<span class="nav-text">{{ $t('admin.bannerManagement') }}</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<router-link to="/admin/announcements" class="nav-link">
|
||||||
|
<el-icon><Bell /></el-icon>
|
||||||
|
<span class="nav-text">公告管理</span>
|
||||||
|
</router-link>
|
||||||
<router-link to="/admin/payment" class="nav-link">
|
<router-link to="/admin/payment" class="nav-link">
|
||||||
<el-icon><Wallet /></el-icon>
|
<el-icon><Wallet /></el-icon>
|
||||||
<span class="nav-text">{{ $t('admin.paymentManagement') }}</span>
|
<span class="nav-text">{{ $t('admin.paymentManagement') }}</span>
|
||||||
@@ -169,7 +173,7 @@ import { ref, computed } from 'vue'
|
|||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import logoImg from '../assets/logo.png'
|
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 { useUserStore } from '../store/user'
|
||||||
import { useCartStore } from '../store/cart'
|
import { useCartStore } from '../store/cart'
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ const routes = [
|
|||||||
{ path: 'settings', name: 'AdminSettings', component: () => import('../views/admin/Settings.vue') },
|
{ path: 'settings', name: 'AdminSettings', component: () => import('../views/admin/Settings.vue') },
|
||||||
{ path: 'articles', name: 'AdminArticles', component: () => import('../views/admin/Articles.vue') },
|
{ path: 'articles', name: 'AdminArticles', component: () => import('../views/admin/Articles.vue') },
|
||||||
{ path: 'banners', name: 'AdminBanners', component: () => import('../views/admin/Banners.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: 'payment', name: 'AdminPayment', component: () => import('../views/admin/PaymentChannels.vue') },
|
||||||
{ path: 'logs', name: 'AdminLogs', component: () => import('../views/admin/Logs.vue') },
|
{ path: 'logs', name: 'AdminLogs', component: () => import('../views/admin/Logs.vue') },
|
||||||
{ path: 'shipping-templates', name: 'AdminShippingTemplates', component: () => import('../views/admin/ShippingTemplates.vue') },
|
{ path: 'shipping-templates', name: 'AdminShippingTemplates', component: () => import('../views/admin/ShippingTemplates.vue') },
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
<template>
|
||||||
|
<div class="announcements-page">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>公告管理</h2>
|
||||||
|
<el-button type="primary" @click="showAdd = true">添加公告</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-card">
|
||||||
|
<el-table :data="announcements" v-loading="loading">
|
||||||
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
|
<el-table-column prop="title" label="标题" />
|
||||||
|
<el-table-column prop="content" label="内容" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="sort_order" label="排序" width="80" />
|
||||||
|
<el-table-column label="状态" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.is_active ? 'success' : 'info'">
|
||||||
|
{{ row.is_active ? '已启用' : '已禁用' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="created_at" label="创建时间" width="180">
|
||||||
|
<template #default="{ row }">{{ formatDate(row.created_at) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button link type="primary" @click="editAnnouncement(row)">编辑</el-button>
|
||||||
|
<el-button link :type="row.is_active ? 'warning' : 'success'" @click="toggleActive(row)">
|
||||||
|
{{ row.is_active ? '禁用' : '启用' }}
|
||||||
|
</el-button>
|
||||||
|
<el-button link type="danger" @click="deleteAnnouncement(row.id)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-dialog v-model="showAdd" :title="editing ? '编辑公告' : '添加公告'" width="500px">
|
||||||
|
<el-form :model="form" label-width="80px">
|
||||||
|
<el-form-item label="标题" required>
|
||||||
|
<el-input v-model="form.title" placeholder="请输入公告标题" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="内容">
|
||||||
|
<el-input v-model="form.content" type="textarea" :rows="3" placeholder="请输入公告内容" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="排序">
|
||||||
|
<el-input-number v-model="form.sort_order" :min="0" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-switch v-model="form.is_active" active-text="启用" inactive-text="禁用" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="showAdd = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="saveAnnouncement" :loading="saving">保存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { adminApi } from '../../api'
|
||||||
|
|
||||||
|
const announcements = ref<any[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const showAdd = ref(false)
|
||||||
|
const editing = ref(false)
|
||||||
|
const saving = ref(false)
|
||||||
|
const editingId = ref<number | null>(null)
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
sort_order: 0,
|
||||||
|
is_active: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
function formatDate(date: string) {
|
||||||
|
if (!date) return '-'
|
||||||
|
const d = new Date(date)
|
||||||
|
return d.toLocaleString('zh-CN')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchAnnouncements() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res: any = await adminApi.getAnnouncements()
|
||||||
|
announcements.value = res.data || []
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function editAnnouncement(row: any) {
|
||||||
|
editing.value = true
|
||||||
|
editingId.value = row.id
|
||||||
|
form.value = {
|
||||||
|
title: row.title,
|
||||||
|
content: row.content || '',
|
||||||
|
sort_order: row.sort_order || 0,
|
||||||
|
is_active: row.is_active,
|
||||||
|
}
|
||||||
|
showAdd.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveAnnouncement() {
|
||||||
|
if (!form.value.title.trim()) {
|
||||||
|
ElMessage.warning('请输入公告标题')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
if (editing.value && editingId.value) {
|
||||||
|
await adminApi.updateAnnouncement(editingId.value, form.value)
|
||||||
|
ElMessage.success('更新成功')
|
||||||
|
} else {
|
||||||
|
await adminApi.createAnnouncement(form.value)
|
||||||
|
ElMessage.success('添加成功')
|
||||||
|
}
|
||||||
|
showAdd.value = false
|
||||||
|
fetchAnnouncements()
|
||||||
|
} catch (err: any) {
|
||||||
|
ElMessage.error(err.response?.data?.error || '保存失败')
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleActive(row: any) {
|
||||||
|
try {
|
||||||
|
await adminApi.toggleAnnouncement(row.id)
|
||||||
|
ElMessage.success('状态已更新')
|
||||||
|
fetchAnnouncements()
|
||||||
|
} catch (err: any) {
|
||||||
|
ElMessage.error(err.response?.data?.error || '操作失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteAnnouncement(id: number) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定要删除该公告吗?', '提示', { type: 'warning' })
|
||||||
|
await adminApi.deleteAnnouncement(id)
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
fetchAnnouncements()
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err !== 'cancel') {
|
||||||
|
ElMessage.error(err.response?.data?.error || '删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchAnnouncements()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.announcements-page {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h2 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-card {
|
||||||
|
background: #2d2d44;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,5 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="home-page">
|
<div class="home-page">
|
||||||
|
<!-- 公告区域 -->
|
||||||
|
<div class="announcement-bar" v-if="announcements.length">
|
||||||
|
<div class="announcement-icon">
|
||||||
|
<el-icon><Bell /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="announcement-content">
|
||||||
|
<el-carousel height="32px" direction="vertical" :autoplay="true" :interval="4000" indicator-position="none">
|
||||||
|
<el-carousel-item v-for="(item, index) in announcements" :key="index">
|
||||||
|
<span class="announcement-text">{{ item.title }}</span>
|
||||||
|
</el-carousel-item>
|
||||||
|
</el-carousel>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="banner-section">
|
<div class="banner-section">
|
||||||
<el-carousel height="280px" :interval="5000" arrow="hover" trigger="click">
|
<el-carousel height="280px" :interval="5000" arrow="hover" trigger="click">
|
||||||
<el-carousel-item v-for="(banner, index) in banners" :key="index">
|
<el-carousel-item v-for="(banner, index) in banners" :key="index">
|
||||||
@@ -111,8 +125,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, shallowRef } from 'vue'
|
import { ref, onMounted, shallowRef } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { Goods, Trophy, Document, ShoppingCart, Folder, Present, Star, Timer, ArrowRight } from '@element-plus/icons-vue'
|
import { Goods, Trophy, Document, ShoppingCart, Folder, Present, Star, Timer, ArrowRight, Bell } from '@element-plus/icons-vue'
|
||||||
import { categoryApi, lotteryApi, articleApi, bannerApi } from '../../api'
|
import { categoryApi, lotteryApi, articleApi, bannerApi, announcementApi } from '../../api'
|
||||||
import { getImageUrl } from '../../utils/image'
|
import { getImageUrl } from '../../utils/image'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -120,6 +134,7 @@ const categories = ref<any[]>([])
|
|||||||
const lotteries = ref<any[]>([])
|
const lotteries = ref<any[]>([])
|
||||||
const articles = ref<any[]>([])
|
const articles = ref<any[]>([])
|
||||||
const banners = ref<any[]>([])
|
const banners = ref<any[]>([])
|
||||||
|
const announcements = ref<any[]>([])
|
||||||
|
|
||||||
const defaultBanners = [
|
const defaultBanners = [
|
||||||
{ title: '新品上市', desc: '精选优质商品,限时特惠', btn: '立即选购', link: '/products', icon: shallowRef(Present), bg: 'linear-gradient(135deg, #4e6ef2 0%, #7c5cfc 100%)' },
|
{ title: '新品上市', desc: '精选优质商品,限时特惠', btn: '立即选购', link: '/products', icon: shallowRef(Present), bg: 'linear-gradient(135deg, #4e6ef2 0%, #7c5cfc 100%)' },
|
||||||
@@ -142,15 +157,17 @@ function formatDate(date: string) {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
const [catRes, lotRes, artRes, banRes]: any[] = await Promise.all([
|
const [catRes, lotRes, artRes, banRes, annRes]: any[] = await Promise.all([
|
||||||
categoryApi.list(),
|
categoryApi.list(),
|
||||||
lotteryApi.list(),
|
lotteryApi.list(),
|
||||||
articleApi.list({ page: 1, page_size: 5 }),
|
articleApi.list({ page: 1, page_size: 5 }),
|
||||||
bannerApi.list(),
|
bannerApi.list(),
|
||||||
|
announcementApi.list(),
|
||||||
])
|
])
|
||||||
categories.value = (catRes.data || []).slice(0, 6)
|
categories.value = (catRes.data || []).slice(0, 6)
|
||||||
lotteries.value = (lotRes.data || []).slice(0, 3)
|
lotteries.value = (lotRes.data || []).slice(0, 3)
|
||||||
articles.value = (artRes.data || []).slice(0, 5)
|
articles.value = (artRes.data || []).slice(0, 5)
|
||||||
|
announcements.value = annRes.data || []
|
||||||
|
|
||||||
if (banRes.data && banRes.data.length > 0) {
|
if (banRes.data && banRes.data.length > 0) {
|
||||||
banners.value = banRes.data
|
banners.value = banRes.data
|
||||||
@@ -173,6 +190,53 @@ onMounted(async () => {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.announcement-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: linear-gradient(135deg, rgba(78, 110, 242, 0.15), rgba(124, 92, 252, 0.15));
|
||||||
|
border: 1px solid rgba(78, 110, 242, 0.2);
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.announcement-icon {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: linear-gradient(135deg, #4e6ef2, #7c5cfc);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #fff;
|
||||||
|
flex-shrink: 0;
|
||||||
|
animation: bell-ring 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bell-ring {
|
||||||
|
0%, 100% { transform: rotate(0); }
|
||||||
|
10%, 30% { transform: rotate(10deg); }
|
||||||
|
20%, 40% { transform: rotate(-10deg); }
|
||||||
|
50% { transform: rotate(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.announcement-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.announcement-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: block;
|
||||||
|
line-height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
.banner-section {
|
.banner-section {
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
Reference in New Issue
Block a user