|
|
@@ -4,7 +4,7 @@
|
|
|
|
|
|
<a-card title="抢课任务">
|
|
|
<div class="buttonGroup buttonGroup-desktop">
|
|
|
- <a-button type="primary" size="large" @click="openModal()">
|
|
|
+ <a-button v-if="canSubmit" type="primary" size="large" @click="openModal()">
|
|
|
<template #icon><icon-plus /></template>
|
|
|
提交任务
|
|
|
</a-button>
|
|
|
@@ -15,7 +15,7 @@
|
|
|
</div>
|
|
|
|
|
|
<div class="buttonGroup buttonGroup-mobile">
|
|
|
- <a-button type="primary" size="large" @click="openModal()">
|
|
|
+ <a-button v-if="canSubmit" type="primary" size="large" @click="openModal()">
|
|
|
<template #icon><icon-plus /></template>
|
|
|
提交任务
|
|
|
</a-button>
|
|
|
@@ -38,7 +38,11 @@
|
|
|
<a-table-column title="ID" data-index="id" :width="80" />
|
|
|
<a-table-column title="备注" data-index="name" :width="140" ellipsis tooltip />
|
|
|
<a-table-column title="学号" data-index="student_num" :width="130" />
|
|
|
- <a-table-column title="选课批次" data-index="jx0502zbid" :width="220" ellipsis tooltip />
|
|
|
+ <a-table-column title="选课批次" :width="220" ellipsis tooltip>
|
|
|
+ <template #cell="{ record }">
|
|
|
+ {{ record.batch_name || record.jx0502zbid || '-' }}
|
|
|
+ </template>
|
|
|
+ </a-table-column>
|
|
|
<a-table-column title="目标课程" :width="260">
|
|
|
<template #cell="{ record }">
|
|
|
<a-space wrap>
|
|
|
@@ -58,11 +62,29 @@
|
|
|
<a-table-column title="更新时间" :width="180">
|
|
|
<template #cell="{ record }">{{ formatTime(record.update_time) }}</template>
|
|
|
</a-table-column>
|
|
|
- <a-table-column title="操作" :width="150" :fixed="tableFixed('right')">
|
|
|
+ <a-table-column title="操作" :width="220" :fixed="tableFixed('right')">
|
|
|
<template #cell="{ record }">
|
|
|
- <a-space>
|
|
|
- <a-button type="text" size="small" @click="openModal(record)">编辑</a-button>
|
|
|
- <a-popconfirm content="确认取消该抢课任务?" @ok="cancelTask(record)">
|
|
|
+ <a-space wrap>
|
|
|
+ <a-button
|
|
|
+ v-if="canControl && canStart(record)"
|
|
|
+ type="text"
|
|
|
+ size="small"
|
|
|
+ status="success"
|
|
|
+ @click="startTask(record)"
|
|
|
+ >
|
|
|
+ 开启
|
|
|
+ </a-button>
|
|
|
+ <a-button
|
|
|
+ v-if="canControl && canPause(record)"
|
|
|
+ type="text"
|
|
|
+ size="small"
|
|
|
+ status="warning"
|
|
|
+ @click="pauseTask(record)"
|
|
|
+ >
|
|
|
+ 暂停
|
|
|
+ </a-button>
|
|
|
+ <a-button v-if="canSubmit" type="text" size="small" :disabled="!canEdit(record)" @click="openModal(record)">编辑</a-button>
|
|
|
+ <a-popconfirm v-if="canSubmit" content="确认取消该抢课任务?" @ok="cancelTask(record)">
|
|
|
<a-button type="text" status="danger" size="small" :disabled="!canCancel(record)">取消</a-button>
|
|
|
</a-popconfirm>
|
|
|
</a-space>
|
|
|
@@ -71,7 +93,7 @@
|
|
|
</template>
|
|
|
<template #empty>
|
|
|
<a-empty description="暂无抢课任务">
|
|
|
- <a-button type="primary" @click="openModal()">提交第一个任务</a-button>
|
|
|
+ <a-button v-if="canSubmit" type="primary" @click="openModal()">提交第一个任务</a-button>
|
|
|
</a-empty>
|
|
|
</template>
|
|
|
</a-table>
|
|
|
@@ -83,14 +105,19 @@
|
|
|
:width="isMobile ? '100%' : '720px'"
|
|
|
draggable
|
|
|
esc-to-close
|
|
|
- @ok="submitTask"
|
|
|
+ :ok-loading="saving"
|
|
|
+ :on-before-ok="submitTask"
|
|
|
>
|
|
|
<a-form :model="form" layout="vertical" class="task-form">
|
|
|
<a-form-item label="备注名" required>
|
|
|
<a-input v-model="form.name" placeholder="例如:张三选修课" allow-clear />
|
|
|
</a-form-item>
|
|
|
- <a-form-item label="选课批次 ID" required>
|
|
|
- <a-input v-model="form.jx0502zbid" placeholder="jx0502zbid" allow-clear />
|
|
|
+ <a-form-item label="选课批次" required>
|
|
|
+ <a-select v-model="form.batch_id" placeholder="请选择抢课批次" allow-clear allow-search>
|
|
|
+ <a-option v-for="item in batchOptions" :key="item.id" :value="item.id">
|
|
|
+ {{ item.name }}({{ item.jx0502zbid }})
|
|
|
+ </a-option>
|
|
|
+ </a-select>
|
|
|
</a-form-item>
|
|
|
<a-row :gutter="16">
|
|
|
<a-col :xs="24" :sm="12">
|
|
|
@@ -106,6 +133,7 @@
|
|
|
</a-row>
|
|
|
<a-form-item label="课程名称(每行一个,精确匹配)">
|
|
|
<a-textarea v-model="form.coursesText" :auto-size="{ minRows: 3 }" placeholder="每行填写一门课程名称" />
|
|
|
+ <template #extra>{{ targetHint }}</template>
|
|
|
</a-form-item>
|
|
|
<a-form-item label="课程分组(每行一个,例如羽毛球)">
|
|
|
<a-textarea v-model="form.courseGroupsText" :auto-size="{ minRows: 2 }" placeholder="每行填写一个课程分组" />
|
|
|
@@ -113,7 +141,8 @@
|
|
|
<a-row :gutter="16">
|
|
|
<a-col :xs="24" :sm="12">
|
|
|
<a-form-item label="抢课间隔(ms)">
|
|
|
- <a-input-number v-model="form.interval_ms" :min="200" :max="10000" style="width: 100%" />
|
|
|
+ <a-input-number v-model="form.interval_ms" :min="minInterval" :max="10000" style="width: 100%" />
|
|
|
+ <template #extra>当前最低 {{ minInterval }}ms</template>
|
|
|
</a-form-item>
|
|
|
</a-col>
|
|
|
<a-col :xs="24" :sm="12">
|
|
|
@@ -128,20 +157,31 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { reactive, ref, onMounted, onUnmounted } from 'vue'
|
|
|
-import { Notification } from '@arco-design/web-vue'
|
|
|
-import { getQkTask, saveQkTask, deleteQkTask } from '@/api/qk'
|
|
|
+import { reactive, ref, onMounted, onUnmounted, computed } from 'vue'
|
|
|
+import { Notification, Modal } from '@arco-design/web-vue'
|
|
|
+import { getQkTask, saveQkTask, deleteQkTask, startQkTask, pauseQkTask, getQkBatchList } from '@/api/qk'
|
|
|
import { useResponsiveTable } from '@/hooks/useResponsiveTable'
|
|
|
+import { hasAnyPermission, QK_ASSISTANT_CONTROL_CODES, QK_ASSISTANT_SUBMIT_CODES } from '@/utils/permission'
|
|
|
+import {
|
|
|
+ getMinQkInterval,
|
|
|
+ getQkTargetHint,
|
|
|
+ splitQkLines,
|
|
|
+ validateQkTaskForm
|
|
|
+} from '@/utils/qkTaskRules'
|
|
|
|
|
|
const { tableFixed, isMobile } = useResponsiveTable()
|
|
|
+const canSubmit = computed(() => hasAnyPermission(QK_ASSISTANT_SUBMIT_CODES))
|
|
|
+const canControl = computed(() => hasAnyPermission(QK_ASSISTANT_CONTROL_CODES))
|
|
|
|
|
|
const loading = ref(false)
|
|
|
+const saving = ref(false)
|
|
|
const visible = ref(false)
|
|
|
const data = ref([])
|
|
|
+const batchOptions = ref([])
|
|
|
const form = reactive({
|
|
|
id: '',
|
|
|
name: '',
|
|
|
- jx0502zbid: '',
|
|
|
+ batch_id: '',
|
|
|
student_num: '',
|
|
|
password: '',
|
|
|
coursesText: '',
|
|
|
@@ -150,7 +190,13 @@ const form = reactive({
|
|
|
interval_ms: 500
|
|
|
})
|
|
|
|
|
|
-const splitLines = (value) => String(value || '').split(/[\n,,]/).map(item => item.trim()).filter(Boolean)
|
|
|
+const targetCount = computed(() => {
|
|
|
+ return splitQkLines(form.coursesText).length + splitQkLines(form.courseGroupsText).length
|
|
|
+})
|
|
|
+const minInterval = computed(() => getMinQkInterval(targetCount.value))
|
|
|
+const targetHint = computed(() => getQkTargetHint(form.coursesText, form.courseGroupsText))
|
|
|
+
|
|
|
+const splitLines = splitQkLines
|
|
|
|
|
|
const formatTime = (time) => {
|
|
|
if (!time) return '-'
|
|
|
@@ -164,6 +210,7 @@ const formatTime = (time) => {
|
|
|
}
|
|
|
|
|
|
const statusText = (status) => ({
|
|
|
+ paused: '未开始',
|
|
|
pending: '待分配',
|
|
|
assigned: '已分配',
|
|
|
running: '抢课中',
|
|
|
@@ -173,6 +220,7 @@ const statusText = (status) => ({
|
|
|
}[status] || status)
|
|
|
|
|
|
const statusColor = (status) => ({
|
|
|
+ paused: 'gray',
|
|
|
pending: 'blue',
|
|
|
assigned: 'arcoblue',
|
|
|
running: 'orange',
|
|
|
@@ -181,14 +229,17 @@ const statusColor = (status) => ({
|
|
|
cancelled: 'gray'
|
|
|
}[status] || 'gray')
|
|
|
|
|
|
-const canCancel = (record) => ['pending', 'failed'].includes(record.status)
|
|
|
+const canStart = (record) => ['paused', 'failed'].includes(record.status)
|
|
|
+const canPause = (record) => ['pending', 'assigned', 'running'].includes(record.status)
|
|
|
+const canEdit = (record) => ['paused', 'failed', 'cancelled'].includes(record.status)
|
|
|
+const canCancel = (record) => ['paused', 'pending', 'failed'].includes(record.status)
|
|
|
const resultText = (record) => record.error_msg || record.result_json?.message || record.result_json?.course || '-'
|
|
|
|
|
|
const resetForm = () => {
|
|
|
Object.assign(form, {
|
|
|
id: '',
|
|
|
name: '',
|
|
|
- jx0502zbid: '',
|
|
|
+ batch_id: '',
|
|
|
student_num: '',
|
|
|
password: '',
|
|
|
coursesText: '',
|
|
|
@@ -204,7 +255,7 @@ const openModal = (record) => {
|
|
|
Object.assign(form, {
|
|
|
id: record.id,
|
|
|
name: record.name,
|
|
|
- jx0502zbid: record.jx0502zbid,
|
|
|
+ batch_id: record.batch_id || '',
|
|
|
student_num: record.student_num,
|
|
|
coursesText: (record.courses || []).join('\n'),
|
|
|
courseGroupsText: (record.course_groups || []).join('\n'),
|
|
|
@@ -215,6 +266,32 @@ const openModal = (record) => {
|
|
|
visible.value = true
|
|
|
}
|
|
|
|
|
|
+const buildCopySnapshot = (validated) => ({
|
|
|
+ name: validated.name,
|
|
|
+ batch_id: validated.batch_id,
|
|
|
+ student_num: validated.student_num,
|
|
|
+ password: validated.password || '',
|
|
|
+ coursesText: validated.courses.join('\n'),
|
|
|
+ courseGroupsText: validated.courseGroups.join('\n'),
|
|
|
+ enable_ggxxk: form.enable_ggxxk,
|
|
|
+ interval_ms: validated.intervalMs
|
|
|
+})
|
|
|
+
|
|
|
+const openModalFromSnapshot = (snapshot) => {
|
|
|
+ resetForm()
|
|
|
+ Object.assign(form, snapshot)
|
|
|
+ visible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+const loadBatches = async () => {
|
|
|
+ try {
|
|
|
+ const res = await getQkBatchList()
|
|
|
+ if (res?.code === 0) {
|
|
|
+ batchOptions.value = res.data || []
|
|
|
+ }
|
|
|
+ } catch (_) {}
|
|
|
+}
|
|
|
+
|
|
|
const getList = async (silent = false) => {
|
|
|
try {
|
|
|
if (!silent) loading.value = true
|
|
|
@@ -238,24 +315,45 @@ const getList = async (silent = false) => {
|
|
|
const refreshList = () => getList()
|
|
|
|
|
|
const submitTask = async () => {
|
|
|
- const payload = {
|
|
|
- id: form.id || undefined,
|
|
|
- name: form.name,
|
|
|
- jx0502zbid: form.jx0502zbid,
|
|
|
- student_num: form.student_num,
|
|
|
- password: form.password,
|
|
|
- courses: splitLines(form.coursesText),
|
|
|
- course_groups: splitLines(form.courseGroupsText),
|
|
|
- enable_ggxxk: form.enable_ggxxk,
|
|
|
- interval_ms: form.interval_ms
|
|
|
+ const validated = validateQkTaskForm(form)
|
|
|
+ if (!validated.ok) {
|
|
|
+ Notification.error({ title: '保存抢课任务失败', content: validated.message })
|
|
|
+ return false
|
|
|
}
|
|
|
- const res = await saveQkTask(payload)
|
|
|
- if (!res || res.code !== 0) {
|
|
|
- return Notification.error({ title: '保存抢课任务失败', content: res?.msg || '请稍后再试' })
|
|
|
+ try {
|
|
|
+ saving.value = true
|
|
|
+ const payload = {
|
|
|
+ id: form.id || undefined,
|
|
|
+ name: validated.name,
|
|
|
+ batch_id: validated.batch_id,
|
|
|
+ student_num: validated.student_num,
|
|
|
+ password: validated.password,
|
|
|
+ courses: validated.courses,
|
|
|
+ course_groups: validated.courseGroups,
|
|
|
+ enable_ggxxk: form.enable_ggxxk,
|
|
|
+ interval_ms: validated.intervalMs
|
|
|
+ }
|
|
|
+ const res = await saveQkTask(payload)
|
|
|
+ if (!res || res.code !== 0) {
|
|
|
+ Notification.error({ title: '保存抢课任务失败', content: res?.msg || '请稍后再试' })
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ const snapshot = buildCopySnapshot(validated)
|
|
|
+ await getList()
|
|
|
+ Modal.confirm({
|
|
|
+ title: '保存成功',
|
|
|
+ content: '是否复制此任务并继续提交新任务?',
|
|
|
+ okText: '复制任务',
|
|
|
+ cancelText: '关闭',
|
|
|
+ onOk: () => openModalFromSnapshot(snapshot)
|
|
|
+ })
|
|
|
+ return true
|
|
|
+ } catch (error) {
|
|
|
+ Notification.error({ title: '保存抢课任务失败', content: error.message || '请稍后再试' })
|
|
|
+ return false
|
|
|
+ } finally {
|
|
|
+ saving.value = false
|
|
|
}
|
|
|
- Notification.success({ title: '保存成功' })
|
|
|
- visible.value = false
|
|
|
- getList()
|
|
|
}
|
|
|
|
|
|
const cancelTask = async (record) => {
|
|
|
@@ -267,6 +365,24 @@ const cancelTask = async (record) => {
|
|
|
getList()
|
|
|
}
|
|
|
|
|
|
+const startTask = async (record) => {
|
|
|
+ const res = await startQkTask({ id: record.id })
|
|
|
+ if (!res || res.code !== 0) {
|
|
|
+ return Notification.error({ title: '开启失败', content: res?.msg || '请稍后再试' })
|
|
|
+ }
|
|
|
+ Notification.success({ title: '已开启', content: '任务已进入待分配队列' })
|
|
|
+ getList()
|
|
|
+}
|
|
|
+
|
|
|
+const pauseTask = async (record) => {
|
|
|
+ const res = await pauseQkTask({ id: record.id })
|
|
|
+ if (!res || res.code !== 0) {
|
|
|
+ return Notification.error({ title: '暂停失败', content: res?.msg || '请稍后再试' })
|
|
|
+ }
|
|
|
+ Notification.success({ title: '已暂停' })
|
|
|
+ getList()
|
|
|
+}
|
|
|
+
|
|
|
let pollTimer = null
|
|
|
|
|
|
const startPolling = () => {
|
|
|
@@ -283,6 +399,7 @@ const stopPolling = () => {
|
|
|
}
|
|
|
|
|
|
onMounted(() => {
|
|
|
+ loadBatches()
|
|
|
getList()
|
|
|
startPolling()
|
|
|
})
|