فهرست منبع

Merge remote-tracking branch 'origin/master'

yuhongqi 1 روز پیش
والد
کامیت
5c687de1dc

+ 15 - 0
src/api/company/company.js

@@ -56,3 +56,18 @@ export function syncCompanyCorIdImage() {
         method: 'get'
     })
 }
+
+export function getSidebarCompanyImage() {
+  return request({
+    url: '/company/company/getSidebarCompanyImage',
+    method: 'get'
+  })
+}
+
+export function saveSidebarCompanyImage(data) {
+  return request({
+    url: '/company/company/saveSidebarCompanyImage',
+    method: 'post',
+    data: data
+  })
+}

+ 10 - 0
src/api/live/liveWatchUser.js

@@ -41,3 +41,13 @@ export function blockUser(query) {
     method: 'get',
   })
 }
+
+
+// 获取直播用户观看数据
+export function liveWatchUserList(data) {
+    return request({
+        url: '/live/liveWatchUser/liveWatchUserList',
+        method: 'post',
+        data: data
+    })
+}

+ 2 - 1
src/views/app/user/index.vue

@@ -176,6 +176,7 @@
       <el-table-column type="selection" width="55" align="center" />
       <el-table-column label="用户ID" align="center" prop="userId" width="150px"/>
       <el-table-column label="用户昵称" align="center" prop="nickName" width="150px"/>
+      <el-table-column label="用户备注" align="center" prop="remark" width="150px"/>
       <el-table-column label="用户头像" align="center" prop="avatar" >
          <template slot-scope="scope">
             <el-image v-if="scope.row.avatar!=null"
@@ -616,7 +617,7 @@ export default {
           habits: null,
           illnessTime: null,
           body: null,
-          family: null, 
+          family: null,
           familyDisease: null,
           disease: null,
           isLine: null,

+ 27 - 1
src/views/course/userCourse/index.vue

@@ -51,6 +51,7 @@
       <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-button type="success" icon="el-icon-picture-outline" size="mini" @click="handleSidebarChange">侧边栏封面设置</el-button>
       </el-form-item>
     </el-form>
 
@@ -118,6 +119,13 @@
          :title="show.title" :visible.sync="show.open" append-to-body>
      <userCourseCatalogDetails  ref="userCourseCatalogDetails" />
    </el-drawer>
+
+    <el-dialog title="侧边栏封面图" :visible.sync="sidebarFrom.open" width="500px" append-to-body>
+      <image-upload v-model="sidebarFrom.imageUrl" :limit="1"/>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="handSaveSidebarCompanyImage">提交</el-button>
+      </div>
+    </el-dialog>
   </div>
 </template>
 
@@ -129,7 +137,7 @@ import Editor from '@/components/Editor/wang';
 import ImageUpload from '@/components/ImageUpload/index';
 import userCourseCatalogDetails from '../../components/course/userCourseCatalogDetails.vue';
 import { getAllCourseCategoryList,getCatePidList ,getCateListByPid} from "@/api/course/userCourseCategory";
-import {allList} from "@/api/company/company";
+import {allList, getSidebarCompanyImage, saveSidebarCompanyImage} from "@/api/company/company";
 
 export default {
   name: "UserCourse",
@@ -148,6 +156,10 @@ export default {
         title:"目录管理",
         open:false
       },
+      sidebarFrom:{
+        open: false,
+        imageUrl: null
+      },
       activeName:"1",
       projectOptions:[],
       tagsOptions:[],
@@ -550,6 +562,20 @@ export default {
           this.getList();
           this.msgSuccess("下架成功");
         }).catch(function() {});
+    },
+    handleSidebarChange() {
+      getSidebarCompanyImage().then(res => {
+        this.sidebarFrom.imageUrl = res.imageUrl
+        this.sidebarFrom.open = true
+      })
+    },
+    handSaveSidebarCompanyImage() {
+      saveSidebarCompanyImage({
+        imageUrl: this.sidebarFrom.imageUrl
+      }).then(() => {
+        this.sidebarFrom.open = false
+        this.$message.success('保存成功')
+      })
     }
   }
 };

+ 84 - 21
src/views/hisStore/components/addUserAddress.vue

@@ -15,7 +15,7 @@
                   v-for="item in province"
                   :key="item.cityId"
                   :label="item.name"
-                  :value="item.cityId">
+                  :value="item.name">
                 </el-option>
               </el-select>
           </el-col>
@@ -25,7 +25,7 @@
                   v-for="item in city"
                   :key="item.cityId"
                   :label="item.name"
-                  :value="item.cityId">
+                  :value="item.name">
                 </el-option>
               </el-select>
           </el-col>
@@ -35,7 +35,7 @@
                   v-for="item in district"
                   :key="item.cityId"
                   :label="item.name"
-                  :value="item.cityId">
+                  :value="item.name">
                 </el-option>
               </el-select>
           </el-col>
