Merge branch 'main' of git.waypeak.work:martial/martial-web

This commit is contained in:
2025-12-26 11:06:52 +08:00
10 changed files with 3121 additions and 211 deletions

46
package-lock.json generated
View File

@@ -851,7 +851,6 @@
"version": "4.17.12",
"resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz",
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
"peer": true,
"dependencies": {
"@types/lodash": "*"
}
@@ -874,7 +873,6 @@
"version": "2.3.4",
"resolved": "https://registry.npmmirror.com/@uppy/core/-/core-2.3.4.tgz",
"integrity": "sha512-iWAqppC8FD8mMVqewavCz+TNaet6HPXitmGXpGGREGrakZ4FeuWytVdrelydzTdXx6vVKkOmI2FLztGg73sENQ==",
"peer": true,
"dependencies": {
"@transloadit/prettier-bytes": "0.0.7",
"@uppy/store-default": "^2.1.1",
@@ -903,7 +901,6 @@
"version": "2.1.3",
"resolved": "https://registry.npmmirror.com/@uppy/xhr-upload/-/xhr-upload-2.1.3.tgz",
"integrity": "sha512-YWOQ6myBVPs+mhNjfdWsQyMRWUlrDLMoaG7nvf/G6Y3GKZf8AyjFDjvvJ49XWQ+DaZOftGkHmF1uh/DBeGivJQ==",
"peer": true,
"dependencies": {
"@uppy/companion-client": "^2.2.2",
"@uppy/utils": "^4.1.2",
@@ -1058,7 +1055,6 @@
"version": "1.1.7",
"resolved": "https://registry.npmmirror.com/@wangeditor/basic-modules/-/basic-modules-1.1.7.tgz",
"integrity": "sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg==",
"peer": true,
"dependencies": {
"is-url": "^1.2.4"
},
@@ -1089,7 +1085,6 @@
"version": "1.1.19",
"resolved": "https://registry.npmmirror.com/@wangeditor/core/-/core-1.1.19.tgz",
"integrity": "sha512-KevkB47+7GhVszyYF2pKGKtCSj/YzmClsD03C3zTt+9SR2XWT5T0e3yQqg8baZpcMvkjs1D8Dv4fk8ok/UaS2Q==",
"peer": true,
"dependencies": {
"@types/event-emitter": "^0.3.3",
"event-emitter": "^0.3.5",
@@ -1119,7 +1114,6 @@
"version": "5.1.23",
"resolved": "https://registry.npmmirror.com/@wangeditor/editor/-/editor-5.1.23.tgz",
"integrity": "sha512-0RxfeVTuK1tktUaPROnCoFfaHVJpRAIE2zdS0mpP+vq1axVQpLjM8+fCvKzqYIkH0Pg+C+44hJpe3VVroSkEuQ==",
"peer": true,
"dependencies": {
"@uppy/core": "^2.1.1",
"@uppy/xhr-upload": "^2.0.3",
@@ -1476,7 +1470,6 @@
"version": "11.13.1",
"resolved": "https://registry.npmmirror.com/diagram-js/-/diagram-js-11.13.1.tgz",
"integrity": "sha512-6kO0rBN6aBIQiMELfv1oX2Ohes/brlIPuOVZUYAioeWM0EyuazhAXgHeq8iKFt29daU9NGRr4n78esGx8QjtjQ==",
"peer": true,
"dependencies": {
"@bpmn-io/diagram-js-ui": "^0.2.2",
"clsx": "^1.2.1",
@@ -1514,7 +1507,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/dom7/-/dom7-3.0.0.tgz",
"integrity": "sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==",
"peer": true,
"dependencies": {
"ssr-window": "^3.0.0-alpha.1"
}
@@ -1540,7 +1532,6 @@
"version": "2.7.3",
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.7.3.tgz",
"integrity": "sha512-OaqY1kQ2xzNyRFyge3fzM7jqMwux+464RBEqd+ybRV9xPiGxtgnj/sVK4iEbnKnzQIa9XK03DOIFzoToUhu1DA==",
"peer": true,
"dependencies": {
"@ctrl/tinycolor": "^3.4.1",
"@element-plus/icons-vue": "^2.3.1",
@@ -1866,8 +1857,7 @@
"node_modules/is-hotkey": {
"version": "0.2.0",
"resolved": "https://registry.npmmirror.com/is-hotkey/-/is-hotkey-0.2.0.tgz",
"integrity": "sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==",
"peer": true
"integrity": "sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw=="
},
"node_modules/is-number": {
"version": "7.0.0",
@@ -1942,14 +1932,12 @@
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"peer": true
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"peer": true
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
},
"node_modules/lodash-unified": {
"version": "1.0.3",
@@ -1964,44 +1952,37 @@
"node_modules/lodash.camelcase": {
"version": "4.3.0",
"resolved": "https://registry.npmmirror.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
"peer": true
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="
},
"node_modules/lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmmirror.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
"peer": true
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="
},
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
"peer": true
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="
},
"node_modules/lodash.foreach": {
"version": "4.5.0",
"resolved": "https://registry.npmmirror.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz",
"integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==",
"peer": true
"integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ=="
},
"node_modules/lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmmirror.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
"peer": true
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
},
"node_modules/lodash.throttle": {
"version": "4.1.1",
"resolved": "https://registry.npmmirror.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
"integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==",
"peer": true
"integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ=="
},
"node_modules/lodash.toarray": {
"version": "4.4.0",
"resolved": "https://registry.npmmirror.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz",
"integrity": "sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw==",
"peer": true
"integrity": "sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw=="
},
"node_modules/magic-string": {
"version": "0.30.10",
@@ -2116,7 +2097,6 @@
"url": "https://github.com/sponsors/ai"
}
],
"peer": true,
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@@ -2308,7 +2288,6 @@
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz",
"integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==",
"dev": true,
"peer": true,
"dependencies": {
"@types/estree": "1.0.5"
},
@@ -2367,7 +2346,6 @@
"resolved": "https://registry.npmjs.org/sass/-/sass-1.77.2.tgz",
"integrity": "sha512-eb4GZt1C3avsX3heBNlrc7I09nyT00IUuo4eFhAbeXWU2fvA7oXI53SxODVAA+zgZCk9aunAZgO+losjR3fAwA==",
"dev": true,
"peer": true,
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",
@@ -2403,7 +2381,6 @@
"version": "0.72.8",
"resolved": "https://registry.npmmirror.com/slate/-/slate-0.72.8.tgz",
"integrity": "sha512-/nJwTswQgnRurpK+bGJFH1oM7naD5qDmHd89JyiKNT2oOKD8marW0QSBtuFnwEbL5aGCS8AmrhXQgNOsn4osAw==",
"peer": true,
"dependencies": {
"immer": "^9.0.6",
"is-plain-object": "^5.0.0",
@@ -2425,7 +2402,6 @@
"version": "3.5.1",
"resolved": "https://registry.npmmirror.com/snabbdom/-/snabbdom-3.5.1.tgz",
"integrity": "sha512-wHMNIOjkm/YNE5EM3RCbr/+DVgPg6AqQAX1eOxO46zYNvCXjKP5Y865tqQj3EXnaMBjkxmQA5jFuDpDK/dbfiA==",
"peer": true,
"engines": {
"node": ">=8.3.0"
}
@@ -2623,7 +2599,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.12.tgz",
"integrity": "sha512-/gC8GxzxMK5ntBwb48pR32GGhENnjtY30G4A0jemunsBkiEZFw60s8InGpN8gkhHEkjnRK1aSAxeQgwvFhUHAA==",
"dev": true,
"peer": true,
"dependencies": {
"esbuild": "^0.20.1",
"postcss": "^8.4.38",
@@ -2791,7 +2766,6 @@
"version": "3.4.27",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.4.27.tgz",
"integrity": "sha512-8s/56uK6r01r1icG/aEOHqyMVxd1bkYcSe9j8HcKtr/xTOFWvnzIVTehNW+5Yt89f+DLBe4A569pnZLS5HzAMA==",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.4.27",
"@vue/compiler-sfc": "3.4.27",

View File

@@ -183,3 +183,16 @@ export const saveDispatch = (data) => {
data
})
}
/**
* 导出赛程表
* @param {Number} competitionId - 赛事ID
*/
export const exportSchedule = (competitionId) => {
return request({
url: '/martial/export/schedule',
method: 'get',
params: { competitionId },
responseType: 'blob'
})
}

View File

@@ -98,6 +98,10 @@ axios.interceptors.request.use(
axios.interceptors.response.use(
res => {
NProgress.done();
// 如果是 blob 类型响应(文件下载),直接返回
if (res.config.responseType === 'blob') {
return res;
}
const status = res.data.code || res.status;
const statusWhiteList = website.statusWhiteList || [];
const message = res.data.msg || res.data.error_description || '系统错误';

View File

@@ -181,13 +181,13 @@ export default {
try {
const res = await getScheduleResult(competition.id)
if (res.data?.data) {
this.$set(this.scheduleStatusMap, competition.id, res.data.data.isCompleted || false)
this.scheduleStatusMap[competition.id] = res.data.data.isCompleted || false
} else {
this.$set(this.scheduleStatusMap, competition.id, false)
this.scheduleStatusMap[competition.id] = false
}
} catch (err) {
// 如果获取失败,默认为未完成
this.$set(this.scheduleStatusMap, competition.id, false)
this.scheduleStatusMap[competition.id] = false
}
}
},

View File

@@ -0,0 +1,355 @@
<template>
<div class="martial-order-container">
<el-card shadow="hover">
<div class="page-header">
<h2 class="page-title">订单管理</h2>
</div>
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item>
<el-input
v-model="searchForm.keyword"
placeholder="搜索赛事名称"
clearable
size="small"
style="width: 240px"
>
<i slot="prefix" class="el-input__icon el-icon-search"></i>
</el-input>
</el-form-item>
<el-form-item>
<el-select
v-model="searchForm.status"
placeholder="赛事状态"
clearable
size="small"
style="width: 180px"
>
<el-option label="未开始" :value="1"></el-option>
<el-option label="报名中" :value="2"></el-option>
<el-option label="进行中" :value="3"></el-option>
<el-option label="已结束" :value="4"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" size="small" @click="handleSearch">查询</el-button>
</el-form-item>
</el-form>
<el-table
v-loading="loading"
:data="tableData"
border
stripe
size="small"
style="width: 100%"
>
<el-table-column type="index" label="序号" width="60" align="center"></el-table-column>
<el-table-column prop="competitionName" label="赛事名称" min-width="200" show-overflow-tooltip></el-table-column>
<el-table-column prop="competitionCode" label="赛事编号" width="150"></el-table-column>
<el-table-column prop="organizer" label="主办单位" min-width="150" show-overflow-tooltip></el-table-column>
<el-table-column prop="location" label="举办地点" width="120"></el-table-column>
<el-table-column prop="registrationTime" label="报名时间" width="180" show-overflow-tooltip>
<template #default="scope">
<span>{{ formatDateRange(scope.row.registrationStartTime, scope.row.registrationEndTime) }}</span>
</template>
</el-table-column>
<el-table-column prop="competitionTime" label="比赛时间" width="180" show-overflow-tooltip>
<template #default="scope">
<span>{{ formatDateRange(scope.row.competitionStartTime, scope.row.competitionEndTime) }}</span>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="90" align="center">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.status)" size="small">
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="320" align="center" fixed="right">
<template #default="scope">
<el-button type="primary" size="small" @click="handleRegistrationDetail(scope.row)">报名详情</el-button>
<el-button type="success" size="small" @click="handleSchedule(scope.row)">编排</el-button>
<el-button
type="warning"
size="small"
@click="handleDispatch(scope.row)"
:title="isScheduleCompleted(scope.row.id) ? '进入调度' : '请先完成编排'"
>
调度
</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
class="pagination"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pagination.current"
:page-sizes="[10, 20, 50, 100]"
:page-size="pagination.size"
layout="total, sizes, prev, pager, next"
:total="pagination.total"
small
></el-pagination>
</el-card>
</div>
</template>
<script>
import { getCompetitionList } from '@/api/martial/competition'
import { getScheduleResult } from '@/api/martial/activitySchedule'
export default {
name: 'MartialOrderList',
data() {
return {
loading: false,
searchForm: {
keyword: '',
status: null
},
tableData: [],
pagination: {
current: 1,
size: 10,
total: 0
},
scheduleStatusMap: {} // 存储每个赛事的编排状态
}
},
mounted() {
this.loadCompetitionList()
},
activated() {
// 当页面被激活时(从其他页面返回),重新加载编排状态
if (this.tableData.length > 0) {
this.loadScheduleStatus()
}
},
methods: {
// 加载赛事列表
loadCompetitionList() {
this.loading = true
const params = {}
if (this.searchForm.keyword) {
params.competitionName = this.searchForm.keyword
}
if (this.searchForm.status !== null && this.searchForm.status !== '') {
params.status = this.searchForm.status
}
getCompetitionList(this.pagination.current, this.pagination.size, params)
.then(res => {
console.log('赛事列表返回数据:', res)
const responseData = res.data?.data
if (responseData && responseData.records) {
// 处理赛事数据,兼容驼峰和下划线命名
this.tableData = responseData.records.map(competition => ({
id: competition.id,
competitionName: competition.competitionName || competition.competition_name,
competitionCode: competition.competitionCode || competition.competition_code,
organizer: competition.organizer,
location: competition.location,
venue: competition.venue,
registrationStartTime: competition.registrationStartTime || competition.registration_start_time,
registrationEndTime: competition.registrationEndTime || competition.registration_end_time,
competitionStartTime: competition.competitionStartTime || competition.competition_start_time,
competitionEndTime: competition.competitionEndTime || competition.competition_end_time,
status: competition.status,
createTime: competition.createTime || competition.create_time
}))
this.pagination.total = responseData.total || 0
// 加载每个赛事的编排状态
this.loadScheduleStatus()
}
})
.catch(err => {
console.error('加载赛事列表失败', err)
this.$message.error('加载赛事列表失败')
})
.finally(() => {
this.loading = false
})
},
// 加载编排状态
async loadScheduleStatus() {
for (const competition of this.tableData) {
try {
const res = await getScheduleResult(competition.id)
if (res.data?.data) {
this.$set(this.scheduleStatusMap, competition.id, res.data.data.isCompleted || false)
} else {
this.$set(this.scheduleStatusMap, competition.id, false)
}
} catch (err) {
// 如果获取失败,默认为未完成
this.$set(this.scheduleStatusMap, competition.id, false)
}
}
},
// 检查编排是否完成
isScheduleCompleted(competitionId) {
return this.scheduleStatusMap[competitionId] === true
},
handleSearch() {
this.pagination.current = 1
this.loadCompetitionList()
},
handleSizeChange(size) {
this.pagination.size = size
this.pagination.current = 1
this.loadCompetitionList()
},
handleCurrentChange(current) {
this.pagination.current = current
this.loadCompetitionList()
},
// 查看报名详情 - 传递赛事ID
handleRegistrationDetail(row) {
this.$router.push({
path: '/martial/registration/detail',
query: { competitionId: row.id }
})
},
// 编排 - 传递赛事ID
handleSchedule(row) {
this.$router.push({
path: '/martial/schedule/list',
query: { competitionId: row.id }
})
},
// 调度 - 传递赛事ID
handleDispatch(row) {
// 检查编排是否完成
if (!this.isScheduleCompleted(row.id)) {
this.$message.warning('请先完成编排后再进行调度')
return
}
this.$router.push({
path: '/martial/dispatch/list',
query: { competitionId: row.id }
})
},
// 格式化日期范围
formatDateRange(startTime, endTime) {
if (!startTime || !endTime) return '-'
// 简单格式化,只显示日期部分
const start = startTime.split(' ')[0]
const end = endTime.split(' ')[0]
return `${start} ~ ${end}`
},
getStatusType(status) {
const statusMap = {
1: 'info', // 未开始
2: 'success', // 报名中
3: 'warning', // 进行中
4: 'info' // 已结束
}
return statusMap[status] || 'info'
},
getStatusText(status) {
const statusMap = {
1: '未开始',
2: '报名中',
3: '进行中',
4: '已结束'
}
return statusMap[status] || '未知'
}
}
}
</script>
<style lang="scss" scoped>
.martial-order-container {
min-height: 100%;
padding: 15px;
background: #fff;
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
.page-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #2c3e50;
}
}
.tip-message {
display: flex;
gap: 12px;
padding: 10px 14px;
margin-bottom: 15px;
background: linear-gradient(90deg, #ffd54f 0%, #ffecb3 100%);
border: 1px solid #ffc107;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(255, 193, 7, 0.2);
.tip-header {
flex-shrink: 0;
.tip-number {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
background: #ff9800;
color: #fff;
font-size: 11px;
font-weight: bold;
border-radius: 50%;
}
}
.tip-content {
flex: 1;
color: #5d4037;
font-size: 12px;
line-height: 1.5;
.tip-subtitle {
margin-top: 3px;
color: #6d4c41;
font-size: 11px;
font-style: italic;
}
}
}
.search-form {
margin-bottom: 15px;
}
.amount-text {
color: #e6a23c;
font-weight: 600;
font-size: 14px;
}
.pagination {
margin-top: 15px;
text-align: right;
}
}
</style>

View File

@@ -94,11 +94,14 @@
</el-table-column>
<el-table-column
prop="age"
label="年龄"
width="80"
align="center"
/>
>
<template #default="scope">
{{ scope.row.age === -1 || scope.row.age === null || scope.row.age === undefined ? '--' : scope.row.age }}
</template>
</el-table-column>
<el-table-column
prop="organization"

View File

@@ -0,0 +1,990 @@
<template>
<div class="participant-container">
<!-- 列表视图 -->
<div v-if="currentView === 'list'" class="list-view">
<el-card shadow="hover">
<div class="list-header">
<h2 class="page-title">参赛选手管理</h2>
<el-button type="primary" icon="el-icon-plus" @click="handleCreate">
添加选手
</el-button>
</div>
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item>
<el-input
v-model="searchForm.keyword"
placeholder="搜索选手姓名"
clearable
size="small"
style="width: 240px"
>
<template #prefix>
<i class="el-input__icon el-icon-search"></i>
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-select v-model="searchForm.competitionId" placeholder="选择赛事" clearable size="small" style="width: 200px">
<el-option label="全部赛事" :value="null" />
<el-option
v-for="item in allCompetitionOptions"
:key="item.id"
:label="item.competitionName"
:value="item.id"
/>
</el-select>
</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
v-loading="loading"
:data="displayList"
border
stripe
style="width: 100%"
class="data-table"
>
<el-table-column
type="index"
label="序号"
width="60"
align="center"
/>
<el-table-column
prop="playerName"
label="选手姓名"
width="120"
/>
<el-table-column
prop="competitionName"
label="所属赛事"
min-width="180"
show-overflow-tooltip
/>
<el-table-column
prop="projectName"
label="参赛项目"
width="120"
/>
<el-table-column
prop="category"
label="组别"
width="100"
/>
<el-table-column
label="性别"
width="80"
align="center"
>
<template #default="scope">
<el-tag :type="scope.row.gender === 1 ? 'primary' : 'danger'" size="small">
{{ scope.row.gender === 1 ? '男' : '女' }}
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="age"
label="年龄"
width="80"
align="center"
/>
<el-table-column
prop="organization"
label="所属单位"
min-width="150"
show-overflow-tooltip
/>
<el-table-column
prop="contactPhone"
label="联系电话"
width="120"
/>
<el-table-column
prop="orderNum"
label="出场顺序"
width="100"
align="center"
/>
<el-table-column
label="操作"
width="220"
fixed="right"
align="center"
>
<template #default="scope">
<el-button
type="primary"
link
size="small"
icon="el-icon-view"
@click="handleView(scope.row)"
>
查看
</el-button>
<el-button
type="warning"
link
size="small"
icon="el-icon-edit"
@click="handleEdit(scope.row)"
>
编辑
</el-button>
<el-button
type="danger"
link
size="small"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-if="pagination.total > 0"
@size-change="handleSizeChange"
@current-change="handlePageChange"
:current-page="pagination.current"
:page-sizes="[10, 20, 50, 100]"
:page-size="pagination.size"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
style="margin-top: 20px; text-align: right"
/>
</el-card>
</div>
<!-- 表单视图 -->
<div v-else class="form-view">
<el-card shadow="hover" v-loading="loading">
<div class="page-header">
<el-button
icon="el-icon-arrow-left"
@click="backToList"
>
返回列表
</el-button>
<h2 class="page-title">{{ pageTitle }}</h2>
<div class="header-actions" v-if="currentView !== 'view'">
<el-button @click="backToList">取消</el-button>
<el-button type="primary" @click="handleSave">
{{ currentView === 'create' ? '创建' : '保存' }}
</el-button>
</div>
<div class="header-actions" v-else>
<el-button type="primary" @click="switchToEdit">编辑</el-button>
</div>
</div>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="120px"
:disabled="currentView === '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 availableCompetitionOptions"
: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="projectId">
<el-select
v-model="formData.projectId"
placeholder="请选择参赛项目"
style="width: 100%"
@change="handleProjectChange"
>
<el-option
v-for="item in projectOptions"
:key="item.id"
:label="item.projectName"
: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="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>
</el-card>
</div>
</div>
</template>
<script>
import { getCompetitionList } from '@/api/martial/competition'
import { getInfoPublishList } from '@/api/martial/infoPublish'
import { getProjectsByCompetition } from '@/api/martial/project'
import {
getParticipantList,
getParticipantDetail,
addParticipant,
updateParticipant,
removeParticipant
} from '@/api/martial/participant'
export default {
name: 'ParticipantManagement',
data() {
return {
loading: false,
currentView: 'list', // list, create, edit, view
participantId: null,
searchForm: {
keyword: '',
competitionId: null
},
pagination: {
current: 1,
size: 10,
total: 0
},
competitionOptions: [], // 已发布的可报名赛事列表(用于新建)
allCompetitionOptions: [], // 所有赛事列表(用于搜索和编辑)
projectOptions: [], // 项目列表
participantList: [],
formData: {
orderId: null,
competitionId: null,
competitionName: '',
playerName: '',
gender: 1,
age: null,
contactPhone: '',
organization: '',
idCard: '',
projectId: null,
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' }
],
projectId: [
{ required: true, message: '请选择参赛项目', trigger: 'change' }
]
}
};
},
computed: {
pageTitle() {
const titleMap = {
create: '添加参赛选手',
edit: '编辑参赛选手',
view: '查看参赛选手'
};
return titleMap[this.currentView] || '参赛选手信息';
},
displayList() {
return this.participantList;
},
// 根据不同模式返回不同的赛事选项
availableCompetitionOptions() {
// 编辑和查看模式:显示所有赛事(因为可能编辑已过报名期的选手)
if (this.currentView === 'edit' || this.currentView === 'view') {
return this.allCompetitionOptions;
}
// 新建模式:只显示可报名的赛事
return this.competitionOptions;
}
},
watch: {
'$route.query': {
handler(query) {
this.initPage();
},
immediate: true
}
},
mounted() {
this.loadAvailableCompetitions();
this.loadAllCompetitions();
},
methods: {
initPage() {
const { mode, id } = this.$route.query;
this.currentView = mode || 'list';
// 不使用 parseInt保持 ID 为字符串避免精度丢失
this.participantId = id || null;
if (this.currentView === 'list') {
this.loadParticipantList();
} else if (this.currentView !== 'list' && this.participantId) {
this.loadParticipantData();
} else if (this.currentView === 'create') {
this.resetFormData();
}
},
// 加载可报名的赛事(从已发布的信息中获取)
loadAvailableCompetitions() {
getInfoPublishList(1, 100, { isPublished: 1 })
.then(res => {
console.log('已发布信息列表返回数据:', res);
const responseData = res.data?.data;
if (responseData && responseData.records) {
const publishedCompetitionIds = new Set(
responseData.records
.filter(item => item.competitionId)
.map(item => item.competitionId)
);
console.log('已发布的赛事ID列表:', Array.from(publishedCompetitionIds));
if (publishedCompetitionIds.size > 0) {
this.loadPublishedCompetitions(Array.from(publishedCompetitionIds));
} else {
// 如果没有发布信息,直接加载所有赛事作为可报名赛事
console.log('没有已发布信息,加载所有赛事');
this.loadPublishedCompetitions([]);
}
}
})
.catch(err => {
console.error('加载已发布信息列表失败', err);
// 出错时也加载所有赛事
this.loadPublishedCompetitions([]);
});
},
// 加载已发布的赛事详细信息,并过滤出可报名的赛事
loadPublishedCompetitions(competitionIds) {
getCompetitionList(1, 100, {})
.then(res => {
console.log('赛事列表返回数据:', res);
const responseData = res.data?.data;
if (responseData && responseData.records) {
const now = new Date();
this.competitionOptions = responseData.records
.filter(item => {
// 如果没有发布信息competitionIds为空数组则显示所有在报名期内的赛事
if (competitionIds.length > 0 && !competitionIds.includes(item.id)) {
return false;
}
// 检查报名时间
if (!item.registrationStartTime || !item.registrationEndTime) {
return false;
}
const regStart = new Date(item.registrationStartTime);
const regEnd = new Date(item.registrationEndTime);
return now >= regStart && now <= regEnd;
})
.map(item => ({
id: item.id,
competitionName: item.competitionName,
registrationStartTime: item.registrationStartTime,
registrationEndTime: item.registrationEndTime
}));
console.log('可报名的赛事列表:', this.competitionOptions);
if (this.competitionOptions.length === 0) {
console.log('当前没有可以报名的赛事(报名时间范围外)');
}
}
})
.catch(err => {
console.error('加载赛事列表失败', err);
this.$message.error('加载赛事列表失败');
});
},
// 加载所有赛事(用于搜索过滤)
loadAllCompetitions() {
getCompetitionList(1, 100, {})
.then(res => {
const responseData = res.data?.data;
if (responseData && responseData.records) {
this.allCompetitionOptions = responseData.records.map(item => ({
id: item.id,
competitionName: item.competitionName
}));
}
})
.catch(err => {
console.error('加载所有赛事失败', err);
});
},
loadParticipantList() {
this.loading = true;
const params = {};
if (this.searchForm.keyword) {
params.playerName = this.searchForm.keyword;
}
if (this.searchForm.competitionId) {
params.competitionId = this.searchForm.competitionId;
}
getParticipantList(null, this.pagination.current, this.pagination.size, params)
.then(res => {
console.log('参赛人员列表返回数据:', res);
const responseData = res.data?.data;
if (responseData && responseData.records) {
this.participantList = responseData.records;
this.pagination.total = responseData.total || 0;
}
})
.catch(err => {
console.error('加载参赛人员列表失败', err);
this.$message.error('加载参赛人员列表失败');
})
.finally(() => {
this.loading = false;
});
},
loadParticipantData() {
if (!this.participantId) return;
this.loading = true;
getParticipantDetail(this.participantId)
.then(res => {
const detailData = res.data?.data;
if (detailData) {
this.formData = { ...detailData };
// 将 attachments 字符串转换为数组(前端需要数组格式)
if (typeof this.formData.attachments === 'string') {
try {
this.formData.attachments = JSON.parse(this.formData.attachments);
} catch (e) {
console.warn('解析 attachments 失败,使用空数组', e);
this.formData.attachments = [];
}
} else if (!this.formData.attachments) {
this.formData.attachments = [];
}
// 加载该赛事的项目列表
if (detailData.competitionId) {
this.loadProjectsByCompetition(detailData.competitionId);
}
}
})
.catch(err => {
console.error('加载参赛人员详情失败', err);
this.$message.error('加载参赛人员详情失败');
})
.finally(() => {
this.loading = false;
});
},
handlePageChange(current) {
this.pagination.current = current;
this.loadParticipantList();
},
handleSizeChange(size) {
this.pagination.size = size;
this.pagination.current = 1;
this.loadParticipantList();
},
handleSearch() {
this.pagination.current = 1;
this.loadParticipantList();
},
handleReset() {
this.searchForm = {
keyword: '',
competitionId: null
};
this.pagination.current = 1;
this.loadParticipantList();
},
handleCreate() {
this.$router.push({
path: '/martial/participant/list',
query: { mode: 'create' }
});
},
handleView(row) {
this.$router.push({
path: '/martial/participant/list',
query: { mode: 'view', id: row.id }
});
},
handleEdit(row) {
this.$router.push({
path: '/martial/participant/list',
query: { mode: 'edit', id: row.id }
});
},
switchToEdit() {
this.$router.push({
path: '/martial/participant/list',
query: { mode: 'edit', id: this.participantId }
});
},
handleDelete(row) {
this.$confirm('确定要删除该选手吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.loading = true;
removeParticipant(row.id.toString())
.then(res => {
this.$message.success('删除成功');
this.loadParticipantList();
})
.catch(err => {
console.error('删除失败', err);
this.$message.error('删除失败');
})
.finally(() => {
this.loading = false;
});
}).catch(() => {});
},
handleCompetitionChange(competitionId) {
// 从可用的选项列表中查找赛事
const competition = this.availableCompetitionOptions.find(item => item.id === competitionId);
if (competition) {
this.formData.competitionName = competition.competitionName;
}
// 加载该赛事的项目列表
this.loadProjectsByCompetition(competitionId);
// 清空已选项目
this.formData.projectId = null;
},
handleProjectChange(projectId) {
const project = this.projectOptions.find(item => item.id === projectId);
if (project) {
// 自动填充组别信息
if (project.category && !this.formData.category) {
this.formData.category = project.category;
}
}
},
loadProjectsByCompetition(competitionId) {
if (!competitionId) {
this.projectOptions = [];
return;
}
console.log('加载赛事项目赛事ID:', competitionId);
getProjectsByCompetition(competitionId)
.then(res => {
console.log('项目列表返回数据:', res);
const responseData = res.data?.data;
// 兼容两种数据格式分页数据有records和直接数组
let projectList = [];
if (responseData) {
if (Array.isArray(responseData)) {
// 直接是数组
projectList = responseData;
console.log('返回的是直接数组,长度:', projectList.length);
} else if (responseData.records && Array.isArray(responseData.records)) {
// 分页数据
projectList = responseData.records;
console.log('返回的是分页数据,记录数:', projectList.length);
} else {
console.warn('未知的数据格式:', responseData);
}
}
if (projectList.length > 0) {
this.projectOptions = projectList.map(item => ({
id: item.id,
projectName: item.projectName,
projectCode: item.projectCode,
category: item.category
}));
console.log('可选项目列表:', this.projectOptions);
} else {
this.projectOptions = [];
console.log('该赛事没有项目数据');
this.$message.warning('该赛事还没有配置项目,请先添加项目');
}
})
.catch(err => {
console.error('加载项目列表失败', err);
this.$message.error('加载项目列表失败: ' + (err.message || '未知错误'));
this.projectOptions = [];
});
},
handleSave() {
this.$refs.formRef.validate((valid) => {
if (valid) {
this.loading = true;
// 确保有赛事名称
if (!this.formData.competitionName) {
const competition = this.availableCompetitionOptions.find(item => item.id === this.formData.competitionId);
if (competition) {
this.formData.competitionName = competition.competitionName;
}
}
const submitData = { ...this.formData };
console.log('=== 提交前的 formData ===', this.formData);
console.log('formData.orderId:', this.formData.orderId);
// 将 attachments 数组转换为 JSON 字符串(后端需要 String 类型)
if (Array.isArray(submitData.attachments)) {
submitData.attachments = JSON.stringify(submitData.attachments);
}
// 临时方案: 如果没有 orderId使用 competitionId 作为 orderId
// 警告: 这是临时解决方案,后续应修改数据库表结构或后端逻辑
if (!submitData.orderId && submitData.competitionId) {
submitData.orderId = submitData.competitionId;
console.warn('⚠️ 临时方案: 使用 competitionId 作为 orderId', submitData.competitionId);
}
console.log('=== 提交的数据 submitData ===', submitData);
console.log('submitData.orderId:', submitData.orderId);
if (this.currentView === 'create') {
// 新建
addParticipant(submitData)
.then(res => {
this.$message.success('添加成功');
this.backToList();
})
.catch(err => {
console.error('添加失败', err);
this.$message.error('添加失败');
})
.finally(() => {
this.loading = false;
});
} else if (this.currentView === 'edit') {
// 编辑
submitData.id = this.participantId;
updateParticipant(submitData)
.then(res => {
this.$message.success('保存成功');
this.backToList();
})
.catch(err => {
console.error('保存失败', err);
this.$message.error('保存失败');
})
.finally(() => {
this.loading = false;
});
}
} else {
this.$message.error('请完善必填信息');
}
});
},
backToList() {
this.$router.push({
path: '/martial/participant/list'
});
},
resetFormData() {
this.formData = {
orderId: null,
competitionId: null,
competitionName: '',
playerName: '',
gender: 1,
age: null,
contactPhone: '',
organization: '',
idCard: '',
projectId: null,
category: '',
orderNum: 1,
introduction: '',
remark: '',
attachments: []
};
}
}
};
</script>
<style lang="scss" scoped>
.participant-container {
padding: 20px;
background: #fff;
border-radius: 8px;
min-height: calc(100vh - 120px);
}
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 20px;
border-bottom: 2px solid #f0f0f0;
}
.page-title {
margin: 0;
font-size: 24px;
font-weight: 600;
color: #333;
}
.search-form {
margin-bottom: 20px;
}
.data-table {
:deep(.el-table__header) {
th {
background-color: #fafafa;
color: #333;
font-weight: 600;
}
}
:deep(.el-button--text) {
padding: 0 8px;
}
}
.page-header {
display: flex;
align-items: center;
gap: 20px;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #f0f0f0;
.page-title {
flex: 1;
margin: 0;
font-size: 24px;
font-weight: 600;
color: #333;
}
.header-actions {
display: flex;
gap: 10px;
}
}
.participant-form {
max-width: 1200px;
}
.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;
}
</style>

