Browse Source

直播看课记录

三七 1 day ago
parent
commit
c5e21b6551

+ 39 - 0
src/api/course/feishuConfig.js

@@ -0,0 +1,39 @@
+import request from '@/utils/request'
+
+export function listFeishuConfig(query) {
+  return request({
+    url: '/course/feishuConfig/list',
+    method: 'get',
+    params: query
+  })
+}
+
+export function getFeishuConfig(id) {
+  return request({
+    url: '/course/feishuConfig/' + id,
+    method: 'get'
+  })
+}
+
+export function addFeishuConfig(data) {
+  return request({
+    url: '/course/feishuConfig',
+    method: 'post',
+    data: data
+  })
+}
+
+export function updateFeishuConfig(data) {
+  return request({
+    url: '/course/feishuConfig',
+    method: 'put',
+    data: data
+  })
+}
+
+export function delFeishuConfig(id) {
+  return request({
+    url: '/course/feishuConfig/' + id,
+    method: 'delete'
+  })
+}

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

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

+ 1 - 1
src/assets/callCenterPhoneBarSdk/phone-bar-ex.html

@@ -1,4 +1,4 @@
-<!DOCTYPE html>
+<!DOCTYPE html>
 <html>
 <head>
 <title>呼叫中心html客户端工具条</title>

+ 30 - 4
src/views/app/user/CusRoleList.vue

@@ -80,6 +80,11 @@ export default {
       type: Array,
       default: () => [],
     },
