fix: 全面修复项目问题
后端修复: - 验证码存储与校验机制 - 订单创建事务+库存扣减 - GetStats字段名错误 - VerifyEmail改为POST - 供应商更新字段白名单 - 文件删除安全检查 - 抽奖安全随机数 - 用户管理CRUD - 订单取消/确认收货 - 工单回复 - Toggle返回新数据 - 移除死代码 前端修复: - 404兜底路由 - 401软跳转 - API层统一 - 面包屑补充banners - 国际化完善 - 购物车并行删除 - 退出清理购物车 - 供应商Dashboard数据 - 工单详情页 - 订单取消/确认收货
This commit is contained in:
@@ -0,0 +1,331 @@
|
||||
<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_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>
|
||||
Reference in New Issue
Block a user