View File

@@ -71,71 +71,176 @@
</el-button>
</div>
<div v-for="(group, index) in filteredCompetitionGroups" :key="group.id" class="competition-group">
<div class="group-header">
<div class="group-info">
<span class="group-title">{{ group.title }}</span>
<span class="group-meta">{{ group.type }}</span>
<span class="group-meta">{{ group.count }}</span>
<span class="group-meta">{{ group.code }}</span>
<!-- 项目卡片列表 -->
<div v-for="(group, groupIndex) in filteredCompetitionGroups" :key="group.id" class="project-card">
<!-- 项目头部 - 可点击展开 -->
<div class="project-header" :class="{ 'project-header-collapsed': !isProjectExpanded(group.id) }" @click="toggleProjectExpand(group.id)">
<div class="project-info">
<!-- 展开图标 -->
<span class="project-expand-icon">
<el-icon v-if="isProjectExpanded(group.id)"><ArrowDown /></el-icon>
<el-icon v-else><ArrowRight /></el-icon>
</span>
<span class="project-index">{{ groupIndex + 1 }}</span>
<span class="project-title">{{ group.title }}</span>
<span class="project-meta">{{ group.type }}</span>
<span class="project-meta">{{ getTeamCount(group) }}</span>
<span class="project-meta">{{ group.items?.length || 0 }}</span>
<span class="project-meta">{{ group.code }}</span>
</div>
<div class="group-actions">
<el-button size="small" type="warning" @click="handleMoveGroup(group)">
移动
</el-button>
<div class="project-actions" @click.stop>
<el-popover
placement="bottom"
:width="200"
trigger="click"
:disabled="isScheduleCompleted"
>
<template #reference>
<el-button size="small" type="warning" :disabled="isScheduleCompleted">
移动
</el-button>
</template>
<div class="move-popover">
<div class="move-label">移动到</div>
<el-select v-model="moveTargetVenueId" placeholder="选择场地" size="small" style="width: 100%; margin-bottom: 10px;">
<el-option
v-for="venue in venues"
:key="venue.id"
:label="venue.venueName"
:value="venue.id"
></el-option>
</el-select>
<el-button size="small" type="primary" @click="quickMoveGroup(group)">移动</el-button>
</div>
</el-popover>
</div>
</div>
<el-table :data="group.items" border stripe size="small">
<el-table-column label="序号" type="index" width="60" align="center"></el-table-column>
<el-table-column prop="schoolUnit" label="学校/单位" min-width="200"></el-table-column>
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="scope">
<el-tag :type="scope.row.status === '已签到' ? 'success' : scope.row.status === '异常' ? 'danger' : 'info'" size="small">
{{ scope.row.status || '未签到' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="150" align="center">
<template #default="scope">
<el-button
link
size="small"
@click="handleMoveUp(group, scope.$index)"
:disabled="scope.$index === 0 || isScheduleCompleted"
title="上移"
class="move-btn"
<!-- 队伍列表 - 可折叠 -->
<div v-if="isProjectExpanded(group.id)" class="team-list">
<div
v-for="(team, teamIndex) in groupItemsByTeam(group.items)"
:key="team.id"
class="team-row"
:class="{ 'team-row-expanded': isTeamExpanded(group.id, team.id) }"
>
<!-- 队伍主行 - 可点击展开 -->
<div class="team-main" @click="toggleTeamExpand(group.id, team.id)">
<!-- 展开图标 -->
<span class="expand-icon">
<el-icon v-if="isTeamExpanded(group.id, team.id)"><ArrowDown /></el-icon>
<el-icon v-else><ArrowRight /></el-icon>
</span>
<!-- 队伍信息 -->
<div class="team-info">
<span class="team-index">{{ teamIndex + 1 }}</span>
<span class="team-name">{{ team.schoolUnit }}</span>
</div>
<!-- 选手列表 -->
<div class="player-list">
<span
v-for="(player, playerIndex) in team.players"
:key="player.id"
class="player-tag"
:class="{ 'player-exception': player.status === '异常' }"
>
{{ player.playerName }}
</span>
</div>
<!-- 操作按钮 -->
<div class="team-actions" @click.stop>
<el-button
link
size="small"
@click="handleTeamMoveUp(group, teamIndex)"
:disabled="teamIndex === 0 || isScheduleCompleted"
title="上移"
class="move-btn"
>
<img src="/img/图标 3@3x.png" class="move-icon" alt="上移" />
</el-button>
<el-button
link
size="small"
@click="handleTeamMoveDown(group, teamIndex)"
:disabled="teamIndex === groupItemsByTeam(group.items).length - 1 || isScheduleCompleted"
title="下移"
class="move-btn"
>
<img src="/img/图标 4@3x.png" class="move-icon" alt="下移" />
</el-button>
</div>
</div>
<!-- 展开内容 - 选手详情 -->
<div v-if="isTeamExpanded(group.id, team.id)" class="team-expand-content">
<div
v-for="(player, playerIndex) in team.players"
:key="player.id"
class="player-detail-row"
>
<img src="/img/图标 3@3x.png" class="move-icon" alt="上移" />
</el-button>
<el-button
link
size="small"
@click="handleMoveDown(group, scope.$index)"
:disabled="scope.$index === group.items.length - 1 || isScheduleCompleted"
title="下移"
class="move-btn"
>
<img src="/img/图标 4@3x.png" class="move-icon" alt="下移" />
</el-button>
<el-button
v-if="(scope.row.status || '未签到') === '未签到'"
link
size="small"
@click="markAsException(group, scope.$index)"
:disabled="isScheduleCompleted"
style="color: #f56c6c;"
>
异常
</el-button>
</template>
</el-table-column>
</el-table>
<span class="player-detail-index">{{ playerIndex + 1 }}</span>
<span class="player-detail-name">{{ player.playerName }}</span>
<span class="player-detail-status">
<el-tag
:type="player.status === '已签到' ? 'success' : player.status === '异常' ? 'danger' : 'info'"
size="small"
>
{{ player.status || '未签到' }}
</el-tag>
</span>
<span class="player-detail-actions">
<el-button
v-if="(player.status || '未签到') === '未签到'"
link
size="small"
@click="markPlayerAsException(group, team, playerIndex)"
:disabled="isScheduleCompleted"
style="color: #f56c6c;"
>
标记异常
</el-button>
<el-button
v-if="player.status === '异常'"
link
size="small"
@click="removePlayerException(group, team, playerIndex)"
:disabled="isScheduleCompleted"
style="color: #67c23a;"
>
取消异常
</el-button>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 场地 Tab -->
<div v-show="activeTab === 'venue'" class="tab-content">
<!-- 场地列表 -->
<div class="venue-list">
<div class="venue-buttons">
<el-button
v-for="venue in venues"
:key="'venue-tab-' + venue.id"
size="small"
:type="selectedVenueIdForVenueTab === venue.id ? 'primary' : ''"
@click="selectedVenueIdForVenueTab = venue.id"
>
{{ venue.venueName }}
</el-button>
<div v-if="venues.length === 0" class="no-venue-hint">
暂无场地信息请先在赛事管理中配置场地
</div>
</div>
</div>
<div class="time-selector">
<el-button
v-for="(time, index) in timeSlots"
@@ -272,19 +377,25 @@
</template>
<script>
import { ArrowDown, ArrowRight } from '@element-plus/icons-vue'
import { getVenuesByCompetition } from '@/api/martial/venue'
import { getCompetitionDetail } from '@/api/martial/competition'
import { getScheduleResult, saveAndLockSchedule, saveDraftSchedule, triggerAutoArrange, moveScheduleGroup } from '@/api/martial/activitySchedule'
import { getScheduleResult, saveAndLockSchedule, saveDraftSchedule, triggerAutoArrange, moveScheduleGroup, exportSchedule } from '@/api/martial/activitySchedule'
export default {
name: 'MartialScheduleList',
components: {
ArrowDown,
ArrowRight
},
data() {
return {
competitionId: null,
orderId: null,
activeTab: 'competition',
selectedTime: 0,
selectedVenueId: null, // 选中的场地ID
selectedVenueId: null, // 选中的场地ID竞赛分组Tab
selectedVenueIdForVenueTab: null, // 选中的场地ID场地Tab
confirmDialogVisible: false,
isScheduleCompleted: false, // 是否已完成编排
loading: false,
@@ -306,7 +417,9 @@ export default {
// 异常组相关
exceptionDialogVisible: false,
exceptionList: [] // 异常参赛人员列表
exceptionList: [], // 异常参赛人员列表
expandedTeams: {}, // 展开的队伍 { 'groupId-teamId': true }
expandedProjects: {} // 展开的项目 { 'groupId': true },默认收起
}
},
computed: {
@@ -334,17 +447,25 @@ export default {
return filtered
},
// 场地标签页的数据 - 根据选中的时间段动态生成
// 场地标签页的数据 - 根据选中的场地和时间段动态生成
venueData() {
if (this.selectedTime === null) {
return []
}
// 获取选中时间段的所有分组
const groupsInTimeSlot = this.competitionGroups.filter(
let groupsInTimeSlot = this.competitionGroups.filter(
group => group.timeSlotIndex === this.selectedTime
)
// 如果选中了场地,进一步过滤
if (this.selectedVenueIdForVenueTab) {
groupsInTimeSlot = groupsInTimeSlot.filter(group => {
return group.venueId === this.selectedVenueIdForVenueTab ||
Number(group.venueId) === Number(this.selectedVenueIdForVenueTab)
})
}
// 将分组转换为场地视图的数据格式
return groupsInTimeSlot.map(group => ({
project: group.title,
@@ -372,6 +493,254 @@ export default {
}
},
methods: {
// 检查项目是否展开
isProjectExpanded(groupId) {
return this.expandedProjects[groupId] === true
},
// 切换项目展开状态
toggleProjectExpand(groupId) {
if (this.expandedProjects[groupId]) {
delete this.expandedProjects[groupId]
} else {
this.expandedProjects[groupId] = true
}
// 触发响应式更新
this.expandedProjects = { ...this.expandedProjects }
},
// 检查队伍是否展开
isTeamExpanded(groupId, teamId) {
const key = groupId + '-' + teamId
return this.expandedTeams[key] === true
},
// 切换队伍展开状态
toggleTeamExpand(groupId, teamId) {
const key = groupId + '-' + teamId
if (this.expandedTeams[key]) {
delete this.expandedTeams[key]
} else {
this.expandedTeams[key] = true
}
// 触发响应式更新
this.expandedTeams = { ...this.expandedTeams }
},
// 标记选手为异常
markPlayerAsException(group, team, playerIndex) {
const player = team.players[playerIndex]
if (player) {
player.status = '异常'
// 添加到异常列表
this.exceptionList.push({
groupId: group.id,
groupTitle: group.title,
teamId: team.id,
schoolUnit: team.schoolUnit,
playerId: player.id,
playerName: player.playerName,
status: '异常'
})
this.$message.success('已标记为异常')
}
},
// 取消选手异常状态
removePlayerException(group, team, playerIndex) {
const player = team.players[playerIndex]
if (player) {
player.status = '未签到'
// 从异常列表中移除
const idx = this.exceptionList.findIndex(
e => e.playerId === player.id && e.groupId === group.id
)
if (idx !== -1) {
this.exceptionList.splice(idx, 1)
}
this.$message.success('已取消异常标记')
}
},
// 将选手按学校/单位分组为队伍
groupItemsByTeam(items) {
if (!items || items.length === 0) return []
const teamMap = new Map()
items.forEach(item => {
const key = item.schoolUnit
if (!teamMap.has(key)) {
teamMap.set(key, {
id: `team_${item.id}`,
schoolUnit: key,
players: [],
playerIds: []
})
}
teamMap.get(key).players.push(item)
teamMap.get(key).playerIds.push(item.id)
})
return Array.from(teamMap.values())
},
// 获取队伍数量
getTeamCount(group) {
if (!group.items || group.items.length === 0) return 0
const teams = this.groupItemsByTeam(group.items)
return teams.length
},
// 快速移动分组从popover中调用
async quickMoveGroup(group) {
if (!this.moveTargetVenueId) {
this.$message.warning('请选择目标场地')
return
}
const targetVenue = this.venues.find(v => v.id === this.moveTargetVenueId)
try {
// 调用后端API移动分组
const res = await moveScheduleGroup({
groupId: group.id,
targetVenueId: this.moveTargetVenueId,
targetTimeSlotIndex: this.selectedTime // 保持当前时间段
})
if (res.data.success) {
// 更新前端数据
group.venueId = this.moveTargetVenueId
group.venueName = targetVenue ? targetVenue.venueName : ''
this.$message.success(`已移动到 ${group.venueName}`)
} else {
this.$message.error(res.data.msg || '移动分组失败')
}
} catch (error) {
console.error('移动分组失败:', error)
this.$message.error('移动分组失败,请稍后重试')
}
},
// 获取队伍状态
getTeamStatus(team) {
if (!team || !team.players) return '未签到'
const statuses = team.players.map(p => p.status || '未签到')
if (statuses.every(s => s === '已签到')) return '已签到'
if (statuses.every(s => s === '未签到')) return '未签到'
if (statuses.some(s => s === '异常')) return '部分异常'
return '部分签到'
},
// 获取队伍状态类型
getTeamStatusType(team) {
const status = this.getTeamStatus(team)
switch(status) {
case '已签到': return 'success'
case '部分异常': return 'danger'
case '部分签到': return 'warning'
default: return 'info'
}
},
// 队伍上移
handleTeamMoveUp(group, teamIndex) {
const teams = this.groupItemsByTeam(group.items)
if (teamIndex === 0 || this.isScheduleCompleted) return
const currentTeam = teams[teamIndex]
const prevTeam = teams[teamIndex - 1]
// 找到两个队伍在原数组中的位置范围
const currentPlayerIds = currentTeam.playerIds
const prevPlayerIds = prevTeam.playerIds
// 重新排序:将当前队伍的所有选手移到前一个队伍之前
const newItems = []
let addedCurrent = false
for (const item of group.items) {
if (prevPlayerIds.includes(item.id) && !addedCurrent) {
// 先添加当前队伍的所有选手
for (const cItem of group.items) {
if (currentPlayerIds.includes(cItem.id)) {
newItems.push(cItem)
}
}
addedCurrent = true
}
if (!currentPlayerIds.includes(item.id)) {
newItems.push(item)
}
}
group.items = newItems
this.$message.success('上移成功')
},
// 队伍下移
handleTeamMoveDown(group, teamIndex) {
const teams = this.groupItemsByTeam(group.items)
if (teamIndex === teams.length - 1 || this.isScheduleCompleted) return
const currentTeam = teams[teamIndex]
const nextTeam = teams[teamIndex + 1]
const currentPlayerIds = currentTeam.playerIds
const nextPlayerIds = nextTeam.playerIds
// 重新排序:将当前队伍的所有选手移到下一个队伍之后
const newItems = []
let addedCurrent = false
for (const item of group.items) {
if (!currentPlayerIds.includes(item.id)) {
newItems.push(item)
}
if (nextPlayerIds.includes(item.id)) {
// 检查是否是下一个队伍的最后一个选手
const isLastOfNext = item.id === nextPlayerIds[nextPlayerIds.length - 1]
if (isLastOfNext && !addedCurrent) {
// 在下一个队伍最后一个选手之后添加当前队伍
for (const cItem of group.items) {
if (currentPlayerIds.includes(cItem.id)) {
newItems.push(cItem)
}
}
addedCurrent = true
}
}
}
group.items = newItems
this.$message.success('下移成功')
},
// 标记单个选手为异常
markPlayerAsException(group, player) {
if (this.isScheduleCompleted) {
this.$message.warning('编排已完成,无法标记异常')
return
}
// 在 group.items 中找到该选手并修改状态
const item = group.items.find(i => i.id === player.id)
if (item) {
item.status = '异常'
// 添加到异常列表
this.exceptionList.push({
groupId: group.id,
groupTitle: group.title,
participantId: player.id,
schoolUnit: player.schoolUnit,
playerName: player.playerName,
status: '异常'
})
this.$message.success(`已将 ${player.playerName} 标记为异常`)
}
},
goBack() {
this.$router.go(-1)
},
@@ -463,6 +832,7 @@ export default {
// 默认选中第一个场地
if (this.venues.length > 0) {
this.selectedVenueId = this.venues[0].id
this.selectedVenueIdForVenueTab = this.venues[0].id
}
console.log('加载的场地数据:', this.venues)
}
@@ -505,6 +875,7 @@ export default {
items: (group.participants || []).map(p => ({
id: p.id,
schoolUnit: p.schoolUnit,
playerName: p.playerName,
status: p.status || '未签到',
sortOrder: p.sortOrder
}))
@@ -742,8 +1113,24 @@ export default {
group.items.splice(itemIndex + 1, 0, temp)
this.$message.success('下移成功')
},
handleExport() {
this.$message.success('导出功能开发中')
async handleExport() {
try {
this.loading = true
const res = await exportSchedule(this.competitionId)
// axios 返回的是 response 对象,需要取 res.data
const blob = new Blob([res.data], { type: 'application/vnd.ms-excel' })
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = `赛程表_${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
}
},
handleConfirm() {
this.confirmDialogVisible = true
@@ -945,6 +1332,196 @@ export default {
}
}
// 新的项目卡片样式
.project-card {
margin-bottom: 20px;
border: 1px solid #e4e7ed;
border-radius: 4px;
overflow: hidden;
.project-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: #fafafa;
border-bottom: 1px solid #e4e7ed;
cursor: pointer;
transition: background-color 0.2s;
&:hover {
background: #f0f0f0;
}
&.project-header-collapsed {
border-bottom: none;
}
.project-info {
display: flex;
align-items: center;
gap: 12px;
.project-expand-icon {
width: 20px;
color: #909399;
display: flex;
align-items: center;
justify-content: center;
}
.project-index {
font-weight: 600;
color: #e6a23c;
font-size: 14px;
}
.project-title {
font-weight: 600;
color: #e6a23c;
font-size: 14px;
}
.project-meta {
color: #606266;
font-size: 13px;
}
}
.project-actions {
display: flex;
gap: 8px;
}
}
.team-list {
.team-row {
border-bottom: 1px solid #ebeef5;
background: #fff;
&:last-child {
border-bottom: none;
}
&.team-row-expanded {
background: #fafafa;
}
.team-main {
display: flex;
align-items: center;
padding: 12px 16px;
cursor: pointer;
&:hover {
background: #f5f7fa;
}
.expand-icon {
width: 20px;
color: #909399;
display: flex;
align-items: center;
justify-content: center;
}
.team-info {
display: flex;
align-items: center;
min-width: 180px;
.team-index {
color: #909399;
font-size: 13px;
margin-right: 4px;
}
.team-name {
color: #303133;
font-size: 13px;
font-weight: 500;
}
}
.player-list {
flex: 1;
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 0 20px;
.player-tag {
color: #606266;
font-size: 13px;
padding: 2px 8px;
background: #f4f4f5;
border-radius: 4px;
&.player-exception {
color: #f56c6c;
background: #fef0f0;
}
}
}
.team-actions {
display: flex;
gap: 4px;
min-width: 80px;
justify-content: flex-end;
}
}
.team-expand-content {
background: #fafafa;
border-top: 1px dashed #e4e7ed;
padding: 8px 16px 8px 56px;
.player-detail-row {
display: flex;
align-items: center;
padding: 8px 12px;
border-bottom: 1px solid #ebeef5;
&:last-child {
border-bottom: none;
}
.player-detail-index {
width: 30px;
color: #909399;
font-size: 12px;
}
.player-detail-name {
flex: 1;
color: #303133;
font-size: 13px;
}
.player-detail-status {
width: 80px;
text-align: center;
}
.player-detail-actions {
width: 100px;
text-align: right;
}
}
}
}
}
}
.move-popover {
.move-label {
font-size: 13px;
color: #606266;
margin-bottom: 8px;
}
}
.group-footer-hints {
margin-top: 15px;
padding: 8px 12px;
@@ -1014,4 +1591,28 @@ export default {
vertical-align: middle;
}
}
// 展开行样式
.player-expand-list {
padding: 10px 20px 10px 60px;
background: #fafafa;
.player-row {
display: flex;
align-items: center;
gap: 15px;
padding: 8px 0;
border-bottom: 1px dashed #eee;
&:last-child {
border-bottom: none;
}
.player-name {
min-width: 80px;
color: #606266;
font-weight: 500;
}
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -154,6 +154,11 @@
<div class="total-score-display">
<span class="label">总分</span>
<span class="value">{{ formatScore(currentDetail.totalScore) }}</span>
<div class="calculation-note">
<span v-if="currentDetail.judgeScores.length > 2">
(去掉最高分和最低分后的平均分)
</span>
</div>
</div>
</div>
</div>
@@ -182,96 +187,6 @@ export default {
projectOptions: [],
venueOptions: [],
scoreList: [],
allTableData: [
{
id: 1,
projectName: '男子组陈氏太极拳',
venueName: '第一场地',
playerName: '张三',
teamName: '少林寺武术大学院',
idCard: '123456789000000000',
playerNo: '123-4567898275',
judgeScores: [8.906, 8.905, 8.908, 8.907, 8.906],
totalScore: 8.907
},
{
id: 2,
projectName: '女子组长拳',
venueName: '第一场地',
playerName: '李四',
teamName: '武当武术学院',
idCard: '123456789000000001',
playerNo: '123-4567898276',
judgeScores: [9.125, 9.130, 9.128, 9.126, 9.129],
totalScore: 9.128
},
{
id: 3,
projectName: '男子组陈氏太极拳',
venueName: '第二场地',
playerName: '王五',
teamName: '峨眉武术协会',
idCard: '123456789000000002',
playerNo: '123-4567898277',
judgeScores: [8.550, 8.548, 8.552, 8.551, 8.549],
totalScore: 8.550
},
{
id: 4,
projectName: '女子组双剑(含长穗双剑)',
venueName: '第一场地',
playerName: '赵六',
teamName: '昆仑武术馆',
idCard: '123456789000000003',
playerNo: '123-4567898278',
judgeScores: [9.245, 9.248, 9.246, 9.247, 9.249],
totalScore: 9.247
},
{
id: 5,
projectName: '男子组杨氏太极拳',
venueName: '第三场地',
playerName: '孙七',
teamName: '华山武术学校',
idCard: '123456789000000004',
playerNo: '123-4567898279',
judgeScores: [8.785, 8.788, 8.786, 8.787, 8.785],
totalScore: 8.786
},
{
id: 6,
projectName: '女子组刀术',
venueName: '第二场地',
playerName: '周八',
teamName: '少林寺武术大学院',
idCard: '123456789000000005',
playerNo: '123-4567898280',
judgeScores: [8.925, 8.928, 8.926, 8.927, 8.925],
totalScore: 8.926
},
{
id: 7,
projectName: '男子组棍术',
venueName: '第四场地',
playerName: '吴九',
teamName: '武当武术学院',
idCard: '123456789000000006',
playerNo: '123-4567898281',
judgeScores: [9.015, 9.018, 9.016, 9.017, 9.015],
totalScore: 9.016
},
{
id: 8,
projectName: '女子组枪术',
venueName: '第三场地',
playerName: '郑十',
teamName: '峨眉武术协会',
idCard: '123456789000000007',
playerNo: '123-4567898282',
judgeScores: [8.665, 8.668, 8.666, 8.667, 8.665],
totalScore: 8.666
}
],
tableData: [],
pagination: {
current: 1,
@@ -345,25 +260,22 @@ export default {
try {
const res = await getScoreList(this.pagination.current, this.pagination.size, params)
console.log('评分列表返回数据:', res)
console.log('===== 调试:后端返回的数据结构 =====')
const responseData = res.data?.data
if (responseData && responseData.records && responseData.records.length > 0) {
console.log('第一条评分记录:', responseData.records[0])
console.log('记录字段:', Object.keys(responseData.records[0]))
console.log('是否包含 projectName:', 'projectName' in responseData.records[0])
console.log('是否包含 venueName:', 'venueName' in responseData.records[0])
console.log('是否包含 playerName:', 'playerName' in responseData.records[0])
console.log('projectId 值:', responseData.records[0].projectId)
console.log('venueId 值:', responseData.records[0].venueId)
console.log('athleteId 值:', responseData.records[0].athleteId)
}
console.log('======================================')
const responseData = res.data?.data
if (responseData && responseData.records) {
this.scoreList = responseData.records
// 过滤掉 projectId 为 null 的无效记录
const validScores = responseData.records.filter(score => {
if (!score.projectId) {
console.warn('发现无效评分记录projectId为空:', score)
return false
}
return true
})
this.scoreList = validScores
// 补充关联数据(项目名称、场地名称、选手名称)
await this.enrichScoreData(responseData.records)
await this.enrichScoreData(validScores)
// 按选手分组评分数据
this.processScoreData(this.scoreList)
@@ -466,6 +378,12 @@ export default {
const athleteMap = new Map()
scores.forEach(score => {
// 确保 projectId 存在
if (!score.projectId) {
console.warn('跳过无效评分记录:', score)
return
}
const key = `${score.athleteId}-${score.projectId}`
if (!athleteMap.has(key)) {
athleteMap.set(key, {
@@ -495,11 +413,10 @@ export default {
})
})
// 计算总分(平均分)
// 计算总分(去掉最高最低分后的平均分)
this.tableData = Array.from(athleteMap.values()).map(athlete => {
if (athlete.judgeScores.length > 0) {
const sum = athlete.judgeScores.reduce((a, b) => a + b, 0)
athlete.totalScore = sum / athlete.judgeScores.length
athlete.totalScore = this.calculateFinalScore(athlete.judgeScores)
}
return athlete
})
@@ -516,6 +433,34 @@ export default {
this.judgeColumns = Array(maxJudges).fill(null)
},
/**
* 计算最终得分
* 规则:
* - 如果裁判数 <= 2直接取平均值
* - 如果裁判数 > 2去掉最高分和最低分后取平均值
*/
calculateFinalScore(scores) {
if (!scores || scores.length === 0) {
return 0
}
// 如果只有1-2个裁判直接取平均值
if (scores.length <= 2) {
const sum = scores.reduce((a, b) => a + b, 0)
return sum / scores.length
}
// 3个及以上裁判去掉最高分和最低分
const sortedScores = [...scores].sort((a, b) => a - b)
// 去掉第一个(最低分)和最后一个(最高分)
const validScores = sortedScores.slice(1, -1)
// 计算平均值
const sum = validScores.reduce((a, b) => a + b, 0)
return sum / validScores.length
},
// 查询
handleSearch() {
this.pagination.current = 1
@@ -663,6 +608,12 @@ export default {
font-weight: 700;
color: #1b7c5e;
}
.calculation-note {
margin-top: 8px;
font-size: 12px;
color: #999;
}
}
}
}