accountList.vue 20 KB

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