userList.vue 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955
  1. <template>
  2. <div class="layout-container">
  3. <!-- 菜单栏 -->
  4. <From :form-items="dynamicFormItemsTarget" @formSubmitted="handleFormSubmitted" @formReset="handleFormReset"
  5. @formChangeSelect="handleFormSelect" />
  6. <div class="btn">
  7. <el-button type="" v-if="formSearch.appIds && selectData.length > 0" @click="clearSelection">取消全选</el-button>
  8. <!-- <el-button type="primary" :disabled="!selectData.length > 0" @click="userCheck">批量审核</el-button> -->
  9. <el-button type="danger" v-if="appShare" :disabled="!selectData.length > 0" @click="edit({}, 2)">批量封禁母包</el-button>
  10. <el-button type="danger" v-if="appShare" :disabled="!selectData.length > 0" @click="edit({}, 1)">批量封禁</el-button>
  11. <el-button type="danger" v-if="appShare" :disabled="!selectData.length > 0" @click="edit({}, 3)">批量封禁IP</el-button>
  12. <el-button type="primary" v-if="appShare" :disabled="!selectData.length > 0" @click="editUserType({}, true)">批量解封</el-button>
  13. <el-button type="success" v-if="appShare" :disabled="!selectData.length > 0" @click="exportUserList">批量导出</el-button>
  14. <el-tooltip class="box-item" v-if="appShare" effect="dark" content="慎用 比较占用服务器资源!!! 导出数量(根据左下角条数)" placement="bottom">
  15. <el-button type="warning" :disabled="!page.total > 0" @click="allExportUserList">全部导出</el-button>
  16. </el-tooltip>
  17. </div>
  18. <!-- 表格 -->
  19. <div class="layout-container">
  20. <Table @getTableData="changeTableData" v-model:page="page" ref="table" :data="tableData" :showSelection="true"
  21. @selection-change="handleSelectionChange" @select-all="handleSelectAll" @select="handleSelect"
  22. :revenue="totalRevenue">
  23. <el-table-column prop="userId" label="查看ECPM" width="130" fixed="left">
  24. <template #default="scope">
  25. <el-button type="primary" @click="lookEcpm(scope.row)">查看ECPM</el-button>
  26. </template>
  27. </el-table-column>
  28. <el-table-column prop="userId" label="用户ID" width="120" fixed="left" />
  29. <el-table-column prop="nickName" label="用户昵称" fixed="left" width="100" />
  30. <el-table-column prop="userStatus" label="用户状态" width="90">
  31. <template #default="scope">
  32. {{ getDictionaryName('user_status', Number(scope.row.userStatus)) }}
  33. </template>
  34. </el-table-column>
  35. <!-- <el-table-column prop="userType" label="用户类型" width="90" />-->
  36. <!-- <el-table-column prop="appId" label="应用ID" width="150" /> -->
  37. <!-- <el-table-column prop="appName" label="应用名称" width="150" /> -->
  38. <!-- <el-table-column prop="appType" label="应用类型" width="90">
  39. <template #default="scope">
  40. {{ getDictionaryName('app_type',scope.row.appType) }}
  41. </template>
  42. </el-table-column> -->
  43. <el-table-column prop="ditchName" label="渠道来源" width="150" />
  44. <el-table-column prop="todayAnswer" label="今日答题数" width="100">
  45. <template #default="scope">
  46. {{ scope.row.todayAnswer || 0 }}
  47. </template>
  48. </el-table-column>
  49. <el-table-column prop="todayVideo" label="当日视频播放数" width="130" />
  50. <el-table-column prop="totalVideo" label="视频播放总数" width="110" />
  51. <el-table-column prop="nearlyIncome" label="前三日总收益" width="110">
  52. <template #default="scope">{{
  53. roundPrice(scope.row.nearlyIncome, 3)
  54. }} </template>
  55. </el-table-column>
  56. <el-table-column prop="todayIncome" label="用户当日贡献" sortable width="140">
  57. <template #default="scope">
  58. {{ roundPrice(scope.row.todayIncome === 0 ? '0.000' : scope.row.todayIncome ?? '0.000', 3) }}
  59. </template>
  60. </el-table-column>
  61. <el-table-column prop="totalIncome" label="用户总贡献" sortable width="140">
  62. <template #default="scope">
  63. {{ roundPrice(scope.row.totalIncome === 0 ? '0.000' : scope.row.totalIncome ?? '0.000', 3) }}
  64. </template>
  65. </el-table-column>
  66. <el-table-column prop="communicationOperator" label="通信运营商" width="130" />
  67. <el-table-column prop="loginRecordList" label="用户设备" width="200">
  68. <template #default="scope">
  69. {{ scope.row.loginRecordList ? scope.row.loginRecordList[0].deviceBrand : '' }} {{
  70. scope.row.loginRecordList ? scope.row.loginRecordList[0].deviceModel : '' }}
  71. </template>
  72. </el-table-column>
  73. <el-table-column prop="deviceRepeatCount" label="设备重复数量" width="110" />
  74. <el-table-column prop="ipRepeatCount" label="IP重复数量" width="100" />
  75. <el-table-column prop="lastLoginIp" label="最新登录IP" width="150">
  76. <template #default="scope">
  77. <div v-if="scope.row.maybeDone === 1 && scope.row.appType === 2" style="color: red">
  78. <el-tooltip class="box-item" effect="dark" content="该ip当日已完成过任务" placement="top">
  79. {{ scope.row.lastLoginIp || '' }}
  80. </el-tooltip>
  81. </div>
  82. <div v-else>
  83. {{ scope.row.lastLoginIp || '' }}
  84. </div>
  85. </template>
  86. </el-table-column>
  87. <el-table-column prop="lastLoginTime" label="最新登录时间" width="160">
  88. <template #default="scope">
  89. {{ convertUTCToBeijing(scope.row.lastLoginTime) }}
  90. </template>
  91. </el-table-column>
  92. <el-table-column prop="loginDays" label="登录天数" width="90" />
  93. <!-- <el-table-column prop="pointsBalance" label="积分余额" width="90" />
  94. <el-table-column prop="pointsTotal" label="积分总额" width="90" />
  95. <el-table-column prop="redPacketAmount" label="红包总额" width="90" />
  96. <el-table-column prop="redPacketBalance" label="红包余额" width="90" /> -->
  97. <el-table-column prop="registryTime" label="注册时间" width="160">
  98. <template #default="scope">
  99. {{ convertUTCToBeijing(scope.row.registryTime) }}
  100. </template>
  101. </el-table-column>
  102. <!-- <el-table-column prop="versionCode" label="版本号" /> -->
  103. <!-- <el-table-column prop="withdrawCount" label="提现笔数" width="90" /> -->
  104. <!-- <el-table-column prop="withdrawTotal" label="提现总额" width="90" /> -->
  105. <el-table-column v-if="false" label="操作" width="220" v-permission="'permission'" fixed="right">
  106. <template #default="scope">
  107. <div class="button">
  108. <el-link class="button-item" type="primary" @click="editUserType(scope.row)">
  109. 风控解除
  110. </el-link>
  111. <el-link v-if="scope.row.userStatus < 3" class="button-item" type="danger" @click="edit(scope.row)">
  112. 封禁用户
  113. </el-link>
  114. <el-popconfirm title="确认锁定该用户?" @confirm="lockUser(scope.row)">
  115. <template #reference>
  116. <el-link v-if="scope.row.userStatus < 2" class="button-item" type="warning">锁定用户</el-link>
  117. </template>
  118. </el-popconfirm>
  119. </div>
  120. </template>
  121. </el-table-column>
  122. </Table>
  123. </div>
  124. <!-- 操作弹窗 -->
  125. <Layer :layer="layer" @confirm="submit(ruleForm)" @close="layer.show = false">
  126. <el-form :model="formEdit" :rules="rules" ref="ruleForm" label-width="140px" style="margin-right:30px;">
  127. <el-form-item label="封禁时间(天):" required prop="bannedLimit">
  128. <!-- <el-input v-model="formEdit.bannedLimit" type="number" placeholder="请输入封禁期限" clearable /> -->
  129. <el-input-number step-strictly v-model="formEdit.bannedLimit" :step="1" :min="0" :precision="0" />
  130. </el-form-item>
  131. <el-form-item label="封禁原因:" required prop="bannedReason">
  132. <el-input v-model="formEdit.bannedReason" placeholder="请输入封禁原因" clearable />
  133. </el-form-item>
  134. </el-form>
  135. </Layer>
  136. <Layer :layer="layer1" @confirm="submit1(ruleForm1)" @close="layer.show = false">
  137. <el-form :model="formEdit1" :rules="rules1" ref="ruleForm1" label-width="120px" style="margin-right:30px;">
  138. <el-form-item v-if="!layer1.isMulty" label="用户状态:" required prop="userStatus">
  139. <el-select v-model="formEdit1.userStatus" placeholder="请选择状态" filterable>
  140. <el-option v-for="option in getOptions('user_status')" :key="option.label" :label="option.label"
  141. :value="option.value">
  142. </el-option>
  143. </el-select>
  144. </el-form-item>
  145. <el-form-item label="原因:" prop="reason">
  146. <el-input v-model="formEdit1.reason" placeholder="请输入原因" clearable />
  147. </el-form-item>
  148. </el-form>
  149. </Layer>
  150. <el-dialog v-model="ecpmLayer.show" :title="`查看用户《${ecpmData.nickName}》的ECPM`" :width="ecpmLayer.width" align-center
  151. center :fullscreen="isFullscreen">
  152. <el-icon class=" full-icon" @click="isFullscreen = !isFullscreen">
  153. <FullScreen />
  154. </el-icon>
  155. <EcpmDialog v-if="ecpmLayer.show" v-model:isFullscreen="isFullscreen" :ecpmData="ecpmData" />
  156. </el-dialog>
  157. <!-- 审核备注 -->
  158. <Layer :layer="layer2" @confirm="submit2(ruleForm2)" @close="layer2.show = false">
  159. <el-form :model="formEdit2" :rules="rules2" ref="ruleForm2" label-width="140px" style="margin-right:30px;">
  160. <el-form-item label="封禁时间(天)" required prop="bannedLimit">
  161. <el-input-number v-model="formEdit2.bannedLimit" :min="1" :step="1" step-strictly style="width: 100%;" />
  162. </el-form-item>
  163. <el-form-item label="生效时间(小时)" required prop="effectTime">
  164. <el-input-number v-model="formEdit2.effectTime" :min="0" :step="1" step-strictly style="width: 100%;" />
  165. <el-alert title="单位为小时,0表示立即执行" type="error" center :closable="false" style="margin-top: 5px;height: 30px;" />
  166. </el-form-item>
  167. <el-form-item label="审核备注" required prop="remark">
  168. <el-input v-model="formEdit2.remark" type="textarea" rows="4" placeholder="请输入审核备注内容" clearable />
  169. </el-form-item>
  170. </el-form>
  171. </Layer>
  172. </div>
  173. </template>
  174. <script setup>
  175. import { onBeforeMount, ref, reactive, nextTick } from "vue";
  176. import From from "@/components/from/index.vue";
  177. import Table from "@/components/table/index.vue";
  178. import Layer from '@/components/layer/index.vue'
  179. import { FullScreen } from '@element-plus/icons'
  180. import { ElMessage, ElLoading } from 'element-plus'
  181. import {
  182. getUserList, riskBannedUser, riskLockUser, appUserEcpm, getRevenueByTime,
  183. batchAudit, batchBanned, batchDeblock, batchBannedIps
  184. } from '@/api/userModule.js'
  185. import { ditchList } from '@/api/outBagModule.js'
  186. import { appList } from "@/api/formworkErection.js";
  187. import { riskChangeUserStatus } from '@/api/riskModule.js'
  188. import { convertUTCToBeijing, roundPrice, getTodayRangeLocal } from '@/utils/index.js'
  189. import { useGetDictList } from '@/hooks/useGetDictList.js'
  190. import { useStore } from 'vuex'
  191. import { exportToExcel } from '@/utils/exportExcel.js'
  192. import EcpmDialog from "./components/ecpmDialog.vue";
  193. const store = useStore()
  194. const { loadDictData, getOptions, getDictionaryName } = useGetDictList();
  195. const tableData = ref([]);
  196. const table = ref(null)
  197. const appType = ref(1)
  198. const appShare = ref(false);
  199. // 新增:存储所有应用选项的响应式变量
  200. const allAppsOptions = ref([]);
  201. // 分页参数, 供table使用
  202. const page = reactive({
  203. pageNum: 1,
  204. pageSize: 20,
  205. total: 0,
  206. });
  207. const formSearch = ref({
  208. // lastLoginTime: undefined,// 最新登录时间
  209. nickName: undefined,// 用户昵称
  210. userId: undefined,// 用户ID
  211. ditchId: undefined,// 渠道来源
  212. userStatus: 1,// 用户状态
  213. appIds: undefined, //所属应用
  214. // registryTimeBegin: getTodayRangeLocal(),// 注册时间
  215. // registryTimeEnd: undefined,// 注册时间
  216. page: 1,// 当前页码
  217. limit: 20,// 当前页数量(查询量)
  218. lastLoginTimeBegin: getTodayRangeLocal(), //最新登录时间开始
  219. lastLoginTimeEnd: undefined, //最新登录时间结束
  220. lastLoginIp: undefined, //最新登陆IP
  221. videoThreshold: undefined //同IP激励视频数提醒阈值
  222. });
  223. const dynamicFormItems = ref([])
  224. const dynamicFormItemsTarget = ref([])
  225. onBeforeMount(() => {
  226. settingData()
  227. // getList();
  228. });
  229. // 获取缓存数据设置筛选数据
  230. const settingData = async () => {
  231. loadDictData().then(() => {
  232. dynamicFormItems.value = [
  233. {
  234. label: '用户昵称',
  235. prop: 'nickName',
  236. type: 'input',
  237. needEnterEvent: true
  238. },
  239. {
  240. label: '用户ID',
  241. prop: 'userId',
  242. type: 'input',
  243. needEnterEvent: true
  244. },
  245. {
  246. label: '所属应用',
  247. prop: 'appIds',
  248. type: 'select',
  249. clearable: false,
  250. options: [],
  251. },
  252. {
  253. label: '渠道来源',
  254. prop: 'ditchId',
  255. type: 'select',
  256. options: getOptions('channel_origin'),
  257. },
  258. {
  259. label: '用户状态',
  260. prop: 'userStatus',
  261. type: 'select',
  262. defaultVal: 1,
  263. options: getOptions('user_status'),
  264. },
  265. { label: '登录时间开始', prop: 'lastLoginTimeBegin', type: 'date', defaultVal: getTodayRangeLocal() },
  266. { label: '登录时间结束', prop: 'lastLoginTimeEnd', type: 'date' },
  267. {
  268. label: '最新登陆IP',
  269. prop: 'lastLoginIp',
  270. type: 'input',
  271. needEnterEvent: true
  272. },
  273. {
  274. label: '同IP视频数阈值',
  275. prop: 'videoThreshold',
  276. type: 'input',
  277. needEnterEvent: true,
  278. placeholder: '请输入同IP激励视频数提醒阈值'
  279. }
  280. ]
  281. // 设置动态选项
  282. getApiOptions()
  283. })
  284. }
  285. const firstApp = ref('')
  286. //渠道来源
  287. const getApiOptions = async () => {
  288. try {
  289. // 并发获取数据
  290. const [{ data: ditchData }, { data: appData }] = await Promise.all([
  291. ditchList({ page: 1, limit: 9999 }),
  292. appList({ page: 1, limit: 9999 })
  293. ])
  294. const ditchOptions = ditchData.map(item => ({
  295. label: item.ditchName,
  296. value: item.ditchId
  297. }))
  298. const appsOptions = appData.map(item => ({
  299. label: item.appName,
  300. value: item.appId,
  301. appType: item.appType,
  302. appUserId: item.userId
  303. }))
  304. allAppsOptions.value = appsOptions;
  305. // 赋值到表单项
  306. dynamicFormItems.value[3].options = ditchOptions
  307. dynamicFormItems.value[2].options = appsOptions
  308. // 如果有应用列表,默认选第一个
  309. if (appsOptions.length > 0) {
  310. // const firstApp = appsOptions[0].value
  311. firstApp.value = appsOptions[0].value
  312. dynamicFormItems.value[2].defaultVal = firstApp.value
  313. formSearch.value.appIds = firstApp.value
  314. // 判断第一个应用是否属于当前用户
  315. appShare.value = appsOptions[0].appUserId == store.state.user.info.userId;
  316. dynamicFormItemsTarget.value = appsOptions[0].appType === 1
  317. ? dynamicFormItems.value.slice(0, 7)
  318. : dynamicFormItems.value
  319. }
  320. // 获取列表数据
  321. getList(0)
  322. } catch (err) {
  323. console.error('获取选项失败:', err)
  324. }
  325. }
  326. const totalRevenue = ref(0)
  327. // 分页数据
  328. const getList = async (timeout = 5000) => {
  329. openFullScreen('数据请求中,请勿重复点击!!!')
  330. return new Promise((resolve) => {
  331. setTimeout(async () => {
  332. try {
  333. // 没有所属应用
  334. if (!formSearch.value.appIds) return ElMessage.info('没有所属应用')
  335. /* //处理根据时间获取收益参数
  336. const param = {...formSearch.value}
  337. delete param.page
  338. delete param.limit
  339. //并发获取数据
  340. const [listRes, revenueRes] = await Promise.all([
  341. getUserList({ ...formSearch.value }),
  342. // getRevenueByTime({ ...param })
  343. ]) */
  344. const listRes = await getUserList({ ...formSearch.value })
  345. // 列表当日总收益
  346. const totalEarnings = listRes.data.reduce((acc, cur) => acc + (cur.todayIncome || 0), 0)
  347. tableData.value = listRes.data;
  348. page.total = listRes.pageMeta.total;
  349. totalRevenue.value = roundPrice(totalEarnings, 2, true)
  350. resolve(true);
  351. } catch (e) {
  352. console.log('数据请求超时或报错', e)
  353. resolve(false);
  354. } finally {
  355. closeFullScreen()
  356. }
  357. }, timeout)
  358. });
  359. };
  360. const changeTableData = (type) => {
  361. formSearch.value.page = type ? 1 : page.pageNum;
  362. formSearch.value.limit = page.pageSize;
  363. // 分页切换
  364. getList(0);
  365. };
  366. // 搜索
  367. const handleFormSubmitted = (formData) => {
  368. // console.log("接收到子组件传递的数据", formData);
  369. formSearch.value.page = page.pageNum;
  370. formSearch.value.limit = page.pageSize;
  371. formSearch.value.nickName = formData.nickName;
  372. formSearch.value.userId = formData.userId;
  373. formSearch.value.ditchId = formData.ditchId;
  374. formSearch.value.userStatus = formData.userStatus;
  375. formSearch.value.appIds = formData.appIds;
  376. formSearch.value.lastLoginTimeBegin = convertUTCToBeijing(formData.lastLoginTimeBegin, false) || undefined
  377. formSearch.value.lastLoginTimeEnd = convertUTCToBeijing(formData.lastLoginTimeEnd, false) || undefined
  378. formSearch.value.lastLoginIp = formData.lastLoginIp
  379. formSearch.value.videoThreshold = formData.videoThreshold
  380. getList(0);
  381. };
  382. // 表单重置
  383. const handleFormReset = () => {
  384. formSearch.value = {
  385. nickName: undefined,// 用户昵称
  386. userId: undefined,// 用户ID
  387. ditchId: undefined,// 渠道来源
  388. userStatus: 1,// 用户状态
  389. appIds: undefined, //所属应用
  390. // registryTimeBegin: getTodayRangeLocal(),// 注册时间
  391. // registryTimeEnd: undefined,// 注册时间
  392. page: 1,// 当前页码
  393. limit: 20,// 当前页数量(查询量)
  394. lastLoginTimeBegin: getTodayRangeLocal(), //最新登录时间开始
  395. lastLoginTimeEnd: undefined, //最新登录时间结束
  396. lastLoginIp: undefined, //最新登陆IP
  397. videoThreshold: undefined //同IP激励视频数提醒阈值
  398. };
  399. page.pageNum = 1
  400. page.pageSize = 20
  401. page.total = 0
  402. // getList();
  403. settingData()
  404. };
  405. const handleFormSelect = (item, e) => {
  406. if (item.prop === 'appIds') {
  407. // 查找选中的应用信息
  408. const selectedApp = allAppsOptions.value.find(app => app.value === e);
  409. if (selectedApp) {
  410. // 判断选中的应用是否属于当前用户
  411. appShare.value = selectedApp.appUserId == store.state.user.info.userId;
  412. }
  413. const options = item.options
  414. // appType 1-安卓 2-IOS
  415. const appTypeNum = options.find((item) => item.value === e).appType
  416. dynamicFormItems.value[0].defaultVal = formSearch.value.nickName
  417. dynamicFormItems.value[1].defaultVal = formSearch.value.userId
  418. dynamicFormItems.value[2].defaultVal = e
  419. dynamicFormItems.value[3].defaultVal = formSearch.value.ditchId
  420. dynamicFormItems.value[4].defaultVal = formSearch.value.userStatus
  421. dynamicFormItems.value[5].defaultVal = formSearch.value.lastLoginTimeBegin
  422. dynamicFormItems.value[6].defaultVal = formSearch.value.lastLoginTimeEnd
  423. if (appTypeNum === 1) {
  424. dynamicFormItemsTarget.value = dynamicFormItems.value.slice(0, 7)
  425. } else {
  426. dynamicFormItems.value[7].defaultVal = formSearch.value.lastLoginIp
  427. dynamicFormItems.value[8].defaultVal = formSearch.value.videoThreshold
  428. dynamicFormItemsTarget.value = dynamicFormItems.value
  429. }
  430. }
  431. }
  432. const selectData = ref([])
  433. const currentAppName = ref(null) // 存储允许全选的 appName
  434. // 选择监听器
  435. const handleSelectionChange = (val) => {
  436. selectData.value = val
  437. }
  438. // 全选
  439. const handleSelectAll = (selection) => {
  440. if (selection.length === 0) {
  441. // 全选取消
  442. currentAppName.value = null
  443. return
  444. }
  445. // 取第一个选中的 appName 作为基准
  446. const firstAppName = selection[0].appName
  447. // 先全部取消
  448. table.value.table.clearSelection()
  449. // 再只选中同一 appName 的行
  450. tableData.value.forEach(row => {
  451. if (row.appName === firstAppName) {
  452. table.value.table.toggleRowSelection(row, true)
  453. }
  454. })
  455. currentAppName.value = firstAppName
  456. }
  457. // 取消全选
  458. const clearSelection = () => {
  459. currentAppName.value = null
  460. table.value.table.clearSelection()
  461. }
  462. // 单选
  463. const handleSelect = (selection, row) => {
  464. // 如果是单选时第一次选中,就记录当前 appName
  465. if (selection.length === 1) {
  466. currentAppName.value = selection[0].appName
  467. }
  468. // 如果选了不同 appName 的数据,则取消
  469. if (selection.length > 0 && row.appName !== currentAppName.value) {
  470. table.value.table.toggleRowSelection(row, false)
  471. ElMessage.warning('只能选择同一个应用的用户哦')
  472. }
  473. }
  474. const loading = ref(null)
  475. // 加载信息
  476. const openFullScreen = (loadText) => {
  477. loading.value = ElLoading.service({
  478. lock: true,
  479. text: loadText,
  480. background: 'rgba(0, 0, 0, 0.7)',
  481. })
  482. }
  483. const closeFullScreen = () => {
  484. loading.value.close()
  485. }
  486. // 弹窗
  487. const layer = ref({
  488. show: false,
  489. title: "封禁用户",
  490. showButton: true,
  491. width: '300px',
  492. isMulty: false
  493. });
  494. const formEdit = ref({
  495. bannedLimit: undefined, //封禁期限
  496. bannedReason: undefined,//封禁原因
  497. bannedTargetId: undefined,//封禁目标ID
  498. bannedType: undefined,//封禁类型 1-渠道 2-平台
  499. operator: undefined,//操作人
  500. operatorName: undefined,//操作人名称
  501. userId: undefined, //用户ID
  502. appId: undefined,//应用ID
  503. userIds: undefined, //用户ID
  504. })
  505. const edit = (row, isMulty = 0) => {
  506. ruleForm.value?.resetFields()
  507. layer.value.isMulty = isMulty
  508. if (isMulty === 1) {
  509. layer.value.title = '批量封禁用户'
  510. formEdit.value = {}
  511. } else if (isMulty === 2) {
  512. layer.value.title = '批量封禁母包'
  513. formEdit.value = {}
  514. } else if (isMulty === 3) {
  515. layer.value.title = '批量封禁IP'
  516. formEdit.value = {}
  517. } else {
  518. layer.value.title = '封禁用户'
  519. formEdit.value.bannedTargetId = row.appId
  520. formEdit.value.bannedType = row.channelType
  521. formEdit.value.operator = store.state.user.info.loginName
  522. formEdit.value.operatorName = store.state.user.info.nickName
  523. formEdit.value.userId = row.userId
  524. formEdit.value.appId = row.appId
  525. }
  526. layer.value.show = true
  527. }
  528. const ruleForm = ref(null);
  529. const rules = reactive({
  530. formEditbannedLimit: [
  531. { required: true, message: "请输入封禁期限", trigger: "blur" },
  532. ],
  533. bannedReason: [
  534. {
  535. required: true,
  536. message: "请输入封禁原因",
  537. trigger: "blur",
  538. },
  539. ],
  540. });
  541. const submit = async (formEl) => {
  542. await formEl.validate(async (valid, fields) => {
  543. if (valid) {
  544. if (layer.value.isMulty === 1 || layer.value.isMulty === 2) {
  545. formEdit.value.appId = selectData.value[0].appId
  546. formEdit.value.userIds = selectData.value.map((item) => item.userId).join(",")
  547. // 批量封禁母包
  548. formEdit.value.ifSuperiorBanned = layer.value.isMulty === 2 ? 1 : undefined
  549. // 批量封禁
  550. batchBanned({ ...formEdit.value }).then((res) => {
  551. layer.value.show = false
  552. getList().then(() => {
  553. const hintText = layer.value.isMulty === 2 ? '批量封禁母包' : '批量封禁用户'
  554. ElMessage.success(`${hintText}成功,如未更新状态,请手动刷新`)
  555. })
  556. })
  557. } else if (layer.value.isMulty === 3) {
  558. formEdit.value.appId = selectData.value[0].appId
  559. formEdit.value.bannedIps = selectData.value.map((item) => item.lastLoginIp).join("|")
  560. batchBannedIps({ ...formEdit.value }).then((res) => {
  561. layer.value.show = false
  562. getList().then(() => {
  563. ElMessage.success(`批量封禁IP成功,如未更新状态,请手动刷新`)
  564. })
  565. })
  566. } else {
  567. // 提交内容
  568. riskBannedUser({ ...formEdit.value }).then((res) => {
  569. layer.value.show = false
  570. getList().then(() => {
  571. ElMessage.success('封禁用户成功,如未更新状态,请手动刷新')
  572. })
  573. })
  574. }
  575. } else {
  576. console.log("error submit!", fields);
  577. }
  578. })
  579. }
  580. // 弹窗2
  581. const layer1 = ref({
  582. show: false,
  583. title: "更改用户状态",
  584. showButton: true,
  585. width: '300px',
  586. isMulty: false
  587. });
  588. const formEdit1 = ref({
  589. bannedType: undefined,//封禁类型 1-渠道 2-平台
  590. operator: undefined,//操作人
  591. operatorName: undefined,//操作人名称
  592. reason: undefined,//原因
  593. userId: undefined, //用户ID
  594. appId: undefined, //应用ID
  595. userStatus: undefined, //用户状态
  596. deblockingReason: undefined,//批量解封原因
  597. agentId: undefined, //加盟商ID
  598. userIds: undefined, //用户ID
  599. })
  600. const editUserType = (row, isMulty = false) => {
  601. ruleForm1.value?.resetFields()
  602. layer1.value.isMulty = isMulty
  603. if (isMulty) {
  604. layer1.value.title = '批量解封用户'
  605. formEdit1.value = {}
  606. } else {
  607. layer1.value.title = '解封用户'
  608. formEdit1.value.bannedType = row.channelType
  609. formEdit1.value.operator = store.state.user.info.loginName
  610. formEdit1.value.operatorName = store.state.user.info.nickName
  611. formEdit1.value.userId = row.userId
  612. formEdit1.value.appId = row.appId
  613. }
  614. layer1.value.show = true
  615. }
  616. const ruleForm1 = ref(null);
  617. const rules1 = reactive({
  618. userStatus: [
  619. {
  620. required: true,
  621. message: "请选择用户状态",
  622. trigger: "change",
  623. },
  624. ],
  625. reason: [
  626. {
  627. required: true,
  628. message: "请输入更改原因",
  629. trigger: ["blur"],
  630. },
  631. ],
  632. });
  633. const submit1 = async (formEl) => {
  634. await formEl.validate(async (valid, fields) => {
  635. if (valid) {
  636. if (layer1.value.isMulty) {
  637. // 批量解封
  638. formEdit1.value.deblockingReason = formEdit1.value.reason
  639. formEdit1.value.appId = selectData.value[0].appId
  640. formEdit1.value.userIds = selectData.value.map((item) => item.userId).join(",")
  641. delete formEdit1.value.reason
  642. batchDeblock(formEdit1.value).then((res) => {
  643. layer1.value.show = false
  644. getList().then(() => {
  645. ElMessage.success('批量解封用户成功,如未更新状态,请手动刷新')
  646. })
  647. })
  648. } else {
  649. // 提交内容
  650. riskChangeUserStatus(formEdit1.value).then((res) => {
  651. layer1.value.show = false
  652. getList().then(() => {
  653. ElMessage.success('更改用户状态成功,如未更新状态,请手动刷新')
  654. })
  655. })
  656. }
  657. } else {
  658. console.log("error submit!", fields);
  659. }
  660. })
  661. }
  662. // 锁定用户
  663. const lockUser = async (row) => {
  664. riskLockUser({ userId: row.userId, appId: row.appId }).then((res) => {
  665. getList().then(() => {
  666. ElMessage.success('锁定用户成功,如未更新状态,请手动刷新')
  667. })
  668. })
  669. }
  670. // 查看ECPM
  671. const ecpmData = ref({})
  672. const isFullscreen = ref(false)
  673. const ecpmLayer = ref({
  674. show: false,
  675. title: "查看ECPM",
  676. showButton: true,
  677. width: '95vw',
  678. });
  679. const lookEcpm = async (row) => {
  680. ecpmData.value = row
  681. ecpmLayer.value.show = true
  682. }
  683. // #region 审核备注
  684. const userCheck = async () => {
  685. ruleForm2.value?.resetFields()
  686. layer2.value.show = true
  687. formEdit2.value = []
  688. }
  689. // 弹窗
  690. const layer2 = ref({
  691. show: false,
  692. title: "批量审核",
  693. showButton: true,
  694. width: '300px'
  695. });
  696. const formEdit2 = ref({
  697. appId: undefined, //应用ID
  698. ditchId: undefined,//渠道ID
  699. userIds: undefined, //用户ID
  700. bannedLimit: undefined, //封禁时间(天)
  701. effectTime: undefined,//生效时间(小时)
  702. remark: undefined, //备注
  703. })
  704. const ruleForm2 = ref(null);
  705. const rules2 = reactive({
  706. remark: [
  707. { required: true, message: "请输入审核备注内容", trigger: "blur" },
  708. ],
  709. bannedLimit: [
  710. { required: true, message: "请选择封禁时间", trigger: "change" },
  711. ],
  712. effectTime: [
  713. { required: true, message: "请选择生效时间", trigger: "change" },
  714. ],
  715. });
  716. const submit2 = async (formEl) => {
  717. await formEl.validate(async (valid, fields) => {
  718. if (valid) {
  719. formEdit2.value.appId = selectData.value[0].appId
  720. formEdit2.value.ditchId = selectData.value[0].ditchId
  721. formEdit2.value.userIds = selectData.value.map((item) => item.userId).join(",")
  722. // 提交内容
  723. await batchAudit({ ...formEdit2.value }).then((res) => {
  724. layer2.value.show = false
  725. getList().then(() => {
  726. ElMessage.success('审核提交成功,如未更新状态,请手动刷新')
  727. })
  728. })
  729. } else {
  730. console.log("error submit!", fields);
  731. }
  732. })
  733. }
  734. // #endregion
  735. // #region 数据导出
  736. // 批量导出
  737. const exportUserList = async () => {
  738. if (!selectData.value || selectData.value.length === 0) {
  739. ElMessage.warning('没有可导出的数据')
  740. return;
  741. }
  742. exportToExcel(getData(selectData.value), tableHeader, 20, '用户列表数据', true).then(() => {
  743. ElMessage.success('导出成功')
  744. })
  745. .catch(() => {
  746. ElMessage.error('导出失败,请重试')
  747. })
  748. }
  749. // 全部导出
  750. const allExportUserList = async () => {
  751. // 请求数据
  752. try {
  753. openFullScreen('数据导出中,请勿操作!!!')
  754. const params = formSearch.value
  755. params.page = 1
  756. params.limit = page.total - 1
  757. const res = await getUserList({ ...params })
  758. if (res.data.length > 0) {
  759. exportToExcel(getData(res.data), tableHeader, 20, '用户列表数据', true).then(() => {
  760. ElMessage.success('导出成功')
  761. })
  762. .catch(() => {
  763. ElMessage.error('导出失败,请重试')
  764. })
  765. }
  766. } catch (e) {
  767. console.log('数据请求超时或报错', e)
  768. } finally {
  769. closeFullScreen()
  770. }
  771. }
  772. // 处理数据
  773. const getData = (data) => {
  774. return data.map((item) => ({
  775. userId: item.userId,
  776. nickName: item.nickName,
  777. userStatus: getDictionaryName('user_status', Number(item.userStatus)),
  778. ditchName: item.ditchName,
  779. todayVideo: item.todayVideo,
  780. totalVideo: item.totalVideo,
  781. todayIncome: `${roundPrice(item.todayIncome === 0 ? '0.000' : item.todayIncome ?? '0.000', 3)}`,
  782. totalIncome: `${roundPrice(item.totalIncome === 0 ? '0.000' : item.totalIncome ?? '0.000', 3)}`,
  783. communicationOperator: item.communicationOperator,
  784. loginRecordList: item.loginRecordList?.length
  785. ? `${item.loginRecordList[0].deviceBrand} ${item.loginRecordList[0].deviceModel}`
  786. : "",
  787. deviceRepeatCount: item.deviceRepeatCount,
  788. ipRepeatCount: item.ipRepeatCount,
  789. lastLoginIp: item.lastLoginIp,
  790. lastLoginTime: convertUTCToBeijing(item.lastLoginTime),
  791. loginDays: item.loginDays,
  792. registryTime: convertUTCToBeijing(item.registryTime),
  793. }))
  794. }
  795. // 表头
  796. const tableHeader = {
  797. "userId": "用户ID",
  798. "nickName": "用户昵称",
  799. "userStatus": "用户状态",
  800. "ditchName": "渠道来源",
  801. "todayVideo": "当日视频播放数",
  802. "totalVideo": "视频播放总数",
  803. "todayIncome": "用户当日贡献",
  804. "totalIncome": "用户总贡献",
  805. "communicationOperator": "通信运营商",
  806. "loginRecordList": "用户设备",
  807. "deviceRepeatCount": "设备重复数量",
  808. "ipRepeatCount": "IP重复数量",
  809. "lastLoginIp": "最新登录IP",
  810. "lastLoginTime": "最新登录时间",
  811. "loginDays": "登录天数",
  812. "registryTime": "注册时间",
  813. }
  814. // #endregion
  815. </script>
  816. <style scoped lang="scss">
  817. .layout-container {
  818. .card {
  819. .title {
  820. margin-bottom: 10px;
  821. font-weight: 600;
  822. }
  823. display: flex;
  824. flex-direction: column;
  825. align-items: start;
  826. width: calc(100% - 60px);
  827. margin: 30px 30px 0;
  828. }
  829. .button {
  830. display: flex;
  831. //flex-direction: column;
  832. .button-item {
  833. margin: 4px;
  834. }
  835. }
  836. }
  837. .btn {
  838. display: flex;
  839. margin: 10px 0 -10px 30px;
  840. }
  841. .full-icon {
  842. position: absolute;
  843. right: 40px;
  844. top: 15px;
  845. cursor: pointer;
  846. padding: 8px;
  847. border-radius: 5px;
  848. }
  849. </style>