Compare commits

...

9 Commits

Author SHA1 Message Date
DevOps
b67a1e039c feat: 移除项目编码手动输入
- 项目编码由后端自动生成
- 移除表单中的项目编码输入框和验证规则

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-08 17:19:33 +08:00
DevOps
c7e78612bf feat: 添加单位统计API调用
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-08 16:08:47 +08:00
DevOps
49c1cd81c6 重构项3: 添加表号生成和显示功能
- 在编排页面项目头部添加表号显示
- 实现generateTableNo方法,格式为: 场地(1位)+时段(1位)+序号(2位)
- 时段规则: 上午=1, 下午=2
- 序号在同场地同时段中按id排序确定

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-08 15:46:48 +08:00
DevOps
420bd29eff 重构项1: 移除项目管理中的场地分配功能
- 移除表格中的所属场地列
- 移除表单中的场地选择字段
- 移除场地相关的API导入和数据定义
- 移除handleCompetitionChangeInForm、loadVenuesForProjects、getVenueName等函数
- 后端已实现动态场地分配,无需前端指定

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-08 15:37:07 +08:00
DevOps
a1b26208a4 feat: 赛事管理页面状态根据时间自动计算
- 添加 calculateStatus 方法根据报名时间和比赛时间计算状态
- 状态显示不再依赖数据库字段,而是实时计算

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-07 12:28:07 +08:00
DevOps
41c67e1ddf fix: 将订单管理改为赛事管理
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-07 11:58:03 +08:00
DevOps
e5b028f084 fix: 修复场地类型(venueType)加载和保存问题
- 在loadVenues中添加venueType字段映射,确保从后端加载时正确回显
- 在saveVenues中添加venueType字段,确保保存时正确提交
- 修复附件上传headers认证问题

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-06 15:17:28 +08:00
DevOps
6385acd43b fix(deduction): 修复扣分项编辑功能
- form对象添加itemName字段解决编辑时名称不显示
- 查询时过滤空字符串参数避免无数据问题
- 字段名deductionPoints改为deductionPoint与后端一致
2026-01-06 14:56:10 +08:00
DevOps
c37b6d8f6f fix(competition): 修复附件上传按钮失灵问题
- 添加 getToken 导入
- 为 avue-form 上传组件添加 Blade-Auth headers 认证

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-06 12:52:28 +08:00
12 changed files with 1154 additions and 124 deletions

View File

@@ -196,3 +196,13 @@ export const exportSchedule = (competitionId) => {
responseType: 'blob' responseType: 'blob'
}) })
} }
// Export schedule template 2 (competition time format)
export const exportScheduleTemplate2 = (competitionId, venueId, venueName, timeSlot) => {
return request({
url: '/martial/export/schedule2',
method: 'get',
params: { competitionId, venueId, venueName, timeSlot },
responseType: 'blob'
})
}

View File

@@ -141,3 +141,16 @@ export const getOrderAmountStats = (orderId) => {
params: { orderId } params: { orderId }
}) })
} }
/**
* 获取单位统计
* @param {Number} competitionId - 赛事ID
*/
export const getOrganizationStats = (competitionId) => {
return request({
url: '/api/martial/registrationOrder/organization-stats',
method: 'get',
params: { competitionId }
})
}

View File

