Parcourir la source

卓美马甲功能

yuhongqi il y a 1 jour
Parent
commit
8dcc3a9366

+ 25 - 17
src/components/ImageUpload/index.vue

@@ -167,20 +167,36 @@ export default {
       // fileSize 默认 500 表示沿用历史 3MB;显式传入且 <=100 时按 MB 校验
       const maxMb = (this.fileSize != null && this.fileSize > 0 && this.fileSize <= 100) ? this.fileSize : 3;
       return new Promise((resolve, reject) => {
-        if (file.size / 1024 / 1024 > maxMb) {
-          this.$message.error(`上传的图片不能超过${maxMb}MB`);
+        const closeLoading = () => {
+          if (this.loading && typeof this.loading.close === 'function') {
+            this.loading.close();
+            this.loading = null;
+          }
+        };
+        const fail = (msg) => {
+          if (msg) {
+            this.$message.error(msg);
+          }
+          closeLoading();
           reject();
+        };
+        if (file.size / 1024 / 1024 > maxMb) {
+          fail(`上传的图片不能超过${maxMb}MB`);
           return;
         }
         const afterRatioCheck = () => {
+          // 校验通过后再进入「上传中」态
+          this.loading = this.$loading({
+            lock: true,
+            text: "上传中",
+            background: "rgba(0, 0, 0, 0.7)",
+          });
           if (file.size / 1024 / 1024 > 1 && maxMb > 1) {
             const loadingInstance = Loading.service({ text: '图片内存过大正在压缩图片...' });
-            // 文件大于1MB时进行压缩
             this.compressImage(file).then((compressedFile) => {
               loadingInstance.close();
               if (compressedFile.size / 1024 / 1024 > maxMb) {
-                this.$message.error(`图片压缩后仍大于${maxMb}MB`);
-                reject();
+                fail(`图片压缩后仍大于${maxMb}MB`);
               } else {
                 console.log(`图片压缩成功,最终质量为: ${this.finalQuality.toFixed(2)}`);
                 console.log(`最终内存大小为: ${(compressedFile.size/1024).toFixed(2)}KB`);
@@ -189,7 +205,7 @@ export default {
             }).catch((err) => {
               loadingInstance.close();
               console.error(err);
-              reject();
+              fail();
             });
           } else {
             resolve(file);
@@ -210,31 +226,23 @@ export default {
             img.onload = () => {
               const actual = img.width / img.height;
               if (Math.abs(actual - expected) / expected > this.aspectTolerance) {
-                this.$message.error(`封面比例须为 ${this.aspectRatio}(当前约 ${(actual).toFixed(2)})`);
-                reject();
+                fail(`封面比例须为 ${this.aspectRatio}(当前约 ${(actual).toFixed(2)})`);
                 return;
               }
               afterRatioCheck();
             };
             img.onerror = () => {
-              this.$message.error('无法读取图片尺寸');
-              reject();
+              fail('无法读取图片尺寸');
             };
             img.src = e.target.result;
           };
           reader.onerror = () => {
-            this.$message.error('无法读取图片');
-            reject();
+            fail('无法读取图片');
           };
           reader.readAsDataURL(file);
         } else {
           afterRatioCheck();
         }
-        this.loading = this.$loading({
-          lock: true,
-          text: "上传中",
-          background: "rgba(0, 0, 0, 0.7)",
-        });
       });
       // if (this.fileSize) {
       //   const isLt = file.size / 1024  < this.fileSize;

+ 26 - 2
src/views/components/course/userCourseCatalogDetails.vue

@@ -365,7 +365,11 @@
             <el-table-column label="商品名称" align="center" prop="productName"/>
             <el-table-column label="产品条码" align="center" prop="barCode"/>
             <el-table-column label="商品价格" align="center" prop="productPrice"/>
-            <el-table-column label="库存" align="center" prop="stock"/>
+            <el-table-column label="库存" align="center" min-width="160">
+              <template slot-scope="scope">
+                <span style="white-space:pre-wrap">{{ scope.row.stockDesc || scope.row.stock || 0 }}</span>
+              </template>
+            </el-table-column>
             <el-table-column label="操作" align="center" width="100px" fixed="right">
               <template slot-scope="scope">
                 <el-button size="mini" type="text" icon="el-icon-delete"
@@ -745,6 +749,7 @@ import draggable from 'vuedraggable'
 import { getConfigByKey } from '@/api/system/config'
 import { options as courserCouponOptions} from "@/api/his/courserCoupon";
 import { getCompanyList } from '@/api/company/company'
+import { listStoreProductAttrValue } from '@/api/hisStore/storeProductAttrValue'
 
 export default {
   name: "userCourseCatalog",
@@ -1074,12 +1079,31 @@ export default {
 
       //先置空 只选择一件商品
       this.form.courseProducts = [];
-      this.form.courseProducts.push(val);
+      const product = Object.assign({}, val, { stockDesc: val.stock })
+      this.form.courseProducts.push(product);
+      this.loadProductSpecStock(product);
       this.$message({
         message: '添加成功',
         type: 'success'
       });
     },
+    loadProductSpecStock(product) {
+      const productId = product.productId || product.id
+      if (!productId) return
+      listStoreProductAttrValue({ productId, pageNum: 1, pageSize: 100 }).then(res => {
+        const rows = res.rows || res.data || []
+        if (!rows.length) return
+        let total = 0
+        const parts = rows.map(r => {
+          const s = Number(r.stock || 0)
+          total += s
+          return (r.sku || '默认规格') + ':' + s
+        })
+        product.stock = total
+        product.stockDesc = parts.join(' / ')
+        this.$forceUpdate()
+      }).catch(() => {})
+    },
 
     //选择结果
     questionBankResult(val) {

+ 28 - 3
src/views/components/course/userCourseCatalogDetailsZM.vue

@@ -339,7 +339,11 @@
           >
             <el-table-column label="商品名称" align="center" prop="productName"/>
             <el-table-column label="商品价格" align="center" prop="price"/>
-            <el-table-column label="库存" align="center" prop="stock"/>
+            <el-table-column label="库存" align="center" min-width="180">
+              <template slot-scope="scope">
+                <span style="white-space:pre-wrap">{{ scope.row.stockDesc || scope.row.stock || 0 }}</span>
+              </template>
+            </el-table-column>
             <el-table-column label="上架时间" align="center" prop="onShelfTime" width="180">
               <template slot-scope="scope">
                 <el-time-picker
@@ -712,6 +716,7 @@ import {getCateListByPid, getCatePidList} from '@/api/course/userCourseCategory'
 import {downloadCommentImportTemplate, importComments, listFeaturedComments, delUserCourseComment} from '@/api/course/userCourseComment'
 import draggable from 'vuedraggable'
 import { getConfigByKey } from '@/api/system/config'
+import { listStoreProductAttrValue } from '@/api/hisStore/storeProductAttrValue'
 
 export default {
   name: "userCourseCatalog",
@@ -999,6 +1004,7 @@ export default {
         productName: val.productName,
         price: val.price,
         stock: val.stock,
+        stockDesc: val.stock,
         id: val.productId,  // 添加 id 字段,方便删除操作
         onShelfTime: val.onShelfTime || '00:00:00',  // 上架时间,默认 00:00:00
         cardPopupTime: val.cardPopupTime || '00:00:00',  // 卡片弹出时间,默认 00:00:00
@@ -1013,10 +1019,27 @@ export default {
         return;
       }
 
-      // 添加商品到列表
+      // 添加商品到列表,并按规格加载库存
       this.form.courseProducts.push(productData);
+      this.loadProductSpecStock(productData);
       this.$message.success("添加成功");
     },
+    loadProductSpecStock(product) {
+      if (!product || !product.productId) return
+      listStoreProductAttrValue({ productId: product.productId, pageNum: 1, pageSize: 100 }).then(res => {
+        const rows = res.rows || res.data || []
+        if (!rows.length) return
+        let total = 0
+        const parts = rows.map(r => {
+          const s = Number(r.stock || 0)
+          total += s
+          return (r.sku || '默认规格') + ':' + s
+        })
+        product.stock = total
+        product.stockDesc = parts.join(' / ')
+        this.$forceUpdate()
+      }).catch(() => {})
+    },
 
     /**
      * 将时间字符串(HH:mm:ss)转换为秒数
@@ -1489,8 +1512,10 @@ export default {
                 cardPopupTime: product.cardPopupTime || '00:00:00',
                 cardCloseTime: product.cardCloseTime || '00:00:00',
                 offShelfTime: product.offShelfTime || '00:00:00',
-                hotSaleTags: product.hotSaleTags || ''
+                hotSaleTags: product.hotSaleTags || '',
+                stockDesc: product.stockDesc || product.stock
               }));
+              this.form.courseProducts.forEach(p => this.loadProductSpecStock(p));
             } else {
               // 否则按照原有逻辑设置到 packageList
               this.packageList = parsedData;

+ 97 - 17
src/views/life/video/index.vue

@@ -33,10 +33,14 @@
       <el-col :span="1.5">
         <el-button type="success" plain icon="el-icon-document-copy" size="mini" @click="handleBatchAdd" v-hasPermi="['life:video:add']">批量添加</el-button>
       </el-col>
+      <el-col :span="1.5">
+        <el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleBatchDelete" v-hasPermi="['life:video:remove']">批量删除</el-button>
+      </el-col>
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
 
-    <el-table border v-loading="loading" :data="videoList">
+    <el-table border v-loading="loading" :data="videoList" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" align="center"/>
       <el-table-column label="视频ID" align="center" prop="videoId" width="90"/>
       <el-table-column label="封面" align="center" width="80">
         <template slot-scope="scope">
@@ -112,6 +116,7 @@
           :fileName.sync="form.fileName"
           @video-ready="onVideoReady"
           @change="onVideoReady"
+          @video-duration="onVideoDuration"
           ref="videoUpload"
         />
         <el-form-item v-if="form.videoUrl" label="上传时间">
@@ -119,7 +124,10 @@
         </el-form-item>
         <el-form-item label="时长(秒)" prop="duration">
           <el-input-number v-model="form.duration" :min="0" :controls="false"/>
-          <span style="margin-left:8px;color:#909399;font-size:12px">默认 0,可手动修改</span>
+          <span style="margin-left:8px;color:#909399;font-size:12px">上传/选取后自动读取,可手动修改</span>
+        </el-form-item>
+        <el-form-item label="播放量" prop="views">
+          <el-input-number v-model="form.views" :min="0" :controls="false" style="width:160px"/>
         </el-form-item>
         <el-form-item label="点赞数" prop="likes">
           <el-input-number v-model="form.likes" :min="0" :controls="false" style="width:160px"/>
@@ -142,7 +150,13 @@
             <el-radio :label="0">否</el-radio>
           </el-radio-group>
         </el-form-item>
-        <el-form-item label="关联商品">
+        <el-form-item label="是否关联商品" prop="isProduct">
+          <el-radio-group v-model="form.isProduct" @change="onIsProductChange">
+            <el-radio :label="1">是</el-radio>
+            <el-radio :label="0">否</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item v-show="form.isProduct === 1" label="关联商品">
           <el-button type="primary" size="mini" :disabled="selectedProducts.length >= 1" @click="openProductDialog">选择商品</el-button>
           <span style="margin-left:8px;color:#909399;font-size:12px">最多关联 1 个商品</span>
           <el-table :data="selectedProducts" border size="mini" style="margin-top:8px">
@@ -204,13 +218,17 @@
               :fileName.sync="row.fileName"
               @video-ready="onBatchVideoReady(index)"
               @change="onBatchVideoReady(index)"
+              @video-duration="d => onBatchVideoDuration(index, d)"
             />
             <el-form-item v-if="row.videoUrl" label="上传时间">
               <el-input :value="row.uploadTimeDisplay" disabled style="width:220px"/>
             </el-form-item>
             <el-form-item label="时长(秒)">
               <el-input-number v-model="row.duration" :min="0" :controls="false"/>
-              <span style="margin-left:8px;color:#909399;font-size:12px">默认 0,可手动修改</span>
+              <span style="margin-left:8px;color:#909399;font-size:12px">自动读取,可改</span>
+            </el-form-item>
+            <el-form-item label="播放量">
+              <el-input-number v-model="row.views" :min="0" :controls="false" style="width:120px"/>
             </el-form-item>
             <el-form-item label="赞/藏/享">
               <el-input-number v-model="row.likes" :min="0" :controls="false" style="width:90px"/>
@@ -280,6 +298,8 @@ export default {
       showSearch: true,
       total: 0,
       videoList: [],
+      ids: [],
+      multiple: true,
       vestOptions: [],
       categoryOptions: [],
       title: '',
@@ -300,7 +320,8 @@ export default {
         title: [{ required: true, message: '标题不能为空', trigger: 'blur' }],
         coverUrl: [{ required: true, message: '封面不能为空', trigger: 'change' }],
         videoUrl: [{ required: true, message: '请上传或选择视频', trigger: 'change' }],
-        isShow: [{ required: true, message: '请选择是否上架', trigger: 'change' }]
+        isShow: [{ required: true, message: '请选择是否上架', trigger: 'change' }],
+        isProduct: [{ required: true, message: '请选择是否关联商品', trigger: 'change' }]
       },
       selectedProducts: [],
       productOpen: false,
@@ -385,12 +406,11 @@ export default {
       return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate())
         + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds())
     },
-    /** 本地上传完成 / 素材库选完后展示上传时间;时长保持默认 0,可手动改 */
+    /** 本地上传完成 / 素材库选完后展示上传时间 */
     onVideoReady() {
       if (!this.form.videoUrl) {
         return
       }
-      // 新增:展示当前选择/上传时间;修改:保留原 createTime
       if (this.form.videoId) {
         this.uploadTimeDisplay = this.parseTime(this.form.createTime) || this.formatNow()
       } else {
@@ -403,6 +423,12 @@ export default {
         this.$refs.form && this.$refs.form.validateField('videoUrl')
       })
     },
+    onVideoDuration(duration) {
+      const d = Number(duration)
+      if (!isNaN(d) && d >= 0) {
+        this.form.duration = Math.round(d)
+      }
+    },
     reset() {
       this.form = {
         videoId: null,
@@ -413,11 +439,13 @@ export default {
         coverUrl: null,
         videoUrl: null,
         duration: 0,
+        views: 0,
         likes: 0,
         favoriteNum: 0,
         shareNum: 0,
         isShow: 1,
         isHot: 0,
+        isProduct: 0,
         productIds: []
       }
       this.uploadTimeDisplay = ''
@@ -459,6 +487,7 @@ export default {
         fileName: null,
         uploadTimeDisplay: '',
         duration: 0,
+        views: 0,
         likes: 0,
         favoriteNum: 0,
         shareNum: 0,
@@ -486,6 +515,14 @@ export default {
         row.duration = 0
       }
     },
+    onBatchVideoDuration(index, duration) {
+      const row = this.batchRows[index]
+      if (!row) return
+      const d = Number(duration)
+      if (!isNaN(d) && d >= 0) {
+        row.duration = Math.round(d)
+      }
+    },
     submitBatch() {
       if (!this.batchVestId) {
         this.$message.warning('请选择马甲号')
@@ -510,6 +547,7 @@ export default {
         videoUrl: row.videoUrl,
         fileName: row.fileName,
         duration: row.duration || 0,
+        views: row.views || 0,
         likes: row.likes || 0,
         favoriteNum: row.favoriteNum || 0,
         shareNum: row.shareNum || 0,
@@ -529,17 +567,29 @@ export default {
     handleUpdate(row) {
       this.reset()
       getLifeVideo(row.videoId).then(res => {
-        this.form = Object.assign({}, res.data)
-        if (!this.form.category) {
-          this.form.category = this.defaultCategory()
+        const data = res.data || {}
+        const ids = data.productIds || []
+        // 一次性赋值,保证 isProduct 等前端字段进入响应式;radio 统一为 Number,避免类型不一致点不动
+        this.form = {
+          videoId: data.videoId,
+          vestId: data.vestId,
+          category: data.category || this.defaultCategory(),
+          title: data.title,
+          fileName: data.fileName,
+          coverUrl: data.coverUrl,
+          videoUrl: data.videoUrl,
+          duration: data.duration == null ? 0 : Number(data.duration),
+          views: Number(data.views || 0),
+          likes: Number(data.likes || 0),
+          favoriteNum: Number(data.favoriteNum || 0),
+          shareNum: Number(data.shareNum || 0),
+          isShow: data.isShow == null ? 1 : Number(data.isShow),
+          isHot: data.isHot == null ? 0 : Number(data.isHot),
+          isProduct: ids.length > 0 ? 1 : 0,
+          productIds: ids,
+          createTime: data.createTime
         }
-        this.form.likes = this.form.likes || 0
-        this.form.favoriteNum = this.form.favoriteNum || 0
-        this.form.shareNum = this.form.shareNum || 0
-        this.form.duration = this.form.duration == null ? 0 : this.form.duration
         this.uploadTimeDisplay = this.parseTime(this.form.createTime) || ''
-        const ids = res.data.productIds || []
-        this.form.productIds = ids
         this.selectedProducts = ids.map(id => ({ productId: id, productName: '商品#' + id, price: '-', stock: '-' }))
         if (ids.length) {
           searchLifeVideoProduct({ pageNum: 1, pageSize: 100 }).then(p => {
@@ -552,14 +602,27 @@ export default {
         this.title = '修改生活号视频'
       })
     },
+    onIsProductChange(val) {
+      // 切到「否」时清空已选商品,避免误保存
+      if (Number(val) !== 1) {
+        this.selectedProducts = []
+        this.form.productIds = []
+      }
+    },
     submitForm() {
       this.$refs['form'].validate(valid => {
         if (!valid) return
+        if (this.form.isProduct === 1 && this.selectedProducts.length === 0) {
+          this.$message.warning('请选择关联商品')
+          return
+        }
         if (this.selectedProducts.length > 1) {
           this.$message.warning('最多关联一个商品')
           return
         }
-        this.form.productIds = this.selectedProducts.map(p => p.productId)
+        this.form.productIds = this.form.isProduct === 1
+          ? this.selectedProducts.map(p => p.productId)
+          : []
         const req = this.form.videoId != null ? updateLifeVideo(this.form) : addLifeVideo(this.form)
         req.then(() => {
           this.msgSuccess('操作成功')
@@ -575,6 +638,10 @@ export default {
         this.getList()
       })
     },
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.videoId)
+      this.multiple = !selection.length
+    },
     handleDelete(row) {
       this.$confirm('删除后不可恢复,确认删除该视频?', '警告', { type: 'warning' }).then(() => {
         return delLifeVideo(row.videoId)
@@ -583,6 +650,18 @@ export default {
         this.msgSuccess('删除成功')
       }).catch(() => {})
     },
+    handleBatchDelete() {
+      if (!this.ids.length) {
+        this.$message.warning('请选择要删除的视频')
+        return
+      }
+      this.$confirm('删除后不可恢复,确认批量删除选中的 ' + this.ids.length + ' 条视频?', '警告', { type: 'warning' }).then(() => {
+        return delLifeVideo(this.ids.join(','))
+      }).then(() => {
+        this.getList()
+        this.msgSuccess('删除成功')
+      }).catch(() => {})
+    },
     openProductDialog() {
       this.productOpen = true
       this.searchProduct()
@@ -607,6 +686,7 @@ export default {
         return
       }
       this.selectedProducts.push(row)
+      this.form.isProduct = 1
       this.productOpen = false
     },
     removeProduct(index) {