Parcourir la source

直播看课记录

三七 il y a 20 heures
Parent
commit
687aae40a5

+ 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,

+ 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>