Pārlūkot izejas kodu

用户列表新增批量操作功能

wangzhiqiang 2 mēneši atpakaļ
vecāks
revīzija
445bf26161

+ 2 - 2
.env.development

@@ -1,11 +1,11 @@
 ENV = 'development'
 
 # 线上测试
-# VITE_BASE_URL = 'http://advise.ytmdm.com/yt-gateway'
+VITE_BASE_URL = 'http://advise.ytmdm.com/gateway'
 # VITE_BASE_URL = 'https://test.book.ytpm.net/yt-gateway'
 # VITE_BASE_URL = 'http://119.45.71.139:25001'
 
 # 本地 
-VITE_BASE_URL = 'http://192.168.1.9:25001'
+# VITE_BASE_URL = 'http://192.168.1.9:25001'
 # VITE_BASE_URL = 'http://192.168.1.159:25001'
 

+ 96 - 1
package-lock.json

@@ -19,7 +19,8 @@
         "qrcode": "^1.5.4",
         "vue": "^3.2.31",
         "vue-router": "4",
-        "vuex": "^4.0.2"
+        "vuex": "^4.0.2",
+        "xlsx": "^0.18.5"
       },
       "devDependencies": {
         "@types/node": "^17.0.21",
@@ -775,6 +776,14 @@
         "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
       }
     },
+    "node_modules/adler-32": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmmirror.com/adler-32/-/adler-32-1.3.1.tgz",
+      "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/ajv": {
       "version": "6.12.6",
       "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz",
@@ -967,6 +976,18 @@
         "node": ">=6"
       }
     },
+    "node_modules/cfb": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmmirror.com/cfb/-/cfb-1.2.2.tgz",
+      "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
+      "dependencies": {
+        "adler-32": "~1.3.0",
+        "crc-32": "~1.2.0"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/chalk": {
       "version": "4.1.2",
       "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz",
@@ -1038,6 +1059,14 @@
         "wrap-ansi": "^6.2.0"
       }
     },
+    "node_modules/codepage": {
+      "version": "1.15.0",
+      "resolved": "https://registry.npmmirror.com/codepage/-/codepage-1.15.0.tgz",
+      "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/color-convert": {
       "version": "2.0.1",
       "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
@@ -1121,6 +1150,17 @@
         "node": ">= 0.10"
       }
     },
+    "node_modules/crc-32": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmmirror.com/crc-32/-/crc-32-1.2.2.tgz",
+      "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
+      "bin": {
+        "crc32": "bin/crc32.njs"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/cross-spawn": {
       "version": "7.0.6",
       "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -2146,6 +2186,14 @@
         }
       }
     },
+    "node_modules/frac": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/frac/-/frac-1.1.2.tgz",
+      "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/fs.realpath": {
       "version": "1.0.0",
       "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -3387,6 +3435,17 @@
       "deprecated": "Please use @jridgewell/sourcemap-codec instead",
       "dev": true
     },
+    "node_modules/ssf": {
+      "version": "0.11.2",
+      "resolved": "https://registry.npmmirror.com/ssf/-/ssf-0.11.2.tgz",
+      "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
+      "dependencies": {
+        "frac": "~1.1.2"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/statuses": {
       "version": "1.5.0",
       "resolved": "https://registry.npmmirror.com/statuses/-/statuses-1.5.0.tgz",
@@ -4056,6 +4115,22 @@
         "node": ">= 10.0.0"
       }
     },
+    "node_modules/wmf": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/wmf/-/wmf-1.0.2.tgz",
+      "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/word": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmmirror.com/word/-/word-0.3.0.tgz",
+      "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/word-wrap": {
       "version": "1.2.5",
       "resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz",
@@ -4084,6 +4159,26 @@
       "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
       "dev": true
     },
+    "node_modules/xlsx": {
+      "version": "0.18.5",
+      "resolved": "https://registry.npmmirror.com/xlsx/-/xlsx-0.18.5.tgz",
+      "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
+      "dependencies": {
+        "adler-32": "~1.3.0",
+        "cfb": "~1.2.1",
+        "codepage": "~1.15.0",
+        "crc-32": "~1.2.1",
+        "ssf": "~0.11.2",
+        "wmf": "~1.0.1",
+        "word": "~0.3.0"
+      },
+      "bin": {
+        "xlsx": "bin/xlsx.njs"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/y18n": {
       "version": "4.0.3",
       "resolved": "https://registry.npmmirror.com/y18n/-/y18n-4.0.3.tgz",

+ 2 - 1
package.json

@@ -20,7 +20,8 @@
     "qrcode": "^1.5.4",
     "vue": "^3.2.31",
     "vue-router": "4",
-    "vuex": "^4.0.2"
+    "vuex": "^4.0.2",
+    "xlsx": "^0.18.5"
   },
   "devDependencies": {
     "@types/node": "^17.0.21",

+ 20 - 1
src/api/userModule.js

@@ -71,4 +71,23 @@ export function batchAudit(data) {
         method: 'post',
         data
     })
-}
+}
+
+// 批量封禁用户
+export function batchBanned(data) {
+    return request({
+        url: '/agent-service/risk/batchBanned',
+        method: 'post',
+        data
+    })
+}
+
+// 批量解封用户
+export function batchDeblock(data) {
+    return request({
+        url: '/agent-service/risk/batchDeblock',
+        method: 'post',
+        data
+    })
+}
+

+ 1 - 1
src/components/table/index.vue

@@ -36,7 +36,7 @@ export default defineComponent({
       }
     },
     pageLayout: { type: String, default: "total, sizes, prev, pager, next, jumper" }, // 分页需要显示的东西,默认全部
-    pageSizes: { type: Array, default: [10, 20, 50, 100] },
+    pageSizes: { type: Array, default: [10, 20, 50, 100, 200] },
     id: { type: String, default: '' }, // 行数据的 Key,用来优化 Table 的渲染;
     height: { type: String, default: '100%' },
     revenue: { type: Number, default: 0 }, //总收益

+ 3 - 3
src/layout/Tabs/index.vue

