| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611 |
- <template>
- <div class="home-banner-manage admin-page">
- <el-card>
- <div slot="header" class="admin-page-header">
- <span class="admin-page-title">首页广告栏</span>
- <el-button type="primary" size="mini" icon="el-icon-plus" @click="openDialog()">新增广告位</el-button>
- </div>
- <div class="filter-bar" style="margin-bottom:12px;">
- <el-select v-model="filterStatus" placeholder="启用状态" @change="loadList" clearable style="width:120px;" clearable>
- <el-option label="全部状态" value="" />
- <el-option label="已启用" value="1" />
- <el-option label="已禁用" value="0" />
- </el-select>
- <el-select v-model="filterType" placeholder="类型" @change="loadList" clearable style="width:120px;">
- <el-option label="全部类型" value="" />
- <el-option label="活动" value="activity" />
- <el-option label="商品" value="product" />
- <el-option label="链接" value="link" />
- </el-select>
- <el-button type="primary" size="mini" @click="loadList">查询</el-button>
- <el-button size="mini" @click="resetFilter">重置</el-button>
- </div>
- <div class="table-scroll-wrap-sm">
- <el-table :max-height="tableHeight" :data="list" v-loading="loading" border stripe>
- <el-table-column prop="id" label="ID" width="60" />
- <el-table-column label="类型" width="80">
- <template slot-scope="{ row }">
- <el-tag size="mini" :type="typeTag(row.bannerType)">{{ typeLabel(row.bannerType) }}</el-tag>
- </template>
- </el-table-column>
- <el-table-column label="标题" min-width="120" show-overflow-tooltip>
- <template slot="header"><span class="sort-header" @click.stop="onSortClick('title')">标题<span v-if="sortIndex('title') !== null" class="sort-index" :class="sortClass('title') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('title') }}</span>{{ sortIcon('title') }}</span></template>
- <template slot-scope="{ row }">{{ row.title }}</template>
- </el-table-column>
- <el-table-column label="关联内容" width="160">
- <template slot-scope="{ row }">
- <div v-if="row.bannerType === 'activity' && row.refId">
- <el-tag size="mini" type="primary">活动 #{{ row.refId }}</el-tag>
- </div>
- <div v-else-if="row.bannerType === 'product' && row.refId">
- <el-tag size="mini" type="info">商品 #{{ row.refId }}</el-tag>
- </div>
- <div v-else-if="row.bannerType === 'link' && row.linkUrl">
- <el-tag size="mini" type="info">{{ row.linkUrl }}</el-tag>
- </div>
- <div v-else style="color:#bbb">-</div>
- </template>
- </el-table-column>
- <el-table-column label="封面" width="80">
- <template slot-scope="{ row }">
- <img v-if="row.coverImage" :src="row.coverImage" style="width:50px;height:35px;object-fit:cover;border-radius:4px;" />
- <span v-else style="color:#999">无</span>
- </template>
- </el-table-column>
- <el-table-column label="排序" width="80">
- <template slot-scope="{ row }">
- <el-input-number v-model="row.sortOrder" :min="0" :max="999" size="mini" @change="onSortChange(row)" controls-position="right" />
- </template>
- </el-table-column>
- <el-table-column label="投放时间" width="180">
- <template slot-scope="{ row }">
- <span v-if="row.startTime || row.endTime">{{ formatDate(row.startTime) }} ~ {{ formatDate(row.endTime) }}</span>
- <span v-else style="color:#bbb">不限</span>
- </template>
- </el-table-column>
- <el-table-column label="状态" width="80">
- <template slot-scope="{ row }">
- <el-switch v-model="row.enabled" :active-value="1" :inactive-value="0" @change="onToggle(row)" />
- </template>
- </el-table-column>
- <el-table-column prop="createdAt" label="创建时间" width="160">
- <template slot-scope="{ row }">{{ formatTime(row.createdAt) }}</template>
- </el-table-column>
- <el-table-column label="操作" width="140" fixed="right">
- <template slot-scope="{ row }">
- <el-button size="mini" type="primary" @click="openDialog(row)">编辑</el-button>
- <el-button size="mini" type="danger" @click="handleDelete(row)">删除</el-button>
- </template>
- </el-table-column>
- </el-table>
- </div>
- <div class="pagination-wrap" style="margin-top:16px;text-align:right;">
- <el-pagination
- @current-change="handlePageChange"
- :current-page="page"
- :page-size="pageSize"
- :total="total"
- layout="total, prev, pager, next"
- background
- />
- </div>
- </el-card>
- <!-- 编辑弹窗 -->
- <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="800px" :close-on-click-modal="false">
- <el-form :model="form" :rules="rules" ref="bannerForm" label-width="150px">
- <el-form-item label="标题" prop="title">
- <el-input v-model="form.title" placeholder="展示标题,最多50字" maxlength="50" show-word-limit />
- </el-form-item>
- <el-form-item label="类型" prop="bannerType">
- <el-select v-model="form.bannerType" placeholder="选择类型" style="width:100%;">
- <el-option label="活动" value="activity" />
- <el-option label="商品" value="product" />
- <el-option label="链接" value="link" />
- </el-select>
- </el-form-item>
- <!-- 活动选择器 -->
- <el-form-item v-if="form.bannerType === 'activity'" label="关联活动">
- <el-select
- v-model="form.refId"
- placeholder="搜索活动"
- filterable
- :remote="true"
- :remote-method="searchActivities"
- style="width:100%"
- clearable
- >
- <el-option key="" label="-- 请选择 --" value="">
- </el-option>
- </el-select>
- </el-form-item>
- <!-- 商品选择器 -->
- <el-form-item v-if="form.bannerType === 'product'" label="关联商品">
- <el-select
- v-model="form.refId"
- placeholder="搜索商品"
- filterable
- :remote="true"
- :remote-method="searchProducts"
- style="width:100%"
- clearable
- >
- <el-option key="" label="-- 请选择 --" value="">
- </el-option>
- </el-select>
- </el-form-item>
- <!-- 链接类型表单 -->
- <el-form-item v-if="form.bannerType === 'link'" label="跳转链接">
- <el-input v-model="form.linkUrl" placeholder="小程序页面路径(如 /pages/activity/activity-detail/activity-detail?id=123)" />
- <el-button type="success" size="mini" @click="selectMiniPage">选择小程序页面</el-button>
- </el-form-item>
- <!-- 封面图 -->
- <el-form-item label="封面图">
- <el-upload
- :action="uploadActionUrl"
- :headers="uploadHeaders"
- :on-success="handleUploadSuccess"
- :on-error="handleUploadError"
- :before-upload="beforeUpload"
- :show-file-list="false"
- accept="image/jpeg,image/png,image/gif,image/webp"
- >
- <img v-if="form.coverImage" :src="form.coverImage" class="cover-preview" />
- <el-button v-else type="primary" plain>+ 上传封面</el-button>
- </el-upload>
- <div v-if="form.coverImage" style="margin-top:8px;">
- <el-image :src="form.coverImage" style="width:200px;height:120px;" fit="cover" />
- </div>
- <el-button v-if="form.coverImage" size="mini" type="text" style="margin-left:10px;" @click="form.coverImage = ''">清除</el-button>
- <div style="margin-top:4px;font-size:12px;color:#999;">建议尺寸 750×320rpx(约 2.35:1),单张不超过 5MB</div>
- </el-form-item>
- <el-form-item label="排序">
- <el-input-number v-model="form.sortOrder" :min="0" :max="999" style="width:100px;" />
- </el-form-item>
- <el-form-item label="投放时间">
- <el-date-picker
- v-model="form.startTime"
- type="datetime"
- placeholder="开始时间"
- format="yyyy-MM-dd HH:mm:ss"
- value-format="timestamp"
- style="margin-right:8px;"
- />
- <span>至</span>
- <el-date-picker
- v-model="form.endTime"
- type="datetime"
- placeholder="结束时间"
- format="yyyy-MM-dd HH:mm:ss"
- value-format="timestamp"
- style="margin-left:8px;"
- />
- </el-form-item>
- <el-form-item label="启用">
- <el-switch v-model="form.enabled" :active-value="1" :inactive-value="0" />
- </el-form-item>
- </el-form>
- <div slot="footer">
- <el-button @click="dialogVisible = false">取消</el-button>
- <el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
- </div>
- </el-dialog>
- </div>
- </template>
- <script>
- import { getHomeBannerList, saveHomeBanner, deleteHomeBanner, batchDeleteHomeBanner, toggleHomeBanner, updateBannerSort } from '@/api/homeBanner'
- import { getActivityList } from '@/api/activity'
- import { getProductList } from '@/api/admin'
- // 小程序已注册页面(供链接类型选择)
- const MINI_PAGES = [
- { path: '/pages/activity/index', label: '活动中心' },
- { path: '/pages/discover/index', label: '发现' },
- { path: '/pages/shop/index/index', label: '商城' },
- { path: '/pages/article-center/index', label: '知识分享' },
- { path: '/pages/diet/index', label: '饮食' },
- { path: '/pages/health/report-list', label: '健康报告' },
- { path: '/pages/energy/index', label: '五维能量' },
- { path: '/pages/membership/index', label: '会员中心' },
- { path: '/pages/promotion/index', label: '财富中心' },
- { path: '/pages/tianpan/index', label: '家庭天盘' },
- { path: '/pages/assessment/apply', label: '申请测评' },
- { path: '/pages/tasks/tasks', label: '任务列表' }
- ]
- export default {
- data() {
- return {
- list: [],
- loading: false,
- page: 1,
- pageSize: 10,
- total: 0,
- filterStatus: '',
- filterType: '',
- dialogVisible: false,
- submitLoading: false,
- editId: null,
- form: {
- id: null,
- title: '',
- bannerType: 'activity',
- refId: null,
- coverImage: '',
- linkUrl: '',
- sortOrder: 0,
- enabled: 1,
- startTime: null,
- endTime: null
- },
- rules: {
- title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
- bannerType: [{ required: true, message: '请选择类型', trigger: 'change' }],
- refId: [
- {
- required: true,
- message: '请选择关联内容',
- trigger: 'blur',
- validator: (rule, value, callback) => {
- if (this.form.bannerType === 'link') {
- callback()
- } else if (!value || value <= 0) {
- callback(new Error('请选择关联内容'))
- } else {
- callback()
- }
- }
- }
- ],
- // 封面图必填(产品类型)
- coverImage: [
- {
- required: true,
- message: '请上传封面图',
- trigger: 'change'
- }
- ]
- },
- // 活动搜索状态
- activitySearchKeyword: '',
- activityLoading: false,
- activityOptions: [],
- activityPage: 1,
- // 商品搜索状态
- productSearchKeyword: '',
- productLoading: false,
- productOptions: [],
- productPage: 1,
- showPagePicker: false
- }
- },
- computed: {
- tableHeight() {
- return window.innerHeight - 280
- },
- dialogTitle() {
- return this.editId ? '编辑广告位' : '新增广告位'
- },
- uploadActionUrl() {
- const baseURL = process.env.VUE_APP_BASE_API || ''
- return baseURL + '/api/admin/articles/upload/image'
- },
- uploadHeaders() {
- const token = localStorage.getItem('token')
- return token ? { Authorization: 'Bearer ' + token } : {}
- }
- },
- mounted() {
- this.loadList()
- },
- methods: {
- loadList() {
- var self = this
- self.loading = true
- var params = {
- current: self.page,
- size: self.pageSize
- }
- if (self.filterStatus !== '') params.enabled = self.filterStatus
- if (self.filterType !== '') params.bannerType = self.filterType
- getHomeBannerList(params).then(function(res) {
- self.loading = false
- if (res.data && res.data.code === 200) {
- var d = res.data.data || {}
- self.list = d.list || []
- self.total = d.total || 0
- } else if (res.data && res.data.list !== undefined) {
- self.list = res.data.list || []
- self.total = res.data.total || 0
- }
- }).catch(function() {
- self.loading = false
- self.$message.error('加载失败')
- })
- },
- handlePageChange(newPage) {
- this.page = newPage
- this.loadList()
- },
- resetFilter() {
- this.filterStatus = ''
- this.filterType = ''
- this.page = 1
- this.loadList()
- },
- openDialog(row) {
- if (row) {
- this.editId = row.id
- this.form = {
- id: row.id,
- title: row.title,
- bannerType: row.bannerType,
- refId: row.refId,
- coverImage: row.coverImage || '',
- linkUrl: row.linkUrl || '',
- sortOrder: row.sortOrder || 0,
- enabled: row.enabled != null ? row.enabled : 1,
- startTime: row.startTime || null,
- endTime: row.endTime || null
- }
- // 根据类型预填搜索结果
- if (row.bannerType === 'activity') {
- this.activitySearchKeyword = ''
- this.activityOptions = []
- this.loadActivityOptions()
- } else if (row.bannerType === 'product') {
- this.productSearchKeyword = ''
- this.productOptions = []
- this.loadProductOptions()
- }
- } else {
- this.editId = null
- this.form = {
- id: null,
- title: '',
- bannerType: 'activity',
- refId: null,
- coverImage: '',
- linkUrl: '',
- sortOrder: 0,
- enabled: 1,
- startTime: null,
- endTime: null
- }
- this.activityOptions = []
- this.productOptions = []
- }
- this.dialogVisible = true
- this.$nextTick(function() {
- if (this.$refs.bannerForm) this.$refs.bannerForm.clearValidate()
- }.bind(this))
- },
- handleSubmit() {
- var self = this
- self.$refs.bannerForm.validate(function(valid) {
- if (!valid) return
- self.submitLoading = true
- var saveData = Object.assign({}, self.form)
- // 如果类型是link,清空refId;反之清空linkUrl
- if (saveData.bannerType === 'link') {
- saveData.refId = null
- } else {
- saveData.linkUrl = ''
- }
- saveHomeBanner(saveData).then(function(res) {
- self.submitLoading = false
- if (res.data && res.data.code === 200) {
- self.$message.success('保存成功')
- self.dialogVisible = false
- self.loadList()
- } else {
- self.$message.error(res.data && res.data.message ? res.data.message : '保存失败')
- }
- }).catch(function(e) {
- self.submitLoading = false
- self.$message.error('保存失败')
- })
- })
- },
- handleDelete(row) {
- var self = this
- this.$confirm('确定删除该广告位?', '提示', { type: 'warning' }).then(function() {
- deleteHomeBanner({ id: row.id }).then(function(res) {
- if (res.data && res.data.code === 200) {
- self.$message.success('删除成功')
- self.loadList()
- } else {
- self.$message.error('删除失败')
- }
- }).catch(function() {
- self.$message.error('删除失败')
- })
- }).catch(function() {})
- },
- onToggle(row) {
- toggleHomeBanner({ id: row.id, enabled: row.enabled }).then(function(res) {
- if (res.data && res.data.code === 200) {
- self.$message.success(row.enabled === 1 ? '已启用' : '已禁用')
- }
- }).catch(function() {})
- },
- onSortChange(row) {
- updateBannerSort({ id: row.id, sortOrder: row.sortOrder }).catch(function() {})
- },
- // --- 活动搜索 ---
- searchActivities: async function(query) {
- this.activityLoading = true
- this.activityOptions = []
- try {
- const res = await getActivityList({
- keyword: query || undefined,
- page: 1,
- size: 20
- })
- const data = (res && res.data) || {}
- this.activityOptions = data.records || []
- this.activityPage = 1
- this.activityLoading = false
- } catch (e) {
- console.error('[HomeBanner] 搜索活动失败', e)
- this.activityLoading = false
- }
- },
- loadActivityOptions() {
- this.searchActivities(this.activitySearchKeyword)
- },
- // 商品搜索
- searchProducts: async function(query) {
- this.productLoading = true
- this.productOptions = []
- try {
- const res = await getProductList({
- keyword: query || undefined,
- page: 1,
- size: 20
- })
- const data = (res && res.data) || {}
- this.productOptions = data.records || []
- this.productPage = 1
- this.productLoading = false
- } catch (e) {
- console.error('[HomeBanner] 搜索商品失败', e)
- this.productLoading = false
- }
- },
- loadProductOptions() {
- this.searchProducts(this.productSearchKeyword)
- },
- handleUploadSuccess(res) {
- if (res && res.code === 200 && res.data && res.data.url) {
- this.form.coverImage = res.data.url
- this..success('上传成功')
- } else {
- this..error('上传失败')
- }
- },
- handleUploadError(err) {
- console.error('[HomeBanner] 上传失败', err)
- this..error('图片上传失败,请重试')
- },
- beforeUpload(file) {
- const isImage = file.type.startsWith('image/jpeg') || file.type.startsWith('image/png') || file.type.startsWith('image/gif') || file.type.startsWith('image/webp')
- if (!isImage) {
- this..error('只能上传图片格式(jpg/png/gif/webp)')
- return false
- }
- const isLt5M = file.size / 1024 / 1024 < 5
- if (!isLt5M) {
- this..error('图片大小不能超过 5MB')
- return false
- }
- return true
- },
- selectMiniPage() {
- this.showPagePicker = true
- },
- selectPage(path) {
- this.form.linkUrl = path
- this.showPagePicker = false
- },
- // --- 工具方法 ---
- typeLabel(type) {
- var map = { activity: '活动', product: '商品', link: '链接' }
- return map[type] || type || '-'
- },
- typeTag(type) {
- var map = { activity: 'success', product: 'warning', link: 'info' }
- return map[type] || ''
- },
- sortIndex(col) {
- return this.sortCol === col ? (this.sortDir === 'asc' ? 1 : 2) : null
- },
- sortClass(col) {
- return this.sortCol === col ? this.sortDir : ''
- },
- sortIcon(col) {
- if (this.sortCol !== col) return '↕'
- return this.sortDir === 'asc' ? '↑' : '↓'
- },
- onSortClick(col) {
- if (this.sortCol === col) {
- this.sortDir = this.sortDir === 'asc' ? 'desc' : 'asc'
- } else {
- this.sortCol = col
- this.sortDir = 'asc'
- }
- this.loadList()
- },
- formatDate(ts) {
- if (!ts) return '-'
- var d = new Date(ts)
- if (isNaN(d.getTime())) return '-'
- var y = d.getFullYear()
- var m = String(d.getMonth() + 1).padStart(2, '0')
- var day = String(d.getDate()).padStart(2, '0')
- var h = String(d.getHours()).padStart(2, '0')
- var min = String(d.getMinutes()).padStart(2, '0')
- var s = String(d.getSeconds()).padStart(2, '0')
- return y + '-' + m + '-' + day + ' ' + h + ':' + min + ':' + s
- },
- formatTime(t) {
- if (!t) return '-'
- var ts = t
- if (typeof t === 'string' && t.indexOf('T') > -1) {
- ts = new Date(t).getTime()
- }
- return this.formatDate(ts)
- },
- // 选择小程序页面
- selectMiniPage() {
- this.$message.info('小程序页面选择功能待后端开发')
- },
- beforeUpload(file) {
- const isJPG = file.type === 'image/jpeg'
- const isPNG = file.type === 'image/png'
- const isGIF = file.type === 'image/gif'
- const isQQ = file.type === 'image/webp'
- if (!isJPG && !isPNG && !isGIF && !isQQ) {
- this.$message.error('只能上传 jpg/png/gif/webp 格式的图片!')
- return false
- }
- this.$message.warning('上传图片受限,请使用后台上传接口上传封面图')
- return false
- }
- }
- }
- </script>
- <style scoped>
- .admin-page-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
- .admin-page-title {
- font-size: 18px;
- font-weight: 600;
- color: #303133;
- }
- .filter-bar {
- display: flex;
- align-items: center;
- gap: 12px;
- flex-wrap: wrap;
- }
- </style>
|