HomeBannerManage.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652
  1. <template>
  2. <div class="home-banner-manage admin-page">
  3. <el-card>
  4. <PageHeader slot="header" title="首页广告栏">
  5. <el-button type="primary" size="mini" icon="el-icon-plus" @click="openDialog()">新增广告位</el-button>
  6. </PageHeader>
  7. <div class="filter-bar" style="margin-bottom:12px;">
  8. <el-select v-model="filterStatus" placeholder="启用状态" @change="loadList" clearable style="width:120px;" clearable>
  9. <el-option label="全部状态" value="" />
  10. <el-option label="已启用" value="1" />
  11. <el-option label="已禁用" value="0" />
  12. </el-select>
  13. <el-select v-model="filterType" placeholder="类型" @change="loadList" clearable style="width:120px;">
  14. <el-option label="全部类型" value="" />
  15. <el-option label="活动" value="activity" />
  16. <el-option label="商品" value="product" />
  17. <el-option label="文章" value="article" />
  18. <el-option label="链接" value="link" />
  19. </el-select>
  20. <el-button type="primary" size="mini" @click="loadList">查询</el-button>
  21. <el-button size="mini" @click="resetFilter">重置</el-button>
  22. </div>
  23. <div class="adm-table-wrap">
  24. <el-table height="100%" :data="list" v-loading="loading" border stripe>
  25. <el-table-column prop="id" label="ID" width="60" />
  26. <el-table-column label="类型" width="80">
  27. <template slot-scope="{ row }">
  28. <el-tag size="mini" :type="typeTag(row.bannerType)">{{ typeLabel(row.bannerType) }}</el-tag>
  29. </template>
  30. </el-table-column>
  31. <el-table-column label="标题" min-width="120" show-overflow-tooltip>
  32. <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>
  33. <template slot-scope="{ row }">{{ row.title }}</template>
  34. </el-table-column>
  35. <el-table-column label="关联内容" width="160">
  36. <template slot-scope="{ row }">
  37. <div v-if="row.bannerType === 'activity' && row.refId">
  38. <el-tag size="mini" type="primary">活动 #{{ row.refId }}</el-tag>
  39. </div>
  40. <div v-else-if="row.bannerType === 'product' && row.refId">
  41. <el-tag size="mini" type="info">商品 #{{ row.refId }}</el-tag>
  42. </div>
  43. <div v-else-if="row.bannerType === 'article' && row.refId">
  44. <el-tag size="mini" type="success">文章 #{{ row.refId }}</el-tag>
  45. </div>
  46. <div v-else-if="row.bannerType === 'link' && row.linkUrl">
  47. <el-tag size="mini" type="info">{{ row.linkUrl }}</el-tag>
  48. </div>
  49. <div v-else style="color:#bbb">-</div>
  50. </template>
  51. </el-table-column>
  52. <el-table-column label="封面" width="80">
  53. <template slot-scope="{ row }">
  54. <img v-if="row.coverImage" :src="row.coverImage" style="width:50px;height:35px;object-fit:cover;border-radius:4px;" />
  55. <span v-else style="color:#999">无</span>
  56. </template>
  57. </el-table-column>
  58. <el-table-column label="排序" width="80">
  59. <template slot-scope="{ row }">
  60. <el-input-number v-model="row.sortOrder" :min="0" :max="999" size="mini" @change="onSortChange(row)" controls-position="right" />
  61. </template>
  62. </el-table-column>
  63. <el-table-column label="投放时间" width="180">
  64. <template slot-scope="{ row }">
  65. <span v-if="row.startTime || row.endTime">{{ formatDate(row.startTime) }} ~ {{ formatDate(row.endTime) }}</span>
  66. <span v-else style="color:#bbb">不限</span>
  67. </template>
  68. </el-table-column>
  69. <el-table-column label="状态" width="80">
  70. <template slot-scope="{ row }">
  71. <el-switch v-model="row.enabled" :active-value="1" :inactive-value="0" @change="onToggle(row)" />
  72. </template>
  73. </el-table-column>
  74. <el-table-column prop="createdAt" label="创建时间" width="160">
  75. <template slot-scope="{ row }">{{ formatTime(row.createdAt) }}</template>
  76. </el-table-column>
  77. <el-table-column label="操作" width="140" fixed="right">
  78. <template slot-scope="{ row }">
  79. <el-button size="mini" type="primary" @click="openDialog(row)">编辑</el-button>
  80. <el-button size="mini" type="danger" @click="handleDelete(row)">删除</el-button>
  81. </template>
  82. </el-table-column>
  83. </el-table>
  84. </div>
  85. <div class="pagination-wrap" style="margin-top:16px;text-align:right;">
  86. <el-pagination
  87. @current-change="handlePageChange"
  88. :current-page="page"
  89. :page-size="pageSize"
  90. :total="total"
  91. layout="total, prev, pager, next"
  92. background
  93. />
  94. </div>
  95. </el-card>
  96. <!-- 编辑弹窗 -->
  97. <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="800px" :close-on-click-modal="false">
  98. <el-form :model="form" :rules="rules" ref="bannerForm" label-width="150px">
  99. <el-form-item label="标题" prop="title">
  100. <el-input v-model="form.title" placeholder="展示标题,最多50字" maxlength="50" show-word-limit />
  101. </el-form-item>
  102. <el-form-item label="类型" prop="bannerType">
  103. <el-select v-model="form.bannerType" placeholder="选择类型" style="width:100%;">
  104. <el-option label="活动" value="activity" />
  105. <el-option label="商品" value="product" />
  106. <el-option label="文章" value="article" />
  107. <el-option label="链接" value="link" />
  108. </el-select>
  109. </el-form-item>
  110. <!-- 活动选择器 -->
  111. <el-form-item v-if="form.bannerType === 'activity'" label="关联活动">
  112. <el-select
  113. v-model="form.refId"
  114. placeholder="搜索活动"
  115. filterable
  116. :remote="true"
  117. :remote-method="searchActivities"
  118. :loading="activityLoading"
  119. style="width:100%"
  120. clearable
  121. >
  122. <el-option v-for="item in activityOptions" :key="item.id" :label="item.title + ' (ID:' + item.id + ')'" :value="item.id" />
  123. <el-option v-if="activityOptions.length === 0 && !activityLoading" label="暂无数据" value="" disabled />
  124. </el-select>
  125. </el-form-item>
  126. <!-- 商品选择器 -->
  127. <el-form-item v-if="form.bannerType === 'product'" label="关联商品">
  128. <el-select
  129. v-model="form.refId"
  130. placeholder="搜索商品"
  131. filterable
  132. :remote="true"
  133. :remote-method="searchProducts"
  134. :loading="productLoading"
  135. style="width:100%"
  136. clearable
  137. >
  138. <el-option v-for="item in productOptions" :key="item.id" :label="item.name + ' (¥' + (item.price/100).toFixed(2) + ')'" :value="item.id" />
  139. <el-option v-if="productOptions.length === 0 && !productLoading" label="暂无数据" value="" disabled />
  140. </el-select>
  141. </el-form-item>
  142. <!-- 文章选择器 -->
  143. <el-form-item v-if="form.bannerType === 'article'" label="关联文章">
  144. <el-select
  145. v-model="form.refId"
  146. placeholder="搜索文章"
  147. filterable
  148. :remote="true"
  149. :remote-method="searchArticles"
  150. :loading="articleLoading"
  151. style="width:100%"
  152. clearable
  153. >
  154. <el-option v-for="item in articleOptions" :key="item.id" :label="item.title + ' (ID:' + item.id + ')'" :value="item.id" />
  155. <el-option v-if="articleOptions.length === 0 && !articleLoading" label="暂无数据" value="" disabled />
  156. </el-select>
  157. </el-form-item>
  158. <!-- 链接类型表单 -->
  159. <el-form-item v-if="form.bannerType === 'link'" label="跳转链接">
  160. <el-input v-model="form.linkUrl" placeholder="小程序页面路径(如 /pages/activity/activity-detail/activity-detail?id=123)" />
  161. <el-button type="success" size="mini" @click="selectMiniPage">选择小程序页面</el-button>
  162. </el-form-item>
  163. <!-- 封面图 -->
  164. <el-form-item label="封面图">
  165. <el-upload
  166. :action="uploadActionUrl"
  167. :headers="uploadHeaders"
  168. :on-success="handleUploadSuccess"
  169. :on-error="handleUploadError"
  170. :before-upload="beforeUpload"
  171. :show-file-list="false"
  172. accept="image/jpeg,image/png,image/gif,image/webp"
  173. >
  174. <img v-if="form.coverImage" :src="form.coverImage" class="cover-preview" />
  175. <el-button v-else type="primary" plain>+ 上传封面</el-button>
  176. </el-upload>
  177. <div v-if="form.coverImage" style="margin-top:8px;">
  178. <el-image :src="form.coverImage" style="width:200px;height:120px;" fit="cover" />
  179. </div>
  180. <el-button v-if="form.coverImage" size="mini" type="text" style="margin-left:10px;" @click="form.coverImage = ''">清除</el-button>
  181. <div style="margin-top:4px;font-size:12px;color:#999;">建议尺寸 750×320rpx(约 2.35:1),单张不超过 5MB</div>
  182. </el-form-item>
  183. <el-form-item label="排序">
  184. <el-input-number v-model="form.sortOrder" :min="0" :max="999" style="width:100px;" />
  185. </el-form-item>
  186. <el-form-item label="投放时间">
  187. <el-date-picker
  188. v-model="form.startTime"
  189. type="datetime"
  190. placeholder="开始时间"
  191. format="yyyy-MM-dd HH:mm:ss"
  192. value-format="timestamp"
  193. style="margin-right:8px;"
  194. />
  195. <span>至</span>
  196. <el-date-picker
  197. v-model="form.endTime"
  198. type="datetime"
  199. placeholder="结束时间"
  200. format="yyyy-MM-dd HH:mm:ss"
  201. value-format="timestamp"
  202. style="margin-left:8px;"
  203. />
  204. </el-form-item>
  205. <el-form-item label="启用">
  206. <el-switch v-model="form.enabled" :active-value="1" :inactive-value="0" />
  207. </el-form-item>
  208. </el-form>
  209. <div slot="footer">
  210. <el-button @click="dialogVisible = false">取消</el-button>
  211. <el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
  212. </div>
  213. </el-dialog>
  214. </div>
  215. </template>
  216. <script>
  217. import { getHomeBannerList, saveHomeBanner, deleteHomeBanner, batchDeleteHomeBanner, toggleHomeBanner, updateBannerSort } from '@/api/homeBanner'
  218. import { getActivityList } from '@/api/activity'
  219. import { getProductList } from '@/api/admin'
  220. import { adminArticleList } from '@/api/article'
  221. // 小程序已注册页面(供链接类型选择)
  222. const MINI_PAGES = [
  223. { path: '/pages/activity/index', label: '活动中心' },
  224. { path: '/pages/discover/index', label: '发现' },
  225. { path: '/pages/shop/index/index', label: '商城' },
  226. { path: '/pages/article-center/index', label: '知识分享' },
  227. { path: '/pages/diet/index', label: '饮食' },
  228. { path: '/pages/health/report-list', label: '健康报告' },
  229. { path: '/pages/energy/index', label: '五维能量' },
  230. { path: '/pages/membership/index', label: '会员中心' },
  231. { path: '/pages/promotion/index', label: '财富中心' },
  232. { path: '/pages/tianpan/index', label: '家庭天盘' },
  233. { path: '/pages/assessment/apply', label: '申请测评' },
  234. { path: '/pages/tasks/tasks', label: '任务列表' }
  235. ]
  236. export default {
  237. data() {
  238. return {
  239. list: [],
  240. loading: false,
  241. page: 1,
  242. pageSize: 10,
  243. total: 0,
  244. filterStatus: '',
  245. filterType: '',
  246. dialogVisible: false,
  247. submitLoading: false,
  248. editId: null,
  249. form: {
  250. id: null,
  251. title: '',
  252. bannerType: 'activity',
  253. refId: null,
  254. coverImage: '',
  255. linkUrl: '',
  256. sortOrder: 0,
  257. enabled: 1,
  258. startTime: null,
  259. endTime: null
  260. },
  261. rules: {
  262. title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
  263. bannerType: [{ required: true, message: '请选择类型', trigger: 'change' }],
  264. refId: [
  265. {
  266. required: true,
  267. message: '请选择关联内容',
  268. trigger: 'blur',
  269. validator: (rule, value, callback) => {
  270. if (this.form.bannerType === 'link') {
  271. callback()
  272. } else if (!value || value <= 0) {
  273. callback(new Error('请选择关联内容'))
  274. } else {
  275. callback()
  276. }
  277. }
  278. }
  279. ],
  280. // 封面图必填(产品类型)
  281. coverImage: [
  282. {
  283. required: true,
  284. message: '请上传封面图',
  285. trigger: 'change'
  286. }
  287. ]
  288. },
  289. // 活动搜索状态
  290. activitySearchKeyword: '',
  291. activityLoading: false,
  292. activityOptions: [],
  293. activityPage: 1,
  294. // 商品搜索状态
  295. productSearchKeyword: '',
  296. productLoading: false,
  297. productOptions: [],
  298. productPage: 1,
  299. // 文章搜索状态
  300. articleSearchKeyword: '',
  301. articleLoading: false,
  302. articleOptions: [],
  303. articlePage: 1,
  304. showPagePicker: false
  305. }
  306. },
  307. computed: {
  308. tableHeight() {
  309. return window.innerHeight - 280
  310. },
  311. dialogTitle() {
  312. return this.editId ? '编辑广告位' : '新增广告位'
  313. },
  314. uploadActionUrl() {
  315. const baseURL = process.env.VUE_APP_BASE_API || ''
  316. return baseURL + '/api/admin/articles/upload/image'
  317. },
  318. uploadHeaders() {
  319. const token = localStorage.getItem('token')
  320. return token ? { Authorization: 'Bearer ' + token } : {}
  321. }
  322. },
  323. mounted() {
  324. this.loadList()
  325. },
  326. methods: {
  327. loadList() {
  328. var self = this
  329. self.loading = true
  330. var params = {
  331. current: self.page,
  332. size: self.pageSize
  333. }
  334. if (self.filterStatus !== '') params.enabled = self.filterStatus
  335. if (self.filterType !== '') params.bannerType = self.filterType
  336. getHomeBannerList(params).then(function(res) {
  337. self.loading = false
  338. if (res.data && res.data.code === 200) {
  339. var d = res.data.data || {}
  340. self.list = d.list || []
  341. self.total = d.total || 0
  342. } else if (res.data && res.data.list !== undefined) {
  343. self.list = res.data.list || []
  344. self.total = res.data.total || 0
  345. }
  346. }).catch(function() {
  347. self.loading = false
  348. self.$message.error('加载失败')
  349. })
  350. },
  351. handlePageChange(newPage) {
  352. this.page = newPage
  353. this.loadList()
  354. },
  355. resetFilter() {
  356. this.filterStatus = ''
  357. this.filterType = ''
  358. this.page = 1
  359. this.loadList()
  360. },
  361. openDialog(row) {
  362. if (row) {
  363. this.editId = row.id
  364. this.form = {
  365. id: row.id,
  366. title: row.title,
  367. bannerType: row.bannerType,
  368. refId: row.refId,
  369. coverImage: row.coverImage || '',
  370. linkUrl: row.linkUrl || '',
  371. sortOrder: row.sortOrder || 0,
  372. enabled: row.enabled != null ? row.enabled : 1,
  373. startTime: row.startTime || null,
  374. endTime: row.endTime || null
  375. }
  376. // 根据类型预填搜索结果
  377. if (row.bannerType === 'activity') {
  378. this.activitySearchKeyword = ''
  379. this.activityOptions = []
  380. this.loadActivityOptions()
  381. } else if (row.bannerType === 'product') {
  382. this.productSearchKeyword = ''
  383. this.productOptions = []
  384. this.loadProductOptions()
  385. } else if (row.bannerType === 'article') {
  386. this.articleSearchKeyword = ''
  387. this.articleOptions = []
  388. this.loadArticleOptions()
  389. }
  390. } else {
  391. this.editId = null
  392. this.form = {
  393. id: null,
  394. title: '',
  395. bannerType: 'activity',
  396. refId: null,
  397. coverImage: '',
  398. linkUrl: '',
  399. sortOrder: 0,
  400. enabled: 1,
  401. startTime: null,
  402. endTime: null
  403. }
  404. this.activityOptions = []
  405. this.productOptions = []
  406. this.articleOptions = []
  407. }
  408. this.dialogVisible = true
  409. this.$nextTick(function() {
  410. if (this.$refs.bannerForm) this.$refs.bannerForm.clearValidate()
  411. }.bind(this))
  412. },
  413. handleSubmit() {
  414. var self = this
  415. self.$refs.bannerForm.validate(function(valid) {
  416. if (!valid) return
  417. self.submitLoading = true
  418. var saveData = Object.assign({}, self.form)
  419. // 如果类型是link,清空refId;反之清空linkUrl
  420. if (saveData.bannerType === 'link') {
  421. saveData.refId = null
  422. } else {
  423. saveData.linkUrl = ''
  424. }
  425. saveHomeBanner(saveData).then(function(res) {
  426. self.submitLoading = false
  427. if (res.data && res.data.code === 200) {
  428. self.$message.success('保存成功')
  429. self.dialogVisible = false
  430. self.loadList()
  431. } else {
  432. self.$message.error(res.data && res.data.message ? res.data.message : '保存失败')
  433. }
  434. }).catch(function(e) {
  435. self.submitLoading = false
  436. self.$message.error('保存失败')
  437. })
  438. })
  439. },
  440. handleDelete(row) {
  441. var self = this
  442. this.$confirm('确定删除该广告位?', '提示', { type: 'warning' }).then(function() {
  443. deleteHomeBanner({ id: row.id }).then(function(res) {
  444. if (res.data && res.data.code === 200) {
  445. self.$message.success('删除成功')
  446. self.loadList()
  447. } else {
  448. self.$message.error('删除失败')
  449. }
  450. }).catch(function() {
  451. self.$message.error('删除失败')
  452. })
  453. }).catch(function() {})
  454. },
  455. onToggle(row) {
  456. toggleHomeBanner({ id: row.id, enabled: row.enabled }).then(function(res) {
  457. if (res.data && res.data.code === 200) {
  458. self.$message.success(row.enabled === 1 ? '已启用' : '已禁用')
  459. }
  460. }).catch(function() {})
  461. },
  462. onSortChange(row) {
  463. updateBannerSort({ id: row.id, sortOrder: row.sortOrder }).catch(function() {})
  464. },
  465. // --- 活动搜索 ---
  466. searchActivities: async function(query) {
  467. this.activityLoading = true
  468. this.activityOptions = []
  469. try {
  470. const res = await getActivityList({
  471. keyword: query || undefined,
  472. page: 1,
  473. size: 20
  474. })
  475. const data = (res && res.data) || {}
  476. this.activityOptions = data.records || []
  477. this.activityPage = 1
  478. this.activityLoading = false
  479. } catch (e) {
  480. console.error('[HomeBanner] 搜索活动失败', e)
  481. this.activityLoading = false
  482. }
  483. },
  484. loadActivityOptions() {
  485. this.searchActivities(this.activitySearchKeyword)
  486. },
  487. // 商品搜索
  488. searchProducts: async function(query) {
  489. this.productLoading = true
  490. this.productOptions = []
  491. try {
  492. const res = await getProductList({
  493. keyword: query || undefined,
  494. page: 1,
  495. size: 20
  496. })
  497. const data = (res && res.data) || {}
  498. this.productOptions = data.records || []
  499. this.productPage = 1
  500. this.productLoading = false
  501. } catch (e) {
  502. console.error('[HomeBanner] 搜索商品失败', e)
  503. this.productLoading = false
  504. }
  505. },
  506. loadProductOptions() {
  507. this.searchProducts(this.productSearchKeyword)
  508. },
  509. // 文章搜索
  510. searchArticles: async function(query) {
  511. this.articleLoading = true
  512. this.articleOptions = []
  513. try {
  514. const res = await adminArticleList({
  515. keyword: query || undefined,
  516. current: 1,
  517. size: 20
  518. })
  519. const data = (res && res.data) || {}
  520. this.articleOptions = data.list || data.records || []
  521. this.articlePage = 1
  522. this.articleLoading = false
  523. } catch (e) {
  524. console.error('[HomeBanner] 搜索文章失败', e)
  525. this.articleLoading = false
  526. }
  527. },
  528. loadArticleOptions() {
  529. this.searchArticles(this.articleSearchKeyword)
  530. },
  531. handleUploadSuccess(res) {
  532. if (res && res.code === 200 && res.data && res.data.url) {
  533. this.form.coverImage = res.data.url
  534. this.$message.success('上传成功')
  535. } else {
  536. this.$message.error('上传失败')
  537. }
  538. },
  539. handleUploadError(err) {
  540. console.error('[HomeBanner] 上传失败', err)
  541. this.$message.error('图片上传失败,请重试')
  542. },
  543. beforeUpload(file) {
  544. const isImage = file.type.startsWith('image/jpeg') || file.type.startsWith('image/png') || file.type.startsWith('image/gif') || file.type.startsWith('image/webp')
  545. if (!isImage) {
  546. this.$message.error('只能上传图片格式(jpg/png/gif/webp)')
  547. return false
  548. }
  549. const isLt5M = file.size / 1024 / 1024 < 5
  550. if (!isLt5M) {
  551. this.$message.error('图片大小不能超过 5MB')
  552. return false
  553. }
  554. return true
  555. },
  556. selectMiniPage() {
  557. this.showPagePicker = true
  558. },
  559. selectPage(path) {
  560. this.form.linkUrl = path
  561. this.showPagePicker = false
  562. },
  563. // --- 工具方法 ---
  564. typeLabel(type) {
  565. var map = { activity: '活动', product: '商品', article: '文章', link: '链接' }
  566. return map[type] || type || '-'
  567. },
  568. typeTag(type) {
  569. var map = { activity: 'success', product: 'warning', article: 'info', link: 'info' }
  570. return map[type] || ''
  571. },
  572. sortIndex(col) {
  573. return this.sortCol === col ? (this.sortDir === 'asc' ? 1 : 2) : null
  574. },
  575. sortClass(col) {
  576. return this.sortCol === col ? this.sortDir : ''
  577. },
  578. sortIcon(col) {
  579. if (this.sortCol !== col) return '↕'
  580. return this.sortDir === 'asc' ? '↑' : '↓'
  581. },
  582. onSortClick(col) {
  583. if (this.sortCol === col) {
  584. this.sortDir = this.sortDir === 'asc' ? 'desc' : 'asc'
  585. } else {
  586. this.sortCol = col
  587. this.sortDir = 'asc'
  588. }
  589. this.loadList()
  590. },
  591. // 选择小程序页面
  592. selectMiniPage() {
  593. this.$message.info('小程序页面选择功能待后端开发')
  594. },
  595. beforeUpload(file) {
  596. const isJPG = file.type === 'image/jpeg'
  597. const isPNG = file.type === 'image/png'
  598. const isGIF = file.type === 'image/gif'
  599. const isQQ = file.type === 'image/webp'
  600. if (!isJPG && !isPNG && !isGIF && !isQQ) {
  601. this.$message.error('只能上传 jpg/png/gif/webp 格式的图片!')
  602. return false
  603. }
  604. this.$message.warning('上传图片受限,请使用后台上传接口上传封面图')
  605. return false
  606. }
  607. }
  608. }
  609. </script>
  610. <style scoped>
  611. .admin-page-header {
  612. display: flex;
  613. justify-content: space-between;
  614. align-items: center;
  615. }
  616. .admin-page-title {
  617. font-size: 18px;
  618. font-weight: 600;
  619. color: #303133;
  620. }
  621. .filter-bar {
  622. display: flex;
  623. align-items: center;
  624. gap: 12px;
  625. flex-wrap: wrap;
  626. }
  627. </style>