+    // 所属公司id:有值时仅查该公司下销售
+    companyId: {
+      type: [Number, String],
+      default: null,
+    },
   },
   data() {
     return {
@@ -94,13 +99,15 @@ export default {
         pageNum: 1,
         pageSize: 10,
         memberName: null,
-        roleName: null
+        roleName: null,
+        companyId: null,
       },
     };
   },
   created() {
     this.queryParams.pageNum = this.defaultPageNum;
     this.queryParams.pageSize = this.defaultPageSize;
+    this.queryParams.companyId = this.companyId || null;
     this.getList();
   },
   watch: {
@@ -109,15 +116,33 @@ export default {
       handler(nv) {
         this.setSelectionStatus();
       }
+    },
+    companyId: {
+      handler(n) {
+        this.reloadByCompany(n);
+      }
     }
   },
   methods: {
+    /**
+     * 按公司刷新列表(搜索页切换所属公司后调用)
+     */
+    reloadByCompany(companyId) {
+      this.queryParams.companyId = companyId || null;
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
     /**
      * 查询客服成员列表
      */
     getList() {
       this.loading = true;
-      let qp = this.queryParams;
+      let qp = { ...this.queryParams };
+      if (this.companyId) {
+        qp.companyId = this.companyId;
+      } else if (!qp.companyId) {
+        delete qp.companyId;
+      }
       if (this.ignoreIds && this.ignoreIds.length) {
         qp['ignoreIds'] = this.ignoreIds;
       } else {
@@ -165,6 +190,7 @@ export default {
         pageSize: this.defaultPageSize,
         memberName: null,
         roleName: null,
+        companyId: this.companyId || null,
       }
       this.resetForm("form");
     },
@@ -197,9 +223,9 @@ export default {
      */
     resetQuery() {
       this.resetForm("queryForm");
+      this.queryParams.companyId = this.companyId || null;
       this.handleQuery();
-    },
-    /**
+    },    /**
      * 单选
      * @param selection
      * @param row

+ 41 - 29
src/views/app/user/index.vue

@@ -31,15 +31,15 @@
         />
       </el-form-item>
 
-      <el-form-item label="邀请码" prop="invitationCode">
-        <el-input
-          v-model="queryParams.invitationCode"
-          placeholder="请输入邀请码"
-          clearable
-          size="small"
-          @keyup.enter.native="handleQuery"
-        />
-      </el-form-item>
+<!--      <el-form-item label="邀请码" prop="invitationCode">-->
+<!--        <el-input-->
+<!--          v-model="queryParams.invitationCode"-->
+<!--          placeholder="请输入邀请码"-->
+<!--          clearable-->
+<!--          size="small"-->
+<!--          @keyup.enter.native="handleQuery"-->
+<!--        />-->
+<!--      </el-form-item>-->
 
       <el-form-item label="所属部门" prop="deptId">
         <treeselect
@@ -58,7 +58,7 @@
           clearable
           filterable
         >
-          <el-option 
+          <el-option
             v-for="company in companyOptions"
             :value="company.companyId"
             :label="company.companyName"
@@ -66,6 +66,23 @@
         </el-select>
       </el-form-item>
 
+      <el-form-item label="销售" prop="customerRoles">
+        <div @click="openCusSearchPage"
+             style="cursor: pointer; border: 1px solid #e6e6e6; background-color: white; overflow: hidden; flex-grow: 1;width: 250px">
+          <div style="min-height: 35px; max-height: 200px; overflow-y: auto;">
+            <el-tag type="success"
+                    closable
+                    :disable-transitions="false"
+                    v-for="list in this.bindAISearch.selected"
+                    :key="list.id"
+                    @close="handleCloseCus(list)"
+                    style="margin: 3px;"
+            >{{ list.memberName }}
+            </el-tag>
+          </div>
+        </div>
+      </el-form-item>
+
       <el-form-item label="邀请日期" prop="inviteDate">
         <el-date-picker clearable size="small" v-model="queryParams.inviteDate" type="daterange" value-format="yyyy-MM-dd"
           placeholder="选择邀请日期">
@@ -89,22 +106,7 @@
         </div>
       </el-form-item>
 
-      <el-form-item label="客服" prop="customerRoles">
-        <div @click="openCusSearchPage"
-             style="cursor: pointer; border: 1px solid #e6e6e6; background-color: white; overflow: hidden; flex-grow: 1;width: 250px">
-          <div style="min-height: 35px; max-height: 200px; overflow-y: auto;">
-            <el-tag type="success"
-                closable
-                :disable-transitions="false"
-                v-for="list in this.bindAISearch.selected"
-                :key="list.id"
-                @close="handleCloseCus(list)"
-                style="margin: 3px;"
-            >{{ list.roleName }}
-            </el-tag>
-          </div>
-        </div>
-      </el-form-item>
+
 
       <el-form-item>
         <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
@@ -211,6 +213,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"
@@ -229,9 +232,9 @@
          </template>
       </el-table-column>
       <el-table-column label="注册时间" align="center" prop="createTime" min-width="150px"/>
-       <el-table-column label="邀请码" align="center" prop="invitationCode" min-width="150px"/>
+<!--       <el-table-column label="邀请码" align="center" prop="invitationCode" min-width="150px"/>-->
        <el-table-column label="邀请时间" align="center" prop="inviteTime" min-width="150px"/>
-       <el-table-column label="客服" align="center" min-width="200px" prop="customerRoles">
+       <el-table-column label="销售" align="center" min-width="200px" prop="customerRoles">
         <template slot-scope="scope">
           <div v-for="(item, idx) in (scope.row.customerRoles ? scope.row.customerRoles.split(',') : [])" :key="idx" style="display: inline;">
             <el-tag type="primary" style="margin: 3px;">{{item}}</el-tag>
@@ -405,6 +408,7 @@
     <!-- app客服搜索 -->
     <el-dialog title="客服搜索" :visible.sync="bindAISearch.visible" width="1000px" append-to-body>
       <CusRoleList ref="customerSearchRef"
+        :company-id="queryParams.companyId"
         @confirm="customerSelectedConfirmCall"
         @selectionChange="customerSelectionChangeCall"
         :selected="bindAISearch.selected"
@@ -654,7 +658,7 @@ export default {
           habits: null,
           illnessTime: null,
           body: null,
-          family: null, 
+          family: null,
           familyDisease: null,
           disease: null,
           isLine: null,
@@ -701,6 +705,9 @@ export default {
         if (!this.isEmpty(n)) {
           this.queryParams.deptId = null;
         }
+        // 切换公司后清空已选销售,避免跨公司条件混用;打开客服弹窗时按新公司过滤
+        this.bindAISearch.selected = [];
+        this.queryParams.customerRoles = [];
       },
     },
     'queryParams.deptId': {
@@ -1084,6 +1091,11 @@ export default {
     },
     openCusSearchPage() {
       this.bindAISearch.visible = true;
+      this.$nextTick(() => {
+        if (this.$refs.customerSearchRef && this.$refs.customerSearchRef.reloadByCompany) {
+          this.$refs.customerSearchRef.reloadByCompany(this.queryParams.companyId);
+        }
+      });
     },
     searchTagSubmit() {
       this.tagSearchConfig.visible = false;

+ 332 - 0
src/views/course/coursePlaySourceConfig/feishuConfig.vue

@@ -0,0 +1,332 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="80px">
+      <el-form-item label="飞书账号" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入飞书账号"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="appId" prop="appId">
+        <el-input
+          v-model="queryParams.appId"
+          placeholder="请输入appId"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择状态" clearable size="small">
+          <el-option
+            v-for="item in statusOptions"
+            :key="item.value"
+            :label="item.label"
+            :value="item.value"
+          />
+        </el-select>
+      </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="['course:feishuConfig:add']"
+        >新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="success"
+          plain
+          icon="el-icon-edit"
+          size="mini"
+          :disabled="single"
+          @click="handleUpdate"
+          v-hasPermi="['course:feishuConfig:edit']"
+        >修改</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="['course:feishuConfig:remove']"
+        >删除</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table v-loading="loading" :data="list" @selection-change="handleSelectionChange" border>
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="ID" align="center" prop="id" width="80" />
+      <el-table-column label="飞书账号" align="center" prop="name" />
+      <el-table-column label="appId" align="center" prop="appId" min-width="160" show-overflow-tooltip />
+      <el-table-column label="appSecret" align="center" prop="appSecret" min-width="180" show-overflow-tooltip />
+      <el-table-column label="状态" align="center" prop="status" width="100">
+        <template slot-scope="scope">
+          <el-tag :type="statusTagType(scope.row.status)">
+            {{ getStatusLabel(scope.row.status) }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="备注" align="center" prop="remark" show-overflow-tooltip />
+      <el-table-column label="创建时间" align="center" prop="createTime" width="170" />
+      <el-table-column label="修改时间" align="center" prop="updateTime" width="170" />
+      <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="['course:feishuConfig:edit']"
+          >修改</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            @click="handleDelete(scope.row)"
+            v-hasPermi="['course:feishuConfig: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="680px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="110px">
+        <el-form-item label="飞书账号" prop="name">
+          <el-input v-model="form.name" placeholder="请输入飞书账号名称" />
+        </el-form-item>
+        <el-form-item label="appId" prop="appId">
+          <el-input v-model="form.appId" placeholder="请输入飞书应用 AppId" />
+        </el-form-item>
+        <el-form-item label="appSecret" prop="appSecret">
+          <el-input v-model="form.appSecret" placeholder="请输入飞书应用 AppSecret" show-password />
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-select v-model="form.status" placeholder="请选择状态" style="width: 100%">
+            <el-option
+              v-for="item in statusOptions"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="权限导入示例">
+          <el-input
+            :value="permissionImportExample"
+            type="textarea"
+            :rows="12"
+            readonly
+            class="permission-example"
+          />
+          <el-button type="primary" plain size="mini" icon="el-icon-document-copy" style="margin-top: 8px" @click="copyPermissionExample">
+            一键复制
+          </el-button>
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="备注" />
+        </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 {
+  listFeishuConfig,
+  getFeishuConfig,
+  addFeishuConfig,
+  updateFeishuConfig,
+  delFeishuConfig
+} from '@/api/course/feishuConfig'
+
+export default {
+  name: 'FeishuConfig',
+  data() {
+    return {
+      loading: false,
+      showSearch: true,
+      ids: [],
+      single: true,
+      multiple: true,
+      total: 0,
+      list: [],
+      title: '',
+      open: false,
+      statusOptions: [
+        { label: '正常', value: 1 },
+        { label: '停用', value: 0 },
+        { label: '已封禁', value: -1 }
+      ],
+      permissionImportExample: `{
+  "scopes": {
+    "tenant": [
+      "docs:permission.setting:read",
+      "docs:permission.setting:write_only",
+      "docx:document",
+      "drive:drive",
+      "space:document:delete"
+    ],
+    "user": []
+  }
+}`,
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        name: null,
+        appId: null,
+        status: null
+      },
+      form: {},
+      rules: {
+        name: [{ required: true, message: '飞书账号不能为空', trigger: 'blur' }],
+        appId: [{ required: true, message: 'appId不能为空', trigger: 'blur' }],
+        appSecret: [{ required: true, message: 'appSecret不能为空', trigger: 'blur' }],
+        status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
+      }
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    getList() {
+      this.loading = true
+      listFeishuConfig(this.queryParams).then(response => {
+        this.list = response.rows
+        this.total = response.total
+        this.loading = false
+      }).catch(() => {
+        this.loading = false
+      })
+    },
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    resetQuery() {
+      this.resetForm('queryForm')
+      this.handleQuery()
+    },
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id)
+      this.single = selection.length !== 1
+      this.multiple = !selection.length
+    },
+    reset() {
+      this.form = {
+        id: null,
+        name: null,
+        appId: null,
+        appSecret: null,
+        status: 1,
+        remark: null
+      }
+      this.resetForm('form')
+    },
+    handleAdd() {
+      this.reset()
+      this.open = true
+      this.title = '新增飞书配置'
+    },
+    handleUpdate(row) {
+      this.reset()
+      const id = row.id || this.ids[0]
+      getFeishuConfig(id).then(response => {
+        this.form = response.data
+        this.open = true
+        this.title = '修改飞书配置'
+      })
+    },
+    submitForm() {
+      this.$refs['form'].validate(valid => {
+        if (!valid) {
+          return
+        }
+        const req = this.form.id != null ? updateFeishuConfig(this.form) : addFeishuConfig(this.form)
+        req.then(response => {
+          if (response.code !== 200) {
+            this.msgError(response.msg)
+            return
+          }
+          this.msgSuccess(this.form.id != null ? '修改成功' : '新增成功')
+          this.open = false
+          this.getList()
+        })
+      })
+    },
+    cancel() {
+      this.open = false
+      this.reset()
+    },
+    handleDelete(row) {
+      const ids = row.id || this.ids
+      this.$confirm('是否确认删除飞书配置编号为"' + ids + '"的数据项?', '警告', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        return delFeishuConfig(ids)
+      }).then(() => {
+        this.getList()
+        this.msgSuccess('删除成功')
+      }).catch(() => {})
+    },
+    getStatusLabel(status) {
+      const map = { 1: '正常', 0: '停用', [-1]: '已封禁' }
+      return map[status] || '未知'
+    },
+    statusTagType(status) {
+      if (status === 1) return 'success'
+      if (status === 0) return 'warning'
+      if (status === -1) return 'danger'
+      return 'info'
+    },
+    copyPermissionExample() {
+      navigator.clipboard.writeText(this.permissionImportExample).then(() => {
+        this.msgSuccess('已复制到剪贴板')
+      }).catch(() => {
+        this.msgError('复制失败,请手动复制')
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.permission-example >>> textarea {
+  font-family: Consolas, Monaco, monospace;
+  background-color: #f5f7fa;
+  color: #606266;
+  cursor: default;
+}
+</style>

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

@@ -567,26 +567,33 @@
               <el-tag v-if="scope.row.isCompleted == 0" type="info">未完课</el-tag>
             </template>
           </el-table-column>
-          <el-table-column prop="rewardType" label="领取类型" min-width="160">
+          <el-table-column prop="rewardType" label="领取类型" min-width="220">
             <template slot-scope="scope">
-              <div v-if="scope.row.rewardType != null && scope.row.rewardType !== '' && scope.row.rewardType != 0 && scope.row.rewardType != 99">
+              <div v-if="hasClaimedReward(scope.row.rewardType)">
                 <dict-tag :options="liveRewardTypeList" :value="scope.row.rewardType"/>
                 <el-button
-                  v-if="scope.row.rewardType != 4 && scope.row.rewardType != 99"
+                  v-if="hasRedOrPointsReward(scope.row.rewardType)"
                   type="text"
                   size="small"
                   style="margin-left: 8px;"
                   @click="handleOpenRedPacketLog(scope.row)"
                 >红包/积分领取详细</el-button>
                 <el-button
-                  v-if="scope.row.rewardType == 4 || scope.row.rewardType == 5 || scope.row.rewardType == 6 || scope.row.rewardType == 7"
+                  v-if="hasCouponReward(scope.row.rewardType)"
                   type="text"
                   size="small"
                   style="margin-left: 8px;"
                   @click="handleOpenCouponLog(scope.row)"
                 >核销卷领取详情</el-button>
               </div>
-              <span v-else>未领取</span>
+              <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>
@@ -797,14 +804,33 @@ export default {
     this.liveId = this.$route.query.liveId;
     this.queryParams.liveId = this.liveId;
 
-    this.getDicts("sys_live_reward_type").then((response) => {
-      this.liveRewardTypeList = response.data;
-    });
+    this.getDicts('live_watch_reward_type').then((response) => {
+      this.liveRewardTypeList = response.data || []
+    })
 
     this.getList();
   },
   methods: {
     selectDictLabel,
+    /** live_watch_reward_type:0无 / 99未设置 视为未领取;97/98 特殊状态只展示文案 */
+    hasClaimedReward(type) {
+      if (type == null || type === '') return false
+      const t = Number(type)
+      if (Number.isNaN(t) || t === 0 || t === 99) return false
+      return true
+    },
+    /** 按位:1红包 2积分(排除 97+ 特殊码) */
+    hasRedOrPointsReward(type) {
+      const t = Number(type)
+      if (Number.isNaN(t) || t >= 97) return false
+      return (t & 1) !== 0 || (t & 2) !== 0
+    },
+    /** 按位:4核销券 */
+    hasCouponReward(type) {
+      const t = Number(type)
+      if (Number.isNaN(t) || t >= 97) return false
+      return (t & 4) !== 0
+    },
     /** 获取统计数据 */
     getStatistics() {
       if (!this.liveId) return;

+ 753 - 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,788 @@
           @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="公司" prop="companyId">
+        <el-select
+          v-model="queryParams.companyId"
+          placeholder="请选择公司"
+          filterable
+          clearable
+          size="small"
+          style="width: 200px"
+        >
+          <el-option
+            v-for="item in companyList"
+            :key="item.companyId"
+            :label="item.companyName"
+            :value="item.companyId"
+          />
+        </el-select>
+      </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 { getCompanyList } from '@/api/company/company'
+import { listLiveRedPacketLog } from '@/api/live/liveRedPacketLog'
+import { listCourseCouponUserListVO } from '@/api/his/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: [],
+      companyList: [],
+      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
       }
     }
   },
   created() {
-
+    this.getDicts('live_watch_reward_type').then(res => {
+      this.liveRewardTypeList = res.data || []
+    })
+    this.loadCompanyList()
+    this.getList()
   },
   methods: {
+    // 多选框选中数据(按会员 userId 去重)
+    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: []
+      }
+    },
+    /** 筛选条件绑标签:映射到 FsUserParam 可用字段(直播专属条件后端不会按 live 过滤) */
+    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))
+          })
+        })
+      })
+    },
+    /** live_watch_reward_type:0无 / 99未设置 视为未领取;97/98 特殊状态只展示文案 */
+    hasClaimedReward(type) {
+      if (type == null || type === '') return false
+      const t = Number(type)
+      if (Number.isNaN(t) || t === 0 || t === 99) return false
+      return true
+    },
+    /** 按位:1红包 2积分(排除 97+ 特殊码) */
+    hasRedOrPointsReward(type) {
+      const t = Number(type)
+      if (Number.isNaN(t) || t >= 97) return false
+      return (t & 1) !== 0 || (t & 2) !== 0
+    },
+    /** 按位:4核销券 */
+    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
+      listCourseCouponUserListVO({
+        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
+      })
+    },
+    loadCompanyList() {
+      getCompanyList().then(res => {
+        this.companyList = res.data || []
+      })
+    },
+    buildQuery() {
+      const params = { ...this.queryParams }
+      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>

+ 0 - 2
src/views/system/user/index.vue

@@ -219,8 +219,6 @@
                     v-hasPermi="['system:user:resetPwd']">重置密码</el-dropdown-item>
                   <el-dropdown-item command="handleAuthRole" icon="el-icon-circle-check"
                     v-hasPermi="['system:user:edit']">分配角色</el-dropdown-item>
-                    <el-dropdown-item command="handleSetUser" icon="el-icon-warning-outline"
-                    v-hasPermi="['system:user:edit']">用户设置</el-dropdown-item>
                   <el-dropdown-item command="unBind" icon="el-icon-key"
                                     v-hasPermi="['system:user:unBind']">换绑微信</el-dropdown-item>
                   <el-dropdown-item command="adminBindSipCall" icon="el-icon-key"