修改前端页面
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
2025-12-03 13:51:22 +08:00
parent aa6facf13a
commit ab69968bda
44 changed files with 15366 additions and 890 deletions

View File

@@ -3,7 +3,8 @@
"allow": [
"Bash(dir:*)",
"Bash(npm run build:*)",
"Bash(findstr:*)"
"Bash(findstr:*)",
"WebFetch(domain:www.cmiassn.org)"
],
"deny": [],
"ask": []

211
src/api/martial/activity.js Normal file
View File

@@ -0,0 +1,211 @@
import request from '@/axios';
// ==================== 活动日程管理接口 ====================
/**
* 活动日程分页查询
* @param {Number} current - 当前页,默认1
* @param {Number} size - 每页条数,默认10
* @param {Object} params - 查询参数
* @param {Number} params.competitionId - 赛事ID
* @param {String} params.activityType - 活动类型(可选)
* @param {String} params.activityName - 活动名称(可选)
* @param {String} params.startDate - 开始日期(可选)
* @param {String} params.endDate - 结束日期(可选)
*/
export const getActivityScheduleList = (current, size, params) => {
return request({
url: '/api/blade-martial/activity-schedule/list',
method: 'get',
params: {
current,
size,
...params
}
})
}
/**
* 获取活动日程详情
* @param {Number} id - 活动ID
*/
export const getActivityScheduleDetail = (id) => {
return request({
url: '/api/blade-martial/activity-schedule/detail',
method: 'get',
params: { id }
})
}
/**
* 添加活动日程
* @param {Object} data - 活动数据
* @param {Number} data.competitionId - 赛事ID
* @param {String} data.activityType - 活动类型(opening_ceremony/closing_ceremony/competition/training/meeting/other)
* @param {String} data.activityName - 活动名称
* @param {String} data.description - 活动描述
* @param {String} data.startTime - 开始时间
* @param {String} data.endTime - 结束时间
* @param {String} data.location - 活动地点
* @param {Number} data.venueId - 场地ID(可选)
* @param {String} data.organizer - 组织者
* @param {String} data.participants - 参与人员(可选)
* @param {String} data.remarks - 备注(可选)
*/
export const addActivity = (data) => {
return request({
url: '/api/blade-martial/activity-schedule/add',
method: 'post',
data
})
}
/**
* 修改活动日程
* @param {Object} data - 活动数据
*/
export const updateActivity = (data) => {
return request({
url: '/api/blade-martial/activity-schedule/update',
method: 'post',
data
})
}
/**
* 删除活动日程
* @param {String} ids - 活动ID,多个用逗号分隔
*/
export const removeActivity = (ids) => {
return request({
url: '/api/blade-martial/activity-schedule/remove',
method: 'post',
params: { ids }
})
}
/**
* 获取日历视图数据
* @param {Number} competitionId - 赛事ID
* @param {String} month - 月份(格式: YYYY-MM)
*/
export const getActivityCalendar = (competitionId, month) => {
return request({
url: '/api/blade-martial/activity-schedule/calendar',
method: 'get',
params: { competitionId, month }
})
}
/**
* 检查活动时间冲突
* @param {Object} data - 活动数据
* @param {Number} data.competitionId - 赛事ID
* @param {String} data.startTime - 开始时间
* @param {String} data.endTime - 结束时间
* @param {Number} data.venueId - 场地ID(可选)
* @param {Number} data.excludeId - 排除的活动ID(编辑时使用)
*/
export const checkActivityConflict = (data) => {
return request({
url: '/api/blade-martial/activity-schedule/check-conflict',
method: 'post',
data
})
}
/**
* 复制活动
* @param {Object} data - 复制数据
* @param {Number} data.sourceId - 源活动ID
* @param {String} data.newStartTime - 新开始时间
* @param {String} data.newEndTime - 新结束时间
*/
export const copyActivity = (data) => {
return request({
url: '/api/blade-martial/activity-schedule/copy',
method: 'post',
data
})
}
/**
* 按日期查询活动
* @param {Number} competitionId - 赛事ID
* @param {String} date - 日期(格式: YYYY-MM-DD)
*/
export const getActivitiesByDate = (competitionId, date) => {
return request({
url: '/api/blade-martial/activity-schedule/by-date',
method: 'get',
params: { competitionId, date }
})
}
/**
* 获取活动统计
* @param {Number} competitionId - 赛事ID
*/
export const getActivityStatistics = (competitionId) => {
return request({
url: '/api/blade-martial/activity-schedule/statistics',
method: 'get',
params: { competitionId }
})
}
/**
* 按类型统计活动
* @param {Number} competitionId - 赛事ID
*/
export const getActivityByType = (competitionId) => {
return request({
url: '/api/blade-martial/activity-schedule/statistics-by-type',
method: 'get',
params: { competitionId }
})
}
/**
* 导出活动日程
* @param {Object} params - 导出参数
*/
export const exportActivitySchedule = (params) => {
return request({
url: '/api/blade-martial/activity-schedule/export',
method: 'get',
params,
responseType: 'blob'
})
}
/**
* 导入活动日程
* @param {File} file - Excel文件
* @param {Number} competitionId - 赛事ID
*/
export const importActivitySchedule = (file, competitionId) => {
const formData = new FormData()
formData.append('file', file)
formData.append('competitionId', competitionId)
return request({
url: '/api/blade-martial/activity-schedule/import',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
/**
* 批量添加活动
* @param {Array} data - 活动数据数组
*/
export const batchAddActivities = (data) => {
return request({
url: '/api/blade-martial/activity-schedule/batch-add',
method: 'post',
data
})
}

View File

@@ -0,0 +1,291 @@
import request from '@/axios';
// ==================== 武术赛事活动日程管理接口 ====================
/**
* 活动日程分页查询
* @param {Number} current - 当前页,默认1
* @param {Number} size - 每页条数,默认10
* @param {Object} params - 查询参数
* @param {Number} params.competitionId - 赛事ID
* @param {String} params.activityDate - 活动日期(可选)
* @param {Number} params.activityType - 活动类型(可选)
*/
export const getActivityScheduleList = (current, size, params) => {
return request({
url: '/api/blade-martial/activitySchedule/list',
method: 'get',
params: {
current,
size,
...params
}
})
}
/**
* 获取活动日程详情
* @param {Number} id - 活动日程ID
*/
export const getActivityScheduleDetail = (id) => {
return request({
url: '/api/blade-martial/activitySchedule/detail',
method: 'get',
params: { id }
})
}
/**
* 新增活动日程
* @param {Object} data - 活动日程数据
* @param {Number} data.competitionId - 赛事ID
* @param {String} data.activityDate - 活动日期
* @param {String} data.startTime - 开始时间
* @param {String} data.endTime - 结束时间
* @param {Number} data.activityType - 活动类型1开幕式2闭幕式3比赛4培训5会议6其他
* @param {String} data.activityName - 活动名称
* @param {String} data.activityLocation - 活动地点
* @param {String} data.activityDescription - 活动描述
* @param {String} data.organizer - 组织者
* @param {String} data.participants - 参与人员
* @param {Number} data.sortOrder - 排序序号
*/
export const addActivity = (data) => {
return request({
url: '/api/blade-martial/activitySchedule/save',
method: 'post',
data
})
}
/**
* 修改活动日程
* @param {Object} data - 活动日程数据
*/
export const updateActivity = (data) => {
return request({
url: '/api/blade-martial/activitySchedule/update',
method: 'post',
data
})
}
/**
* 删除活动日程
* @param {String} ids - 活动日程ID,多个用逗号分隔
*/
export const removeActivity = (ids) => {
return request({
url: '/api/blade-martial/activitySchedule/remove',
method: 'post',
params: { ids }
})
}
/**
* 批量添加活动日程
* @param {Array} data - 活动日程数据数组
*/
export const batchAddActivities = (data) => {
return request({
url: '/api/blade-martial/activitySchedule/batch-save',
method: 'post',
data
})
}
/**
* 获取某日期的活动日程
* @param {Number} competitionId - 赛事ID
* @param {String} activityDate - 活动日期
*/
export const getActivitiesByDate = (competitionId, activityDate) => {
return request({
url: '/api/blade-martial/activitySchedule/list-by-date',
method: 'get',
params: { competitionId, activityDate }
})
}
/**
* 获取日期范围内的活动日程
* @param {Number} competitionId - 赛事ID
* @param {String} startDate - 开始日期
* @param {String} endDate - 结束日期
*/
export const getActivitiesByDateRange = (competitionId, startDate, endDate) => {
return request({
url: '/api/blade-martial/activitySchedule/list-by-range',
method: 'get',
params: { competitionId, startDate, endDate }
})
}
/**
* 获取某类型的活动日程
* @param {Number} competitionId - 赛事ID
* @param {Number} activityType - 活动类型
*/
export const getActivitiesByType = (competitionId, activityType) => {
return request({
url: '/api/blade-martial/activitySchedule/list-by-type',
method: 'get',
params: { competitionId, activityType }
})
}
/**
* 获取赛事的所有活动日程(不分页)
* @param {Number} competitionId - 赛事ID
*/
export const getAllActivities = (competitionId) => {
return request({
url: '/api/blade-martial/activitySchedule/all',
method: 'get',
params: { competitionId }
})
}
/**
* 调整活动日程顺序
* @param {Object} data - 调整参数
* @param {Number} data.id - 活动日程ID
* @param {Number} data.targetOrder - 目标顺序
*/
export const adjustActivityOrder = (data) => {
return request({
url: '/api/blade-martial/activitySchedule/adjust-order',
method: 'post',
data
})
}
/**
* 检查活动时间冲突
* @param {Object} data - 检查参数
* @param {Number} data.competitionId - 赛事ID
* @param {String} data.activityDate - 活动日期
* @param {String} data.startTime - 开始时间
* @param {String} data.endTime - 结束时间
* @param {String} data.activityLocation - 活动地点
* @param {Number} data.excludeId - 排除的活动ID编辑时使用
*/
export const checkActivityConflict = (data) => {
return request({
url: '/api/blade-martial/activitySchedule/check-conflict',
method: 'post',
data
})
}
/**
* 复制活动日程到其他日期
* @param {Object} data - 复制参数
* @param {Number} data.sourceActivityId - 源活动ID
* @param {String} data.targetDate - 目标日期
*/
export const copyActivity = (data) => {
return request({
url: '/api/blade-martial/activitySchedule/copy',
method: 'post',
data
})
}
/**
* 获取活动日程日历视图数据
* @param {Number} competitionId - 赛事ID
* @param {String} month - 月份格式YYYY-MM
*/
export const getActivityCalendar = (competitionId, month) => {
return request({
url: '/api/blade-martial/activitySchedule/calendar',
method: 'get',
params: { competitionId, month }
})
}
/**
* 发布活动日程
* @param {Number} id - 活动日程ID
*/
export const publishActivity = (id) => {
return request({
url: '/api/blade-martial/activitySchedule/publish',
method: 'post',
params: { id }
})
}
/**
* 取消活动日程
* @param {Number} id - 活动日程ID
* @param {String} cancelReason - 取消原因
*/
export const cancelActivity = (id, cancelReason) => {
return request({
url: '/api/blade-martial/activitySchedule/cancel',
method: 'post',
params: { id },
data: { cancelReason }
})
}
/**
* 完成活动日程
* @param {Number} id - 活动日程ID
* @param {String} completionNote - 完成备注
*/
export const completeActivity = (id, completionNote) => {
return request({
url: '/api/blade-martial/activitySchedule/complete',
method: 'post',
params: { id },
data: { completionNote }
})
}
/**
* 导出活动日程
* @param {Object} params - 导出参数
*/
export const exportActivities = (params) => {
return request({
url: '/api/blade-martial/activitySchedule/export',
method: 'get',
params,
responseType: 'blob'
})
}
/**
* 导入活动日程
* @param {Number} competitionId - 赛事ID
* @param {File} file - Excel文件
*/
export const importActivities = (competitionId, file) => {
const formData = new FormData()
formData.append('competitionId', competitionId)
formData.append('file', file)
return request({
url: '/api/blade-martial/activitySchedule/import',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
/**
* 打印活动日程表
* @param {Number} competitionId - 赛事ID
*/
export const printActivitySchedule = (competitionId) => {
return request({
url: '/api/blade-martial/activitySchedule/print',
method: 'get',
params: { competitionId },
responseType: 'blob'
})
}

View File

@@ -0,0 +1,138 @@
import request from '@/axios';
// ==================== 扣分项管理接口 ====================
/**
* 扣分项分页查询
* @param {Number} current - 当前页,默认1
* @param {Number} size - 每页条数,默认10
* @param {Object} params - 查询参数
* @param {Number} params.projectId - 项目ID
* @param {String} params.itemName - 扣分项名称(可选)
*/
export const getDeductionList = (current, size, params) => {
return request({
url: '/api/blade-martial/deductionItem/list',
method: 'get',
params: {
current,
size,
...params
}
})
}
/**
* 获取扣分项详情
* @param {Number} id - 扣分项ID
*/
export const getDeductionDetail = (id) => {
return request({
url: '/api/blade-martial/deductionItem/detail',
method: 'get',
params: { id }
})
}
/**
* 新增扣分项
* @param {Object} data - 扣分项数据
* @param {Number} data.projectId - 项目ID
* @param {String} data.itemName - 扣分项名称
* @param {Number} data.deductionPoints - 扣分值
* @param {String} data.description - 描述说明
* @param {Number} data.sortOrder - 排序序号
*/
export const addDeduction = (data) => {
return request({
url: '/api/blade-martial/deductionItem/save',
method: 'post',
data
})
}
/**
* 修改扣分项
* @param {Object} data - 扣分项数据
*/
export const updateDeduction = (data) => {
return request({
url: '/api/blade-martial/deductionItem/update',
method: 'post',
data
})
}
/**
* 删除扣分项
* @param {String} ids - 扣分项ID,多个用逗号分隔
*/
export const removeDeduction = (ids) => {
return request({
url: '/api/blade-martial/deductionItem/remove',
method: 'post',
params: { ids }
})
}
/**
* 获取项目的扣分项列表(不分页)
* @param {Number} projectId - 项目ID
*/
export const getDeductionsByProject = (projectId) => {
return request({
url: '/api/blade-martial/deductionItem/list-by-project',
method: 'get',
params: { projectId }
})
}
/**
* 克隆扣分项
* @param {Object} data - 克隆数据
* @param {Number} data.sourceProjectId - 源项目ID
* @param {Number} data.targetProjectId - 目标项目ID
*/
export const cloneDeductions = (data) => {
return request({
url: '/api/blade-martial/deductionItem/clone',
method: 'post',
data
})
}
/**
* 更新扣分项排序
* @param {Array} sortData - 排序数据数组 [{id, sortOrder}, ...]
*/
export const updateDeductionOrder = (sortData) => {
return request({
url: '/api/blade-martial/deductionItem/update-order',
method: 'post',
data: sortData
})
}
/**
* 导出扣分项模板
*/
export const exportDeductionTemplate = () => {
return request({
url: '/api/blade-martial/deductionItem/export-template',
method: 'get',
responseType: 'blob'
})
}
/**
* 导出扣分项列表
* @param {Object} params - 查询参数
*/
export const exportDeductions = (params) => {
return request({
url: '/api/blade-martial/deductionItem/export',
method: 'get',
params,
responseType: 'blob'
})
}

View File

@@ -0,0 +1,118 @@
import request from '@/axios';
// ==================== 武术赛事扣分项管理接口 ====================
/**
* 扣分项分页查询
* @param {Number} current - 当前页,默认1
* @param {Number} size - 每页条数,默认10
* @param {Object} params - 查询参数
* @param {Number} params.projectId - 项目ID
* @param {String} params.itemName - 扣分项名称(可选)
*/
export const getDeductionList = (current, size, params) => {
return request({
url: '/api/blade-martial/deductionItem/list',
method: 'get',
params: {
current,
size,
...params
}
})
}
/**
* 获取扣分项详情
* @param {Number} id - 扣分项ID
*/
export const getDeductionDetail = (id) => {
return request({
url: '/api/blade-martial/deductionItem/detail',
method: 'get',
params: { id }
})
}
/**
* 新增扣分项
* @param {Object} data - 扣分项数据
* @param {Number} data.projectId - 项目ID
* @param {String} data.itemName - 扣分项名称
* @param {Number} data.deductionPoints - 扣分值
* @param {String} data.description - 描述
* @param {Number} data.sortOrder - 排序序号
*/
export const addDeduction = (data) => {
return request({
url: '/api/blade-martial/deductionItem/save',
method: 'post',
data
})
}
/**
* 修改扣分项
* @param {Object} data - 扣分项数据
*/
export const updateDeduction = (data) => {
return request({
url: '/api/blade-martial/deductionItem/update',
method: 'post',
data
})
}
/**
* 删除扣分项
* @param {String} ids - 扣分项ID,多个用逗号分隔
*/
export const removeDeduction = (ids) => {
return request({
url: '/api/blade-martial/deductionItem/remove',
method: 'post',
params: { ids }
})
}
/**
* 获取项目的扣分项列表(不分页)
* @param {Number} projectId - 项目ID
*/
export const getDeductionsByProject = (projectId) => {
return request({
url: '/api/blade-martial/deductionItem/list-by-project',
method: 'get',
params: { projectId }
})
}
/**
* 批量导入扣分项
* @param {Number} projectId - 项目ID
* @param {File} file - Excel文件
*/
export const importDeductions = (projectId, file) => {
const formData = new FormData()
formData.append('projectId', projectId)
formData.append('file', file)
return request({
url: '/api/blade-martial/deductionItem/import',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
/**
* 导出扣分项模板
*/
export const exportDeductionTemplate = () => {
return request({
url: '/api/blade-martial/deductionItem/export-template',
method: 'get',
responseType: 'blob'
})
}

View File

@@ -0,0 +1,262 @@
import request from '@/axios';
// ==================== 武术赛事异常事件管理接口 ====================
/**
* 异常事件分页查询
* @param {Number} current - 当前页,默认1
* @param {Number} size - 每页条数,默认10
* @param {Object} params - 查询参数
* @param {Number} params.competitionId - 赛事ID
* @param {Number} params.exceptionType - 异常类型(可选)
* @param {Number} params.severity - 严重程度(可选)
* @param {Number} params.status - 处理状态(可选)
*/
export const getExceptionList = (current, size, params) => {
return request({
url: '/api/blade-martial/exception/list',
method: 'get',
params: {
current,
size,
...params
}
})
}
/**
* 获取异常事件详情
* @param {Number} id - 异常事件ID
*/
export const getExceptionDetail = (id) => {
return request({
url: '/api/blade-martial/exception/detail',
method: 'get',
params: { id }
})
}
/**
* 上报异常事件
* @param {Object} data - 异常事件数据
* @param {Number} data.competitionId - 赛事ID
* @param {Number} data.projectId - 项目ID可选
* @param {Number} data.venueId - 场地ID可选
* @param {Number} data.exceptionType - 异常类型1设备故障2人员问题3时间冲突4评分异常5安全事故6其他
* @param {Number} data.severity - 严重程度1轻微2一般3严重4紧急
* @param {String} data.title - 标题
* @param {String} data.description - 描述
* @param {String} data.reporterName - 上报人姓名
* @param {String} data.reporterPhone - 上报人电话
* @param {Array} data.images - 图片URL数组可选
*/
export const reportException = (data) => {
return request({
url: '/api/blade-martial/exception/report',
method: 'post',
data
})
}
/**
* 修改异常事件
* @param {Object} data - 异常事件数据
*/
export const updateException = (data) => {
return request({
url: '/api/blade-martial/exception/update',
method: 'post',
data
})
}
/**
* 删除异常事件
* @param {String} ids - 异常事件ID,多个用逗号分隔
*/
export const removeException = (ids) => {
return request({
url: '/api/blade-martial/exception/remove',
method: 'post',
params: { ids }
})
}
/**
* 处理异常事件
* @param {Object} data - 处理数据
* @param {Number} data.id - 异常事件ID
* @param {String} data.handlerName - 处理人姓名
* @param {String} data.handleResult - 处理结果
* @param {String} data.handleNote - 处理备注
*/
export const handleException = (data) => {
return request({
url: '/api/blade-martial/exception/handle',
method: 'post',
data
})
}
/**
* 关闭异常事件
* @param {Number} id - 异常事件ID
* @param {String} closeReason - 关闭原因
*/
export const closeException = (id, closeReason) => {
return request({
url: '/api/blade-martial/exception/close',
method: 'post',
params: { id },
data: { closeReason }
})
}
/**
* 重新打开异常事件
* @param {Number} id - 异常事件ID
* @param {String} reopenReason - 重开原因
*/
export const reopenException = (id, reopenReason) => {
return request({
url: '/api/blade-martial/exception/reopen',
method: 'post',
params: { id },
data: { reopenReason }
})
}
/**
* 分配异常事件
* @param {Object} data - 分配数据
* @param {Number} data.id - 异常事件ID
* @param {String} data.assigneeName - 分配人姓名
* @param {String} data.assigneePhone - 分配人电话
*/
export const assignException = (data) => {
return request({
url: '/api/blade-martial/exception/assign',
method: 'post',
data
})
}
/**
* 获取待处理异常事件列表
* @param {Number} competitionId - 赛事ID
* @param {Number} severity - 严重程度(可选)
*/
export const getPendingExceptions = (competitionId, severity) => {
return request({
url: '/api/blade-martial/exception/pending',
method: 'get',
params: { competitionId, severity }
})
}
/**
* 获取我的异常事件列表
* @param {Number} competitionId - 赛事ID
* @param {String} assigneeName - 分配人姓名
*/
export const getMyExceptions = (competitionId, assigneeName) => {
return request({
url: '/api/blade-martial/exception/my-exceptions',
method: 'get',
params: { competitionId, assigneeName }
})
}
/**
* 获取异常事件统计
* @param {Number} competitionId - 赛事ID
*/
export const getExceptionStatistics = (competitionId) => {
return request({
url: '/api/blade-martial/exception/statistics',
method: 'get',
params: { competitionId }
})
}
/**
* 按类型统计异常事件
* @param {Number} competitionId - 赛事ID
*/
export const getExceptionByType = (competitionId) => {
return request({
url: '/api/blade-martial/exception/statistics-by-type',
method: 'get',
params: { competitionId }
})
}
/**
* 按严重程度统计异常事件
* @param {Number} competitionId - 赛事ID
*/
export const getExceptionBySeverity = (competitionId) => {
return request({
url: '/api/blade-martial/exception/statistics-by-severity',
method: 'get',
params: { competitionId }
})
}
/**
* 上传异常事件图片
* @param {File} file - 图片文件
*/
export const uploadExceptionImage = (file) => {
const formData = new FormData()
formData.append('file', file)
return request({
url: '/api/blade-martial/exception/upload-image',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
/**
* 添加异常处理记录
* @param {Object} data - 处理记录数据
* @param {Number} data.exceptionId - 异常事件ID
* @param {String} data.operatorName - 操作人姓名
* @param {String} data.operationType - 操作类型
* @param {String} data.operationNote - 操作备注
*/
export const addExceptionLog = (data) => {
return request({
url: '/api/blade-martial/exception/add-log',
method: 'post',
data
})
}
/**
* 获取异常处理记录
* @param {Number} exceptionId - 异常事件ID
*/
export const getExceptionLogs = (exceptionId) => {
return request({
url: '/api/blade-martial/exception/logs',
method: 'get',
params: { exceptionId }
})
}
/**
* 导出异常事件报表
* @param {Object} params - 导出参数
*/
export const exportExceptions = (params) => {
return request({
url: '/api/blade-martial/exception/export',
method: 'get',
params,
responseType: 'blob'
})
}

287
src/api/martial/export.js Normal file
View File

@@ -0,0 +1,287 @@
import request from '@/axios';
// ==================== 武术赛事导出打印接口 ====================
/**
* 导出成绩单Excel
* @param {Object} params - 导出参数
* @param {Number} params.competitionId - 赛事ID
* @param {Number} params.projectId - 项目ID可选
* @param {String} params.category - 分组类别(可选)
* @param {Boolean} params.includeRanking - 是否包含排名默认true
* @param {Boolean} params.includeMedal - 是否包含奖牌默认true
*/
export const exportResults = (params) => {
return request({
url: '/api/blade-martial/export/results',
method: 'get',
params,
responseType: 'blob'
})
}
/**
* 导出运动员名单Excel
* @param {Object} params - 导出参数
* @param {Number} params.competitionId - 赛事ID
* @param {Number} params.projectId - 项目ID可选
* @param {String} params.category - 分组类别(可选)
* @param {String} params.teamName - 团队名称(可选)
* @param {Number} params.status - 报名状态(可选)
*/
export const exportAthletes = (params) => {
return request({
url: '/api/blade-martial/export/athletes',
method: 'get',
params,
responseType: 'blob'
})
}
/**
* 导出赛程表Excel
* @param {Object} params - 导出参数
* @param {Number} params.competitionId - 赛事ID
* @param {Number} params.venueId - 场地ID可选
* @param {String} params.scheduleDate - 赛程日期(可选)
* @param {Number} params.projectId - 项目ID可选
*/
export const exportSchedule = (params) => {
return request({
url: '/api/blade-martial/export/schedule',
method: 'get',
params,
responseType: 'blob'
})
}
/**
* 导出评分表Excel
* @param {Object} params - 导出参数
* @param {Number} params.competitionId - 赛事ID
* @param {Number} params.projectId - 项目ID可选
* @param {Number} params.judgeId - 裁判ID可选
* @param {String} params.scoreDate - 评分日期(可选)
*/
export const exportScores = (params) => {
return request({
url: '/api/blade-martial/export/scores',
method: 'get',
params,
responseType: 'blob'
})
}
/**
* 导出裁判名单Excel
* @param {Object} params - 导出参数
* @param {Number} params.competitionId - 赛事ID
* @param {String} params.judgeLevel - 裁判等级(可选)
* @param {Number} params.projectId - 项目ID可选
*/
export const exportReferees = (params) => {
return request({
url: '/api/blade-martial/export/referees',
method: 'get',
params,
responseType: 'blob'
})
}
/**
* 导出奖牌榜Excel
* @param {Object} params - 导出参数
* @param {Number} params.competitionId - 赛事ID
* @param {String} params.groupBy - 分组方式team/region
*/
export const exportMedalRanking = (params) => {
return request({
url: '/api/blade-martial/export/medal-ranking',
method: 'get',
params,
responseType: 'blob'
})
}
// ==================== 证书生成接口 ====================
/**
* 生成单个证书HTML/PDF
* @param {Number} resultId - 成绩ID
* @param {String} format - 格式html/pdf默认pdf
*/
export const generateCertificate = (resultId, format = 'pdf') => {
return request({
url: `/api/blade-martial/export/certificate/${resultId}`,
method: 'get',
params: { format },
responseType: format === 'pdf' ? 'blob' : 'json'
})
}
/**
* 批量生成证书
* @param {Object} data - 生成参数
* @param {Array} data.resultIds - 成绩ID数组
* @param {Number} data.projectId - 项目ID可选为空时使用resultIds
* @param {Number} data.competitionId - 赛事ID可选为空时使用resultIds
* @param {String} data.format - 格式html/pdf默认pdf
* @param {Boolean} data.mergeFiles - 是否合并为一个文件默认false
*/
export const batchCertificates = (data) => {
return request({
url: '/api/blade-martial/export/certificates/batch',
method: 'post',
data,
responseType: 'blob'
})
}
/**
* 获取证书数据(用于预览)
* @param {Number} resultId - 成绩ID
*/
export const getCertificateData = (resultId) => {
return request({
url: `/api/blade-martial/export/certificate/data/${resultId}`,
method: 'get'
})
}
/**
* 获取证书模板列表
* @param {Number} competitionId - 赛事ID可选
*/
export const getCertificateTemplates = (competitionId) => {
return request({
url: '/api/blade-martial/export/certificate/templates',
method: 'get',
params: { competitionId }
})
}
/**
* 上传证书模板
* @param {File} file - 模板文件
* @param {String} templateName - 模板名称
* @param {Number} competitionId - 赛事ID可选
*/
export const uploadCertificateTemplate = (file, templateName, competitionId) => {
const formData = new FormData()
formData.append('file', file)
formData.append('templateName', templateName)
if (competitionId) {
formData.append('competitionId', competitionId)
}
return request({
url: '/api/blade-martial/export/certificate/template/upload',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
// ==================== 报表导出接口 ====================
/**
* 导出赛事统计报表
* @param {Number} competitionId - 赛事ID
*/
export const exportCompetitionReport = (competitionId) => {
return request({
url: '/api/blade-martial/export/competition-report',
method: 'get',
params: { competitionId },
responseType: 'blob'
})
}
/**
* 导出项目统计报表
* @param {Number} projectId - 项目ID
*/
export const exportProjectReport = (projectId) => {
return request({
url: '/api/blade-martial/export/project-report',
method: 'get',
params: { projectId },
responseType: 'blob'
})
}
/**
* 导出裁判工作量统计
* @param {Object} params - 导出参数
* @param {Number} params.competitionId - 赛事ID
* @param {String} params.startDate - 开始日期(可选)
* @param {String} params.endDate - 结束日期(可选)
*/
export const exportJudgeWorkload = (params) => {
return request({
url: '/api/blade-martial/export/judge-workload',
method: 'get',
params,
responseType: 'blob'
})
}
/**
* 导出报名统计报表
* @param {Number} competitionId - 赛事ID
*/
export const exportRegistrationReport = (competitionId) => {
return request({
url: '/api/blade-martial/export/registration-report',
method: 'get',
params: { competitionId },
responseType: 'blob'
})
}
// ==================== 打印接口 ====================
/**
* 打印签到表
* @param {Object} params - 打印参数
* @param {Number} params.competitionId - 赛事ID
* @param {Number} params.projectId - 项目ID可选
* @param {String} params.scheduleDate - 赛程日期(可选)
*/
export const printSignInSheet = (params) => {
return request({
url: '/api/blade-martial/export/print/sign-in',
method: 'get',
params,
responseType: 'blob'
})
}
/**
* 打印秩序册
* @param {Number} competitionId - 赛事ID
*/
export const printProgramBook = (competitionId) => {
return request({
url: '/api/blade-martial/export/print/program-book',
method: 'get',
params: { competitionId },
responseType: 'blob'
})
}
/**
* 打印成绩公告
* @param {Object} params - 打印参数
* @param {Number} params.projectId - 项目ID
* @param {Number} params.competitionId - 赛事ID
*/
export const printResultAnnouncement = (params) => {
return request({
url: '/api/blade-martial/export/print/result-announcement',
method: 'get',
params,
responseType: 'blob'
})
}

221
src/api/martial/info.js Normal file
View File

@@ -0,0 +1,221 @@
import request from '@/axios';
// ==================== <20>o<EFBFBD><03><06><> ====================
/**
* <20><><EFBFBD>o<EFBFBD>h
* @param {Number} current - SMu ؤ1
* @param {Number} size - <20>up<75> ؤ10
* @param {Object} params - <20><><EFBFBD>p
* @param {Number} params.competitionId - <20>[ID
* @param {String} params.infoType - <20>o{<7B>announcementlJ/notice<1A>/news<77><73>/rules<65>
* @param {String} params.title - <07>!<21>" <0C>
* @param {Number} params.publishStatus - <20><03>0<><30>/1<><31>/2<> <0B> <0C>
* @param {Boolean} params.isTop - /&nv<08>
* @param {Boolean} params.isImportant - /&́<08>
*/
export const getInfoPublishList = (current, size, params) => {
return request({
url: '/api/blade-martial/info-publish/list',
method: 'get',
params: {
current,
size,
...params
}
})
}
/**
* <20><><EFBFBD>o<EFBFBD><03><>
* @param {Number} id - <20>oID
*/
export const getInfoPublishDetail = (id) => {
return request({
url: '/api/blade-martial/info-publish/detail',
method: 'get',
params: { id }
})
}
/**
* <20><03>o
* @param {Object} data - <20>opn
* @param {Number} data.competitionId - <20>[ID
* @param {String} data.infoType - <20>o{<7B>
* @param {String} data.title - <07>
* @param {String} data.coverImage - b<>URL<08>
* @param {String} data.summary - X<>
* @param {String} data.content - <20><>̇,
* @param {Array} data.attachments - D<>URLp<4C><08>
* @param {String} data.publishTime - <20><03><><08>
k<19>s<EFBFBD>
* @param {Boolean} data.isTop - /&nv
* @param {Boolean} data.isImportant - /&́
*/
export const publishInfo = (data) => {
return request({
url: '/api/blade-martial/info-publish/publish',
method: 'post',
data
})
}
/**
* <20><><EFBFBD>o
* @param {Object} data - <20>opn
*/
export const updateInfoPublish = (data) => {
return request({
url: '/api/blade-martial/info-publish/update',
method: 'post',
data
})
}
/**
* d<>o
* @param {String} ids - <20>oID *(<17><06>
*/
export const removeInfoPublish = (ids) => {
return request({
url: '/api/blade-martial/info-publish/remove',
method: 'post',
params: { ids }
})
}
/**
* bnv<6E>
* @param {Number} id - <20>oID
* @param {Boolean} isTop - /&nv
*/
export const toggleTop = (id, isTop) => {
return request({
url: '/api/blade-martial/info-publish/toggle-top',
method: 'post',
data: { id, isTop }
})
}
/**
* <62>
* @param {Number} id - <20>oID
* @param {Boolean} isImportant - /&́
*/
export const toggleImportant = (id, isImportant) => {
return request({
url: '/api/blade-martial/info-publish/toggle-important',
method: 'post',
data: { id, isImportant }
})
}
/**
*
b<>
* @param {File} file - <20>G<EFBFBD><47>
*/
export const uploadCoverImage = (file) => {
const formData = new FormData()
formData.append('file', file)
return request({
url: '/api/blade-martial/info-publish/upload-cover',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
/**
*
D<>
* @param {File} file - D<><44><EFBFBD>
* @param {Function} onProgress -
ۦ<>
*/
export const uploadAttachment = (file, onProgress) => {
const formData = new FormData()
formData.append('file', file)
return request({
url: '/api/blade-martial/info-publish/upload-attachment',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
},
onUploadProgress: onProgress
})
}
/**
* y<>
D<>
* @param {Array} files - D<><44><EFBFBD>p<EFBFBD>
*/
export const batchUploadAttachments = (files) => {
const formData = new FormData()
files.forEach(file => {
formData.append('files', file)
})
return request({
url: '/api/blade-martial/info-publish/batch-upload-attachments',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
/**
*
<EFBFBD>/ <0B><>o
* @param {Number} id - <20>oID
* @param {Number} publishStatus - <20><03>1<><31>/2<> <0B>
*/
export const updatePublishStatus = (id, publishStatus) => {
return request({
url: '/api/blade-martial/info-publish/update-status',
method: 'post',
data: { id, publishStatus }
})
}
/**
* <20><><EFBFBD>
* @param {Number} competitionId - <20>[ID
*/
export const getInfoStatistics = (competitionId) => {
return request({
url: '/api/blade-martial/info-publish/statistics',
method: 'get',
params: { competitionId }
})
}
/**
* <20><><05><>
* @param {Number} id - <20>oID
*/
export const incrementViewCount = (id) => {
return request({
url: '/api/blade-martial/info-publish/increment-view',
method: 'post',
data: { id }
})
}
/**
* <20><><EFBFBD>o<EFBFBD><03>U
* @param {Object} params - <20><><EFBFBD>p
*/
export const exportInfoPublish = (params) => {
return request({
url: '/api/blade-martial/info-publish/export',
method: 'get',
params,
responseType: 'blob'
})

View File

@@ -0,0 +1,259 @@
import request from '@/axios';
// ==================== 武术赛事信息发布管理接口 ====================
/**
* 信息发布分页查询
* @param {Number} current - 当前页,默认1
* @param {Number} size - 每页条数,默认10
* @param {Object} params - 查询参数
* @param {Number} params.competitionId - 赛事ID
* @param {Number} params.infoType - 信息类型(可选)
* @param {Number} params.publishStatus - 发布状态(可选)
* @param {String} params.title - 标题(可选)
*/
export const getInfoPublishList = (current, size, params) => {
return request({
url: '/api/blade-martial/infoPublish/list',
method: 'get',
params: {
current,
size,
...params
}
})
}
/**
* 获取信息发布详情
* @param {Number} id - 信息ID
*/
export const getInfoPublishDetail = (id) => {
return request({
url: '/api/blade-martial/infoPublish/detail',
method: 'get',
params: { id }
})
}
/**
* 发布信息
* @param {Object} data - 信息数据
* @param {Number} data.competitionId - 赛事ID
* @param {Number} data.infoType - 信息类型1公告2通知3新闻4规则5其他
* @param {String} data.title - 标题
* @param {String} data.content - 内容
* @param {String} data.summary - 摘要(可选)
* @param {String} data.coverImage - 封面图片(可选)
* @param {Array} data.attachments - 附件URL数组可选
* @param {Number} data.isTop - 是否置顶0否1是
* @param {Number} data.isImportant - 是否重要0否1是
* @param {String} data.publishTime - 发布时间(可选,为空则立即发布)
*/
export const publishInfo = (data) => {
return request({
url: '/api/blade-martial/infoPublish/publish',
method: 'post',
data
})
}
/**
* 修改信息
* @param {Object} data - 信息数据
*/
export const updateInfo = (data) => {
return request({
url: '/api/blade-martial/infoPublish/update',
method: 'post',
data
})
}
/**
* 删除信息
* @param {String} ids - 信息ID,多个用逗号分隔
*/
export const removeInfo = (ids) => {
return request({
url: '/api/blade-martial/infoPublish/remove',
method: 'post',
params: { ids }
})
}
/**
* 撤回信息
* @param {Number} id - 信息ID
*/
export const withdrawInfo = (id) => {
return request({
url: '/api/blade-martial/infoPublish/withdraw',
method: 'post',
params: { id }
})
}
/**
* 置顶/取消置顶信息
* @param {Number} id - 信息ID
* @param {Number} isTop - 是否置顶0否1是
*/
export const toggleTop = (id, isTop) => {
return request({
url: '/api/blade-martial/infoPublish/toggle-top',
method: 'post',
params: { id, isTop }
})
}
/**
* 设置/取消重要信息
* @param {Number} id - 信息ID
* @param {Number} isImportant - 是否重要0否1是
*/
export const toggleImportant = (id, isImportant) => {
return request({
url: '/api/blade-martial/infoPublish/toggle-important',
method: 'post',
params: { id, isImportant }
})
}
/**
* 获取已发布信息列表(不分页)
* @param {Number} competitionId - 赛事ID
* @param {Number} infoType - 信息类型(可选)
* @param {Number} limit - 获取数量默认10
*/
export const getPublishedInfoList = (competitionId, infoType, limit = 10) => {
return request({
url: '/api/blade-martial/infoPublish/published',
method: 'get',
params: { competitionId, infoType, limit }
})
}
/**
* 获取置顶信息列表
* @param {Number} competitionId - 赛事ID
*/
export const getTopInfoList = (competitionId) => {
return request({
url: '/api/blade-martial/infoPublish/top-info',
method: 'get',
params: { competitionId }
})
}
/**
* 获取重要信息列表
* @param {Number} competitionId - 赛事ID
*/
export const getImportantInfoList = (competitionId) => {
return request({
url: '/api/blade-martial/infoPublish/important-info',
method: 'get',
params: { competitionId }
})
}
/**
* 搜索信息
* @param {Object} params - 搜索参数
* @param {Number} params.competitionId - 赛事ID
* @param {String} params.keyword - 关键词
* @param {Number} params.infoType - 信息类型(可选)
* @param {String} params.startTime - 开始时间(可选)
* @param {String} params.endTime - 结束时间(可选)
*/
export const searchInfo = (params) => {
return request({
url: '/api/blade-martial/infoPublish/search',
method: 'get',
params
})
}
/**
* 增加阅读量
* @param {Number} id - 信息ID
*/
export const increaseViewCount = (id) => {
return request({
url: '/api/blade-martial/infoPublish/view',
method: 'post',
params: { id }
})
}
/**
* 上传封面图片
* @param {File} file - 图片文件
*/
export const uploadCoverImage = (file) => {
const formData = new FormData()
formData.append('file', file)
return request({
url: '/api/blade-martial/infoPublish/upload-cover',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
/**
* 上传附件
* @param {File} file - 附件文件
*/
export const uploadAttachment = (file) => {
const formData = new FormData()
formData.append('file', file)
return request({
url: '/api/blade-martial/infoPublish/upload-attachment',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
/**
* 下载附件
* @param {String} attachmentUrl - 附件URL
*/
export const downloadAttachment = (attachmentUrl) => {
return request({
url: '/api/blade-martial/infoPublish/download',
method: 'get',
params: { attachmentUrl },
responseType: 'blob'
})
}
/**
* 获取信息统计
* @param {Number} competitionId - 赛事ID
*/
export const getInfoStatistics = (competitionId) => {
return request({
url: '/api/blade-martial/infoPublish/statistics',
method: 'get',
params: { competitionId }
})
}
/**
* 批量发布信息
* @param {Array} data - 信息数据数组
*/
export const batchPublishInfo = (data) => {
return request({
url: '/api/blade-martial/infoPublish/batch-publish',
method: 'post',
data
})
}

View File

@@ -0,0 +1,213 @@
import request from '@/axios';
// ==================== 武术赛事裁判邀请管理接口 ====================
/**
* 裁判邀请分页查询
* @param {Number} current - 当前页,默认1
* @param {Number} size - 每页条数,默认10
* @param {Object} params - 查询参数
* @param {Number} params.competitionId - 赛事ID
* @param {String} params.judgeName - 裁判姓名(可选)
* @param {String} params.judgeLevel - 裁判等级(可选)
* @param {Number} params.inviteStatus - 邀请状态(可选)
*/
export const getJudgeInviteList = (current, size, params) => {
return request({
url: '/api/blade-martial/judgeInvite/list',
method: 'get',
params: {
current,
size,
...params
}
})
}
/**
* 获取裁判邀请详情
* @param {Number} id - 邀请ID
*/
export const getJudgeInviteDetail = (id) => {
return request({
url: '/api/blade-martial/judgeInvite/detail',
method: 'get',
params: { id }
})
}
/**
* 发送邀请
* @param {Object} data - 邀请数据
* @param {Number} data.competitionId - 赛事ID
* @param {String} data.judgeName - 裁判姓名
* @param {String} data.judgeLevel - 裁判等级
* @param {String} data.contactPhone - 联系电话
* @param {String} data.contactEmail - 联系邮箱
* @param {String} data.inviteMessage - 邀请信息(可选)
*/
export const sendInvite = (data) => {
return request({
url: '/api/blade-martial/judgeInvite/send',
method: 'post',
data
})
}
/**
* 修改邀请
* @param {Object} data - 邀请数据
*/
export const updateInvite = (data) => {
return request({
url: '/api/blade-martial/judgeInvite/update',
method: 'post',
data
})
}
/**
* 删除邀请
* @param {String} ids - 邀请ID,多个用逗号分隔
*/
export const removeInvite = (ids) => {
return request({
url: '/api/blade-martial/judgeInvite/remove',
method: 'post',
params: { ids }
})
}
/**
* 批量发送邀请
* @param {Object} data - 批量邀请参数
* @param {Number} data.competitionId - 赛事ID
* @param {Array} data.judges - 裁判信息数组
* @param {String} data.inviteMessage - 邀请信息(可选)
*/
export const batchSendInvites = (data) => {
return request({
url: '/api/blade-martial/judgeInvite/batch-send',
method: 'post',
data
})
}
/**
* 重新发送邀请
* @param {Number} id - 邀请ID
*/
export const resendInvite = (id) => {
return request({
url: '/api/blade-martial/judgeInvite/resend',
method: 'post',
params: { id }
})
}
/**
* 裁判回复邀请
* @param {Object} data - 回复数据
* @param {Number} data.inviteId - 邀请ID
* @param {String} data.inviteToken - 邀请令牌
* @param {Number} data.replyStatus - 回复状态1接受2拒绝
* @param {String} data.replyNote - 回复备注
*/
export const replyInvite = (data) => {
return request({
url: '/api/blade-martial/judgeInvite/reply',
method: 'post',
data
})
}
/**
* 取消邀请
* @param {Number} id - 邀请ID
* @param {String} cancelReason - 取消原因
*/
export const cancelInvite = (id, cancelReason) => {
return request({
url: '/api/blade-martial/judgeInvite/cancel',
method: 'post',
params: { id },
data: { cancelReason }
})
}
/**
* 确认邀请
* @param {Number} id - 邀请ID
*/
export const confirmInvite = (id) => {
return request({
url: '/api/blade-martial/judgeInvite/confirm',
method: 'post',
params: { id }
})
}
/**
* 获取邀请统计
* @param {Number} competitionId - 赛事ID
*/
export const getInviteStatistics = (competitionId) => {
return request({
url: '/api/blade-martial/judgeInvite/statistics',
method: 'get',
params: { competitionId }
})
}
/**
* 获取已接受邀请的裁判列表
* @param {Number} competitionId - 赛事ID
*/
export const getAcceptedJudges = (competitionId) => {
return request({
url: '/api/blade-martial/judgeInvite/accepted-judges',
method: 'get',
params: { competitionId }
})
}
/**
* 从裁判库导入
* @param {Object} data - 导入参数
* @param {Number} data.competitionId - 赛事ID
* @param {Array} data.judgeIds - 裁判ID数组从裁判库选择
*/
export const importFromJudgePool = (data) => {
return request({
url: '/api/blade-martial/judgeInvite/import-from-pool',
method: 'post',
data
})
}
/**
* 导出邀请名单
* @param {Object} params - 导出参数
*/
export const exportInvites = (params) => {
return request({
url: '/api/blade-martial/judgeInvite/export',
method: 'get',
params,
responseType: 'blob'
})
}
/**
* 发送提醒消息
* @param {Number} id - 邀请ID
* @param {String} reminderMessage - 提醒消息
*/
export const sendReminder = (id, reminderMessage) => {
return request({
url: '/api/blade-martial/judgeInvite/send-reminder',
method: 'post',
params: { id },
data: { reminderMessage }
})
}

View File

@@ -0,0 +1,248 @@
import request from '@/axios';
// ==================== 武术赛事裁判-项目关联管理接口 ====================
/**
* 裁判-项目关联分页查询
* @param {Number} current - 当前页,默认1
* @param {Number} size - 每页条数,默认10
* @param {Object} params - 查询参数
* @param {Number} params.competitionId - 赛事ID
* @param {Number} params.judgeId - 裁判ID可选
* @param {Number} params.projectId - 项目ID可选
* @param {String} params.role - 角色(可选)
*/
export const getJudgeProjectList = (current, size, params) => {
return request({
url: '/api/blade-martial/judge-project/list',
method: 'get',
params: {
current,
size,
...params
}
})
}
/**
* 获取裁判-项目关联详情
* @param {Number} id - 关联ID
*/
export const getJudgeProjectDetail = (id) => {
return request({
url: '/api/blade-martial/judge-project/detail',
method: 'get',
params: { id }
})
}
/**
* 分配裁判到项目
* @param {Object} data - 分配数据
* @param {Number} data.competitionId - 赛事ID
* @param {Number} data.judgeId - 裁判ID
* @param {String} data.judgeName - 裁判姓名
* @param {Number} data.projectId - 项目ID
* @param {String} data.projectName - 项目名称
* @param {String} data.role - 角色(主裁、副裁、执裁)
*/
export const assignJudge = (data) => {
return request({
url: '/api/blade-martial/judge-project/assign',
method: 'post',
data
})
}
/**
* 修改分配
* @param {Object} data - 分配数据
*/
export const updateAssignment = (data) => {
return request({
url: '/api/blade-martial/judge-project/update',
method: 'post',
data
})
}
/**
* 删除分配
* @param {String} ids - 关联ID,多个用逗号分隔
*/
export const removeAssignment = (ids) => {
return request({
url: '/api/blade-martial/judge-project/remove',
method: 'post',
params: { ids }
})
}
/**
* 批量分配裁判
* @param {Object} data - 批量分配参数
* @param {Number} data.competitionId - 赛事ID
* @param {Array} data.assignments - 分配信息数组
* @param {Number} data.assignments[].judgeId - 裁判ID
* @param {Number} data.assignments[].projectId - 项目ID
* @param {String} data.assignments[].role - 角色
*/
export const batchAssignJudges = (data) => {
return request({
url: '/api/blade-martial/judge-project/batch-assign',
method: 'post',
data
})
}
/**
* 自动分配裁判
* @param {Object} data - 自动分配参数
* @param {Number} data.competitionId - 赛事ID
* @param {Number} data.projectId - 项目ID可选
* @param {String} data.strategy - 分配策略balance负载均衡/level按等级
*/
export const autoAssignJudges = (data) => {
return request({
url: '/api/blade-martial/judge-project/auto-assign',
method: 'post',
data
})
}
/**
* 获取项目的裁判列表
* @param {Number} projectId - 项目ID
*/
export const getJudgesByProject = (projectId) => {
return request({
url: '/api/blade-martial/judge-project/judges-by-project',
method: 'get',
params: { projectId }
})
}
/**
* 获取裁判的项目列表
* @param {Number} judgeId - 裁判ID
* @param {Number} competitionId - 赛事ID
*/
export const getProjectsByJudge = (judgeId, competitionId) => {
return request({
url: '/api/blade-martial/judge-project/projects-by-judge',
method: 'get',
params: { judgeId, competitionId }
})
}
/**
* 获取裁判工作量统计
* @param {Number} competitionId - 赛事ID
*/
export const getJudgeWorkload = (competitionId) => {
return request({
url: '/api/blade-martial/judge-project/workload',
method: 'get',
params: { competitionId }
})
}
/**
* 检查裁判冲突
* @param {Object} data - 检查参数
* @param {Number} data.judgeId - 裁判ID
* @param {Number} data.projectId - 项目ID
* @param {Number} data.competitionId - 赛事ID
*/
export const checkJudgeConflict = (data) => {
return request({
url: '/api/blade-martial/judge-project/check-conflict',
method: 'post',
data
})
}
/**
* 交换两个裁判的项目分配
* @param {Object} data - 交换参数
* @param {Number} data.assignmentId1 - 分配1的ID
* @param {Number} data.assignmentId2 - 分配2的ID
*/
export const swapJudgeAssignment = (data) => {
return request({
url: '/api/blade-martial/judge-project/swap',
method: 'post',
data
})
}
/**
* 复制分配到其他项目
* @param {Object} data - 复制参数
* @param {Number} data.sourceProjectId - 源项目ID
* @param {Number} data.targetProjectId - 目标项目ID
*/
export const copyAssignment = (data) => {
return request({
url: '/api/blade-martial/judge-project/copy',
method: 'post',
data
})
}
/**
* 获取可分配的裁判列表
* @param {Number} competitionId - 赛事ID
* @param {Number} projectId - 项目ID可选
*/
export const getAvailableJudges = (competitionId, projectId) => {
return request({
url: '/api/blade-martial/judge-project/available-judges',
method: 'get',
params: { competitionId, projectId }
})
}
/**
* 获取未分配裁判的项目列表
* @param {Number} competitionId - 赛事ID
*/
export const getUnassignedProjects = (competitionId) => {
return request({
url: '/api/blade-martial/judge-project/unassigned-projects',
method: 'get',
params: { competitionId }
})
}
/**
* 导出裁判分配表
* @param {Object} params - 导出参数
*/
export const exportAssignments = (params) => {
return request({
url: '/api/blade-martial/judge-project/export',
method: 'get',
params,
responseType: 'blob'
})
}
/**
* 导入裁判分配
* @param {Number} competitionId - 赛事ID
* @param {File} file - Excel文件
*/
export const importAssignments = (competitionId, file) => {
const formData = new FormData()
formData.append('competitionId', competitionId)
formData.append('file', file)
return request({
url: '/api/blade-martial/judge-project/import',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}

203
src/api/martial/live.js Normal file
View File

@@ -0,0 +1,203 @@
import request from '@/axios';
// ==================== f/[<5B><><EFBFBD><EFBFBD><06><> ====================
/**
* <20><><EFBFBD><EFBFBD>u<><75>
* @param {Number} current - SMu,ؤ1
* @param {Number} size - <20>uap,ؤ10
* @param {Object} params - <20><><EFBFBD>p
* @param {Number} params.competitionId - [<5B>ID
* @param {String} params.updateType - <20><>{<7B>match[<5B>/result<10>/notice<1A>
* @param {Number} params.projectId - y<>ID<08>
* @param {Number} params.venueId - :0ID<08>
*/
export const getLiveUpdateList = (current, size, params) => {
return request({
url: '/api/blade-martial/live-update/list',
method: 'get',
params: {
current,
size,
...params
}
})
}
/**
* <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
* @param {Number} id - <20><>ID
*/
export const getLiveUpdateDetail = (id) => {
return request({
url: '/api/blade-martial/live-update/detail',
method: 'get',
params: { id }
})
}
/**
* <20><03><><EFBFBD><EFBFBD>
* @param {Object} data - <20><><EFBFBD><EFBFBD>pn
* @param {Number} data.competitionId - [<5B>ID
* @param {String} data.title - <07>
* @param {String} data.content - <20><>̇,
* @param {String} data.updateType - <20><>{<7B>
* @param {Number} data.projectId - y<>ID<08>
* @param {Number} data.venueId - :0ID<08>
* @param {Array} data.images - <20>GURLp<4C>
* @param {Array} data.videos - ƑURLp<4C>
* @param {Boolean} data.isTop - /&nv
*/
export const publishLiveUpdate = (data) => {
return request({
url: '/api/blade-martial/live-update/publish',
method: 'post',
data
})
}
/**
* <20>9<EFBFBD><39><EFBFBD><EFBFBD>
* @param {Object} data - <20><><EFBFBD><EFBFBD>pn
*/
export const updateLiveUpdate = (data) => {
return request({
url: '/api/blade-martial/live-update/update',
method: 'post',
data
})
}
/**
* d<><64><EFBFBD><EFBFBD>
* @param {String} ids - <20><>ID,*(<17><06>
*/
export const removeLiveUpdate = (ids) => {
return request({
url: '/api/blade-martial/live-update/remove',
method: 'post',
params: { ids }
})
}
/**
* bnv<6E>
* @param {Number} id - <20><>ID
* @param {Boolean} isTop - /&nv
*/
export const toggleTop = (id, isTop) => {
return request({
url: '/api/blade-martial/live-update/toggle-top',
method: 'post',
data: { id, isTop }
})
}
/**
*
<20><><EFBFBD>G
* @param {File} file - <20>G<EFBFBD><47>
*/
export const uploadLiveImage = (file) => {
const formData = new FormData()
formData.append('file', file)
return request({
url: '/api/blade-martial/live-update/upload-image',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
/**
*
<20><>Ƒ
* @param {File} file - Ƒ<><C691>
* @param {Function} onProgress -
ۦ<>
*/
export const uploadLiveVideo = (file, onProgress) => {
const formData = new FormData()
formData.append('file', file)
return request({
url: '/api/blade-martial/live-update/upload-video',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
},
onUploadProgress: onProgress
})
}
/**
* y<>
<20>G
* @param {Array} files - <20>G<EFBFBD><47>p<EFBFBD>
*/
export const batchUploadImages = (files) => {
const formData = new FormData()
files.forEach(file => {
formData.append('files', file)
})
return request({
url: '/api/blade-martial/live-update/batch-upload-images',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
/**
* <20><><EFBFBD><EFBFBD>ߡpn
* @param {Number} competitionId - [<5B>ID
*/
export const getLiveStatistics = (competitionId) => {
return request({
url: '/api/blade-martial/live-update/statistics',
method: 'get',
params: { competitionId }
})
}
/**
* <20>^<5E><><EFBFBD><EFBFBD>
* @param {Number} id - <20><>ID
*/
export const likeLiveUpdate = (id) => {
return request({
url: '/api/blade-martial/live-update/like',
method: 'post',
data: { id }
})
}
/**
* ֈ<>^
* @param {Number} id - <20><>ID
*/
export const unlikeLiveUpdate = (id) => {
return request({
url: '/api/blade-martial/live-update/unlike',
method: 'post',
data: { id }
})
}
/**
* <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
* @param {Number} competitionId - [<5B>ID
* @param {Number} limit - p<>P6
*/
export const getHotLiveUpdates = (competitionId, limit = 10) => {
return request({
url: '/api/blade-martial/live-update/hot',
method: 'get',
params: { competitionId, limit }
})
}

View File

@@ -0,0 +1,221 @@
import request from '@/axios';
// ==================== 武术赛事直播更新管理接口 ====================
/**
* 直播更新分页查询
* @param {Number} current - 当前页,默认1
* @param {Number} size - 每页条数,默认10
* @param {Object} params - 查询参数
* @param {Number} params.competitionId - 赛事ID
* @param {Number} params.projectId - 项目ID可选
* @param {Number} params.venueId - 场地ID可选
* @param {Number} params.updateType - 更新类型(可选)
*/
export const getLiveUpdateList = (current, size, params) => {
return request({
url: '/api/blade-martial/liveUpdate/list',
method: 'get',
params: {
current,
size,
...params
}
})
}
/**
* 获取直播更新详情
* @param {Number} id - 更新ID
*/
export const getLiveUpdateDetail = (id) => {
return request({
url: '/api/blade-martial/liveUpdate/detail',
method: 'get',
params: { id }
})
}
/**
* 发布直播更新
* @param {Object} data - 更新数据
* @param {Number} data.competitionId - 赛事ID
* @param {Number} data.projectId - 项目ID可选
* @param {Number} data.venueId - 场地ID可选
* @param {Number} data.updateType - 更新类型1赛况2成绩3通知4其他
* @param {String} data.title - 标题
* @param {String} data.content - 内容
* @param {Array} data.images - 图片URL数组可选
* @param {String} data.videoUrl - 视频URL可选
* @param {Number} data.isTop - 是否置顶0否1是
*/
export const publishLiveUpdate = (data) => {
return request({
url: '/api/blade-martial/liveUpdate/publish',
method: 'post',
data
})
}
/**
* 修改直播更新
* @param {Object} data - 更新数据
*/
export const updateLiveUpdate = (data) => {
return request({
url: '/api/blade-martial/liveUpdate/update',
method: 'post',
data
})
}
/**
* 删除直播更新
* @param {String} ids - 更新ID,多个用逗号分隔
*/
export const removeLiveUpdate = (ids) => {
return request({
url: '/api/blade-martial/liveUpdate/remove',
method: 'post',
params: { ids }
})
}
/**
* 批量发布直播更新
* @param {Array} data - 更新数据数组
*/
export const batchPublishLiveUpdates = (data) => {
return request({
url: '/api/blade-martial/liveUpdate/batch-publish',
method: 'post',
data
})
}
/**
* 置顶/取消置顶直播更新
* @param {Number} id - 更新ID
* @param {Number} isTop - 是否置顶0否1是
*/
export const toggleTop = (id, isTop) => {
return request({
url: '/api/blade-martial/liveUpdate/toggle-top',
method: 'post',
params: { id, isTop }
})
}
/**
* 获取最新直播更新列表(不分页)
* @param {Number} competitionId - 赛事ID
* @param {Number} limit - 获取数量默认10
*/
export const getLatestUpdates = (competitionId, limit = 10) => {
return request({
url: '/api/blade-martial/liveUpdate/latest',
method: 'get',
params: { competitionId, limit }
})
}
/**
* 获取置顶直播更新列表
* @param {Number} competitionId - 赛事ID
*/
export const getTopUpdates = (competitionId) => {
return request({
url: '/api/blade-martial/liveUpdate/top-updates',
method: 'get',
params: { competitionId }
})
}
/**
* 获取项目的直播更新
* @param {Number} projectId - 项目ID
* @param {Number} limit - 获取数量默认20
*/
export const getProjectUpdates = (projectId, limit = 20) => {
return request({
url: '/api/blade-martial/liveUpdate/project-updates',
method: 'get',
params: { projectId, limit }
})
}
/**
* 获取场地的直播更新
* @param {Number} venueId - 场地ID
* @param {Number} limit - 获取数量默认20
*/
export const getVenueUpdates = (venueId, limit = 20) => {
return request({
url: '/api/blade-martial/liveUpdate/venue-updates',
method: 'get',
params: { venueId, limit }
})
}
/**
* 搜索直播更新
* @param {Object} params - 搜索参数
* @param {Number} params.competitionId - 赛事ID
* @param {String} params.keyword - 关键词
* @param {Number} params.updateType - 更新类型(可选)
* @param {String} params.startTime - 开始时间(可选)
* @param {String} params.endTime - 结束时间(可选)
*/
export const searchLiveUpdates = (params) => {
return request({
url: '/api/blade-martial/liveUpdate/search',
method: 'get',
params
})
}
/**
* 上传直播图片
* @param {File} file - 图片文件
*/
export const uploadLiveImage = (file) => {
const formData = new FormData()
formData.append('file', file)
return request({
url: '/api/blade-martial/liveUpdate/upload-image',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
/**
* 上传直播视频
* @param {File} file - 视频文件
*/
export const uploadLiveVideo = (file) => {
const formData = new FormData()
formData.append('file', file)
return request({
url: '/api/blade-martial/liveUpdate/upload-video',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
/**
* 获取直播统计
* @param {Number} competitionId - 赛事ID
*/
export const getLiveStatistics = (competitionId) => {
return request({
url: '/api/blade-martial/liveUpdate/statistics',
method: 'get',
params: { competitionId }
})
}

138
src/api/martial/project.js Normal file
View File

@@ -0,0 +1,138 @@
import request from '@/axios';
// ==================== 武术赛事项目管理接口 ====================
/**
* 项目分页查询
* @param {Number} current - 当前页,默认1
* @param {Number} size - 每页条数,默认10
* @param {Object} params - 查询参数
* @param {Number} params.competitionId - 赛事ID
* @param {String} params.projectName - 项目名称(可选)
* @param {String} params.category - 分组类别(可选)
* @param {String} params.eventType - 项目类型(可选)
*/
export const getProjectList = (current, size, params) => {
return request({
url: '/api/blade-martial/project/list',
method: 'get',
params: {
current,
size,
...params
}
})
}
/**
* 获取项目详情
* @param {Number} id - 项目ID
*/
export const getProjectDetail = (id) => {
return request({
url: '/api/blade-martial/project/detail',
method: 'get',
params: { id }
})
}
/**
* 新增项目
* @param {Object} data - 项目数据
* @param {Number} data.competitionId - 赛事ID
* @param {String} data.projectName - 项目名称
* @param {String} data.projectCode - 项目编码
* @param {String} data.category - 分组类别(男子、女子、团体)
* @param {String} data.eventType - 项目类型(套路、散打等)
* @param {Number} data.registrationFee - 报名费
* @param {String} data.registrationStartTime - 报名开始时间
* @param {String} data.registrationEndTime - 报名结束时间
* @param {Number} data.maxParticipants - 最大参赛人数
* @param {String} data.rules - 比赛规则
*/
export const addProject = (data) => {
return request({
url: '/api/blade-martial/project/save',
method: 'post',
data
})
}
/**
* 修改项目
* @param {Object} data - 项目数据
*/
export const updateProject = (data) => {
return request({
url: '/api/blade-martial/project/update',
method: 'post',
data
})
}
/**
* 删除项目
* @param {String} ids - 项目ID,多个用逗号分隔
*/
export const removeProject = (ids) => {
return request({
url: '/api/blade-martial/project/remove',
method: 'post',
params: { ids }
})
}
/**
* 获取赛事的项目列表(不分页)
* @param {Number} competitionId - 赛事ID
*/
export const getProjectsByCompetition = (competitionId) => {
return request({
url: '/api/blade-martial/project/list-by-competition',
method: 'get',
params: { competitionId }
})
}
/**
* 批量导入项目
* @param {Number} competitionId - 赛事ID
* @param {File} file - Excel文件
*/
export const importProjects = (competitionId, file) => {
const formData = new FormData()
formData.append('competitionId', competitionId)
formData.append('file', file)
return request({
url: '/api/blade-martial/project/import',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
/**
* 导出项目模板
*/
export const exportProjectTemplate = () => {
return request({
url: '/api/blade-martial/project/export-template',
method: 'get',
responseType: 'blob'
})
}
/**
* 导出项目列表
* @param {Object} params - 查询参数
*/
export const exportProjects = (params) => {
return request({
url: '/api/blade-martial/project/export',
method: 'get',
params,
responseType: 'blob'
})
}

234
src/api/martial/result.js Normal file
View File

@@ -0,0 +1,234 @@
import request from '@/axios';
// ==================== 武术赛事成绩管理接口 ====================
/**
* 成绩分页查询
* @param {Number} current - 当前页,默认1
* @param {Number} size - 每页条数,默认10
* @param {Object} params - 查询参数
* @param {Number} params.competitionId - 赛事ID
* @param {Number} params.projectId - 项目ID可选
* @param {Number} params.athleteId - 运动员ID可选
* @param {String} params.playerName - 运动员姓名(可选)
* @param {String} params.teamName - 团队名称(可选)
* @param {Number} params.isFinal - 是否最终成绩(可选)
*/
export const getResultList = (current, size, params) => {
return request({
url: '/api/blade-martial/result/list',
method: 'get',
params: {
current,
size,
...params
}
})
}
/**
* 获取成绩详情
* @param {Number} id - 成绩ID
*/
export const getResultDetail = (id) => {
return request({
url: '/api/blade-martial/result/detail',
method: 'get',
params: { id }
})
}
/**
* 新增成绩
* @param {Object} data - 成绩数据
* @param {Number} data.competitionId - 赛事ID
* @param {Number} data.athleteId - 运动员ID
* @param {Number} data.projectId - 项目ID
* @param {Number} data.venueId - 场地ID
* @param {String} data.playerName - 运动员姓名
* @param {String} data.teamName - 团队名称
* @param {Number} data.totalScore - 总分
* @param {Number} data.difficultyCoefficient - 难度系数
* @param {Number} data.finalScore - 最终成绩
*/
export const addResult = (data) => {
return request({
url: '/api/blade-martial/result/save',
method: 'post',
data
})
}
/**
* 修改成绩
* @param {Object} data - 成绩数据
*/
export const updateResult = (data) => {
return request({
url: '/api/blade-martial/result/update',
method: 'post',
data
})
}
/**
* 删除成绩
* @param {String} ids - 成绩ID,多个用逗号分隔
*/
export const removeResult = (ids) => {
return request({
url: '/api/blade-martial/result/remove',
method: 'post',
params: { ids }
})
}
// ==================== 成绩高级功能接口 ====================
/**
* 计算成绩
* @param {Object} data - 计算参数
* @param {Number} data.athleteId - 运动员ID
* @param {Number} data.projectId - 项目ID
* @param {Number} data.competitionId - 赛事ID
* @param {Boolean} data.autoRanking - 是否自动排名(可选)
*/
export const calculateResult = (data) => {
return request({
url: '/api/blade-martial/result/calculate',
method: 'post',
data
})
}
/**
* 批量计算成绩
* @param {Object} data - 计算参数
* @param {Number} data.projectId - 项目ID
* @param {Number} data.competitionId - 赛事ID
* @param {Array} data.athleteIds - 运动员ID数组可选为空时计算所有
*/
export const batchCalculateResults = (data) => {
return request({
url: '/api/blade-martial/result/batch-calculate',
method: 'post',
data
})
}
/**
* 自动排名
* @param {Object} data - 排名参数
* @param {Number} data.projectId - 项目ID
* @param {Number} data.competitionId - 赛事ID
* @param {String} data.category - 分组类别(可选)
*/
export const autoRanking = (data) => {
return request({
url: '/api/blade-martial/result/ranking',
method: 'post',
data
})
}
/**
* 分配奖牌
* @param {Object} data - 奖牌分配参数
* @param {Number} data.projectId - 项目ID
* @param {Number} data.competitionId - 赛事ID
* @param {Number} data.goldCount - 金牌数量
* @param {Number} data.silverCount - 银牌数量
* @param {Number} data.bronzeCount - 铜牌数量
*/
export const allocateMedals = (data) => {
return request({
url: '/api/blade-martial/result/medals',
method: 'post',
data
})
}
/**
* 成绩复核
* @param {Object} data - 复核参数
* @param {Number} data.resultId - 成绩ID
* @param {String} data.reviewNote - 复核说明
* @param {Number} data.reviewStatus - 复核状态1通过2驳回
*/
export const reviewResult = (data) => {
return request({
url: '/api/blade-martial/result/review',
method: 'post',
data
})
}
/**
* 发布成绩
* @param {Object} data - 发布参数
* @param {Number} data.projectId - 项目ID可选
* @param {Number} data.competitionId - 赛事ID
* @param {Array} data.resultIds - 成绩ID数组可选
* @param {Boolean} data.publishAll - 是否发布全部(可选)
*/
export const publishResult = (data) => {
return request({
url: '/api/blade-martial/result/publish',
method: 'post',
data
})
}
/**
* 撤销发布成绩
* @param {Object} data - 撤销参数
* @param {Number} data.projectId - 项目ID可选
* @param {Number} data.competitionId - 赛事ID
* @param {Array} data.resultIds - 成绩ID数组可选
*/
export const unpublishResult = (data) => {
return request({
url: '/api/blade-martial/result/unpublish',
method: 'post',
data
})
}
/**
* 获取项目成绩统计
* @param {Number} projectId - 项目ID
* @param {Number} competitionId - 赛事ID
*/
export const getResultStatistics = (projectId, competitionId) => {
return request({
url: '/api/blade-martial/result/statistics',
method: 'get',
params: { projectId, competitionId }
})
}
/**
* 获取奖牌榜
* @param {Number} competitionId - 赛事ID
* @param {String} groupBy - 分组方式team团队/region地区
*/
export const getMedalRanking = (competitionId, groupBy = 'team') => {
return request({
url: '/api/blade-martial/result/medal-ranking',
method: 'get',
params: { competitionId, groupBy }
})
}
/**
* 导出成绩单
* @param {Object} params - 查询参数
*/
export const exportResults = (params) => {
return request({
url: '/api/blade-martial/result/export',
method: 'get',
params,
responseType: 'blob'
})
}

View File

@@ -0,0 +1,239 @@
import request from '@/axios';
// ==================== 武术赛事赛程选手关联管理接口 ====================
/**
* 赛程选手关联分页查询
* @param {Number} current - 当前页,默认1
* @param {Number} size - 每页条数,默认10
* @param {Object} params - 查询参数
* @param {Number} params.scheduleId - 赛程ID
* @param {Number} params.schedulePlanId - 赛程计划ID可选
* @param {Number} params.athleteId - 运动员ID可选
* @param {Number} params.status - 状态(可选)
*/
export const getScheduleAthleteList = (current, size, params) => {
return request({
url: '/api/blade-martial/scheduleAthlete/list',
method: 'get',
params: {
current,
size,
...params
}
})
}
/**
* 获取赛程选手关联详情
* @param {Number} id - 关联ID
*/
export const getScheduleAthleteDetail = (id) => {
return request({
url: '/api/blade-martial/scheduleAthlete/detail',
method: 'get',
params: { id }
})
}
/**
* 新增赛程选手关联
* @param {Object} data - 关联数据
* @param {Number} data.scheduleId - 赛程ID
* @param {Number} data.schedulePlanId - 赛程计划ID
* @param {Number} data.athleteId - 运动员ID
* @param {String} data.athleteName - 运动员姓名
* @param {Number} data.competitionOrder - 出场顺序
* @param {Number} data.status - 状态0待出场1进行中2已完成
*/
export const addScheduleAthlete = (data) => {
return request({
url: '/api/blade-martial/scheduleAthlete/save',
method: 'post',
data
})
}
/**
* 修改赛程选手关联
* @param {Object} data - 关联数据
*/
export const updateScheduleAthlete = (data) => {
return request({
url: '/api/blade-martial/scheduleAthlete/update',
method: 'post',
data
})
}
/**
* 删除赛程选手关联
* @param {String} ids - 关联ID,多个用逗号分隔
*/
export const removeScheduleAthlete = (ids) => {
return request({
url: '/api/blade-martial/scheduleAthlete/remove',
method: 'post',
params: { ids }
})
}
/**
* 批量添加赛程选手
* @param {Object} data - 批量添加参数
* @param {Number} data.schedulePlanId - 赛程计划ID
* @param {Array} data.athleteIds - 运动员ID数组
* @param {Boolean} data.autoOrder - 是否自动排序默认true
*/
export const batchAddScheduleAthletes = (data) => {
return request({
url: '/api/blade-martial/scheduleAthlete/batch-save',
method: 'post',
data
})
}
/**
* 自动分配选手到赛程
* @param {Object} data - 分配参数
* @param {Number} data.scheduleId - 赛程ID
* @param {Number} data.projectId - 项目ID
* @param {String} data.allocateStrategy - 分配策略random随机/order顺序
*/
export const autoAllocateAthletes = (data) => {
return request({
url: '/api/blade-martial/scheduleAthlete/auto-allocate',
method: 'post',
data
})
}
/**
* 调整选手出场顺序
* @param {Object} data - 调整参数
* @param {Number} data.id - 关联ID
* @param {Number} data.targetOrder - 目标顺序
*/
export const adjustAthleteOrder = (data) => {
return request({
url: '/api/blade-martial/scheduleAthlete/adjust-order',
method: 'post',
data
})
}
/**
* 交换两个选手的出场顺序
* @param {Object} data - 交换参数
* @param {Number} data.athleteId1 - 选手1的关联ID
* @param {Number} data.athleteId2 - 选手2的关联ID
*/
export const swapAthleteOrder = (data) => {
return request({
url: '/api/blade-martial/scheduleAthlete/swap-order',
method: 'post',
data
})
}
/**
* 随机排序选手出场顺序
* @param {Number} schedulePlanId - 赛程计划ID
*/
export const randomizeAthleteOrder = (schedulePlanId) => {
return request({
url: '/api/blade-martial/scheduleAthlete/randomize',
method: 'post',
params: { schedulePlanId }
})
}
/**
* 更新选手状态
* @param {Object} data - 状态更新参数
* @param {Number} data.id - 关联ID
* @param {Number} data.status - 状态0待出场1进行中2已完成
*/
export const updateAthleteStatus = (data) => {
return request({
url: '/api/blade-martial/scheduleAthlete/update-status',
method: 'post',
data
})
}
/**
* 获取赛程计划的选手列表(不分页)
* @param {Number} schedulePlanId - 赛程计划ID
*/
export const getAthletesBySchedulePlan = (schedulePlanId) => {
return request({
url: '/api/blade-martial/scheduleAthlete/list-by-plan',
method: 'get',
params: { schedulePlanId }
})
}
/**
* 获取运动员的赛程安排
* @param {Number} athleteId - 运动员ID
* @param {Number} competitionId - 赛事ID
*/
export const getAthleteSchedules = (athleteId, competitionId) => {
return request({
url: '/api/blade-martial/scheduleAthlete/athlete-schedules',
method: 'get',
params: { athleteId, competitionId }
})
}
/**
* 检查选手时间冲突
* @param {Object} data - 检查参数
* @param {Number} data.athleteId - 运动员ID
* @param {Number} data.schedulePlanId - 赛程计划ID
*/
export const checkAthleteConflict = (data) => {
return request({
url: '/api/blade-martial/scheduleAthlete/check-conflict',
method: 'post',
data
})
}
/**
* 签到选手
* @param {Number} id - 关联ID
*/
export const checkInAthlete = (id) => {
return request({
url: '/api/blade-martial/scheduleAthlete/check-in',
method: 'post',
params: { id }
})
}
/**
* 批量签到选手
* @param {Array} ids - 关联ID数组
*/
export const batchCheckInAthletes = (ids) => {
return request({
url: '/api/blade-martial/scheduleAthlete/batch-check-in',
method: 'post',
data: ids
})
}
/**
* 导出赛程选手名单
* @param {Object} params - 导出参数
*/
export const exportScheduleAthletes = (params) => {
return request({
url: '/api/blade-martial/scheduleAthlete/export',
method: 'get',
params,
responseType: 'blob'
})
}

View File

@@ -0,0 +1,196 @@
import request from '@/axios';
// ==================== 武术赛事赛程计划管理接口 ====================
/**
* 赛程计划分页查询
* @param {Number} current - 当前页,默认1
* @param {Number} size - 每页条数,默认10
* @param {Object} params - 查询参数
* @param {Number} params.scheduleId - 赛程ID
* @param {Number} params.venueId - 场地ID可选
* @param {Number} params.projectId - 项目ID可选
* @param {String} params.planDate - 计划日期(可选)
*/
export const getSchedulePlanList = (current, size, params) => {
return request({
url: '/api/blade-martial/schedule-plan/list',
method: 'get',
params: {
current,
size,
...params
}
})
}
/**
* 获取赛程计划详情
* @param {Number} id - 赛程计划ID
*/
export const getSchedulePlanDetail = (id) => {
return request({
url: '/api/blade-martial/schedule-plan/detail',
method: 'get',
params: { id }
})
}
/**
* 新增赛程计划
* @param {Object} data - 赛程计划数据
* @param {Number} data.scheduleId - 赛程ID
* @param {String} data.planDate - 计划日期
* @param {String} data.startTime - 开始时间
* @param {String} data.endTime - 结束时间
* @param {Number} data.venueId - 场地ID
* @param {String} data.venueName - 场地名称
* @param {Number} data.projectId - 项目ID
* @param {String} data.projectName - 项目名称
* @param {Number} data.planOrder - 计划顺序
*/
export const addSchedulePlan = (data) => {
return request({
url: '/api/blade-martial/schedule-plan/save',
method: 'post',
data
})
}
/**
* 修改赛程计划
* @param {Object} data - 赛程计划数据
*/
export const updateSchedulePlan = (data) => {
return request({
url: '/api/blade-martial/schedule-plan/update',
method: 'post',
data
})
}
/**
* 删除赛程计划
* @param {String} ids - 赛程计划ID,多个用逗号分隔
*/
export const removeSchedulePlan = (ids) => {
return request({
url: '/api/blade-martial/schedule-plan/remove',
method: 'post',
params: { ids }
})
}
/**
* 批量添加赛程计划
* @param {Array} data - 赛程计划数据数组
*/
export const batchAddSchedulePlans = (data) => {
return request({
url: '/api/blade-martial/schedule-plan/batch-save',
method: 'post',
data
})
}
/**
* 自动编排赛程
* @param {Object} data - 编排参数
* @param {Number} data.scheduleId - 赛程ID
* @param {Number} data.competitionId - 赛事ID
* @param {String} data.startDate - 开始日期
* @param {String} data.endDate - 结束日期
* @param {Array} data.venueIds - 场地ID数组
* @param {Array} data.projectIds - 项目ID数组
*/
export const autoArrangeSchedule = (data) => {
return request({
url: '/api/blade-martial/schedule-plan/auto-arrange',
method: 'post',
data
})
}
/**
* 调整赛程计划顺序
* @param {Object} data - 调整参数
* @param {Number} data.id - 赛程计划ID
* @param {Number} data.targetOrder - 目标顺序
*/
export const adjustScheduleOrder = (data) => {
return request({
url: '/api/blade-martial/schedule-plan/adjust-order',
method: 'post',
data
})
}
/**
* 获取某日期的赛程计划
* @param {Number} scheduleId - 赛程ID
* @param {String} planDate - 计划日期
*/
export const getSchedulePlansByDate = (scheduleId, planDate) => {
return request({
url: '/api/blade-martial/schedule-plan/list-by-date',
method: 'get',
params: { scheduleId, planDate }
})
}
/**
* 获取某场地的赛程计划
* @param {Number} venueId - 场地ID
* @param {String} planDate - 计划日期(可选)
*/
export const getSchedulePlansByVenue = (venueId, planDate) => {
return request({
url: '/api/blade-martial/schedule-plan/list-by-venue',
method: 'get',
params: { venueId, planDate }
})
}
/**
* 检查时间冲突
* @param {Object} data - 检查参数
* @param {Number} data.venueId - 场地ID
* @param {String} data.planDate - 计划日期
* @param {String} data.startTime - 开始时间
* @param {String} data.endTime - 结束时间
* @param {Number} data.excludeId - 排除的赛程计划ID编辑时使用
*/
export const checkTimeConflict = (data) => {
return request({
url: '/api/blade-martial/schedule-plan/check-conflict',
method: 'post',
data
})
}
/**
* 复制赛程计划到其他日期
* @param {Object} data - 复制参数
* @param {Number} data.sourcePlanId - 源赛程计划ID
* @param {String} data.targetDate - 目标日期
*/
export const copySchedulePlan = (data) => {
return request({
url: '/api/blade-martial/schedule-plan/copy',
method: 'post',
data
})
}
/**
* 导出赛程计划
* @param {Object} params - 导出参数
*/
export const exportSchedulePlans = (params) => {
return request({
url: '/api/blade-martial/schedule-plan/export',
method: 'get',
params,
responseType: 'blob'
})
}

View File

@@ -82,3 +82,94 @@ export const getProjectList = (competitionId, venueId) => {
params: { competitionId, venueId }
})
}
// ==================== 评分提交接口 ====================
/**
* 提交评分
* @param {Object} data - 评分数据
* @param {Number} data.competitionId - 赛事ID
* @param {Number} data.athleteId - 运动员ID
* @param {Number} data.projectId - 项目ID
* @param {Number} data.scheduleId - 赛程ID
* @param {Number} data.venueId - 场地ID
* @param {Number} data.judgeId - 裁判ID
* @param {String} data.judgeName - 裁判姓名
* @param {Number} data.score - 评分
* @param {Number} data.originalScore - 原始分
* @param {Array} data.deductionItems - 扣分项ID数组
* @param {String} data.note - 备注
*/
export const submitScore = (data) => {
return request({
url: '/api/blade-martial/score/submit',
method: 'post',
data
})
}
/**
* 删除评分
* @param {String} ids - 评分ID,多个用逗号分隔
*/
export const removeScore = (ids) => {
return request({
url: '/api/blade-martial/score/remove',
method: 'post',
params: { ids }
})
}
/**
* 获取异常评分列表
* @param {Object} params - 查询参数
* @param {Number} params.competitionId - 赛事ID
* @param {Number} params.projectId - 项目ID可选
*/
export const getAnomalies = (params) => {
return request({
url: '/api/blade-martial/score/anomalies',
method: 'get',
params
})
}
/**
* 验证评分
* @param {Object} data - 验证数据
* @param {Number} data.athleteId - 运动员ID
* @param {Number} data.projectId - 项目ID
* @param {Number} data.score - 评分
*/
export const validateScores = (data) => {
return request({
url: '/api/blade-martial/score/validate',
method: 'post',
data
})
}
/**
* 批量提交评分
* @param {Array} data - 评分数据数组
*/
export const batchSubmitScores = (data) => {
return request({
url: '/api/blade-martial/score/batch-submit',
method: 'post',
data
})
}
/**
* 获取裁判待评分列表
* @param {Number} judgeId - 裁判ID
* @param {Number} competitionId - 赛事ID
*/
export const getPendingScores = (judgeId, competitionId) => {
return request({
url: '/api/blade-martial/score/pending',
method: 'get',
params: { judgeId, competitionId }
})
}

View File

@@ -0,0 +1,186 @@
<template>
<el-select
v-model="selectedValue"
:placeholder="placeholder"
:multiple="multiple"
:disabled="disabled"
:filterable="filterable"
:clearable="clearable"
:loading="loading"
@change="handleChange"
class="competition-selector"
>
<el-option
v-for="item in filteredList"
:key="item.id"
:label="item.name"
:value="item.id"
:disabled="item.disabled"
>
<span class="option-label">{{ item.name }}</span>
<span class="option-status">
<el-tag :type="getStatusType(item.status)" size="small">
{{ getStatusText(item.status) }}
</el-tag>
</span>
</el-option>
</el-select>
</template>
<script>
import { getCompetitionList } from '@/api/martial/competition'
export default {
name: 'CompetitionSelector',
props: {
modelValue: {
type: [Number, Array],
default: null
},
multiple: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
},
placeholder: {
type: String,
default: '请选择赛事'
},
filterable: {
type: Boolean,
default: true
},
clearable: {
type: Boolean,
default: true
},
// 过滤状态: 0-未开始, 1-报名中, 2-进行中, 3-已结束
status: {
type: Array,
default: null
}
},
emits: ['update:modelValue', 'change'],
data() {
return {
loading: false,
competitionList: [],
selectedValue: this.modelValue
}
},
computed: {
filteredList() {
if (!this.status || this.status.length === 0) {
return this.competitionList
}
return this.competitionList.filter(item =>
this.status.includes(item.status)
)
}
},
watch: {
modelValue(newVal) {
this.selectedValue = newVal
}
},
mounted() {
this.loadCompetitions()
},
methods: {
async loadCompetitions() {
this.loading = true
try {
const res = await getCompetitionList({
current: 1,
size: 1000
})
if (res.data && res.data.records) {
this.competitionList = res.data.records.map(item => ({
id: item.id,
name: item.name,
status: item.status,
disabled: false
}))
}
} catch (error) {
console.error('加载赛事列表失败:', error)
this.$message.error('加载赛事列表失败')
} finally {
this.loading = false
}
},
handleChange(value) {
this.$emit('update:modelValue', value)
// 获取选中的选项
let selectedOptions = null
if (this.multiple) {
selectedOptions = this.competitionList.filter(item =>
value.includes(item.id)
)
} else {
selectedOptions = this.competitionList.find(item =>
item.id === value
)
}
this.$emit('change', value, selectedOptions)
},
getStatusType(status) {
const typeMap = {
0: 'info',
1: 'success',
2: 'warning',
3: 'info'
}
return typeMap[status] || 'info'
},
getStatusText(status) {
const textMap = {
0: '未开始',
1: '报名中',
2: '进行中',
3: '已结束'
}
return textMap[status] || '未知'
}
}
}
</script>
<style lang="scss" scoped>
.competition-selector {
width: 100%;
:deep(.el-select-dropdown__item) {
display: flex;
justify-content: space-between;
align-items: center;
}
.option-label {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.option-status {
margin-left: 10px;
flex-shrink: 0;
}
}
</style>

View File

@@ -0,0 +1,330 @@
<template>
<el-dialog
v-model="visible"
title="分配奖牌"
width="800px"
:close-on-click-modal="false"
@close="handleClose"
>
<!-- 赛事和项目信息 -->
<div class="medal-info">
<el-descriptions :column="2" border>
<el-descriptions-item label="赛事名称">{{ competitionName }}</el-descriptions-item>
<el-descriptions-item label="项目名称">{{ projectName }}</el-descriptions-item>
</el-descriptions>
</div>
<!-- 奖牌分配表单 -->
<div class="medal-form">
<!-- 金牌 -->
<div class="medal-section">
<div class="section-header">
<span class="medal-icon gold">🥇</span>
<span class="medal-title">金牌</span>
</div>
<el-select
v-model="form.goldMedal"
placeholder="请选择金牌获得者"
filterable
clearable
class="medal-select"
>
<el-option
v-for="item in athleteList"
:key="item.id"
:label="`${item.name} (${item.number}) - ${item.score}分`"
:value="item.id"
:disabled="isDisabled(item.id, 'gold')"
></el-option>
</el-select>
</div>
<!-- 银牌 -->
<div class="medal-section">
<div class="section-header">
<span class="medal-icon silver">🥈</span>
<span class="medal-title">银牌</span>
</div>
<el-select
v-model="form.silverMedal"
placeholder="请选择银牌获得者"
filterable
clearable
class="medal-select"
>
<el-option
v-for="item in athleteList"
:key="item.id"
:label="`${item.name} (${item.number}) - ${item.score}分`"
:value="item.id"
:disabled="isDisabled(item.id, 'silver')"
></el-option>
</el-select>
</div>
<!-- 铜牌 -->
<div class="medal-section">
<div class="section-header">
<span class="medal-icon bronze">🥉</span>
<span class="medal-title">铜牌</span>
</div>
<el-select
v-model="form.bronzeMedals"
placeholder="请选择铜牌获得者(可多选)"
filterable
clearable
multiple
class="medal-select"
>
<el-option
v-for="item in athleteList"
:key="item.id"
:label="`${item.name} (${item.number}) - ${item.score}分`"
:value="item.id"
:disabled="isDisabled(item.id, 'bronze')"
></el-option>
</el-select>
</div>
</div>
<!-- 自动分配建议 -->
<el-alert
v-if="autoSuggestion"
:title="autoSuggestion"
type="info"
:closable="false"
class="auto-suggestion"
>
<template #default>
<div>{{ autoSuggestion }}</div>
<el-button type="primary" size="small" @click="applyAutoSuggestion">
应用建议
</el-button>
</template>
</el-alert>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" :loading="submitting" @click="handleSubmit">
确定分配
</el-button>
</div>
</template>
</el-dialog>
</template>
<script>
import { allocateMedals } from '@/api/martial/result'
export default {
name: 'MedalDialog',
props: {
modelValue: {
type: Boolean,
default: false
},
competitionId: {
type: Number,
required: true
},
projectId: {
type: Number,
required: true
},
competitionName: {
type: String,
default: ''
},
projectName: {
type: String,
default: ''
},
athleteList: {
type: Array,
default: () => []
}
},
emits: ['update:modelValue', 'success'],
data() {
return {
visible: this.modelValue,
submitting: false,
form: {
goldMedal: null,
silverMedal: null,
bronzeMedals: []
}
}
},
computed: {
autoSuggestion() {
if (this.athleteList.length < 3) {
return null
}
// 按分数排序
const sorted = [...this.athleteList].sort((a, b) => b.score - a.score)
return `建议: 金牌-${sorted[0].name}(${sorted[0].score}分), 银牌-${sorted[1].name}(${sorted[1].score}分), 铜牌-${sorted[2].name}(${sorted[2].score}分)`
}
},
watch: {
modelValue(newVal) {
this.visible = newVal
if (newVal) {
this.resetForm()
}
},
visible(newVal) {
this.$emit('update:modelValue', newVal)
}
},
methods: {
isDisabled(athleteId, medalType) {
// 同一选手不能获得多个奖牌
if (medalType === 'gold') {
return this.form.silverMedal === athleteId ||
this.form.bronzeMedals.includes(athleteId)
}
if (medalType === 'silver') {
return this.form.goldMedal === athleteId ||
this.form.bronzeMedals.includes(athleteId)
}
if (medalType === 'bronze') {
return this.form.goldMedal === athleteId ||
this.form.silverMedal === athleteId
}
return false
},
applyAutoSuggestion() {
if (this.athleteList.length < 3) {
return
}
const sorted = [...this.athleteList].sort((a, b) => b.score - a.score)
this.form.goldMedal = sorted[0].id
this.form.silverMedal = sorted[1].id
this.form.bronzeMedals = [sorted[2].id]
this.$message.success('已应用自动分配建议')
},
resetForm() {
this.form = {
goldMedal: null,
silverMedal: null,
bronzeMedals: []
}
},
handleClose() {
this.visible = false
},
async handleSubmit() {
// 验证
if (!this.form.goldMedal && !this.form.silverMedal && this.form.bronzeMedals.length === 0) {
this.$message.warning('请至少分配一个奖牌')
return
}
this.submitting = true
try {
await allocateMedals({
competitionId: this.competitionId,
projectId: this.projectId,
goldMedal: this.form.goldMedal,
silverMedal: this.form.silverMedal,
bronzeMedals: this.form.bronzeMedals
})
this.$message.success('奖牌分配成功')
this.$emit('success')
this.handleClose()
} catch (error) {
console.error('奖牌分配失败:', error)
this.$message.error('奖牌分配失败')
} finally {
this.submitting = false
}
}
}
}
</script>
<style lang="scss" scoped>
.medal-info {
margin-bottom: 20px;
}
.medal-form {
display: flex;
flex-direction: column;
gap: 20px;
margin: 20px 0;
}
.medal-section {
display: flex;
flex-direction: column;
gap: 10px;
}
.section-header {
display: flex;
align-items: center;
gap: 10px;
}
.medal-icon {
font-size: 24px;
&.gold {
filter: drop-shadow(0 0 5px rgba(212, 175, 55, 0.5));
}
&.silver {
filter: drop-shadow(0 0 5px rgba(192, 192, 192, 0.5));
}
&.bronze {
filter: drop-shadow(0 0 5px rgba(205, 127, 50, 0.5));
}
}
.medal-title {
font-size: 16px;
font-weight: bold;
color: #303133;
}
.medal-select {
width: 100%;
}
.auto-suggestion {
margin-top: 20px;
:deep(.el-alert__content) {
display: flex;
justify-content: space-between;
align-items: center;
}
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
</style>

View File

@@ -0,0 +1,186 @@
<template>
<el-select
v-model="selectedValue"
:placeholder="placeholder"
:multiple="multiple"
:disabled="disabled"
:filterable="filterable"
:clearable="clearable"
:loading="loading"
@change="handleChange"
class="project-selector"
>
<el-option
v-for="item in filteredList"
:key="item.id"
:label="item.name"
:value="item.id"
:disabled="item.disabled"
>
<span class="option-label">{{ item.name }}</span>
<span class="option-type" v-if="item.type">
<el-tag size="small">{{ item.type }}</el-tag>
</span>
</el-option>
</el-select>
</template>
<script>
import { getProjectList } from '@/api/martial/project'
export default {
name: 'ProjectSelector',
props: {
modelValue: {
type: [Number, Array],
default: null
},
multiple: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
},
placeholder: {
type: String,
default: '请选择项目'
},
filterable: {
type: Boolean,
default: true
},
clearable: {
type: Boolean,
default: true
},
// 赛事ID,用于过滤项目
competitionId: {
type: Number,
default: null
},
// 项目类型过滤
projectType: {
type: String,
default: null
}
},
emits: ['update:modelValue', 'change'],
data() {
return {
loading: false,
projectList: [],
selectedValue: this.modelValue
}
},
computed: {
filteredList() {
let list = this.projectList
if (this.projectType) {
list = list.filter(item => item.type === this.projectType)
}
return list
}
},
watch: {
modelValue(newVal) {
this.selectedValue = newVal
},
competitionId(newVal) {
if (newVal) {
this.loadProjects()
} else {
this.projectList = []
}
}
},
mounted() {
if (this.competitionId) {
this.loadProjects()
}
},
methods: {
async loadProjects() {
if (!this.competitionId) {
this.projectList = []
return
}
this.loading = true
try {
const res = await getProjectList({
competitionId: this.competitionId,
current: 1,
size: 1000
})
if (res.data && res.data.records) {
this.projectList = res.data.records.map(item => ({
id: item.id,
name: item.name,
type: item.type,
disabled: false
}))
}
} catch (error) {
console.error('加载项目列表失败:', error)
this.$message.error('加载项目列表失败')
} finally {
this.loading = false
}
},
handleChange(value) {
this.$emit('update:modelValue', value)
// 获取选中的选项
let selectedOptions = null
if (this.multiple) {
selectedOptions = this.projectList.filter(item =>
value.includes(item.id)
)
} else {
selectedOptions = this.projectList.find(item =>
item.id === value
)
}
this.$emit('change', value, selectedOptions)
}
}
}
</script>
<style lang="scss" scoped>
.project-selector {
width: 100%;
:deep(.el-select-dropdown__item) {
display: flex;
justify-content: space-between;
align-items: center;
}
.option-label {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.option-type {
margin-left: 10px;
flex-shrink: 0;
}
}
</style>

View File

@@ -0,0 +1,268 @@
<template>
<div class="score-input">
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
<!-- 基础分 -->
<el-form-item label="基础分" prop="baseScore">
<el-input-number
v-model="form.baseScore"
:min="0"
:max="10"
:step="0.1"
:precision="2"
controls-position="right"
@change="handleCalculate"
></el-input-number>
</el-form-item>
<!-- 扣分项 -->
<el-form-item label="扣分项">
<el-tag
v-for="(deduction, index) in form.deductions"
:key="index"
closable
@close="handleRemoveDeduction(index)"
class="deduction-tag"
>
{{ deduction.name }} (-{{ deduction.score }})
</el-tag>
<el-button
size="small"
type="primary"
plain
@click="showDeductionSelector"
>
添加扣分项
</el-button>
</el-form-item>
<!-- 总扣分 -->
<el-form-item label="总扣分">
<el-tag type="danger">-{{ totalDeduction }}</el-tag>
</el-form-item>
<!-- 最终得分 -->
<el-form-item label="最终得分">
<el-tag :type="getScoreType(finalScore)" size="large" class="final-score-tag">
{{ finalScore }}
</el-tag>
</el-form-item>
<!-- 备注 -->
<el-form-item label="备注" prop="remark">
<el-input
v-model="form.remark"
type="textarea"
:rows="3"
placeholder="请输入备注信息"
></el-input>
</el-form-item>
</el-form>
<!-- 扣分项选择器弹窗 -->
<el-dialog
v-model="deductionDialogVisible"
title="选择扣分项"
width="600px"
>
<el-checkbox-group v-model="selectedDeductions">
<div class="deduction-list">
<el-checkbox
v-for="item in deductionList"
:key="item.id"
:label="item.id"
class="deduction-item"
>
<span class="deduction-name">{{ item.name }}</span>
<span class="deduction-score">-{{ item.score }}</span>
</el-checkbox>
</div>
</el-checkbox-group>
<template #footer>
<el-button @click="deductionDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleConfirmDeductions">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script>
import { getDeductionList } from '@/api/martial/deduction'
export default {
name: 'ScoreInput',
props: {
modelValue: {
type: Object,
default: () => ({
baseScore: 10,
deductions: [],
remark: ''
})
},
projectId: {
type: Number,
required: true
}
},
emits: ['update:modelValue', 'change'],
data() {
return {
form: {
baseScore: 10,
deductions: [],
remark: ''
},
rules: {
baseScore: [
{ required: true, message: '请输入基础分', trigger: 'blur' }
]
},
deductionDialogVisible: false,
deductionList: [],
selectedDeductions: []
}
},
computed: {
totalDeduction() {
return this.form.deductions.reduce((sum, item) => sum + item.score, 0)
},
finalScore() {
const score = this.form.baseScore - this.totalDeduction
return Math.max(0, parseFloat(score.toFixed(2)))
}
},
watch: {
modelValue: {
handler(newVal) {
if (newVal) {
this.form = { ...newVal }
}
},
immediate: true,
deep: true
},
projectId: {
handler(newVal) {
if (newVal) {
this.loadDeductions()
}
},
immediate: true
}
},
methods: {
async loadDeductions() {
try {
const res = await getDeductionList({
projectId: this.projectId
})
if (res.data && res.data.records) {
this.deductionList = res.data.records
}
} catch (error) {
console.error('加载扣分项失败:', error)
}
},
handleCalculate() {
this.emitChange()
},
showDeductionSelector() {
this.selectedDeductions = this.form.deductions.map(item => item.id)
this.deductionDialogVisible = true
},
handleConfirmDeductions() {
this.form.deductions = this.deductionList.filter(item =>
this.selectedDeductions.includes(item.id)
).map(item => ({
id: item.id,
name: item.name,
score: item.score
}))
this.deductionDialogVisible = false
this.emitChange()
},
handleRemoveDeduction(index) {
this.form.deductions.splice(index, 1)
this.emitChange()
},
emitChange() {
const value = {
...this.form,
totalDeduction: this.totalDeduction,
finalScore: this.finalScore
}
this.$emit('update:modelValue', value)
this.$emit('change', value)
},
getScoreType(score) {
if (score >= 9) return 'success'
if (score >= 8) return 'warning'
if (score >= 7) return 'info'
return 'danger'
},
async validate() {
return await this.$refs.formRef.validate()
}
}
}
</script>
<style lang="scss" scoped>
.score-input {
.deduction-tag {
margin-right: 8px;
margin-bottom: 8px;
}
.final-score-tag {
font-size: 24px;
font-weight: bold;
padding: 10px 20px;
}
.deduction-list {
display: flex;
flex-direction: column;
gap: 10px;
max-height: 400px;
overflow-y: auto;
}
.deduction-item {
display: flex;
justify-content: space-between;
padding: 10px;
border: 1px solid #dcdfe6;
border-radius: 4px;
&:hover {
background-color: #f5f7fa;
}
}
.deduction-name {
flex: 1;
}
.deduction-score {
color: #f56c6c;
font-weight: bold;
}
}
</style>

View File

@@ -53,23 +53,13 @@ export default [
meta: {
keepAlive: false,
},
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/competition/list.vue'),
},
{
path: 'competition/create',
name: '赛事详情',
meta: {
keepAlive: false,
menu: false,
},
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/competition/create.vue'),
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/competition/index.vue'),
},
{
path: 'registration/detail',
name: '报名详情',
meta: {
keepAlive: false,
menu: false,
},
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/registration/index.vue'),
},
@@ -127,16 +117,106 @@ export default [
meta: {
keepAlive: false,
},
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/participant/list.vue'),
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/participant/index.vue'),
},
// 新增页面 - P0核心页面
{
path: 'project/list',
name: '项目管理',
meta: {
keepAlive: true,
},
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/project/index.vue'),
},
{
path: 'participant/manage',
name: '选手详情',
path: 'deduction/list',
name: '扣分项管理',
meta: {
keepAlive: false,
menu: false,
keepAlive: true,
},
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/participant/manage.vue'),
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/deduction/index.vue'),
},
{
path: 'result/list',
name: '成绩管理',
meta: {
keepAlive: true,
},
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/result/index.vue'),
},
{
path: 'export/index',
name: '导出中心',
meta: {
keepAlive: true,
},
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/export/index.vue'),
},
// P1重要页面
{
path: 'schedulePlan/list',
name: '赛程计划',
meta: {
keepAlive: true,
},
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/schedulePlan/index.vue'),
},
{
path: 'scheduleAthlete/list',
name: '选手关联',
meta: {
keepAlive: true,
},
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/scheduleAthlete/index.vue'),
},
{
path: 'judgeInvite/list',
name: '裁判邀请',
meta: {
keepAlive: true,
},
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/judgeInvite/index.vue'),
},
{
path: 'judgeProject/list',
name: '裁判分配',
meta: {
keepAlive: true,
},
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/judgeProject/index.vue'),
},
// P2增强页面
{
path: 'live/list',
name: '直播管理',
meta: {
keepAlive: true,
},
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/live/index.vue'),
},
{
path: 'info/list',
name: '信息发布',
meta: {
keepAlive: true,
},
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/info/index.vue'),
},
{
path: 'exception/list',
name: '异常事件',
meta: {
keepAlive: true,
},
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/exception/index.vue'),
},
{
path: 'activity/list',
name: '活动日程',
meta: {
keepAlive: true,
},
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/activity/index.vue'),
},
],
},

File diff suppressed because it is too large Load Diff

View File

@@ -59,7 +59,7 @@
</el-table-column>
<el-table-column prop="linkUrl" label="跳转链接" min-width="180" show-overflow-tooltip></el-table-column>
<el-table-column prop="sortOrder" label="排序" width="80" align="center"></el-table-column>
<el-table-column prop="status" label="状态" width="100" align="center">
<el-table-column prop="status" label="状态" width="150" align="center">
<template #default="scope">
<el-switch
v-model="scope.row.status"

File diff suppressed because it is too large Load Diff

View File

@@ -105,6 +105,309 @@
</template>
</el-table-column>
</el-table>
<!-- 新增/编辑/查看弹窗 -->
<el-dialog
:title="dialogTitle"
:visible.sync="dialogVisible"
width="900px"
:close-on-click-modal="false"
@close="handleDialogClose"
top="5vh"
>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="120px"
:disabled="dialogMode === 'view'"
class="competition-form"
>
<!-- 基本信息 -->
<div class="form-section">
<div class="section-title">
<i class="el-icon-document"></i>
基本信息
</div>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="赛事名称" prop="competitionName">
<el-input
v-model="formData.competitionName"
placeholder="请输入赛事名称"
maxlength="100"
show-word-limit
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="主办单位" prop="organizer">
<el-input
v-model="formData.organizer"
placeholder="请输入主办单位"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="举办地点" prop="location">
<el-input
v-model="formData.location"
placeholder="请输入举办地点"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="比赛场馆" prop="venue">
<el-input
v-model="formData.venue"
placeholder="请输入比赛场馆"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="报名时间" prop="registrationTime">
<el-input
v-model="formData.registrationTime"
placeholder="例如2025-01-01 至 2025-02-28"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="比赛时间" prop="competitionTime">
<el-input
v-model="formData.competitionTime"
placeholder="例如2025-03-15 至 2025-03-20"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="赛事状态" prop="status">
<el-select v-model="formData.status" placeholder="请选择状态" style="width: 100%">
<el-option label="未开始" :value="1" />
<el-option label="报名中" :value="2" />
<el-option label="进行中" :value="3" />
<el-option label="已结束" :value="4" />
</el-select>
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 信息发布 -->
<div class="form-section">
<div class="section-title">
<i class="el-icon-reading"></i>
信息发布
</div>
<el-form-item label="赛事简介" prop="introduction">
<el-input
v-model="formData.introduction"
type="textarea"
:rows="4"
placeholder="请输入赛事简介"
maxlength="500"
show-word-limit
/>
</el-form-item>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="联系人" prop="contactPerson">
<el-input
v-model="formData.contactPerson"
placeholder="请输入联系人姓名"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="联系电话" prop="contactPhone">
<el-input
v-model="formData.contactPhone"
placeholder="请输入联系电话"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="联系邮箱" prop="contactEmail">
<el-input
v-model="formData.contactEmail"
placeholder="请输入联系邮箱"
/>
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 赛事规程 -->
<div class="form-section">
<div class="section-title">
<i class="el-icon-document-checked"></i>
赛事规程
</div>
<el-form-item label="比赛规则" prop="rules">
<el-input
v-model="formData.rules"
type="textarea"
:rows="4"
placeholder="请输入比赛规则"
/>
</el-form-item>
<el-form-item label="参赛要求" prop="requirements">
<el-input
v-model="formData.requirements"
type="textarea"
:rows="4"
placeholder="请输入参赛要求"
/>
</el-form-item>
<el-form-item label="奖项设置" prop="awards">
<el-input
v-model="formData.awards"
type="textarea"
:rows="4"
placeholder="请输入奖项设置"
/>
</el-form-item>
</div>
<!-- 活动日程 -->
<div class="form-section">
<div class="section-title">
<i class="el-icon-date"></i>
活动日程
</div>
<el-form-item label="">
<el-button
v-if="dialogMode !== 'view'"
type="primary"
icon="el-icon-plus"
size="small"
@click="handleAddSchedule"
>
添加日程
</el-button>
</el-form-item>
<el-table
:data="formData.schedule"
border
style="width: 100%"
>
<el-table-column
label="日期"
width="150"
>
<template #default="scope">
<el-input
v-if="dialogMode !== 'view'"
v-model="scope.row.date"
placeholder="2025-03-15"
size="small"
/>
<span v-else>{{ scope.row.date }}</span>
</template>
</el-table-column>
<el-table-column
label="时间"
width="150"
>
<template #default="scope">
<el-input
v-if="dialogMode !== 'view'"
v-model="scope.row.time"
placeholder="09:00-12:00"
size="small"
/>
<span v-else>{{ scope.row.time }}</span>
</template>
</el-table-column>
<el-table-column
label="赛事项目"
min-width="200"
>
<template #default="scope">
<el-input
v-if="dialogMode !== 'view'"
v-model="scope.row.event"
placeholder="请输入赛事项目"
size="small"
/>
<span v-else>{{ scope.row.event }}</span>
</template>
</el-table-column>
<el-table-column
label="比赛场馆"
width="150"
>
<template #default="scope">
<el-input
v-if="dialogMode !== 'view'"
v-model="scope.row.venue"
placeholder="请输入场馆"
size="small"
/>
<span v-else>{{ scope.row.venue }}</span>
</template>
</el-table-column>
<el-table-column
v-if="dialogMode !== 'view'"
label="操作"
width="80"
align="center"
>
<template #default="scope">
<el-button
type="danger"
link
size="small"
icon="el-icon-delete"
@click="handleDeleteSchedule(scope.$index)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button
v-if="dialogMode !== 'view'"
type="primary"
:loading="submitLoading"
@click="handleSubmit"
>
确定
</el-button>
<el-button
v-else
type="primary"
@click="switchToEdit"
>
编辑
</el-button>
</div>
</el-dialog>
</div>
</template>
@@ -184,9 +487,51 @@ export default {
{ date: '2025-05-21', time: '09:00-18:00', event: '初赛', venue: '比赛馆' },
]
}
]
],
dialogVisible: false,
dialogMode: 'create', // create, edit, view
submitLoading: false,
currentCompetitionId: null,
formData: {
competitionName: '',
organizer: '',
location: '',
venue: '',
registrationTime: '',
competitionTime: '',
status: 1,
introduction: '',
contactPerson: '',
contactPhone: '',
contactEmail: '',
rules: '',
requirements: '',
awards: '',
schedule: []
},
formRules: {
competitionName: [
{ required: true, message: '请输入赛事名称', trigger: 'blur' }
],
organizer: [
{ required: true, message: '请输入主办单位', trigger: 'blur' }
],
location: [
{ required: true, message: '请输入举办地点', trigger: 'blur' }
]
}
};
},
computed: {
dialogTitle() {
const titleMap = {
create: '新建赛事',
edit: '编辑赛事',
view: '查看赛事'
};
return titleMap[this.dialogMode] || '赛事信息';
}
},
mounted() {
this.loadCompetitionList();
},
@@ -231,24 +576,28 @@ export default {
},
handleCreate() {
this.$router.push({
path: '/martial/competition/create',
query: { mode: 'create' }
});
this.dialogMode = 'create';
this.currentCompetitionId = null;
this.resetFormData();
this.dialogVisible = true;
},
handleView(row) {
this.$router.push({
path: '/martial/competition/create',
query: { mode: 'view', id: row.id }
});
this.dialogMode = 'view';
this.currentCompetitionId = row.id;
this.formData = { ...row };
this.dialogVisible = true;
},
handleEdit(row) {
this.$router.push({
path: '/martial/competition/create',
query: { mode: 'edit', id: row.id }
});
this.dialogMode = 'edit';
this.currentCompetitionId = row.id;
this.formData = { ...row };
this.dialogVisible = true;
},
switchToEdit() {
this.dialogMode = 'edit';
},
handleDelete(row) {
@@ -264,6 +613,85 @@ export default {
this.$message.success('删除成功');
}
}).catch(() => {});
},
handleAddSchedule() {
this.formData.schedule.push({
date: '',
time: '',
event: '',
venue: ''
});
},
handleDeleteSchedule(index) {
this.formData.schedule.splice(index, 1);
},
handleSubmit() {
this.$refs.formRef.validate((valid) => {
if (valid) {
this.submitLoading = true;
setTimeout(() => {
if (this.dialogMode === 'create') {
// 新建
const newId = this.competitionList.length > 0
? Math.max(...this.competitionList.map(item => item.id)) + 1
: 1;
const newCompetition = {
...this.formData,
id: newId,
posterImages: [],
regulationFiles: []
};
this.competitionList.push(newCompetition);
this.$message.success('创建成功');
} else if (this.dialogMode === 'edit') {
// 编辑
const index = this.competitionList.findIndex(item => item.id === this.currentCompetitionId);
if (index !== -1) {
this.competitionList[index] = {
...this.competitionList[index],
...this.formData
};
this.$message.success('保存成功');
}
}
this.saveCompetitionList();
this.dialogVisible = false;
this.submitLoading = false;
}, 500);
} else {
this.$message.error('请完善必填信息');
}
});
},
handleDialogClose() {
this.$refs.formRef.resetFields();
this.resetFormData();
},
resetFormData() {
this.formData = {
competitionName: '',
organizer: '',
location: '',
venue: '',
registrationTime: '',
competitionTime: '',
status: 1,
introduction: '',
contactPerson: '',
contactPhone: '',
contactEmail: '',
rules: '',
requirements: '',
awards: '',
schedule: []
};
}
}
};
@@ -306,4 +734,49 @@ export default {
padding: 0 8px;
}
}
.competition-form {
max-height: 60vh;
overflow-y: auto;
}
.form-section {
margin-bottom: 30px;
padding: 20px;
background: #fafafa;
border-radius: 8px;
&:last-child {
margin-bottom: 0;
}
}
.section-title {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 20px;
font-size: 16px;
font-weight: 600;
color: #333;
i {
color: #dc2626;
font-size: 18px;
}
}
:deep(.el-form-item) {
margin-bottom: 18px;
}
:deep(.el-table) {
.el-input {
width: 100%;
}
}
.dialog-footer {
text-align: right;
}
</style>

View File

@@ -0,0 +1,698 @@
<template>
<div class="deduction-container">
<!-- 搜索区域 -->
<el-card shadow="never" class="search-card">
<el-form :inline="true" :model="queryParams" class="search-form">
<el-form-item label="所属项目">
<el-select
v-model="queryParams.projectId"
placeholder="请选择项目"
clearable
filterable
style="width: 250px"
>
<el-option
v-for="item in projectList"
:key="item.id"
:label="item.projectName"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="扣分项名称">
<el-input
v-model="queryParams.itemName"
placeholder="请输入扣分项名称"
clearable
style="width: 200px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" :icon="Search" @click="handleSearch">
查询
</el-button>
<el-button :icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 工具栏 -->
<el-card shadow="never" class="toolbar-card">
<div class="toolbar">
<div class="toolbar-left">
<el-button type="primary" :icon="Plus" @click="handleAdd">
新增扣分项
</el-button>
<el-button
type="danger"
:icon="Delete"
:disabled="!selection.length"
@click="handleBatchDelete"
>
批量删除
</el-button>
<el-button type="success" :icon="DocumentCopy" @click="handleClone">
克隆扣分项
</el-button>
<el-button type="warning" :icon="Download" @click="handleExport">
导出模板
</el-button>
</div>
<div class="toolbar-right">
<el-tooltip content="拖拽行可调整顺序" placement="top">
<el-icon :size="20" color="#409EFF">
<InfoFilled />
</el-icon>
</el-tooltip>
<el-tooltip content="刷新" placement="top">
<el-button circle :icon="Refresh" @click="fetchData" />
</el-tooltip>
</div>
</div>
</el-card>
<!-- 数据表格 -->
<el-card shadow="never" class="table-card">
<el-table
v-loading="loading"
:data="tableData"
stripe
border
row-key="id"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="排序" width="80" align="center">
<template #default="{ row }">
<el-icon class="drag-handle" :size="20">
<Rank />
</el-icon>
<span style="margin-left: 5px">{{ row.sortOrder }}</span>
</template>
</el-table-column>
<el-table-column
prop="itemName"
label="扣分项名称"
min-width="200"
show-overflow-tooltip
/>
<el-table-column
prop="projectName"
label="所属项目"
min-width="180"
show-overflow-tooltip
/>
<el-table-column
prop="deductionPoints"
label="扣分值(分)"
width="120"
align="center"
>
<template #default="{ row }">
<el-tag type="danger" effect="dark">
-{{ row.deductionPoints }}
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="description"
label="描述说明"
min-width="250"
show-overflow-tooltip
/>
<el-table-column
prop="createTime"
label="创建时间"
width="160"
align="center"
>
<template #default="{ row }">
{{ formatDate(row.createTime) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" :icon="Edit" @click="handleEdit(row)">
编辑
</el-button>
<el-button link type="success" :icon="DocumentCopy" @click="handleCloneSingle(row)">
克隆
</el-button>
<el-button link type="danger" :icon="Delete" @click="handleDelete(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="queryParams.current"
v-model:page-size="queryParams.size"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="fetchData"
@current-change="fetchData"
/>
</div>
</el-card>
<!-- 新增/编辑弹窗 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="600px"
:close-on-click-modal="false"
@close="handleDialogClose"
>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="120px"
>
<el-form-item label="所属项目" prop="projectId">
<el-select
v-model="form.projectId"
placeholder="请选择所属项目"
filterable
style="width: 100%"
>
<el-option
v-for="item in projectList"
:key="item.id"
:label="item.projectName"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="扣分项名称" prop="itemName">
<el-input
v-model="form.itemName"
placeholder="请输入扣分项名称"
maxlength="100"
/>
</el-form-item>
<el-form-item label="扣分值(分)" prop="deductionPoints">
<el-input-number
v-model="form.deductionPoints"
:min="0.1"
:max="10"
:precision="1"
:step="0.1"
style="width: 100%"
/>
<div class="form-tip">扣分范围0.1 - 10.0 </div>
</el-form-item>
<el-form-item label="排序序号" prop="sortOrder">
<el-input-number
v-model="form.sortOrder"
:min="0"
:max="999"
style="width: 100%"
/>
<div class="form-tip">数字越小越靠前</div>
</el-form-item>
<el-form-item label="描述说明" prop="description">
<el-input
v-model="form.description"
type="textarea"
:rows="4"
placeholder="请输入描述说明"
maxlength="500"
show-word-limit
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">
确定
</el-button>
</template>
</el-dialog>
<!-- 克隆扣分项弹窗 -->
<el-dialog
v-model="cloneDialogVisible"
title="克隆扣分项"
width="500px"
:close-on-click-modal="false"
>
<el-form
ref="cloneFormRef"
:model="cloneForm"
:rules="cloneRules"
label-width="120px"
>
<el-form-item label="源项目">
<el-input :value="cloneForm.sourceProjectName" disabled />
</el-form-item>
<el-form-item label="目标项目" prop="targetProjectId">
<el-select
v-model="cloneForm.targetProjectId"
placeholder="请选择目标项目"
filterable
style="width: 100%"
>
<el-option
v-for="item in projectList.filter(p => p.id !== cloneForm.sourceProjectId)"
:key="item.id"
:label="item.projectName"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-alert
type="info"
:closable="false"
show-icon
>
<template #title>
将复制 <strong>{{ cloneForm.itemCount }}</strong> 个扣分项到目标项目
</template>
</el-alert>
</el-form>
<template #footer>
<el-button @click="cloneDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="cloneLoading" @click="handleCloneSubmit">
确定克隆
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, nextTick } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
Search,
Refresh,
Plus,
Delete,
Edit,
Download,
DocumentCopy,
InfoFilled,
Rank
} from '@element-plus/icons-vue'
import {
getDeductionList,
addDeduction,
updateDeduction,
removeDeduction,
cloneDeductions,
updateDeductionOrder
} from '@/api/martial/deduction'
import { getProjectList } from '@/api/martial/project'
import dayjs from 'dayjs'
import Sortable from 'sortablejs'
// 数据定义
const loading = ref(false)
const submitLoading = ref(false)
const cloneLoading = ref(false)
const tableData = ref([])
const total = ref(0)
const selection = ref([])
const projectList = ref([])
const dialogVisible = ref(false)
const cloneDialogVisible = ref(false)
const dialogTitle = ref('')
const formRef = ref(null)
const cloneFormRef = ref(null)
// 查询参数
const queryParams = reactive({
current: 1,
size: 10,
projectId: '',
itemName: ''
})
// 表单数据
const form = reactive({
id: null,
projectId: '',
itemName: '',
deductionPoints: 0.5,
sortOrder: 0,
description: ''
})
// 克隆表单
const cloneForm = reactive({
sourceProjectId: '',
sourceProjectName: '',
targetProjectId: '',
itemCount: 0
})
// 表单验证规则
const rules = {
projectId: [
{ required: true, message: '请选择所属项目', trigger: 'change' }
],
itemName: [
{ required: true, message: '请输入扣分项名称', trigger: 'blur' },
{ min: 2, max: 100, message: '长度在 2 到 100 个字符', trigger: 'blur' }
],
deductionPoints: [
{ required: true, message: '请输入扣分值', trigger: 'blur' }
],
sortOrder: [
{ required: true, message: '请输入排序序号', trigger: 'blur' }
]
}
// 克隆表单验证规则
const cloneRules = {
targetProjectId: [
{ required: true, message: '请选择目标项目', trigger: 'change' }
]
}
// 加载项目列表
const loadProjectList = async () => {
try {
const res = await getProjectList(1, 1000, {})
if (res.data && res.data.records) {
projectList.value = res.data.records
}
} catch (error) {
console.error('加载项目列表失败:', error)
}
}
// 查询数据
const fetchData = async () => {
loading.value = true
try {
const res = await getDeductionList(
queryParams.current,
queryParams.size,
queryParams
)
if (res.data) {
tableData.value = res.data.records || []
total.value = res.data.total || 0
}
} catch (error) {
ElMessage.error('获取数据失败')
console.error(error)
} finally {
loading.value = false
}
}
// 搜索
const handleSearch = () => {
queryParams.current = 1
fetchData()
}
// 重置
const handleReset = () => {
Object.assign(queryParams, {
current: 1,
size: 10,
projectId: '',
itemName: ''
})
fetchData()
}
// 新增
const handleAdd = () => {
dialogTitle.value = '新增扣分项'
resetForm()
dialogVisible.value = true
}
// 编辑
const handleEdit = (row) => {
dialogTitle.value = '编辑扣分项'
Object.keys(form).forEach((key) => {
form[key] = row[key]
})
dialogVisible.value = true
}
// 删除
const handleDelete = (row) => {
ElMessageBox.confirm('确定要删除该扣分项吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
try {
await removeDeduction(row.id)
ElMessage.success('删除成功')
fetchData()
} catch (error) {
ElMessage.error('删除失败')
}
})
.catch(() => {})
}
// 批量删除
const handleBatchDelete = () => {
ElMessageBox.confirm(`确定要删除选中的 ${selection.value.length} 个扣分项吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
try {
const ids = selection.value.map((item) => item.id).join(',')
await removeDeduction(ids)
ElMessage.success('删除成功')
fetchData()
} catch (error) {
ElMessage.error('删除失败')
}
})
.catch(() => {})
}
// 克隆单个扣分项
const handleCloneSingle = (row) => {
cloneForm.sourceProjectId = row.projectId
cloneForm.sourceProjectName = row.projectName
cloneForm.targetProjectId = ''
cloneForm.itemCount = 1
cloneDialogVisible.value = true
}
// 克隆扣分项(批量)
const handleClone = () => {
if (!queryParams.projectId) {
ElMessage.warning('请先选择源项目(通过搜索筛选)')
return
}
const sourceProject = projectList.value.find(p => p.id === queryParams.projectId)
cloneForm.sourceProjectId = queryParams.projectId
cloneForm.sourceProjectName = sourceProject?.projectName || ''
cloneForm.targetProjectId = ''
cloneForm.itemCount = total.value
cloneDialogVisible.value = true
}
// 提交克隆
const handleCloneSubmit = async () => {
if (!cloneFormRef.value) return
await cloneFormRef.value.validate(async (valid) => {
if (valid) {
cloneLoading.value = true
try {
await cloneDeductions({
sourceProjectId: cloneForm.sourceProjectId,
targetProjectId: cloneForm.targetProjectId
})
ElMessage.success('克隆成功')
cloneDialogVisible.value = false
fetchData()
} catch (error) {
ElMessage.error('克隆失败')
} finally {
cloneLoading.value = false
}
}
})
}
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
submitLoading.value = true
try {
if (form.id) {
await updateDeduction(form)
ElMessage.success('修改成功')
} else {
await addDeduction(form)
ElMessage.success('新增成功')
}
dialogVisible.value = false
fetchData()
} catch (error) {
ElMessage.error(form.id ? '修改失败' : '新增失败')
} finally {
submitLoading.value = false
}
}
})
}
// 选择变化
const handleSelectionChange = (val) => {
selection.value = val
}
// 关闭弹窗
const handleDialogClose = () => {
resetForm()
}
// 重置表单
const resetForm = () => {
Object.assign(form, {
id: null,
projectId: '',
itemName: '',
deductionPoints: 0.5,
sortOrder: 0,
description: ''
})
if (formRef.value) {
formRef.value.clearValidate()
}
}
// 导出模板
const handleExport = async () => {
try {
const csvContent = [
['项目ID', '扣分项名称', '扣分值', '描述说明', '排序序号'],
['1', '示例扣分项', '0.5', '这是一个示例', '1']
].map(row => row.join(',')).join('\n')
const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/csv;charset=utf-8;' })
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = `扣分项导入模板_${dayjs().format('YYYYMMDDHHmmss')}.csv`
link.click()
window.URL.revokeObjectURL(link.href)
ElMessage.success('导出成功')
} catch (error) {
ElMessage.error('导出失败')
}
}
// 初始化拖拽排序
const initSortable = () => {
nextTick(() => {
const tbody = document.querySelector('.el-table__body-wrapper tbody')
if (!tbody) return
Sortable.create(tbody, {
handle: '.drag-handle',
animation: 150,
onEnd: async ({ oldIndex, newIndex }) => {
if (oldIndex === newIndex) return
const movedItem = tableData.value.splice(oldIndex, 1)[0]
tableData.value.splice(newIndex, 0, movedItem)
// 更新排序
try {
const sortData = tableData.value.map((item, index) => ({
id: item.id,
sortOrder: index
}))
await updateDeductionOrder(sortData)
ElMessage.success('排序已更新')
} catch (error) {
ElMessage.error('排序更新失败')
fetchData() // 重新加载数据
}
}
})
})
}
// 格式化日期
const formatDate = (date) => {
if (!date) return '-'
return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
}
// 生命周期
onMounted(() => {
loadProjectList()
fetchData()
initSortable()
})
</script>
<style scoped lang="scss">
.deduction-container {
padding: 20px;
.search-card,
.toolbar-card,
.table-card {
margin-bottom: 20px;
}
.search-form {
.el-form-item {
margin-bottom: 0;
}
}
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
.toolbar-left {
display: flex;
gap: 10px;
}
.toolbar-right {
display: flex;
gap: 10px;
align-items: center;
}
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
.drag-handle {
cursor: move;
vertical-align: middle;
}
.form-tip {
font-size: 12px;
color: #909399;
margin-top: 5px;
}
}
</style>

View File

@@ -154,7 +154,7 @@ export default {
},
methods: {
goBack() {
this.$router.push('/martial/order/list')
this.$router.go(-1)
},
setViewMode(index, mode) {
this.dispatchGroups[index].viewMode = mode

Binary file not shown.

View File

@@ -0,0 +1,698 @@
<template>
<div class="export-container">
<el-card shadow="never">
<el-tabs v-model="activeTab" type="border-card">
<!-- Tab 1: 成绩导出 -->
<el-tab-pane label="成绩导出" name="result">
<div class="tab-content">
<el-form :model="resultForm" label-width="120px" style="max-width: 800px">
<el-form-item label="选择赛事" required>
<el-select
v-model="resultForm.competitionId"
placeholder="请选择赛事"
clearable
filterable
style="width: 100%"
@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-item label="选择项目">
<el-select
v-model="resultForm.projectId"
placeholder="请选择项目(不选则导出全部)"
clearable
filterable
style="width: 100%"
>
<el-option
v-for="item in projectList"
:key="item.id"
:label="item.projectName"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="分组类别">
<el-select
v-model="resultForm.category"
placeholder="请选择分组类别(可选)"
clearable
style="width: 100%"
>
<el-option label="男子" value="1" />
<el-option label="女子" value="2" />
<el-option label="团体" value="3" />
<el-option label="混合" value="4" />
</el-select>
</el-form-item>
<el-form-item label="导出内容">
<el-checkbox-group v-model="resultForm.exportFields">
<el-checkbox label="ranking">排名</el-checkbox>
<el-checkbox label="athleteName">选手姓名</el-checkbox>
<el-checkbox label="teamName">团队名称</el-checkbox>
<el-checkbox label="projectName">项目</el-checkbox>
<el-checkbox label="totalScore">总分</el-checkbox>
<el-checkbox label="finalScore">最终成绩</el-checkbox>
<el-checkbox label="medal">奖牌</el-checkbox>
<el-checkbox label="judgeScores">裁判评分明细</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="导出格式">
<el-radio-group v-model="resultForm.format">
<el-radio label="xlsx">Excel (xlsx)</el-radio>
<el-radio label="pdf">PDF</el-radio>
<el-radio label="csv">CSV</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button
type="primary"
:icon="Download"
:loading="resultLoading"
@click="handleExportResult"
>
导出成绩单
</el-button>
</el-form-item>
</el-form>
</div>
</el-tab-pane>
<!-- Tab 2: 名单导出 -->
<el-tab-pane label="名单导出" name="list">
<div class="tab-content">
<el-form :model="listForm" label-width="120px" style="max-width: 800px">
<el-form-item label="选择赛事" required>
<el-select
v-model="listForm.competitionId"
placeholder="请选择赛事"
clearable
filterable
style="width: 100%"
>
<el-option
v-for="item in competitionList"
:key="item.id"
:label="item.competitionName"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="名单类型" required>
<el-radio-group v-model="listForm.type">
<el-radio label="athlete">运动员名单</el-radio>
<el-radio label="referee">裁判名单</el-radio>
<el-radio label="staff">工作人员名单</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="导出格式">
<el-radio-group v-model="listForm.format">
<el-radio label="xlsx">Excel (xlsx)</el-radio>
<el-radio label="pdf">PDF</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button
type="primary"
:icon="Download"
:loading="listLoading"
@click="handleExportList"
>
导出名单
</el-button>
</el-form-item>
</el-form>
</div>
</el-tab-pane>
<!-- Tab 3: 证书生成 -->
<el-tab-pane label="证书生成" name="certificate">
<div class="tab-content">
<el-form :model="certForm" label-width="120px" style="max-width: 800px">
<el-form-item label="选择赛事" required>
<el-select
v-model="certForm.competitionId"
placeholder="请选择赛事"
clearable
filterable
style="width: 100%"
@change="handleCertCompetitionChange"
>
<el-option
v-for="item in competitionList"
:key="item.id"
:label="item.competitionName"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="选择项目">
<el-select
v-model="certForm.projectId"
placeholder="请选择项目"
clearable
filterable
style="width: 100%"
>
<el-option
v-for="item in certProjectList"
:key="item.id"
:label="item.projectName"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="证书类型" required>
<el-radio-group v-model="certForm.certType">
<el-radio label="medal">获奖证书金银铜牌</el-radio>
<el-radio label="participation">参赛证书</el-radio>
<el-radio label="excellent">优秀运动员证书</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="证书模板" required>
<el-select
v-model="certForm.templateId"
placeholder="请选择证书模板"
style="width: 100%"
>
<el-option
v-for="item in certificateTemplates"
:key="item.id"
:label="item.templateName"
:value="item.id"
>
<span style="float: left">{{ item.templateName }}</span>
<span style="float: right; color: #8492a6; font-size: 13px">
{{ item.description }}
</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="导出格式">
<el-radio-group v-model="certForm.format">
<el-radio label="pdf">PDF</el-radio>
<el-radio label="jpg">JPG图片</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="批量生成">
<el-switch v-model="certForm.batch" />
<span style="margin-left: 10px; color: #909399; font-size: 13px">
开启后将根据筛选条件批量生成
</span>
</el-form-item>
<el-form-item>
<el-button
type="primary"
:icon="DocumentAdd"
:loading="certLoading"
@click="handleGenerateCertificate"
>
生成证书
</el-button>
<el-button
type="success"
:icon="View"
@click="handlePreviewCertificate"
>
预览模板
</el-button>
</el-form-item>
</el-form>
<!-- 证书列表 -->
<el-divider />
<h4>已生成证书</h4>
<el-table :data="certificateList" border stripe>
<el-table-column prop="athleteName" label="选手姓名" width="120" />
<el-table-column prop="projectName" label="项目" min-width="150" />
<el-table-column prop="ranking" label="排名" width="80" align="center" />
<el-table-column prop="medal" label="奖牌" width="100" align="center">
<template #default="{ row }">
<el-tag v-if="row.medal === 'gold'" type="danger">🏅 金牌</el-tag>
<el-tag v-else-if="row.medal === 'silver'" type="warning">🥈 银牌</el-tag>
<el-tag v-else-if="row.medal === 'bronze'" type="success">🥉 铜牌</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="generateTime" label="生成时间" width="160" align="center">
<template #default="{ row }">
{{ formatDate(row.generateTime) }}
</template>
</el-table-column>
<el-table-column label="操作" width="180" align="center">
<template #default="{ row }">
<el-button link type="primary" :icon="View" @click="handleViewCertificate(row)">
预览
</el-button>
<el-button link type="success" :icon="Download" @click="handleDownloadCertificate(row)">
下载
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-tab-pane>
<!-- Tab 4: 报表导出 -->
<el-tab-pane label="报表导出" name="report">
<div class="tab-content">
<el-form :model="reportForm" label-width="120px" style="max-width: 800px">
<el-form-item label="报表类型" required>
<el-select
v-model="reportForm.reportType"
placeholder="请选择报表类型"
style="width: 100%"
@change="handleReportTypeChange"
>
<el-option
label="赛事统计报表"
value="competition"
description="包含赛事基本信息、参赛人数、项目数量等统计"
/>
<el-option
label="项目统计报表"
value="project"
description="各项目报名人数、参赛人数、获奖分布等"
/>
<el-option
label="裁判工作量报表"
value="referee"
description="裁判评分数量、工作时长等统计"
/>
<el-option
label="报名统计报表"
value="registration"
description="报名趋势、地域分布、团队排名等"
/>
<el-option
label="奖牌榜报表"
value="medal"
description="团队/地区奖牌榜"
/>
</el-select>
</el-form-item>
<el-form-item label="选择赛事" required>
<el-select
v-model="reportForm.competitionId"
placeholder="请选择赛事"
clearable
filterable
style="width: 100%"
>
<el-option
v-for="item in competitionList"
:key="item.id"
:label="item.competitionName"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="导出格式">
<el-radio-group v-model="reportForm.format">
<el-radio label="xlsx">Excel (xlsx)</el-radio>
<el-radio label="pdf">PDF</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="包含图表">
<el-switch v-model="reportForm.includeCharts" />
<span style="margin-left: 10px; color: #909399; font-size: 13px">
导出PDF时包含统计图表
</span>
</el-form-item>
<el-form-item>
<el-button
type="primary"
:icon="Download"
:loading="reportLoading"
@click="handleExportReport"
>
导出报表
</el-button>
</el-form-item>
</el-form>
<!-- 报表说明 -->
<el-divider />
<el-alert type="info" :closable="false" show-icon>
<template #title>
<strong>报表说明</strong>
</template>
<div style="margin-top: 10px">
<p><strong>赛事统计报表</strong>包含赛事基本信息时间地点参赛人数项目数量裁判数量等综合统计信息</p>
<p><strong>项目统计报表</strong>各项目报名人数实际参赛人数获奖分布平均分最高分最低分等详细数据</p>
<p><strong>裁判工作量报表</strong>每位裁判的评分数量负责项目工作时长评分准确性等统计</p>
<p><strong>报名统计报表</strong>报名趋势分析地域分布团队排名报名费收入统计等</p>
<p><strong>奖牌榜报表</strong>按团队或地区统计金银铜牌数量包含总积分排名</p>
</div>
</el-alert>
</div>
</el-tab-pane>
</el-tabs>
</el-card>
<!-- 证书预览弹窗 -->
<el-dialog v-model="previewVisible" title="证书预览" width="900px">
<div class="certificate-preview">
<img v-if="previewUrl" :src="previewUrl" style="width: 100%" />
<el-empty v-else description="暂无预览" />
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import {
Download,
DocumentAdd,
View
} from '@element-plus/icons-vue'
import {
exportResults,
exportAwardList,
generateCertificate,
batchGenerateCertificates
} from '@/api/martial/result'
import { getCompetitionList } from '@/api/martial/competition'
import { getProjectsByCompetition } from '@/api/martial/project'
import {
exportAthletes,
exportReferees,
exportStaff
} from '@/api/martial/export'
import dayjs from 'dayjs'
// 数据定义
const activeTab = ref('result')
const resultLoading = ref(false)
const listLoading = ref(false)
const certLoading = ref(false)
const reportLoading = ref(false)
const previewVisible = ref(false)
const previewUrl = ref('')
const competitionList = ref([])
const projectList = ref([])
const certProjectList = ref([])
const certificateList = ref([])
const certificateTemplates = ref([
{ id: 1, templateName: '标准证书模板', description: '简洁大方' },
{ id: 2, templateName: '豪华证书模板', description: '精美设计' },
{ id: 3, templateName: '传统证书模板', description: '古典风格' }
])
// 成绩导出表单
const resultForm = reactive({
competitionId: '',
projectId: '',
category: '',
exportFields: ['ranking', 'athleteName', 'teamName', 'projectName', 'finalScore', 'medal'],
format: 'xlsx'
})
// 名单导出表单
const listForm = reactive({
competitionId: '',
type: 'athlete',
format: 'xlsx'
})
// 证书生成表单
const certForm = reactive({
competitionId: '',
projectId: '',
certType: 'medal',
templateId: null,
format: 'pdf',
batch: false
})
// 报表导出表单
const reportForm = reactive({
reportType: 'competition',
competitionId: '',
format: 'xlsx',
includeCharts: true
})
// 加载赛事列表
const loadCompetitionList = async () => {
try {
const res = await getCompetitionList(1, 1000, { status: 1 })
if (res.data && res.data.records) {
competitionList.value = res.data.records
}
} catch (error) {
console.error('加载赛事列表失败:', error)
}
}
// 赛事变化
const handleCompetitionChange = async (competitionId) => {
resultForm.projectId = ''
projectList.value = []
if (competitionId) {
try {
const res = await getProjectsByCompetition(competitionId)
if (res.data) {
projectList.value = res.data
}
} catch (error) {
console.error('加载项目列表失败:', error)
}
}
}
// 证书赛事变化
const handleCertCompetitionChange = async (competitionId) => {
certForm.projectId = ''
certProjectList.value = []
if (competitionId) {
try {
const res = await getProjectsByCompetition(competitionId)
if (res.data) {
certProjectList.value = res.data
}
} catch (error) {
console.error('加载项目列表失败:', error)
}
}
}
// 报表类型变化
const handleReportTypeChange = (type) => {
// 可根据不同报表类型显示不同的配置项
console.log('报表类型:', type)
}
// 导出成绩
const handleExportResult = async () => {
if (!resultForm.competitionId) {
ElMessage.warning('请选择赛事')
return
}
resultLoading.value = true
try {
const res = await exportResults(resultForm)
const blob = new Blob([res], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
})
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = `成绩单_${dayjs().format('YYYYMMDDHHmmss')}.${resultForm.format}`
link.click()
window.URL.revokeObjectURL(link.href)
ElMessage.success('导出成功')
} catch (error) {
ElMessage.error('导出失败')
} finally {
resultLoading.value = false
}
}
// 导出名单
const handleExportList = async () => {
if (!listForm.competitionId) {
ElMessage.warning('请选择赛事')
return
}
listLoading.value = true
try {
let exportFunc
switch (listForm.type) {
case 'athlete':
exportFunc = exportAthletes
break
case 'referee':
exportFunc = exportReferees
break
case 'staff':
exportFunc = exportStaff
break
}
const res = await exportFunc({
competitionId: listForm.competitionId,
format: listForm.format
})
const blob = new Blob([res], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
})
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = `${listForm.type}_名单_${dayjs().format('YYYYMMDDHHmmss')}.${listForm.format}`
link.click()
window.URL.revokeObjectURL(link.href)
ElMessage.success('导出成功')
} catch (error) {
ElMessage.error('导出失败')
} finally {
listLoading.value = false
}
}
// 生成证书
const handleGenerateCertificate = async () => {
if (!certForm.competitionId) {
ElMessage.warning('请选择赛事')
return
}
if (!certForm.templateId) {
ElMessage.warning('请选择证书模板')
return
}
certLoading.value = true
try {
let res
if (certForm.batch) {
res = await batchGenerateCertificates(certForm)
ElMessage.success('批量生成成功,正在下载...')
} else {
res = await generateCertificate(certForm)
ElMessage.success('生成成功,正在下载...')
}
const blob = new Blob([res], { type: 'application/pdf' })
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = `证书_${dayjs().format('YYYYMMDDHHmmss')}.${certForm.format}`
link.click()
window.URL.revokeObjectURL(link.href)
} catch (error) {
ElMessage.error('生成失败')
} finally {
certLoading.value = false
}
}
// 预览证书模板
const handlePreviewCertificate = () => {
if (!certForm.templateId) {
ElMessage.warning('请先选择证书模板')
return
}
// 这里可以加载模板预览图
previewUrl.value = `/static/templates/cert_${certForm.templateId}.jpg`
previewVisible.value = true
}
// 查看证书
const handleViewCertificate = (row) => {
previewUrl.value = row.certificateUrl
previewVisible.value = true
}
// 下载证书
const handleDownloadCertificate = (row) => {
window.open(row.certificateUrl, '_blank')
}
// 导出报表
const handleExportReport = async () => {
if (!reportForm.competitionId) {
ElMessage.warning('请选择赛事')
return
}
reportLoading.value = true
try {
// 根据不同的报表类型调用不同的API
// 这里简化处理实际应该有不同的API接口
const res = await exportResults({
...reportForm,
reportType: reportForm.reportType
})
const blob = new Blob([res], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
})
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = `${reportForm.reportType}_报表_${dayjs().format('YYYYMMDDHHmmss')}.${reportForm.format}`
link.click()
window.URL.revokeObjectURL(link.href)
ElMessage.success('导出成功')
} catch (error) {
ElMessage.error('导出失败')
} finally {
reportLoading.value = false
}
}
// 格式化日期
const formatDate = (date) => {
if (!date) return '-'
return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
}
// 生命周期
onMounted(() => {
loadCompetitionList()
})
</script>
<style scoped lang="scss">
.export-container {
padding: 20px;
.tab-content {
padding: 20px 0;
}
.certificate-preview {
text-align: center;
padding: 20px;
}
:deep(.el-tabs__content) {
padding: 20px;
}
:deep(.el-checkbox-group) {
display: flex;
flex-direction: column;
gap: 8px;
}
:deep(.el-alert) {
p {
margin: 5px 0;
line-height: 1.8;
}
}
}
</style>

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -32,11 +32,12 @@
</el-form-item>
<el-form-item>
<el-button type="primary" size="small" @click="handleSearch">查询</el-button>
<el-button size="small" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
<el-table
:data="participantList"
:data="displayList"
border
stripe
style="width: 100%"
@@ -150,6 +151,190 @@
</template>
</el-table-column>
</el-table>
<!-- 新增/编辑/查看弹窗 -->
<el-dialog
:title="dialogTitle"
:visible.sync="dialogVisible"
width="800px"
:close-on-click-modal="false"
@close="handleDialogClose"
top="5vh"
>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="120px"
:disabled="dialogMode === 'view'"
class="participant-form"
>
<!-- 基本信息 -->
<div class="form-section">
<div class="section-title">
<i class="el-icon-user"></i>
基本信息
</div>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="选手姓名" prop="playerName">
<el-input
v-model="formData.playerName"
placeholder="请输入选手姓名"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="性别" prop="gender">
<el-radio-group v-model="formData.gender">
<el-radio :label="1"></el-radio>
<el-radio :label="2"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="年龄" prop="age">
<el-input-number
v-model="formData.age"
:min="6"
:max="100"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="身份证号" prop="idCard">
<el-input
v-model="formData.idCard"
placeholder="请输入身份证号"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系电话" prop="contactPhone">
<el-input
v-model="formData.contactPhone"
placeholder="请输入联系电话"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="所属单位" prop="organization">
<el-input
v-model="formData.organization"
placeholder="请输入所属单位"
/>
</el-form-item>
</div>
<!-- 赛事信息 -->
<div class="form-section">
<div class="section-title">
<i class="el-icon-trophy"></i>
赛事信息
</div>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="所属赛事" prop="competitionId">
<el-select
v-model="formData.competitionId"
placeholder="请选择赛事"
style="width: 100%"
@change="handleCompetitionChange"
>
<el-option
v-for="item in competitionOptions"
:key="item.id"
:label="item.competitionName"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="参赛项目" prop="projectName">
<el-input
v-model="formData.projectName"
placeholder="请输入参赛项目"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="组别" prop="category">
<el-input
v-model="formData.category"
placeholder="例如:成年男子组"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="出场顺序" prop="orderNum">
<el-input-number
v-model="formData.orderNum"
:min="1"
:max="9999"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 其他信息 -->
<div class="form-section">
<div class="section-title">
<i class="el-icon-document"></i>
其他信息
</div>
<el-form-item label="选手简介" prop="introduction">
<el-input
v-model="formData.introduction"
type="textarea"
:rows="4"
placeholder="请输入选手简介"
maxlength="500"
show-word-limit
/>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="formData.remark"
type="textarea"
:rows="3"
placeholder="请输入备注"
/>
</el-form-item>
</div>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button
v-if="dialogMode !== 'view'"
type="primary"
:loading="submitLoading"
@click="handleSubmit"
>
确定
</el-button>
<el-button
v-else
type="primary"
@click="switchToEdit"
>
编辑
</el-button>
</div>
</el-dialog>
</div>
</template>
@@ -182,6 +367,7 @@ export default {
category: '成年男子组',
orderNum: 1,
introduction: '国家一级运动员',
remark: '',
attachments: []
},
{
@@ -198,6 +384,7 @@ export default {
category: '成年女子组',
orderNum: 2,
introduction: '国家二级运动员',
remark: '',
attachments: []
},
{
@@ -214,6 +401,7 @@ export default {
category: '少年男子组',
orderNum: 1,
introduction: '市级青少年冠军',
remark: '',
attachments: []
},
{
@@ -230,6 +418,7 @@ export default {
category: '少年女子组',
orderNum: 2,
introduction: '省级青少年亚军',
remark: '',
attachments: []
},
{
@@ -246,11 +435,80 @@ export default {
category: '中年组',
orderNum: 1,
introduction: '武当第十五代传人',
remark: '',
attachments: []
}
]
],
dialogVisible: false,
dialogMode: 'create', // create, edit, view
submitLoading: false,
currentParticipantId: null,
formData: {
competitionId: null,
competitionName: '',
playerName: '',
gender: 1,
age: null,
contactPhone: '',
organization: '',
idCard: '',
projectName: '',
category: '',
orderNum: 1,
introduction: '',
remark: '',
attachments: []
},
formRules: {
playerName: [
{ required: true, message: '请输入选手姓名', trigger: 'blur' }
],
gender: [
{ required: true, message: '请选择性别', trigger: 'change' }
],
age: [
{ required: true, message: '请输入年龄', trigger: 'blur' }
],
contactPhone: [
{ required: true, message: '请输入联系电话', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
],
competitionId: [
{ required: true, message: '请选择赛事', trigger: 'change' }
],
projectName: [
{ required: true, message: '请输入参赛项目', trigger: 'blur' }
]
}
};
},
computed: {
dialogTitle() {
const titleMap = {
create: '添加参赛选手',
edit: '编辑参赛选手',
view: '查看参赛选手'
};
return titleMap[this.dialogMode] || '参赛选手信息';
},
displayList() {
let list = [...this.participantList];
// 关键词搜索
if (this.searchForm.keyword) {
list = list.filter(item =>
item.playerName.includes(this.searchForm.keyword)
);
}
// 赛事筛选
if (this.searchForm.competitionId) {
list = list.filter(item => item.competitionId === this.searchForm.competitionId);
}
return list;
}
},
mounted() {
this.loadParticipantList();
},
@@ -285,29 +543,39 @@ export default {
},
handleSearch() {
// 实现搜索逻辑
this.loadParticipantList();
// 搜索逻辑已在 computed 中实现
},
handleReset() {
this.searchForm = {
keyword: '',
competitionId: null
};
},
handleCreate() {
this.$router.push({
path: '/martial/participant/manage',
query: { mode: 'create' }
});
this.dialogMode = 'create';
this.currentParticipantId = null;
this.resetFormData();
this.dialogVisible = true;
},
handleView(row) {
this.$router.push({
path: '/martial/participant/manage',
query: { mode: 'view', id: row.id }
});
this.dialogMode = 'view';
this.currentParticipantId = row.id;
this.formData = { ...row };
this.dialogVisible = true;
},
handleEdit(row) {
this.$router.push({
path: '/martial/participant/manage',
query: { mode: 'edit', id: row.id }
});
this.dialogMode = 'edit';
this.currentParticipantId = row.id;
this.formData = { ...row };
this.dialogVisible = true;
},
switchToEdit() {
this.dialogMode = 'edit';
},
handleDelete(row) {
@@ -323,6 +591,84 @@ export default {
this.$message.success('删除成功');
}
}).catch(() => {});
},
handleCompetitionChange(competitionId) {
const competition = this.competitionOptions.find(item => item.id === competitionId);
if (competition) {
this.formData.competitionName = competition.competitionName;
}
},
handleSubmit() {
this.$refs.formRef.validate((valid) => {
if (valid) {
this.submitLoading = true;
// 确保有赛事名称
if (!this.formData.competitionName) {
const competition = this.competitionOptions.find(item => item.id === this.formData.competitionId);
if (competition) {
this.formData.competitionName = competition.competitionName;
}
}
setTimeout(() => {
if (this.dialogMode === 'create') {
// 新建
const newId = this.participantList.length > 0
? Math.max(...this.participantList.map(item => item.id)) + 1
: 1;
const newParticipant = {
...this.formData,
id: newId
};
this.participantList.push(newParticipant);
this.$message.success('添加成功');
} else if (this.dialogMode === 'edit') {
// 编辑
const index = this.participantList.findIndex(item => item.id === this.currentParticipantId);
if (index !== -1) {
this.participantList[index] = {
...this.participantList[index],
...this.formData
};
this.$message.success('保存成功');
}
}
this.saveParticipantList();
this.dialogVisible = false;
this.submitLoading = false;
}, 500);
} else {
this.$message.error('请完善必填信息');
}
});
},
handleDialogClose() {
this.$refs.formRef.resetFields();
this.resetFormData();
},
resetFormData() {
this.formData = {
competitionId: null,
competitionName: '',
playerName: '',
gender: 1,
age: null,
contactPhone: '',
organization: '',
idCard: '',
projectName: '',
category: '',
orderNum: 1,
introduction: '',
remark: '',
attachments: []
};
}
}
};
@@ -369,4 +715,43 @@ export default {
padding: 0 8px;
}
}
.participant-form {
max-height: 60vh;
overflow-y: auto;
}
.form-section {
margin-bottom: 30px;
padding: 20px;
background: #fafafa;
border-radius: 8px;
&:last-child {
margin-bottom: 0;
}
}
.section-title {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 20px;
font-size: 16px;
font-weight: 600;
color: #333;
i {
color: #dc2626;
font-size: 18px;
}
}
:deep(.el-form-item) {
margin-bottom: 18px;
}
.dialog-footer {
text-align: right;
}
</style>

View File

@@ -0,0 +1,837 @@
<template>
<div class="project-container">
<!-- 搜索区域 -->
<el-card shadow="never" class="search-card">
<el-form :inline="true" :model="queryParams" class="search-form">
<el-form-item label="赛事">
<el-select
v-model="queryParams.competitionId"
placeholder="请选择赛事"
clearable
filterable
style="width: 200px"
>
<el-option
v-for="item in competitionList"
:key="item.id"
:label="item.competitionName"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="项目名称">
<el-input
v-model="queryParams.projectName"
placeholder="请输入项目名称"
clearable
style="width: 200px"
/>
</el-form-item>
<el-form-item label="分组类别">
<el-select
v-model="queryParams.category"
placeholder="请选择分组类别"
clearable
style="width: 150px"
>
<el-option label="男子" value="1" />
<el-option label="女子" value="2" />
<el-option label="团体" value="3" />
<el-option label="混合" value="4" />
</el-select>
</el-form-item>
<el-form-item label="项目类型">
<el-select
v-model="queryParams.eventType"
placeholder="请选择项目类型"
clearable
style="width: 150px"
>
<el-option label="套路" value="1" />
<el-option label="散打" value="2" />
<el-option label="器械" value="3" />
<el-option label="对练" value="4" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :icon="Search" @click="handleSearch">
查询
</el-button>
<el-button :icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 工具栏 -->
<el-card shadow="never" class="toolbar-card">
<div class="toolbar">
<div class="toolbar-left">
<el-button type="primary" :icon="Plus" @click="handleAdd">
新增项目
</el-button>
<el-button
type="danger"
:icon="Delete"
:disabled="!selection.length"
@click="handleBatchDelete"
>
批量删除
</el-button>
<el-upload
:action="uploadUrl"
:headers="uploadHeaders"
:before-upload="beforeUpload"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
:show-file-list="false"
accept=".xlsx,.xls"
>
<el-button type="success" :icon="Upload">导入Excel</el-button>
</el-upload>
<el-button type="warning" :icon="Download" @click="handleExport">
导出Excel
</el-button>
</div>
<div class="toolbar-right">
<el-tooltip content="刷新" placement="top">
<el-button circle :icon="Refresh" @click="fetchData" />
</el-tooltip>
</div>
</div>
</el-card>
<!-- 数据表格 -->
<el-card shadow="never" class="table-card">
<el-table
v-loading="loading"
:data="tableData"
stripe
border
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column
prop="projectCode"
label="项目编码"
width="120"
align="center"
/>
<el-table-column
prop="projectName"
label="项目名称"
min-width="180"
show-overflow-tooltip
/>
<el-table-column
prop="competitionName"
label="所属赛事"
min-width="150"
show-overflow-tooltip
/>
<el-table-column prop="category" label="分组类别" width="100" align="center">
<template #default="{ row }">
<el-tag v-if="row.category === 1" type="primary">男子</el-tag>
<el-tag v-else-if="row.category === 2" type="danger">女子</el-tag>
<el-tag v-else-if="row.category === 3" type="success">团体</el-tag>
<el-tag v-else-if="row.category === 4" type="warning">混合</el-tag>
</template>
</el-table-column>
<el-table-column prop="eventType" label="项目类型" width="100" align="center">
<template #default="{ row }">
<span v-if="row.eventType === 1">套路</span>
<span v-else-if="row.eventType === 2">散打</span>
<span v-else-if="row.eventType === 3">器械</span>
<span v-else-if="row.eventType === 4">对练</span>
</template>
</el-table-column>
<el-table-column
prop="registrationFee"
label="报名费(元)"
width="110"
align="center"
>
<template #default="{ row }">
<span style="color: #f56c6c">¥{{ row.registrationFee || 0 }}</span>
</template>
</el-table-column>
<el-table-column label="报名时间" width="180" align="center">
<template #default="{ row }">
<div v-if="row.registrationStartTime && row.registrationEndTime">
<div style="font-size: 12px">
{{ formatDate(row.registrationStartTime) }}
</div>
<div style="font-size: 12px"></div>
<div style="font-size: 12px">
{{ formatDate(row.registrationEndTime) }}
</div>
</div>
<span v-else style="color: #909399">未设置</span>
</template>
</el-table-column>
<el-table-column label="报名人数" width="120" align="center">
<template #default="{ row }">
<span :style="{ color: row.currentCount >= row.maxParticipants ? '#f56c6c' : '#67c23a' }">
{{ row.currentCount || 0 }}
</span>
/ {{ row.maxParticipants || 0 }}
</template>
</el-table-column>
<el-table-column
prop="createTime"
label="创建时间"
width="160"
align="center"
>
<template #default="{ row }">
{{ formatDate(row.createTime) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" :icon="Edit" @click="handleEdit(row)">
编辑
</el-button>
<el-button link type="primary" :icon="View" @click="handleView(row)">
查看
</el-button>
<el-button link type="danger" :icon="Delete" @click="handleDelete(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="queryParams.current"
v-model:page-size="queryParams.size"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="fetchData"
@current-change="fetchData"
/>
</div>
</el-card>
<!-- 新增/编辑弹窗 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="800px"
:close-on-click-modal="false"
@close="handleDialogClose"
>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="120px"
>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="所属赛事" prop="competitionId">
<el-select
v-model="form.competitionId"
placeholder="请选择赛事"
filterable
style="width: 100%"
>
<el-option
v-for="item in competitionList"
:key="item.id"
:label="item.competitionName"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="项目编码" prop="projectCode">
<el-input
v-model="form.projectCode"
placeholder="请输入项目编码"
maxlength="50"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="项目名称" prop="projectName">
<el-input
v-model="form.projectName"
placeholder="请输入项目名称"
maxlength="100"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="分组类别" prop="category">
<el-select
v-model="form.category"
placeholder="请选择分组类别"
style="width: 100%"
>
<el-option label="男子" :value="1" />
<el-option label="女子" :value="2" />
<el-option label="团体" :value="3" />
<el-option label="混合" :value="4" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="项目类型" prop="eventType">
<el-select
v-model="form.eventType"
placeholder="请选择项目类型"
style="width: 100%"
>
<el-option label="套路" :value="1" />
<el-option label="散打" :value="2" />
<el-option label="器械" :value="3" />
<el-option label="对练" :value="4" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="报名费(元)" prop="registrationFee">
<el-input-number
v-model="form.registrationFee"
:min="0"
:precision="2"
:step="10"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="报名开始时间" prop="registrationStartTime">
<el-date-picker
v-model="form.registrationStartTime"
type="datetime"
placeholder="选择开始时间"
style="width: 100%"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="报名结束时间" prop="registrationEndTime">
<el-date-picker
v-model="form.registrationEndTime"
type="datetime"
placeholder="选择结束时间"
style="width: 100%"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="最大参赛人数" prop="maxParticipants">
<el-input-number
v-model="form.maxParticipants"
:min="1"
:max="1000"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="排序序号" prop="sortOrder">
<el-input-number
v-model="form.sortOrder"
:min="0"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="比赛规则" prop="rules">
<el-input
v-model="form.rules"
type="textarea"
:rows="4"
placeholder="请输入比赛规则说明"
maxlength="500"
show-word-limit
/>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="form.remark"
type="textarea"
:rows="3"
placeholder="请输入备注信息"
maxlength="200"
show-word-limit
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">
确定
</el-button>
</template>
</el-dialog>
<!-- 查看详情弹窗 -->
<el-dialog v-model="detailVisible" title="项目详情" width="700px">
<el-descriptions :column="2" border>
<el-descriptions-item label="项目编码">
{{ detailData.projectCode }}
</el-descriptions-item>
<el-descriptions-item label="项目名称">
{{ detailData.projectName }}
</el-descriptions-item>
<el-descriptions-item label="所属赛事">
{{ detailData.competitionName }}
</el-descriptions-item>
<el-descriptions-item label="分组类别">
<el-tag v-if="detailData.category === 1" type="primary">男子</el-tag>
<el-tag v-else-if="detailData.category === 2" type="danger">女子</el-tag>
<el-tag v-else-if="detailData.category === 3" type="success">团体</el-tag>
<el-tag v-else-if="detailData.category === 4" type="warning">混合</el-tag>
</el-descriptions-item>
<el-descriptions-item label="项目类型">
<span v-if="detailData.eventType === 1">套路</span>
<span v-else-if="detailData.eventType === 2">散打</span>
<span v-else-if="detailData.eventType === 3">器械</span>
<span v-else-if="detailData.eventType === 4">对练</span>
</el-descriptions-item>
<el-descriptions-item label="报名费">
<span style="color: #f56c6c; font-weight: bold">
¥{{ detailData.registrationFee || 0 }}
</span>
</el-descriptions-item>
<el-descriptions-item label="报名开始时间">
{{ formatDate(detailData.registrationStartTime) }}
</el-descriptions-item>
<el-descriptions-item label="报名结束时间">
{{ formatDate(detailData.registrationEndTime) }}
</el-descriptions-item>
<el-descriptions-item label="最大参赛人数">
{{ detailData.maxParticipants }}
</el-descriptions-item>
<el-descriptions-item label="当前报名人数">
<span :style="{
color: detailData.currentCount >= detailData.maxParticipants ? '#f56c6c' : '#67c23a',
fontWeight: 'bold'
}">
{{ detailData.currentCount || 0 }}
</span>
</el-descriptions-item>
<el-descriptions-item label="创建时间" :span="2">
{{ formatDate(detailData.createTime) }}
</el-descriptions-item>
<el-descriptions-item label="比赛规则" :span="2">
{{ detailData.rules || '暂无' }}
</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">
{{ detailData.remark || '暂无' }}
</el-descriptions-item>
</el-descriptions>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
Search,
Refresh,
Plus,
Delete,
Edit,
View,
Upload,
Download
} from '@element-plus/icons-vue'
import {
getProjectList,
addProject,
updateProject,
removeProject,
importProjects,
exportProjects
} from '@/api/martial/project'
import { getCompetitionList } from '@/api/martial/competition'
import { getToken } from '@/util/auth'
import dayjs from 'dayjs'
// 数据定义
const loading = ref(false)
const submitLoading = ref(false)
const tableData = ref([])
const total = ref(0)
const selection = ref([])
const competitionList = ref([])
const dialogVisible = ref(false)
const detailVisible = ref(false)
const dialogTitle = ref('')
const formRef = ref(null)
const detailData = ref({})
// 查询参数
const queryParams = reactive({
current: 1,
size: 10,
competitionId: '',
projectName: '',
category: '',
eventType: ''
})
// 表单数据
const form = reactive({
id: null,
competitionId: '',
projectCode: '',
projectName: '',
category: null,
eventType: null,
registrationFee: 0,
registrationStartTime: '',
registrationEndTime: '',
maxParticipants: 100,
sortOrder: 0,
rules: '',
remark: ''
})
// 上传配置
const uploadUrl = computed(() => {
return import.meta.env.VITE_APP_BASE_URL + '/api/blade-martial/project/import'
})
const uploadHeaders = computed(() => {
return {
'Blade-Auth': 'bearer ' + getToken()
}
})
// 表单验证规则
const rules = {
competitionId: [
{ required: true, message: '请选择所属赛事', trigger: 'change' }
],
projectCode: [
{ required: true, message: '请输入项目编码', trigger: 'blur' },
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
],
projectName: [
{ required: true, message: '请输入项目名称', trigger: 'blur' },
{ min: 2, max: 100, message: '长度在 2 到 100 个字符', trigger: 'blur' }
],
category: [
{ required: true, message: '请选择分组类别', trigger: 'change' }
],
eventType: [
{ required: true, message: '请选择项目类型', trigger: 'change' }
],
registrationFee: [
{ required: true, message: '请输入报名费', trigger: 'blur' }
],
registrationStartTime: [
{ required: true, message: '请选择报名开始时间', trigger: 'change' }
],
registrationEndTime: [
{ required: true, message: '请选择报名结束时间', trigger: 'change' },
{
validator: (rule, value, callback) => {
if (value && form.registrationStartTime && value <= form.registrationStartTime) {
callback(new Error('结束时间必须大于开始时间'))
} else {
callback()
}
},
trigger: 'change'
}
],
maxParticipants: [
{ required: true, message: '请输入最大参赛人数', trigger: 'blur' }
]
}
// 加载赛事列表
const loadCompetitionList = async () => {
try {
const res = await getCompetitionList(1, 1000, { status: 1 })
if (res.data && res.data.records) {
competitionList.value = res.data.records
}
} catch (error) {
console.error('加载赛事列表失败:', error)
}
}
// 查询数据
const fetchData = async () => {
loading.value = true
try {
const res = await getProjectList(
queryParams.current,
queryParams.size,
queryParams
)
if (res.data) {
tableData.value = res.data.records || []
total.value = res.data.total || 0
}
} catch (error) {
ElMessage.error('获取数据失败')
console.error(error)
} finally {
loading.value = false
}
}
// 搜索
const handleSearch = () => {
queryParams.current = 1
fetchData()
}
// 重置
const handleReset = () => {
Object.assign(queryParams, {
current: 1,
size: 10,
competitionId: '',
projectName: '',
category: '',
eventType: ''
})
fetchData()
}
// 新增
const handleAdd = () => {
dialogTitle.value = '新增项目'
resetForm()
dialogVisible.value = true
}
// 编辑
const handleEdit = (row) => {
dialogTitle.value = '编辑项目'
Object.keys(form).forEach((key) => {
form[key] = row[key]
})
dialogVisible.value = true
}
// 查看
const handleView = (row) => {
detailData.value = { ...row }
detailVisible.value = true
}
// 删除
const handleDelete = (row) => {
ElMessageBox.confirm('确定要删除该项目吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
try {
await removeProject(row.id)
ElMessage.success('删除成功')
fetchData()
} catch (error) {
ElMessage.error('删除失败')
}
})
.catch(() => {})
}
// 批量删除
const handleBatchDelete = () => {
ElMessageBox.confirm(`确定要删除选中的 ${selection.value.length} 个项目吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
try {
const ids = selection.value.map((item) => item.id).join(',')
await removeProject(ids)
ElMessage.success('删除成功')
fetchData()
} catch (error) {
ElMessage.error('删除失败')
}
})
.catch(() => {})
}
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
submitLoading.value = true
try {
if (form.id) {
await updateProject(form)
ElMessage.success('修改成功')
} else {
await addProject(form)
ElMessage.success('新增成功')
}
dialogVisible.value = false
fetchData()
} catch (error) {
ElMessage.error(form.id ? '修改失败' : '新增失败')
} finally {
submitLoading.value = false
}
}
})
}
// 选择变化
const handleSelectionChange = (val) => {
selection.value = val
}
// 关闭弹窗
const handleDialogClose = () => {
resetForm()
}
// 重置表单
const resetForm = () => {
Object.assign(form, {
id: null,
competitionId: '',
projectCode: '',
projectName: '',
category: null,
eventType: null,
registrationFee: 0,
registrationStartTime: '',
registrationEndTime: '',
maxParticipants: 100,
sortOrder: 0,
rules: '',
remark: ''
})
if (formRef.value) {
formRef.value.clearValidate()
}
}
// 上传前检查
const beforeUpload = (file) => {
if (!queryParams.competitionId) {
ElMessage.warning('请先选择赛事')
return false
}
const isExcel =
file.type === 'application/vnd.ms-excel' ||
file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
if (!isExcel) {
ElMessage.error('只能上传 Excel 文件!')
return false
}
const isLt5M = file.size / 1024 / 1024 < 5
if (!isLt5M) {
ElMessage.error('文件大小不能超过 5MB!')
return false
}
return true
}
// 上传成功
const handleUploadSuccess = (response) => {
if (response.success) {
ElMessage.success('导入成功')
fetchData()
} else {
ElMessage.error(response.msg || '导入失败')
}
}
// 上传失败
const handleUploadError = () => {
ElMessage.error('导入失败')
}
// 导出
const handleExport = async () => {
try {
const res = await exportProjects(queryParams)
const blob = new Blob([res], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
})
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = `项目列表_${dayjs().format('YYYYMMDDHHmmss')}.xlsx`
link.click()
window.URL.revokeObjectURL(link.href)
ElMessage.success('导出成功')
} catch (error) {
ElMessage.error('导出失败')
}
}
// 格式化日期
const formatDate = (date) => {
if (!date) return '-'
return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
}
// 生命周期
onMounted(() => {
loadCompetitionList()
fetchData()
})
</script>
<style scoped lang="scss">
.project-container {
padding: 20px;
.search-card,
.toolbar-card,
.table-card {
margin-bottom: 20px;
}
.search-form {
.el-form-item {
margin-bottom: 0;
}
}
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
.toolbar-left {
display: flex;
gap: 10px;
}
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<div class="martial-registration-detail">
<div class="martial-registration-container">
<div class="page-header">
<el-button icon="el-icon-back" size="small" @click="goBack">返回</el-button>
<h2 class="page-title">报名详情</h2>
@@ -273,7 +273,7 @@ export default {
},
methods: {
goBack() {
this.$router.push('/martial/order/list')
this.$router.go(-1)
},
handleExport() {
this.$message.success('导出功能开发中')
@@ -283,7 +283,7 @@ export default {
</script>
<style lang="scss" scoped>
.martial-registration-detail {
.martial-registration-container {
padding: 15px;
background: #fff;

View File

@@ -0,0 +1,786 @@
<template>
<div class="result-container">
<!-- 搜索区域 -->
<el-card shadow="never" class="search-card">
<el-form :inline="true" :model="queryParams" class="search-form">
<el-form-item label="赛事">
<el-select
v-model="queryParams.competitionId"
placeholder="请选择赛事"
clearable
filterable
style="width: 200px"
@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-item label="项目">
<el-select
v-model="queryParams.projectId"
placeholder="请选择项目"
clearable
filterable
style="width: 200px"
>
<el-option
v-for="item in projectList"
:key="item.id"
:label="item.projectName"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="选手姓名">
<el-input
v-model="queryParams.athleteName"
placeholder="请输入选手姓名"
clearable
style="width: 150px"
/>
</el-form-item>
<el-form-item label="团队名称">
<el-input
v-model="queryParams.teamName"
placeholder="请输入团队名称"
clearable
style="width: 150px"
/>
</el-form-item>
<el-form-item label="是否最终成绩">
<el-select
v-model="queryParams.isFinal"
placeholder="请选择"
clearable
style="width: 130px"
>
<el-option label="是" :value="1" />
<el-option label="否" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :icon="Search" @click="handleSearch">
查询
</el-button>
<el-button :icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 统计卡片 -->
<el-row :gutter="20" class="stats-row">
<el-col :span="6">
<el-card shadow="hover" class="stats-card">
<div class="stats-content">
<div class="stats-icon" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%)">
<el-icon :size="30"><User /></el-icon>
</div>
<div class="stats-info">
<div class="stats-value">{{ statistics.totalCount || 0 }}</div>
<div class="stats-label">总参赛人数</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stats-card">
<div class="stats-content">
<div class="stats-icon" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%)">
<el-icon :size="30"><Finished /></el-icon>
</div>
<div class="stats-info">
<div class="stats-value">{{ statistics.calculatedCount || 0 }}</div>
<div class="stats-label">已计算成绩</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stats-card">
<div class="stats-content">
<div class="stats-icon" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)">
<el-icon :size="30"><Trophy /></el-icon>
</div>
<div class="stats-info">
<div class="stats-value">{{ statistics.medalCount || 0 }}</div>
<div class="stats-label">已分配奖牌</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stats-card">
<div class="stats-content">
<div class="stats-icon" style="background: linear-gradient(135deg, #fa709a 0%, #fee140 100%)">
<el-icon :size="30"><Document /></el-icon>
</div>
<div class="stats-info">
<div class="stats-value">{{ statistics.publishedCount || 0 }}</div>
<div class="stats-label">已发布成绩</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 工具栏 -->
<el-card shadow="never" class="toolbar-card">
<div class="toolbar">
<div class="toolbar-left">
<el-button
type="primary"
:icon="Operation"
:disabled="!selection.length"
@click="handleBatchCalculate"
>
批量计算成绩
</el-button>
<el-button
type="success"
:icon="Sort"
:disabled="!queryParams.projectId"
@click="handleAutoRanking"
>
自动排名
</el-button>
<el-button
type="warning"
:icon="Medal"
:disabled="!queryParams.projectId"
@click="handleAllocateMedals"
>
分配奖牌
</el-button>
<el-button
type="info"
:icon="Promotion"
:disabled="!selection.length"
@click="handlePublish"
>
发布成绩
</el-button>
<el-button
:icon="Remove"
:disabled="!selection.length"
@click="handleUnpublish"
>
撤销发布
</el-button>
<el-button
type="danger"
:icon="Download"
@click="handleExport"
>
导出成绩单
</el-button>
</div>
<div class="toolbar-right">
<el-tooltip content="刷新" placement="top">
<el-button circle :icon="Refresh" @click="fetchData" />
</el-tooltip>
</div>
</div>
</el-card>
<!-- 数据表格 -->
<el-card shadow="never" class="table-card">
<el-table
v-loading="loading"
:data="tableData"
stripe
border
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column prop="ranking" label="排名" width="80" align="center">
<template #default="{ row }">
<el-tag v-if="row.ranking === 1" type="danger" effect="dark">🥇 {{ row.ranking }}</el-tag>
<el-tag v-else-if="row.ranking === 2" type="warning" effect="dark">🥈 {{ row.ranking }}</el-tag>
<el-tag v-else-if="row.ranking === 3" type="success" effect="dark">🥉 {{ row.ranking }}</el-tag>
<span v-else>{{ row.ranking || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="athleteName" label="选手姓名" width="120" />
<el-table-column prop="teamName" label="团队名称" min-width="150" show-overflow-tooltip />
<el-table-column prop="projectName" label="项目" min-width="150" show-overflow-tooltip />
<el-table-column prop="totalScore" label="总分" width="100" align="center">
<template #default="{ row }">
{{ row.totalScore !== null ? row.totalScore.toFixed(2) : '-' }}
</template>
</el-table-column>
<el-table-column prop="highestScore" label="最高分" width="100" align="center">
<template #default="{ row }">
<span style="color: #f56c6c">{{ row.highestScore !== null ? row.highestScore.toFixed(2) : '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="lowestScore" label="最低分" width="100" align="center">
<template #default="{ row }">
<span style="color: #909399">{{ row.lowestScore !== null ? row.lowestScore.toFixed(2) : '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="validScoreCount" label="有效评分数" width="110" align="center" />
<el-table-column prop="difficultyCoefficient" label="难度系数" width="100" align="center">
<template #default="{ row }">
×{{ row.difficultyCoefficient || 1 }}
</template>
</el-table-column>
<el-table-column prop="finalScore" label="最终成绩" width="110" align="center">
<template #default="{ row }">
<span v-if="row.finalScore !== null" style="color: #409eff; font-weight: bold; font-size: 16px">
{{ row.finalScore.toFixed(2) }}
</span>
<span v-else style="color: #909399">未计算</span>
</template>
</el-table-column>
<el-table-column prop="medal" label="奖牌" width="100" align="center">
<template #default="{ row }">
<el-tag v-if="row.medal === 'gold'" type="danger" effect="dark">🏅 金牌</el-tag>
<el-tag v-else-if="row.medal === 'silver'" type="warning" effect="dark">🥈 银牌</el-tag>
<el-tag v-else-if="row.medal === 'bronze'" type="success" effect="dark">🥉 铜牌</el-tag>
<span v-else style="color: #909399">-</span>
</template>
</el-table-column>
<el-table-column prop="isPublished" label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag v-if="row.isPublished === 1" type="success">已发布</el-tag>
<el-tag v-else type="info">未发布</el-tag>
</template>
</el-table-column>
<el-table-column prop="publishTime" label="发布时间" width="160" align="center">
<template #default="{ row }">
{{ row.publishTime ? formatDate(row.publishTime) : '-' }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" :icon="View" @click="handleView(row)">
查看详情
</el-button>
<el-button link type="success" :icon="Edit" @click="handleCalculate(row)">
重新计算
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="queryParams.current"
v-model:page-size="queryParams.size"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="fetchData"
@current-change="fetchData"
/>
</div>
</el-card>
<!-- 成绩详情弹窗 -->
<el-dialog v-model="detailVisible" title="成绩详情" width="900px">
<el-descriptions :column="3" border>
<el-descriptions-item label="选手姓名">{{ detailData.athleteName }}</el-descriptions-item>
<el-descriptions-item label="团队名称">{{ detailData.teamName }}</el-descriptions-item>
<el-descriptions-item label="项目">{{ detailData.projectName }}</el-descriptions-item>
<el-descriptions-item label="排名">
<el-tag v-if="detailData.ranking === 1" type="danger" effect="dark">🥇 第1名</el-tag>
<el-tag v-else-if="detailData.ranking === 2" type="warning" effect="dark">🥈 第2名</el-tag>
<el-tag v-else-if="detailData.ranking === 3" type="success" effect="dark">🥉 第3名</el-tag>
<span v-else>{{ detailData.ranking }}</span>
</el-descriptions-item>
<el-descriptions-item label="总分">{{ detailData.totalScore?.toFixed(2) || '-' }}</el-descriptions-item>
<el-descriptions-item label="最终成绩">
<span style="color: #409eff; font-weight: bold; font-size: 18px">
{{ detailData.finalScore?.toFixed(2) || '-' }}
</span>
</el-descriptions-item>
<el-descriptions-item label="最高分">
<span style="color: #f56c6c">{{ detailData.highestScore?.toFixed(2) || '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="最低分">
<span style="color: #909399">{{ detailData.lowestScore?.toFixed(2) || '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="有效评分数">{{ detailData.validScoreCount || 0 }}</el-descriptions-item>
<el-descriptions-item label="难度系数">×{{ detailData.difficultyCoefficient || 1 }}</el-descriptions-item>
<el-descriptions-item label="奖牌">
<el-tag v-if="detailData.medal === 'gold'" type="danger" effect="dark">🏅 金牌</el-tag>
<el-tag v-else-if="detailData.medal === 'silver'" type="warning" effect="dark">🥈 银牌</el-tag>
<el-tag v-else-if="detailData.medal === 'bronze'" type="success" effect="dark">🥉 铜牌</el-tag>
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="发布状态">
<el-tag v-if="detailData.isPublished === 1" type="success">已发布</el-tag>
<el-tag v-else type="info">未发布</el-tag>
</el-descriptions-item>
</el-descriptions>
<el-divider />
<h4>评分明细</h4>
<el-table :data="detailData.scoreList" border size="small">
<el-table-column prop="judgeName" label="裁判" width="120" />
<el-table-column prop="score" label="评分" width="100" align="center">
<template #default="{ row }">
{{ row.score?.toFixed(2) }}
</template>
</el-table-column>
<el-table-column prop="deductionPoints" label="扣分" width="100" align="center">
<template #default="{ row }">
<span style="color: #f56c6c">-{{ row.deductionPoints?.toFixed(2) || 0 }}</span>
</template>
</el-table-column>
<el-table-column prop="isExcluded" label="是否去除" width="100" align="center">
<template #default="{ row }">
<el-tag v-if="row.isExcluded" type="info" size="small">已去除</el-tag>
<el-tag v-else type="success" size="small">有效</el-tag>
</template>
</el-table-column>
<el-table-column prop="scoreTime" label="评分时间" width="160" align="center">
<template #default="{ row }">
{{ formatDate(row.scoreTime) }}
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip />
</el-table>
</el-dialog>
<!-- 奖牌分配弹窗 -->
<el-dialog v-model="medalDialogVisible" title="奖牌分配" width="500px">
<el-form :model="medalForm" label-width="100px">
<el-form-item label="金牌数量">
<el-input-number v-model="medalForm.goldCount" :min="0" :max="20" />
</el-form-item>
<el-form-item label="银牌数量">
<el-input-number v-model="medalForm.silverCount" :min="0" :max="20" />
</el-form-item>
<el-form-item label="铜牌数量">
<el-input-number v-model="medalForm.bronzeCount" :min="0" :max="20" />
</el-form-item>
<el-alert type="info" :closable="false" show-icon>
<template #title>
将根据当前项目的排名自动分配奖牌
</template>
</el-alert>
</el-form>
<template #footer>
<el-button @click="medalDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="medalLoading" @click="handleMedalSubmit">
确认分配
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
Search,
Refresh,
View,
Edit,
Operation,
Sort,
Medal,
Promotion,
Remove,
Download,
User,
Finished,
Trophy,
Document
} from '@element-plus/icons-vue'
import {
getResultList,
calculateResult,
batchCalculateResults,
autoRanking,
allocateMedals,
publishResult,
unpublishResult,
getResultDetail,
getResultStatistics,
exportResults
} from '@/api/martial/result'
import { getCompetitionList } from '@/api/martial/competition'
import { getProjectsByCompetition } from '@/api/martial/project'
import dayjs from 'dayjs'
// 数据定义
const loading = ref(false)
const medalLoading = ref(false)
const tableData = ref([])
const total = ref(0)
const selection = ref([])
const competitionList = ref([])
const projectList = ref([])
const detailVisible = ref(false)
const medalDialogVisible = ref(false)
const detailData = ref({})
const statistics = ref({
totalCount: 0,
calculatedCount: 0,
medalCount: 0,
publishedCount: 0
})
// 查询参数
const queryParams = reactive({
current: 1,
size: 10,
competitionId: '',
projectId: '',
athleteName: '',
teamName: '',
isFinal: ''
})
// 奖牌分配表单
const medalForm = reactive({
goldCount: 1,
silverCount: 2,
bronzeCount: 3
})
// 加载赛事列表
const loadCompetitionList = async () => {
try {
const res = await getCompetitionList(1, 1000, { status: 1 })
if (res.data && res.data.records) {
competitionList.value = res.data.records
}
} catch (error) {
console.error('加载赛事列表失败:', error)
}
}
// 赛事变化
const handleCompetitionChange = async (competitionId) => {
queryParams.projectId = ''
projectList.value = []
if (competitionId) {
try {
const res = await getProjectsByCompetition(competitionId)
if (res.data) {
projectList.value = res.data
}
} catch (error) {
console.error('加载项目列表失败:', error)
}
}
}
// 加载统计数据
const loadStatistics = async () => {
if (!queryParams.projectId && !queryParams.competitionId) return
try {
const res = await getResultStatistics({
projectId: queryParams.projectId,
competitionId: queryParams.competitionId
})
if (res.data) {
statistics.value = res.data
}
} catch (error) {
console.error('加载统计数据失败:', error)
}
}
// 查询数据
const fetchData = async () => {
loading.value = true
try {
const res = await getResultList(
queryParams.current,
queryParams.size,
queryParams
)
if (res.data) {
tableData.value = res.data.records || []
total.value = res.data.total || 0
}
await loadStatistics()
} catch (error) {
ElMessage.error('获取数据失败')
console.error(error)
} finally {
loading.value = false
}
}
// 搜索
const handleSearch = () => {
queryParams.current = 1
fetchData()
}
// 重置
const handleReset = () => {
Object.assign(queryParams, {
current: 1,
size: 10,
competitionId: '',
projectId: '',
athleteName: '',
teamName: '',
isFinal: ''
})
projectList.value = []
fetchData()
}
// 查看详情
const handleView = async (row) => {
try {
const res = await getResultDetail(row.id)
if (res.data) {
detailData.value = res.data
detailVisible.value = true
}
} catch (error) {
ElMessage.error('获取详情失败')
}
}
// 计算单个成绩
const handleCalculate = async (row) => {
try {
await calculateResult({ resultId: row.id })
ElMessage.success('计算成功')
fetchData()
} catch (error) {
ElMessage.error('计算失败')
}
}
// 批量计算成绩
const handleBatchCalculate = () => {
ElMessageBox.confirm(`确定要计算选中的 ${selection.value.length} 个选手的成绩吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
try {
const ids = selection.value.map((item) => item.id)
await batchCalculateResults({ resultIds: ids })
ElMessage.success('批量计算成功')
fetchData()
} catch (error) {
ElMessage.error('批量计算失败')
}
})
.catch(() => {})
}
// 自动排名
const handleAutoRanking = () => {
if (!queryParams.projectId) {
ElMessage.warning('请先选择项目')
return
}
ElMessageBox.confirm('确定要对当前项目的所有选手自动排名吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
try {
await autoRanking({ projectId: queryParams.projectId })
ElMessage.success('自动排名成功')
fetchData()
} catch (error) {
ElMessage.error('自动排名失败')
}
})
.catch(() => {})
}
// 分配奖牌
const handleAllocateMedals = () => {
if (!queryParams.projectId) {
ElMessage.warning('请先选择项目')
return
}
medalDialogVisible.value = true
}
// 提交奖牌分配
const handleMedalSubmit = async () => {
medalLoading.value = true
try {
await allocateMedals({
projectId: queryParams.projectId,
...medalForm
})
ElMessage.success('奖牌分配成功')
medalDialogVisible.value = false
fetchData()
} catch (error) {
ElMessage.error('奖牌分配失败')
} finally {
medalLoading.value = false
}
}
// 发布成绩
const handlePublish = () => {
ElMessageBox.confirm(`确定要发布选中的 ${selection.value.length} 个选手的成绩吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
try {
const ids = selection.value.map((item) => item.id)
await publishResult({ resultIds: ids })
ElMessage.success('发布成功')
fetchData()
} catch (error) {
ElMessage.error('发布失败')
}
})
.catch(() => {})
}
// 撤销发布
const handleUnpublish = () => {
ElMessageBox.confirm(`确定要撤销选中的 ${selection.value.length} 个选手的成绩发布吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
try {
const ids = selection.value.map((item) => item.id)
await unpublishResult({ resultIds: ids })
ElMessage.success('撤销成功')
fetchData()
} catch (error) {
ElMessage.error('撤销失败')
}
})
.catch(() => {})
}
// 导出成绩单
const handleExport = async () => {
try {
const res = await exportResults(queryParams)
const blob = new Blob([res], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
})
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = `成绩单_${dayjs().format('YYYYMMDDHHmmss')}.xlsx`
link.click()
window.URL.revokeObjectURL(link.href)
ElMessage.success('导出成功')
} catch (error) {
ElMessage.error('导出失败')
}
}
// 选择变化
const handleSelectionChange = (val) => {
selection.value = val
}
// 格式化日期
const formatDate = (date) => {
if (!date) return '-'
return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
}
// 生命周期
onMounted(() => {
loadCompetitionList()
fetchData()
})
</script>
<style scoped lang="scss">
.result-container {
padding: 20px;
.search-card,
.toolbar-card,
.table-card {
margin-bottom: 20px;
}
.search-form {
.el-form-item {
margin-bottom: 0;
}
}
.stats-row {
margin-bottom: 20px;
.stats-card {
.stats-content {
display: flex;
align-items: center;
padding: 10px 0;
.stats-icon {
width: 60px;
height: 60px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
margin-right: 20px;
}
.stats-info {
flex: 1;
.stats-value {
font-size: 28px;
font-weight: bold;
color: #303133;
line-height: 1;
margin-bottom: 8px;
}
.stats-label {
font-size: 14px;
color: #909399;
}
}
}
}
}
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
.toolbar-left {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
}
</style>

View File

@@ -241,7 +241,7 @@ export default {
},
methods: {
goBack() {
this.$router.push('/martial/order/list')
this.$router.go(-1)
},
handleVenueCommand(command, groupIndex) {
const venueName = command === 'venue1' ? '一号场地' : '二号场地'

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -21,11 +21,11 @@ export default ({ mode, command }) => {
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, ''),
},
// 武术业务模块保留 /api 前缀
'/api': {
target: 'http://localhost:8123',
changeOrigin: true,
},
// // 武术业务模块保留 /api 前缀
// '/api': {
// target: 'http://localhost:8123',
// changeOrigin: true,
// },
},
},
resolve: {