332 lines
11 KiB
Vue
332 lines
11 KiB
Vue
<template>
|
||
<div class="banners-page">
|
||
<div class="page-header">
|
||
<el-button type="primary" @click="openAdd">添加轮播图</el-button>
|
||
</div>
|
||
<div class="table-card">
|
||
<el-table :data="banners">
|
||
<el-table-column prop="id" label="ID" width="80" />
|
||
<el-table-column label="图片" width="150">
|
||
<template #default="{ row }">
|
||
<img :src="getImageUrl(row.image)" style="width: 120px; height: 60px; object-fit: cover; border-radius: 4px;" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="title" label="标题" />
|
||
<el-table-column prop="desc" label="描述" />
|
||
<el-table-column prop="button" label="按钮文字" width="120" />
|
||
<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 label="操作" width="200">
|
||
<template #default="{ row }">
|
||
<el-button link type="primary" size="small" @click="editBanner(row)">编辑</el-button>
|
||
<el-button link :type="row.is_active ? 'warning' : 'success'" size="small" @click="toggleStatus(row)">
|
||
{{ row.is_active ? '禁用' : '启用' }}
|
||
</el-button>
|
||
<el-button link type="danger" size="small" @click="deleteBanner(row.id)">删除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
|
||
<el-dialog v-model="showAdd" :title="editing ? '编辑轮播图' : '添加轮播图'" width="600px">
|
||
<el-form :model="form" label-width="100px">
|
||
<el-form-item label="标题" required>
|
||
<el-input v-model="form.title" placeholder="请输入标题" />
|
||
</el-form-item>
|
||
<el-form-item label="描述">
|
||
<el-input v-model="form.desc" type="textarea" :rows="2" placeholder="请输入描述" />
|
||
</el-form-item>
|
||
<el-form-item label="图片" required>
|
||
<el-upload
|
||
class="banner-uploader"
|
||
:action="uploadUrl"
|
||
:headers="uploadHeaders"
|
||
:show-file-list="false"
|
||
:on-success="handleImageSuccess"
|
||
:before-upload="beforeImageUpload"
|
||
accept="image/*"
|
||
>
|
||
<img v-if="form.image" :src="form.image" class="banner-image" />
|
||
<el-icon v-else class="banner-uploader-icon"><Plus /></el-icon>
|
||
</el-upload>
|
||
<div class="image-tip">建议尺寸: 1920x400px,支持 jpg、png、webp 格式</div>
|
||
</el-form-item>
|
||
<el-form-item label="链接类型">
|
||
<el-radio-group v-model="linkType" @change="handleLinkTypeChange">
|
||
<el-radio-button value="custom">自定义</el-radio-button>
|
||
<el-radio-button value="product">商品</el-radio-button>
|
||
<el-radio-button value="article">资讯</el-radio-button>
|
||
<el-radio-button value="lottery">活动</el-radio-button>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
<el-form-item label="链接" v-if="linkType === 'custom'">
|
||
<el-input v-model="form.link" placeholder="点击跳转链接,如: /products" />
|
||
</el-form-item>
|
||
<el-form-item label="选择商品" v-if="linkType === 'product'">
|
||
<el-select v-model="selectedProduct" placeholder="请选择商品" filterable @change="handleProductSelect" style="width: 100%">
|
||
<el-option v-for="p in products" :key="p.id" :label="p.name" :value="p.id" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="选择资讯" v-if="linkType === 'article'">
|
||
<el-select v-model="selectedArticle" placeholder="请选择资讯" filterable @change="handleArticleSelect" style="width: 100%">
|
||
<el-option v-for="a in articles" :key="a.id" :label="a.title" :value="a.id" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="选择活动" v-if="linkType === 'lottery'">
|
||
<el-select v-model="selectedLottery" placeholder="请选择活动" filterable @change="handleLotterySelect" style="width: 100%">
|
||
<el-option v-for="l in lotteries" :key="l.id" :label="l.name" :value="l.id" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="按钮文字">
|
||
<el-input v-model="form.button" placeholder="如: 立即选购" />
|
||
</el-form-item>
|
||
<el-form-item label="背景颜色">
|
||
<el-input v-model="form.bg_color" placeholder="如: linear-gradient(135deg, #4e6ef2 0%, #7c5cfc 100%)" />
|
||
</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" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="showAdd = false">取消</el-button>
|
||
<el-button type="primary" @click="saveBanner">保存</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, reactive, onMounted } from 'vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { Plus } from '@element-plus/icons-vue'
|
||
import { adminApi, productApi, articleApi, lotteryApi } from '../../api'
|
||
import { getImageUrl } from '../../utils/image'
|
||
|
||
const uploadUrl = import.meta.env.VITE_API_BASE_URL + '/upload'
|
||
const uploadHeaders = { Authorization: `Bearer ${localStorage.getItem('token')}` }
|
||
|
||
const banners = ref<any[]>([])
|
||
const products = ref<any[]>([])
|
||
const articles = ref<any[]>([])
|
||
const lotteries = ref<any[]>([])
|
||
const showAdd = ref(false)
|
||
const editing = ref<any>(null)
|
||
const linkType = ref('custom')
|
||
const selectedProduct = ref<number | null>(null)
|
||
const selectedArticle = ref<number | null>(null)
|
||
const selectedLottery = ref<number | null>(null)
|
||
|
||
const form = reactive({
|
||
title: '',
|
||
desc: '',
|
||
image: '',
|
||
link: '',
|
||
button: '',
|
||
bg_color: '',
|
||
sort_order: 0,
|
||
is_active: true
|
||
})
|
||
|
||
async function fetchBanners() {
|
||
const res: any = await adminApi.getBanners()
|
||
banners.value = res.data || []
|
||
}
|
||
|
||
async function fetchProducts() {
|
||
const res: any = await productApi.list({ page: 1, page_size: 100 })
|
||
products.value = res.data || []
|
||
}
|
||
|
||
async function fetchArticles() {
|
||
const res: any = await articleApi.list({ page: 1, page_size: 100 })
|
||
articles.value = res.data || []
|
||
}
|
||
|
||
async function fetchLotteries() {
|
||
const res: any = await lotteryApi.list()
|
||
lotteries.value = res.data || []
|
||
}
|
||
|
||
function handleLinkTypeChange() {
|
||
form.link = ''
|
||
selectedProduct.value = null
|
||
selectedArticle.value = null
|
||
selectedLottery.value = null
|
||
}
|
||
|
||
function handleProductSelect(productId: number) {
|
||
form.link = `/products/${productId}`
|
||
}
|
||
|
||
function handleArticleSelect(articleId: number) {
|
||
form.link = `/articles/${articleId}`
|
||
}
|
||
|
||
function handleLotterySelect(lotteryId: number) {
|
||
form.link = `/lotteries/${lotteryId}`
|
||
}
|
||
|
||
function openAdd() {
|
||
editing.value = null
|
||
linkType.value = 'custom'
|
||
selectedProduct.value = null
|
||
selectedArticle.value = null
|
||
selectedLottery.value = null
|
||
Object.assign(form, { title: '', desc: '', image: '', link: '', button: '', bg_color: '', sort_order: 0, is_active: true })
|
||
showAdd.value = true
|
||
}
|
||
|
||
function editBanner(row: any) {
|
||
editing.value = row
|
||
Object.assign(form, {
|
||
title: row.title,
|
||
desc: row.desc || '',
|
||
image: row.image || '',
|
||
link: row.link || '',
|
||
button: row.button || '',
|
||
bg_color: row.bg_color || '',
|
||
sort_order: row.sort_order,
|
||
is_active: row.is_active
|
||
})
|
||
|
||
// 根据链接判断类型
|
||
if (row.link) {
|
||
if (row.link.startsWith('/products/')) {
|
||
linkType.value = 'product'
|
||
const id = parseInt(row.link.split('/')[2])
|
||
selectedProduct.value = isNaN(id) ? null : id
|
||
} else if (row.link.startsWith('/articles/')) {
|
||
linkType.value = 'article'
|
||
const id = parseInt(row.link.split('/')[2])
|
||
selectedArticle.value = isNaN(id) ? null : id
|
||
} else if (row.link.startsWith('/lotteries/')) {
|
||
linkType.value = 'lottery'
|
||
const id = parseInt(row.link.split('/')[2])
|
||
selectedLottery.value = isNaN(id) ? null : id
|
||
} else {
|
||
linkType.value = 'custom'
|
||
}
|
||
} else {
|
||
linkType.value = 'custom'
|
||
}
|
||
|
||
showAdd.value = true
|
||
}
|
||
|
||
function beforeImageUpload(file: File) {
|
||
const isImage = file.type.startsWith('image/')
|
||
const isLt5M = file.size / 1024 / 1024 < 5
|
||
|
||
if (!isImage) {
|
||
ElMessage.error('只能上传图片文件!')
|
||
return false
|
||
}
|
||
if (!isLt5M) {
|
||
ElMessage.error('图片大小不能超过 5MB!')
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
function handleImageSuccess(response: any) {
|
||
if (response.url) {
|
||
form.image = response.url
|
||
ElMessage.success('图片上传成功')
|
||
} else {
|
||
ElMessage.error('上传失败')
|
||
}
|
||
}
|
||
|
||
async function saveBanner() {
|
||
if (!form.title || !form.image) {
|
||
ElMessage.warning('请填写必填项')
|
||
return
|
||
}
|
||
|
||
if (editing.value) {
|
||
await adminApi.updateBanner(editing.value.id, form)
|
||
} else {
|
||
await adminApi.createBanner(form)
|
||
}
|
||
ElMessage.success('保存成功')
|
||
showAdd.value = false
|
||
editing.value = null
|
||
await fetchBanners()
|
||
}
|
||
|
||
async function toggleStatus(row: any) {
|
||
await adminApi.toggleBanner(row.id)
|
||
ElMessage.success('操作成功')
|
||
await fetchBanners()
|
||
}
|
||
|
||
async function deleteBanner(id: number) {
|
||
await ElMessageBox.confirm('确定删除此轮播图?', '确认删除')
|
||
await adminApi.deleteBanner(id)
|
||
ElMessage.success('删除成功')
|
||
await fetchBanners()
|
||
}
|
||
|
||
onMounted(async () => {
|
||
await Promise.all([
|
||
fetchBanners(),
|
||
fetchProducts(),
|
||
fetchArticles(),
|
||
fetchLotteries()
|
||
])
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.banners-page { padding: 0; }
|
||
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }
|
||
.table-card { background: #2d2d44; border: 1px solid rgba(255, 255, 255, 0.06); border-radius: 12px; padding: 20px; }
|
||
|
||
.banner-uploader {
|
||
:deep(.el-upload) {
|
||
border: 1px dashed rgba(255, 255, 255, 0.2);
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
position: relative;
|
||
overflow: hidden;
|
||
transition: all 0.3s;
|
||
width: 300px;
|
||
height: 120px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
&:hover {
|
||
border-color: #4e6ef2;
|
||
}
|
||
}
|
||
}
|
||
|
||
.banner-image {
|
||
width: 300px;
|
||
height: 120px;
|
||
object-fit: cover;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.banner-uploader-icon {
|
||
font-size: 28px;
|
||
color: rgba(255, 255, 255, 0.4);
|
||
}
|
||
|
||
.image-tip {
|
||
margin-top: 8px;
|
||
font-size: 12px;
|
||
color: rgba(255, 255, 255, 0.4);
|
||
}
|
||
</style>
|