vending-machine/admin/src/views/banner/index.vue
18631081161 fa0cf7e41c
All checks were successful
continuous-integration/drone/push Build is passing
bug修复
2026-04-21 04:08:37 +08:00

282 lines
8.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="banner-manage">
<!-- 顶部操作栏 -->
<div class="toolbar">
<el-button type="primary" @click="handleAdd">新增 Banner</el-button>
<el-text type="info" size="small">拖拽行可调整排序</el-text>
</div>
<!-- Banner 列表表格支持拖拽排序 -->
<el-table ref="tableRef" :data="bannerList" row-key="id" border style="width: 100%">
<el-table-column width="60" align="center" label="排序">
<template #default>
<el-icon class="drag-handle" style="cursor: move"><Rank /></el-icon>
</template>
</el-table-column>
<el-table-column label="简体中文图片" min-width="160">
<template #default="{ row }">
<el-image
v-if="row.imageUrlZhCn"
:src="row.imageUrlZhCn"
style="width: 120px; height: 60px"
fit="cover"
:preview-src-list="[row.imageUrlZhCn]"
/>
<span v-else>未配置</span>
</template>
</el-table-column>
<el-table-column label="繁体中文图片" min-width="160">
<template #default="{ row }">
<el-image
v-if="row.imageUrlZhTw"
:src="row.imageUrlZhTw"
style="width: 120px; height: 60px"
fit="cover"
:preview-src-list="[row.imageUrlZhTw]"
/>
<span v-else>未配置</span>
</template>
</el-table-column>
<el-table-column label="英文图片" min-width="160">
<template #default="{ row }">
<el-image
v-if="row.imageUrlEn"
:src="row.imageUrlEn"
style="width: 120px; height: 60px"
fit="cover"
:preview-src-list="[row.imageUrlEn]"
/>
<span v-else>未配置</span>
</template>
</el-table-column>
<el-table-column label="跳转类型" width="120" align="center">
<template #default="{ row }">
<el-tag v-if="row.linkType === 'internal'" type="primary">内部页面</el-tag>
<el-tag v-else-if="row.linkType === 'external'" type="warning">外部链接</el-tag>
<el-tag v-else type="info">无跳转</el-tag>
</template>
</el-table-column>
<el-table-column label="跳转地址" prop="linkUrl" min-width="180" show-overflow-tooltip />
<el-table-column label="操作" width="180" align="center" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="handleEdit(row)">编辑</el-button>
<el-popconfirm title="确定删除此 Banner" @confirm="handleDelete(row.id)">
<template #reference>
<el-button size="small" type="danger">删除</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<!-- 新增/编辑弹窗 -->
<el-dialog
v-model="dialogVisible"
:title="isEdit ? '编辑 Banner' : '新增 Banner'"
width="600px"
destroy-on-close
>
<el-form :model="form" label-width="120px">
<el-form-item label="简体中文图片" required>
<ImageUpload v-model="form.imageUrlZhCn" />
</el-form-item>
<el-form-item label="繁体中文图片" required>
<ImageUpload v-model="form.imageUrlZhTw" />
</el-form-item>
<el-form-item label="英文图片" required>
<ImageUpload v-model="form.imageUrlEn" />
</el-form-item>
<el-form-item label="跳转类型">
<el-select v-model="form.linkType" style="width: 100%">
<el-option label="无跳转" value="none" />
<el-option label="内部页面" value="internal" />
<el-option label="外部链接" value="external" />
</el-select>
</el-form-item>
<el-form-item v-if="form.linkType !== 'none'" label="跳转地址">
<el-select v-if="form.linkType === 'internal'" v-model="form.linkUrl" placeholder="请选择内部页面" style="width: 100%">
<el-option
v-for="page in internalPages"
:key="page.path"
:label="page.label"
:value="page.path"
/>
</el-select>
<el-input v-else v-model="form.linkUrl" placeholder="请输入外部链接地址" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitting" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, nextTick } from 'vue'
import { Rank } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import Sortable from 'sortablejs'
import ImageUpload from '@/components/ImageUpload.vue'
import { getBanners, createBanner, updateBanner, deleteBanner, updateBannerSort } from '@/api/banner'
// Banner 数据类型
interface BannerItem {
id: string
imageUrlZhCn: string
imageUrlZhTw: string
imageUrlEn: string
linkType: string
linkUrl: string | null
sortOrder: number
}
const tableRef = ref()
const bannerList = ref<BannerItem[]>([])
// 移动端内部页面列表
const internalPages = [
{ path: '/pages/index/index', label: '首页' },
{ path: '/pages/membership/membership', label: '会员' },
{ path: '/pages/stamps/stamps', label: '节日印花' },
{ path: '/pages/points/points', label: '我的积分' },
{ path: '/pages/coupons/index', label: '我的优惠券' },
{ path: '/pages/profile/index', label: '我的' },
{ path: '/pages/agreement/index', label: '用户协议' },
{ path: '/pages/privacy/index', label: '隐私政策' },
{ path: '/pages/about/index', label: '关于' },
]
const dialogVisible = ref(false)
const isEdit = ref(false)
const submitting = ref(false)
const editingId = ref('')
const defaultForm = () => ({
imageUrlZhCn: '',
imageUrlZhTw: '',
imageUrlEn: '',
linkType: 'none',
linkUrl: '',
})
const form = ref(defaultForm())
// 加载 Banner 列表
async function loadBanners() {
try {
const res: any = await getBanners()
bannerList.value = res.data || []
} catch {
// 错误已由拦截器处理
}
}
// 初始化拖拽排序
function initSortable() {
const el = tableRef.value?.$el?.querySelector('.el-table__body-wrapper tbody')
if (!el) return
Sortable.create(el, {
handle: '.drag-handle',
animation: 150,
onEnd: async ({ oldIndex, newIndex }: { oldIndex?: number; newIndex?: number }) => {
if (oldIndex == null || newIndex == null || oldIndex === newIndex) return
// 更新本地数组顺序
const list = [...bannerList.value]
const [moved] = list.splice(oldIndex, 1)
list.splice(newIndex, 0, moved)
bannerList.value = list
// 保存排序到后端
const items = list.map((b, i) => ({ id: b.id, sortOrder: i + 1 }))
try {
await updateBannerSort(items)
ElMessage.success('排序已更新')
} catch {
// 排序失败时重新加载
await loadBanners()
}
},
})
}
// 新增
function handleAdd() {
isEdit.value = false
editingId.value = ''
form.value = defaultForm()
dialogVisible.value = true
}
// 编辑
function handleEdit(row: BannerItem) {
isEdit.value = true
editingId.value = row.id
form.value = {
imageUrlZhCn: row.imageUrlZhCn,
imageUrlZhTw: row.imageUrlZhTw,
imageUrlEn: row.imageUrlEn,
linkType: row.linkType || 'none',
linkUrl: row.linkUrl || '',
}
dialogVisible.value = true
}
// 提交表单
async function handleSubmit() {
if (!form.value.imageUrlZhCn || !form.value.imageUrlZhTw || !form.value.imageUrlEn) {
ElMessage.warning('请填写所有语言的图片 URL')
return
}
submitting.value = true
try {
if (isEdit.value) {
await updateBanner(editingId.value, form.value)
ElMessage.success('更新成功')
} else {
await createBanner(form.value)
ElMessage.success('创建成功')
}
dialogVisible.value = false
await loadBanners()
} catch {
// 错误已由拦截器处理
} finally {
submitting.value = false
}
}
// 删除
async function handleDelete(id: string) {
try {
await deleteBanner(id)
ElMessage.success('删除成功')
await loadBanners()
} catch {
// 错误已由拦截器处理
}
}
onMounted(async () => {
await loadBanners()
await nextTick()
initSortable()
})
</script>
<style scoped>
.banner-manage {
padding: 20px;
}
.toolbar {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 16px;
}
</style>