accountList.vue 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918
  1. <template>
  2. <div class="store-page">
  3. <Breadcrumb />
  4. <a-card title="乐跑账号管理">
  5. <a-row class="query-row">
  6. <a-col :flex="'1000px'" class="query-main">
  7. <a-form class="queryForm app-query-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. <EmailAutoComplete 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="notes" label="备注">
  37. <a-input v-model="queryData.notes" allow-clear />
  38. </a-form-item>
  39. </a-col>
  40. <a-col :span="6">
  41. <a-form-item field="student_num" label="学号">
  42. <a-input v-model="queryData.student_num" allow-clear />
  43. </a-form-item>
  44. </a-col>
  45. <a-col :span="6">
  46. <a-form-item field="bind_code" label="绑定码">
  47. <a-input v-model="queryData.bind_code" allow-clear />
  48. </a-form-item>
  49. </a-col>
  50. <a-col :span="6">
  51. <a-form-item field="bot_account" label="机器人账号">
  52. <a-input v-model="queryData.bot_account" allow-clear />
  53. </a-form-item>
  54. </a-col>
  55. <a-col :span="6">
  56. <a-form-item field="state" label="账号状态">
  57. <a-select v-model="queryData.state" :options="state" placeholder="请选择账号状态"
  58. :default-value="-1" />
  59. </a-form-item>
  60. </a-col>
  61. <a-col :span="6">
  62. <a-form-item field="auto_time" label="乐跑时段">
  63. <a-select v-model="queryData.auto_time" placeholder="请选择自动乐跑时段" :default-value="0">
  64. <a-option :value="0">所有时段</a-option>
  65. <a-option v-for="(item, index) in auto_time" :key="index" :value="item.value">
  66. {{ item.label }}
  67. </a-option>
  68. </a-select>
  69. </a-form-item>
  70. </a-col>
  71. <a-col :span="6">
  72. <a-form-item field="queryTime" label="更新时间">
  73. <a-range-picker v-model="queryData.queryTime" show-time format="YY-MM-DD HH:mm"
  74. value-format="x" @ok="search()" />
  75. </a-form-item>
  76. </a-col>
  77. </a-row>
  78. </a-form>
  79. </a-col>
  80. <a-divider class="query-divider" style="height: 84px" direction="vertical" />
  81. <a-col :flex="1" class="app-query-actions">
  82. <a-space class="query-actions-space" direction="vertical" :size="18">
  83. <a-button type="primary" @click="search">
  84. <template #icon>
  85. <icon-search />
  86. </template>
  87. 搜索
  88. </a-button>
  89. <a-button @click="reset">
  90. <template #icon>
  91. <icon-refresh />
  92. </template>
  93. 重置
  94. </a-button>
  95. </a-space>
  96. </a-col>
  97. </a-row>
  98. <a-table :data="data" :bordered="false" hoverable class="table table-clickable" :loading="loading" :columns="columns" :scroll="{ x: 2850 }" @row-click="onRowClick" :pagination="{
  99. showPageSize: true,
  100. showJumper: true,
  101. showTotal: true,
  102. pageSize: pagination.pagesize,
  103. current: pagination.current,
  104. total: pagination.total
  105. }" @page-change="handlePageChange" @page-size-change="handlePageSizeChange">
  106. <template #create_user="{ record }">
  107. <a-avatar :size="30">
  108. <img :alt="record.create_user ?? ''"
  109. :src="record.avatar || 'https://lepao-cloud.xxoo365.top/view.php/25aa126dc406974ff3579a99a2c6501a.png'" />
  110. </a-avatar>
  111. {{ record.create_user }}
  112. </template>
  113. <template #name="{ record }">
  114. <a-avatar :size="30">
  115. <img :alt="record.name ?? ''"
  116. :src="record.user_avatar || 'https://lepao-cloud.xxoo365.top/view.php/25aa126dc406974ff3579a99a2c6501a.png'" />
  117. </a-avatar>
  118. {{ record.name }}
  119. </template>
  120. <template #sex="{ record }">
  121. <icon-man v-if="record.sex === 1" />
  122. <icon-woman v-else-if="record.sex === 2" />
  123. {{ record.sex === 1 ? '男' : (record.sex === 2 ? '女' : '') }}
  124. </template>
  125. <template #auto_run="{ record }">
  126. <a-tag color="green" v-if="record.auto_run">{{ record.target_count === 0 ? '开启-∞次' :
  127. `开启-${record.target_count}次` }}</a-tag>
  128. <a-tag color="red" v-else>关闭</a-tag>
  129. </template>
  130. <template #notice_type="{ record }">
  131. <span v-if="record.notice_type === 'email'"><icon-email /> {{ record.email ?? '未绑定' }}</span>
  132. <span v-else-if="record.notice_type === 'bot'"><icon-robot /> {{ record.bot_account ?? '未绑定'
  133. }}</span>
  134. <span v-else><icon-notification-close /> 无通知</span>
  135. </template>
  136. <template #auto_day="{ record }">
  137. <span v-if="record.auto_run && record.auto_day && record.auto_day.length > 0">
  138. {{record.auto_day.slice().sort((a, b) => {
  139. if (a === 0) return 1; if (b === 0) return -1; return a - b;
  140. }).map(day => auto_day.find(item => item.value === day)?.label).join(',').replace(/周/g, '')}}
  141. </span>
  142. <span v-else>-</span>
  143. </template>
  144. <template #num="{ record }">
  145. {{ record.term_num != record.total_num ? `${record.total_num} / ${record.term_num}` :
  146. '已完成' }}
  147. </template>
  148. <template #state="{ record }">
  149. <div v-if="record.state === 0" class="state">
  150. <div class="circle zero"></div>需登录
  151. </div>
  152. <div v-else-if="record.state === 1" class="state">
  153. <div class="circle one"></div>正常
  154. </div>
  155. <div v-else class="state">
  156. <div class="circle else"></div>状态异常
  157. </div>
  158. </template>
  159. <template #face_state="{ record }">
  160. <div v-if="record.face_state === 0" class="state">
  161. <div class="circle zero"></div>未采集
  162. </div>
  163. <div v-else-if="record.face_state === 1" class="state">
  164. <div class="circle one"></div>已通过
  165. </div>
  166. <div v-else class="state">
  167. <div class="circle else"></div>不通过
  168. </div>
  169. </template>
  170. <template #auto_time="{ record }">
  171. {{ autoTimeLabel(record) }}
  172. </template>
  173. <template #create_time="{ record }">
  174. {{ stramptoTime(record.create_time) }}
  175. </template>
  176. <template #update_time="{ record }">
  177. {{ record.update_time ? stramptoTime(record.update_time) : '待登录' }}
  178. </template>
  179. <template #optional="{ record }">
  180. <a-dropdown :popup-max-height="false" trigger="hover" @click.stop>
  181. <a-button>操作 <icon-down /></a-button>
  182. <template #content>
  183. <a-doption v-if="hasPermission('action.lepao.admin.updateAccount')" @click="editAccount(record)"><icon-edit /> 编辑账号</a-doption>
  184. <a-doption @click="faceRecoRef.openModal(record)"><icon-video-camera /> 人脸采集</a-doption>
  185. <a-doption @click="bindBotRef.openModal(record)"><icon-robot-add /> 绑定智能机器人</a-doption>
  186. <a-doption @click="openBindAudit(record)"><icon-history /> 绑定解绑记录</a-doption>
  187. <a-doption v-if="hasPermission('action.lepao.singleRun')" @click="SingleRun(record)"><icon-play-circle /> 开始单次乐跑</a-doption>
  188. <a-doption v-if="hasPermission('action.lepao.changeAutoRun')" @click="ChangeAutoRun(record)"><icon-translate /> {{ record.auto_run ? '关闭' :
  189. '开启' }}自动乐跑</a-doption>
  190. <a-doption v-if="hasPermission('action.lepao.admin.updateAccount')" @click="UpdateAccountInfo(record)"><icon-refresh /> 更新账号信息</a-doption>
  191. <a-doption v-if="hasPermission('action.lepao.deleteAccount')" @click="DeleteAccount(record)"><icon-minus-circle /> 解绑账号</a-doption>
  192. </template>
  193. </a-dropdown>
  194. </template>
  195. </a-table>
  196. </a-card>
  197. </div>
  198. <!-- 账号编辑对话框 -->
  199. <a-modal v-model:visible="visible" title="编辑账号" @cancel="handleCancel" @before-ok="handleBeforeOk" draggable
  200. :ok-loading="ok_loading" esc-to-close closable>
  201. <a-form :model="form">
  202. <a-form-item field="student_num" label="学号">
  203. <a-input v-model="form.student_num" placeholder="账号所有者学号,填写错误将无法登录" />
  204. </a-form-item>
  205. <a-form-item field="notice_type" label="通知方式">
  206. <a-radio-group v-model="form.notice_type">
  207. <a-radio value="email"><icon-email /> 邮件</a-radio>
  208. <a-radio value="bot"><icon-robot /> 智能机器人</a-radio>
  209. <a-radio value="none"><icon-notification-close /> 无通知</a-radio>
  210. </a-radio-group>
  211. </a-form-item>
  212. <a-form-item field="email" label="通知邮箱" v-if="form.notice_type === 'email'">
  213. <EmailAutoComplete v-model="form.email" placeholder="用于接收乐跑失败、登录失效的通知" allow-clear />
  214. </a-form-item>
  215. <a-form-item field="area" label="乐跑跑区">
  216. <a-select v-model="form.area" placeholder="请选择乐跑跑区" default-value="">
  217. <a-option value="">随机分配</a-option>
  218. <a-option v-for="(item, index) in area" :key="index" :value="item">
  219. <span class="vipcontent">
  220. <span>{{ item }} </span>
  221. </span>
  222. </a-option>
  223. </a-select>
  224. </a-form-item>
  225. <a-form-item field="auto_run" label="自动乐跑开关">
  226. <a-switch v-model="form.auto_run" :checked-value="1" :unchecked-value="0" />
  227. </a-form-item>
  228. <a-form-item field="target_count" label="乐跑目标次数" v-if="form.auto_run">
  229. <a-input-number v-model="form.target_count" placeholder="请输入乐跑目标次数" mode="button" />
  230. <template #extra>
  231. <div>当学期有效次数达到目标次数时自动乐跑将关闭,0为不限次</div>
  232. </template>
  233. </a-form-item>
  234. <a-form-item field="auto_day" label="自动乐跑星期" v-if="form.auto_run">
  235. <a-checkbox-group v-model="form.auto_day" placeholder="请选择每天自动乐跑的星期" :options="auto_day" />
  236. </a-form-item>
  237. <a-form-item field="auto_time" label="自动乐跑时段" v-if="form.auto_run">
  238. <a-select v-model="form.auto_time" placeholder="请选择每天自动乐跑的时段" :options="auto_time" />
  239. </a-form-item>
  240. <a-form-item field="notes" label="备注">
  241. <a-textarea v-model="form.notes" no-trim placeholder="添加对账号的备注(非必填)" :max-length="{ length: 50 }" allow-clear
  242. show-word-limit />
  243. </a-form-item>
  244. </a-form>
  245. </a-modal>
  246. <faceModal :faceInfo="faceInfo" ref="faceRecoRef" />
  247. <bindBot ref="bindBotRef" />
  248. <accountDetailCard ref="accountDetailRef" admin />
  249. <a-modal v-model:visible="bindAuditVisible" title="绑定解绑记录" :footer="false" width="980px" draggable>
  250. <a-table :bordered="false" :data="bindAuditData" :columns="bindAuditColumns" :loading="bindAuditLoading" :pagination="false"
  251. :scroll="{ x: 1450, y: 420 }">
  252. <template #lepao_user="{ record }">
  253. <a-space>
  254. <a-avatar :size="26">
  255. <img :src="record.lepao_avatar || 'https://lepao-cloud.xxoo365.top/view.php/25aa126dc406974ff3579a99a2c6501a.png'" />
  256. </a-avatar>
  257. {{ record.lepao_name || '-' }}
  258. </a-space>
  259. </template>
  260. <template #owner_user="{ record }">
  261. <a-space>
  262. <a-avatar :size="26">
  263. <img :src="record.owner_avatar || 'https://lepao-cloud.xxoo365.top/view.php/25aa126dc406974ff3579a99a2c6501a.png'" />
  264. </a-avatar>
  265. {{ record.owner_username || '-' }}
  266. </a-space>
  267. </template>
  268. <template #action="{ record }">
  269. <a-tag>{{ actionLabel(record.action) }}</a-tag>
  270. </template>
  271. <template #source="{ record }">
  272. {{ sourceLabel(record.source) }}
  273. </template>
  274. <template #operator_user="{ record }">
  275. <a-space>
  276. <a-avatar :size="26">
  277. <img :src="record.operator_avatar || 'https://lepao-cloud.xxoo365.top/view.php/25aa126dc406974ff3579a99a2c6501a.png'" />
  278. </a-avatar>
  279. {{ record.operator_username || '-' }}
  280. </a-space>
  281. </template>
  282. <template #detail_json="{ record }">
  283. {{ formatDetail(record.detail_json) }}
  284. </template>
  285. <template #created_at="{ record }">
  286. {{ stramptoTime(record.created_at) }}
  287. </template>
  288. <template #empty>
  289. <a-empty description="暂无绑定解绑记录" />
  290. </template>
  291. </a-table>
  292. </a-modal>
  293. </template>
  294. <script setup>
  295. import { ref, reactive, onMounted, onUnmounted, h } from 'vue'
  296. import { addAccount, adminAccountList, deleteAccount, changeAutoRun, singleRun, adminUpdateAccountInfo, getAdminBindAuditByAccount, getAdminBindAuditList } from '@/api/lepao'
  297. import { Modal, Notification, Message } from '@arco-design/web-vue'
  298. import faceModal from '@/components/FaceModal/faceModal.vue'
  299. import bindBot from '@/components/BindBot/bindBot.vue'
  300. import accountDetailCard from '@/components/LepaoAccountCard/accountDetailCard.vue'
  301. import { getSemesterTimestamps } from '@/utils/util'
  302. import { hasPermission } from '@/utils/permission'
  303. const faceRecoRef = ref(null)
  304. const bindBotRef = ref(null)
  305. const accountDetailRef = ref(null)
  306. const ROW_DETAIL_CONFIRM_MS = 3000
  307. const pendingDetailAccountId = ref(null)
  308. let pendingDetailTimer = null
  309. const clearPendingDetail = () => {
  310. pendingDetailAccountId.value = null
  311. if (pendingDetailTimer) {
  312. clearTimeout(pendingDetailTimer)
  313. pendingDetailTimer = null
  314. }
  315. }
  316. const bindAuditVisible = ref(false)
  317. const bindAuditLoading = ref(false)
  318. const bindAuditData = ref([])
  319. const queryData = reactive({
  320. area: '',
  321. student_num: '',
  322. user_uuid: '',
  323. email: '',
  324. username: '',
  325. notes: '',
  326. bind_code: '',
  327. bot_account: '',
  328. state: -1,
  329. auto_time: 0,
  330. queryTime: []
  331. })
  332. const pagination = reactive({
  333. total: 0,
  334. current: 1, // 默认从第1页开始
  335. pagesize: 20
  336. })
  337. const visible = ref(false)
  338. const ok_loading = ref(false)
  339. const form = reactive({
  340. id: null,
  341. student_num: '',
  342. email: '',
  343. area: '',
  344. notice_type: 'email',
  345. auto_run: 1,
  346. auto_time: -1,
  347. target_count: 30,
  348. auto_day: [0, 1, 2, 3, 4, 5, 6],
  349. notes: ''
  350. })
  351. const loading = ref(false)
  352. const data = ref([])
  353. const auto_day = [
  354. { label: '周一', value: 1 },
  355. { label: '周二', value: 2 },
  356. { label: '周三', value: 3 },
  357. { label: '周四', value: 4 },
  358. { label: '周五', value: 5 },
  359. { label: '周六', value: 6 },
  360. { label: '周日', value: 0 }
  361. ]
  362. const state = [
  363. { label: '全部', value: -1 }, { label: '需登录', value: 0 }, { label: '正常', value: 1 }, { label: '状态异常', value: 2 }
  364. ]
  365. const area = ["兰花湖校区跑区", "主校区北跑区", "主校区南跑区", "重庆工商大学茶园校区"]
  366. const columns = [
  367. {
  368. title: 'ID',
  369. dataIndex: 'id',
  370. fixed: 'left',
  371. width: 68
  372. }, {
  373. title: '创建用户',
  374. slotName: 'create_user',
  375. width: 160
  376. }, {
  377. title: '账号名称',
  378. slotName: 'name',
  379. width: 180
  380. }, {
  381. title: '学号',
  382. dataIndex: 'student_num',
  383. width: 118
  384. }, {
  385. title: '性别',
  386. slotName: 'sex',
  387. width: 80
  388. }, {
  389. title: '学院',
  390. dataIndex: 'academy_name',
  391. width: 200
  392. }, {
  393. title: '年级',
  394. dataIndex: 'grade_id',
  395. width: 80
  396. }, {
  397. title: '通知方式',
  398. slotName: 'notice_type',
  399. width: 200
  400. }, {
  401. title: '帐号状态',
  402. slotName: 'state',
  403. width: 100
  404. }, {
  405. title: '乐跑跑区',
  406. dataIndex: 'area',
  407. width: 150
  408. }, {
  409. title: '自动乐跑',
  410. slotName: 'auto_run',
  411. width: 90
  412. }, {
  413. title: '自动乐跑星期',
  414. slotName: 'auto_day',
  415. width: 155
  416. }, {
  417. title: '自动乐跑时段',
  418. slotName: 'auto_time',
  419. width: 130
  420. }, {
  421. title: '学期目标',
  422. slotName: 'num',
  423. width: 90
  424. }, {
  425. title: '手机型号',
  426. dataIndex: 'deviceModel',
  427. width: 100
  428. }, {
  429. title: 'UA',
  430. dataIndex: 'userAgent',
  431. width: 120,
  432. ellipsis: true,
  433. tooltip: true
  434. }, {
  435. title: '人脸状态',
  436. slotName: 'face_state',
  437. width: 100
  438. }, {
  439. title: '添加时间',
  440. slotName: 'create_time',
  441. width: 150
  442. }, {
  443. title: '上次更新时间',
  444. slotName: 'update_time',
  445. width: 150
  446. }, {
  447. title: '备注',
  448. dataIndex: 'notes',
  449. width: 200
  450. }, {
  451. title: '操作',
  452. slotName: 'optional',
  453. fixed: 'right',
  454. width: 90
  455. }]
  456. const bindAuditColumns = [
  457. { title: '学号', dataIndex: 'student_num', width: 140 },
  458. { title: '乐跑账号', slotName: 'lepao_user', width: 180 },
  459. { title: '所属用户', slotName: 'owner_user', width: 180 },
  460. { title: '动作', slotName: 'action', width: 120 },
  461. { title: '来源', slotName: 'source', width: 140 },
  462. { title: '操作者', slotName: 'operator_user', width: 180 },
  463. { title: '详情', slotName: 'detail_json', width: 220, ellipsis: true, tooltip: true },
  464. { title: '时间', slotName: 'created_at', width: 180 }
  465. ]
  466. const search = () => {
  467. pagination.current = 1
  468. getAccounts()
  469. }
  470. const reset = () => {
  471. queryData.area = ''
  472. queryData.student_num = ''
  473. queryData.user_uuid = ''
  474. queryData.email = ''
  475. queryData.username = ''
  476. queryData.notes = ''
  477. queryData.bind_code = ''
  478. queryData.bot_account = ''
  479. queryData.state = -1
  480. queryData.auto_time = 0
  481. queryData.queryTime = getSemesterTimestamps()
  482. getAccounts()
  483. }
  484. const getAccounts = async () => {
  485. try {
  486. loading.value = true
  487. const reqData = {
  488. ...queryData,
  489. pagesize: pagination.pagesize,
  490. current: pagination.current
  491. }
  492. const res = await adminAccountList(reqData)
  493. if (!res || res.code !== 0)
  494. return Notification.error({
  495. title: '获取账号数据失败!',
  496. content: res?.msg ?? '请稍后再试'
  497. })
  498. data.value = res.data
  499. pagination.total = res.pagination.total
  500. } catch (error) {
  501. Notification.error({
  502. title: '获取账号数据失败!',
  503. content: error.message || '请稍后再试'
  504. })
  505. } finally {
  506. loading.value = false
  507. }
  508. }
  509. const onRowClick = (record, ev) => {
  510. if (ev?.target?.closest?.('.arco-dropdown, .arco-btn')) return
  511. const accountId = record?.id
  512. if (accountId == null) return
  513. if (pendingDetailAccountId.value === accountId) {
  514. clearPendingDetail()
  515. accountDetailRef.value?.openModal(record)
  516. return
  517. }
  518. clearPendingDetail()
  519. pendingDetailAccountId.value = accountId
  520. Message.info('再点一次即可查看账号详情')
  521. pendingDetailTimer = setTimeout(clearPendingDetail, ROW_DETAIL_CONFIRM_MS)
  522. }
  523. const editAccount = (item) => {
  524. if (item) {
  525. form.id = item.id
  526. form.student_num = item.student_num
  527. form.email = item.email
  528. form.area = item.area
  529. form.auto_run = item.auto_run
  530. form.auto_time = item.auto_time
  531. form.auto_day = item.auto_day
  532. form.notice_type = item.notice_type || 'email'
  533. form.target_count = item.target_count
  534. form.notes = item.notes
  535. } else {
  536. form.id = null
  537. form.student_num = ''
  538. form.auto_run = 1
  539. form.auto_time = 7
  540. form.target_count = 30
  541. form.auto_day = [0, 1, 2, 3, 4, 5, 6]
  542. form.email = ''
  543. form.notes = ''
  544. }
  545. visible.value = true
  546. }
  547. const handleBeforeOk = async (done) => {
  548. try {
  549. ok_loading.value = true
  550. const { student_num, email, notice_type } = form
  551. if (!student_num || (!email && notice_type === 'email')) {
  552. Message.error('请填写完整的账号信息')
  553. return false
  554. }
  555. const studentNumRegex = /^\d{10}$/
  556. if (!studentNumRegex.test(student_num)) {
  557. Message.error('请检查学号格式是否正确')
  558. return false
  559. }
  560. const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
  561. if (form.notice_type === 'email' && !emailRegex.test(email)) {
  562. Message.error('请检查邮箱格式是否正确')
  563. return false
  564. }
  565. let data = {
  566. ...form
  567. }
  568. const res = await addAccount(data)
  569. if (!res || res.code !== 0) {
  570. Notification.error({
  571. title: '保存乐跑账号失败!',
  572. content: res?.msg ?? '请稍后再试'
  573. })
  574. return false
  575. }
  576. Message.success('保存成功!')
  577. if (form.notice_type === 'bot' && res.data && !res.data.umo) {
  578. bindBotRef.value.openModal(res.data)
  579. }
  580. done()
  581. getAccounts()
  582. } catch (error) {
  583. Notification.error({
  584. title: '保存乐跑账号失败!',
  585. content: error.message || '请稍后再试'
  586. })
  587. return false
  588. } finally {
  589. ok_loading.value = false
  590. }
  591. }
  592. const handleCancel = () => {
  593. visible.value = false;
  594. }
  595. // 分页 - 页码变化
  596. const handlePageChange = (page) => {
  597. pagination.current = page
  598. getAccounts()
  599. }
  600. // 分页 - 每页条数变化
  601. const handlePageSizeChange = (size) => {
  602. pagination.pagesize = size
  603. pagination.current = 1 // 页大小变化后回到第一页
  604. getAccounts()
  605. }
  606. const DeleteAccount = async (item) => {
  607. Modal.confirm({
  608. title: '解绑账号',
  609. content: () => h('div', [
  610. h('p', '您是否要解绑该账号?该操作不可逆')
  611. ]),
  612. onOk: async () => {
  613. const res = await deleteAccount({ id: item.id })
  614. if (!res || res.code !== 0)
  615. return Notification.error({
  616. title: '解绑失败',
  617. content: res?.msg ?? '请稍后再试'
  618. })
  619. Message.success('解绑成功!')
  620. getAccounts()
  621. }
  622. })
  623. }
  624. const ChangeAutoRun = async (record) => {
  625. const oldValue = record.auto_run;
  626. record.auto_run = oldValue === 1 ? 0 : 1;
  627. try {
  628. const res = await changeAutoRun({ id: record.id });
  629. if (!res || res.code !== 0) {
  630. record.auto_run = oldValue;
  631. Notification.error({
  632. title: '切换自动乐跑状态失败!',
  633. content: res?.msg ?? '请稍后再试'
  634. })
  635. } else {
  636. Message.success(`自动乐跑状态已${record.auto_run === 1 ? '开启' : '关闭'}`)
  637. getAccounts()
  638. }
  639. } catch (error) {
  640. console.log(error)
  641. record.auto_run = oldValue
  642. Message.error('切换自动乐跑状态失败!')
  643. }
  644. }
  645. const SingleRun = async (item) => {
  646. if (item.state !== 1)
  647. return Notification.warning({
  648. title: '当前乐跑账号需登录,请登录后再试',
  649. content: '如有疑问请联系客服'
  650. })
  651. Modal.confirm({
  652. title: '开始乐跑',
  653. content: () => h('div', [
  654. h('p', `您是否要为${item.name}(${item.student_num})乐跑?若乐跑成功将扣减乐跑次数`)
  655. ]),
  656. onOk: async () => {
  657. const res = await singleRun({ student_num: item.student_num })
  658. if (!res || res.code !== 0)
  659. return Notification.error({
  660. title: '提交乐跑任务失败',
  661. content: res?.msg ?? '请稍后再试'
  662. })
  663. Message.success('提交乐跑任务成功!')
  664. }
  665. })
  666. }
  667. const UpdateAccountInfo = async (record) => {
  668. try {
  669. const res = await adminUpdateAccountInfo({
  670. student_num: record.student_num
  671. })
  672. if (!res || res.code !== 0) {
  673. return Notification.error({
  674. title: '更新账号信息失败!',
  675. content: res?.msg ?? '请稍后再试'
  676. })
  677. }
  678. Message.success('已异步触发账号信息更新')
  679. getAccounts()
  680. } catch (error) {
  681. Notification.error({
  682. title: '更新账号信息失败!',
  683. content: error.message || '请稍后再试'
  684. })
  685. }
  686. }
  687. const actionLabel = (action) => {
  688. const map = {
  689. platform_bind: '平台绑定',
  690. platform_unbind: '平台解绑',
  691. bot_bind: '机器人绑定',
  692. bot_unbind: '机器人解绑'
  693. }
  694. return map[action] || action
  695. }
  696. const sourceLabel = (source) => {
  697. const map = {
  698. user_api: '用户接口',
  699. admin_api: '管理员接口',
  700. service_api: '客服接口',
  701. mcp_qq: '机器人MCP',
  702. mcp_work_order: '工单MCP'
  703. }
  704. return map[source] || source
  705. }
  706. const formatDetail = (detail) => {
  707. if (!detail) return '-'
  708. if (typeof detail === 'string') return detail
  709. const keyMap = {
  710. via: '触发方式',
  711. sender: '发送者',
  712. old_owner_uuid: '原所属用户UUID',
  713. new_owner_uuid: '新所属用户UUID'
  714. }
  715. const valueMap = {
  716. AddAccount: '用户新增账号',
  717. DeleteAccount: '账号解绑接口',
  718. 'AddAccount:auto_unbind_rebind': '新增账号自动解绑并重绑'
  719. }
  720. return Object.keys(detail)
  721. .map((key) => {
  722. const label = keyMap[key] || key
  723. const rawValue = detail[key]
  724. const value = (typeof rawValue === 'string' && valueMap[rawValue]) ? valueMap[rawValue] : rawValue
  725. return `${label}:${value}`
  726. })
  727. .join(';')
  728. }
  729. const openBindAudit = async (record) => {
  730. bindAuditVisible.value = true
  731. bindAuditLoading.value = true
  732. bindAuditData.value = []
  733. try {
  734. const studentNum = String(record.student_num || '')
  735. if (!studentNum) {
  736. Message.warning('该账号缺少学号,无法查询审计记录')
  737. return
  738. }
  739. const res = await getAdminBindAuditByAccount({
  740. student_num: studentNum,
  741. pagesize: 100,
  742. current: 1
  743. })
  744. if (!res || res.code !== 0) {
  745. Notification.error({
  746. title: '获取绑定解绑记录失败',
  747. content: res?.msg ?? '请稍后再试'
  748. })
  749. return
  750. }
  751. const primaryData = Array.isArray(res.data) ? res.data : []
  752. if (primaryData.length > 0) {
  753. bindAuditData.value = primaryData
  754. return
  755. }
  756. const fallbackRes = await getAdminBindAuditList({
  757. student_num: studentNum,
  758. pagesize: 100,
  759. current: 1
  760. })
  761. const fallbackData = (fallbackRes && fallbackRes.code === 0 && Array.isArray(fallbackRes.data)) ? fallbackRes.data : []
  762. bindAuditData.value = fallbackData
  763. } catch (error) {
  764. Notification.error({
  765. title: '获取绑定解绑记录失败',
  766. content: error.message || '请稍后再试'
  767. })
  768. } finally {
  769. bindAuditLoading.value = false
  770. }
  771. }
  772. onMounted(() => {
  773. queryData.queryTime = getSemesterTimestamps()
  774. getAccounts()
  775. })
  776. onUnmounted(() => {
  777. clearPendingDetail()
  778. })
  779. const stramptoTime = (time) => {
  780. return new Date(time).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
  781. }
  782. const auto_time = [
  783. { label: '随机', value: -1 },
  784. ...Array.from({ length: 17 }, (_, i) => {
  785. const hour = i + 7
  786. return { label: `${hour} ~ ${hour + 1}时`, value: hour }
  787. })
  788. ]
  789. const autoTimeLabel = (record) => {
  790. if (record.auto_time === -1 && record.today_auto_time) {
  791. return `随机-今日${record.today_auto_time}时`
  792. }
  793. const match = auto_time.find(item => item.value === record.auto_time)
  794. return match ? match.label : '-'
  795. }
  796. </script>
  797. <style scoped>
  798. .query-row {
  799. margin-bottom: 10px;
  800. }
  801. .query-main {
  802. min-width: 0;
  803. }
  804. .query-actions-space {
  805. width: 100%;
  806. }
  807. .table-clickable {
  808. :deep(.arco-table-tr) {
  809. cursor: pointer;
  810. }
  811. }
  812. .table {
  813. font-family: -apple-system, BlinkMacSystemFont;
  814. margin-top: 15px;
  815. .state {
  816. display: flex;
  817. align-items: center;
  818. .circle {
  819. border-radius: 50%;
  820. height: 8px;
  821. min-height: 8px;
  822. width: 8px;
  823. min-width: 8px;
  824. margin-right: 5px;
  825. }
  826. .zero {
  827. background-color: rgb(var(--orange-6));
  828. }
  829. .one {
  830. background-color: rgb(var(--green-6));
  831. }
  832. .else {
  833. background-color: rgb(var(--red-6));
  834. }
  835. }
  836. }
  837. @media (max-width: 768px) {
  838. .query-row {
  839. display: block;
  840. }
  841. .query-divider {
  842. display: none;
  843. }
  844. .query-actions-space {
  845. margin-top: 4px;
  846. flex-direction: row !important;
  847. gap: 10px !important;
  848. }
  849. .query-actions-space :deep(.arco-btn) {
  850. flex: 1;
  851. }
  852. }
  853. </style>