@@ -64,7 +64,7 @@ export default defineComponent({
     const scrollLeft = ref(0)
     const defaultMenu = {
       path: '/dashboard',
-      meta: { title: '首页', hideClose: true }
+      meta: { title: '首页', hideClose: false }
     }
     const contentFullScreen = computed(() => store.state.app.contentFullScreen)
     const currentDisabled = computed(() => route.path === defaultMenu.path)
@@ -98,11 +98,11 @@ export default defineComponent({
 
     // 关闭当前标签,首页不关闭
     function closeCurrentRoute() {
-      if (route.path !== defaultMenu.path) {
+      // if (route.path !== defaultMenu.path) {
         const tab = document.getElementById('vueAdminBoxTabCloseSelf')
         const nextPath = tab?.getAttribute('nextPath')
         delMenu(route, nextPath)
-      }
+      // }
     }
     // 关闭除了当前标签之外的所有标签
     function closeOtherRoute() {

+ 2 - 1
src/router/modules/dashboard.js

@@ -10,7 +10,8 @@ const route = [
       {
         path: 'dashboard',
         component: createNameComponent(() => import('@/views/main/dashboard/index.vue')),
-        meta: { title: '首页', icon: 'iconfont icon-home', hideClose: true }
+        // meta: { title: '首页', icon: 'iconfont icon-home', hideClose: true }
+        meta: { title: '首页', icon: 'iconfont icon-home' }
       }
     ]
   }

+ 84 - 0
src/utils/exportExcel.js

@@ -0,0 +1,84 @@
+import * as XLSX from 'xlsx';
+
+/**
+ * 导出Excel
+ * @param {*} data 表格数据-Array
+ * @param {*} tableHeader 表头数据-Object
+ * @param {*} columnWidth 列宽-Number
+ * @param {*} tableName 表格名称-String
+ * @param {*} addTime 表格名称加上导出时间-Boolean 格式:yyyy-mm-dd_hh-mm-ss
+ */
+export function exportToExcel(data, tableHeader, columnWidth = 15, tableName, addTime = false) {
+    return new Promise((resolve, reject) => {
+        try {
+            // 创建一个工作簿
+            const wb = XLSX.utils.book_new();
+            // 将表格数据转换为工作表
+            data = [...data]
+            let colWidth = [] //列宽
+
+            // 使用 map 方法遍历 data 数组
+            let translatedData = data.map(item => {
+                let newItem = {};
+                // 遍历 tableHeader 进行键名替换
+                Object.keys(tableHeader).forEach(key => {
+                    if (item[key] !== undefined) {
+                        newItem[tableHeader[key]] = item[key]; // 赋值为 tableHeader 映射的中文名
+                        colWidth.push({ wch: columnWidth }) // 设置列宽
+                    }
+                });
+
+                return newItem;
+            });
+
+            const ws = XLSX.utils.json_to_sheet(translatedData)
+
+            // 设置列宽
+            ws['!cols'] = colWidth;
+
+            // 设置对齐方式
+            const cellAlignment = {
+                alignment: { horizontal: "center" }
+            };
+
+            // 遍历工作表的每个单元格并应用样式
+            for (let row = 0; row < translatedData.length; row++) {
+                for (let col = 0; col < Object.keys(translatedData[row]).length; col++) {
+                    const cellAddress = XLSX.utils.encode_cell({ c: col, r: row });
+                    if (!ws[cellAddress]) {
+                        ws[cellAddress] = {};
+                    }
+                    ws[cellAddress].s = cellAlignment; // 应用样式
+                }
+            }
+
+            // 将工作表添加到工作簿
+            XLSX.utils.book_append_sheet(wb, ws, 'Sheet1');
+
+            // 导出 Excel 文件
+            let name = addTime ? `${getCurrentDateTime()}_${tableName}.xlsx` : `${tableName}.xlsx`;
+            XLSX.writeFile(wb, name);
+
+            resolve(true);
+        } catch (error) {
+            console.error("Excel 导出失败:", error);
+            reject(false);
+        }
+    });
+}
+
+/**
+ * 导出时间
+ * @returns 时间格式:yyyy-mm-dd_hh-mm-ss
+ */
+function getCurrentDateTime() {
+    const now = new Date();
+    const year = now.getFullYear();
+    const month = String(now.getMonth() + 1).padStart(2, '0'); // 月份从 0 开始,需要 +1
+    const day = String(now.getDate()).padStart(2, '0');
+    const hours = String(now.getHours()).padStart(2, '0');
+    const minutes = String(now.getMinutes()).padStart(2, '0');
+    const seconds = String(now.getSeconds()).padStart(2, '0');
+
+    return `${year}-${month}-${day}_${hours}-${minutes}-${seconds}`;
+}

+ 2 - 2
src/views/main/userModule/riskLogsList.vue

@@ -16,9 +16,9 @@
             {{ getDictionaryName('user_status',scope.row.userStatus) }}
           </template>
         </el-table-column>
-        <el-table-column prop="bannedLimit" label="风控期限" width="90">
+        <el-table-column prop="bannedLimit" label="风控期限(天)" width="130">
           <template #default="scope">
-            {{ scope.row.bannedLimit }}
+            {{ (scope.row.bannedLimit / 24) }}
           </template>
         </el-table-column>
         <el-table-column prop="bannedReason" label="封禁原因" width="260" />

+ 693 - 521
src/views/main/userModule/userList.vue

@@ -3,16 +3,22 @@
     <!-- 菜单栏 -->
     <From :form-items="dynamicFormItems" @formSubmitted="handleFormSubmitted" @formReset="handleFormReset" />
 
-    <!-- 其他按钮(隐藏) -->
-    <div v-if="false" class="btn">
-      <el-button type="" v-if="!formSearch.appIds && selectData.length > 0"  @click="clearSelection">取消全选</el-button>
-      <el-button type="primary" :disabled="!selectData.length > 0" @click="userCheck">批量审核</el-button>
+    <div class="btn">
+      <el-button type="" v-if="formSearch.appIds && selectData.length > 0" @click="clearSelection">取消全选</el-button>
+      <!-- <el-button type="primary" :disabled="!selectData.length > 0" @click="userCheck">批量审核</el-button> -->
+      <el-button type="danger" :disabled="!selectData.length > 0" @click="edit({}, true)">批量封禁</el-button>
+      <el-button type="primary" :disabled="!selectData.length > 0" @click="editUserType({}, true)">批量解封</el-button>
+      <el-button type="success" :disabled="!selectData.length > 0" @click="exportUserList">批量导出</el-button>
+      <el-tooltip class="box-item" effect="dark" content="慎用 比较占用服务器资源!!! 导出数量(根据左下角条数)" placement="bottom">
+        <el-button type="warning" :disabled="!page.total > 0" @click="allExportUserList">全部导出</el-button>
+      </el-tooltip>
     </div>
 
     <!-- 表格 -->
     <div class="layout-container">
-      <Table @getTableData="changeTableData" v-model:page="page" ref="table" :data="tableData" :showSelection="false"
-        @selection-change="handleSelectionChange" @select-all="handleSelectAll" @select="handleSelect" :revenue="totalRevenue">
+      <Table @getTableData="changeTableData" v-model:page="page" ref="table" :data="tableData" :showSelection="true"
+        @selection-change="handleSelectionChange" @select-all="handleSelectAll" @select="handleSelect"
+        :revenue="totalRevenue">
         <el-table-column prop="userId" label="查看ECPM" width="130" fixed="left">
           <template #default="scope">
             <el-button type="primary" @click="lookEcpm(scope.row)">查看ECPM</el-button>
@@ -22,27 +28,32 @@
         <el-table-column prop="nickName" label="用户昵称" fixed="left" width="100" />
         <el-table-column prop="userStatus" label="用户状态" width="90">
           <template #default="scope">
-            {{ getDictionaryName('user_status',Number(scope.row.userStatus)) }}
+            {{ getDictionaryName('user_status', Number(scope.row.userStatus)) }}
           </template>
         </el-table-column>
-<!--        <el-table-column prop="userType" label="用户类型" width="90" />-->
-        <el-table-column prop="appId" label="应用ID" width="150" />
-        <el-table-column prop="appName" label="应用名称" width="150" />
-        <el-table-column prop="appType" label="应用类型" width="90">
+        <!-- <el-table-column prop="userType" label="用户类型" width="90" />-->
+        <!-- <el-table-column prop="appId" label="应用ID" width="150" /> -->
+        <!-- <el-table-column prop="appName" label="应用名称" width="150" /> -->
+        <!-- <el-table-column prop="appType" label="应用类型" width="90">
           <template #default="scope">
             {{ getDictionaryName('app_type',scope.row.appType) }}
           </template>
-        </el-table-column>
+        </el-table-column> -->
         <el-table-column prop="ditchName" label="渠道来源" width="150" />
         <el-table-column prop="todayVideo" label="今日视频播放数" width="130" />
         <el-table-column prop="totalVideo" label="视频播放总数" width="110" />
         <el-table-column prop="totalIncome" label="用户贡献(当日/总共)" sortable width="200">
           <template #default="scope">
-            {{ roundPrice(scope.row.todayIncome === 0 ? '0.00' : scope.row.todayIncome ?? '0.00')}} /
+            {{ roundPrice(scope.row.todayIncome === 0 ? '0.00' : scope.row.todayIncome ?? '0.00') }} /
             {{ roundPrice(scope.row.totalIncome === 0 ? '0.00' : scope.row.totalIncome ?? '0.00') }}
           </template>
         </el-table-column>
         <el-table-column prop="communicationOperator" label="通信运营商" width="130" />
+        <el-table-column prop="loginRecordList" label="用户设备" width="200">
+          <template #default="scope">
+            {{ scope.row.loginRecordList[0].deviceBrand }} {{ scope.row.loginRecordList[0].deviceModel }}
+          </template>
+        </el-table-column>
         <el-table-column prop="deviceRepeatCount" label="设备重复数量" width="110" />
         <el-table-column prop="ipRepeatCount" label="IP重复数量" width="100" />
         <el-table-column prop="lastLoginIp" label="最新登录IP" width="150" />
@@ -52,25 +63,25 @@
           </template>
         </el-table-column>
         <el-table-column prop="loginDays" label="登录天数" width="90" />
-        <el-table-column prop="pointsBalance" label="积分余额" width="90" />
+        <!-- <el-table-column prop="pointsBalance" label="积分余额" width="90" />
         <el-table-column prop="pointsTotal" label="积分总额" width="90" />
         <el-table-column prop="redPacketAmount" label="红包总额" width="90" />
-        <el-table-column prop="redPacketBalance" label="红包余额" width="90" />
+        <el-table-column prop="redPacketBalance" label="红包余额" width="90" /> -->
         <el-table-column prop="registryTime" label="注册时间" width="160">
           <template #default="scope">
             {{ convertUTCToBeijing(scope.row.registryTime) }}
           </template>
         </el-table-column>
-        <el-table-column prop="versionCode" label="版本号" />
-        <el-table-column prop="withdrawCount" label="提现笔数" width="90" />
-        <el-table-column prop="withdrawTotal" label="提现总额" width="90" />
-        <el-table-column label="操作" width="220" v-permission="'permission'">
+        <!-- <el-table-column prop="versionCode" label="版本号" /> -->
+        <!-- <el-table-column prop="withdrawCount" label="提现笔数" width="90" /> -->
+        <!-- <el-table-column prop="withdrawTotal" label="提现总额" width="90" /> -->
+        <el-table-column v-if="false" label="操作" width="220" v-permission="'permission'" fixed="right">
           <template #default="scope">
             <div class="button">
               <el-link class="button-item" type="primary" @click="editUserType(scope.row)">
                 风控解除
               </el-link>
-              <el-link  v-if="scope.row.userStatus < 3" class="button-item" type="danger" @click="edit(scope.row)">
+              <el-link v-if="scope.row.userStatus < 3" class="button-item" type="danger" @click="edit(scope.row)">
                 封禁用户
               </el-link>
               <el-popconfirm title="确认锁定该用户?" @confirm="lockUser(scope.row)">
@@ -86,9 +97,10 @@
 
     <!-- 操作弹窗 -->
     <Layer :layer="layer" @confirm="submit(ruleForm)" @close="layer.show = false">
-      <el-form :model="formEdit" :rules="rules" ref="ruleForm" label-width="120px" style="margin-right:30px;">
+      <el-form :model="formEdit" :rules="rules" ref="ruleForm" label-width="140px" style="margin-right:30px;">
         <el-form-item label="封禁时间(天):" required prop="bannedLimit">
-          <el-input v-model="formEdit.bannedLimit" type="number" placeholder="请输入封禁期限" clearable />
+          <!-- <el-input v-model="formEdit.bannedLimit" type="number" placeholder="请输入封禁期限" clearable /> -->
+          <el-input-number step-strictly v-model="formEdit.bannedLimit" :step="1" :min="0" :precision="0" />
         </el-form-item>
         <el-form-item label="封禁原因:" required prop="bannedReason">
           <el-input v-model="formEdit.bannedReason" placeholder="请输入封禁原因" clearable />
@@ -98,7 +110,7 @@
 
     <Layer :layer="layer1" @confirm="submit1(ruleForm1)" @close="layer.show = false">
       <el-form :model="formEdit1" :rules="rules1" ref="ruleForm1" label-width="120px" style="margin-right:30px;">
-        <el-form-item label="用户状态:" required prop="userStatus">
+        <el-form-item v-if="!layer1.isMulty" label="用户状态:" required prop="userStatus">
           <el-select v-model="formEdit1.userStatus" placeholder="请选择状态" filterable>
             <el-option v-for="option in getOptions('user_status')" :key="option.label" :label="option.label"
               :value="option.value">
@@ -112,52 +124,52 @@
     </Layer>
 
     <Layer :layer="ecpmLayer" @confirm="ecpmLayer.show = false" @close="ecpmLayer.show = false">
-        <From :form-items="dynamicFormItems1" @formSubmitted="handleFormSubmitted1" @formReset="handleFormReset1" />
-        <el-divider></el-divider>
-        <Table :showPage="false" ref="ecpmTable" :data="ecpmData" :height="ecpmLayer.height">
-          <el-table-column prop="userId" label="用户ID" width="120" />
-          <el-table-column prop="adSourceId" label="广告源ID" width="100" />
-          <el-table-column prop="beginTime" label="开始时间" width="100" />
-          <el-table-column prop="finishTime" label="完成时间" width="100" />
-          <el-table-column prop="ecpm" label="ECPM" width="100" >
-            <template #default="scope">
-              {{ roundPrice(scope.row.ecpm) || 0.00 }}
-            </template>
-          </el-table-column>
-          <el-table-column prop="adSourceType" label="广告源类型" width="160">
-            <template #default="scope">
-              {{ getDictionaryName('ad_source_type',scope.row.adSourceType) }}
-            </template>
-          </el-table-column>
-          <el-table-column prop="adSourceIndex" label="竞价匹配优先级" width="130" />
-          <el-table-column prop="networkFormId" label="广告平台ID" width="100" />
-          <el-table-column prop="networkName" label="广告平台名称" width="120" />
-          <el-table-column prop="networkPlacementId" label="广告平台广告位ID" width="160" />
-          <el-table-column prop="nickName" label="用户昵称" min-width="100" />
-          <el-table-column prop="placementId" label="广告位ID" width="100" />
-          <el-table-column prop="recordId" label="广告记录ID" width="160" />
-          <el-table-column prop="revenue" label="收益" width="100">
-            <template #default="scope">
-              {{ roundPrice(scope.row.revenue) || 0.00 }}
-            </template>
-          </el-table-column>
-        </Table>
-        <template #bottom>
-          <div style="position: absolute;bottom: 0px;right: 10px;">
-            共:{{ ecpmData.length }}
-          </div>
-        </template>
+      <From :form-items="dynamicFormItems1" @formSubmitted="handleFormSubmitted1" @formReset="handleFormReset1" />
+      <el-divider></el-divider>
+      <Table :showPage="false" ref="ecpmTable" :data="ecpmData" :height="ecpmLayer.height">
+        <el-table-column prop="userId" label="用户ID" width="120" />
+        <el-table-column prop="adSourceId" label="广告源ID" width="100" />
+        <el-table-column prop="beginTime" label="开始时间" width="100" />
+        <el-table-column prop="finishTime" label="完成时间" width="100" />
+        <el-table-column prop="ecpm" label="ECPM" width="100">
+          <template #default="scope">
+            {{ roundPrice(scope.row.ecpm) || 0.00 }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="adSourceType" label="广告源类型" width="160">
+          <template #default="scope">
+            {{ getDictionaryName('ad_source_type', scope.row.adSourceType) }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="adSourceIndex" label="竞价匹配优先级" width="130" />
+        <el-table-column prop="networkFormId" label="广告平台ID" width="100" />
+        <el-table-column prop="networkName" label="广告平台名称" width="120" />
+        <el-table-column prop="networkPlacementId" label="广告平台广告位ID" width="160" />
+        <el-table-column prop="nickName" label="用户昵称" min-width="100" />
+        <el-table-column prop="placementId" label="广告位ID" width="100" />
+        <el-table-column prop="recordId" label="广告记录ID" width="160" />
+        <el-table-column prop="revenue" label="收益" width="100">
+          <template #default="scope">
+            {{ roundPrice(scope.row.revenue) || 0.00 }}
+          </template>
+        </el-table-column>
+      </Table>
+      <template #bottom>
+        <div style="position: absolute;bottom: 0px;right: 10px;">
+          共:{{ ecpmData.length }}
+        </div>
+      </template>
     </Layer>
 
     <!-- 审核备注 -->
     <Layer :layer="layer2" @confirm="submit2(ruleForm2)" @close="layer2.show = false">
-      <el-form :model="formEdit2" :rules="rules2" ref="ruleForm2" label-width="120px" style="margin-right:30px;">
+      <el-form :model="formEdit2" :rules="rules2" ref="ruleForm2" label-width="140px" style="margin-right:30px;">
         <el-form-item label="封禁时间(天)" required prop="bannedLimit">
           <el-input-number v-model="formEdit2.bannedLimit" :min="1" :step="1" step-strictly style="width: 100%;" />
         </el-form-item>
         <el-form-item label="生效时间(小时)" required prop="effectTime">
           <el-input-number v-model="formEdit2.effectTime" :min="0" :step="1" step-strictly style="width: 100%;" />
-          <el-alert title="单位为小时,0表示立即执行" type="error" center :closable="false" style="margin-top: 5px;height: 30px;"  />
+          <el-alert title="单位为小时,0表示立即执行" type="error" center :closable="false" style="margin-top: 5px;height: 30px;" />
         </el-form-item>
         <el-form-item label="审核备注" required prop="remark">
           <el-input v-model="formEdit2.remark" type="textarea" rows="4" placeholder="请输入审核备注内容" clearable />
@@ -168,33 +180,215 @@
 </template>
 
 <script setup>
-  import { onBeforeMount, ref, reactive, nextTick } from "vue";
-  import From from "@/components/from/index.vue";
-  import Table from "@/components/table/index.vue";
-  import Layer from '@/components/layer/index.vue'
-  import { ElMessage, ElLoading } from 'element-plus'
-  import { getUserList, riskBannedUser, riskLockUser, appUserEcpm, getRevenueByTime, batchAudit } from '@/api/userModule.js'
-  import { ditchList } from '@/api/outBagModule.js'
-  import { appList } from "@/api/formworkErection.js";
-  import { riskChangeUserStatus } from '@/api/riskModule.js'
-  import { convertUTCToBeijing, roundPrice, getTodayRangeLocal } from '@/utils/index.js'
-  import { useGetDictList } from '@/hooks/useGetDictList.js'
-  import { useStore } from 'vuex'
-
-  const store = useStore()
-  const { loadDictData, getOptions, getDictionaryName } = useGetDictList();
-  const tableData = ref([]);
-  const table = ref(null)
-
-  // 分页参数, 供table使用
-  const page = reactive({
-    pageNum: 1,
-    pageSize: 20,
-    total: 0,
+import { onBeforeMount, ref, reactive, nextTick } from "vue";
+import From from "@/components/from/index.vue";
+import Table from "@/components/table/index.vue";
+import Layer from '@/components/layer/index.vue'
+import { ElMessage, ElLoading } from 'element-plus'
+import {
+  getUserList, riskBannedUser, riskLockUser, appUserEcpm, getRevenueByTime,
+  batchAudit, batchBanned, batchDeblock
+} from '@/api/userModule.js'
+import { ditchList } from '@/api/outBagModule.js'
+import { appList } from "@/api/formworkErection.js";
+import { riskChangeUserStatus } from '@/api/riskModule.js'
+import { convertUTCToBeijing, roundPrice, getTodayRangeLocal } from '@/utils/index.js'
+import { useGetDictList } from '@/hooks/useGetDictList.js'
+import { useStore } from 'vuex'
+import { exportToExcel } from '@/utils/exportExcel.js'
+
+const store = useStore()
+const { loadDictData, getOptions, getDictionaryName } = useGetDictList();
+const tableData = ref([]);
+const table = ref(null)
+
+// 分页参数, 供table使用
+const page = reactive({
+  pageNum: 1,
+  pageSize: 20,
+  total: 0,
+});
+
+const formSearch = ref({
+  // lastLoginTime: undefined,// 最新登录时间
+  nickName: undefined,// 用户昵称
+  userId: undefined,// 用户ID
+  ditchId: undefined,// 渠道来源
+  userStatus: 1,// 用户状态
+  appIds: undefined, //所属应用
+  registryTimeBegin: getTodayRangeLocal(),// 注册时间
+  registryTimeEnd: undefined,// 注册时间
+  page: 1,// 当前页码
+  limit: 20,// 当前页数量(查询量)
+});
+
+const dynamicFormItems = ref([])
+const dynamicFormItems1 = ref([])
+
+onBeforeMount(() => {
+  settingData()
+  // getList();
+});
+
+// 获取缓存数据设置筛选数据
+const settingData = async () => {
+  loadDictData().then(() => {
+    dynamicFormItems.value = [
+      {
+        label: '用户昵称',
+        prop: 'nickName',
+        type: 'input',
+        needEnterEvent: true
+      },
+      {
+        label: '用户ID',
+        prop: 'userId',
+        type: 'input',
+        needEnterEvent: true
+      },
+      {
+        label: '所属应用',
+        prop: 'appIds',
+        type: 'select',
+        clearable: false,
+        options: [],
+      },
+      {
+        label: '渠道来源',
+        prop: 'ditchId',
+        type: 'select',
+        options: getOptions('channel_origin'),
+      },
+      {
+        label: '用户状态',
+        prop: 'userStatus',
+        type: 'select',
+        defaultVal: 1,
+        options: getOptions('user_status'),
+      },
+      { label: '注册时间开始', prop: 'registryTimeBegin', type: 'date', defaultVal: getTodayRangeLocal() },
+      { label: '注册时间结束', prop: 'registryTimeEnd', type: 'date' },
+    ]
+
+    // 查看ECPM
+    dynamicFormItems1.value = [
+      {
+        label: '广告源类型',
+        prop: 'adsourceType',
+        type: 'select',
+        labelWidth: 100,
+        options: getOptions('ad_source_type'),
+      },
+    ]
+    // 设置动态选项
+    getApiOptions()
+  })
+}
+
+
+const firstApp = ref('')
+//渠道来源
+const getApiOptions = async () => {
+  try {
+    // 并发获取数据
+    const [{ data: ditchData }, { data: appData }] = await Promise.all([
+      ditchList({ page: 1, limit: 9999 }),
+      appList({ page: 1, limit: 9999 })
+    ])
+
+    const ditchOptions = ditchData.map(item => ({
+      label: item.ditchName,
+      value: item.ditchId
+    }))
+
+    const appsOptions = appData.map(item => ({
+      label: item.appName,
+      value: item.appId
+    }))
+
+    // 赋值到表单项
+    dynamicFormItems.value[3].options = ditchOptions
+    dynamicFormItems.value[2].options = appsOptions
+
+    // 如果有应用列表,默认选第一个
+    if (appsOptions.length > 0) {
+      // const firstApp = appsOptions[0].value
+      firstApp.value = appsOptions[0].value
+      dynamicFormItems.value[2].defaultVal = firstApp.value
+      formSearch.value.appIds = firstApp.value
+    }
+
+    // 获取列表数据
+    getList(0)
+  } catch (err) {
+    console.error('获取选项失败:', err)
+  }
+}
+
+const totalRevenue = ref(0)
+
+// 分页数据
+const getList = async (timeout = 5000) => {
+  openFullScreen('数据请求中,请勿重复点击!!!')
+  return new Promise((resolve) => {
+    setTimeout(async () => {
+      try {
+        /* //处理根据时间获取收益参数
+        const param = {...formSearch.value}
+        delete param.page
+        delete param.limit
+
+        //并发获取数据
+        const [listRes, revenueRes] = await Promise.all([
+          getUserList({ ...formSearch.value }),
+          // getRevenueByTime({ ...param })
+        ]) */
+
+        const listRes = await getUserList({ ...formSearch.value })
+        // 列表当日总收益
+        const totalEarnings = listRes.data.reduce((acc, cur) => acc + (cur.todayIncome || 0), 0)
+
+        tableData.value = listRes.data;
+        page.total = listRes.pageMeta.total;
+        totalRevenue.value = roundPrice(totalEarnings, 2, true)
+
+        resolve(true);
+      } catch (e) {
+        console.log('数据请求超时或报错', e)
+        resolve(false);
+      } finally {
+        closeFullScreen()
+      }
+    }, timeout)
   });
+};
+
+const changeTableData = (type) => {
+  formSearch.value.page = type ? 1 : page.pageNum;
+  formSearch.value.limit = page.pageSize;
+  // 分页切换
+  getList(0);
+};
 
-  const formSearch = ref({
-    // lastLoginTime: undefined,// 最新登录时间
+// 搜索
+const handleFormSubmitted = (formData) => {
+  // console.log("接收到子组件传递的数据", formData);
+  formSearch.value.page = page.pageNum;
+  formSearch.value.limit = page.pageSize;
+  formSearch.value.nickName = formData.nickName;
+  formSearch.value.userId = formData.userId;
+  formSearch.value.ditchId = formData.ditchId;
+  formSearch.value.userStatus = formData.userStatus;
+  formSearch.value.appIds = formData.appIds;
+  formSearch.value.registryTimeBegin = convertUTCToBeijing(formData.registryTimeBegin, false) || undefined
+  formSearch.value.registryTimeEnd = convertUTCToBeijing(formData.registryTimeEnd, false) || undefined
+
+  getList(0);
+};
+
+// 表单重置
+const handleFormReset = () => {
+  formSearch.value = {
     nickName: undefined,// 用户昵称
     userId: undefined,// 用户ID
     ditchId: undefined,// 渠道来源
@@ -204,274 +398,113 @@
     registryTimeEnd: undefined,// 注册时间
     page: 1,// 当前页码
     limit: 20,// 当前页数量(查询量)
-  });
-
-  const dynamicFormItems = ref([])
-  const dynamicFormItems1 = ref([])
-
-  onBeforeMount(() => {
-     settingData()
-    // getList();
-  });
-
-  // 获取缓存数据设置筛选数据
-  const settingData = async() => {
-    loadDictData().then(() => {
-      dynamicFormItems.value = [
-        {
-          label: '用户昵称',
-          prop: 'nickName',
-          type: 'input',
-          needEnterEvent: true
-        },
-        {
-          label: '用户ID',
-          prop: 'userId',
-          type: 'input',
-          needEnterEvent: true
-        },
-        {
-          label: '所属应用',
-          prop: 'appIds',
-          type: 'select',
-          clearable: false,
-          options: [],
-        },
-        {
-          label: '渠道来源',
-          prop: 'ditchId',
-          type: 'select',
-          options: getOptions('channel_origin'),
-        },
-        {
-          label: '用户状态',
-          prop: 'userStatus',
-          type: 'select',
-          defaultVal: 1,
-          options: getOptions('user_status'),
-        },
-        { label: '注册时间开始', prop: 'registryTimeBegin', type: 'date',defaultVal: getTodayRangeLocal() },
-        { label: '注册时间结束', prop: 'registryTimeEnd', type: 'date' },
-      ]
-
-      // 查看ECPM
-      dynamicFormItems1.value = [
-        {
-          label: '广告源类型',
-          prop: 'adsourceType',
-          type: 'select',
-          labelWidth: 100,
-          options: getOptions('ad_source_type'),
-        }, 
-      ]
-      // 设置动态选项
-      getApiOptions()
-    })
-  }
-
-
-  const firstApp = ref('')
-  //渠道来源
-  const getApiOptions = async() => {
-    try {
-      // 并发获取数据
-      const [{ data: ditchData }, { data: appData }] = await Promise.all([
-        ditchList({ page: 1, limit: 9999 }),
-        appList({ page: 1, limit: 9999 })
-      ])
-
-      const ditchOptions = ditchData.map(item => ({
-        label: item.ditchName,
-        value: item.ditchId
-      }))
-
-      const appsOptions = appData.map(item => ({
-        label: item.appName,
-        value: item.appId
-      }))
-
-      // 赋值到表单项
-      dynamicFormItems.value[3].options = ditchOptions
-      dynamicFormItems.value[2].options = appsOptions
-
-      // 如果有应用列表,默认选第一个
-      if (appsOptions.length > 0) {
-        // const firstApp = appsOptions[0].value
-        firstApp.value = appsOptions[0].value
-        dynamicFormItems.value[2].defaultVal = firstApp.value
-        formSearch.value.appIds = firstApp.value
-      }
-
-      // 获取列表数据
-      getList()
-    } catch (err) {
-      console.error('获取选项失败:', err)
-    }
-  }
-
-  const totalRevenue = ref(0)
-
-  // 分页数据
-  const getList = async () => {
-    try{
-      openFullScreen('数据请求中,请勿重复点击!!!')
-
-      // 处理根据时间获取收益参数
-      const param = {...formSearch.value}
-      delete param.page
-      delete param.limit
-
-      // 并发获取数据
-      const [listRes, revenueRes] = await Promise.all([
-        getUserList({ ...formSearch.value }),
-        // getRevenueByTime({ ...param })
-      ])
-      // 列表当日总收益
-      const totalEarnings = listRes.data.reduce((acc,cur) => acc + (cur.todayIncome || 0), 0)
-
-      tableData.value = listRes.data;
-      page.total = listRes.pageMeta.total;
-      totalRevenue.value = roundPrice(totalEarnings, 2, true)
-      
-    }catch(e){
-      //TODO handle the exception
-      console.log('数据请求超时或报错',e)
-    } finally {
-      closeFullScreen()
-    }
   };
 
-  const changeTableData = (type) => {
-    formSearch.value.page = type ? 1 : page.pageNum;
-    formSearch.value.limit = page.pageSize;
-    // 分页切换
-    getList();
+  page.pageNum = 1
+  page.pageSize = 20
+  page.total = 0
+  // getList();
+  settingData()
 };
 
-  // 搜索
-  const handleFormSubmitted = (formData) => {
-    // console.log("接收到子组件传递的数据", formData);
-    formSearch.value.page = page.pageNum;
-    formSearch.value.limit = page.pageSize;
-    formSearch.value.nickName = formData.nickName;
-    formSearch.value.userId = formData.userId;
-    formSearch.value.ditchId = formData.ditchId;
-    formSearch.value.userStatus = formData.userStatus;
-    formSearch.value.appIds = formData.appIds;
-    formSearch.value.registryTimeBegin = convertUTCToBeijing(formData.registryTimeBegin,false) || undefined
-    formSearch.value.registryTimeEnd = convertUTCToBeijing(formData.registryTimeEnd,false) || undefined
-
-    getList();
-  };
-
-  // 表单重置
-  const handleFormReset = () => {
-    formSearch.value = {
-      nickName: undefined,// 用户昵称
-      userId: undefined,// 用户ID
-      ditchId: undefined,// 渠道来源
-      userStatus: 1,// 用户状态
-      appIds: undefined, //所属应用
-      registryTimeBegin: getTodayRangeLocal(),// 注册时间
-      registryTimeEnd: undefined,// 注册时间
-      page: 1,// 当前页码
-      limit: 20,// 当前页数量(查询量)
-    };
-   
-    page.pageNum = 1
-    page.pageSize = 20
-    page.total = 0
-    // getList();
-    settingData()
-  };
-
-  const selectData = ref([])
-  const currentAppName = ref(null) // 存储允许全选的 appName
-  // 选择监听器
-  const handleSelectionChange = (val) => {
-    selectData.value = val
-  }
-
-  // 全选
-  const handleSelectAll = (selection) => {
-    if (selection.length === 0) {
-      // 全选取消
-      currentAppName.value = null
-      return
-    }
-
-    // 取第一个选中的 appName 作为基准
-    const firstAppName = selection[0].appName
-    // 先全部取消
-    table.value.table.clearSelection()
-
-    // 再只选中同一 appName 的行
-    tableData.value.forEach(row => {
-      if (row.appName === firstAppName) {
-        table.value.table.toggleRowSelection(row, true)
-      }
-    })
-
-    currentAppName.value = firstAppName
-  }
-
-  // 取消全选
-  const clearSelection = () => {
+const selectData = ref([])
+const currentAppName = ref(null) // 存储允许全选的 appName
+// 选择监听器
+const handleSelectionChange = (val) => {
+  selectData.value = val
+}
+
+// 全选
+const handleSelectAll = (selection) => {
+  if (selection.length === 0) {
+    // 全选取消
     currentAppName.value = null
-    table.value.table.clearSelection()
+    return
   }
 
-  // 单选
-  const handleSelect = (selection, row) => {
-    // 如果是单选时第一次选中,就记录当前 appName
-    if (selection.length === 1) {
-      currentAppName.value = selection[0].appName
-    }
+  // 取第一个选中的 appName 作为基准
+  const firstAppName = selection[0].appName
+  // 先全部取消
+  table.value.table.clearSelection()
 
-    // 如果选了不同 appName 的数据,则取消
-    if (selection.length > 0 && row.appName !== currentAppName.value) {
-      table.value.table.toggleRowSelection(row, false)
-      ElMessage.warning('只能选择同一个应用的用户哦')
+  // 再只选中同一 appName 的行
+  tableData.value.forEach(row => {
+    if (row.appName === firstAppName) {
+      table.value.table.toggleRowSelection(row, true)
     }
-  }
+  })
 
-  const loading = ref(null)
+  currentAppName.value = firstAppName
 
-  // 加载信息
-  const openFullScreen = (loadText) => {
-      loading.value = ElLoading.service({
-          lock: true,
-          text: loadText,
-          background: 'rgba(0, 0, 0, 0.7)',
-      })
+}
+
+// 取消全选
+const clearSelection = () => {
+  currentAppName.value = null
+  table.value.table.clearSelection()
+}
+
+// 单选
+const handleSelect = (selection, row) => {
+  // 如果是单选时第一次选中,就记录当前 appName
+  if (selection.length === 1) {
+    currentAppName.value = selection[0].appName
   }
 
-  const closeFullScreen = () => {
-      loading.value.close()
+  // 如果选了不同 appName 的数据,则取消
+  if (selection.length > 0 && row.appName !== currentAppName.value) {
+    table.value.table.toggleRowSelection(row, false)
+    ElMessage.warning('只能选择同一个应用的用户哦')
   }
+}
 
-  // 弹窗
-  const layer = ref({
-    show: false,
-    title: "封禁用户",
-    showButton: true,
-    width: '300px'
-  });
+const loading = ref(null)
 
-  const formEdit = ref({
-    bannedLimit: null, //封禁期限
-    bannedReason: '',//封禁原因
-    bannedTargetId: '',//封禁目标ID
-    bannedType: '',//封禁类型 1-渠道 2-平台
-    operator: '',//操作人
-    operatorName: '',//操作人名称
-    userId: '', //用户ID
-    appId: '',//应用ID
+// 加载信息
+const openFullScreen = (loadText) => {
+  loading.value = ElLoading.service({
+    lock: true,
+    text: loadText,
+    background: 'rgba(0, 0, 0, 0.7)',
   })
+}
+
+const closeFullScreen = () => {
+  loading.value.close()
+}
+
+// 弹窗
+const layer = ref({
+  show: false,
+  title: "封禁用户",
+  showButton: true,
+  width: '300px',
+  isMulty: false
+});
+
+const formEdit = ref({
+  bannedLimit: undefined, //封禁期限
+  bannedReason: undefined,//封禁原因
+  bannedTargetId: undefined,//封禁目标ID
+  bannedType: undefined,//封禁类型 1-渠道 2-平台
+  operator: undefined,//操作人
+  operatorName: undefined,//操作人名称
+  userId: undefined, //用户ID
+  appId: undefined,//应用ID
+  userIds: undefined, //用户ID
+})
+
+const edit = (row, isMulty = false) => {
+  ruleForm.value?.resetFields()
+  layer.value.isMulty = isMulty
+
+  if (isMulty) {
+    layer.value.title = '批量封禁用户'
+    formEdit.value = {}
+
+  } else {
+    layer.value.title = '封禁用户'
 
-  const edit = (row) => {
-    ruleForm.value?.resetFields()
-    layer.value.show = true
     formEdit.value.bannedTargetId = row.appId
     formEdit.value.bannedType = row.channelType
     formEdit.value.operator = store.state.user.info.loginName
@@ -480,57 +513,85 @@
     formEdit.value.appId = row.appId
   }
 
-  const ruleForm = ref(null);
-
-  const rules = reactive({
-    formEditbannedLimit: [
-      { required: true, message: "请输入封禁期限", trigger: "blur" },
-    ],
-    bannedReason: [
-      {
-        required: true,
-        message: "请输入封禁原因",
-        trigger: "blur",
-      },
-    ],
-  });
-
-  const submit = async (formEl) => {
-    await formEl.validate(async (valid, fields) => {
-      if (valid) {
+  layer.value.show = true
+}
+
+const ruleForm = ref(null);
+
+const rules = reactive({
+  formEditbannedLimit: [
+    { required: true, message: "请输入封禁期限", trigger: "blur" },
+  ],
+  bannedReason: [
+    {
+      required: true,
+      message: "请输入封禁原因",
+      trigger: "blur",
+    },
+  ],
+});
+
+const submit = async (formEl) => {
+  await formEl.validate(async (valid, fields) => {
+    if (valid) {
+      if (layer.value.isMulty) {
+        formEdit.value.appId = selectData.value[0].appId
+        formEdit.value.userIds = selectData.value.map((item) => item.userId).join(",")
+        // 批量封禁
+        batchBanned({ ...formEdit.value }).then((res) => {
+          layer.value.show = false
+          getList().then(() => {
+            ElMessage.success('批量封禁用户成功,如未更新状态,请手动刷新')
+          })
+        })
+      } else {
         // 提交内容
         riskBannedUser({ ...formEdit.value }).then((res) => {
-          ElMessage.success('封禁用户成功')
           layer.value.show = false
-          getList();
+          getList().then(() => {
+            ElMessage.success('封禁用户成功,如未更新状态,请手动刷新')
+          })
         })
-      } else {
-        console.log("error submit!", fields);
       }
-    })  
-  }
-
-  // 弹窗2
-  const layer1 = ref({
-    show: false,
-    title: "更改用户状态",
-    showButton: true,
-    width: '300px'
-  });
-
-  const formEdit1 = ref({
-    bannedType: null,//封禁类型 1-渠道 2-平台
-    operator: '',//操作人
-    operatorName: '',//操作人名称
-    reason: '',//原因
-    userId: '', //用户ID
-    appId: '', //应用ID
-    userStatus: null, //用户状态
+    } else {
+      console.log("error submit!", fields);
+    }
   })
-
-  const editUserType = (row) => {
-    ruleForm1.value?.resetFields()
-    layer1.value.show = true
+}
+
+// 弹窗2
+const layer1 = ref({
+  show: false,
+  title: "更改用户状态",
+  showButton: true,
+  width: '300px',
+  isMulty: false
+});
+
+const formEdit1 = ref({
+  bannedType: undefined,//封禁类型 1-渠道 2-平台
+  operator: undefined,//操作人
+  operatorName: undefined,//操作人名称
+  reason: undefined,//原因
+  userId: undefined, //用户ID
+  appId: undefined, //应用ID
+  userStatus: undefined, //用户状态
+
+  deblockingReason: undefined,//批量解封原因
+  agentId: undefined, //加盟商ID
+  userIds: undefined, //用户ID
+})
+
+const editUserType = (row, isMulty = false) => {
+  ruleForm1.value?.resetFields()
+  layer1.value.isMulty = isMulty
+
+  if (isMulty) {
+    layer1.value.title = '批量解封用户'
+    formEdit1.value = {}
+
+  } else {
+    layer1.value.title = '解封用户'
 
     formEdit1.value.bannedType = row.channelType
     formEdit1.value.operator = store.state.user.info.loginName
@@ -538,181 +599,292 @@
     formEdit1.value.userId = row.userId
     formEdit1.value.appId = row.appId
   }
+  layer1.value.show = true
+}
+
+const ruleForm1 = ref(null);
+
+const rules1 = reactive({
+  userStatus: [
+    {
+      required: true,
+      message: "请选择用户状态",
+      trigger: "change",
+    },
+  ],
+  reason: [
+    {
+      required: true,
+      message: "请输入更改原因",
+      trigger: ["blur"],
+    },
+  ],
+});
+
+const submit1 = async (formEl) => {
+  await formEl.validate(async (valid, fields) => {
+    if (valid) {
+
+      if (layer1.value.isMulty) {
+        // 批量解封
+        formEdit1.value.deblockingReason = formEdit1.value.reason
+        formEdit1.value.appId = selectData.value[0].appId
+        formEdit1.value.userIds = selectData.value.map((item) => item.userId).join(",")
+        delete formEdit1.value.reason
+
+        batchDeblock(formEdit1.value).then((res) => {
+          layer1.value.show = false
+          getList().then(() => {
+            ElMessage.success('批量解封用户成功,如未更新状态,请手动刷新')
+          })
+        })
 
-  const ruleForm1 = ref(null);
-
-  const rules1 = reactive({
-    userStatus: [
-      {
-        required: true,
-        message: "请选择用户状态",
-        trigger: "change",
-      },
-    ],
-    reason: [
-      {
-        required: true,
-        message: "请输入更改原因",
-        trigger: ["blur"],
-      },
-    ],
-  });
-
-  const submit1 = async (formEl) => {
-    await formEl.validate(async (valid, fields) => {
-      if (valid) {
+      } else {
         // 提交内容
         riskChangeUserStatus(formEdit1.value).then((res) => {
-          ElMessage.success('更改用户状态成功')
           layer1.value.show = false
-          getList();
+          getList().then(() => {
+            ElMessage.success('更改用户状态成功,如未更新状态,请手动刷新')
+          })
         })
-      } else {
-        console.log("error submit!", fields);
       }
-    })
-  }
+
+    } else {
+      console.log("error submit!", fields);
+    }
+  })
+}
 
 
-  // 锁定用户
-  const lockUser = async (row) => {
-    riskLockUser({ userId: row.userId, appId: row.appId }).then((res) => {
-      ElMessage.success('锁定用户成功')
-      getList();
+// 锁定用户
+const lockUser = async (row) => {
+  riskLockUser({ userId: row.userId, appId: row.appId }).then((res) => {
+    getList().then(() => {
+      ElMessage.success('锁定用户成功,如未更新状态,请手动刷新')
     })
-  }
+  })
+}
 
-  // 查看ECPM
-  const ecpmLayer = ref({
-    show: false,
-    title: "查看ECPM",
-    showButton: true,
-    width: '95vw',
-    height: '60vh'
-  });
+// 查看ECPM
+const ecpmLayer = ref({
+  show: false,
+  title: "查看ECPM",
+  showButton: true,
+  width: '95vw',
+  height: '60vh'
+});
 
-  
-  const formSearch1 = ref({
-    appId: undefined, // 应用ID
-    userId: undefined, //用户ID
-    adsourceType: undefined,// 广告源类型
-  });
 
-  const handleFormSubmitted1 = (formData) => {
-    // console.log("接收到子组件传递的数据", formData);
-    formSearch1.value.adsourceType = formData.adsourceType;
+const formSearch1 = ref({
+  appId: undefined, // 应用ID
+  userId: undefined, //用户ID
+  adsourceType: undefined,// 广告源类型
+});
 
-    lookEcpm();
-  };
-  const handleFormReset1 = () => {
-    delete formSearch1.value.adsourceType // 渠道来源
+const handleFormSubmitted1 = (formData) => {
+  // console.log("接收到子组件传递的数据", formData);
+  formSearch1.value.adsourceType = formData.adsourceType;
 
-    lookEcpm();
-  };
+  lookEcpm();
+};
+const handleFormReset1 = () => {
+  delete formSearch1.value.adsourceType // 渠道来源
 
-  const ecpmTable = ref(null)
-  const ecpmData = ref([])
+  lookEcpm();
+};
 
-  const lookEcpm = async(row)=> {
-    if(row) {
-      formSearch1.value.appId = row.appId
-      formSearch1.value.userId = row.userId
-    }
-    ecpmData.value = []
-    let res = await appUserEcpm({...formSearch1.value})
-    ecpmData.value = res.data
-    ecpmLayer.value.show = true
-  }
+const ecpmTable = ref(null)
+const ecpmData = ref([])
 
-  // #region  审核备注
-  const userCheck = async() => {
-    ruleForm2.value?.resetFields()
-    layer2.value.show = true
-    formEdit2.value = []
+const lookEcpm = async (row) => {
+  if (row) {
+    formSearch1.value.appId = row.appId
+    formSearch1.value.userId = row.userId
+    formSearch1.value.adsourceType = undefined
   }
+  ecpmData.value = []
+  let res = await appUserEcpm({ ...formSearch1.value })
+  ecpmData.value = res.data
+  ecpmLayer.value.show = true
+}
+
+// #region  审核备注
+const userCheck = async () => {
+  ruleForm2.value?.resetFields()
+  layer2.value.show = true
+  formEdit2.value = []
+}
+
+// 弹窗
+const layer2 = ref({
+  show: false,
+  title: "批量审核",
+  showButton: true,
+  width: '300px'
+});
+
+const formEdit2 = ref({
+  appId: undefined, //应用ID
+  ditchId: undefined,//渠道ID
+  userIds: undefined, //用户ID
+  bannedLimit: undefined, //封禁时间(天)
+  effectTime: undefined,//生效时间(小时)
+  remark: undefined, //备注
+})
+
+const ruleForm2 = ref(null);
+
+const rules2 = reactive({
+  remark: [
+    { required: true, message: "请输入审核备注内容", trigger: "blur" },
+  ],
+  bannedLimit: [
+    { required: true, message: "请选择封禁时间", trigger: "change" },
+  ],
+  effectTime: [
+    { required: true, message: "请选择生效时间", trigger: "change" },
+  ],
+});
+
+const submit2 = async (formEl) => {
+  await formEl.validate(async (valid, fields) => {
+    if (valid) {
+      formEdit2.value.appId = selectData.value[0].appId
+      formEdit2.value.ditchId = selectData.value[0].ditchId
+      formEdit2.value.userIds = selectData.value.map((item) => item.userId).join(",")
+
+      // 提交内容
+      await batchAudit({ ...formEdit2.value }).then((res) => {
+        layer2.value.show = false
+        getList().then(() => {
+          ElMessage.success('审核提交成功,如未更新状态,请手动刷新')
+        })
+      })
 
-    // 弹窗
-  const layer2 = ref({
-    show: false,
-    title: "批量审核",
-    showButton: true,
-    width: '300px'
-  });
-
-  const formEdit2 = ref({
-    appId: undefined, //应用ID
-    ditchId: undefined,//渠道ID
-    userIds: undefined, //用户ID
-    bannedLimit: undefined, //封禁时间(天)
-    effectTime: undefined,//生效时间(小时)
-    remark: undefined, //备注
+    } else {
+      console.log("error submit!", fields);
+    }
   })
+}
+// #endregion
 
-  const ruleForm2 = ref(null);
-
-  const rules2 = reactive({
-    remark: [
-      { required: true, message: "请输入审核备注内容", trigger: "blur" },
-    ],
-    bannedLimit: [
-      { required: true, message: "请选择封禁时间", trigger: "change" },
-    ],
-    effectTime: [
-      { required: true, message: "请选择生效时间", trigger: "change" },
-    ],
-  });
 
-  const submit2 = async (formEl) => {
-    await formEl.validate(async (valid, fields) => {
-      if (valid) {
-        formEdit2.value.appId = selectData.value[0].appId
-        formEdit2.value.ditchId = selectData.value[0].ditchId
-        formEdit2.value.userIds = selectData.value.map((item) => item.userId).join(",")
+// #region  数据导出
+// 批量导出
+const exportUserList = async () => {
+  if (!selectData.value || selectData.value.length === 0) {
+    ElMessage.warning('没有可导出的数据')
+    return;
+  }
 
-        // 提交内容
-        await batchAudit({ ...formEdit2.value }).then((res) => {
-          ElMessage.success('审核提交成功')
-          layer2.value.show = false
-          getList();
+  exportToExcel(getData(selectData.value), tableHeader, 20, '用户列表数据', true).then(() => {
+    ElMessage.success('导出成功')
+  })
+    .catch(() => {
+      ElMessage.error('导出失败,请重试')
+    })
+}
+
+// 全部导出
+const allExportUserList = async () => {
+  // 请求数据
+  try {
+    openFullScreen('数据导出中,请勿操作!!!')
+
+    const params = formSearch.value
+    params.page = 1
+    params.limit = page.total - 1
+    const res = await getUserList({ ...params })
+    if (res.data.length > 0) {
+      exportToExcel(getData(res.data), tableHeader, 20, '用户列表数据', true).then(() => {
+        ElMessage.success('导出成功')
+      })
+        .catch(() => {
+          ElMessage.error('导出失败,请重试')
         })
-       
-      } else {
-        console.log("error submit!", fields);
-      }
-    })  
+    }
+  } catch (e) {
+    console.log('数据请求超时或报错', e)
+  } finally {
+    closeFullScreen()
   }
-  // #endregion
+
+}
+
+// 处理数据
+const getData = (data) => {
+  return data.map((item) => ({
+    userId: item.userId,
+    nickName: item.nickName,
+    userStatus: getDictionaryName('user_status', Number(item.userStatus)),
+    ditchName: item.ditchName,
+    todayVideo: item.todayVideo,
+    totalVideo: item.totalVideo,
+    totalIncome: `${roundPrice(item.todayIncome === 0 ? '0.00' : item.todayIncome ?? '0.00')} / ${roundPrice(item.totalIncome === 0 ? '0.00' : item.totalIncome ?? '0.00')}`,
+    communicationOperator: item.communicationOperator,
+    loginRecordList: item.loginRecordList?.length
+      ? `${item.loginRecordList[0].deviceBrand} ${item.loginRecordList[0].deviceModel}`
+      : "",
+    deviceRepeatCount: item.deviceRepeatCount,
+    ipRepeatCount: item.ipRepeatCount,
+    lastLoginIp: item.lastLoginIp,
+    lastLoginTime: convertUTCToBeijing(item.lastLoginTime),
+    loginDays: item.loginDays,
+    registryTime: convertUTCToBeijing(item.registryTime),
+  }))
+}
+
+// 表头
+const tableHeader = {
+  "userId": "用户ID",
+  "nickName": "用户昵称",
+  "userStatus": "用户状态",
+  "ditchName": "渠道来源",
+  "todayVideo": "今日视频播放数",
+  "totalVideo": "视频播放总数",
+  "totalIncome": "用户贡献(当日/总共)",
+  "communicationOperator": "通信运营商",
+  "loginRecordList": "用户设备",
+  "deviceRepeatCount": "设备重复数量",
+  "ipRepeatCount": "IP重复数量",
+  "lastLoginIp": "最新登录IP",
+  "lastLoginTime": "最新登录时间",
+  "loginDays": "登录天数",
+  "registryTime": "注册时间",
+}
+
+// #endregion
 
 </script>
 
 <style scoped lang="scss">
-  .layout-container {
+.layout-container {
 
-    .card {
-      .title {
-        margin-bottom: 10px;
-        font-weight: 600;
-      }
-
-      display: flex;
-      flex-direction: column;
-      align-items: start;
-      width: calc(100% - 60px);
-      margin: 30px 30px 0;
+  .card {
+    .title {
+      margin-bottom: 10px;
+      font-weight: 600;
     }
 
-    .button {
-      display: flex;
-      //flex-direction: column;
-
-      .button-item {
-        margin: 4px;
-      }
-    }
+    display: flex;
+    flex-direction: column;
+    align-items: start;
+    width: calc(100% - 60px);
+    margin: 30px 30px 0;
   }
 
-  .btn {
+  .button {
     display: flex;
-    margin: 10px 0 -10px 30px;
+    //flex-direction: column;
+
+    .button-item {
+      margin: 4px;
+    }
   }
+}
+
+.btn {
+  display: flex;
+  margin: 10px 0 -10px 30px;
+}
 </style>