fix bugs
continuous-integration/drone/push Build is passing

This commit is contained in:
2025-12-12 17:54:40 +08:00
parent 5b806e29b7
commit 669f29878b
27 changed files with 2781 additions and 4256 deletions
+78 -1
View File
@@ -88,7 +88,8 @@ export const saveAndLockSchedule = (competitionId) => {
return request({
url: '/martial/schedule/save-and-lock',
method: 'post',
data: { competitionId }
data: { competitionId },
timeout: 60000 // 设置60秒超时,因为锁定操作可能耗时较长
})
}
@@ -103,6 +104,82 @@ export const saveDraftSchedule = (data) => {
return request({
url: '/martial/schedule/save-draft',
method: 'post',
data,
timeout: 60000 // 设置60秒超时,因为保存草稿可能涉及大量数据
})
}
/**
* 触发自动编排
* @param {Number} competitionId - 赛事ID
*/
export const triggerAutoArrange = (competitionId) => {
return request({
url: '/martial/schedule/auto-arrange',
method: 'post',
data: { competitionId },
timeout: 60000 // 设置60秒超时,因为自动编排可能耗时较长
})
}
/**
* 移动赛程分组到指定场地和时间段
* @param {Object} data - 移动请求数据
* @param {Number} data.groupId - 分组ID
* @param {Number} data.targetVenueId - 目标场地ID
* @param {Number} data.targetTimeSlotIndex - 目标时间段索引
*/
export const moveScheduleGroup = (data) => {
return request({
url: '/martial/schedule/move-group',
method: 'post',
data
})
}
// ==================== 调度功能接口 ====================
/**
* 获取调度数据
* @param {Object} params - 查询参数
* @param {Number} params.competitionId - 赛事ID
* @param {Number} params.venueId - 场地ID
* @param {Number} params.timeSlotIndex - 时间段索引
*/
export const getDispatchData = (params) => {
return request({
url: '/martial/schedule/dispatch-data',
method: 'get',
params
})
}
/**
* 调整出场顺序
* @param {Object} data - 调整请求数据
* @param {Number} data.detailId - 编排明细ID
* @param {Number} data.participantId - 参赛者记录ID
* @param {String} data.action - 调整动作(move_up/move_down/swap)
* @param {Number} data.targetOrder - 目标顺序(交换时使用)
*/
export const adjustOrder = (data) => {
return request({
url: '/martial/schedule/adjust-order',
method: 'post',
data
})
}
/**
* 批量保存调度
* @param {Object} data - 保存调度数据
* @param {Number} data.competitionId - 赛事ID
* @param {Array} data.adjustments - 调整列表
*/
export const saveDispatch = (data) => {
return request({
url: '/martial/schedule/save-dispatch',
method: 'post',
data
})
}
+15
View File
@@ -88,6 +88,21 @@ export const exportReferees = (params) => {
})
}
/**
* 导出工作人员名单(Excel
* @param {Object} params - 导出参数
* @param {Number} params.competitionId - 赛事ID
* @param {String} params.role - 角色类型(可选)
*/
export const exportStaff = (params) => {
return request({
url: '/api/blade-martial/export/staff',
method: 'get',
params,
responseType: 'blob'
})
}
/**
* 导出奖牌榜(Excel
* @param {Object} params - 导出参数
+75 -22
View File
@@ -99,9 +99,8 @@ export const batchSendInvites = (data) => {
*/
export const resendInvite = (id) => {
return request({
url: '/api/blade-martial/judgeInvite/resend',
method: 'post',
params: { id }
url: `/api/blade-martial/judgeInvite/resend/${id}`,
method: 'post'
})
}
@@ -124,14 +123,13 @@ export const replyInvite = (data) => {
/**
* 取消邀请
* @param {Number} id - 邀请ID
* @param {String} cancelReason - 取消原因
* @param {String} reason - 取消原因
*/
export const cancelInvite = (id, cancelReason) => {
export const cancelInvite = (id, reason) => {
return request({
url: '/api/blade-martial/judgeInvite/cancel',
url: `/api/blade-martial/judgeInvite/cancel/${id}`,
method: 'post',
params: { id },
data: { cancelReason }
params: { reason }
})
}
@@ -141,9 +139,8 @@ export const cancelInvite = (id, cancelReason) => {
*/
export const confirmInvite = (id) => {
return request({
url: '/api/blade-martial/judgeInvite/confirm',
method: 'post',
params: { id }
url: `/api/blade-martial/judgeInvite/confirm/${id}`,
method: 'post'
})
}
@@ -173,15 +170,14 @@ export const getAcceptedJudges = (competitionId) => {
/**
* 从裁判库导入
* @param {Object} data - 导入参数
* @param {Number} data.competitionId - 赛事ID
* @param {Array} data.judgeIds - 裁判ID数组(从裁判库选择)
* @param {Number} competitionId - 赛事ID
* @param {String} judgeIds - 裁判ID(逗号分隔)
*/
export const importFromJudgePool = (data) => {
export const importFromJudgePool = (competitionId, judgeIds) => {
return request({
url: '/api/blade-martial/judgeInvite/import-from-pool',
url: '/api/blade-martial/judgeInvite/import/pool',
method: 'post',
data
params: { competitionId, judgeIds }
})
}
@@ -201,13 +197,70 @@ export const exportInvites = (params) => {
/**
* 发送提醒消息
* @param {Number} id - 邀请ID
* @param {String} reminderMessage - 提醒消息
* @param {String} message - 提醒消息
*/
export const sendReminder = (id, reminderMessage) => {
export const sendReminder = (id, message) => {
return request({
url: '/api/blade-martial/judgeInvite/send-reminder',
url: `/api/blade-martial/judgeInvite/reminder/${id}`,
method: 'post',
params: { id },
data: { reminderMessage }
params: { message }
})
}
/**
* 生成邀请码
* @param {Object} data - 生成参数
* @param {Number} data.competitionId - 赛事ID
* @param {Number} data.judgeId - 评委ID
* @param {String} data.role - 角色(judge/chief_judge
* @param {Number} data.venueId - 场地ID(普通评委必填)
* @param {String} data.projects - 项目列表(JSON
* @param {Number} data.expireDays - 过期天数(默认30
*/
export const generateInviteCode = (data) => {
return request({
url: '/api/blade-martial/judgeInvite/generate',
method: 'post',
data
})
}
/**
* 批量生成邀请码
* @param {Object} data - 批量生成参数
* @param {Number} data.competitionId - 赛事ID
* @param {Array} data.judgeIds - 评委ID数组
* @param {String} data.role - 角色(默认judge
* @param {Number} data.expireDays - 过期天数(默认30
*/
export const batchGenerateInviteCode = (data) => {
return request({
url: '/api/blade-martial/judgeInvite/generate/batch',
method: 'post',
data
})
}
/**
* 重新生成邀请码
* @param {Number} inviteId - 邀请ID
*/
export const regenerateInviteCode = (inviteId) => {
return request({
url: `/api/blade-martial/judgeInvite/regenerate/${inviteId}`,
method: 'put'
})
}
/**
* 查询评委的邀请码
* @param {Number} competitionId - 赛事ID
* @param {Number} judgeId - 评委ID
*/
export const getInviteByJudge = (competitionId, judgeId) => {
return request({
url: '/api/blade-martial/judgeInvite/byJudge',
method: 'get',
params: { competitionId, judgeId }
})
}
+49
View File
@@ -35,6 +35,30 @@ export const getProjectDetail = (id) => {
})
}
/**
* 新增项目
* @param {Object} data - 项目数据
*/
export const addProject = (data) => {
return request({
url: '/api/martial/project/save',
method: 'post',
data
})
}
/**
* 修改项目
* @param {Object} data - 项目数据
*/
export const updateProject = (data) => {
return request({
url: '/api/martial/project/update',
method: 'post',
data
})
}
/**
* 新增或修改项目
* @param {Object} data - 项目数据
@@ -73,6 +97,31 @@ export const removeProject = (ids) => {
})
}
/**
* 导入项目
* @param {Object} data - 导入数据
*/
export const importProjects = (data) => {
return request({
url: '/api/martial/project/import',
method: 'post',
data
})
}
/**
* 导出项目
* @param {Object} params - 导出参数
*/
export const exportProjects = (params) => {
return request({
url: '/api/martial/project/export',
method: 'get',
params,
responseType: 'blob'
})
}
/**
* 获取赛事的项目列表(不分页,用于下拉选择)
* @param {Number} competitionId - 赛事ID
+39
View File
@@ -232,3 +232,42 @@ export const exportResults = (params) => {
responseType: 'blob'
})
}
/**
* 导出获奖名单
* @param {Object} params - 查询参数
*/
export const exportAwardList = (params) => {
return request({
url: '/api/blade-martial/result/export-award',
method: 'get',
params,
responseType: 'blob'
})
}
/**
* 生成单个证书
* @param {Object} data - 证书参数
*/
export const generateCertificate = (data) => {
return request({
url: '/api/blade-martial/result/certificate/generate',
method: 'post',
data,
responseType: 'blob'
})
}
/**
* 批量生成证书
* @param {Object} data - 批量证书参数
*/
export const batchGenerateCertificates = (data) => {
return request({
url: '/api/blade-martial/result/certificate/batch-generate',
method: 'post',
data,
responseType: 'blob'
})
}
+115
View File
@@ -0,0 +1,115 @@
import request from '@/axios'
// 获取赛事列表
export const getCompetitionList = (params) => {
return request({
url: '/api/martial/competition/list',
method: 'get',
params
})
}
// 获取赛事规程(小程序端)
export const getCompetitionRules = (params) => {
return request({
url: '/api/martial/competition/rules',
method: 'get',
params
})
}
// ==================== 附件管理 ====================
// 获取附件列表
export const getAttachmentList = (params) => {
return request({
url: '/api/martial/competition/rules/attachment/list',
method: 'get',
params
})
}
// 保存附件
export const saveAttachment = (data) => {
return request({
url: '/api/martial/competition/rules/attachment/save',
method: 'post',
data
})
}
// 删除附件
export const removeAttachment = (params) => {
return request({
url: '/api/martial/competition/rules/attachment/remove',
method: 'post',
params
})
}
// ==================== 章节管理 ====================
// 获取章节列表
export const getChapterList = (params) => {
return request({
url: '/api/martial/competition/rules/chapter/list',
method: 'get',
params
})
}
// 保存章节
export const saveChapter = (data) => {
return request({
url: '/api/martial/competition/rules/chapter/save',
method: 'post',
data
})
}
// 删除章节
export const removeChapter = (params) => {
return request({
url: '/api/martial/competition/rules/chapter/remove',
method: 'post',
params
})
}
// ==================== 章节内容管理 ====================
// 获取章节内容列表
export const getContentList = (params) => {
return request({
url: '/api/martial/competition/rules/content/list',
method: 'get',
params
})
}
// 保存章节内容
export const saveContent = (data) => {
return request({
url: '/api/martial/competition/rules/content/save',
method: 'post',
data
})
}
// 批量保存章节内容
export const batchSaveContents = (data) => {
return request({
url: '/api/martial/competition/rules/content/batch-save',
method: 'post',
data
})
}
// 删除章节内容
export const removeContent = (params) => {
return request({
url: '/api/martial/competition/rules/content/remove',
method: 'post',
params
})
}
+1 -1
View File
@@ -19,7 +19,7 @@ import { Base64 } from 'js-base64';
import { baseUrl } from '@/config/env';
import crypto from '@/utils/crypto';
axios.defaults.timeout = 10000;
axios.defaults.timeout = 60000; // 60秒超时,支持编排等耗时操作
//返回其他状态吗
axios.defaults.validateStatus = function (status) {
return status >= 200 && status <= 500; // 默认的
+24 -16
View File
@@ -71,22 +71,6 @@ export default [
},
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/order/index.vue'),
},
{
path: 'schedule/list',
name: '编排',
meta: {
keepAlive: false,
},
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/schedule/index.vue'),
},
{
path: 'dispatch/list',
name: '调度',
meta: {
keepAlive: false,
},
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/dispatch/index.vue'),
},
{
path: 'banner/index',
name: '轮播图管理',
@@ -119,6 +103,22 @@ export default [
},
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/participant/index.vue'),
},
{
path: 'schedule/list',
name: '编排',
meta: {
keepAlive: false,
},
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/schedule/index.vue'),
},
{
path: 'dispatch/list',
name: '调度',
meta: {
keepAlive: false,
},
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/dispatch/index.vue'),
},
// 新增页面 - P0核心页面
{
path: 'project/list',
@@ -218,6 +218,14 @@ export default [
},
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/activity/index.vue'),
},
{
path: 'rules/index',
name: '赛事规程管理',
meta: {
keepAlive: true,
},
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/rules/index.vue'),
},
],
},
];
+90 -3
View File
@@ -32,6 +32,27 @@
show-overflow-tooltip
/>
<el-table-column
prop="competitionCode"
label="比赛编码"
width="150"
align="center"
>
<template #default="scope">
<el-tag
v-if="scope.row.competitionCode"
type="success"
effect="dark"
size="default"
style="font-family: monospace; font-weight: bold; cursor: pointer;"
@click="copyToClipboard(scope.row.competitionCode, '比赛编码')"
>
{{ scope.row.competitionCode }}
</el-tag>
<el-tag v-else type="info" size="small">未设置</el-tag>
</template>
</el-table-column>
<el-table-column
prop="organizer"
label="主办单位"
@@ -171,6 +192,31 @@
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="比赛编码" prop="competitionCode">
<el-input
v-model="formData.competitionCode"
placeholder="例如:WS2025 (留空自动生成)"
maxlength="50"
>
<template #append>
<el-button
v-if="formData.competitionCode"
icon="el-icon-document-copy"
@click="copyToClipboard(formData.competitionCode, '比赛编码')"
>
复制
</el-button>
</template>
</el-input>
<div style="font-size: 12px; color: #909399; margin-top: 4px;">
💡 用于评委小程序登录建议格式项目简称+年份如WS2025
</div>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="主办单位" prop="organizer">
<el-input
@@ -179,9 +225,6 @@
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="举办地点" prop="location">
<el-input
@@ -190,6 +233,9 @@
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="比赛场馆" prop="venue">
<el-input
@@ -776,6 +822,7 @@ export default {
},
formData: {
competitionName: '',
competitionCode: '', // 比赛编码
organizer: '',
location: '',
venue: '',
@@ -1433,6 +1480,7 @@ export default {
resetFormData() {
this.formData = {
competitionName: '',
competitionCode: '', // 比赛编码
organizer: '',
location: '',
venue: '',
@@ -1592,6 +1640,45 @@ export default {
}
// 未开始(默认状态)
return 1;
},
// 复制到剪贴板
copyToClipboard(text, label) {
if (!text) {
this.$message.warning(`${label}为空`);
return;
}
// 使用现代API复制
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(() => {
this.$message.success(`${label}已复制: ${text}`);
}).catch(() => {
this.fallbackCopyToClipboard(text, label);
});
} else {
this.fallbackCopyToClipboard(text, label);
}
},
// 降级复制方法(兼容旧浏览器)
fallbackCopyToClipboard(text, label) {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.top = '-9999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
this.$message.success(`${label}已复制: ${text}`);
} catch (err) {
this.$message.error('复制失败,请手动复制');
}
document.body.removeChild(textArea);
}
}
};
+272 -58
View File
@@ -11,13 +11,27 @@
</div>
<div class="tab-content">
<!-- 场地选择器 -->
<div class="venue-selector">
<el-button
v-for="venue in venues"
:key="venue.id"
size="small"
:type="selectedVenueId === venue.id ? 'primary' : ''"
@click="selectedVenueId = venue.id; loadDispatchData()"
>
{{ venue.venueName }}
</el-button>
</div>
<!-- 时间段选择器 -->
<div class="time-selector">
<el-button
v-for="(time, index) in timeSlots"
:key="index"
size="small"
:type="selectedTime === index ? 'primary' : ''"
@click="selectedTime = index"
@click="selectedTime = index; loadDispatchData()"
>
{{ time }}
</el-button>
@@ -65,7 +79,7 @@
<el-table-column label="操作" width="100" align="center">
<template #default="scope">
<el-button
type="text"
link
size="small"
@click="handleMoveUp(index, scope.$index)"
:disabled="scope.$index === 0"
@@ -75,7 +89,7 @@
<img src="/img/图标 3@3x.png" class="move-icon" alt="上移" />
</el-button>
<el-button
type="text"
link
size="small"
@click="handleMoveDown(index, scope.$index)"
:disabled="scope.$index === group.items.length - 1"
@@ -88,110 +102,290 @@
</el-table-column>
</el-table>
</div>
<!-- 保存按钮 -->
<div class="dispatch-footer" v-if="dispatchGroups.length > 0">
<el-button @click="goBack">返回</el-button>
<el-button type="primary" @click="handleSaveDispatch" :disabled="!hasChanges">
保存调度
</el-button>
</div>
</div>
</div>
</div>
</template>
<script>
import { getVenuesByCompetition } from '@/api/martial/venue'
import { getCompetitionDetail } from '@/api/martial/competition'
import { getDispatchData, saveDispatch, getScheduleResult } from '@/api/martial/activitySchedule'
export default {
name: 'MartialDispatchList',
data() {
return {
orderId: null,
selectedTime: 1,
timeSlots: [
'2025年11月6日上午8:30',
'2025年11月6日下午13:00',
'2025年11月7日上午8:30'
],
dispatchGroups: [
{
title: '1. 小学组小组赛男女类',
type: '集体',
count: '2队',
code: '1101',
venueType: 1,
viewMode: 'dispatch',
items: [
{ schoolUnit: '少林寺武校', completed: true, refereed: false },
{ schoolUnit: '访河社区', completed: false, refereed: false }
]
},
{
title: '1. 小学组小组赛男女类',
type: '单人',
count: '3队',
code: '1组',
venueType: 2,
viewMode: 'dispatch',
items: [
{ schoolUnit: '少林寺武校', completed: true, refereed: true },
{ schoolUnit: '访河社区', completed: false, refereed: false },
{ schoolUnit: '少林寺武校', completed: true, refereed: true }
]
},
{
title: '2. 中学组决赛',
type: '集体',
count: '4队',
code: '2101',
venueType: 1,
viewMode: 'dispatch',
items: [
{ schoolUnit: '成都体育学院', completed: true, refereed: true },
{ schoolUnit: '武侯实验中学', completed: true, refereed: false },
{ schoolUnit: '石室中学', completed: false, refereed: false },
{ schoolUnit: '七中育才', completed: false, refereed: false }
]
}
]
competitionId: null,
loading: false,
selectedTime: 0,
selectedVenueId: null,
venues: [], // 场地列表
timeSlots: [], // 时间段列表
dispatchGroups: [], // 调度分组列表
hasChanges: false, // 是否有未保存的更改
originalData: null // 原始数据(用于取消时恢复)
}
},
mounted() {
this.orderId = this.$route.query.orderId
// 使用静态数据,不调用API
async mounted() {
this.competitionId = this.$route.query.competitionId
if (this.competitionId) {
// 先检查编排状态
await this.checkScheduleStatus()
this.loadCompetitionInfo()
this.loadVenues()
} else {
this.$message.warning('未获取到赛事ID')
}
},
methods: {
goBack() {
this.$router.go(-1)
},
// 检查编排状态
async checkScheduleStatus() {
try {
const res = await getScheduleResult(this.competitionId)
const scheduleStatus = res.data?.data?.scheduleStatus || res.data?.scheduleStatus || 0
console.log('编排状态:', scheduleStatus)
// 如果编排未完成(状态不是2),提示用户并返回
if (scheduleStatus !== 2) {
this.$message.warning('请先完成编排并锁定后,才能进行调度操作')
// 延迟返回上一页
setTimeout(() => {
this.$router.go(-1)
}, 2000)
return false
}
return true
} catch (error) {
console.error('检查编排状态失败:', error)
this.$message.error('无法获取编排状态,请稍后重试')
setTimeout(() => {
this.$router.go(-1)
}, 2000)
return false
}
},
// 加载赛事信息
async loadCompetitionInfo() {
try {
this.loading = true
const res = await getCompetitionDetail(this.competitionId)
const data = res.data?.data
if (data) {
// 生成时间段
this.generateTimeSlots(data.competitionStartTime || data.competition_start_time, data.competitionEndTime || data.competition_end_time)
}
} catch (err) {
console.error('加载赛事信息失败', err)
this.$message.error('加载赛事信息失败')
} finally {
this.loading = false
}
},
// 生成时间段列表
generateTimeSlots(startTime, endTime) {
if (!startTime || !endTime) {
this.timeSlots = ['2025年11月6日 上午8:30', '2025年11月6日 下午13:30']
return
}
const slots = []
const start = new Date(startTime)
const end = new Date(endTime)
let currentDate = new Date(start)
while (currentDate <= end) {
const year = currentDate.getFullYear()
const month = currentDate.getMonth() + 1
const day = currentDate.getDate()
const dateStr = `${year}${month}${day}`
slots.push(`${dateStr} 上午8:30`)
slots.push(`${dateStr} 下午13:30`)
currentDate.setDate(currentDate.getDate() + 1)
}
this.timeSlots = slots
},
// 加载场地列表
async loadVenues() {
try {
this.loading = true
const res = await getVenuesByCompetition(this.competitionId)
const venuesData = res.data?.data?.records || res.data?.data || []
if (venuesData.length === 0) {
this.$message.warning('该赛事暂无场地信息')
this.venues = []
} else {
this.venues = venuesData.map(v => ({
id: v.id,
venueName: v.venueName || v.venue_name
}))
// 默认选中第一个场地
if (this.venues.length > 0) {
this.selectedVenueId = this.venues[0].id
this.loadDispatchData()
}
}
} catch (err) {
console.error('加载场地失败', err)
this.$message.error('加载场地失败')
this.venues = []
} finally {
this.loading = false
}
},
// 加载调度数据
async loadDispatchData() {
if (!this.selectedVenueId || this.selectedTime === null) {
this.dispatchGroups = []
return
}
try {
this.loading = true
const res = await getDispatchData({
competitionId: this.competitionId,
venueId: this.selectedVenueId,
timeSlotIndex: this.selectedTime
})
if (res.data.success) {
const groups = res.data.data.groups || []
this.dispatchGroups = groups.map(group => ({
...group,
viewMode: 'dispatch',
title: group.groupName,
items: group.participants.map(p => ({
...p,
schoolUnit: p.organization,
completed: false,
refereed: false
}))
}))
// 保存原始数据
this.originalData = JSON.parse(JSON.stringify(this.dispatchGroups))
this.hasChanges = false
} else {
this.$message.error(res.data.msg || '加载调度数据失败')
}
} catch (error) {
console.error('加载调度数据失败:', error)
this.$message.error('加载调度数据失败')
} finally {
this.loading = false
}
},
setViewMode(index, mode) {
this.dispatchGroups[index].viewMode = mode
this.$message.success(`已切换到${mode === 'dispatch' ? '调度列表' : '场地'}模式`)
},
handleMoveUp(groupIndex, itemIndex) {
if (itemIndex === 0) return
const group = this.dispatchGroups[groupIndex]
const temp = group.items[itemIndex]
group.items.splice(itemIndex, 1)
group.items.splice(itemIndex - 1, 0, temp)
// 更新顺序号
group.items.forEach((item, index) => {
item.performanceOrder = index + 1
})
this.hasChanges = true
this.$message.success('上移成功')
},
handleMoveDown(groupIndex, itemIndex) {
const group = this.dispatchGroups[groupIndex]
if (itemIndex === group.items.length - 1) return
const temp = group.items[itemIndex]
group.items.splice(itemIndex, 1)
group.items.splice(itemIndex + 1, 0, temp)
// 更新顺序号
group.items.forEach((item, index) => {
item.performanceOrder = index + 1
})
this.hasChanges = true
this.$message.success('下移成功')
},
handleBatchComplete() {
this.$confirm('确定要标记当前批次所有为完赛状态吗?', '批次完赛', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 标记当前时间段所有为完赛
this.dispatchGroups.forEach(group => {
group.items.forEach(item => {
item.completed = true
})
})
this.$message.success('批次完赛成功')
}).catch(() => {
// 取消操作
})
}).catch(() => {})
},
// 保存调度
async handleSaveDispatch() {
if (!this.hasChanges) {
this.$message.info('没有需要保存的更改')
return
}
try {
this.loading = true
const adjustments = this.dispatchGroups.map(group => ({
detailId: group.detailId,
participants: group.items.map(p => ({
id: p.id,
performanceOrder: p.performanceOrder
}))
}))
const res = await saveDispatch({
competitionId: this.competitionId,
adjustments
})
if (res.data.success) {
this.$message.success('调度保存成功')
this.hasChanges = false
await this.loadDispatchData()
} else {
this.$message.error(res.data.msg || '保存失败')
}
} catch (error) {
console.error('保存调度失败:', error)
this.$message.error('保存失败,请稍后重试')
} finally {
this.loading = false
}
}
}
}
@@ -223,6 +417,14 @@ export default {
}
.tab-content {
.venue-selector {
margin-bottom: 15px;
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
}
.time-selector {
margin-bottom: 15px;
display: flex;
@@ -290,5 +492,17 @@ export default {
display: inline-block;
vertical-align: middle;
}
.dispatch-footer {
margin-top: 30px;
text-align: center;
padding: 20px;
background: #f5f7fa;
border-radius: 4px;
.el-button {
min-width: 120px;
}
}
}
</style>
Binary file not shown.
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large Load Diff
Binary file not shown.
+49 -2
View File
@@ -69,7 +69,15 @@
<template #default="scope">
<el-button type="primary" size="small" @click="handleRegistrationDetail(scope.row)">报名详情</el-button>
<el-button type="success" size="small" @click="handleSchedule(scope.row)">编排</el-button>
<el-button type="warning" size="small" @click="handleDispatch(scope.row)">调度</el-button>
<el-button
type="warning"
size="small"
@click="handleDispatch(scope.row)"
:disabled="!isScheduleCompleted(scope.row.id)"
:title="isScheduleCompleted(scope.row.id) ? '进入调度' : '请先完成编排'"
>
调度
</el-button>
</template>
</el-table-column>
</el-table>
@@ -91,6 +99,7 @@
<script>
import { getCompetitionList } from '@/api/martial/competition'
import { getScheduleResult } from '@/api/martial/activitySchedule'
export default {
name: 'MartialOrderList',
@@ -106,12 +115,19 @@ export default {
current: 1,
size: 10,
total: 0
}
},
scheduleStatusMap: {} // 存储每个赛事的编排状态
}
},
mounted() {
this.loadCompetitionList()
},
activated() {
// 当页面被激活时(从其他页面返回),重新加载编排状态
if (this.tableData.length > 0) {
this.loadScheduleStatus()
}
},
methods: {
// 加载赛事列表
loadCompetitionList() {
@@ -146,6 +162,9 @@ export default {
createTime: competition.createTime || competition.create_time
}))
this.pagination.total = responseData.total || 0
// 加载每个赛事的编排状态
this.loadScheduleStatus()
}
})
.catch(err => {
@@ -157,6 +176,28 @@ export default {
})
},
// 加载编排状态
async loadScheduleStatus() {
for (const competition of this.tableData) {
try {
const res = await getScheduleResult(competition.id)
if (res.data?.data) {
this.$set(this.scheduleStatusMap, competition.id, res.data.data.isCompleted || false)
} else {
this.$set(this.scheduleStatusMap, competition.id, false)
}
} catch (err) {
// 如果获取失败,默认为未完成
this.$set(this.scheduleStatusMap, competition.id, false)
}
}
},
// 检查编排是否完成
isScheduleCompleted(competitionId) {
return this.scheduleStatusMap[competitionId] === true
},
handleSearch() {
this.pagination.current = 1
this.loadCompetitionList()
@@ -191,6 +232,12 @@ export default {
// 调度 - 传递赛事ID
handleDispatch(row) {
// 检查编排是否完成
if (!this.isScheduleCompleted(row.id)) {
this.$message.warning('请先完成编排后再进行调度')
return
}
this.$router.push({
path: '/martial/dispatch/list',
query: { competitionId: row.id }
+1 -1
View File
@@ -467,7 +467,7 @@ import {
exportProjects
} from '@/api/martial/project'
import { getCompetitionList } from '@/api/martial/competition'
import { getToken } from '@/util/auth'
import { getToken } from '@/utils/auth'
import dayjs from 'dayjs'
// 数据定义
+8 -18
View File
@@ -310,12 +310,10 @@ export default {
// 计算总金额(从缓存中获取)
let totalAmount = 0
const projectIds = new Set()
for (const athlete of participants) {
const projectId = athlete.projectId || athlete.project_id
if (projectId && !projectIds.has(projectId)) {
projectIds.add(projectId)
if (projectId) {
const project = this.projectCache.get(projectId)
if (project) {
totalAmount += parseFloat(project.price || 0)
@@ -436,38 +434,30 @@ export default {
if (!unitMap.has(unit)) {
unitMap.set(unit, {
projectIds: new Set(),
projectPrices: new Map()
totalAmount: 0
})
}
const stat = unitMap.get(unit)
// 添加项目IDSet自动去重)
// 添加项目IDSet自动去重,用于统计项目数量
if (projectId) {
stat.projectIds.add(projectId)
// 从缓存中获取价格(不再重复调用API
if (!stat.projectPrices.has(projectId)) {
const project = this.projectCache.get(projectId)
const price = project ? (project.price || 0) : 0
stat.projectPrices.set(projectId, parseFloat(price))
}
// 从缓存中获取价格并累加到总金额
const project = this.projectCache.get(projectId)
const price = project ? (project.price || 0) : 0
stat.totalAmount += parseFloat(price)
}
}
// 3. 计算每个单位的总金额
const amountStats = []
for (const [unit, stat] of unitMap) {
let totalAmount = 0
// 遍历该单位的所有项目,累加价格
for (const price of stat.projectPrices.values()) {
totalAmount += price
}
amountStats.push({
schoolUnit: unit,
projectCount: stat.projectIds.size,
totalAmount: totalAmount.toFixed(2)
totalAmount: stat.totalAmount.toFixed(2)
})
}
+625
View File
@@ -0,0 +1,625 @@
<template>
<div class="rules-container">
<!-- 赛事选择 -->
<el-card shadow="never" class="search-card">
<el-form :inline="true" class="search-form">
<el-form-item label="选择赛事">
<el-select
v-model="competitionId"
placeholder="请选择赛事"
clearable
filterable
style="width: 300px"
@change="handleCompetitionChange"
>
<el-option
v-for="item in competitionList"
:key="item.id"
:label="item.competitionName"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-form>
</el-card>
<div v-if="competitionId" class="content-wrapper">
<!-- 附件管理 -->
<el-card shadow="never" class="section-card">
<template #header>
<div class="card-header">
<span class="card-title">📎 规程附件</span>
<el-button type="primary" size="small" :icon="Plus" @click="handleAddAttachment">
添加附件
</el-button>
</div>
</template>
<el-table :data="attachmentList" border stripe>
<el-table-column prop="fileName" label="文件名称" min-width="200" />
<el-table-column prop="fileType" label="文件类型" width="100" align="center">
<template #default="{ row }">
<el-tag>{{ row.fileType }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="fileSize" label="文件大小" width="120" align="center">
<template #default="{ row }">
{{ formatFileSize(row.fileSize) }}
</template>
</el-table-column>
<el-table-column prop="orderNum" label="排序" width="80" align="center" />
<el-table-column prop="status" label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
{{ row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="180" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="handleEditAttachment(row)">
编辑
</el-button>
<el-button link type="primary" size="small" @click="handlePreviewFile(row)">
预览
</el-button>
<el-button link type="danger" size="small" @click="handleDeleteAttachment(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 章节管理 -->
<el-card shadow="never" class="section-card">
<template #header>
<div class="card-header">
<span class="card-title">📄 规程章节</span>
<el-button type="primary" size="small" :icon="Plus" @click="handleAddChapter">
添加章节
</el-button>
</div>
</template>
<el-table :data="chapterList" border stripe>
<el-table-column prop="chapterNumber" label="章节编号" width="120" />
<el-table-column prop="title" label="章节标题" min-width="200" />
<el-table-column prop="orderNum" label="排序" width="80" align="center" />
<el-table-column prop="status" label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
{{ row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="220" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="handleEditChapter(row)">
编辑
</el-button>
<el-button link type="success" size="small" @click="handleManageContent(row)">
管理内容
</el-button>
<el-button link type="danger" size="small" @click="handleDeleteChapter(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
<el-empty v-else description="请先选择赛事" />
<!-- 附件编辑对话框 -->
<el-dialog
v-model="attachmentDialogVisible"
:title="attachmentForm.id ? '编辑附件' : '添加附件'"
width="600px"
>
<el-form :model="attachmentForm" :rules="attachmentRules" ref="attachmentFormRef" label-width="100px">
<el-form-item label="文件上传" prop="fileUrl">
<el-upload
class="upload-demo"
:action="uploadUrl"
:headers="uploadHeaders"
:on-success="handleFileUploadSuccess"
:before-upload="beforeFileUpload"
:file-list="fileList"
:limit="1"
>
<el-button type="primary">点击上传</el-button>
<template #tip>
<div class="el-upload__tip">
支持 pdf/doc/docx/xls/xlsx/ppt/pptx 等文档格式文件大小不超过50MB
</div>
</template>
</el-upload>
</el-form-item>
<el-form-item label="文件名称" prop="fileName">
<el-input v-model="attachmentForm.fileName" placeholder="请输入文件名称" />
</el-form-item>
<el-form-item label="排序" prop="orderNum">
<el-input-number v-model="attachmentForm.orderNum" :min="0" :max="999" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="attachmentForm.status">
<el-radio :label="1">启用</el-radio>
<el-radio :label="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="attachmentDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSaveAttachment">确定</el-button>
</template>
</el-dialog>
<!-- 章节编辑对话框 -->
<el-dialog
v-model="chapterDialogVisible"
:title="chapterForm.id ? '编辑章节' : '添加章节'"
width="600px"
>
<el-form :model="chapterForm" :rules="chapterRules" ref="chapterFormRef" label-width="100px">
<el-form-item label="章节编号" prop="chapterNumber">
<el-input v-model="chapterForm.chapterNumber" placeholder="如:第一章" />
</el-form-item>
<el-form-item label="章节标题" prop="title">
<el-input v-model="chapterForm.title" placeholder="请输入章节标题" />
</el-form-item>
<el-form-item label="排序" prop="orderNum">
<el-input-number v-model="chapterForm.orderNum" :min="0" :max="999" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="chapterForm.status">
<el-radio :label="1">启用</el-radio>
<el-radio :label="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="chapterDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSaveChapter">确定</el-button>
</template>
</el-dialog>
<!-- 章节内容管理对话框 -->
<el-dialog
v-model="contentDialogVisible"
title="管理章节内容"
width="800px"
>
<div class="content-header">
<span class="content-title">{{ currentChapter.chapterNumber }} {{ currentChapter.title }}</span>
<el-button type="primary" size="small" :icon="Plus" @click="handleAddContent">
添加内容
</el-button>
</div>
<el-table :data="contentList" border stripe class="content-table">
<el-table-column prop="content" label="内容" min-width="400">
<template #default="{ row, $index }">
<el-input
v-if="row.editing"
v-model="row.content"
type="textarea"
:rows="2"
placeholder="请输入内容"
/>
<span v-else>{{ row.content }}</span>
</template>
</el-table-column>
<el-table-column prop="orderNum" label="排序" width="80" align="center" />
<el-table-column label="操作" width="150" align="center">
<template #default="{ row, $index }">
<el-button
v-if="!row.editing"
link
type="primary"
size="small"
@click="handleEditContent(row, $index)"
>
编辑
</el-button>
<el-button
v-else
link
type="success"
size="small"
@click="handleSaveContent(row, $index)"
>
保存
</el-button>
<el-button link type="danger" size="small" @click="handleDeleteContent($index)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="contentDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleBatchSaveContents">保存全部</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Delete, Upload, Download, Refresh, Search } from '@element-plus/icons-vue'
import { getToken } from '@/utils/auth'
import {
getCompetitionList,
getAttachmentList,
saveAttachment,
removeAttachment,
getChapterList,
saveChapter,
removeChapter,
getContentList,
batchSaveContents
} from '@/api/martial/rules'
// 赛事列表
const competitionList = ref([])
const competitionId = ref(null)
// 附件相关
const attachmentList = ref([])
const attachmentDialogVisible = ref(false)
const attachmentFormRef = ref(null)
const attachmentForm = reactive({
id: null,
competitionId: null,
fileName: '',
fileUrl: '',
fileSize: null,
fileType: '',
orderNum: 0,
status: 1
})
const attachmentRules = {
fileName: [{ required: true, message: '请输入文件名称', trigger: 'blur' }],
fileUrl: [{ required: true, message: '请上传文件', trigger: 'change' }]
}
const fileList = ref([])
// 章节相关
const chapterList = ref([])
const chapterDialogVisible = ref(false)
const chapterFormRef = ref(null)
const chapterForm = reactive({
id: null,
competitionId: null,
chapterNumber: '',
title: '',
orderNum: 0,
status: 1
})
const chapterRules = {
chapterNumber: [{ required: true, message: '请输入章节编号', trigger: 'blur' }],
title: [{ required: true, message: '请输入章节标题', trigger: 'blur' }]
}
// 章节内容相关
const contentDialogVisible = ref(false)
const currentChapter = ref({})
const contentList = ref([])
// 文件上传配置
const uploadUrl = ref(import.meta.env.VITE_API_URL + '/blade-resource/oss/endpoint/put-file')
const uploadHeaders = ref({
'Blade-Auth': 'bearer ' + getToken()
})
// 初始化
onMounted(() => {
fetchCompetitionList()
})
// 获取赛事列表
const fetchCompetitionList = async () => {
try {
const res = await getCompetitionList({ current: 1, size: 1000 })
competitionList.value = res.data.records || []
} catch (error) {
ElMessage.error('获取赛事列表失败')
}
}
// 赛事切换
const handleCompetitionChange = () => {
if (competitionId.value) {
fetchAttachmentList()
fetchChapterList()
}
}
// 获取附件列表
const fetchAttachmentList = async () => {
try {
const res = await getAttachmentList({ competitionId: competitionId.value })
attachmentList.value = res.data || []
} catch (error) {
ElMessage.error('获取附件列表失败')
}
}
// 添加附件
const handleAddAttachment = () => {
Object.assign(attachmentForm, {
id: null,
competitionId: competitionId.value,
fileName: '',
fileUrl: '',
fileSize: null,
fileType: '',
orderNum: 0,
status: 1
})
fileList.value = []
attachmentDialogVisible.value = true
}
// 编辑附件
const handleEditAttachment = (row) => {
Object.assign(attachmentForm, { ...row })
fileList.value = row.fileUrl ? [{ name: row.fileName, url: row.fileUrl }] : []
attachmentDialogVisible.value = true
}
// 文件上传前校验
const beforeFileUpload = (file) => {
const allowedTypes = ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']
const isAllowed = allowedTypes.includes(file.type)
const isLt50M = file.size / 1024 / 1024 < 50
if (!isAllowed) {
ElMessage.error('只能上传文档格式文件!')
}
if (!isLt50M) {
ElMessage.error('文件大小不能超过 50MB')
}
return isAllowed && isLt50M
}
// 文件上传成功
const handleFileUploadSuccess = (response) => {
if (response.code === 200) {
attachmentForm.fileUrl = response.data.link
attachmentForm.fileName = response.data.originalName
attachmentForm.fileSize = response.data.size
attachmentForm.fileType = response.data.extension
ElMessage.success('文件上传成功')
} else {
ElMessage.error('文件上传失败')
}
}
// 保存附件
const handleSaveAttachment = async () => {
await attachmentFormRef.value.validate()
try {
await saveAttachment(attachmentForm)
ElMessage.success('保存成功')
attachmentDialogVisible.value = false
fetchAttachmentList()
} catch (error) {
ElMessage.error('保存失败')
}
}
// 删除附件
const handleDeleteAttachment = async (row) => {
try {
await ElMessageBox.confirm('确定要删除该附件吗?', '提示', {
type: 'warning'
})
await removeAttachment({ id: row.id })
ElMessage.success('删除成功')
fetchAttachmentList()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除失败')
}
}
}
// 预览文件
const handlePreviewFile = (row) => {
window.open(row.fileUrl, '_blank')
}
// 格式化文件大小
const formatFileSize = (bytes) => {
if (!bytes || bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i]
}
// 获取章节列表
const fetchChapterList = async () => {
try {
const res = await getChapterList({ competitionId: competitionId.value })
chapterList.value = res.data || []
} catch (error) {
ElMessage.error('获取章节列表失败')
}
}
// 添加章节
const handleAddChapter = () => {
Object.assign(chapterForm, {
id: null,
competitionId: competitionId.value,
chapterNumber: '',
title: '',
orderNum: 0,
status: 1
})
chapterDialogVisible.value = true
}
// 编辑章节
const handleEditChapter = (row) => {
Object.assign(chapterForm, { ...row })
chapterDialogVisible.value = true
}
// 保存章节
const handleSaveChapter = async () => {
await chapterFormRef.value.validate()
try {
await saveChapter(chapterForm)
ElMessage.success('保存成功')
chapterDialogVisible.value = false
fetchChapterList()
} catch (error) {
ElMessage.error('保存失败')
}
}
// 删除章节
const handleDeleteChapter = async (row) => {
try {
await ElMessageBox.confirm('确定要删除该章节吗?删除后章节下的所有内容也将被删除!', '提示', {
type: 'warning'
})
await removeChapter({ id: row.id })
ElMessage.success('删除成功')
fetchChapterList()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除失败')
}
}
}
// 管理章节内容
const handleManageContent = async (row) => {
currentChapter.value = row
await fetchContentList(row.id)
contentDialogVisible.value = true
}
// 获取章节内容列表
const fetchContentList = async (chapterId) => {
try {
const res = await getContentList({ chapterId })
contentList.value = (res.data || []).map(item => ({
...item,
editing: false
}))
} catch (error) {
ElMessage.error('获取章节内容失败')
}
}
// 添加内容
const handleAddContent = () => {
contentList.value.push({
id: null,
chapterId: currentChapter.value.id,
content: '',
orderNum: contentList.value.length + 1,
status: 1,
editing: true
})
}
// 编辑内容
const handleEditContent = (row, index) => {
row.editing = true
}
// 保存单个内容
const handleSaveContent = (row, index) => {
if (!row.content.trim()) {
ElMessage.warning('内容不能为空')
return
}
row.editing = false
}
// 删除内容
const handleDeleteContent = (index) => {
contentList.value.splice(index, 1)
// 重新排序
contentList.value.forEach((item, idx) => {
item.orderNum = idx + 1
})
}
// 批量保存内容
const handleBatchSaveContents = async () => {
const contents = contentList.value.map(item => item.content).filter(c => c.trim())
if (contents.length === 0) {
ElMessage.warning('请至少添加一条内容')
return
}
try {
await batchSaveContents({
chapterId: currentChapter.value.id,
contents
})
ElMessage.success('保存成功')
contentDialogVisible.value = false
} catch (error) {
ElMessage.error('保存失败')
}
}
</script>
<style lang="scss" scoped>
.rules-container {
padding: 20px;
}
.search-card {
margin-bottom: 20px;
}
.content-wrapper {
display: flex;
flex-direction: column;
gap: 20px;
}
.section-card {
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
.card-title {
font-size: 16px;
font-weight: bold;
}
}
}
.content-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
.content-title {
font-size: 16px;
font-weight: bold;
}
}
.content-table {
margin-top: 10px;
}
</style>
+115 -30
View File
@@ -6,6 +6,14 @@
<h2 class="page-title">编排</h2>
</div>
<div class="header-right">
<el-button
size="small"
type="primary"
:loading="autoArrangeLoading"
@click="handleAutoArrange"
>
{{ isScheduleCompleted ? '重新编排' : '自动编排' }}
</el-button>
<el-button size="small" type="danger" @click="showExceptionDialog">
异常组 <el-badge :value="exceptionList.length" :hidden="exceptionList.length === 0" />
</el-button>
@@ -18,6 +26,7 @@
size="small"
:type="activeTab === 'competition' ? 'primary' : ''"
@click="activeTab = 'competition'"
:disabled="isScheduleCompleted"
>
竞赛分组
</el-button>
@@ -25,6 +34,7 @@
size="small"
:type="activeTab === 'venue' ? 'primary' : ''"
@click="activeTab = 'venue'"
:disabled="isScheduleCompleted"
>
场地
</el-button>
@@ -91,7 +101,7 @@
<el-table-column label="操作" width="150" align="center">
<template #default="scope">
<el-button
type="text"
link
size="small"
@click="handleMoveUp(group, scope.$index)"
:disabled="scope.$index === 0 || isScheduleCompleted"
@@ -101,7 +111,7 @@
<img src="/img/图标 3@3x.png" class="move-icon" alt="上移" />
</el-button>
<el-button
type="text"
link
size="small"
@click="handleMoveDown(group, scope.$index)"
:disabled="scope.$index === group.items.length - 1 || isScheduleCompleted"
@@ -112,7 +122,7 @@
</el-button>
<el-button
v-if="(scope.row.status || '未签到') === '未签到'"
type="text"
link
size="small"
@click="markAsException(group, scope.$index)"
:disabled="isScheduleCompleted"
@@ -174,23 +184,25 @@
<!-- 确认对话框 -->
<el-dialog
title="确定完成编排"
:visible.sync="confirmDialogVisible"
v-model="confirmDialogVisible"
width="450px"
center
>
<p style="text-align: center; padding: 20px; color: #606266;">
完成编排后不可再次调整确定完成编排吗
</p>
<span slot="footer" class="dialog-footer">
<el-button @click="confirmDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmComplete">确定</el-button>
</span>
<template #footer>
<span class="dialog-footer">
<el-button @click="confirmDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmComplete">确定</el-button>
</span>
</template>
</el-dialog>
<!-- 移动分组对话框 -->
<el-dialog
title="移动竞赛分组"
:visible.sync="moveDialogVisible"
v-model="moveDialogVisible"
width="500px"
center
>
@@ -216,16 +228,18 @@
</el-select>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="moveDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmMoveGroup">确定</el-button>
</span>
<template #footer>
<span class="dialog-footer">
<el-button @click="moveDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmMoveGroup">确定</el-button>
</span>
</template>
</el-dialog>
<!-- 异常组对话框 -->
<el-dialog
title="异常组参赛人员"
:visible.sync="exceptionDialogVisible"
v-model="exceptionDialogVisible"
width="700px"
center
>
@@ -241,7 +255,7 @@
<el-table-column label="操作" width="100" align="center">
<template #default="scope">
<el-button
type="text"
link
size="small"
@click="removeFromException(scope.$index)"
style="color: #409eff;"
@@ -254,9 +268,11 @@
<div v-if="exceptionList.length === 0" style="text-align: center; padding: 40px; color: #909399;">
暂无异常参赛人员
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="exceptionDialogVisible = false">关闭</el-button>
</span>
<template #footer>
<span class="dialog-footer">
<el-button @click="exceptionDialogVisible = false">关闭</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
@@ -264,7 +280,7 @@
<script>
import { getVenuesByCompetition } from '@/api/martial/venue'
import { getCompetitionDetail } from '@/api/martial/competition'
import { getScheduleResult, saveAndLockSchedule, saveDraftSchedule } from '@/api/martial/activitySchedule'
import { getScheduleResult, saveAndLockSchedule, saveDraftSchedule, triggerAutoArrange, moveScheduleGroup } from '@/api/martial/activitySchedule'
export default {
name: 'MartialScheduleList',
@@ -278,6 +294,7 @@ export default {
confirmDialogVisible: false,
isScheduleCompleted: false, // 是否已完成编排
loading: false,
autoArrangeLoading: false, // 自动编排加载状态
venues: [], // 场地列表(从后端加载)
competitionInfo: {
competitionName: '',
@@ -551,7 +568,7 @@ export default {
},
// 确认移动分组
confirmMoveGroup() {
async confirmMoveGroup() {
if (!this.moveTargetVenueId) {
this.$message.warning('请选择目标场地')
return
@@ -564,13 +581,30 @@ export default {
const group = this.competitionGroups[this.moveGroupIndex]
const targetVenue = this.venues.find(v => v.id === this.moveTargetVenueId)
group.venueId = this.moveTargetVenueId
group.venueName = targetVenue ? targetVenue.venueName : ''
group.timeSlotIndex = this.moveTargetTimeSlot
group.timeSlot = this.timeSlots[this.moveTargetTimeSlot]
try {
// 调用后端API移动分组
const res = await moveScheduleGroup({
groupId: group.id,
targetVenueId: this.moveTargetVenueId,
targetTimeSlotIndex: this.moveTargetTimeSlot
})
this.$message.success(`已移动到 ${group.venueName} - ${group.timeSlot}`)
this.moveDialogVisible = false
if (res.data.success) {
// 更新前端数据
group.venueId = this.moveTargetVenueId
group.venueName = targetVenue ? targetVenue.venueName : ''
group.timeSlotIndex = this.moveTargetTimeSlot
group.timeSlot = this.timeSlots[this.moveTargetTimeSlot]
this.$message.success(`已移动到 ${group.venueName} - ${group.timeSlot}`)
this.moveDialogVisible = false
} else {
this.$message.error(res.data.msg || '移动分组失败')
}
} catch (error) {
console.error('移动分组失败:', error)
this.$message.error('移动分组失败,请稍后重试')
}
},
// 标记为异常
@@ -624,6 +658,39 @@ export default {
this.$message.success(`已将 ${exceptionItem.schoolUnit} 从异常组移除`)
},
// 触发自动编排
async handleAutoArrange() {
try {
// 确认操作
await this.$confirm('自动编排将重新生成赛程安排,当前编排数据将被覆盖。确定继续吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
this.autoArrangeLoading = true
// 调用自动编排接口
const res = await triggerAutoArrange(this.competitionId)
if (res.data?.success) {
this.$message.success('自动编排完成')
// 重新加载编排数据
await this.loadScheduleData()
} else {
this.$message.error(res.data?.msg || '自动编排失败')
}
} catch (err) {
if (err !== 'cancel') {
console.error('自动编排失败', err)
this.$message.error('自动编排失败: ' + (err.message || ''))
}
} finally {
this.autoArrangeLoading = false
}
},
// 保存草稿
async handleSaveDraft() {
try {
@@ -691,6 +758,16 @@ export default {
try {
this.loading = true
// 检查是否有编排数据
if (!this.competitionGroups || this.competitionGroups.length === 0) {
this.$message.warning('请先进行自动编排,生成编排数据后再完成编排')
this.confirmDialogVisible = false
return
}
console.log('开始完成编排,竞赛ID:', this.competitionId)
console.log('编排分组数量:', this.competitionGroups.length)
// 1. 先保存当前的草稿数据
const saveData = {
competitionId: this.competitionId,
@@ -714,19 +791,27 @@ export default {
}))
}
await saveDraftSchedule(saveData)
console.log('保存草稿成功,准备锁定')
console.log('准备保存草稿数据:', saveData)
const draftRes = await saveDraftSchedule(saveData)
console.log('保存草稿成功,返回:', draftRes)
// 2. 然后调用锁定接口
await saveAndLockSchedule(saveData)
console.log('准备锁定编排,竞赛ID:', this.competitionId)
const lockRes = await saveAndLockSchedule(this.competitionId)
console.log('锁定接口返回:', lockRes)
// 3. 更新UI状态
this.isScheduleCompleted = true
this.confirmDialogVisible = false
this.$message.success('编排已完成并锁定')
// 4. 重新加载编排数据以获取最新状态
await this.loadScheduleData()
} catch (err) {
console.error('完成编排失败', err)
this.$message.error('完成编排失败: ' + (err.message || ''))
console.error('错误详情:', err.response?.data || err.message)
this.$message.error('完成编排失败: ' + (err.response?.data?.msg || err.message || '未知错误'))
} finally {
this.loading = false
}
File diff suppressed because it is too large Load Diff
Binary file not shown.
+26 -14
View File
@@ -65,30 +65,30 @@
<i class="card-arrow el-icon-arrow-right"></i>
</div>
<div class="access-card" @click="navigateTo('/martial/schedule/list')">
<div class="access-card" @click="navigateTo('/martial/project/list')">
<div class="card-bg"></div>
<div class="card-icon">
<i class="el-icon-menu"></i>
</div>
<div class="card-info">
<h3>项目管理</h3>
<p>武术项目管理</p>
</div>
<i class="card-arrow el-icon-arrow-right"></i>
</div>
<div class="access-card" @click="navigateTo('/martial/schedulePlan/list')">
<div class="card-bg"></div>
<div class="card-icon">
<i class="el-icon-date"></i>
</div>
<div class="card-info">
<h3>赛程编排</h3>
<h3>赛程计划</h3>
<p>比赛赛程安排</p>
</div>
<i class="card-arrow el-icon-arrow-right"></i>
</div>
<div class="access-card" @click="navigateTo('/martial/dispatch/list')">
<div class="card-bg"></div>
<div class="card-icon">
<i class="el-icon-s-promotion"></i>
</div>
<div class="card-info">
<h3>赛事调度</h3>
<p>实时进度跟踪</p>
</div>
<i class="card-arrow el-icon-arrow-right"></i>
</div>
<div class="access-card" @click="navigateTo('/martial/referee/list')">
<div class="card-bg"></div>
<div class="card-icon">
@@ -112,6 +112,18 @@
</div>
<i class="card-arrow el-icon-arrow-right"></i>
</div>
<div class="access-card" @click="navigateTo('/martial/result/list')">
<div class="card-bg"></div>
<div class="card-icon">
<i class="el-icon-trophy"></i>
</div>
<div class="card-info">
<h3>成绩管理</h3>
<p>比赛成绩查询</p>
</div>
<i class="card-arrow el-icon-arrow-right"></i>
</div>
</div>
</div>