userList.vue 26 KB

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