Selaa lähdekoodia

投票功能,红包功能

yuhongqi 20 tuntia sitten
vanhempi
commit
3c6fda8a62

+ 53 - 0
src/api/live/quickComment.js

@@ -0,0 +1,53 @@
+import request from '@/utils/request'
+
+// 查询一键评论列表
+export function listQuickComment(query) {
+  return request({
+    url: '/live/quickComment/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询一键评论详细
+export function getQuickComment(id) {
+  return request({
+    url: '/live/quickComment/' + id,
+    method: 'get'
+  })
+}
+
+// 新增一键评论
+export function addQuickComment(data) {
+  return request({
+    url: '/live/quickComment',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改一键评论
+export function updateQuickComment(data) {
+  return request({
+    url: '/live/quickComment',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除一键评论
+export function delQuickComment(ids) {
+  return request({
+    url: '/live/quickComment/' + ids,
+    method: 'delete'
+  })
+}
+
+// 保存排序
+export function sortQuickComment(data) {
+  return request({
+    url: '/live/quickComment/sort',
+    method: 'put',
+    data: data
+  })
+}

+ 60 - 0
src/api/live/vote.js

@@ -0,0 +1,60 @@
+import request from '@/utils/request'
+
+// 查询直播投票列表
+export function listVote(query) {
+  return request({
+    url: '/live/vote/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询直播投票详细
+export function getVote(id) {
+  return request({
+    url: '/live/vote/' + id,
+    method: 'get'
+  })
+}
+
+// 新增直播投票
+export function addVote(data) {
+  return request({
+    url: '/live/vote',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改直播投票
+export function updateVote(data) {
+  return request({
+    url: '/live/vote',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除直播投票
+export function delVote(ids) {
+  return request({
+    url: '/live/vote/' + ids,
+    method: 'delete'
+  })
+}
+
+// 开始投票
+export function startVote(id) {
+  return request({
+    url: '/live/vote/start/' + id,
+    method: 'put'
+  })
+}
+
+// 结束投票
+export function endVote(id) {
+  return request({
+    url: '/live/vote/end/' + id,
+    method: 'put'
+  })
+}

+ 26 - 0
src/router/index.js

@@ -457,6 +457,32 @@ export const constantRoutes = [
                 hidden: true
             }
         ]
+    },
+    {
+      path: '/live/liveConfig',
+      component: Layout,
+      hidden: true,
+      children: [
+        {
+          path: ':liveId(\\d+)',
+          component: () => import('@/views/live/liveConfig/index'),
+          name: 'LiveConfig',
+          meta: { title: '直播配置', activeMenu: '/live/live' }
+        }
+      ]
+    },
+    {
+      path: '/live',
+      component: Layout,
+      hidden: true,
+      children: [
+        {
+          path: 'quickComment',
+          component: () => import('@/views/live/quickComment/index'),
+          name: 'QuickComment',
+          meta: { title: '一键评论', activeMenu: '/live/quickComment' }
+        }
+      ]
     }
 
 ]

+ 1 - 1
src/utils/liveWS.js

@@ -17,7 +17,7 @@ export class LiveWS {
       CryptoJS.enc.Utf8.parse('' + liveId + userId + userType + timestamp),
       CryptoJS.enc.Utf8.parse(adminSecret)).toString(CryptoJS.enc.Hex)
     this.url = url + `?liveId=${liveId}&userId=${userId}&userType=${userType}&timestamp=${timestamp}&signature=${signature}`;
-    console.log(this.url)
+    // 【V-33 修复】禁止打印含 signature 的完整 WS URL,避免控制台泄露签名
     this.liveId = liveId;
     this.userId = userId;
     this.checkInterval = checkInterval;

+ 9 - 0
src/views/live/live/index.vue

@@ -410,6 +410,14 @@
                         <el-radio :label="2">下架</el-radio>
                     </el-radio-group>
                 </el-form-item>
+                <el-form-item label="下单消息播报" prop="orderBroadcastEnabled">
+                    <el-switch
+                        v-model="form.orderBroadcastEnabled"
+                        :active-value="1"
+                        :inactive-value="0"
+                        :disabled="isViewOnly"
+                    />
+                </el-form-item>
                 <el-form-item label="可见范围" prop="visibleScope">
                     <el-radio-group v-model="form.visibleScope" :disabled="isViewOnly">
                         <el-radio :label="0">私域直播</el-radio>
@@ -915,6 +923,7 @@ export default {
                 showType: 1,
                 liveType: 2,
                 isShow: 1,
+                orderBroadcastEnabled: 1,
                 isTranscode:0,
                 uploadType:1,
                 transcodeFileKey:null,

+ 7 - 1
src/views/live/liveConfig/index.vue

@@ -74,6 +74,8 @@ import LiveReplay from './liveReplay.vue'
 import Preview from './preview.vue'
 import LiveCoupon from './liveCoupon.vue'
 import Barrage from './barrage.vue'
+import PasswordBag from './passwordBag.vue'
+import Vote from './vote.vue'
 import { listLive, getLive, delLive, addLive, updateLive, exportLive,selectCompanyTalent,handleShelfOrUn,handleDeleteSelected } from "@/api/live/live";
 
 
@@ -90,7 +92,9 @@ export default {
     Task,
     LiveCoupon,
     Barrage,
-    Preview
+    Preview,
+    PasswordBag,
+    Vote
   },
   data() {
     return {
@@ -105,6 +109,8 @@ export default {
         { name: '观看奖励', label: '观看奖励', index: 'watchReward'},
         { name: '直播预告', label: '直播预告', index: 'preview'},
         { name: '红包配置', label: '红包配置', index: 'liveRedConf'},
+        { name: '口令福袋', label: '口令福袋', index: 'passwordBag'},
+        { name: '投票', label: '投票', index: 'vote'},
         { name: '抽奖配置', label: '抽奖配置', index: 'liveLotteryConf'},
         { name: '优惠券配置', label: '优惠券配置', index: 'liveCoupon'},
         { name: '答题', label: '答题', index: 'answer'},

+ 206 - 130
src/views/live/liveConfig/liveRedConf.vue

@@ -6,7 +6,7 @@
           <el-option v-for="(item,index) in redStatusOptions" :key="item.dictValue+index" :label="item.dictLabel" :value="item.dictValue" />
         </el-select>
       </el-form-item>
-      <el-form-item label="红包类型" prop="redType">
+      <el-form-item label="红包类型" prop="redType" v-if="!isPasswordBagMode">
         <el-select v-model="queryParams.redType" placeholder="请选择红包类型" clearable size="small">
           <el-option v-for="(item,index) in redTypeOptions" :key="item.dictValue+index" :label="item.dictLabel" :value="item.dictValue" />
         </el-select>
@@ -21,7 +21,7 @@
           @keyup.enter.native="handleQuery"
         />
       </el-form-item>
-      <el-form-item label="红包标题" prop="desc">
+      <el-form-item :label="isPasswordBagMode ? '标题' : '红包标题'" prop="desc">
         <el-input
           v-model="queryParams.desc"
           placeholder="请输入描述"
@@ -30,22 +30,6 @@
           @keyup.enter.native="handleQuery"
         />
       </el-form-item>
-<!--      <el-form-item label="创建日期" prop="createTime">-->
-<!--        <el-date-picker clearable size="small"-->
-<!--          v-model="queryParams.createTime"-->
-<!--          type="date"-->
-<!--          value-format="yyyy-MM-dd"-->
-<!--          placeholder="选择创建日期">-->
-<!--        </el-date-picker>-->
-<!--      </el-form-item>-->
-<!--      <el-form-item label="修改日期" prop="updateTime">-->
-<!--        <el-date-picker clearable size="small"-->
-<!--          v-model="queryParams.updateTime"-->
-<!--          type="date"-->
-<!--          value-format="yyyy-MM-dd"-->
-<!--          placeholder="选择修改日期">-->
-<!--        </el-date-picker>-->
-<!--      </el-form-item>-->
       <el-form-item>
         <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
         <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
@@ -55,7 +39,6 @@
     <el-row :gutter="10" class="mb8">
       <el-col :span="1.5">
         <el-button
-          :disabled="isViewOnly"
           type="primary"
           plain
           icon="el-icon-plus"
@@ -64,20 +47,19 @@
           v-hasPermi="['live:liveRedConf:add']"
         >新增</el-button>
       </el-col>
-      <el-col :span="1.5">
+      <el-col :span="1.5" v-if="!isPasswordBagMode">
         <el-button
           type="success"
           plain
           icon="el-icon-edit"
           size="mini"
-          :disabled="single || isViewOnly"
+          :disabled="single"
           @click="handleUpdate"
           v-hasPermi="['live:liveRedConf:edit']"
         >修改</el-button>
       </el-col>
-      <el-col :span="1.5">
+      <el-col :span="1.5" v-if="!isPasswordBagMode">
         <el-button
-          :disabled="isViewOnly"
           type="warning"
           plain
           icon="el-icon-download"
@@ -91,73 +73,100 @@
     </el-row>
 
     <el-table border v-loading="loading" :data="liveRedConfList" @selection-change="handleSelectionChange">
-      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column type="selection" width="55" align="center" v-if="!isPasswordBagMode" />
       <el-table-column label="红包ID" align="center" prop="redId" />
       <el-table-column label="红包状态" align="center" prop="redStatus" :formatter="redStatusFormatter"/>
-      <el-table-column label="有效时间 单位:分" align="center" prop="duration" />
-      <el-table-column label="红包类型" align="center" prop="redType" :formatter="redTypeFormatter"/>
+      <el-table-column :label="isPasswordBagMode ? '持续时间(分)' : '有效时间 单位:分'" align="center" prop="duration" />
+      <el-table-column label="发放类型" align="center" prop="distributeType" :formatter="distributeTypeFormatter" v-if="!isPasswordBagMode" />
+      <el-table-column label="红包类型" align="center" prop="redType" :formatter="redTypeFormatter" v-if="!isPasswordBagMode"/>
+      <el-table-column label="口令" align="center" prop="passwordCmd" v-if="isPasswordBagMode" :show-overflow-tooltip="true" />
       <el-table-column label="直播间ID" align="center" prop="liveId" />
-      <el-table-column label="芳华币数量" align="center" prop="redNum" />
-      <el-table-column label="可中奖份量" align="center" prop="totalLots" />
-      <el-table-column label="实际发放奖励份量" align="center" prop="totalSend" />
-      <el-table-column label="红包标题" align="center" prop="desc" />
+      <el-table-column :label="isPasswordBagMode ? '奖励积分' : '芳华币数量'" align="center" prop="redNum" />
+      <el-table-column :label="isPasswordBagMode ? '奖励份数' : '可中奖份量'" align="center" prop="totalLots" />
+      <el-table-column label="参与人数" align="center" prop="joinCount" v-if="isPasswordBagMode" />
+      <el-table-column label="实际发放奖励份量" align="center" prop="totalSend" v-if="!isPasswordBagMode" />
+      <el-table-column :label="isPasswordBagMode ? '标题' : '红包标题'" align="center" prop="desc" />
       <el-table-column label="创建日期" align="center" prop="createTime" width="120">
         <template slot-scope="scope">
           <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="修改日期" align="center" prop="updateTime" width="120">
+      <el-table-column label="修改日期" align="center" prop="updateTime" width="120" v-if="!isPasswordBagMode">
         <template slot-scope="scope">
           <span>{{ parseTime(scope.row.updateTime, '{y}-{m}-{d}') }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="220">
         <template slot-scope="scope">
-          <el-button
-            :disabled="isViewOnly"
-            size="mini"
-            type="text"
-            icon="el-icon-edit"
-            @click="handleUpdate(scope.row)"
-            v-hasPermi="['live:liveRedConf:edit']"
-            style="margin-left: 10px"
-          >修改</el-button>
-          <!-- 开始 -->
-          <el-button
-            :disabled="isViewOnly"
-            size="mini"
-            type="text"
-            icon="el-icon-delete"
-            @click="handleStatusChange(scope.row,'1')"
-            v-hasPermi="['live:liveRedConf:edit']"
-          >开始</el-button>
-          <!-- 暂停 -->
-          <el-button
-            :disabled="isViewOnly"
-            size="mini"
-            type="text"
-            icon="el-icon-delete"
-            @click="handleStatusChange(scope.row,'3')"
-            v-hasPermi="['live:liveRedConf:edit']"
-          >暂停</el-button>
-          <!-- 结算 -->
-          <el-button
-            :disabled="isViewOnly"
-            size="mini"
-            type="text"
-            icon="el-icon-delete"
-            @click="handleStatusChange(scope.row,'2')"
-            v-hasPermi="['live:liveRedConf:edit']"
-          >结算</el-button>
-
-          <el-button
-            :disabled="isViewOnly"
-            size="mini"
-            type="text"
-            icon="el-icon-delete"
-            @click="handleDelete(scope.row)"
-            v-hasPermi="['live:liveRedConf:edit']"
-          >删除</el-button>
+          <template v-if="isPasswordBagMode">
+            <el-button
+              v-if="String(scope.row.redStatus) === '0'"
+              size="mini"
+              type="text"
+              @click="handleStatusChange(scope.row,'1')"
+              v-hasPermi="['live:liveRedConf:edit']"
+            >开始</el-button>
+            <el-button
+              v-if="String(scope.row.redStatus) === '0'"
+              size="mini"
+              type="text"
+              icon="el-icon-edit"
+              @click="handleUpdate(scope.row)"
+              v-hasPermi="['live:liveRedConf:edit']"
+            >修改</el-button>
+            <el-button
+              v-if="String(scope.row.redStatus) === '1'"
+              size="mini"
+              type="text"
+              @click="handleStatusChange(scope.row,'2')"
+              v-hasPermi="['live:liveRedConf:edit']"
+            >结束</el-button>
+            <el-button
+              size="mini"
+              type="text"
+              icon="el-icon-delete"
+              @click="handleDelete(scope.row)"
+              v-hasPermi="['live:liveRedConf:edit']"
+            >删除</el-button>
+          </template>
+          <template v-else>
+            <el-button
+              size="mini"
+              type="text"
+              icon="el-icon-edit"
+              @click="handleUpdate(scope.row)"
+              v-hasPermi="['live:liveRedConf:edit']"
+              style="margin-left: 10px"
+            >修改</el-button>
+            <el-button
+              size="mini"
+              type="text"
+              icon="el-icon-delete"
+              @click="handleStatusChange(scope.row,'1')"
+              v-hasPermi="['live:liveRedConf:edit']"
+            >开始</el-button>
+            <el-button
+              size="mini"
+              type="text"
+              icon="el-icon-delete"
+              @click="handleStatusChange(scope.row,'3')"
+              v-hasPermi="['live:liveRedConf:edit']"
+            >暂停</el-button>
+            <el-button
+              size="mini"
+              type="text"
+              icon="el-icon-delete"
+              @click="handleStatusChange(scope.row,'2')"
+              v-hasPermi="['live:liveRedConf:edit']"
+            >结算</el-button>
+            <el-button
+              size="mini"
+              type="text"
+              icon="el-icon-delete"
+              @click="handleDelete(scope.row)"
+              v-hasPermi="['live:liveRedConf:edit']"
+            >删除</el-button>
+          </template>
         </template>
       </el-table-column>
     </el-table>
@@ -171,27 +180,37 @@
     />
 
     <!-- 添加或修改直播红包记录配置对话框 -->
-    <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
-      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
-        <el-form-item label="持续时间" prop="duration">
-          <el-input v-model="form.duration" placeholder="请输入红包有效时间 单位:分" />
+    <el-dialog :title="title" :visible.sync="open" width="520px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="100px">
+        <el-form-item label="发放类型" prop="distributeType" v-if="!isPasswordBagMode">
+          <el-radio-group v-model="form.distributeType" :disabled="form.redId != null">
+            <el-radio :label="1">普通均分</el-radio>
+            <el-radio :label="2">拼手气</el-radio>
+            <el-radio :label="3">口令福袋</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="口令" prop="passwordCmd" v-if="showPasswordFields">
+          <el-input
+            v-model="form.passwordCmd"
+            maxlength="30"
+            show-word-limit
+            placeholder="请输入口令,最多30字"
+          />
+        </el-form-item>
+        <el-form-item :label="showPasswordFields ? '持续时间' : '持续时间'" prop="duration">
+          <el-input v-model="form.duration" :placeholder="showPasswordFields ? '请输入持续时间(分)' : '请输入红包有效时间 单位:分'" />
         </el-form-item>
-<!--        <el-form-item label="红包类型" prop="redType">
-          <el-select v-model="form.redType" placeholder="请选择红包类型">
-            <el-option v-for="(item,index) in redTypeOptions" :key="item.dictValue+index" :label="item.dictLabel" :value="item.dictValue" />
-          </el-select>
-        </el-form-item>-->
         <el-form-item label="直播间ID" prop="liveId">
           <el-input v-model="form.liveId" placeholder="请输入直播间ID" :disabled="canLiveId"/>
         </el-form-item>
-        <el-form-item label="芳华币数" prop="redNum">
-          <el-input v-model="form.redNum" placeholder="请输入芳华币数量" />
+        <el-form-item :label="showPasswordFields ? '奖励积分' : '积分'" prop="redNum">
+          <el-input v-model="form.redNum" :placeholder="showPasswordFields ? '请输入奖励积分' : '请输入积分数量'" />
         </el-form-item>
-        <el-form-item label="中奖份量" prop="totalLots">
-          <el-input v-model="form.totalLots" placeholder="请输入可中奖份量" />
+        <el-form-item :label="showPasswordFields ? '奖励份数' : '中奖份量'" prop="totalLots">
+          <el-input v-model="form.totalLots" :placeholder="showPasswordFields ? '请输入奖励份数' : '请输入可中奖份量'" />
         </el-form-item>
-        <el-form-item label="红包标题" prop="desc">
-          <el-input v-model="form.desc" placeholder="请输入红包描述" />
+        <el-form-item :label="showPasswordFields ? '标题' : '红包标题'" prop="desc">
+          <el-input v-model="form.desc" :placeholder="showPasswordFields ? '请输入标题' : '请输入红包描述'" />
         </el-form-item>
       </el-form>
       <div slot="footer" class="dialog-footer">
@@ -206,19 +225,36 @@
 import { listLiveRedConf, getLiveRedConf, delLiveRedConf, addLiveRedConf, updateLiveRedConf, exportLiveRedConf } from "@/api/live/liveRedConf";
 
 export default {
-  props:{
-    isViewOnly: {
-      type: Boolean,
-      default: false,
+  name: "LiveRedConf",
+  props: {
+    forceDistributeType: {
+      type: Number,
+      default: null
     }
   },
-  name: "LiveRedConf",
   data() {
+    const validatePassword = (rule, value, callback) => {
+      if (!this.showPasswordFields) {
+        callback()
+        return
+      }
+      if (!value || !String(value).trim()) {
+        callback(new Error('口令不能为空'))
+      } else if (String(value).trim().length > 30) {
+        callback(new Error('口令不能超过30字'))
+      } else {
+        callback()
+      }
+    }
     return {
       //字典
-      // redStatusOptions: [{label:"未发放",value:0},{label:"发放中",value:1},{label:"已结束",value:2},{label:"暂停",value:3}],
       redStatusOptions: [],
       redTypeOptions: [],
+      distributeTypeMap: {
+        1: '普通均分',
+        2: '拼手气',
+        3: '口令福袋'
+      },
       canLiveId:false,
       //parentLiveId
       parentLiveId : null,
@@ -250,6 +286,7 @@ export default {
         redType: null,
         liveId: null,
         desc: null,
+        distributeType: null,
         createTime: null,
         updateTime: null,
       },
@@ -257,28 +294,39 @@ export default {
       form: {},
       // 表单校验
       rules: {
+        distributeType: [
+          { required: true, message: "发放类型不能为空", trigger: "change" }
+        ],
+        passwordCmd: [
+          { validator: validatePassword, trigger: "blur" }
+        ],
         duration: [
           { required: true, message: "有效时间 单位:分不能为空", trigger: "blur" }
         ],
-        /* redType: [
-          { required: true, message: "红包类型不能为空", trigger: "change" }
-        ], */
         liveId: [
           { required: true, message: "直播间ID不能为空", trigger: "blur" }
         ],
         redNum: [
-          { required: true, message: "芳华币数量不能为空", trigger: "blur" }
+          { required: true, message: "积分数量不能为空", trigger: "blur" }
         ],
         totalLots: [
-          { required: true, message: "可中奖份量不能为空", trigger: "blur" }
+          { required: true, message: "份量不能为空", trigger: "blur" }
         ],
         desc: [
           { required: true, message: "描述不能为空", trigger: "blur" }
         ],
       },
-
+      liveId: null,
     };
   },
+  computed: {
+    isPasswordBagMode() {
+      return this.forceDistributeType === 3
+    },
+    showPasswordFields() {
+      return this.isPasswordBagMode || Number(this.form.distributeType) === 3
+    }
+  },
   watch: {
     // 监听路由的 query 参数变化
     '$route.query': {
@@ -290,6 +338,9 @@ export default {
         }
         this.queryParams.liveId = this.liveId;
         this.parentLiveId = this.liveId;
+        if (this.isPasswordBagMode) {
+          this.queryParams.distributeType = 3
+        }
         if(this.queryParams.liveId){
           this.form.liveId = this.parentLiveId;
           //设置查询条件直播间ID不可修改
@@ -305,15 +356,15 @@ export default {
     }
   },
   created() {
+    if (this.isPasswordBagMode) {
+      this.queryParams.distributeType = 3
+    }
     this.getDicts("sys_live_red_status").then(response => {
       this.redStatusOptions = response.data;
     });
     this.getDicts("sys_live_red_type").then(response => {
       this.redTypeOptions = response.data;
     });
-  },
-  computed: {
-
   },
   methods: {
     handleStatusChange(row, status) {
@@ -359,8 +410,10 @@ export default {
         redType: row.redType,
         redNum: row.redNum,
         liveId: this.liveId,
-        status: status,
-        duration: row.duration
+        duration: row.duration,
+        distributeType: row.distributeType,
+        passwordCmd: row.passwordCmd,
+        status: status
       };
       updateLiveRedConf(doRedParam).then(response => {
         if(response.code === 200){
@@ -369,15 +422,13 @@ export default {
             data: doRedParam
           };
           this.$store.state.liveWs[this.liveId].send(JSON.stringify(msg));
+
           this.msgSuccess("修改成功");
-          console.log(1)
         }else{
           this.msgError(response.msg);
-          console.log(2)
         }
         this.open = false;
         this.getList();
-        console.log(3)
       });
     },
     redStatusFormatter(row, column) {
@@ -386,9 +437,15 @@ export default {
     redTypeFormatter(row, column) {
       return this.selectDictLabel(this.redTypeOptions, row.redType);
     },
+    distributeTypeFormatter(row) {
+      return this.distributeTypeMap[row.distributeType] || this.distributeTypeMap[1]
+    },
     /** 查询直播红包记录配置列表 */
     getList() {
       this.loading = true;
+      if (this.isPasswordBagMode) {
+        this.queryParams.distributeType = 3
+      }
       listLiveRedConf(this.queryParams).then(response => {
         this.liveRedConfList = response.rows;
         this.total = response.total;
@@ -412,6 +469,8 @@ export default {
         totalLots: null,
         totalSend: null,
         desc: null,
+        distributeType: this.isPasswordBagMode ? 3 : 1,
+        passwordCmd: null,
         createTime: null,
         updateTime: null,
         createBy: null,
@@ -436,6 +495,9 @@ export default {
     resetQuery() {
       this.resetForm("queryForm");
       this.checkParentLiveId();
+      if (this.isPasswordBagMode) {
+        this.queryParams.distributeType = 3
+      }
       this.handleQuery();
     },
     // 多选框选中数据
@@ -448,7 +510,7 @@ export default {
     handleAdd() {
       this.reset();
       this.open = true;
-      this.title = "添加直播红包记录配置";
+      this.title = this.isPasswordBagMode ? "添加口令福袋" : "添加直播红包记录配置";
     },
     /** 修改按钮操作 */
     handleUpdate(row) {
@@ -461,23 +523,33 @@ export default {
       const redId = row.redId || this.ids
       getLiveRedConf(redId).then(response => {
         this.form = response.data;
+        if (this.form.distributeType == null) {
+          this.form.distributeType = 1
+        }
         this.open = true;
-        this.title = "修改直播红包记录配置";
+        this.title = this.isPasswordBagMode ? "修改口令福袋" : "修改直播红包记录配置";
       });
     },
     /** 提交按钮 */
     submitForm() {
       this.$refs["form"].validate(valid => {
         if (valid) {
-          if (this.form.redId != null) {
-            updateLiveRedConf(this.form).then(response => {
+          const data = {
+            ...this.form,
+            distributeType: this.isPasswordBagMode ? 3 : this.form.distributeType,
+            passwordCmd: this.showPasswordFields && this.form.passwordCmd
+              ? String(this.form.passwordCmd).trim()
+              : this.form.passwordCmd
+          }
+          if (data.redId != null) {
+            updateLiveRedConf(data).then(response => {
               this.msgSuccess("修改成功");
               this.open = false;
               this.getList();
             });
           } else {
-            this.form.redType = 1;
-            addLiveRedConf(this.form).then(response => {
+            data.redType = 1;
+            addLiveRedConf(data).then(response => {
               this.msgSuccess("新增成功");
               this.open = false;
               this.getList();
@@ -488,8 +560,8 @@ export default {
     },
     /** 删除按钮操作 */
     handleDelete(row) {
-      //如果状态不为0,则不能删除
-      if (row.redStatus+"" !== "0") {
+      // 口令福袋允许按状态删除;普通红包仅未发放可删
+      if (!this.isPasswordBagMode && row.redStatus+"" !== "0") {
         this.msgError("只能删除未发放的红包");
         return;
       }
@@ -503,16 +575,20 @@ export default {
         }).then(() => {
           this.getList();
           this.msgSuccess("删除成功");
-          const msg = {
-            cmd: 'red',
-            data: {
-              redId: row.redId,
-              status: 2,
-              totalLots: row.totalLots,
-              duration: row.duration
-            }
-          };
-          this.$store.state.liveWs[this.liveId].send(JSON.stringify(msg));
+          if (this.$store.state.liveWs[this.liveId]) {
+            const msg = {
+              cmd: 'red',
+              data: {
+                redId: row.redId,
+                redStatus: 2,
+                totalLots: row.totalLots,
+                duration: row.duration,
+                distributeType: row.distributeType,
+                passwordCmd: row.passwordCmd
+              }
+            };
+            this.$store.state.liveWs[this.liveId].send(JSON.stringify(msg));
+          }
         }).catch(() => {});
     },
     /** 导出按钮操作 */

+ 12 - 0
src/views/live/liveConfig/passwordBag.vue

@@ -0,0 +1,12 @@
+<template>
+  <live-red-conf :force-distribute-type="3" />
+</template>
+
+<script>
+import LiveRedConf from './liveRedConf.vue'
+
+export default {
+  name: 'PasswordBag',
+  components: { LiveRedConf }
+}
+</script>

+ 265 - 0
src/views/live/liveConfig/vote.vue

@@ -0,0 +1,265 @@
+<template>
+  <div class="app-container">
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          plain
+          icon="el-icon-plus"
+          size="mini"
+          @click="handleAdd"
+        >新增</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table border v-loading="loading" :data="voteList">
+      <el-table-column label="描述" align="center" prop="description" :show-overflow-tooltip="true" min-width="120" />
+      <el-table-column label="选项A" align="center" prop="optionA" min-width="100" />
+      <el-table-column label="选项B" align="center" prop="optionB" min-width="100" />
+      <el-table-column label="可投票时间(分)" align="center" prop="duration" width="120" />
+      <el-table-column label="下架时间" align="center" prop="hideAt" width="160">
+        <template slot-scope="scope">
+          <span>{{ scope.row.hideAt ? parseTime(scope.row.hideAt) : '-' }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="状态" align="center" prop="status" width="90">
+        <template slot-scope="scope">
+          <span>{{ statusLabel(scope.row.status) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="票数" align="center" width="120">
+        <template slot-scope="scope">
+          <span>A:{{ scope.row.voteACount || 0 }} / B:{{ scope.row.voteBCount || 0 }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="获胜选项" align="center" prop="winner" width="90">
+        <template slot-scope="scope">
+          <span>{{ scope.row.winner || (String(scope.row.status) === '2' ? '平局' : '-') }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="200">
+        <template slot-scope="scope">
+          <el-button
+            v-if="String(scope.row.status) === '0'"
+            size="mini"
+            type="text"
+            @click="handleStart(scope.row)"
+          >开始</el-button>
+          <el-button
+            v-if="String(scope.row.status) === '0'"
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleUpdate(scope.row)"
+          >修改</el-button>
+          <el-button
+            v-if="String(scope.row.status) === '1'"
+            size="mini"
+            type="text"
+            @click="handleEnd(scope.row)"
+          >结束</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            @click="handleDelete(scope.row)"
+          >删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <el-dialog :title="title" :visible.sync="open" width="520px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="120px">
+        <el-form-item label="投票描述" prop="description">
+          <el-input v-model="form.description" maxlength="20" show-word-limit placeholder="请输入投票描述,最多20字" />
+        </el-form-item>
+        <el-form-item label="选项A" prop="optionA">
+          <el-input v-model="form.optionA" maxlength="12" show-word-limit placeholder="请输入选项A,最多12字" />
+        </el-form-item>
+        <el-form-item label="选项B" prop="optionB">
+          <el-input v-model="form.optionB" maxlength="12" show-word-limit placeholder="请输入选项B,最多12字" />
+        </el-form-item>
+        <el-form-item label="可投票时间" prop="duration">
+          <el-input-number v-model="form.duration" :min="1" controls-position="right" />
+          <span style="margin-left:8px;color:#909399">分钟</span>
+        </el-form-item>
+        <el-form-item label="结束后下架" prop="hideDuration">
+          <el-input-number v-model="form.hideDuration" :min="0" controls-position="right" />
+          <span style="margin-left:8px;color:#909399">分钟</span>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { listVote, getVote, addVote, updateVote, delVote, startVote, endVote } from '@/api/live/vote'
+
+export default {
+  name: 'LiveVote',
+  data() {
+    const lenValidator = (max, label) => (rule, value, callback) => {
+      if (!value || !String(value).trim()) {
+        callback(new Error(label + '不能为空'))
+      } else if (String(value).trim().length > max) {
+        callback(new Error(label + '不能超过' + max + '字'))
+      } else {
+        callback()
+      }
+    }
+    return {
+      loading: true,
+      showSearch: true,
+      total: 0,
+      voteList: [],
+      title: '',
+      open: false,
+      liveId: null,
+      statusMap: {
+        0: '未开始',
+        1: '进行中',
+        2: '已结束'
+      },
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        liveId: null
+      },
+      form: {},
+      rules: {
+        description: [{ required: true, validator: lenValidator(20, '投票描述'), trigger: 'blur' }],
+        optionA: [{ required: true, validator: lenValidator(12, '选项A'), trigger: 'blur' }],
+        optionB: [{ required: true, validator: lenValidator(12, '选项B'), trigger: 'blur' }],
+        duration: [{ required: true, message: '可投票时间不能为空', trigger: 'change' }]
+      }
+    }
+  },
+  created() {
+    this.liveId = this.$route.params.liveId || this.$route.query.liveId
+    this.queryParams.liveId = this.liveId
+    this.getList()
+  },
+  methods: {
+    statusLabel(status) {
+      return this.statusMap[status] || '-'
+    },
+    getList() {
+      this.loading = true
+      listVote(this.queryParams).then(response => {
+        this.voteList = response.rows || []
+        this.total = response.total
+        this.loading = false
+      })
+    },
+    cancel() {
+      this.open = false
+      this.reset()
+    },
+    reset() {
+      this.form = {
+        id: null,
+        liveId: this.liveId,
+        description: null,
+        optionA: null,
+        optionB: null,
+        duration: 5,
+        hideDuration: 0
+      }
+      this.resetForm('form')
+    },
+    handleAdd() {
+      this.reset()
+      this.open = true
+      this.title = '添加投票'
+    },
+    handleUpdate(row) {
+      if (String(row.status) !== '0') {
+        this.msgError('只能修改未开始的投票')
+        return
+      }
+      this.reset()
+      getVote(row.id).then(response => {
+        this.form = response.data
+        this.open = true
+        this.title = '修改投票'
+      })
+    },
+    submitForm() {
+      this.$refs['form'].validate(valid => {
+        if (!valid) {
+          return
+        }
+        const data = {
+          ...this.form,
+          liveId: this.liveId,
+          description: String(this.form.description).trim(),
+          optionA: String(this.form.optionA).trim(),
+          optionB: String(this.form.optionB).trim()
+        }
+        if (data.id != null) {
+          updateVote(data).then(() => {
+            this.msgSuccess('修改成功')
+            this.open = false
+            this.getList()
+          })
+        } else {
+          addVote(data).then(() => {
+            this.msgSuccess('新增成功')
+            this.open = false
+            this.getList()
+          })
+        }
+      })
+    },
+    handleStart(row) {
+      this.$confirm('确认开始该投票?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        return startVote(row.id)
+      }).then(() => {
+        this.msgSuccess('已开始')
+        this.getList()
+      }).catch(() => {})
+    },
+    handleEnd(row) {
+      this.$confirm('确认结束该投票?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        return endVote(row.id)
+      }).then(() => {
+        this.msgSuccess('已结束')
+        this.getList()
+      }).catch(() => {})
+    },
+    handleDelete(row) {
+      this.$confirm('是否确认删除该投票?', '警告', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        return delVote(row.id)
+      }).then(() => {
+        this.getList()
+        this.msgSuccess('删除成功')
+      }).catch(() => {})
+    }
+  }
+}
+</script>

+ 8 - 1
src/views/live/liveData/index.vue

@@ -476,6 +476,12 @@
                 <span class="detail-value">{{ detailData.paidOrders || 0 }}</span>
               </div>
             </el-col>
+            <el-col :span="12">
+              <div class="detail-item">
+                <span class="detail-label">销量统计:</span>
+                <span class="detail-value">{{ detailData.salesCount || 0 }}</span>
+              </div>
+            </el-col>
             <el-col :span="12">
               <div class="detail-item">
                 <span class="detail-label">峰值转化率:</span>
@@ -563,7 +569,7 @@
             </el-table-column>
             <el-table-column prop="rewardType" label="领取类型" min-width="160">
                 <template slot-scope="scope">
-                    <div v-if="scope.row.isCompleted == 1">
+                    <div v-if="scope.row.rewardType != null && scope.row.rewardType !== '' && scope.row.rewardType != 0 && scope.row.rewardType != 99">
                         <dict-tag :options="liveRewardTypeList" :value="scope.row.rewardType"/>
                         <el-button
                             v-if="scope.row.rewardType != 4 && scope.row.rewardType != 99"
@@ -580,6 +586,7 @@
                             @click="handleOpenCouponLog(scope.row)"
                         >核销卷领取详情</el-button>
                     </div>
+                    <span v-else>未领取</span>
                 </template>
             </el-table-column>
         </el-table>

+ 316 - 0
src/views/live/quickComment/index.vue

@@ -0,0 +1,316 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="内容" prop="content">
+        <el-input
+          v-model="queryParams.content"
+          placeholder="请输入评论文案"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          plain
+          icon="el-icon-plus"
+          size="mini"
+          @click="handleAdd"
+          v-hasPermi="['live:quickComment:add']"
+        >新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="danger"
+          plain
+          icon="el-icon-delete"
+          size="mini"
+          :disabled="multiple"
+          @click="handleDelete"
+          v-hasPermi="['live:quickComment:remove']"
+        >删除</el-button>
+      </el-col>
+      <el-col :span="1.5" v-if="sortDirty">
+        <el-button
+          type="warning"
+          plain
+          icon="el-icon-check"
+          size="mini"
+          :loading="sortLoading"
+          @click="handleSaveSort"
+          v-hasPermi="['live:quickComment:edit']"
+        >保存顺序</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table
+      ref="dragTable"
+      border
+      v-loading="loading"
+      :data="quickCommentList"
+      row-key="id"
+      @selection-change="handleSelectionChange"
+    >
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="" width="50" align="center" class-name="allowDrag">
+        <template>
+          <i class="el-icon-rank" style="cursor: move;"></i>
+        </template>
+      </el-table-column>
+      <el-table-column label="内容" align="center" prop="content" :show-overflow-tooltip="true" />
+      <el-table-column label="排序" align="center" prop="sortOrder" width="100" />
+      <el-table-column label="创建时间" align="center" prop="createTime" width="180">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.createTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="160">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleUpdate(scope.row)"
+            v-hasPermi="['live:quickComment:edit']"
+          >修改</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            @click="handleDelete(scope.row)"
+            v-hasPermi="['live:quickComment:remove']"
+          >删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="内容" prop="content">
+          <el-input
+            v-model="form.content"
+            type="textarea"
+            :rows="3"
+            maxlength="30"
+            show-word-limit
+            placeholder="请输入评论文案,最多30字"
+          />
+        </el-form-item>
+        <el-form-item label="排序" prop="sortOrder">
+          <el-input-number v-model="form.sortOrder" :min="0" controls-position="right" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import Sortable from 'sortablejs'
+import {
+  listQuickComment,
+  getQuickComment,
+  delQuickComment,
+  addQuickComment,
+  updateQuickComment,
+  sortQuickComment
+} from '@/api/live/quickComment'
+
+export default {
+  name: 'QuickComment',
+  data() {
+    const validateContent = (rule, value, callback) => {
+      if (!value || !String(value).trim()) {
+        callback(new Error('内容不能为空'))
+      } else if (String(value).trim().length > 30) {
+        callback(new Error('内容不能超过30字'))
+      } else {
+        callback()
+      }
+    }
+    return {
+      loading: true,
+      sortLoading: false,
+      sortDirty: false,
+      ids: [],
+      multiple: true,
+      showSearch: true,
+      total: 0,
+      quickCommentList: [],
+      title: '',
+      open: false,
+      sortable: null,
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        content: null
+      },
+      form: {},
+      rules: {
+        content: [{ required: true, validator: validateContent, trigger: 'blur' }]
+      }
+    }
+  },
+  created() {
+    this.getList()
+  },
+  beforeDestroy() {
+    if (this.sortable) {
+      this.sortable.destroy()
+      this.sortable = null
+    }
+  },
+  methods: {
+    getList() {
+      this.loading = true
+      listQuickComment(this.queryParams).then(response => {
+        this.quickCommentList = response.rows || []
+        this.total = response.total
+        this.loading = false
+        this.sortDirty = false
+        this.$nextTick(() => {
+          this.initSortable()
+        })
+      })
+    },
+    initSortable() {
+      if (this.sortable) {
+        this.sortable.destroy()
+        this.sortable = null
+      }
+      const table = this.$refs.dragTable
+      if (!table || !table.$el) {
+        return
+      }
+      const el = table.$el.querySelector('.el-table__body-wrapper tbody')
+      if (!el) {
+        return
+      }
+      this.sortable = Sortable.create(el, {
+        handle: '.allowDrag',
+        animation: 150,
+        onEnd: evt => {
+          if (evt.oldIndex === evt.newIndex) {
+            return
+          }
+          const currRow = this.quickCommentList.splice(evt.oldIndex, 1)[0]
+          this.quickCommentList.splice(evt.newIndex, 0, currRow)
+          this.quickCommentList.forEach((item, index) => {
+            item.sortOrder = index
+          })
+          this.sortDirty = true
+        }
+      })
+    },
+    cancel() {
+      this.open = false
+      this.reset()
+    },
+    reset() {
+      this.form = {
+        id: null,
+        content: null,
+        sortOrder: 0
+      }
+      this.resetForm('form')
+    },
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    resetQuery() {
+      this.resetForm('queryForm')
+      this.handleQuery()
+    },
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id)
+      this.multiple = !selection.length
+    },
+    handleAdd() {
+      this.reset()
+      this.open = true
+      this.title = '添加一键评论'
+    },
+    handleUpdate(row) {
+      this.reset()
+      const id = row.id || this.ids
+      getQuickComment(id).then(response => {
+        this.form = response.data
+        this.open = true
+        this.title = '修改一键评论'
+      })
+    },
+    submitForm() {
+      this.$refs['form'].validate(valid => {
+        if (!valid) {
+          return
+        }
+        const data = {
+          ...this.form,
+          content: String(this.form.content).trim()
+        }
+        if (data.id != null) {
+          updateQuickComment(data).then(() => {
+            this.msgSuccess('修改成功')
+            this.open = false
+            this.getList()
+          })
+        } else {
+          addQuickComment(data).then(() => {
+            this.msgSuccess('新增成功')
+            this.open = false
+            this.getList()
+          })
+        }
+      })
+    },
+    handleDelete(row) {
+      const ids = row.id || this.ids
+      this.$confirm('是否确认删除选中的一键评论?', '警告', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        return delQuickComment(ids)
+      }).then(() => {
+        this.getList()
+        this.msgSuccess('删除成功')
+      }).catch(() => {})
+    },
+    handleSaveSort() {
+      const payload = this.quickCommentList.map((item, index) => ({
+        id: item.id,
+        sortOrder: index
+      }))
+      this.sortLoading = true
+      sortQuickComment(payload).then(() => {
+        this.msgSuccess('排序保存成功')
+        this.sortDirty = false
+        this.getList()
+      }).finally(() => {
+        this.sortLoading = false
+      })
+    }
+  }
+}
+</script>