@@ -48,7 +48,7 @@ export default [
redirect: '/martial/order/list', redirect: '/martial/order/list',
children: [ children: [
{ {
path: 'competition/list', path: 'competition/index',
name: '赛事管理', name: '赛事管理',
meta: { meta: {
keepAlive: false, keepAlive: false,
@@ -65,7 +65,7 @@ export default [
}, },
{ {
path: 'order/list', path: 'order/list',
name: '订单管理', name: '赛事管理',
meta: { meta: {
keepAlive: false, keepAlive: false,
}, },

View File

@@ -959,6 +959,7 @@ import {
ATTACHMENT_TYPES, ATTACHMENT_TYPES,
ATTACHMENT_TYPE_LABELS ATTACHMENT_TYPE_LABELS
} from '@/api/martial/attachment' } from '@/api/martial/attachment'
import { getToken } from '@/utils/auth'
export default { export default {
name: 'CompetitionManagement', name: 'CompetitionManagement',
@@ -1002,6 +1003,7 @@ export default {
propsHttp: { propsHttp: {
res: 'data', res: 'data',
}, },
action: '/blade-resource/oss/endpoint/put-file' action: '/blade-resource/oss/endpoint/put-file'
} }
] ]
@@ -1333,7 +1335,8 @@ export default {
venueCode: item.venueCode, venueCode: item.venueCode,
capacity: item.capacity, capacity: item.capacity,
location: item.location, location: item.location,
remark: item.facilities || '' remark: item.facilities || '',
venueType: item.venueType || 'indoor'
})); }));
console.log('✅ 加载的场地列表:', this.formData.venues); console.log('✅ 加载的场地列表:', this.formData.venues);
console.log('✅ 场地数量:', this.formData.venues.length); console.log('✅ 场地数量:', this.formData.venues.length);
@@ -1405,6 +1408,7 @@ export default {
handleOpenAttachmentUpload(type) { handleOpenAttachmentUpload(type) {
this.currentAttachmentType = type; this.currentAttachmentType = type;
this.attachmentUploadForm = {}; this.attachmentUploadForm = {};
this.attachmentUploadOption.column[0].headers = { "Blade-Auth": "bearer " + getToken() };
this.attachmentUploadDialogVisible = true; this.attachmentUploadDialogVisible = true;
}, },
@@ -1577,7 +1581,7 @@ export default {
handleCreate() { handleCreate() {
this.$router.push({ this.$router.push({
path: '/martial/competition/list', path: '/martial/competition/index',
query: { mode: 'create' } query: { mode: 'create' }
}); });
}, },
@@ -1585,21 +1589,21 @@ export default {
handleView(row) { handleView(row) {
console.log(row) console.log(row)
this.$router.push({ this.$router.push({
path: '/martial/competition/list', path: '/martial/competition/index',
query: { mode: 'view', id: row.id } query: { mode: 'view', id: row.id }
}); });
}, },
handleEdit(row) { handleEdit(row) {
this.$router.push({ this.$router.push({
path: '/martial/competition/list', path: '/martial/competition/index',
query: { mode: 'edit', id: row.id } query: { mode: 'edit', id: row.id }
}); });
}, },
switchToEdit() { switchToEdit() {
this.$router.push({ this.$router.push({
path: '/martial/competition/list', path: '/martial/competition/index',
query: { mode: 'edit', id: this.competitionId } query: { mode: 'edit', id: this.competitionId }
}); });
}, },
@@ -1860,7 +1864,8 @@ export default {
venueCode: venue.venueCode, venueCode: venue.venueCode,
capacity: venue.capacity || 100, capacity: venue.capacity || 100,
location: venue.location || '', location: venue.location || '',
facilities: venue.remark || '' facilities: venue.remark || '',
venueType: venue.venueType || 'indoor'
}; };
// 如果有 id说明是编辑已有的场地 // 如果有 id说明是编辑已有的场地
@@ -1877,7 +1882,7 @@ export default {
backToList() { backToList() {
this.$router.push({ this.$router.push({
path: '/martial/competition/list' path: '/martial/competition/index'
}); });
// 路由跳转后,在 initPage 中会自动加载列表 // 路由跳转后,在 initPage 中会自动加载列表
}, },

View File

@@ -26,7 +26,7 @@
v-for="item in competitionList" v-for="item in competitionList"
:key="item.id" :key="item.id"
:label="item.competitionName" :label="item.competitionName"
:value="item.id" :value="String(item.id)"
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
@@ -43,7 +43,7 @@
v-for="item in projectList" v-for="item in projectList"
:key="item.id" :key="item.id"
:label="item.projectName" :label="item.projectName"
:value="item.id" :value="String(item.id)"
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
@@ -146,14 +146,14 @@
show-overflow-tooltip show-overflow-tooltip
/> />
<el-table-column <el-table-column
prop="deductionPoints" prop="deductionPoint"
label="扣分值(分)" label="扣分值(分)"
width="120" width="120"
align="center" align="center"
> >
<template #default="{ row }"> <template #default="{ row }">
<el-tag type="danger" effect="dark"> <el-tag type="danger" effect="dark">
-{{ row.deductionPoints }} -{{ row.deductionPoint }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
@@ -225,7 +225,7 @@
v-for="item in competitionList" v-for="item in competitionList"
:key="item.id" :key="item.id"
:label="item.competitionName" :label="item.competitionName"
:value="item.id" :value="String(item.id)"
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
@@ -240,7 +240,7 @@
v-for="item in projectList" v-for="item in projectList"
:key="item.id" :key="item.id"
:label="item.projectName" :label="item.projectName"
:value="item.id" :value="String(item.id)"
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
@@ -251,9 +251,9 @@
maxlength="100" maxlength="100"
/> />
</el-form-item> </el-form-item>
<el-form-item label="扣分值(分)" prop="deductionPoints"> <el-form-item label="扣分值(分)" prop="deductionPoint">
<el-input-number <el-input-number
v-model="form.deductionPoints" v-model="form.deductionPoint"
:min="0.1" :min="0.1"
:max="10" :max="10"
:precision="1" :precision="1"
@@ -317,7 +317,7 @@
v-for="item in projectList.filter(p => p.id !== cloneForm.sourceProjectId)" v-for="item in projectList.filter(p => p.id !== cloneForm.sourceProjectId)"
:key="item.id" :key="item.id"
:label="item.projectName" :label="item.projectName"
:value="item.id" :value="String(item.id)"
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
@@ -389,6 +389,7 @@ const queryParams = reactive({
size: 10, size: 10,
competitionId: null, competitionId: null,
projectId: null, projectId: null,
itemName: '',
}) })
// 表单数据 // 表单数据
@@ -396,7 +397,8 @@ const form = reactive({
id: null, id: null,
competitionId: null, competitionId: null,
projectId: null, projectId: null,
deductionPoints: 0.5, itemName: '',
deductionPoint: 0.5,
sortOrder: 0, sortOrder: 0,
description: '' description: ''
}) })
@@ -421,7 +423,7 @@ const rules = {
{ required: true, message: '请输入扣分项名称', trigger: 'blur' }, { required: true, message: '请输入扣分项名称', trigger: 'blur' },
{ min: 2, max: 100, message: '长度在 2 到 100 个字符', trigger: 'blur' } { min: 2, max: 100, message: '长度在 2 到 100 个字符', trigger: 'blur' }
], ],
deductionPoints: [ deductionPoint: [
{ required: true, message: '请输入扣分值', trigger: 'blur' } { required: true, message: '请输入扣分值', trigger: 'blur' }
], ],
sortOrder: [ sortOrder: [
@@ -504,10 +506,17 @@ const fetchData = async () => {
loading.value = true loading.value = true
try { try {
// 过滤掉空字符串的参数
const params = {}
Object.keys(queryParams).forEach(key => {
if (queryParams[key] !== '' && queryParams[key] !== null && queryParams[key] !== undefined) {
params[key] = queryParams[key]
}
})
const res = await getDeductionList( const res = await getDeductionList(
queryParams.current, queryParams.current,
queryParams.size, queryParams.size,
queryParams params
) )
// 根据axios响应拦截器的处理数据在 res.data.data 中 // 根据axios响应拦截器的处理数据在 res.data.data 中
const data = res.data?.data || {} const data = res.data?.data || {}
@@ -535,7 +544,7 @@ const handleReset = () => {
size: 10, size: 10,
competitionId: competitionId, competitionId: competitionId,
projectId: null, projectId: null,
itemName: '' itemName: '',
}) })
if (competitionId) { if (competitionId) {
fetchData() fetchData()
@@ -555,11 +564,22 @@ const handleAdd = () => {
} }
// 编辑 // 编辑
const handleEdit = (row) => { const handleEdit = async (row) => {
dialogTitle.value = '编辑扣分项' dialogTitle.value = '编辑扣分项'
Object.keys(form).forEach((key) => { Object.keys(form).forEach((key) => {
form[key] = row[key] form[key] = row[key]
}) })
// Convert competitionId to string for el-select matching
if (form.competitionId) {
form.competitionId = String(form.competitionId)
}
if (form.projectId) {
form.projectId = String(form.projectId)
}
// Load project list for the competition first
if (form.competitionId) {
await loadProjectList(form.competitionId)
}
dialogVisible.value = true dialogVisible.value = true
} }
@@ -670,7 +690,7 @@ const resetForm = () => {
competitionId: null, competitionId: null,
projectId: null, projectId: null,
itemName: '', itemName: '',
deductionPoints: 0.5, deductionPoint: 0.5,
sortOrder: 0, sortOrder: 0,
description: '' description: ''
}) })

