Browse Source

fix: improve activity image upload

Developer 2 weeks ago
parent
commit
259740d940

BIN
favicon.ico


+ 29 - 1
src/components/editor/index.vue

@@ -10,6 +10,8 @@ import '@wangeditor/editor/dist/css/style.css'
 import { createEditor, createToolbar } from '@wangeditor/editor'
 import Cookies from 'js-cookie'
 
+const MAX_IMAGE_SIZE = 10 * 1024 * 1024
+
 export default {
     name: 'EditorVue',
     props: {
@@ -86,14 +88,40 @@ export default {
                         // 沿用项目原有的 OSS 上传端点,token 走 URL 参数
                         server: `${window.SITE_CONFIG['apiURL']}/sys/oss/upload?token=${Cookies.get('token') || ''}`,
                         fieldName: 'file',
+                        maxFileSize: MAX_IMAGE_SIZE,
+                        onBeforeUpload: (file) => {
+                            if (file.size > MAX_IMAGE_SIZE) {
+                                this.$message.error('图片大小不能超过10MB')
+                                return false
+                            }
+                            return file
+                        },
+                        onFailed: (file, res) => {
+                            this.$message.error((res && (res.msg || res.message)) || `${file.name}上传失败`)
+                        },
+                        onError: (file, xhr, error) => {
+                            this.$message.error(`${file.name}上传失败,请检查图片大小或网络连接`)
+                        },
                         // 后端响应是项目老格式 { code: 0, data: 'url', msg: '...' },
                         // wangEditor 默认期望 { errno, data: [{ url, alt, href }] },这里桥接一下
-                        customInsert(res, insertFn) {
+                        customInsert: (res, insertFn) => {
                             if (res && res.code === 0 && res.data) {
                                 insertFn(res.data, '', res.data)
+                            } else {
+                                this.$message.error((res && (res.msg || res.message)) || '图片上传失败')
                             }
                         }
                     },
+                    insertImage: {
+                        checkImage: (src) => {
+                            const valid = /^https?:\/\/\S+$/i.test(src || '')
+                            if (!valid) this.$message.error('请输入有效的图片链接地址(需以 http:// 或 https:// 开头)')
+                            return valid
+                        },
+                        onInsertedImage: (image) => {
+                            if (!image || !image.src) this.$message.error('图片链接插入失败')
+                        }
+                    },
                     fontFamily: {
                         fontFamilyList: [
                             { name: '宋体', value: 'SimSun' },

+ 19 - 6
src/views/modules/activity/add.vue

@@ -80,7 +80,7 @@
                             >
                             <i class="el-icon-plus"></i>
                         </el-upload>
-                        <div class="upload_tip">尺寸:158*214px,上传1张,支持图片格式:jpg、png、jpeg</div>
+                        <div class="upload_tip">尺寸:158*214px,上传1张,支持图片格式:jpg、png、jpeg,单张图片不超过10MB</div>
                     </el-form-item>
                     <el-form-item label="活动详情banner" prop="imageFiles" class="redLabel">
                         <el-upload
@@ -96,11 +96,11 @@
                             >
                             <i class="el-icon-plus"></i>
                         </el-upload>
-                        <div class="upload_tip">尺寸:702*440px,最多上传6张,图片格式:jpg、png、jpeg(可拖动调整顺序)</div>
-                    </el-form-item>
-                    <el-form-item label="活动详情" prop="activityDetails" class="redLabel">
-                        <editor-vue :content="basicForm.activityDetails" @EditorChange="getEditor" ref="infoIntroduceRef"></editor-vue>
-                    </el-form-item>
+                        <div class="upload_tip">尺寸:702*440px,最多上传6张,图片格式:jpg、png、jpeg,单张图片不超过10MB(可拖动调整顺序)</div>
+                    </el-form-item>
+                    <el-form-item label="活动详情" prop="activityDetails" class="redLabel">
+                        <editor-vue :content="basicForm.activityDetails" @EditorChange="getEditor" ref="infoIntroduceRef"></editor-vue>
+                    </el-form-item>
                 </el-form>
                 <div class="btns adfac">
                     <el-button type="primary" @click="handleNext">下一步</el-button>
@@ -230,6 +230,7 @@ import Cookies from 'js-cookie'
 import Sortable from 'sortablejs'
 import editorVue from '@/components/editor'
 import mixinRegionModule from '@/mixins/region-module.js'
+const MAX_IMAGE_SIZE = 10 * 1024 * 1024
 const getInitialBasicForm = () => ({
   id: '',
   typeId: '',
@@ -514,6 +515,10 @@ export default {
         )
         return false
       }
+      if (file.size > MAX_IMAGE_SIZE) {
+        this.$message.error('图片大小不能超过10MB')
+        return false
+      }
       if (this.updType === 1) {
         if (this.isCropper) {
           this.currentFile = file
@@ -542,7 +547,11 @@ export default {
         this.$http.post('/sys/oss/upload', data).then((res) => {
           if (res.data.code === 0) {
             this.successHandleCover(res.data)
+          } else {
+            this.$message.error(res.data.msg || '图片上传失败')
           }
+        }).catch(() => {
+          this.$message.error('图片上传失败,请检查图片大小或网络连接')
         })
       }
     },
@@ -552,7 +561,11 @@ export default {
         this.$http.post('/sys/oss/upload', data).then((res) => {
           if (res.data.code === 0) {
             this.successHandleBanner(res.data)
+          } else {
+            this.$message.error(res.data.msg || '图片上传失败')
           }
+        }).catch(() => {
+          this.$message.error('图片上传失败,请检查图片大小或网络连接')
         })
       }
     },

+ 16 - 7
src/views/modules/activity/detail.vue

@@ -18,8 +18,8 @@
                     <el-form-item label="活动类型" prop="typeId">
                         <div class="form-text">{{ typeOptions.find(t=>t.id===basicForm.typeId)?(typeOptions.find(t=>t.id===basicForm.typeId).typeName||''):'' }}</div>
                     </el-form-item>
-                    <el-form-item label="活动分类" prop="categoryId">
-                        <div class="form-text">{{ categoryOptions.find(t=>t.id===basicForm.categoryId)?(categoryOptions.find(t=>t.id===basicForm.categoryId).categoryName||''):'' }}</div>
+                    <el-form-item label="活动分类" prop="categoryIds">
+                        <div class="form-text">{{ getCategoryNames(basicForm) }}</div>
                     </el-form-item>
                     <el-form-item label="活动名称" prop="activityName">
                         <div class="form-text">{{ basicForm.activityName||'' }}</div>
@@ -276,11 +276,20 @@ export default {
         this.typeOptions = res.data.data.list.filter(l => l.enable === 1) || []
       })
     },
-    getCategoryOptions () {
-      this.$http.get('/core/activity/category/page', { params: { page: 1, limit: -1 } }).then(res => {
-        this.categoryOptions = res.data.data.list.filter(l => l.enable === 1) || []
-      })
-    },
+    getCategoryOptions () {
+      this.$http.get('/core/activity/category/page', { params: { page: 1, limit: -1 } }).then(res => {
+        this.categoryOptions = res.data.data.list.filter(l => l.enable === 1) || []
+      })
+    },
+    getCategoryNames (activity) {
+      const rawIds = activity.categoryIds || activity.categoryId
+      if (!rawIds) return ''
+      const ids = String(rawIds).split(',').map(id => id.trim()).filter(Boolean)
+      return ids.map(id => {
+        const category = this.categoryOptions.find(item => String(item.id) === id)
+        return category ? category.categoryName : ''
+      }).filter(Boolean).join('、')
+    },
     getSupplierOptions () {
       this.$http.get('/core/channel/page', { params: { page: 1, limit: -1 } }).then(res => {
         this.channelOptions = res.data.data.list.filter(item => item.type === 1) || []

+ 15 - 0
tests/activity-detail-category.test.js

@@ -0,0 +1,15 @@
+import test from 'node:test'
+import assert from 'node:assert/strict'
+import fs from 'node:fs'
+import path from 'node:path'
+
+const detailFile = path.resolve('src/views/modules/activity/detail.vue')
+
+test('活动详情页展示全部多选活动分类并兼容旧数据', () => {
+  const source = fs.readFileSync(detailFile, 'utf8')
+
+  assert.match(source, /getCategoryNames\(basicForm\)/)
+  assert.match(source, /categoryIds/)
+  assert.match(source, /categoryId/)
+  assert.match(source, /join\(['"]、['"]\)/)
+})

+ 33 - 0
tests/activity-image-upload.test.js

@@ -0,0 +1,33 @@
+import test from 'node:test'
+import assert from 'node:assert/strict'
+import fs from 'node:fs'
+import path from 'node:path'
+
+const editorFile = path.resolve('src/components/editor/index.vue')
+const activityFile = path.resolve('src/views/modules/activity/add.vue')
+
+test('富文本图片上传配置大小限制并提示失败原因', () => {
+  const source = fs.readFileSync(editorFile, 'utf8')
+
+  assert.match(source, /MAX_IMAGE_SIZE\s*=\s*10\s*\*\s*1024\s*\*\s*1024/)
+  assert.match(source, /maxFileSize:\s*MAX_IMAGE_SIZE/)
+  assert.match(source, /onBeforeUpload\s*:\s*\(file\)[\s\S]*file\.size\s*>\s*MAX_IMAGE_SIZE/)
+  assert.match(source, /onFailed\s*:\s*\(file,\s*res\)/)
+  assert.match(source, /onError\s*:\s*\(file,\s*xhr,\s*error\)/)
+})
+
+test('富文本图片链接插入配置校验地址并反馈插入结果', () => {
+  const source = fs.readFileSync(editorFile, 'utf8')
+
+  assert.match(source, /insertImage:\s*\{[\s\S]*checkImage:\s*\(src\)/)
+  assert.match(source, /onInsertedImage:\s*\(image\)/)
+})
+
+test('活动封面和 banner 显示图片大小限制并处理裁剪上传失败', () => {
+  const source = fs.readFileSync(activityFile, 'utf8')
+
+  assert.match(source, /单张图片不超过\s*10MB/)
+  assert.match(source, /file\.size\s*>\s*MAX_IMAGE_SIZE/)
+  assert.match(source, /cropperUpload\s*\(data\)[\s\S]*\.catch\s*\(/)
+  assert.match(source, /cropperUpload2\s*\(data\)[\s\S]*\.catch\s*\(/)
+})