accountList.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629
  1. <template>
  2. <div class="container">
  3. <Breadcrumb :items="['网站管理', '乐跑账号管理']" />
  4. <a-card title="乐跑账号管理">
  5. <a-row>
  6. <a-col :flex="1">
  7. <a-form :model="queryData" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }"
  8. label-align="left">
  9. <a-row :gutter="16">
  10. <a-col :span="6">
  11. <a-form-item field="area" label="跑区">
  12. <a-select v-model="queryData.area" placeholder="请选择乐跑跑区" default-value="">
  13. <a-option value="">所有</a-option>
  14. <a-option v-for="(item, index) in area" :key="index" :value="item">
  15. {{ item }}
  16. </a-option>
  17. </a-select>
  18. </a-form-item>
  19. </a-col>
  20. <a-col :span="6">
  21. <a-form-item field="user_uuid" label="创建人UUID">
  22. <a-input v-model="queryData.user_uuid" allow-clear />
  23. </a-form-item>
  24. </a-col>
  25. <a-col :span="6">
  26. <a-form-item field="email" label="用户邮箱">
  27. <a-input v-model="queryData.email" allow-clear />
  28. </a-form-item>
  29. </a-col>
  30. <a-col :span="6">
  31. <a-form-item field="username" label="账号名称">
  32. <a-input v-model="queryData.username" allow-clear />
  33. </a-form-item>
  34. </a-col>
  35. <a-col :span="6">
  36. <a-form-item field="student_num" label="学号">
  37. <a-input v-model="queryData.student_num" allow-clear />
  38. </a-form-item>
  39. </a-col>
  40. <a-col :span="6">
  41. <a-form-item field="state" label="账号状态">
  42. <a-select v-model="queryData.state" :options="state" placeholder="请选择账号状态"
  43. :default-value="-1" />
  44. </a-form-item>
  45. </a-col>
  46. <a-col :span="6">
  47. <a-form-item field="auto_time" label="乐跑时段">
  48. <a-select v-model="queryData.auto_time" placeholder="请选择自动乐跑时段" :default-value="0">
  49. <a-option :value="0">所有时段</a-option>
  50. <a-option v-for="(item, index) in auto_time" :key="index" :value="item.value">
  51. {{ item.label }}
  52. </a-option>
  53. </a-select>
  54. </a-form-item>
  55. </a-col>
  56. <a-col :span="6">
  57. <a-form-item field="queryTime" label="更新时间">
  58. <a-range-picker v-model="queryData.queryTime" show-time format="YY-MM-DD HH:mm"
  59. value-format="x" @ok="search()" />
  60. </a-form-item>
  61. </a-col>
  62. </a-row>
  63. </a-form>
  64. </a-col>
  65. <a-divider style="height: 84px" direction="vertical" />
  66. <a-col :flex="'86px'" style="text-align: right">
  67. <a-space direction="vertical" :size="18">
  68. <a-button type="primary" @click="search">
  69. <template #icon>
  70. <icon-search />
  71. </template>
  72. 搜索
  73. </a-button>
  74. <a-button @click="reset">
  75. <template #icon>
  76. <icon-refresh />
  77. </template>
  78. 重置
  79. </a-button>
  80. </a-space>
  81. </a-col>
  82. </a-row>
  83. <a-table :data="data" :bordered="false" class="table" :loading="loading" :columns="columns" :pagination="{
  84. showPageSize: true,
  85. showJumper: true,
  86. showTotal: true,
  87. pageSize: pagination.pagesize,
  88. current: pagination.current,
  89. total: pagination.total
  90. }" @page-change="handlePageChange" @page-size-change="handlePageSizeChange" >
  91. <template #create_user="{ record }">
  92. <a-avatar :size="30">
  93. <img :alt="record.create_user ?? ''"
  94. :src="record.avatar || 'https://lepao-cloud.xxoo365.top/view.php/25aa126dc406974ff3579a99a2c6501a.png'" />
  95. </a-avatar>
  96. {{ record.create_user }}
  97. </template>
  98. <template #name="{ record }">
  99. <a-avatar :size="30">
  100. <img :alt="record.name ?? ''"
  101. :src="record.user_avatar || 'https://lepao-cloud.xxoo365.top/view.php/25aa126dc406974ff3579a99a2c6501a.png'" />
  102. </a-avatar>
  103. {{ record.name }}
  104. </template>
  105. <template #sex="{ record }">
  106. <icon-man v-if="record.sex === 1" />
  107. <icon-woman v-else-if="record.sex === 2" />
  108. {{ record.sex === 1 ? '男' : (record.sex === 2 ? '女' : '') }}
  109. </template>
  110. <template #auto_run="{ record }">
  111. <a-tag color="green" v-if="record.auto_run">{{ record.target_count === 0 ? '开启-∞次' :
  112. `开启-${record.target_count}次` }}</a-tag>
  113. <a-tag color="red" v-else>关闭</a-tag>
  114. </template>
  115. <template #auto_day="{ record }">
  116. <span v-if="record.auto_run && record.auto_day && record.auto_day.length > 0">
  117. {{record.auto_day.slice().sort((a, b) => {
  118. if (a === 0) return 1; if (b === 0) return -1; return a - b;
  119. }).map(day => auto_day.find(item => item.value === day)?.label).join(',').replace(/周/g, '')}}
  120. </span>
  121. <span v-else>-</span>
  122. </template>
  123. <template #num="{ record }">
  124. {{ record.term_num != record.total_num ? `${record.total_num} / ${record.term_num}` :
  125. '已完成' }}
  126. </template>
  127. <template #state="{ record }">
  128. <div v-if="record.state === 0" class="state">
  129. <div class="circle zero"></div>需登录
  130. </div>
  131. <div v-else-if="record.state === 1" class="state">
  132. <div class="circle one"></div>正常
  133. </div>
  134. <div v-else class="state">
  135. <div class="circle else"></div>状态异常
  136. </div>
  137. </template>
  138. <template #face_state="{ record }">
  139. <div v-if="record.face_state === 0" class="state">
  140. <div class="circle zero"></div>未采集
  141. </div>
  142. <div v-else-if="record.face_state === 1" class="state">
  143. <div class="circle one"></div>已通过
  144. </div>
  145. <div v-else class="state">
  146. <div class="circle else"></div>不通过
  147. </div>
  148. </template>
  149. <template #auto_time="{ record }">
  150. {{ autoTimeLabel(record) }}
  151. </template>
  152. <template #create_time="{ record }">
  153. {{ stramptoTime(record.create_time) }}
  154. </template>
  155. <template #update_time="{ record }">
  156. {{ record.update_time ? stramptoTime(record.update_time) : '待登录' }}
  157. </template>
  158. <template #optional="{ record }">
  159. <a-dropdown :popup-max-height="false" trigger="hover">
  160. <a-button>操作 <icon-down /></a-button>
  161. <template #content>
  162. <a-doption @click="editAccount(record)"><icon-edit /> 编辑账号</a-doption>
  163. <a-doption @click="faceRecoRef.openModal(record)"><icon-video-camera /> 人脸采集</a-doption>
  164. <a-doption @click="SingleRun(record)"><icon-play-circle /> 开始单次乐跑</a-doption>
  165. <a-doption @click="ChangeAutoRun(record)"><icon-translate /> {{ record.auto_run ? '关闭' :
  166. '开启' }}自动乐跑</a-doption>
  167. <a-doption @click="DeleteAccount(record)"><icon-minus-circle /> 解绑账号</a-doption>
  168. </template>
  169. </a-dropdown>
  170. </template>
  171. </a-table>
  172. </a-card>
  173. </div>
  174. <!-- 账号编辑对话框 -->
  175. <a-modal v-model:visible="visible" title="编辑账号" @cancel="handleCancel" @before-ok="handleBeforeOk" draggable
  176. :ok-loading="ok_loading" esc-to-close closable>
  177. <a-form :model="form">
  178. <a-form-item field="student_num" label="学号">
  179. <a-input v-model="form.student_num" placeholder="账号所有者学号,填写错误将无法登录" />
  180. </a-form-item>
  181. <a-form-item field="email" label="通知邮箱">
  182. <a-input v-model="form.email" placeholder="用于接收乐跑失败、登录失效的通知" />
  183. </a-form-item>
  184. <a-form-item field="area" label="乐跑跑区">
  185. <a-select v-model="form.area" placeholder="请选择乐跑跑区" default-value="">
  186. <a-option value="">随机分配</a-option>
  187. <a-option v-for="(item, index) in area" :key="index" :value="item">
  188. <span class="vipcontent">
  189. <span>{{ item }} </span>
  190. </span>
  191. </a-option>
  192. </a-select>
  193. </a-form-item>
  194. <a-form-item field="auto_run" label="自动乐跑开关">
  195. <a-switch v-model="form.auto_run" :checked-value="1" :unchecked-value="0" />
  196. </a-form-item>
  197. <a-form-item field="target_count" label="乐跑目标次数" v-if="form.auto_run">
  198. <a-input-number v-model="form.target_count" placeholder="请输入乐跑目标次数" mode="button" />
  199. <template #extra>
  200. <div>当学期有效次数达到目标次数时自动乐跑将关闭,0为不限次</div>
  201. </template>
  202. </a-form-item>
  203. <a-form-item field="auto_day" label="自动乐跑星期" v-if="form.auto_run">
  204. <a-checkbox-group v-model="form.auto_day" placeholder="请选择每天自动乐跑的星期" :options="auto_day" />
  205. </a-form-item>
  206. <a-form-item field="auto_time" label="自动乐跑时段" v-if="form.auto_run">
  207. <a-select v-model="form.auto_time" placeholder="请选择每天自动乐跑的时段" :options="auto_time" />
  208. </a-form-item>
  209. <a-form-item field="notes" label="备注">
  210. <a-textarea v-model="form.notes" placeholder="添加对账号的备注(非必填)" :max-length="{ length: 50 }" allow-clear
  211. show-word-limit />
  212. </a-form-item>
  213. </a-form>
  214. </a-modal>
  215. <faceModal :faceInfo="faceInfo" ref="faceRecoRef" />
  216. </template>
  217. <script setup>
  218. import { ref, reactive, onMounted, h } from 'vue'
  219. import { addAccount, adminAccountList, deleteAccount, changeAutoRun, singleRun } from '@/api/lepao'
  220. import { Modal, Notification, Message } from '@arco-design/web-vue'
  221. import faceModal from '@/components/FaceModal/faceModal.vue'
  222. import { getSemesterTimestamps } from '@/utils/util'
  223. const faceRecoRef = ref(null)
  224. const queryData = reactive({
  225. area: '',
  226. student_num: '',
  227. user_uuid: '',
  228. email: '',
  229. username: '',
  230. state: -1,
  231. auto_time: 0,
  232. queryTime: []
  233. })
  234. const pagination = reactive({
  235. total: 0,
  236. current: 1, // 默认从第1页开始
  237. pagesize: 20
  238. })
  239. const visible = ref(false)
  240. const ok_loading = ref(false)
  241. const form = reactive({
  242. id: null,
  243. student_num: '',
  244. email: '',
  245. area: '',
  246. auto_run: 1,
  247. auto_time: -1,
  248. target_count: 30,
  249. auto_day: [0, 1, 2, 3, 4, 5, 6],
  250. notes: ''
  251. })
  252. const loading = ref(false)
  253. const data = ref([])
  254. const auto_day = [
  255. { label: '周一', value: 1 },
  256. { label: '周二', value: 2 },
  257. { label: '周三', value: 3 },
  258. { label: '周四', value: 4 },
  259. { label: '周五', value: 5 },
  260. { label: '周六', value: 6 },
  261. { label: '周日', value: 0 }
  262. ]
  263. const state = [
  264. { label: '全部', value: -1 }, { label: '需登录', value: 0 }, { label: '正常', value: 1 }, { label: '状态异常', value: 2 }
  265. ]
  266. const area = ["兰花湖校区跑区", "主校区北跑区", "主校区南跑区", "重庆工商大学茶园校区"]
  267. const columns = [
  268. {
  269. title: 'ID',
  270. dataIndex: 'id',
  271. fixed: 'left',
  272. width: 65
  273. }, {
  274. title: '创建用户',
  275. slotName: 'create_user',
  276. width: 160
  277. }, {
  278. title: '账号名称',
  279. slotName: 'name',
  280. width: 180
  281. }, {
  282. title: '学号',
  283. dataIndex: 'student_num',
  284. width: 115
  285. }, {
  286. title: '性别',
  287. slotName: 'sex',
  288. width: 80
  289. }, {
  290. title: '学院',
  291. dataIndex: 'academy_name',
  292. width: 200
  293. }, {
  294. title: '年级',
  295. dataIndex: 'grade_id',
  296. width: 80
  297. }, {
  298. title: '通知邮箱',
  299. dataIndex: 'email',
  300. width: 200
  301. },
  302. {
  303. title: '乐跑跑区',
  304. dataIndex: 'area',
  305. width: 150
  306. }, {
  307. title: '自动乐跑',
  308. slotName: 'auto_run',
  309. width: 90
  310. }, {
  311. title: '自动乐跑星期',
  312. slotName: 'auto_day',
  313. width: 155
  314. }, {
  315. title: '自动乐跑时段',
  316. slotName: 'auto_time',
  317. width: 130
  318. }, {
  319. title: '学期目标',
  320. slotName: 'num',
  321. width: 90
  322. }, {
  323. title: '帐号状态',
  324. slotName: 'state',
  325. width: 100
  326. }, {
  327. title: '手机型号',
  328. dataIndex: 'deviceModel',
  329. width: 100
  330. }, {
  331. title: 'UA',
  332. dataIndex: 'userAgent',
  333. width: 120,
  334. ellipsis: true,
  335. tooltip: true
  336. }, {
  337. title: '人脸状态',
  338. slotName: 'face_state',
  339. width: 100
  340. }, {
  341. title: '添加时间',
  342. slotName: 'create_time',
  343. width: 150
  344. }, {
  345. title: '上次更新时间',
  346. slotName: 'update_time',
  347. width: 150
  348. }, {
  349. title: '备注',
  350. dataIndex: 'notes',
  351. width: 200
  352. }, {
  353. title: '操作',
  354. slotName: 'optional',
  355. fixed: 'right',
  356. width: 90
  357. }]
  358. const search = () => {
  359. pagination.current = 1
  360. getAccounts()
  361. }
  362. const reset = () => {
  363. queryData.area = ''
  364. queryData.student_num = ''
  365. queryData.user_uuid = ''
  366. queryData.email = ''
  367. queryData.username = ''
  368. queryData.state = -1
  369. queryData.auto_time = 0
  370. queryData.queryTime = getSemesterTimestamps()
  371. getAccounts()
  372. }
  373. const getAccounts = async () => {
  374. try {
  375. loading.value = true
  376. const reqData = {
  377. ...queryData,
  378. pagesize: pagination.pagesize,
  379. current: pagination.current
  380. }
  381. const res = await adminAccountList(reqData)
  382. if (!res || res.code !== 0)
  383. return Notification.error({
  384. title: '获取账号数据失败!',
  385. content: res?.msg ?? '请稍后再试'
  386. })
  387. data.value = res.data
  388. pagination.total = res.pagination.total
  389. } catch (error) {
  390. Notification.error({
  391. title: '获取账号数据失败!',
  392. content: error.message || '请稍后再试'
  393. })
  394. } finally {
  395. loading.value = false
  396. }
  397. }
  398. const editAccount = (item) => {
  399. if (item) {
  400. form.id = item.id
  401. form.student_num = item.student_num
  402. form.email = item.email
  403. form.area = item.area
  404. form.auto_run = item.auto_run
  405. form.auto_time = item.auto_time
  406. form.auto_day = item.auto_day
  407. form.target_count = item.target_count
  408. form.notes = item.notes
  409. } else {
  410. form.id = null
  411. form.student_num = ''
  412. form.auto_run = 1
  413. form.auto_time = 7
  414. form.target_count = 30
  415. form.auto_day = [0, 1, 2, 3, 4, 5, 6]
  416. form.email = email.value
  417. form.notes = ''
  418. }
  419. visible.value = true
  420. }
  421. const handleBeforeOk = async (done) => {
  422. try {
  423. ok_loading.value = true
  424. const { student_num, email } = form
  425. if (!student_num || !email) {
  426. Message.error('请填写完整的账号信息')
  427. return false
  428. }
  429. const studentNumRegex = /^\d{10}$/
  430. if (!studentNumRegex.test(student_num)) {
  431. Message.error('请检查学号格式是否正确')
  432. return false
  433. }
  434. const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
  435. if (!emailRegex.test(email)) {
  436. Message.error('请检查邮箱格式是否正确')
  437. return false
  438. }
  439. let data = {
  440. ...form
  441. }
  442. const res = await addAccount(data)
  443. if (!res || res.code !== 0) {
  444. Notification.error({
  445. title: '保存乐跑账号失败!',
  446. content: res?.msg ?? '请稍后再试'
  447. })
  448. return false
  449. }
  450. Message.success('保存成功!')
  451. done()
  452. getAccounts()
  453. } catch (error) {
  454. Notification.error({
  455. title: '保存乐跑账号失败!',
  456. content: error.message || '请稍后再试'
  457. })
  458. return false
  459. } finally {
  460. ok_loading.value = false
  461. }
  462. }
  463. const handleCancel = () => {
  464. visible.value = false;
  465. }
  466. // 分页 - 页码变化
  467. const handlePageChange = (page) => {
  468. pagination.current = page
  469. getAccounts()
  470. }
  471. // 分页 - 每页条数变化
  472. const handlePageSizeChange = (size) => {
  473. pagination.pagesize = size
  474. pagination.current = 1 // 页大小变化后回到第一页
  475. getAccounts()
  476. }
  477. const DeleteAccount = async (item) => {
  478. Modal.confirm({
  479. title: '解绑账号',
  480. content: () => h('div', [
  481. h('p', '您是否要解绑该账号?该操作不可逆')
  482. ]),
  483. onOk: async () => {
  484. const res = await deleteAccount({ id: item.id })
  485. if (!res || res.code !== 0)
  486. return Notification.error({
  487. title: '解绑失败',
  488. content: res?.msg ?? '请稍后再试'
  489. })
  490. Message.success('解绑成功!')
  491. getAccounts()
  492. }
  493. })
  494. }
  495. const ChangeAutoRun = async (record) => {
  496. const oldValue = record.auto_run;
  497. record.auto_run = oldValue === 1 ? 0 : 1;
  498. try {
  499. const res = await changeAutoRun({ id: record.id });
  500. if (!res || res.code !== 0) {
  501. record.auto_run = oldValue;
  502. Notification.error({
  503. title: '切换自动乐跑状态失败!',
  504. content: res?.msg ?? '请稍后再试'
  505. })
  506. } else {
  507. Message.success(`自动乐跑状态已${record.auto_run === 1 ? '开启' : '关闭'}`)
  508. getAccounts()
  509. }
  510. } catch (error) {
  511. console.log(error)
  512. record.auto_run = oldValue
  513. Message.error('切换自动乐跑状态失败!')
  514. }
  515. }
  516. const SingleRun = async (item) => {
  517. if (item.state !== 1)
  518. return Notification.warning({
  519. title: '当前乐跑账号需登录,请登录后再试',
  520. content: '如有疑问请联系客服'
  521. })
  522. Modal.confirm({
  523. title: '开始乐跑',
  524. content: () => h('div', [
  525. h('p', `您是否要为${item.name}(${item.student_num})乐跑?若乐跑成功将扣减乐跑次数`)
  526. ]),
  527. onOk: async () => {
  528. const res = await singleRun({ student_num: item.student_num })
  529. if (!res || res.code !== 0)
  530. return Notification.error({
  531. title: '提交乐跑任务失败',
  532. content: res?.msg ?? '请稍后再试'
  533. })
  534. Message.success('提交乐跑任务成功!')
  535. }
  536. })
  537. }
  538. onMounted(() => {
  539. queryData.queryTime = getSemesterTimestamps()
  540. getAccounts()
  541. })
  542. const stramptoTime = (time) => {
  543. return new Date(time).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
  544. }
  545. const auto_time = [
  546. { label: '随机', value: -1 },
  547. ...Array.from({ length: 17 }, (_, i) => {
  548. const hour = i + 7
  549. return { label: `${hour} ~ ${hour + 1}时`, value: hour }
  550. })
  551. ]
  552. const autoTimeLabel = (record) => {
  553. if (record.auto_time === -1 && record.today_auto_time) {
  554. return `随机-今日${record.today_auto_time}时`
  555. }
  556. const match = auto_time.find(item => item.value === record.auto_time)
  557. return match ? match.label : '-'
  558. }
  559. </script>
  560. <style scoped>
  561. .container {
  562. padding: 0 20px 20px 20px;
  563. }
  564. .table {
  565. font-family: -apple-system, BlinkMacSystemFont;
  566. margin-top: 15px;
  567. .state {
  568. display: flex;
  569. align-items: center;
  570. .circle {
  571. border-radius: 50%;
  572. height: 8px;
  573. min-height: 8px;
  574. width: 8px;
  575. min-width: 8px;
  576. margin-right: 5px;
  577. }
  578. .zero {
  579. background-color: rgb(var(--orange-6));
  580. }
  581. .one {
  582. background-color: rgb(var(--green-6));
  583. }
  584. .else {
  585. background-color: rgb(var(--red-6));
  586. }
  587. }
  588. }
  589. </style>