@@ -58,16 +58,23 @@ export default {
   name: "add",
   data() {
     return {
+      isEdit: false,
+      editAddressId: null,
       citys:[],
       province:[],
       city:[],
       district:[],
       // 表单参数
       form: {
+        userId: null,
+        realName: null,
+        phone: null,
+        detail: null,
         province:null,
         city:null,
         district:null,
         cityId:null,
+        isDefault: null,
       },
       // 表单校验
       rules: {
@@ -95,26 +102,24 @@ export default {
   },
   methods: {
     districtChange(val){
-      const item = this.district.find(i => i.cityId === val)
-      this.form.district=item.name;
+      const item = this.district.find(i => i.name === val)
     },
     cityChange(val){
-      const item = this.city.find(i => i.cityId === val)
-      this.district = item.children
+      const item = this.city.find(i => i.name === val)
+      this.district = item.children || []
       this.form.district=null;
-      this.form.city=item.name;
-      this.form.cityId=val;
+      this.form.cityId=item.cityId;
     },
     provinceChange(val){
-      const item = this.citys.find(i => i.cityId === val)
-      this.city = item.children
+      const item = this.citys.find(i => i.name === val)
+      this.city = item.children || []
       this.district=[];
       this.form.city=null;
       this.form.district=null;
-      this.form.province=item.name;
+      this.form.cityId=null;
     },
     getCityList(){
-      getCitys().then(res => {
+      return getCitys().then(res => {
           this.loading = false;
           this.citys = this.convertCityData(res.data)
           this.province=this.citys.filter(item => item.parentId===0 )
@@ -130,18 +135,76 @@ export default {
         }
       });
     },
-    init(userId){
-      this.form.userId=userId;
+    async init(userId, addressData){
+      this.form.userId = userId;
+      if (this.province.length === 0) {
+        await this.getCityList();
+      }
+      this.$nextTick(() => {
+        if (this.$refs["form"]) {
+          this.$refs["form"].clearValidate();
+        }
+      });
+      if (addressData) {
+        this.isEdit = true;
+        this.editAddressId = addressData.addressId;
+        this.form.realName = addressData.realName;
+        this.form.phone = addressData.phone;
+        this.form.detail = addressData.detail;
+        this.form.isDefault = addressData.isDefault;
+        this.findAndSetCityIds(addressData);
+      } else {
+        this.isEdit = false;
+        this.editAddressId = null;
+        if (this.$refs["form"]) {
+          this.$refs["form"].resetFields();
+        }
+        this.form.realName = null;
+        this.form.phone = null;
+        this.form.detail = null;
+        this.form.province = null;
+        this.form.city = null;
+        this.form.district = null;
+        this.city = [];
+        this.district = [];
+      }
+    },
+    findAndSetCityIds(addressData) {
+      const provinceItem = this.province.find(p => p.name === addressData.province);
+      if (provinceItem) {
+        this.form.province = provinceItem.name;
+        this.city = provinceItem.children || [];
+        const cityItem = this.city.find(c => c.name === addressData.city);
+        if (cityItem) {
+          this.form.city = cityItem.name;
+          this.form.cityId = cityItem.cityId;
+          this.district = cityItem.children || [];
+          const districtItem = this.district.find(d => d.name === addressData.district);
+          if (districtItem) {
+            this.form.district = districtItem.name;
+          }
+        }
+      }
     },
     submitForm() {
       this.$refs["form"].validate(valid => {
         if (valid) {
-          addUserAddress(this.form).then(response => {
-            if (response.code === 200) {
-              this.msgSuccess("新增成功");
-              this.$emit("addUserAddress")
-            }
-          });
+          if (this.isEdit) {
+            this.form.addressId = this.editAddressId;
+            updateUserAddress(this.form).then(response => {
+              if (response.code === 200) {
+                this.msgSuccess("修改成功");
+                this.$emit("addUserAddress");
+              }
+            });
+          } else {
+            addUserAddress(this.form).then(response => {
+              if (response.code === 200) {
+                this.msgSuccess("新增成功");
+                this.$emit("addUserAddress");
+              }
+            });
+          }
         }
       });
     },

+ 6 - 0
src/views/hisStore/components/productOrder.vue

@@ -163,6 +163,11 @@
                   {{ order.remark }}
                 </span>
           </el-descriptions-item>
+          <el-descriptions-item v-if="projectCode === 'ddgy'" label="交易码">
+                <span v-if="order!=null ">
+                  {{ order.transactionCode }}
+                </span>
+          </el-descriptions-item>
           <el-descriptions-item label="销售名称" v-if="order!=null && order.orderType != 0">
                 <span >
                   {{ order.companyUserName }}
@@ -1120,6 +1125,7 @@ export default {
   data() {
     return {
       zdyInfo: process.env.VUE_APP_FS_USER_INFO,
+      projectCode: process.env.VUE_APP_PROJECT_CODE,
       // 遮罩层
       productOrderLoading: true,
       isStores: true,

+ 53 - 12
src/views/hisStore/storeOrder/list.vue

@@ -192,7 +192,7 @@
               v-hasPermi="['store:storeOrder:exportItems']"
             >导出订单明细</el-button>
           </el-col>
-          <!-- <el-col :span="1.5">
+          <el-col :span="1.5">
             <el-button
               type="success"
               icon="el-icon-s-check"
@@ -202,7 +202,7 @@
               v-hasPermi="['store:storeOrder:batchAudit']"
             >确认审核
             </el-button>
-          </el-col> -->
+          </el-col>
         <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
         </el-row>
         <el-tabs type="card" v-model="activeName" @tab-click="handleClick">
@@ -411,6 +411,16 @@
                        {{scope.row.province}} {{scope.row.city}} {{scope.row.district}} {{scope.row.detail}}
                     </template>
                 </el-table-column>
+                <el-table-column label="操作" align="center" width="80px" >
+                  <template slot-scope="scope">
+                    <el-button
+                      size="mini"
+                      type="text"
+                      icon="el-icon-edit"
+                      @click="handleEditUserAddress(scope.row)"
+                    >编辑</el-button>
+                  </template>
+                </el-table-column>
               </el-table>
               </el-radio-group>
             </el-form-item>
@@ -463,6 +473,9 @@
                 </el-col>
               </el-row>
             </el-form-item>
+            <el-form-item label="交易码" prop="transactionCode" v-if ="projectCode === 'ddgy'" style="width:380px;">
+              <el-input  v-model="form.transactionCode" placeholder="交易码"  />
+            </el-form-item>
             <el-form-item label="支付方式" prop="payType">
               <el-select   v-model="form.payType" placeholder="请选择支付方式" clearable size="small" >
               <el-option
@@ -480,13 +493,13 @@
                 <el-input-number  v-model="form.totalPrice" placeholder="修改商品总价" size="medium" :precision="2" :min=0.01 :step="0.1" />
             </el-form-item>
             <el-form-item label="定金" prop="payPrice" v-if ="projectCode === 'ddgy'">
-                <el-input-number  v-model="form.payPrice" @change="syncAmountByTotalPriceAndDeposit" placeholder="修改实际支付金额" size="medium" :precision="2" :min=0.01 :step="0.1" />
+                <el-input-number  v-model="form.payPrice" @change="syncAmountByTotalPriceAndDeposit" placeholder="修改实际支付金额" size="medium" :precision="2" :min=0 :step="0.1" />
             </el-form-item>
             <el-form-item label="物流代收" prop="amount" v-if="form.payType == '3' && projectCode !== 'ddgy'" >
               <el-input-number  v-model="form.amount" placeholder="平台支付价格" size="medium" :precision="2" :min=0.01 :step="0.1" />
             </el-form-item>
             <el-form-item label="代收货款" prop="amount" v-if="form.payType == '3' && projectCode === 'ddgy'">
-                <el-input-number  v-model="form.amount" placeholder="代收货款价格" size="medium" :precision="2" :min=0.01 :step="0.1" />
+                <el-input-number  v-model="form.amount" placeholder="代收货款价格" size="medium" :precision="2" :min=0 :step="0.1" :disabled="true" />
             </el-form-item>
             <el-form-item label="订单备注" prop="mark">
               <el-input  type="textarea" rows="2" v-model="form.mark" placeholder="" />
@@ -674,6 +687,18 @@ export default {
         products: [
           { required: true, message: "商品不能为空" }
         ],
+        payPrice: [
+          {
+            validator: (rule, value, callback) => {
+              if (this.projectCode === 'ddgy' && value != null && this.form.totalPrice != null && Number(value) > Number(this.form.totalPrice)) {
+                callback(new Error('定金金额不能大于总价'));
+              } else {
+                callback();
+              }
+            },
+            trigger: 'change'
+          }
+        ],
 
       }
     };
@@ -758,11 +783,19 @@ export default {
         this.msgError("请选择会员");
         return;
       }
+      this.userAddress.title = "创建收货地址";
       this.userAddress.open=true;
       setTimeout(() => {
         this.$refs.addUserAddress.init(this.form.userId);
       }, 500);
     },
+    handleEditUserAddress(row){
+      this.userAddress.title = "编辑收货地址";
+      this.userAddress.open=true;
+      setTimeout(() => {
+        this.$refs.addUserAddress.init(this.form.userId, row);
+      }, 500);
+    },
     addUserAddress(){
       this.userAddress.open=false;
       //获取地址
@@ -898,7 +931,7 @@ export default {
         return;
       }
       const amount = Number((this.form.totalPrice - this.form.payPrice).toFixed(2));
-      this.$set(this.form, "amount", amount > 0 ? amount : null);
+      this.$set(this.form, "amount", amount >= 0 ? amount : null);
     },
     /** 搜索按钮操作 */
     handleQuery() {
@@ -1031,14 +1064,22 @@ export default {
         }).catch(function() {});
     },
 
-    /** 订单确认审核 */
+    /** 订单确认审核(支持单行和批量) */
     handleOrderAudit(row){
-      // if (this.ids.length === 0) {
-      //   this.$message.warning('请至少勾选一条订单进行审核');
-      //   return;
-      // }
-      const ids = [row.id];
-      this.$confirm('是否确认审核选中的订单?', "提示", {
+      let ids;
+      let tipMsg;
+      if (row && row.id) {
+        ids = [row.id];
+        tipMsg = '是否确认审核该订单?';
+      } else {
+        ids = this.ids.filter(id => id != null);
+        if (ids.length === 0) {
+          this.$message.warning('请至少勾选一条订单进行审核');
+          return;
+        }
+        tipMsg = `是否确认审核选中的 ${ids.length} 条订单?`;
+      }
+      this.$confirm(tipMsg, "提示", {
         confirmButtonText: "确定",
         cancelButtonText: "取消",
         type: "warning"

+ 729 - 5
src/views/live/liveWatchUser/index.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="app-container">
-    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="88px">
       <el-form-item label="直播ID" prop="liveId">
         <el-input
           v-model="queryParams.liveId"
@@ -10,40 +10,764 @@
           @keyup.enter.native="handleQuery"
         />
       </el-form-item>
+      <el-form-item label="直播名称" prop="liveName">
+        <el-input
+          v-model="queryParams.liveName"
+          placeholder="请输入直播名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="用户ID" prop="userId">
+        <el-input
+          v-model="queryParams.userId"
+          placeholder="请输入用户ID"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="用户昵称" prop="userName">
+        <el-input
+          v-model="queryParams.userName"
+          placeholder="请输入用户昵称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="销售ID" prop="companyUserId">
+        <el-input
+          v-model="queryParams.companyUserId"
+          placeholder="请输入销售用户ID"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="观看时间" prop="dateRange">
+        <el-date-picker
+          v-model="dateRange"
+          type="datetimerange"
+          size="small"
+          value-format="yyyy-MM-dd HH:mm:ss"
+          range-separator="-"
+          start-placeholder="开始时间"
+          end-placeholder="结束时间"
+          :default-time="['00:00:00', '23:59:59']"
+        />
+      </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
+          size="mini"
+          v-hasPermi="['app:user:bindTag']"
+          @click="openBindTagPage(false, 0)"
+        >批量添加标签
+        </el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="info"
+          plain
+          size="mini"
+          v-hasPermi="['app:user:unbindTag']"
+          @click="openUnbindTagPage(false, 1)"
+        >批量移除标签
+        </el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          plain
+          size="mini"
+          v-hasPermi="['app:user:batchBindTag']"
+          @click="openBindTagPage(true, 0)"
+        >批量添加标签(筛选条件)
+        </el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="info"
+          plain
+          size="mini"
+          v-hasPermi="['app:user:batchUnbindTag']"
+          @click="openUnbindTagPage(true, 1)"
+        >批量移除标签(筛选条件)
+        </el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="success"
+          plain
+          size="mini"
+          @click="openRemarkPage(false)"
+        >批量修改备注
+        </el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="success"
+          plain
+          size="mini"
+          @click="openRemarkPage(true)"
+        >批量修改备注(筛选条件)
+        </el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table v-loading="loading" :data="list" border @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="用户ID" align="center" prop="userId" width="90" />
+      <el-table-column label="用户昵称" align="center" prop="userName" min-width="120" show-overflow-tooltip />
+      <el-table-column label="直播ID" align="center" prop="liveId" width="90" />
+      <el-table-column label="直播名称" align="center" prop="liveName" min-width="140" show-overflow-tooltip />
+      <el-table-column label="直播观看时长" align="center" min-width="120">
+        <template slot-scope="scope">
+          {{ formatDuration(scope.row.liveWatchDuration) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="回放观看时长" align="center" min-width="120">
+        <template slot-scope="scope">
+          {{ formatDuration(scope.row.playbackWatchDuration) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="是否完课" align="center" width="90">
+        <template slot-scope="scope">
+          <el-tag :type="scope.row.isCompleted === 1 ? 'success' : 'info'" size="mini">
+            {{ scope.row.isCompleted === 1 ? '是' : '否' }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="订单数" align="center" prop="orderCount" width="80" />
+      <el-table-column label="订单金额" align="center" prop="orderAmount" width="100" />
+      <el-table-column label="公司" align="center" prop="companyName" min-width="120" show-overflow-tooltip />
+      <el-table-column label="销售" align="center" prop="salesName" min-width="100" show-overflow-tooltip />
+      <el-table-column prop="rewardType" label="领取类型" min-width="220" align="center">
+        <template slot-scope="scope">
+          <div v-if="hasClaimedReward(scope.row.rewardType)">
+            <dict-tag :options="liveRewardTypeList" :value="scope.row.rewardType"/>
+            <el-button
+              v-if="hasRedOrPointsReward(scope.row.rewardType)"
+              type="text"
+              size="small"
+              style="margin-left: 8px;"
+              @click="handleOpenRedPacketLog(scope.row)"
+            >红包/积分领取详细</el-button>
+            <el-button
+              v-if="hasCouponReward(scope.row.rewardType)"
+              type="text"
+              size="small"
+              style="margin-left: 8px;"
+              @click="handleOpenCouponLog(scope.row)"
+            >核销卷领取详情</el-button>
+          </div>
+          <span v-else>
+            <dict-tag
+              v-if="scope.row.rewardType === 0 || scope.row.rewardType === '0' || scope.row.rewardType === 99 || scope.row.rewardType === '99'"
+              :options="liveRewardTypeList"
+              :value="scope.row.rewardType"
+            />
+            <span v-else>未领取</span>
+          </span>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total > 0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <!-- 红包领取详情侧边栏 -->
+    <el-drawer
+      title="红包领取详情"
+      :visible.sync="redPacketLogDrawerVisible"
+      direction="rtl"
+      size="60%"
+    >
+      <el-table
+        :data="redPacketLogList"
+        v-loading="redPacketLogLoading"
+        border
+        style="width: 100%"
+      >
+        <el-table-column type="index" label="序号" width="60" align="center"></el-table-column>
+        <el-table-column prop="liveId" label="直播间id" min-width="120"></el-table-column>
+        <el-table-column prop="userId" label="用户id" min-width="120"></el-table-column>
+        <el-table-column prop="amount" label="金额" min-width="120" align="center">
+          <template slot-scope="scope">
+            {{ scope.row.amount != null ? scope.row.amount : '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="status" label="状态" min-width="100" align="center">
+          <template slot-scope="scope">
+            <el-tag v-if="scope.row.status == 1" type="success">已领取</el-tag>
+            <el-tag v-else-if="scope.row.status == 0" type="info">未领取</el-tag>
+            <el-tag v-else type="info">{{ scope.row.status }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="createTime" label="领取时间" min-width="160" align="center"></el-table-column>
+        <el-table-column prop="remark" label="备注" min-width="160" align="center"></el-table-column>
+      </el-table>
+      <pagination
+        v-show="redPacketLogTotal > 0"
+        :total="redPacketLogTotal"
+        :page.sync="redPacketLogQueryParams.pageNum"
+        :limit.sync="redPacketLogQueryParams.pageSize"
+        :page-sizes="[10, 20, 50]"
+        @pagination="loadRedPacketLogList"
+        style="margin-top: 20px;"
+      />
+    </el-drawer>
+
+    <!-- 核销卷领取详情侧边栏 -->
+    <el-drawer
+      title="核销卷领取详情"
+      :visible.sync="couponLogDrawerVisible"
+      direction="rtl"
+      size="60%"
+    >
+      <el-table
+        :data="couponLogList"
+        v-loading="couponLogLoading"
+        border
+        style="width: 100%"
+      >
+        <el-table-column type="index" label="序号" width="60" align="center"></el-table-column>
+        <el-table-column prop="nickName" label="会员昵称" min-width="120"></el-table-column>
+        <el-table-column prop="couponTitle" label="优惠券名称" min-width="120"></el-table-column>
+        <el-table-column prop="limitTime" label="优惠券有效期" min-width="160" align="center"></el-table-column>
+        <el-table-column prop="startTime" label="开始时间" min-width="160" align="center"></el-table-column>
+        <el-table-column prop="updateTime" label="更新时间" min-width="160" align="center"></el-table-column>
+        <el-table-column prop="status" label="状态" min-width="100" align="center">
+          <template slot-scope="scope">
+            <el-tag v-if="scope.row.status == 1" type="success">已使用</el-tag>
+            <el-tag v-else-if="scope.row.status == 0" type="info">未使用</el-tag>
+            <el-tag v-else type="info">{{ scope.row.status }}</el-tag>
+          </template>
+        </el-table-column>
+      </el-table>
+      <pagination
+        v-show="couponLogTotal > 0"
+        :total="couponLogTotal"
+        :page.sync="couponLogQueryParams.pageNum"
+        :limit.sync="couponLogQueryParams.pageSize"
+        :page-sizes="[10, 20, 50]"
+        @pagination="loadCouponLogList"
+        style="margin-top: 20px;"
+      />
+    </el-drawer>
+
+    <!-- 批量添加/移除标签 -->
+    <el-dialog :title="tagConfig.title" :visible.sync="tagConfig.visible" width="800px" append-to-body>
+      <div>搜索标签:
+        <el-input
+          v-model="tagConfig.queryParams.groupName"
+          placeholder="请输入标签名称"
+          clearable
+          size="small"
+          style="width: 200px;margin-right: 10px"
+        />
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleSearchTags">搜索</el-button>
+        <el-button type="primary" icon="el-icon-refresh" size="mini" @click="cancelSearchTags">重置</el-button>
+      </div>
+      <div class="dialog-body">
+        <div v-for="groupItem in tagConfig.tagGroupList" :key="groupItem.id">
+          <div style="font-size: 20px;margin-top: 20px;margin-bottom: 20px;">
+            <span class="name-background">{{ groupItem.name }}</span>
+          </div>
+          <div class="tag-container">
+            <a
+              v-for="tagItem in groupItem.tags"
+              :key="tagItem.id"
+              class="tag-box"
+              @click="tagSelection(tagItem)"
+              :class="{ 'tag-selected': tagItem.isSelected }"
+            >
+              {{ tagItem.name }}
+            </a>
+          </div>
+        </div>
+      </div>
+      <pagination
+        v-show="tagConfig.total > 0"
+        :total="tagConfig.total"
+        :page.sync="tagConfig.queryParams.pageNum"
+        :limit.sync="tagConfig.queryParams.pageSize"
+        @pagination="getTagGroupList"
+      />
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="bindOrUnbindTagSubmit">确 定</el-button>
+        <el-button @click="bindTagCancel">取 消</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 批量修改备注 -->
+    <el-dialog :title="remarkConfig.title" :visible.sync="remarkConfig.visible" width="600px" append-to-body>
+      <el-form label-width="90px">
+        <el-form-item label="备注类型">
+          <el-radio-group v-model="remarkConfig.remarkType">
+            <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="备注内容">
+          <el-input
+            v-model="remarkConfig.remark"
+            type="textarea"
+            :rows="3"
+            placeholder="请输入备注信息"
+            maxlength="30"
+            show-word-limit
+          />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" :loading="remarkConfig.loading" @click="submitRemark">确 定</el-button>
+        <el-button @click="cancelRemark">取 消</el-button>
+      </div>
+    </el-dialog>
   </div>
 </template>
 
 <script>
+import { liveWatchUserList } from '@/api/live/liveWatchUser'
+import { listLiveRedPacketLog } from '@/api/live/liveRedPacketLog'
+import { getCourseCouponUserListVO } from '@/api/course/courseCouponUser'
+import { listTagGroupForUserBindTag } from '@/api/app/tag/tagGroup'
+import { bindTagV2, unbindTagV2 } from '@/api/app/tag/bindTag'
+import { batchUpdateFriendRemark } from '@/api/course/courseWatchLog'
 
 export default {
   name: 'LiveWatchUser',
   data() {
     return {
+      loading: false,
       showSearch: true,
-      queryForm: null,
+      total: 0,
+      list: [],
+      ids: [],
+      liveRewardTypeList: [],
+      dateRange: [],
       queryParams: {
         pageNum: 1,
-        pageSize: 10
+        pageSize: 10,
+        liveId: null,
+        liveName: null,
+        userId: null,
+        userName: null,
+        companyId: null,
+        companyUserId: null,
+        bTime: null,
+        eTime: null
+      },
+      tagConfig: {
+        visible: false,
+        enableFilter: false,
+        title: null,
+        opType: 0,
+        queryParams: {
+          groupName: null,
+          pageNum: 1,
+          pageSize: 10
+        },
+        total: 0,
+        tagGroupList: [],
+        selected: []
+      },
+      redPacketLogDrawerVisible: false,
+      redPacketLogList: [],
+      redPacketLogLoading: false,
+      redPacketLogTotal: 0,
+      redPacketLogQueryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        liveId: null,
+        userId: null
+      },
+      couponLogDrawerVisible: false,
+      couponLogList: [],
+      couponLogLoading: false,
+      couponLogTotal: 0,
+      couponLogQueryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        userId: null,
+        liveId: null,
+        sendType: 2
+      },
+      remarkConfig: {
+        visible: false,
+        enableFilter: false,
+        title: '批量修改备注',
+        remarkType: 1,
+        remark: '',
+        loading: false
       }
     }
   },
+  computed: {
+    currentCompanyId() {
+      const user = this.$store.state.user && this.$store.state.user.user
+      return user ? user.companyId : null
+    }
+  },
   created() {
-
+    this.getDicts('live_watch_reward_type').then(res => {
+      this.liveRewardTypeList = res.data || []
+    })
+    this.getList()
   },
   methods: {
+    handleSelectionChange(selection) {
+      this.ids = [...new Set(selection.map(item => item.userId).filter(id => id != null))]
+    },
+    openBindTagPage(enableFilter = false, opType = 0) {
+      this.tagConfig.enableFilter = enableFilter
+      this.tagConfig.opType = opType
+      if (!enableFilter && (!this.ids || this.ids.length === 0)) {
+        this.$message.closeAll()
+        return this.$message('请选择需要添加标签的用户')
+      }
+      this.getTagGroupList()
+      this.tagConfig.title = '批量添加标签' + (enableFilter ? '(筛选条件)' : '')
+      this.tagConfig.visible = true
+    },
+    openUnbindTagPage(enableFilter = false, opType = 1) {
+      this.tagConfig.enableFilter = enableFilter
+      this.tagConfig.opType = opType
+      if (!enableFilter && (!this.ids || this.ids.length === 0)) {
+        this.$message.closeAll()
+        return this.$message('请选择需要移除标签的用户')
+      }
+      this.getTagGroupList()
+      this.tagConfig.title = '批量移除标签' + (enableFilter ? '(筛选条件)' : '')
+      this.tagConfig.visible = true
+    },
+    handleSearchTags() {
+      this.tagConfig.queryParams.pageNum = 1
+      this.getTagGroupList()
+    },
+    cancelSearchTags() {
+      this.tagConfig.queryParams = {
+        groupName: null,
+        pageNum: 1,
+        pageSize: 10
+      }
+      this.getTagGroupList()
+    },
+    tagSelection(row) {
+      row.isSelected = !row.isSelected
+      if (row.isSelected) {
+        this.tagConfig.selected.push({ tagId: row.id, tagName: row.name })
+      } else {
+        this.tagConfig.selected = this.tagConfig.selected.filter(item => item.tagId !== row.id)
+      }
+      this.$forceUpdate()
+    },
+    resetBindTag() {
+      this.tagConfig = {
+        visible: false,
+        enableFilter: false,
+        title: null,
+        opType: 0,
+        queryParams: {
+          groupName: null,
+          pageNum: 1,
+          pageSize: 10
+        },
+        total: 0,
+        tagGroupList: [],
+        selected: []
+      }
+    },
+    buildFsUserParam() {
+      const q = this.buildQuery()
+      return {
+        userId: q.userId,
+        nickName: q.userName,
+        companyId: q.companyId,
+        companyUserId: q.companyUserId
+      }
+    },
+    bindOrUnbindTagSubmit() {
+      const opType = this.tagConfig.opType
+      if (!this.tagConfig.selected || this.tagConfig.selected.length === 0) {
+        return this.$message('请选择标签')
+      }
+      const tmpParam = {
+        enableFilter: this.tagConfig.enableFilter,
+        tagId: this.tagConfig.selected.map(item => item.tagId),
+        userId: this.ids,
+        fsUserParam: this.buildFsUserParam()
+      }
+      if (opType === 0) {
+        bindTagV2(tmpParam)
+          .then(() => {
+            this.tagConfig.visible = false
+            this.resetBindTag()
+            this.msgSuccess('正在添加中!')
+            setTimeout(() => this.getList(), 500)
+          })
+          .catch(() => {
+            this.msgError('添加标签失败!')
+          })
+      } else {
+        unbindTagV2(tmpParam)
+          .then(() => {
+            this.tagConfig.visible = false
+            this.resetBindTag()
+            this.msgSuccess('正在移除中!')
+            setTimeout(() => this.getList(), 500)
+          })
+          .catch(() => {
+            this.msgError('移除标签失败!')
+          })
+      }
+    },
+    bindTagCancel() {
+      this.resetBindTag()
+    },
+    openRemarkPage(enableFilter = false) {
+      if (!enableFilter && (!this.ids || this.ids.length === 0)) {
+        this.$message.closeAll()
+        return this.$message('请选择需要修改备注的用户')
+      }
+      if (enableFilter && (!this.total || this.total <= 0)) {
+        this.$message.closeAll()
+        return this.$message('当前筛选条件下没有数据')
+      }
+      this.remarkConfig.enableFilter = enableFilter
+      this.remarkConfig.remarkType = 1
+      this.remarkConfig.remark = ''
+      this.remarkConfig.loading = false
+      this.remarkConfig.title = '批量修改备注' + (enableFilter ? '(筛选条件)' : '')
+      this.remarkConfig.visible = true
+    },
+    cancelRemark() {
+      this.remarkConfig.visible = false
+      this.remarkConfig.enableFilter = false
+      this.remarkConfig.remarkType = 1
+      this.remarkConfig.remark = ''
+      this.remarkConfig.loading = false
+    },
+    async submitRemark() {
+      const remark = (this.remarkConfig.remark || '').trim()
+      if (!remark) {
+        return this.$message.error('请输入备注内容')
+      }
+      if (remark.length > 30) {
+        return this.$message.error('备注内容不能超过30个字符')
+      }
+      this.remarkConfig.loading = true
+      try {
+        let userIds = []
+        if (this.remarkConfig.enableFilter) {
+          const queryParams = this.buildQuery()
+          queryParams.pageNum = 1
+          queryParams.pageSize = this.total > 0 ? this.total : 10000
+          const response = await liveWatchUserList(queryParams)
+          const rows = response.data || []
+          userIds = [...new Set(rows.map(item => item.userId).filter(id => id != null))]
+        } else {
+          userIds = this.ids
+        }
+        if (!userIds || userIds.length === 0) {
+          this.remarkConfig.loading = false
+          return this.$message.error('没有可修改备注的用户')
+        }
+        await batchUpdateFriendRemark({
+          remarkType: this.remarkConfig.remarkType,
+          remark,
+          userId: userIds
+        })
+        this.msgSuccess('正在修改备注中!')
+        this.cancelRemark()
+        setTimeout(() => {
+          this.getList()
+        }, 500)
+      } catch (e) {
+        this.msgError('修改备注失败!')
+      } finally {
+        this.remarkConfig.loading = false
+      }
+    },
+
+    getTagGroupList() {
+      listTagGroupForUserBindTag(this.tagConfig.queryParams).then(response => {
+        this.tagConfig.tagGroupList = response.rows || []
+        this.tagConfig.total = response.total || 0
+        const selectedIds = new Set(this.tagConfig.selected.map(i => i.tagId))
+        this.tagConfig.tagGroupList.forEach(group => {
+          (group.tags || []).forEach(tag => {
+            this.$set(tag, 'isSelected', selectedIds.has(tag.id))
+          })
+        })
+      })
+    },
+    hasClaimedReward(type) {
+      if (type == null || type === '') return false
+      const t = Number(type)
+      if (Number.isNaN(t) || t === 0 || t === 99) return false
+      return true
+    },
+    hasRedOrPointsReward(type) {
+      const t = Number(type)
+      if (Number.isNaN(t) || t >= 97) return false
+      return (t & 1) !== 0 || (t & 2) !== 0
+    },
+    hasCouponReward(type) {
+      const t = Number(type)
+      if (Number.isNaN(t) || t >= 97) return false
+      return (t & 4) !== 0
+    },
+    handleOpenRedPacketLog(row) {
+      this.redPacketLogDrawerVisible = true
+      this.redPacketLogQueryParams.liveId = row.liveId
+      this.redPacketLogQueryParams.userId = row.userId
+      this.redPacketLogQueryParams.pageNum = 1
+      this.loadRedPacketLogList()
+    },
+    handleOpenCouponLog(row) {
+      this.couponLogDrawerVisible = true
+      this.couponLogQueryParams.userId = row.userId
+      this.couponLogQueryParams.liveId = row.liveId
+      this.couponLogQueryParams.pageNum = 1
+      this.loadCouponLogList()
+    },
+    loadCouponLogList() {
+      this.couponLogLoading = true
+      getCourseCouponUserListVO({
+        pageNum: this.couponLogQueryParams.pageNum,
+        pageSize: this.couponLogQueryParams.pageSize,
+        userId: this.couponLogQueryParams.userId,
+        sendType: this.couponLogQueryParams.sendType,
+        liveId: this.couponLogQueryParams.liveId
+      }).then(response => {
+        this.couponLogList = response.rows || []
+        this.couponLogTotal = response.total || 0
+        this.couponLogLoading = false
+      }).catch(() => {
+        this.couponLogLoading = false
+      })
+    },
+    loadRedPacketLogList() {
+      this.redPacketLogLoading = true
+      listLiveRedPacketLog({
+        pageNum: this.redPacketLogQueryParams.pageNum,
+        pageSize: this.redPacketLogQueryParams.pageSize,
+        liveId: this.redPacketLogQueryParams.liveId,
+        userId: this.redPacketLogQueryParams.userId
+      }).then(response => {
+        if (response.code === 200) {
+          this.redPacketLogList = response.rows || []
+          this.redPacketLogTotal = response.total || 0
+        }
+        this.redPacketLogLoading = false
+      }).catch(() => {
+        this.redPacketLogLoading = false
+      })
+    },
+    buildQuery() {
+      const params = { ...this.queryParams }
+      params.companyId = this.currentCompanyId
+      if (this.dateRange && this.dateRange.length === 2) {
+        params.bTime = this.dateRange[0]
+        params.eTime = this.dateRange[1]
+      } else {
+        params.bTime = null
+        params.eTime = null
+      }
+      ;['liveId', 'userId', 'companyId', 'companyUserId'].forEach(key => {
+        if (params[key] === '' || params[key] === undefined) {
+          params[key] = null
+        } else if (params[key] != null) {
+          params[key] = Number(params[key])
+        }
+      })
+      if (params.liveName === '') params.liveName = null
+      if (params.userName === '') params.userName = null
+      return params
+    },
+    getList() {
+      this.loading = true
+      liveWatchUserList(this.buildQuery()).then(res => {
+        this.list = res.data || []
+        this.total = res.total || 0
+      }).finally(() => {
+        this.loading = false
+      })
+    },
     handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
     },
     resetQuery() {
+      this.dateRange = []
+      this.resetForm('queryForm')
+      this.queryParams.pageNum = 1
+      this.queryParams.pageSize = 10
+      this.handleQuery()
+    },
+    formatDuration(seconds) {
+      const s = Number(seconds) || 0
+      if (s < 60) return s + '秒'
+      const m = Math.floor(s / 60)
+      const remain = s % 60
+      if (m < 60) return remain ? `${m}分${remain}秒` : `${m}分钟`
+      const h = Math.floor(m / 60)
+      const rm = m % 60
+      return rm ? `${h}小时${rm}分` : `${h}小时`
     }
   }
 }
 </script>
 
-<style scoped>
+<style lang="scss" scoped>
+.dialog-body {
+  height: 400px;
+  overflow: auto;
+}
+
+.tag-container {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+}
 
+.name-background {
+  display: inline-block;
+  background-color: #abece6;
+  padding: 4px 8px;
+  border-radius: 4px;
+}
+
+.tag-box {
+  padding: 8px 12px;
+  border: 1px solid #989797;
+  border-radius: 4px;
+  cursor: pointer;
+  display: inline-block;
+}
+
+.tag-selected {
+  background-color: #00bc98;
+  color: #fff;
+  border-color: #00bc98;
+}
 </style>