View File

@@ -2,7 +2,7 @@
<div class="martial-order-container"> <div class="martial-order-container">
<el-card shadow="hover"> <el-card shadow="hover">
<div class="page-header"> <div class="page-header">
<h2 class="page-title">订单管理</h2> <h2 class="page-title">赛事管理</h2>
</div> </div>
<el-form :inline="true" :model="searchForm" class="search-form"> <el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item> <el-form-item>
@@ -60,8 +60,8 @@
</el-table-column> </el-table-column>
<el-table-column prop="status" label="状态" width="90" align="center"> <el-table-column prop="status" label="状态" width="90" align="center">
<template #default="scope"> <template #default="scope">
<el-tag :type="getStatusType(scope.row.status)" size="small"> <el-tag :type="getStatusType(calculateStatus(scope.row))" size="small">
{{ getStatusText(scope.row.status) }} {{ getStatusText(calculateStatus(scope.row)) }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
@@ -252,6 +252,19 @@ export default {
return `${start} ~ ${end}` return `${start} ~ ${end}`
}, },
// 根据时间计算赛事状态
calculateStatus(row) {
const now = new Date()
const regStart = row.registrationStartTime ? new Date(row.registrationStartTime) : null
const regEnd = row.registrationEndTime ? new Date(row.registrationEndTime) : null
const compStart = row.competitionStartTime ? new Date(row.competitionStartTime) : null
const compEnd = row.competitionEndTime ? new Date(row.competitionEndTime) : null
if (compEnd && now > compEnd) return 4
if (compStart && now >= compStart) return 3
if (regStart && regEnd && now >= regStart && now <= regEnd) return 2
return 1
},
getStatusType(status) { getStatusType(status) {
const statusMap = { const statusMap = {
1: 'info', // 未开始 1: 'info', // 未开始

View File

@@ -174,6 +174,7 @@
{{ row.maxParticipants || 0 }} {{ row.maxParticipants || 0 }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
prop="createTime" prop="createTime"
label="创建时间" label="创建时间"
@@ -235,7 +236,6 @@
placeholder="请选择赛事" placeholder="请选择赛事"
filterable filterable
style="width: 100%" style="width: 100%"
@change="handleCompetitionChangeInForm"
> >
<el-option <el-option
v-for="item in competitionList" v-for="item in competitionList"
@@ -246,34 +246,6 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12">
<el-form-item label="所属场地" prop="venueId">
<el-select
v-model="form.venueId"
placeholder="请选择场地"
clearable
style="width: 100%"
>
<el-option
v-for="item in venueList"
:key="item.id"
:label="item.venueName"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<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>
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="12"> <el-col :span="12">
@@ -481,7 +453,6 @@ import {
exportProjects exportProjects
} from '@/api/martial/project' } from '@/api/martial/project'
import { getCompetitionList } from '@/api/martial/competition' import { getCompetitionList } from '@/api/martial/competition'
import { getVenuesByCompetition } from '@/api/martial/venue'
import { getToken } from '@/utils/auth' import { getToken } from '@/utils/auth'
import dayjs from 'dayjs' import dayjs from 'dayjs'
@@ -492,7 +463,6 @@ const tableData = ref([])
const total = ref(0) const total = ref(0)
const selection = ref([]) const selection = ref([])
const competitionList = ref([]) const competitionList = ref([])
const venueList = ref([])
const dialogVisible = ref(false) const dialogVisible = ref(false)
const detailVisible = ref(false) const detailVisible = ref(false)
const dialogTitle = ref('') const dialogTitle = ref('')
@@ -514,7 +484,6 @@ const queryParams = reactive({
const form = reactive({ const form = reactive({
id: null, id: null,
competitionId: '', competitionId: '',
venueId: null,
projectCode: '', projectCode: '',
projectName: '', projectName: '',
category: null, category: null,
@@ -543,10 +512,6 @@ const rules = {
competitionId: [ competitionId: [
{ required: true, message: '请选择所属赛事', trigger: 'change' } { required: true, message: '请选择所属赛事', trigger: 'change' }
], ],
projectCode: [
{ required: true, message: '请输入项目编码', trigger: 'blur' },
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
],
projectName: [ projectName: [
{ required: true, message: '请输入项目名称', trigger: 'blur' }, { required: true, message: '请输入项目名称', trigger: 'blur' },
{ min: 2, max: 100, message: '长度在 2 到 100 个字符', trigger: 'blur' } { min: 2, max: 100, message: '长度在 2 到 100 个字符', trigger: 'blur' }
@@ -583,20 +548,6 @@ const loadCompetitionList = async () => {
} }
} }
// 表单中赛事变更时加载场地列表
const handleCompetitionChangeInForm = async (competitionId) => {
form.venueId = null
venueList.value = []
if (competitionId) {
try {
const res = await getVenuesByCompetition(competitionId)
venueList.value = res.data?.data?.records || []
} catch (error) {
console.error('加载场地列表失败:', error)
}
}
}
// 查询数据 // 查询数据
const fetchData = async () => { const fetchData = async () => {
loading.value = true loading.value = true
@@ -663,15 +614,6 @@ const handleEdit = async (row) => {
if (row.price !== undefined) { if (row.price !== undefined) {
form.registrationFee = row.price form.registrationFee = row.price
} }
// 加载该赛事的场地列表
if (row.competitionId) {
try {
const res = await getVenuesByCompetition(row.competitionId)
venueList.value = res.data?.data?.records || []
} catch (error) {
console.error('加载场地列表失败:', error)
}
}
dialogVisible.value = true dialogVisible.value = true
} }

View File

@@ -0,0 +1,931 @@
<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-input
v-model="queryParams.category"
placeholder="请输入分组类别"
clearable
style="width: 150px"
/>
</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 label="参赛类型">
<el-select
v-model="queryParams.type"
placeholder="请选择参赛类型"
clearable
style="width: 150px"
>
<el-option label="单人" :value="1" />
<el-option label="集体" :value="2" />
</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 label="所属赛事" min-width="150" show-overflow-tooltip>
<template #default="{ row }">
{{ getCompetitionName(row.competitionId) }}
</template>
</el-table-column>
<el-table-column prop="category" label="分组类别" width="100" align="center">
<template #default="{ row }">
<span>{{ row.category || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="eventType" label="项目类型" width="100" align="center">
<template #default="{ row }">
<el-tag v-if="row.eventType === 1" type="primary" size="small">套路</el-tag>
<el-tag v-else-if="row.eventType === 2" type="danger" size="small">散打</el-tag>
<el-tag v-else-if="row.eventType === 3" type="success" size="small">器械</el-tag>
<el-tag v-else-if="row.eventType === 4" type="warning" size="small">对练</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="type" label="参赛类型" width="100" align="center">
<template #default="{ row }">
<el-tag v-if="row.type === 1" type="success" size="small">单人</el-tag>
<el-tag v-else-if="row.type === 2" type="warning" size="small">集体</el-tag>
</template>
</el-table-column>
<el-table-column
prop="price"
label="报名费(元)"
width="110"
align="center"
>
<template #default="{ row }">
<span style="color: #f56c6c">¥{{ row.price || 0 }}</span>
</template>
</el-table-column>
<el-table-column prop="estimatedDuration" label="预计时长" width="100" align="center">
<template #default="{ row }">
<span>{{ row.estimatedDuration || 5 }}分钟</span>
</template>
</el-table-column>
<el-table-column label="单位容纳人数" width="120" align="center">
<template #default="{ row }">
{{ row.maxParticipants || 0 }}
</template>
</el-table-column>
<el-table-column label="所属场地" width="120" align="center">
<template #default="{ row }">
<span>{{ getVenueName(row.venueId) }}</span>
</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%"
@change="handleCompetitionChangeInForm"
>
<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="venueId">
<el-select
v-model="form.venueId"
placeholder="请选择场地"
clearable
style="width: 100%"
>
<el-option
v-for="item in venueList"
:key="item.id"
:label="item.venueName"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<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="type">
<el-select
v-model="form.type"
placeholder="请选择参赛类型"
style="width: 100%"
>
<el-option label="单人" :value="1" />
<el-option label="集体" :value="2" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="预计时长(分钟)" prop="estimatedDuration">
<el-input-number
v-model="form.estimatedDuration"
:min="1"
:max="120"
placeholder="每人/队预计比赛时长"
style="width: 100%"
/>
</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="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="所属赛事">
{{ getCompetitionName(detailData.competitionId) }}
</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="参赛类型">
<el-tag v-if="detailData.type === 1" type="success" size="small">单人</el-tag>
<el-tag v-else-if="detailData.type === 2" type="warning" size="small">集体</el-tag>
</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="单位容纳人数">
{{ 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,
submitProject,
removeProject,
importProjects,
exportProjects
} from '@/api/martial/project'
import { getCompetitionList } from '@/api/martial/competition'
import { getVenuesByCompetition } from '@/api/martial/venue'
import { getToken } from '@/utils/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 venueList = ref([])
const allVenuesCache = ref(new Map()) // 全局场地缓存
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: '',
type: ''
})
// 表单数据
const form = reactive({
id: null,
competitionId: '',
venueId: null,
projectCode: '',
projectName: '',
category: null,
eventType: null,
type: null,
estimatedDuration: 5,
registrationFee: 0,
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' }
],
estimatedDuration: [
{ required: true, message: '请输入预计时长', trigger: 'blur' }
],
type: [
{ required: true, message: '请选择参赛类型', trigger: 'change' }
],
registrationFee: [
{ required: true, message: '请输入报名费', trigger: 'blur' }
],
maxParticipants: [
{ required: true, message: '请输入单位容纳人数', trigger: 'blur' }
]
}
// 加载赛事列表
const loadCompetitionList = async () => {
try {
const res = await getCompetitionList(1, 1000, {})
if (res.data && res.data.data && res.data.data.records) {
competitionList.value = res.data.data.records
}
} catch (error) {
console.error('加载赛事列表失败:', error)
}
}
// 表单中赛事变更时加载场地列表
const handleCompetitionChangeInForm = async (competitionId) => {
form.venueId = null
venueList.value = []
if (competitionId) {
try {
const res = await getVenuesByCompetition(competitionId)
venueList.value = res.data?.data?.records || []
} catch (error) {
console.error('加载场地列表失败:', error)
}
}
}
// 查询数据
const fetchData = async () => {
loading.value = true
try {
// Only pass parameters that backend supports
const params = {
competitionId: queryParams.competitionId || undefined,
projectName: queryParams.projectName || undefined,
category: queryParams.category || undefined,
eventType: queryParams.eventType || undefined,
type: queryParams.type || undefined
}
const res = await getProjectList(
queryParams.current,
queryParams.size,
params
)
if (res.data && res.data.data) {
tableData.value = res.data.data.records || []
total.value = res.data.data.total || 0
// 加载项目对应的场地信息
await loadVenuesForProjects(tableData.value)
}
} catch (error) {
ElMessage.error('获取数据失败')
console.error(error)
} finally {
loading.value = false
}
}
// 加载项目对应的场地信息
const loadVenuesForProjects = async (projects) => {
// 获取所有不同的赛事ID
const competitionIds = [...new Set(projects.map(p => p.competitionId).filter(Boolean))]
for (const compId of competitionIds) {
try {
const res = await getVenuesByCompetition(compId)
const venues = res.data?.data?.records || []
// 缓存场地信息
venues.forEach(v => {
allVenuesCache.value.set(v.id, v.venueName)
})
} catch (err) {
console.error('加载场地失败:', err)
}
}
}
// 搜索
const handleSearch = () => {
queryParams.current = 1
fetchData()
}
// 重置
const handleReset = () => {
Object.assign(queryParams, {
current: 1,
size: 10,
competitionId: '',
projectName: '',
category: '',
eventType: '',
type: ''
})
fetchData()
}
// 新增
const handleAdd = () => {
dialogTitle.value = '新增项目'
resetForm()
dialogVisible.value = true
}
// 编辑
const handleEdit = async (row) => {
dialogTitle.value = '编辑项目'
Object.keys(form).forEach((key) => {
form[key] = row[key]
})
// 处理字段名映射:后端返回 price表单使用 registrationFee
if (row.price !== undefined) {
form.registrationFee = row.price
}
// 加载该赛事的场地列表
if (row.competitionId) {
try {
const res = await getVenuesByCompetition(row.competitionId)
venueList.value = res.data?.data?.records || []
} catch (error) {
console.error('加载场地列表失败:', error)
}
}
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 {
// 构建提交数据,确保字段名与后端一致
const submitData = {
...form,
price: form.registrationFee // 后端使用 price 字段
}
if (form.id) {
await submitProject(submitData)
ElMessage.success('修改成功')
} else {
await submitProject(submitData)
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,
type: null,
estimatedDuration: 5,
registrationFee: 0,
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('导出失败')
}
}
// 根据ID获取赛事名称
const getCompetitionName = (competitionId) => {
if (!competitionId) return '-'
const competition = competitionList.value.find(item => item.id === competitionId)
return competition ? competition.competitionName : '-'
}
const getVenueName = (venueId) => {
if (!venueId) return '-'
// 先从当前场地列表查找
let venue = venueList.value.find(item => item.id === venueId)
if (venue) return venue.venueName
// 再从全局缓存查找
if (allVenuesCache.value.has(venueId)) {
return allVenuesCache.value.get(venueId)
}
return '-'
}
// 格式化日期
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

@@ -123,10 +123,11 @@
<div v-if="scope.row.hint" class="row-hint">{{ scope.row.hint }}</div> <div v-if="scope.row.hint" class="row-hint">{{ scope.row.hint }}</div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="participantCategory" label="人/单位" width="100"></el-table-column> <el-table-column prop="projectType" label="人/集体" width="100" align="center"></el-table-column>
<el-table-column prop="teamCount" label="队伍" width="80" align="center"></el-table-column> <el-table-column prop="athleteCount" label="人数/集体" width="100" align="center"></el-table-column>
<el-table-column prop="singleTeamPeople" label="报名人数" width="120" align="center"></el-table-column> <el-table-column prop="groupCount" label="数" width="80" align="center"></el-table-column>
<el-table-column prop="estimatedDuration" label="预计时长(分)" width="150" align="center"></el-table-column> <el-table-column prop="estimatedDuration" label="时长(分)" width="100" align="center"></el-table-column>
<el-table-column prop="projectCode" label="项目编码" width="120" align="center"></el-table-column>
</el-table> </el-table>
</div> </div>
@@ -387,39 +388,31 @@ export default {
// 使用缓存的参赛者列表 // 使用缓存的参赛者列表
const participants = await this.getParticipants() const participants = await this.getParticipants()
// 2. 按项目ID分组 // 预加载项目信息
const projectMap = new Map() await this.preloadProjectInfo(participants)
// 按项目ID分组统计人数
const projectAthleteCount = new Map()
participants.forEach(athlete => { participants.forEach(athlete => {
// 兼容驼峰和下划线命名
const projectId = athlete.projectId || athlete.project_id const projectId = athlete.projectId || athlete.project_id
if (projectId) { if (projectId) {
if (!projectMap.has(projectId)) { projectAthleteCount.set(projectId, (projectAthleteCount.get(projectId) || 0) + 1)
projectMap.set(projectId, [])
}
projectMap.get(projectId).push(athlete)
} }
}) })
// 3. 从缓存中获取项目信息并统计(项目信息已经在 loadRegistrationStats 中预加载) // 从缓存中获取项目信息并构建统计数据
const projectStats = [] const projectStats = []
for (const [projectId, athleteList] of projectMap) { for (const [projectId, count] of projectAthleteCount) {
const project = this.projectCache.get(projectId) const project = this.projectCache.get(projectId)
if (project) { if (project) {
const projectType = project.type || 1 // 1=单人, 2=集体
projectStats.push({ projectStats.push({
projectName: project.projectName || project.project_name || '未知项目', projectName: project.projectName || project.project_name || '未知项目',
participantCategory: project.category || '', projectType: projectType === 1 ? '单人' : '集体',
teamCount: 1, // 简化处理设为1 athleteCount: count,
singleTeamPeople: athleteList.length, groupCount: projectType === 2 ? count : '-', // 集体项目显示组数,单人显示-
estimatedDuration: project.estimatedDuration || project.estimated_duration || 0 estimatedDuration: project.estimatedDuration || project.estimated_duration || 0,
}) projectCode: project.projectCode || project.project_code || ''
} else {
// 如果缓存中没有理论上<E8AEBA><E4B88A><EFBFBD>应该发生添加基本信息
projectStats.push({
projectName: `项目ID:${projectId}`,
participantCategory: '',
teamCount: 1,
singleTeamPeople: athleteList.length,
estimatedDuration: 0
}) })
} }
} }

View File

@@ -87,6 +87,7 @@
<span class="project-meta">{{ getTeamCount(group) }}</span> <span class="project-meta">{{ getTeamCount(group) }}</span>
<span class="project-meta">{{ group.items?.length || 0 }}</span> <span class="project-meta">{{ group.items?.length || 0 }}</span>
<span class="project-meta">{{ group.code }}</span> <span class="project-meta">{{ group.code }}</span>
<span class="project-table-no">表号: {{ generateTableNo(group) }}</span>
</div> </div>
<div class="project-actions" @click.stop> <div class="project-actions" @click.stop>
<el-popover <el-popover
@@ -276,7 +277,15 @@
<div class="footer-actions"> <div class="footer-actions">
<el-button size="small" @click="handleSaveDraft" v-if="!isScheduleCompleted">保存草稿</el-button> <el-button size="small" @click="handleSaveDraft" v-if="!isScheduleCompleted">保存草稿</el-button>
<el-button size="small" @click="handleExport" v-if="isScheduleCompleted">导出</el-button> <el-dropdown v-if="isScheduleCompleted" @command="handleExportCommand" trigger="click">
<el-button size="small">导出 <i class="el-icon-arrow-down el-icon--right"></i></el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="template1">模板1 - 详细赛程表</el-dropdown-item>
<el-dropdown-item command="template2">模板2 - 比赛时间表</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button size="small" type="primary" @click="handleConfirm" v-if="!isScheduleCompleted">完成编排</el-button> <el-button size="small" type="primary" @click="handleConfirm" v-if="!isScheduleCompleted">完成编排</el-button>
</div> </div>
@@ -380,7 +389,7 @@
import { ArrowDown, ArrowRight } from '@element-plus/icons-vue' import { ArrowDown, ArrowRight } from '@element-plus/icons-vue'
import { getVenuesByCompetition } from '@/api/martial/venue' import { getVenuesByCompetition } from '@/api/martial/venue'
import { getCompetitionDetail } from '@/api/martial/competition' import { getCompetitionDetail } from '@/api/martial/competition'
import { getScheduleResult, saveAndLockSchedule, saveDraftSchedule, triggerAutoArrange, moveScheduleGroup, exportSchedule } from '@/api/martial/activitySchedule' import { getScheduleResult, saveAndLockSchedule, saveDraftSchedule, triggerAutoArrange, moveScheduleGroup, exportSchedule, exportScheduleTemplate2 } from '@/api/martial/activitySchedule'
import { updateCheckInStatus, getScheduleConfig } from '@/api/martial/schedulePlan' import { updateCheckInStatus, getScheduleConfig } from '@/api/martial/schedulePlan'
export default { export default {
@@ -496,6 +505,44 @@ export default {
} }
}, },
methods: { methods: {
// 生成表号: 场地(1位) + 时段(1位,上午=1/下午=2) + 序号(2位)
generateTableNo(group) {
// 1. 获取场地编号
let venueNo = 1
if (group.venueId) {
const venue = this.venues.find(v => v.id === group.venueId || String(v.id) === String(group.venueId))
if (venue && venue.venueName) {
// 从场地名称提取数字
const match = venue.venueName.match(/\d+/)
if (match) {
venueNo = parseInt(match[0])
}
}
}
// 2. 获取时段:上午=1, 下午=2
let period = 1
if (group.timeSlot) {
const hour = parseInt(group.timeSlot.split(':')[0])
period = hour < 12 ? 1 : 2
}
// 3. 获取序号:在同场地同时段中的顺序
const sameSlotGroups = this.competitionGroups.filter(g => {
const gVenueMatch = String(g.venueId) === String(group.venueId)
if (!gVenueMatch) return false
const gHour = parseInt((g.timeSlot || '08:30').split(':')[0])
const gPeriod = gHour < 12 ? 1 : 2
return gPeriod === period
})
// 按id排序保持稳定顺序
sameSlotGroups.sort((a, b) => (a.id || 0) - (b.id || 0))
const orderIndex = sameSlotGroups.findIndex(g => g.id === group.id) + 1
// 4. 格式化: 场地(1位) + 时段(1位) + 序号(2位)
return `${venueNo}${period}${String(orderIndex).padStart(2, '0')}`
},
// 检查项目是否展开 // 检查项目是否展开
isProjectExpanded(groupId) { isProjectExpanded(groupId) {
return this.expandedProjects[groupId] === true return this.expandedProjects[groupId] === true
@@ -1148,6 +1195,34 @@ export default {
group.items.splice(itemIndex + 1, 0, temp) group.items.splice(itemIndex + 1, 0, temp)
this.$message.success('下移成功') this.$message.success('下移成功')
}, },
handleExportCommand(command) {
if (command === 'template1') {
this.handleExport()
} else if (command === 'template2') {
this.handleExportTemplate2()
}
},
async handleExportTemplate2() {
try {
this.loading = true
const venueId = this.selectedVenueId
const venue = this.venues.find(v => v.id === venueId)
const venueName = venue ? venue.venueName : null
const res = await exportScheduleTemplate2(this.competitionId, venueId, venueName, null)
const blob = new Blob([res.data], { type: 'application/vnd.ms-excel' })
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = `比赛时间_${venueName || '全部场地'}_${this.competitionInfo.competitionName || this.competitionId}.xlsx`
link.click()
window.URL.revokeObjectURL(link.href)
this.$message.success('导出成功')
} catch (error) {
console.error('导出失败:', error)
this.$message.error('导出失败,请稍后重试')
} finally {
this.loading = false
}
},
async handleExport() { async handleExport() {
try { try {
this.loading = true this.loading = true
@@ -1421,6 +1496,16 @@ export default {
color: #606266; color: #606266;
font-size: 13px; font-size: 13px;
} }
.project-table-no {
color: #409EFF;
font-size: 13px;
font-weight: 500;
margin-left: 10px;
padding: 2px 8px;
background-color: #ecf5ff;
border-radius: 4px;
}
} }
.project-actions { .project-actions {

View File

@@ -94,7 +94,8 @@
<el-table-column label="总裁判分数" width="120" align="center" fixed="right"> <el-table-column label="总裁判分数" width="120" align="center" fixed="right">
<template #default="scope"> <template #default="scope">
<span class="total-score">{{ formatScore(scope.row.totalScore) }}</span> <span v-if="scope.row.scoreStatus === 2" class="total-score">{{ formatScore(scope.row.chiefJudgeScore) }}</span>
<span v-else class="pending-score">待确认</span>
</template> </template>
</el-table-column> </el-table-column>
@@ -153,12 +154,17 @@
<div class="total-score-display"> <div class="total-score-display">
<span class="label">总分</span> <span class="label">总分</span>
<span class="value">{{ formatScore(currentDetail.totalScore) }}</span> <template v-if="currentDetail.scoreStatus === 2">
<span class="value">{{ formatScore(currentDetail.chiefJudgeScore) }}</span>
<div class="calculation-note">(主裁判已确认)</div>
</template>
<template v-else>
<span class="value pending">待确认最终得分</span>
<div class="calculation-note"> <div class="calculation-note">
<span v-if="currentDetail.judgeScores.length > 2"> 裁判员评分: {{ formatScore(currentDetail.totalScore) }}
(去掉最高分和最低分后平均) <span v-if="currentDetail.judgeScores.length > 2">(去掉最高最低分后平均)</span>
</span>
</div> </div>
</template>
</div> </div>
</div> </div>
</div> </div>
@@ -398,7 +404,9 @@ export default {
playerNo: score.playerNo || '', playerNo: score.playerNo || '',
judgeScores: [], judgeScores: [],
scoreDetails: [], scoreDetails: [],
totalScore: 0 totalScore: 0,
chiefJudgeScore: score.chiefJudgeScore,
scoreStatus: score.scoreStatus || 0
}) })
} }
@@ -565,6 +573,16 @@ export default {
} }
.total-score { .total-score {
}
.pending-score {
color: #e6a23c;
font-weight: 500;
}
.value.pending {
color: #e6a23c;
font-weight: 500;
color: #1b7c5e; color: #1b7c5e;
font-weight: 700; font-weight: 700;
font-size: 14px; font-size: 14px;

View File

@@ -59,7 +59,7 @@
<i class="el-icon-s-order"></i> <i class="el-icon-s-order"></i>
</div> </div>
<div class="card-info"> <div class="card-info">
<h3>订单管理</h3> <h3>赛事管理</h3>
<p>查看和管理订单</p> <p>查看和管理订单</p>
</div> </div>
<i class="card-arrow el-icon-arrow-right"></i> <i class="card-arrow el-icon-arrow-right"></i>