index.vue 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017
  1. <template>
  2. <div class="store-page">
  3. <Breadcrumb />
  4. <userCard />
  5. <a-card title="账号列表" style="margin-top: 15px;">
  6. <div class="buttonGroup buttonGroup-desktop">
  7. <a-button v-if="hasPermission('action.lepao.addAccount')" type="primary" size="large" @click="editAccount()">
  8. <template #icon>
  9. <icon-plus />
  10. </template>
  11. 添加账号
  12. </a-button>
  13. <a-button size="large" @click="download('windows')">
  14. <template #icon>
  15. <icon-desktop />
  16. </template>
  17. Windows操作说明
  18. </a-button>
  19. <a-button size="large" @click="download('android')">
  20. <template #icon>
  21. <icon-mobile />
  22. </template>
  23. 安卓手机操作说明
  24. </a-button>
  25. <a-button size="large" @click="download('iphone')">
  26. <template #icon>
  27. <icon-mobile />
  28. </template>
  29. iPhone操作说明
  30. </a-button>
  31. <a-button size="large" @click="download('page')" v-if="!isElectron()">
  32. <template #icon>
  33. <icon-download />
  34. </template>
  35. 客户端/登录器下载
  36. </a-button>
  37. </div>
  38. <div class="buttonGroup buttonGroup-mobile">
  39. <a-button v-if="hasPermission('action.lepao.addAccount')" type="primary" size="large" @click="editAccount()">
  40. <template #icon>
  41. <icon-plus />
  42. </template>
  43. 添加账号
  44. </a-button>
  45. <a-dropdown trigger="click" position="br">
  46. <a-button size="large" class="more-actions-btn">
  47. 更多操作
  48. <icon-down />
  49. </a-button>
  50. <template #content>
  51. <a-doption @click="download('windows')">
  52. <icon-desktop />
  53. Windows操作说明
  54. </a-doption>
  55. <a-doption @click="download('android')">
  56. <icon-mobile />
  57. 安卓手机操作说明
  58. </a-doption>
  59. <a-doption @click="download('iphone')">
  60. <icon-mobile />
  61. iPhone操作说明
  62. </a-doption>
  63. <a-doption v-if="!isElectron()" @click="download('page')">
  64. <icon-download />
  65. 客户端/登录器下载
  66. </a-doption>
  67. </template>
  68. </a-dropdown>
  69. </div>
  70. <AppQueryFilter>
  71. <a-row class="queryForm app-query-form query-row">
  72. <a-col :flex="'1000px'" class="query-main">
  73. <a-form :model="queryDataForm" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }"
  74. label-align="left">
  75. <a-row :gutter="16">
  76. <a-col :span="6">
  77. <a-form-item field="area" label="跑区">
  78. <a-select v-model="queryDataForm.area" placeholder="请选择乐跑跑区" default-value="">
  79. <a-option value="">所有跑区</a-option>
  80. <a-option v-for="(item, index) in area" :key="index" :value="item">
  81. {{ item }}
  82. </a-option>
  83. </a-select>
  84. </a-form-item>
  85. </a-col>
  86. <a-col :span="6">
  87. <a-form-item field="email" label="邮箱">
  88. <EmailAutoComplete v-model="queryDataForm.email" allow-clear />
  89. </a-form-item>
  90. </a-col>
  91. <a-col :span="6">
  92. <a-form-item field="username" label="姓名">
  93. <a-input v-model="queryDataForm.username" allow-clear />
  94. </a-form-item>
  95. </a-col>
  96. <a-col :span="6">
  97. <a-form-item field="student_num" label="学号">
  98. <a-input v-model="queryDataForm.student_num" allow-clear />
  99. </a-form-item>
  100. </a-col>
  101. <a-col :span="6">
  102. <a-form-item field="area" label="状态">
  103. <a-select v-model="queryDataForm.state" :options="state" placeholder="请选择账号状态" :default-value="-1" />
  104. </a-form-item>
  105. </a-col>
  106. <a-col :span="6">
  107. <a-form-item field="auto_time" label="乐跑时段">
  108. <a-select v-model="queryDataForm.auto_time" placeholder="请选择自动乐跑时段" :default-value="-1">
  109. <a-option :value="0">所有时段</a-option>
  110. <a-option v-for="(item, index) in auto_time" :key="index" :value="item.value">
  111. {{ item.label }}
  112. </a-option>
  113. </a-select>
  114. </a-form-item>
  115. </a-col>
  116. <a-col :span="6">
  117. <a-form-item field="notes" label="备注">
  118. <a-input v-model="queryDataForm.notes" allow-clear />
  119. </a-form-item>
  120. </a-col>
  121. <a-col :span="6">
  122. <a-form-item field="queryTime" label="更新时间">
  123. <a-range-picker v-model="queryDataForm.queryTime" show-time format="YY-MM-DD HH:mm" value-format="x"
  124. popup-container="body"
  125. @ok="search()" />
  126. </a-form-item>
  127. </a-col>
  128. </a-row>
  129. </a-form>
  130. </a-col>
  131. <a-divider class="query-divider" style="height: 84px" direction="vertical" />
  132. <a-col :flex="1" class="app-query-actions">
  133. <a-space class="query-actions-space" direction="vertical" :size="18">
  134. <a-button type="primary" @click="search">
  135. <template #icon>
  136. <icon-search />
  137. </template>
  138. 搜索
  139. </a-button>
  140. <a-button @click="reset">
  141. <template #icon>
  142. <icon-refresh />
  143. </template>
  144. 重置
  145. </a-button>
  146. </a-space>
  147. </a-col>
  148. </a-row>
  149. </AppQueryFilter>
  150. <a-alert v-if="notice" style="margin-bottom: 15px;">{{ notice }}</a-alert>
  151. <a-table :data="data" :bordered="false" hoverable class="table table-clickable" :loading="loading" expandable :scroll="{
  152. x: 2400
  153. }" @row-click="onRowClick" :pagination="{
  154. showPageSize: true,
  155. showJumper: true,
  156. showTotal: true,
  157. pageSize: pagination.pagesize,
  158. current: pagination.current,
  159. total: pagination.total
  160. }" @page-change="handlePageChange" @page-size-change="handlePageSizeChange">
  161. <template #name-filter="{ filterValue, setFilterValue, handleFilterConfirm, handleFilterReset }">
  162. <div class="custom-filter">
  163. <a-space direction="vertical">
  164. <a-input :model-value="filterValue[0]" @input="(value) => setFilterValue([value])" />
  165. <div class="custom-filter-footer">
  166. <a-button @click="handleFilterReset">重置</a-button>
  167. <a-button @click="handleFilterConfirm">确定</a-button>
  168. </div>
  169. </a-space>
  170. </div>
  171. </template>
  172. <template #columns>
  173. <a-table-column title="" :width="50" data-index="user_avatar" tooltip>
  174. <template #cell="{ record }">
  175. <a-avatar>
  176. <img :alt="record.name ?? ''"
  177. :src="record.user_avatar || 'https://lepao-cloud.xxoo365.top/view.php/25aa126dc406974ff3579a99a2c6501a.png'" />
  178. </a-avatar>
  179. </template>
  180. </a-table-column>
  181. <a-table-column title="学号" :width="117" data-index="student_num" ellipsis tooltip></a-table-column>
  182. <a-table-column title="姓名" :width="130">
  183. <template #cell="{ record }">
  184. {{ record.name ?? '请使用乐跑登录器更新账号信息' }}
  185. </template>
  186. </a-table-column>
  187. <a-table-column title="性别" :width="80" ellipsis tooltip :filterable="{
  188. filters: [
  189. { text: '男', value: 1 },
  190. { text: '女', value: 2 }
  191. ],
  192. filter: (value, record) => record.sex == value
  193. }">
  194. <template #cell="{ record }">
  195. <icon-man v-if="record.sex === 1" />
  196. <icon-woman v-else-if="record.sex === 2" />
  197. {{ record.sex === 1 ? '男' : (record.sex === 2 ? '女' : '') }}
  198. </template>
  199. </a-table-column>
  200. <a-table-column title="年级" :width="70" data-index="grade_id" tooltip></a-table-column>
  201. <a-table-column title="学院" :width="220" data-index="academy_name" tooltip></a-table-column>
  202. <a-table-column title="跑区" :width="130">
  203. <template #cell="{ record }">
  204. {{ record.area || '随机分配' }}
  205. </template>
  206. </a-table-column>
  207. <a-table-column title="通知方式" :width="200" ellipsis tooltip>
  208. <template #cell="{ record }">
  209. <span v-if="record.notice_type === 'email'"><icon-email /> {{ record.email ?? '未绑定' }}</span>
  210. <span v-else-if="record.notice_type === 'bot'"><icon-robot /> {{ record.bot_account ?? '未绑定' }}</span>
  211. <span v-else><icon-notification-close /> 无通知</span>
  212. </template>
  213. </a-table-column>
  214. <a-table-column title="帐号状态" :width="100" ellipsis tooltip>
  215. <template #cell="{ record }">
  216. <div v-if="record.state === 0" class="state">
  217. <div class="circle zero"></div>需登录
  218. </div>
  219. <div v-else-if="record.state === 1" class="state">
  220. <div class="circle one"></div>正常
  221. </div>
  222. <div v-else class="state">
  223. <div class="circle else"></div>状态异常
  224. </div>
  225. </template>
  226. </a-table-column>
  227. <!-- <a-table-column title="人脸状态" :width="100" ellipsis tooltip>
  228. <template #cell="{ record }">
  229. <div v-if="record.face_state === 0" class="state">
  230. <div class="circle zero"></div>未采集
  231. </div>
  232. <div v-else-if="record.face_state === 1" class="state">
  233. <div class="circle one"></div>已通过
  234. </div>
  235. <div v-else class="state">
  236. <div class="circle else"></div>不通过
  237. </div>
  238. </template>
  239. </a-table-column> -->
  240. <a-table-column title="自动乐跑" :width="105" ellipsis tooltip :filterable="{
  241. filters: [
  242. { text: '开启', value: 1 },
  243. { text: '关闭', value: 0 }
  244. ],
  245. filter: (value, record) => record.auto_run == value
  246. }">
  247. <template #cell="{ record }">
  248. <a-tag color="green" v-if="record.auto_run">{{ record.target_count === 0 ? '开启-∞次' :
  249. `开启-${record.target_count}次` }}</a-tag>
  250. <a-tag color="red" v-else>关闭</a-tag>
  251. </template>
  252. </a-table-column>
  253. <a-table-column title="自动乐跑星期" :width="155" ellipsis tooltip>
  254. <template #cell="{ record }">
  255. <span v-if="record.auto_run && record.auto_day && record.auto_day.length > 0">
  256. {{record.auto_day.slice().sort((a, b) => {
  257. if (a === 0) return 1; if (b === 0) return -1; return a - b;
  258. }).map(day => auto_day.find(item => item.value === day)?.label).join(',').replace(/周/g, '')}}
  259. </span>
  260. <span v-else>-</span>
  261. </template>
  262. </a-table-column>
  263. <a-table-column title="自动乐跑时段" :width="130" ellipsis tooltip>
  264. <template #cell="{ record }">
  265. {{ record.auto_run ? autoTimeLabel(record) : '-' }}
  266. </template>
  267. </a-table-column>
  268. <a-table-column title="学期目标" :width="88" ellipsis tooltip>
  269. <template #cell="{ record }">
  270. {{ record.term_num != record.total_num ? `${record.total_num} / ${record.term_num}` :
  271. '已完成' }}
  272. </template>
  273. </a-table-column>
  274. <a-table-column title="添加时间" :width="150" ellipsis tooltip :sortable="{
  275. sortDirections: ['ascend', 'descend']
  276. }">
  277. <template #cell="{ record }">
  278. {{ stramptoTime(record.create_time) }}
  279. </template>
  280. </a-table-column>
  281. <a-table-column title="上次更新时间" :width="150" ellipsis tooltip>
  282. <template #cell="{ record }">
  283. {{ record.update_time ? stramptoTime(record.update_time) : '待登录' }}
  284. </template>
  285. </a-table-column>
  286. <a-table-column title="备注" :width="200" ellipsis tooltip>
  287. <template #cell="{ record }">
  288. {{ record.notes }}
  289. </template>
  290. </a-table-column>
  291. <a-table-column title="" :fixed="tableFixed('right')" :width="100">
  292. <template #cell="{ record }">
  293. <a-dropdown :popup-max-height="false" trigger="hover" @click.stop>
  294. <a-button>操作 <icon-down /></a-button>
  295. <template #content>
  296. <a-doption v-if="hasPermission('action.lepao.updateAccount')" @click="editAccount(record)"><icon-edit /> 编辑账号</a-doption>
  297. <!-- <a-doption @click="faceRecoRef.openModal(record)"><icon-video-camera /> 人脸采集</a-doption> -->
  298. <!-- <a-doption @click="bindBotRef.openModal(record)"><icon-robot-add /> 绑定智能机器人</a-doption> -->
  299. <a-doption v-if="hasPermission('action.lepao.singleRun')" @click="SingleRun(record)"><icon-play-circle /> 开始单次乐跑</a-doption>
  300. <a-doption v-if="hasPermission('action.lepao.changeAutoRun')" @click="ChangeAutoRun(record)"><icon-translate /> {{ record.auto_run ? '关闭' :
  301. '开启' }}自动乐跑</a-doption>
  302. <a-doption v-if="hasPermission('action.lepao.updateAccount')" @click="UpdateSelfAccount(record)"><icon-refresh /> 更新账号信息</a-doption>
  303. <a-doption v-if="hasPermission('action.lepao.deleteAccount')" @click="DeleteAccount(record)"><icon-minus-circle /> 解绑账号</a-doption>
  304. </template>
  305. </a-dropdown>
  306. </template>
  307. </a-table-column>
  308. </template>
  309. </a-table>
  310. </a-card>
  311. </div>
  312. <!-- 账号编辑对话框 -->
  313. <a-modal v-model:visible="visible" title="编辑账号" @cancel="handleCancel" @before-ok="handleBeforeOk" draggable
  314. :ok-loading="ok_loading" esc-to-close closable>
  315. <a-form :model="form">
  316. <a-form-item label="自动识别" v-if="!form.id">
  317. <a-textarea
  318. v-model="autoFillText"
  319. no-trim
  320. placeholder="粘贴包含学号、邮箱、跑区等信息的文本,点击“识别并填入”自动填写"
  321. :auto-size="{ minRows: 2, maxRows: 4 }"
  322. allow-clear
  323. />
  324. <template #extra>
  325. <a-space>
  326. <a-button type="primary" @click="handleAutoFill" size="small">识别并填入</a-button>
  327. <span class="auto-fill-tip">示例:2023012345,abc@ctbu.edu.cn,主校区北跑区</span>
  328. </a-space>
  329. </template>
  330. </a-form-item>
  331. <a-form-item field="student_num" label="学号">
  332. <a-input v-model="form.student_num" placeholder="账号所有者学号,填写错误将无法登录" allow-clear />
  333. </a-form-item>
  334. <a-form-item field="notice_type" label="通知方式">
  335. <a-radio-group v-model="form.notice_type">
  336. <a-radio value="email"><icon-email /> 邮件</a-radio>
  337. <!-- <a-radio value="bot"><icon-robot /> 智能机器人</a-radio> -->
  338. <a-radio value="none"><icon-notification-close /> 无通知</a-radio>
  339. </a-radio-group>
  340. </a-form-item>
  341. <a-form-item field="email" label="通知邮箱" v-if="form.notice_type === 'email'">
  342. <EmailAutoComplete v-model="form.email" placeholder="用于接收乐跑失败、登录失效的通知" allow-clear />
  343. </a-form-item>
  344. <a-form-item field="area" label="乐跑跑区">
  345. <a-select v-model="form.area" placeholder="请选择乐跑跑区" default-value="">
  346. <a-option value="">随机分配</a-option>
  347. <a-option v-for="(item, index) in area" :key="index" :value="item">
  348. <span class="vipcontent">
  349. <span>{{ item }} </span>
  350. <!-- <img src="@/assets/vip.svg" alt="vip" height="20"> -->
  351. </span>
  352. </a-option>
  353. </a-select>
  354. </a-form-item>
  355. <a-form-item field="auto_run" label="自动乐跑开关">
  356. <a-switch v-model="form.auto_run" :checked-value="1" :unchecked-value="0" />
  357. </a-form-item>
  358. <a-form-item field="target_count" label="乐跑目标次数" v-if="form.auto_run">
  359. <a-input-number v-model="form.target_count" placeholder="请输入乐跑目标次数" mode="button" />
  360. <template #extra>
  361. <div>当学期有效次数达到目标次数时自动乐跑将关闭,0为不限次</div>
  362. </template>
  363. </a-form-item>
  364. <a-form-item field="auto_day" label="自动乐跑星期" v-if="form.auto_run">
  365. <a-checkbox-group v-model="form.auto_day" placeholder="请选择每天自动乐跑的星期" :options="auto_day" />
  366. </a-form-item>
  367. <a-form-item field="auto_time" label="自动乐跑时段" v-if="form.auto_run">
  368. <a-select v-model="form.auto_time" placeholder="请选择每天自动乐跑的时段" :options="auto_time" />
  369. </a-form-item>
  370. <a-form-item field="notes" label="备注">
  371. <a-textarea v-model="form.notes" no-trim placeholder="添加对账号的备注(非必填)" :max-length="{ length: 50 }" allow-clear
  372. show-word-limit />
  373. </a-form-item>
  374. </a-form>
  375. </a-modal>
  376. <faceModal :faceInfo="faceInfo" ref="faceRecoRef" />
  377. <bindBot ref="bindBotRef" />
  378. <accountDetailCard ref="accountDetailRef" />
  379. </template>
  380. <script setup>
  381. import { ref, reactive, onUnmounted, onMounted, h } from 'vue'
  382. import { accountList, deleteAccount, addAccount, changeAutoRun, singleRun, updateSelfAccount } from '@/api/lepao'
  383. import { Modal, Notification, Message } from '@arco-design/web-vue'
  384. import { IconSearch } from '@arco-design/web-vue/es/icon'
  385. import userCard from '@/components/userCard/userCard.vue'
  386. import { isElectron } from '@/utils/electron'
  387. import faceModal from '@/components/FaceModal/faceModal.vue'
  388. import bindBot from '@/components/BindBot/bindBot.vue'
  389. import accountDetailCard from '@/components/LepaoAccountCard/accountDetailCard.vue'
  390. import { useRoute } from 'vue-router'
  391. import { getNotice, getSemesterTimestamps } from '@/utils/util'
  392. import { hasPermission } from '@/utils/permission'
  393. import { useResponsiveTable } from '@/hooks/useResponsiveTable'
  394. const { tableFixed } = useResponsiveTable()
  395. const notice = ref('')
  396. const faceInfo = ref({})
  397. const bindBotRef = ref(null)
  398. const accountDetailRef = ref(null)
  399. const ROW_DETAIL_CONFIRM_MS = 3000
  400. const pendingDetailAccountId = ref(null)
  401. let pendingDetailTimer = null
  402. const clearPendingDetail = () => {
  403. pendingDetailAccountId.value = null
  404. if (pendingDetailTimer) {
  405. clearTimeout(pendingDetailTimer)
  406. pendingDetailTimer = null
  407. }
  408. }
  409. const queryDataForm = reactive({
  410. area: '',
  411. student_num: '',
  412. email: '',
  413. username: '',
  414. notes: '',
  415. state: -1,
  416. auto_time: 0,
  417. queryTime: []
  418. })
  419. const queryData = reactive({
  420. area: '',
  421. student_num: '',
  422. email: '',
  423. username: '',
  424. notes: '',
  425. state: -1,
  426. auto_time: 0,
  427. queryTime: []
  428. })
  429. const pagination = reactive({
  430. total: 0,
  431. current: 1, // 默认从第1页开始
  432. pagesize: 20
  433. })
  434. const area = ["兰花湖校区跑区", "主校区北跑区", "主校区南跑区", "重庆工商大学茶园校区"]
  435. const state = [
  436. { label: '全部', value: -1 }, { label: '需登录', value: 0 }, { label: '正常', value: 1 }, { label: '状态异常', value: 2 }
  437. ]
  438. const search = () => {
  439. pagination.current = 1
  440. queryData.area = queryDataForm.area
  441. queryData.student_num = queryDataForm.student_num
  442. queryData.email = queryDataForm.email
  443. queryData.username = queryDataForm.username
  444. queryData.notes = queryDataForm.notes
  445. queryData.state = queryDataForm.state
  446. queryData.auto_time = queryDataForm.auto_time
  447. queryData.queryTime = queryDataForm.queryTime
  448. getAccountsAsync()
  449. }
  450. const reset = () => {
  451. queryDataForm.area = ''
  452. queryDataForm.student_num = ''
  453. queryDataForm.email = ''
  454. queryDataForm.username = ''
  455. queryDataForm.notes = ''
  456. queryDataForm.state = -1
  457. queryDataForm.auto_time = 0
  458. queryDataForm.queryTime = getSemesterTimestamps()
  459. queryData.area = ''
  460. queryData.student_num = ''
  461. queryData.email = ''
  462. queryData.username = ''
  463. queryData.notes = ''
  464. queryData.state = -1
  465. queryData.auto_time = 0
  466. queryData.queryTime = getSemesterTimestamps()
  467. getAccountsAsync()
  468. }
  469. // 分页 - 页码变化
  470. const handlePageChange = (page) => {
  471. pagination.current = page
  472. getAccountsAsync()
  473. }
  474. // 分页 - 每页条数变化
  475. const handlePageSizeChange = (size) => {
  476. pagination.pagesize = size
  477. pagination.current = 1 // 页大小变化后回到第一页
  478. getAccountsAsync()
  479. }
  480. const auto_day = [
  481. { label: '周一', value: 1 },
  482. { label: '周二', value: 2 },
  483. { label: '周三', value: 3 },
  484. { label: '周四', value: 4 },
  485. { label: '周五', value: 5 },
  486. { label: '周六', value: 6 },
  487. { label: '周日', value: 0 }
  488. ]
  489. const auto_time = [
  490. { label: '随机分配', value: -1 },
  491. ...Array.from({ length: 17 }, (_, 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) {
  498. if (record.today_auto_time)
  499. return `随机-今日${record.today_auto_time}时`
  500. return '随机-待分配'
  501. }
  502. const match = auto_time.find(item => item.value === record.auto_time)
  503. return match ? match.label : '-'
  504. }
  505. const data = ref([])
  506. const loading = ref(false)
  507. const visible = ref(false)
  508. const ok_loading = ref(false)
  509. const autoFillText = ref('')
  510. const form = reactive({
  511. id: null,
  512. student_num: '',
  513. email: '',
  514. area: '',
  515. auto_time: -1,
  516. auto_run: 1,
  517. notice_type: 'email',
  518. target_count: 30,
  519. auto_day: [0, 1, 2, 3, 4, 5, 6],
  520. notes: ''
  521. })
  522. const download = (device) => {
  523. const a = document.createElement('a')
  524. if (device === 'windows') {
  525. a.href = 'https:\/\/lepao-cloud.xxoo365.top/down.php\/682d99f9694c6fe76b64b86c5741a2d8.pdf'
  526. a.download = 'RunForge-Windows操作说明.pdf'
  527. } else if (device === 'iphone') {
  528. a.href = 'https:\/\/lepao-cloud.xxoo365.top/down.php\/fba1d571166b4c95592c7c4e624a9390.pdf'
  529. a.download = 'RunForge-iPhone操作说明.pdf'
  530. } else if (device === 'android') {
  531. a.href = 'https:\/\/lepao-cloud.xxoo365.top\/down.php\/3326850aa879cea586677a15af470beb.pdf'
  532. a.download = 'RunForge-Android操作说明.pdf'
  533. } else if (device === 'page') {
  534. a.href = 'https:\/\/download.xxoo365.top'
  535. } else {
  536. return
  537. }
  538. a.target = '_blank'
  539. document.body.appendChild(a)
  540. a.click()
  541. document.body.removeChild(a)
  542. }
  543. const normalizeText = (value) => (value || '').replace(/[\n\r\t,;、]/g, ' ').trim()
  544. const matchArea = (text) => {
  545. const normalized = normalizeText(text)
  546. if (!normalized) return null
  547. const exact = area.find(item => normalized.includes(item))
  548. if (exact) return exact
  549. const areaAliasMap = [
  550. { keywords: ['兰花'], value: '兰花湖校区跑区' },
  551. { keywords: ['主校北', '主校区北', '北跑区', '北区', '北'], value: '主校区北跑区' },
  552. { keywords: ['主校南', '主校区南', '南跑区', '南区', '南'], value: '主校区南跑区' },
  553. { keywords: ['茶园', '茶园校区'], value: '重庆工商大学茶园校区' }
  554. ]
  555. const matchedAlias = areaAliasMap.find(item => item.keywords.some(keyword => normalized.includes(keyword)))
  556. return matchedAlias ? matchedAlias.value : null
  557. }
  558. const handleAutoFill = () => {
  559. const text = normalizeText(autoFillText.value)
  560. if (!text) {
  561. Message.warning('请先粘贴需要识别的文本')
  562. return
  563. }
  564. const studentMatch = text.match(/\b20\d{8}\b/)
  565. const emailMatch = text.match(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/)
  566. const areaMatch = matchArea(text)
  567. let hitCount = 0
  568. if (studentMatch?.[0]) {
  569. form.student_num = studentMatch[0]
  570. hitCount += 1
  571. }
  572. if (emailMatch?.[0]) {
  573. form.email = emailMatch[0]
  574. form.notice_type = 'email'
  575. hitCount += 1
  576. }
  577. if (areaMatch) {
  578. form.area = areaMatch
  579. hitCount += 1
  580. }
  581. if (hitCount === 0) {
  582. Message.warning('未识别到可填写信息,请检查文本内容')
  583. return
  584. }
  585. Message.success(`识别成功,已填入 ${hitCount} 项信息`)
  586. }
  587. const onRowClick = (record, ev) => {
  588. if (ev?.target?.closest?.('.arco-dropdown, .arco-btn')) return
  589. const accountId = record?.id
  590. if (accountId == null) return
  591. if (pendingDetailAccountId.value === accountId) {
  592. clearPendingDetail()
  593. accountDetailRef.value?.openModal(record)
  594. return
  595. }
  596. clearPendingDetail()
  597. pendingDetailAccountId.value = accountId
  598. Message.info('再点一次即可查看账号详情')
  599. pendingDetailTimer = setTimeout(clearPendingDetail, ROW_DETAIL_CONFIRM_MS)
  600. }
  601. const editAccount = (item) => {
  602. if (item) {
  603. form.id = item.id
  604. form.student_num = item.student_num
  605. form.email = item.email
  606. form.area = item.area
  607. form.auto_time = item.auto_time
  608. form.auto_run = item.auto_run
  609. form.target_count = item.target_count
  610. form.notice_type = item.notice_type || 'email'
  611. form.auto_day = item.auto_day
  612. form.notes = item.notes
  613. autoFillText.value = ''
  614. } else {
  615. form.id = null
  616. form.student_num = ''
  617. form.email = ''
  618. form.auto_run = 1
  619. form.auto_time = -1
  620. form.target_count = 30
  621. form.auto_day = [0, 1, 2, 3, 4, 5, 6]
  622. form.notice_type = 'email'
  623. form.area = ''
  624. form.notes = ''
  625. autoFillText.value = ''
  626. }
  627. visible.value = true
  628. }
  629. const handleBeforeOk = async (done) => {
  630. try {
  631. ok_loading.value = true
  632. const { student_num, email, notice_type } = form
  633. if (!student_num || (!email && notice_type === 'email')) {
  634. Message.error('请填写完整的账号信息')
  635. return false
  636. }
  637. const studentNumRegex = /^\d{10}$/
  638. if (!studentNumRegex.test(student_num)) {
  639. Message.error('请检查学号格式是否正确')
  640. return false
  641. }
  642. const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
  643. if (notice_type === 'email' && !emailRegex.test(email)) {
  644. Message.error('请检查邮箱格式是否正确')
  645. return false
  646. }
  647. let data = {
  648. ...form
  649. }
  650. const res = await addAccount(data)
  651. if (!res || res.code !== 0) {
  652. Notification.error({
  653. title: '保存乐跑账号失败!',
  654. content: res?.msg ?? '请稍后再试'
  655. })
  656. return false
  657. }
  658. Message.success('保存成功!')
  659. if (form.notice_type === 'bot' && res.data && !res.data.umo) {
  660. bindBotRef.value.openModal(res.data)
  661. }
  662. done()
  663. getAccountsAsync()
  664. } catch (error) {
  665. Notification.error({
  666. title: '保存乐跑账号失败!',
  667. content: error.message || '请稍后再试'
  668. })
  669. return false
  670. } finally {
  671. ok_loading.value = false
  672. }
  673. }
  674. const handleCancel = () => {
  675. visible.value = false
  676. }
  677. const getAccountsAsync = async () => {
  678. loading.value = true
  679. await getAccounts()
  680. loading.value = false
  681. }
  682. const getAccounts = async () => {
  683. try {
  684. const reqData = {
  685. ...queryData,
  686. pagesize: pagination.pagesize,
  687. current: pagination.current
  688. }
  689. const res = await accountList(reqData)
  690. if (!res || res.code !== 0)
  691. return Notification.error({
  692. title: '获取乐跑账号数据失败!',
  693. content: res?.msg ?? '请稍后再试'
  694. })
  695. data.value = res.data
  696. pagination.total = res.pagination.total
  697. } catch (error) {
  698. Notification.error({
  699. title: '获取乐跑账号数据失败!',
  700. content: error.message || '请稍后再试'
  701. })
  702. }
  703. }
  704. const GetNotice = async () => {
  705. const { path } = useRoute()
  706. const res = await getNotice(path)
  707. notice.value = res
  708. }
  709. const SingleRun = async (item) => {
  710. if (item.state !== 1)
  711. return Notification.warning({
  712. title: '当前乐跑账号需登录,请登录后再试',
  713. content: '如有疑问请联系RunForge客服'
  714. })
  715. Modal.confirm({
  716. title: '开始乐跑',
  717. content: () => h('div', [
  718. h('p', `您是否要为 ${item.name}(${item.student_num}) 乐跑?若乐跑成功将扣减乐跑次数`)
  719. ]),
  720. onOk: async () => {
  721. const res = await singleRun({ student_num: item.student_num })
  722. if (!res || res.code !== 0)
  723. return Notification.error({
  724. title: '提交乐跑任务失败',
  725. content: res?.msg ?? '请稍后再试'
  726. })
  727. Message.success('提交乐跑任务成功!')
  728. }
  729. })
  730. }
  731. const DeleteAccount = async (item) => {
  732. Modal.confirm({
  733. title: '解绑账号',
  734. content: () => h('div', [
  735. h('p', '您是否要解绑该账号?')
  736. ]),
  737. onOk: async () => {
  738. const res = await deleteAccount({ id: item.id })
  739. if (!res || res.code !== 0)
  740. return Notification.error({
  741. title: '解绑失败',
  742. content: res?.msg ?? '请稍后再试'
  743. })
  744. Message.success('解绑成功!')
  745. getAccounts()
  746. }
  747. })
  748. }
  749. const ChangeAutoRun = async (record) => {
  750. const oldValue = record.auto_run;
  751. record.auto_run = oldValue === 1 ? 0 : 1;
  752. try {
  753. const res = await changeAutoRun({ id: record.id });
  754. if (!res || res.code !== 0) {
  755. record.auto_run = oldValue;
  756. Notification.error({
  757. title: '切换自动乐跑状态失败!',
  758. content: res?.msg ?? '请稍后再试'
  759. })
  760. } else {
  761. Message.success(`自动乐跑状态已${record.auto_run === 1 ? '开启' : '关闭'}`)
  762. getAccounts()
  763. }
  764. } catch (error) {
  765. record.auto_run = oldValue;
  766. Message.error('切换自动乐跑状态失败!');
  767. }
  768. }
  769. const UpdateSelfAccount = async (record) => {
  770. try {
  771. const res = await updateSelfAccount({
  772. student_num: record.student_num
  773. })
  774. if (!res || res.code !== 0) {
  775. return Notification.error({
  776. title: '更新账号信息失败!',
  777. content: res?.msg ?? '请稍后再试'
  778. })
  779. }
  780. Message.success('更新账号信息成功!')
  781. getAccounts()
  782. } catch (error) {
  783. Notification.error({
  784. title: '更新账号信息失败!',
  785. content: error.message || '请稍后再试'
  786. })
  787. }
  788. }
  789. const stramptoTime = (time) => {
  790. return new Date(time).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
  791. }
  792. let timer = null
  793. // 轮询
  794. const startPolling = () => {
  795. if (!timer) {
  796. timer = setInterval(async () => {
  797. await getAccounts()
  798. }, 5000)
  799. }
  800. }
  801. // 停止轮询
  802. const stopPolling = () => {
  803. if (timer) {
  804. clearInterval(timer)
  805. timer = null
  806. }
  807. }
  808. onMounted(async () => {
  809. queryData.queryTime = getSemesterTimestamps()
  810. queryDataForm.queryTime = getSemesterTimestamps()
  811. getAccountsAsync()
  812. GetNotice()
  813. startPolling()
  814. })
  815. // 组件销毁时停止轮询
  816. onUnmounted(() => {
  817. clearPendingDetail()
  818. stopPolling()
  819. })
  820. </script>
  821. <style scoped lang="less">
  822. @import '@/styles/store-theme.less';
  823. .queryForm {
  824. padding: 0;
  825. width: 100%;
  826. box-sizing: border-box;
  827. @media (min-width: (@app-breakpoint-md + 1px)) {
  828. margin-top: 20px;
  829. }
  830. }
  831. .buttonGroup {
  832. display: flex;
  833. flex-wrap: wrap;
  834. gap: 10px;
  835. margin-bottom: 12px;
  836. }
  837. .buttonGroup-mobile {
  838. display: none;
  839. }
  840. .query-main {
  841. min-width: 0;
  842. }
  843. .query-actions-space {
  844. width: 100%;
  845. }
  846. .table-clickable {
  847. :deep(.arco-table-tr) {
  848. cursor: pointer;
  849. }
  850. }
  851. .table {
  852. font-family: -apple-system, BlinkMacSystemFont;
  853. .state {
  854. display: flex;
  855. align-items: center;
  856. .circle {
  857. border-radius: 50%;
  858. height: 8px;
  859. min-height: 8px;
  860. width: 8px;
  861. min-width: 8px;
  862. margin-right: 5px;
  863. }
  864. .zero {
  865. background-color: rgb(var(--orange-6));
  866. }
  867. .one {
  868. background-color: rgb(var(--green-6));
  869. }
  870. .else {
  871. background-color: rgb(var(--red-6));
  872. }
  873. }
  874. }
  875. .custom-filter {
  876. padding: 20px;
  877. background: var(--color-bg-5);
  878. border: 1px solid var(--color-neutral-3);
  879. border-radius: var(--border-radius-medium);
  880. box-shadow: 0 2px 5px rgb(0 0 0 / 10%);
  881. }
  882. .custom-filter-footer {
  883. display: flex;
  884. justify-content: space-between;
  885. }
  886. .vipcontent {
  887. display: flex;
  888. align-items: center;
  889. }
  890. .auto-fill-tip {
  891. color: rgb(var(--gray-6));
  892. }
  893. @media (max-width: 768px) {
  894. .buttonGroup-desktop {
  895. display: none;
  896. }
  897. .buttonGroup-mobile {
  898. display: flex;
  899. align-items: center;
  900. }
  901. .query-row {
  902. display: block;
  903. }
  904. .query-divider {
  905. display: none;
  906. }
  907. .query-actions-space {
  908. margin-top: 4px;
  909. flex-direction: row !important;
  910. gap: 10px !important;
  911. }
  912. .query-actions-space :deep(.arco-btn) {
  913. flex: 1;
  914. }
  915. }
  916. </style>