|
@@ -0,0 +1,414 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="container">
|
|
|
|
|
+ <Breadcrumb :items="['网站管理', '乐跑账号管理']" />
|
|
|
|
|
+
|
|
|
|
|
+ <a-card title="乐跑账号管理">
|
|
|
|
|
+ <a-row>
|
|
|
|
|
+ <a-col :flex="1">
|
|
|
|
|
+ <a-form :model="queryData" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }"
|
|
|
|
|
+ label-align="left">
|
|
|
|
|
+
|
|
|
|
|
+ <a-row :gutter="16">
|
|
|
|
|
+ <a-col :span="8">
|
|
|
|
|
+ <a-form-item field="area" label="跑区">
|
|
|
|
|
+ <a-select v-model="queryData.area" placeholder="请选择乐跑跑区" default-value="">
|
|
|
|
|
+ <a-option value="">所有</a-option>
|
|
|
|
|
+ <a-option v-for="(item, index) in areas" :key="index" :value="item">
|
|
|
|
|
+ {{ item }}
|
|
|
|
|
+ </a-option>
|
|
|
|
|
+ </a-select>
|
|
|
|
|
+ </a-form-item>
|
|
|
|
|
+ </a-col>
|
|
|
|
|
+ <a-col :span="8">
|
|
|
|
|
+ <a-form-item field="user_uuid" label="创建人UUID">
|
|
|
|
|
+ <a-input v-model="queryData.user_uuid" />
|
|
|
|
|
+ </a-form-item>
|
|
|
|
|
+ </a-col>
|
|
|
|
|
+ <a-col :span="8">
|
|
|
|
|
+ <a-form-item field="email" label="用户邮箱">
|
|
|
|
|
+ <a-input v-model="queryData.email" />
|
|
|
|
|
+ </a-form-item>
|
|
|
|
|
+ </a-col>
|
|
|
|
|
+ <a-col :span="8">
|
|
|
|
|
+ <a-form-item field="username" label="账号名称">
|
|
|
|
|
+ <a-input v-model="queryData.username" />
|
|
|
|
|
+ </a-form-item>
|
|
|
|
|
+ </a-col>
|
|
|
|
|
+ <a-col :span="8">
|
|
|
|
|
+ <a-form-item field="student_num" label="学号">
|
|
|
|
|
+ <a-input v-model="queryData.student_num" />
|
|
|
|
|
+ </a-form-item>
|
|
|
|
|
+ </a-col>
|
|
|
|
|
+ <a-col :span="8">
|
|
|
|
|
+ <a-form-item field="area" label="账号状态">
|
|
|
|
|
+ <a-select v-model="queryData.state" :options="state" placeholder="请选择账号状态"
|
|
|
|
|
+ :default-value="-1" />
|
|
|
|
|
+ </a-form-item>
|
|
|
|
|
+ </a-col>
|
|
|
|
|
+ </a-row>
|
|
|
|
|
+ </a-form>
|
|
|
|
|
+ </a-col>
|
|
|
|
|
+ <a-divider style="height: 84px" direction="vertical" />
|
|
|
|
|
+ <a-col :flex="'86px'" style="text-align: right">
|
|
|
|
|
+ <a-space direction="vertical" :size="18">
|
|
|
|
|
+ <a-button type="primary" @click="search">
|
|
|
|
|
+ <template #icon>
|
|
|
|
|
+ <icon-search />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ 搜索
|
|
|
|
|
+ </a-button>
|
|
|
|
|
+ <a-button @click="reset">
|
|
|
|
|
+ <template #icon>
|
|
|
|
|
+ <icon-refresh />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ 重置
|
|
|
|
|
+ </a-button>
|
|
|
|
|
+ </a-space>
|
|
|
|
|
+ </a-col>
|
|
|
|
|
+ </a-row>
|
|
|
|
|
+
|
|
|
|
|
+ <a-table :data="data" class="table" :loading="loading" :columns="columns" :pagination="{
|
|
|
|
|
+ showPageSize: true,
|
|
|
|
|
+ showJumper: true,
|
|
|
|
|
+ showTotal: true,
|
|
|
|
|
+ pageSize: pagination.pagesize,
|
|
|
|
|
+ current: pagination.current,
|
|
|
|
|
+ total: pagination.total
|
|
|
|
|
+ }" @page-change="handlePageChange" @page-size-change="handlePageSizeChange">
|
|
|
|
|
+ <template #create_user="{ record }">
|
|
|
|
|
+ <a-avatar :size="30">
|
|
|
|
|
+ <IconUser v-if="!record.avatar" />
|
|
|
|
|
+ <img :alt="record.create_user ?? ''" :src="record.avatar" v-else />
|
|
|
|
|
+ </a-avatar>
|
|
|
|
|
+ {{ record.create_user }}
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <template #name="{ record }">
|
|
|
|
|
+ <a-avatar :size="30">
|
|
|
|
|
+ <IconUser v-if="!record.user_avatar" />
|
|
|
|
|
+ <img :alt="record.name ?? ''" :src="record.user_avatar" v-else />
|
|
|
|
|
+ </a-avatar>
|
|
|
|
|
+ {{ record.name }}
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <template #sex="{ record }">
|
|
|
|
|
+ <icon-man v-if="record.sex === 1" />
|
|
|
|
|
+ <icon-woman v-else-if="record.sex === 2" />
|
|
|
|
|
+ {{ record.sex === 1 ? '男' : (record.sex === 2 ? '女' : '') }}
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <template #auto_run="{ record }">
|
|
|
|
|
+ <a-tag color="green" v-if="record.auto_run">开启</a-tag>
|
|
|
|
|
+ <a-tag color="red" v-else>关闭</a-tag>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <template #num="{ record }">
|
|
|
|
|
+ {{ record.term_num - record.total_num > 0 ? (record.term_num - record.total_num) : '已完成' }}
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <template #state="{ record }">
|
|
|
|
|
+ <div v-if="record.state === 0" class="state">
|
|
|
|
|
+ <div class="circle zero"></div>需登录
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-else-if="record.state === 1" class="state">
|
|
|
|
|
+ <div class="circle one"></div>正常
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-else class="state">
|
|
|
|
|
+ <div class="circle else"></div>状态异常
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <template #auto_time="{ record }">
|
|
|
|
|
+ {{ autoTimeLabel(record.auto_time) }}
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <template #create_time="{ record }">
|
|
|
|
|
+ {{ stramptoTime(record.create_time) }}
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <template #update_time="{ record }">
|
|
|
|
|
+ {{ stramptoTime(record.update_time) }}
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <template #optional="{ record }">
|
|
|
|
|
+ <a-dropdown :popup-max-height="false" trigger="hover">
|
|
|
|
|
+ <a-button>操作 <icon-down /></a-button>
|
|
|
|
|
+ <template #content>
|
|
|
|
|
+ <a-doption @click="SingleRun(record)"><icon-play-circle /> 开始单次乐跑</a-doption>
|
|
|
|
|
+ <a-doption @click="ChangeAutoRun(record)"><icon-translate /> {{ record.auto_run ? '关闭' :
|
|
|
|
|
+ '开启' }}自动乐跑</a-doption>
|
|
|
|
|
+ <a-doption @click="DeleteAccount(record)"><icon-delete /> 解绑账号</a-doption>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </a-dropdown>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </a-table>
|
|
|
|
|
+ </a-card>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup>
|
|
|
|
|
+import { ref, reactive, onMounted, h } from 'vue'
|
|
|
|
|
+import { adminAccountList, deleteAccount, changeAutoRun, singleRun } from '@/api/lepao'
|
|
|
|
|
+import { Modal, Notification, Message } from '@arco-design/web-vue'
|
|
|
|
|
+
|
|
|
|
|
+const queryData = reactive({
|
|
|
|
|
+ area: '',
|
|
|
|
|
+ student_num: '',
|
|
|
|
|
+ user_uuid: '',
|
|
|
|
|
+ email: '',
|
|
|
|
|
+ username: '',
|
|
|
|
|
+ state: -1
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const pagination = reactive({
|
|
|
|
|
+ total: 0,
|
|
|
|
|
+ current: 1, // 默认从第1页开始
|
|
|
|
|
+ pagesize: 20
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const loading = ref(false)
|
|
|
|
|
+const data = ref([])
|
|
|
|
|
+
|
|
|
|
|
+const state = [
|
|
|
|
|
+ { label: '全部', value: -1 }, { label: '需登录', value: 0 }, { label: '正常', value: 1 }, { label: '状态异常', value: 2 }
|
|
|
|
|
+]
|
|
|
|
|
+const areas = ["兰花湖校区跑区", "主校区北跑区", "主校区南跑区"]
|
|
|
|
|
+
|
|
|
|
|
+const columns = [
|
|
|
|
|
+
|
|
|
|
|
+ {
|
|
|
|
|
+ title: 'ID',
|
|
|
|
|
+ dataIndex: 'id',
|
|
|
|
|
+ fixed: 'left',
|
|
|
|
|
+ width: 50
|
|
|
|
|
+ }, {
|
|
|
|
|
+ title: '创建用户',
|
|
|
|
|
+ slotName: 'create_user',
|
|
|
|
|
+ width: 180
|
|
|
|
|
+ }, {
|
|
|
|
|
+ title: '账号名称',
|
|
|
|
|
+ slotName: 'name',
|
|
|
|
|
+ width: 180
|
|
|
|
|
+ }, {
|
|
|
|
|
+ title: '学号',
|
|
|
|
|
+ dataIndex: 'student_num',
|
|
|
|
|
+ width: 115
|
|
|
|
|
+ }, {
|
|
|
|
|
+ title: '性别',
|
|
|
|
|
+ slotName: 'sex',
|
|
|
|
|
+ width: 80
|
|
|
|
|
+ }, {
|
|
|
|
|
+ title: '学院',
|
|
|
|
|
+ dataIndex: 'academy_name',
|
|
|
|
|
+ width: 200
|
|
|
|
|
+ }, {
|
|
|
|
|
+ title: '年级',
|
|
|
|
|
+ dataIndex: 'grade_id',
|
|
|
|
|
+ width: 80
|
|
|
|
|
+ }, {
|
|
|
|
|
+ title: '通知邮箱',
|
|
|
|
|
+ dataIndex: 'email',
|
|
|
|
|
+ width: 200
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '乐跑跑区',
|
|
|
|
|
+ dataIndex: 'area',
|
|
|
|
|
+ width: 150
|
|
|
|
|
+ }, {
|
|
|
|
|
+ title: '累计次数',
|
|
|
|
|
+ dataIndex: 'total_num',
|
|
|
|
|
+ width: 90
|
|
|
|
|
+ }, {
|
|
|
|
|
+ title: '剩余次数',
|
|
|
|
|
+ slotName: 'num',
|
|
|
|
|
+ width: 90
|
|
|
|
|
+ }, {
|
|
|
|
|
+ title: '自动乐跑',
|
|
|
|
|
+ slotName: 'auto_run',
|
|
|
|
|
+ width: 90
|
|
|
|
|
+ }, {
|
|
|
|
|
+ title: '自动乐跑时段',
|
|
|
|
|
+ slotName: 'auto_time',
|
|
|
|
|
+ width: 130
|
|
|
|
|
+ }, {
|
|
|
|
|
+ title: '帐号状态',
|
|
|
|
|
+ slotName: 'state',
|
|
|
|
|
+ width: 100
|
|
|
|
|
+ }, {
|
|
|
|
|
+ title: '添加时间',
|
|
|
|
|
+ slotName: 'create_time',
|
|
|
|
|
+ width: 150
|
|
|
|
|
+ }, {
|
|
|
|
|
+ title: '上次更新时间',
|
|
|
|
|
+ slotName: 'update_time',
|
|
|
|
|
+ width: 150
|
|
|
|
|
+ }, {
|
|
|
|
|
+ title: '操作',
|
|
|
|
|
+ slotName: 'optional',
|
|
|
|
|
+ fixed: 'right',
|
|
|
|
|
+ width: 90
|
|
|
|
|
+ }]
|
|
|
|
|
+
|
|
|
|
|
+const search = () => {
|
|
|
|
|
+ pagination.current = 1
|
|
|
|
|
+ getAccounts()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const reset = () => {
|
|
|
|
|
+ queryData.area = ''
|
|
|
|
|
+ queryData.student_num = ''
|
|
|
|
|
+ queryData.user_uuid = ''
|
|
|
|
|
+ queryData.email = ''
|
|
|
|
|
+ queryData.username = ''
|
|
|
|
|
+ getAccounts()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const getAccounts = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ loading.value = true
|
|
|
|
|
+ const reqData = {
|
|
|
|
|
+ ...queryData,
|
|
|
|
|
+ pagesize: pagination.pagesize,
|
|
|
|
|
+ current: pagination.current
|
|
|
|
|
+ }
|
|
|
|
|
+ const res = await adminAccountList(reqData)
|
|
|
|
|
+ if (!res || res.code !== 0)
|
|
|
|
|
+ return Notification.error({
|
|
|
|
|
+ title: '获取路径数据失败!',
|
|
|
|
|
+ content: res?.msg ?? '请稍后再试'
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ data.value = res.data
|
|
|
|
|
+ pagination.total = res.pagination.total
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ Notification.error({
|
|
|
|
|
+ title: '获取路径数据失败!',
|
|
|
|
|
+ content: error.message || '请稍后再试'
|
|
|
|
|
+ })
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ loading.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 分页 - 页码变化
|
|
|
|
|
+const handlePageChange = (page) => {
|
|
|
|
|
+ pagination.current = page
|
|
|
|
|
+ getAccounts()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 分页 - 每页条数变化
|
|
|
|
|
+const handlePageSizeChange = (size) => {
|
|
|
|
|
+ pagination.pagesize = size
|
|
|
|
|
+ pagination.current = 1 // 页大小变化后回到第一页
|
|
|
|
|
+ getAccounts()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const DeleteAccount = async (item) => {
|
|
|
|
|
+ Modal.confirm({
|
|
|
|
|
+ title: '解绑账号',
|
|
|
|
|
+ content: () => h('div', [
|
|
|
|
|
+ h('p', '您是否要解绑该账号?该操作不可逆')
|
|
|
|
|
+ ]),
|
|
|
|
|
+ onOk: async () => {
|
|
|
|
|
+ const res = await deleteAccount({ id: item.id })
|
|
|
|
|
+ if (!res || res.code !== 0)
|
|
|
|
|
+ return Notification.error({
|
|
|
|
|
+ title: '解绑失败',
|
|
|
|
|
+ content: res?.msg ?? '请稍后再试'
|
|
|
|
|
+ })
|
|
|
|
|
+ Message.success('解绑成功!')
|
|
|
|
|
+ getAccounts()
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const ChangeAutoRun = async (record) => {
|
|
|
|
|
+ const oldValue = record.auto_run;
|
|
|
|
|
+ record.auto_run = oldValue === 1 ? 0 : 1;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await changeAutoRun({ id: record.id });
|
|
|
|
|
+ if (!res || res.code !== 0) {
|
|
|
|
|
+ record.auto_run = oldValue;
|
|
|
|
|
+ Message.error('切换自动乐跑状态失败!')
|
|
|
|
|
+ } else {
|
|
|
|
|
+ Message.success(`自动乐跑状态已${record.auto_run === 1 ? '开启' : '关闭'}`)
|
|
|
|
|
+ getAccounts()
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.log(error)
|
|
|
|
|
+ record.auto_run = oldValue
|
|
|
|
|
+ Message.error('切换自动乐跑状态失败!')
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const SingleRun = async (item) => {
|
|
|
|
|
+ if (item.state !== 1)
|
|
|
|
|
+ return Notification.error({
|
|
|
|
|
+ title: '当前乐跑账号需登录,请登录后再试',
|
|
|
|
|
+ content: '如有疑问请联系RunForge客服'
|
|
|
|
|
+ })
|
|
|
|
|
+ Modal.confirm({
|
|
|
|
|
+ title: '开始乐跑',
|
|
|
|
|
+ content: () => h('div', [
|
|
|
|
|
+ h('p', `您是否要为${item.name}乐跑?若乐跑成功将扣减乐跑次数`)
|
|
|
|
|
+ ]),
|
|
|
|
|
+ onOk: async () => {
|
|
|
|
|
+ const res = await singleRun({ student_num: item.student_num })
|
|
|
|
|
+ if (!res || res.code !== 0)
|
|
|
|
|
+ return Notification.error({
|
|
|
|
|
+ title: '提交乐跑任务失败',
|
|
|
|
|
+ content: res?.msg ?? '请稍后再试'
|
|
|
|
|
+ })
|
|
|
|
|
+ Message.success('提交乐跑任务成功!')
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+onMounted(() => {
|
|
|
|
|
+ getAccounts()
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const stramptoTime = (time) => {
|
|
|
|
|
+ return new Date(time).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const auto_time = Array.from({ length: 17 }, (_, i) => {
|
|
|
|
|
+ const hour = i + 7
|
|
|
|
|
+ return {
|
|
|
|
|
+ label: `${hour} ~ ${hour + 1}时`,
|
|
|
|
|
+ value: hour
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const autoTimeLabel = (val) => {
|
|
|
|
|
+ const match = auto_time.find(item => item.value === val)
|
|
|
|
|
+ return match ? match.label : '-'
|
|
|
|
|
+}
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped>
|
|
|
|
|
+.container {
|
|
|
|
|
+ padding: 0 20px 20px 20px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.table {
|
|
|
|
|
+ margin-top: 15px;
|
|
|
|
|
+
|
|
|
|
|
+ .state {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+
|
|
|
|
|
+ .circle {
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ height: 8px;
|
|
|
|
|
+ min-height: 8px;
|
|
|
|
|
+ width: 8px;
|
|
|
|
|
+ min-width: 8px;
|
|
|
|
|
+ margin-right: 5px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .zero {
|
|
|
|
|
+ background-color: rgb(var(--orange-6));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .one {
|
|
|
|
|
+ background-color: rgb(var(--green-6));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .else {
|
|
|
|
|
+ background-color: rgb(var(--red-6));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|