This commit is contained in:
2025-11-30 12:14:27 +08:00
parent 0d4bbcd1f2
commit 4e26a5089c
32 changed files with 5605 additions and 226 deletions

92
src/api/martial/banner.js Normal file
View File

@@ -0,0 +1,92 @@
import request from '@/axios';
// ==================== 轮播图管理接口 ====================
/**
* 轮播图分页查询
* @param {Number} current - 当前页,默认1
* @param {Number} size - 每页条数,默认10
* @param {Object} params - 查询参数
*/
export const getBannerList = (current, size, params) => {
return request({
url: '/api/blade-martial/banner/list',
method: 'get',
params: {
current,
size,
...params
}
})
}
/**
* 获取轮播图详情
* @param {Number} id - 轮播图主键ID
*/
export const getBannerDetail = (id) => {
return request({
url: '/api/blade-martial/banner/detail',
method: 'get',
params: { id }
})
}
/**
* 新增轮播图
* @param {Object} data - 轮播图数据
*/
export const addBanner = (data) => {
return request({
url: '/api/blade-martial/banner/save',
method: 'post',
data
})
}
/**
* 修改轮播图
* @param {Object} data - 轮播图数据
*/
export const updateBanner = (data) => {
return request({
url: '/api/blade-martial/banner/update',
method: 'post',
data
})
}
/**
* 删除轮播图
* @param {String} ids - 轮播图ID,多个用逗号分隔
*/
export const removeBanner = (ids) => {
return request({
url: '/api/blade-martial/banner/remove',
method: 'post',
params: { ids }
})
}
/**
* 获取启用的轮播图列表(小程序端使用)
*/
export const getActiveBannerList = () => {
return request({
url: '/api/blade-martial/banner/active-list',
method: 'get'
})
}
/**
* 修改轮播图状态
* @param {Number} id - 轮播图ID
* @param {Number} status - 状态0-禁用 1-启用)
*/
export const updateBannerStatus = (id, status) => {
return request({
url: '/api/blade-martial/banner/update-status',
method: 'post',
params: { id, status }
})
}

View File

@@ -255,3 +255,89 @@ export const removeVenue = (ids) => {
params: { ids }
})
}
// ==================== 赛事管理接口 ====================
/**
* 新增赛事
* @param {Object} data - 赛事数据
* @param {String} data.competitionName - 赛事名称
* @param {String} data.organizer - 主办单位
* @param {String} data.location - 地区
* @param {String} data.venue - 详细地点
* @param {String} data.registrationStartTime - 报名开始时间
* @param {String} data.registrationEndTime - 报名结束时间
* @param {String} data.competitionStartTime - 比赛开始时间
* @param {String} data.competitionEndTime - 比赛结束时间
* @param {String} data.introduction - 赛事简介
* @param {Array} data.posterImages - 宣传图片
* @param {String} data.contactPerson - 联系人
* @param {String} data.contactPhone - 联系电话
* @param {String} data.contactEmail - 联系邮箱
* @param {String} data.rules - 竞赛规则
* @param {String} data.requirements - 参赛要求
* @param {String} data.awards - 奖项设置
* @param {Array} data.regulationFiles - 规程文件
* @param {Array} data.schedule - 活动日程
*/
export const addCompetition = (data) => {
return request({
url: '/api/blade-martial/competition/save',
method: 'post',
data
})
}
/**
* 赛事列表查询
* @param {Number} current - 当前页
* @param {Number} size - 每页条数
* @param {Object} params - 查询参数
*/
export const getCompetitionList = (current, size, params) => {
return request({
url: '/api/blade-martial/competition/list',
method: 'get',
params: {
current,
size,
...params
}
})
}
/**
* 获取赛事详情
* @param {Number} id - 赛事ID
*/
export const getCompetitionDetail = (id) => {
return request({
url: '/api/blade-martial/competition/detail',
method: 'get',
params: { id }
})
}
/**
* 修改赛事
* @param {Object} data - 赛事数据
*/
export const updateCompetition = (data) => {
return request({
url: '/api/blade-martial/competition/update',
method: 'post',
data
})
}
/**
* 删除赛事
* @param {String} ids - 赛事ID,多个用逗号分隔
*/
export const removeCompetition = (ids) => {
return request({
url: '/api/blade-martial/competition/remove',
method: 'post',
params: { ids }
})
}

View File

@@ -0,0 +1,142 @@
import request from '@/axios';
// ==================== 参赛选手管理接口 ====================
/**
* 参赛选手分页查询
* @param {Number} competitionId - 赛事ID
* @param {Number} current - 当前页,默认1
* @param {Number} size - 每页条数,默认10
* @param {Object} params - 查询参数
*/
export const getParticipantList = (competitionId, current, size, params = {}) => {
return request({
url: '/api/blade-martial/participant/list',
method: 'get',
params: {
competitionId,
current,
size,
...params
}
})
}
/**
* 获取参赛选手详情
* @param {Number} id - 选手主键ID
*/
export const getParticipantDetail = (id) => {
return request({
url: '/api/blade-martial/participant/detail',
method: 'get',
params: { id }
})
}
/**
* 新增参赛选手
* @param {Object} data - 选手数据
* @param {Number} data.competitionId - 赛事ID
* @param {String} data.playerName - 选手姓名
* @param {Number} data.gender - 性别1-男2-女)
* @param {Number} data.age - 年龄
* @param {String} data.contactPhone - 联系电话
* @param {String} data.organization - 所属单位
* @param {String} data.idCard - 身份证号
* @param {String} data.projectName - 参赛项目
* @param {String} data.category - 组别
* @param {Number} data.orderNum - 出场顺序
* @param {String} data.introduction - 选手简介
* @param {Array} data.attachments - 附件列表
* @param {String} data.remark - 备注
*/
export const addParticipant = (data) => {
return request({
url: '/api/blade-martial/participant/save',
method: 'post',
data
})
}
/**
* 修改参赛选手
* @param {Object} data - 选手数据
*/
export const updateParticipant = (data) => {
return request({
url: '/api/blade-martial/participant/update',
method: 'post',
data
})
}
/**
* 删除参赛选手
* @param {String} ids - 选手ID,多个用逗号分隔
*/
export const removeParticipant = (ids) => {
return request({
url: '/api/blade-martial/participant/remove',
method: 'post',
params: { ids }
})
}
/**
* 更新出场顺序
* @param {Number} id - 选手ID
* @param {Number} orderNum - 出场顺序
*/
export const updateOrder = (id, orderNum) => {
return request({
url: '/api/blade-martial/participant/update-order',
method: 'post',
params: { id, orderNum }
})
}
/**
* 批量导入参赛选手
* @param {Number} competitionId - 赛事ID
* @param {File} file - Excel文件
*/
export const importParticipants = (competitionId, file) => {
const formData = new FormData()
formData.append('competitionId', competitionId)
formData.append('file', file)
return request({
url: '/api/blade-martial/participant/import',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
/**
* 导出参赛选手名单
* @param {Number} competitionId - 赛事ID
*/
export const exportParticipants = (competitionId) => {
return request({
url: '/api/blade-martial/participant/export',
method: 'get',
params: { competitionId },
responseType: 'blob'
})
}
/**
* 批量更新出场顺序
* @param {Array} data - 选手顺序数据 [{id: 1, orderNum: 1}, {id: 2, orderNum: 2}]
*/
export const batchUpdateOrder = (data) => {
return request({
url: '/api/blade-martial/participant/batch-update-order',
method: 'post',
data
})
}

View File

@@ -0,0 +1,91 @@
import request from '@/axios';
// ==================== 评委管理接口 ====================
/**
* 评委分页查询
* @param {Number} current - 当前页,默认1
* @param {Number} size - 每页条数,默认10
* @param {Object} params - 查询参数
* @param {String} params.keyword - 关键词搜索(姓名/手机号)
* @param {Number} params.refereeType - 裁判类型1-主裁判2-普通裁判)
*/
export const getRefereeList = (current, size, params) => {
return request({
url: '/api/blade-martial/referee/list',
method: 'get',
params: {
current,
size,
...params
}
})
}
/**
* 获取评委详情
* @param {Number} id - 评委主键ID
*/
export const getRefereeDetail = (id) => {
return request({
url: '/api/blade-martial/referee/detail',
method: 'get',
params: { id }
})
}
/**
* 新增评委
* @param {Object} data - 评委数据
* @param {String} data.name - 姓名
* @param {Number} data.gender - 性别1-男2-女)
* @param {String} data.phone - 手机号
* @param {String} data.idCard - 身份证号
* @param {Number} data.refereeType - 裁判类型1-主裁判2-普通裁判)
* @param {String} data.level - 等级/职称
* @param {String} data.specialty - 擅长项目
* @param {String} data.remark - 备注
*/
export const addReferee = (data) => {
return request({
url: '/api/blade-martial/referee/save',
method: 'post',
data
})
}
/**
* 修改评委
* @param {Object} data - 评委数据
*/
export const updateReferee = (data) => {
return request({
url: '/api/blade-martial/referee/update',
method: 'post',
data
})
}
/**
* 删除评委
* @param {String} ids - 评委ID,多个用逗号分隔
*/
export const removeReferee = (ids) => {
return request({
url: '/api/blade-martial/referee/remove',
method: 'post',
params: { ids }
})
}
/**
* 获取可用评委列表(用于下拉选择)
* @param {Number} refereeType - 裁判类型(可选)
*/
export const getAvailableReferees = (refereeType) => {
return request({
url: '/api/blade-martial/referee/available',
method: 'get',
params: { refereeType }
})
}

84
src/api/martial/score.js Normal file
View File

@@ -0,0 +1,84 @@
import request from '@/axios';
// ==================== 评分管理接口 ====================
/**
* 评分分页查询
* @param {Number} current - 当前页,默认1
* @param {Number} size - 每页条数,默认10
* @param {Object} params - 查询参数
*/
export const getScoreList = (current, size, params) => {
return request({
url: '/api/blade-martial/score/list',
method: 'get',
params: {
current,
size,
...params
}
})
}
/**
* 获取评分详情
* @param {Number} id - 评分主键ID
*/
export const getScoreDetail = (id) => {
return request({
url: '/api/blade-martial/score/detail',
method: 'get',
params: { id }
})
}
/**
* 获取选手的所有裁判评分
* @param {Number} playerId - 选手ID
* @param {Number} projectId - 比赛项目ID
*/
export const getPlayerScores = (playerId, projectId) => {
return request({
url: '/api/blade-martial/score/player-scores',
method: 'get',
params: { playerId, projectId }
})
}
/**
* 导出评分数据
* @param {Object} params - 查询参数
*/
export const exportScores = (params) => {
return request({
url: '/api/blade-martial/score/export',
method: 'get',
params,
responseType: 'blob'
})
}
/**
* 获取场地列表
* @param {Number} competitionId - 赛事ID
*/
export const getVenueList = (competitionId) => {
return request({
url: '/api/blade-martial/venue/list',
method: 'get',
params: { competitionId }
})
}
/**
* 获取比赛项目列表
* @param {Number} competitionId - 赛事ID
* @param {Number} venueId - 场地ID可选
*/
export const getProjectList = (competitionId, venueId) => {
return request({
url: '/api/blade-martial/project/list',
method: 'get',
params: { competitionId, venueId }
})
}

View File

@@ -1,44 +1,58 @@
<template>
<div class="login-container" ref="login" @keyup.enter="handleLogin">
<div class="stars"></div>
<div class="stars2"></div>
<div class="stars3"></div>
<div class="login-weaper animated bounceInDown">
<div class="login-wrapper animated bounceInDown">
<!-- 左侧图形装饰区 -->
<div class="login-left">
<div class="login-time">
{{ time }}
<div class="decoration-bg">
<div class="circle circle-1"></div>
<div class="circle circle-2"></div>
<div class="circle circle-3"></div>
</div>
<div class="login-left-content">
<h1 class="title">武术赛事通</h1>
<p class="subtitle">专业的武术赛事管理平台</p>
<div class="feature-box">
<div class="feature-item">
<span>赛事报名管理</span>
<div class="content-wrapper">
<div class="logo-area">
<div class="logo-circle">
<div class="logo-icon"></div>
</div>
<div class="feature-item">
<span>赛程编排</span>
<h1 class="brand-title">武术赛事通</h1>
<p class="brand-subtitle">专业的武术赛事管理平台</p>
</div>
<div class="feature-points">
<div class="point-item">
<i class="el-icon-trophy"></i>
<span>专业赛事管理</span>
</div>
<div class="feature-item">
<span>赛事调度</span>
<div class="point-item">
<i class="el-icon-s-flag"></i>
<span>智能编排调度</span>
</div>
<div class="feature-item">
<span>数据统计分析</span>
<div class="point-item">
<i class="el-icon-medal"></i>
<span>权威评分系统</span>
</div>
</div>
</div>
</div>
<div class="login-border">
<div class="login-main">
<h4 class="login-title">
登录 武术赛事通
<!-- 右侧登录表单区 -->
<div class="login-right">
<div class="login-form-container">
<h2 class="login-title">
系统登录
<top-lang></top-lang>
</h4>
</h2>
<userLogin></userLogin>
</div>
<div class="copyright">
© 2025 武术赛事通 All Rights Reserved
</div>
</div>
</div>
</div>
</template>
<script>
import userLogin from './userlogin.vue';
import codeLogin from './codelogin.vue';
@@ -117,7 +131,6 @@ export default {
.dispatch('LoginBySocial', this.socialForm)
.then(() => {
window.location.href = topUrl.split(redirectUrl)[0];
//加载工作流路由集
this.loadFlowRoutes();
this.$router.push(this.tagWel);
loading.close();
@@ -139,7 +152,6 @@ export default {
.dispatch('LoginBySso', this.socialForm)
.then(() => {
window.location.href = topUrl.split(ssoCode)[0];
//加载工作流路由集
this.loadFlowRoutes();
this.$router.push(this.tagWel);
loading.close();
@@ -156,220 +168,256 @@ export default {
};
</script>
<style lang="scss">
<style lang="scss" scoped>
.login-container {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
background: radial-gradient(ellipse at bottom, #1b2735 0%, #090a0f 100%);
position: relative;
overflow: hidden;
min-height: 100vh;
background: #f5f5f5;
padding: 20px;
}
@function multiple-box-shadow($n) {
$value: '#{random(2000)}px #{random(2000)}px #FFF';
@for $i from 2 through $n {
$value: '#{$value} , #{random(2000)}px #{random(2000)}px #FFF';
}
@return unquote($value);
}
$shadows-small: multiple-box-shadow(700);
$shadows-medium: multiple-box-shadow(200);
$shadows-big: multiple-box-shadow(100);
.stars {
width: 1px;
height: 1px;
background: transparent;
box-shadow: $shadows-small;
animation: animateStars 50s linear infinite;
&:after {
content: ' ';
position: absolute;
top: 2000px;
width: 1px;
height: 1px;
background: transparent;
box-shadow: $shadows-small;
}
}
.stars2 {
width: 2px;
height: 2px;
background: transparent;
box-shadow: $shadows-medium;
animation: animateStars 100s linear infinite;
&:after {
content: ' ';
position: absolute;
top: 2000px;
width: 2px;
height: 2px;
background: transparent;
box-shadow: $shadows-medium;
}
}
.stars3 {
width: 3px;
height: 3px;
background: transparent;
box-shadow: $shadows-big;
animation: animateStars 150s linear infinite;
&:after {
content: ' ';
position: absolute;
top: 2000px;
width: 3px;
height: 3px;
background: transparent;
box-shadow: $shadows-big;
}
}
@keyframes animateStars {
from {
transform: translateY(0px);
}
to {
transform: translateY(-2000px);
}
}
.login-weaper {
position: relative;
z-index: 2;
.login-wrapper {
display: flex;
width: 1000px;
display: flex;
border-radius: 8px;
box-shadow: 0 0 30px rgba(0, 0, 0, 0.3);
background: rgba(255, 255, 255, 0.95);
max-width: 100%;
min-height: 600px;
background: #fff;
border-radius: 16px;
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.12);
overflow: hidden;
backdrop-filter: blur(10px);
}
// 左侧图形装饰区
.login-left {
position: relative;
width: 50%;
background: linear-gradient(135deg, rgba(27, 39, 53, 0.95) 0%, rgba(9, 10, 15, 0.95) 100%);
padding: 40px;
position: relative;
color: #fff;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(circle at 50% 50%, rgba(33, 147, 176, 0.3) 0%, transparent 50%),
radial-gradient(circle at 20% 80%, rgba(40, 60, 190, 0.3) 0%, transparent 50%);
pointer-events: none;
}
}
.login-time {
position: absolute;
top: 20px;
left: 20px;
font-size: 14px;
opacity: 0.8;
}
.login-left-content {
height: 100%;
background: linear-gradient(135deg, #dc2626 0%, #991b1b 100%);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
}
.title {
font-size: 28px;
font-weight: 500;
margin-bottom: 10px;
}
.subtitle {
font-size: 16px;
opacity: 0.8;
margin-bottom: 40px;
}
.feature-box {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
width: 100%;
max-width: 400px;
}
.feature-item {
background: rgba(255, 255, 255, 0.1);
padding: 15px 10px;
border-radius: 4px;
text-align: center;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
border: 1px solid rgba(255, 255, 255, 0.1);
position: relative;
justify-content: center;
padding: 60px 40px;
overflow: hidden;
backdrop-filter: blur(5px);
}
&::before {
content: '';
.decoration-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
.circle {
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: 0.5s;
}
border-radius: 50%;
background: rgba(255, 255, 255, 0.08);
&:hover {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
&.circle-1 {
width: 400px;
height: 400px;
top: -100px;
right: -100px;
animation: float 8s ease-in-out infinite;
}
&::before {
left: 100%;
&.circle-2 {
width: 300px;
height: 300px;
bottom: -80px;
left: -80px;
animation: float 10s ease-in-out infinite reverse;
}
&.circle-3 {
width: 200px;
height: 200px;
top: 50%;
left: 20%;
transform: translate(-50%, -50%);
animation: float 6s ease-in-out infinite;
}
}
}
.login-border {
width: 50%;
padding: 40px;
background: #fff;
display: flex;
align-items: center;
@keyframes float {
0%, 100% {
transform: translateY(0) scale(1);
}
50% {
transform: translateY(-20px) scale(1.05);
}
}
.login-main {
.content-wrapper {
position: relative;
z-index: 1;
color: #fff;
text-align: center;
width: 100%;
max-width: 360px;
}
.logo-area {
margin-bottom: 50px;
}
.logo-circle {
display: inline-flex;
align-items: center;
justify-content: center;
width: 120px;
height: 120px;
background: rgba(255, 255, 255, 0.15);
border: 3px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
margin-bottom: 30px;
backdrop-filter: blur(10px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.logo-icon {
font-size: 56px;
font-weight: 900;
color: #fff;
}
.brand-title {
font-size: 36px;
font-weight: 700;
margin: 0 0 12px 0;
letter-spacing: 4px;
}
.brand-subtitle {
font-size: 14px;
margin: 0;
opacity: 0.9;
font-weight: 300;
letter-spacing: 1px;
}
.feature-points {
display: flex;
flex-direction: column;
gap: 20px;
max-width: 280px;
margin: 0 auto;
}
.point-item {
display: flex;
align-items: center;
gap: 15px;
padding: 16px 20px;
background: rgba(255, 255, 255, 0.1);
border-radius: 12px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.15);
transition: all 0.3s ease;
font-size: 15px;
i {
font-size: 22px;
}
&:hover {
background: rgba(255, 255, 255, 0.15);
transform: translateX(8px);
}
}
// 右侧登录表单区
.login-right {
width: 50%;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 60px 50px 30px;
background: #fff;
}
.login-form-container {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.login-title {
font-size: 20px;
font-size: 28px;
color: #333;
margin-bottom: 30px;
font-weight: 700;
margin: 0 0 40px 0;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
gap: 12px;
position: relative;
&::after {
content: '';
position: absolute;
bottom: -15px;
left: 50%;
transform: translateX(-50%);
width: 60px;
height: 3px;
background: linear-gradient(90deg, #dc2626 0%, #991b1b 100%);
border-radius: 2px;
}
}
.copyright {
text-align: center;
font-size: 12px;
color: #999;
padding-top: 20px;
border-top: 1px solid #f0f0f0;
margin-top: 30px;
}
// 响应式设计
@media (max-width: 768px) {
.login-wrapper {
flex-direction: column;
min-height: auto;
}
.login-left,
.login-right {
width: 100%;
}
.login-left {
padding: 40px 30px;
}
.login-right {
padding: 40px 30px 25px;
}
.logo-circle {
width: 90px;
height: 90px;
margin-bottom: 20px;
}
.logo-icon {
font-size: 42px;
}
.brand-title {
font-size: 28px;
}
.logo-area {
margin-bottom: 30px;
}
.feature-points {
max-width: 100%;
}
}
</style>

View File

@@ -412,4 +412,102 @@ export default {
};
</script>
<style></style>
<style lang="scss" scoped>
.login-form {
:deep(.el-form-item) {
margin-bottom: 24px;
&:last-child {
margin-bottom: 0;
}
}
:deep(.el-input) {
.el-input__wrapper {
border-radius: 8px;
padding: 8px 15px;
box-shadow: 0 0 0 1px #e0e0e0 inset;
transition: all 0.3s;
&:hover {
box-shadow: 0 0 0 1px #dc2626 inset;
}
&.is-focus {
box-shadow: 0 0 0 2px #dc2626 inset;
}
}
.el-input__inner {
height: 40px;
line-height: 40px;
font-size: 14px;
color: #333;
&::placeholder {
color: #999;
}
}
.el-input__prefix {
font-size: 16px;
color: #666;
}
.el-input__suffix {
.el-icon-view {
color: #666;
cursor: pointer;
font-size: 16px;
&:hover {
color: #dc2626;
}
}
}
}
.login-code {
height: 40px;
margin-left: 8px;
border-radius: 8px;
overflow: hidden;
border: 1px solid #e0e0e0;
cursor: pointer;
transition: all 0.3s;
&:hover {
border-color: #dc2626;
box-shadow: 0 2px 8px rgba(220, 38, 38, 0.15);
}
.login-code-img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
}
.login-submit {
width: 100%;
height: 44px;
border-radius: 8px;
font-size: 15px;
font-weight: 600;
letter-spacing: 1px;
background: linear-gradient(135deg, #dc2626 0%, #991b1b 100%);
border: none;
transition: all 0.3s;
&:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(220, 38, 38, 0.3);
}
&:active {
transform: translateY(0);
}
}
}
</style>

View File

@@ -47,13 +47,39 @@ export default [
component: Layout,
redirect: '/martial/order/list',
children: [
{
path: 'competition/list',
name: '赛事管理',
meta: {
keepAlive: false,
},
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/competition/list.vue'),
},
{
path: 'competition/create',
name: '赛事详情',
meta: {
keepAlive: false,
menu: false,
},
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/competition/create.vue'),
},
{
path: 'registration/detail',
name: '报名详情',
meta: {
keepAlive: false,
menu: false,
},
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/registration/detail.vue'),
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/registration/index.vue'),
},
{
path: 'order/list',
name: '订单管理',
meta: {
keepAlive: false,
},
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/order/index.vue'),
},
{
path: 'schedule/list',
@@ -61,7 +87,7 @@ export default [
meta: {
keepAlive: false,
},
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/schedule/list.vue'),
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/schedule/index.vue'),
},
{
path: 'dispatch/list',
@@ -69,7 +95,48 @@ export default [
meta: {
keepAlive: false,
},
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/dispatch/list.vue'),
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/dispatch/index.vue'),
},
{
path: 'banner/index',
name: '轮播图管理',
meta: {
keepAlive: true,
},
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/banner/index.vue'),
},
{
path: 'referee/list',
name: '评委管理',
meta: {
keepAlive: true,
},
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/referee/index.vue'),
},
{
path: 'score/index',
name: '评分管理',
meta: {
keepAlive: true,
},
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/score/index.vue'),
},
{
path: 'participant/list',
name: '参赛选手管理',
meta: {
keepAlive: false,
},
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/participant/list.vue'),
},
{
path: 'participant/manage',
name: '选手详情',
meta: {
keepAlive: false,
menu: false,
},
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/participant/manage.vue'),
},
],
},

View File

@@ -1,3 +1,14 @@
// Element Plus 主题色覆盖 - 改为红色主题
:root {
--el-color-primary: #dc2626;
--el-color-primary-light-3: #ef4444;
--el-color-primary-light-5: #f87171;
--el-color-primary-light-7: #fca5a5;
--el-color-primary-light-8: #fecaca;
--el-color-primary-light-9: #fee2e2;
--el-color-primary-dark-2: #b91c1c;
}
.el-card.is-always-shadow {
box-shadow: none;
border: none !important;

View File

@@ -6,10 +6,10 @@
position: relative;
height: 100%;
position: relative;
background-color: #031527;
background: linear-gradient(180deg, #1a1a1a 0%, #2d2d2d 100%);
transition: width .2s;
box-sizing: border-box;
box-shadow: 2px 0 6px rgba(0, 21, 41, .35);
box-shadow: 2px 0 6px rgba(0, 0, 0, .15);
.el-scrollbar__wrap {
overflow-x: hidden;
@@ -60,11 +60,11 @@
left: 0;
bottom: 0;
width: 4px;
background: #409eff;
background: linear-gradient(180deg, #dc2626 0%, #991b1b 100%);
position: absolute;
}
background-color: rgba(0, 0, 0, .8);
background-color: rgba(220, 38, 38, .12);
i, span {
color: #fff;

View File

@@ -32,8 +32,8 @@
color: #ccc;
&.is-active {
color: #409EFF;
border-bottom: 3px solid #409EFF;
color: #dc2626;
border-bottom: 3px solid #dc2626;
}
}
@@ -85,7 +85,7 @@
}
&:hover {
background-color: #409EFF;
background-color: #dc2626;
color: #fff;
}
}

View File

@@ -116,11 +116,11 @@
.avue-logo {
height: $top_height;
line-height: $top_height;
background-color: #031527;
background: linear-gradient(135deg, #dc2626 0%, #991b1b 100%);
font-size: 20px;
overflow: hidden;
box-sizing: border-box;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.15);
box-shadow: 0 2px 4px 0 rgba(220, 38, 38, 0.2);
color: #fff;
&_title {

View File

@@ -0,0 +1,434 @@
<template>
<div class="banner-container">
<el-card shadow="hover">
<div class="page-header">
<h2 class="page-title">轮播图管理</h2>
<el-button type="primary" size="small" icon="el-icon-plus" @click="handleAdd">
新增轮播图
</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"
>
<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: 120px">
<el-option label="全部" :value="null"></el-option>
<el-option label="启用" :value="1"></el-option>
<el-option label="禁用" :value="0"></el-option>
</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="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="title" label="轮播图标题" min-width="150"></el-table-column>
<el-table-column prop="imageUrl" label="轮播图图片" width="150" align="center">
<template #default="scope">
<el-image
v-if="scope.row.imageUrl"
:src="scope.row.imageUrl"
:preview-src-list="[scope.row.imageUrl]"
style="width: 100px; height: 50px; cursor: pointer;"
fit="cover"
/>
<span v-else>暂无图片</span>
</template>
</el-table-column>
<el-table-column prop="linkUrl" label="跳转链接" min-width="180" show-overflow-tooltip></el-table-column>
<el-table-column prop="sortOrder" label="排序" width="80" align="center"></el-table-column>
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="scope">
<el-switch
v-model="scope.row.status"
:active-value="1"
:inactive-value="0"
active-text="启用"
inactive-text="禁用"
@change="handleStatusChange(scope.row)"
/>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="160"></el-table-column>
<el-table-column label="操作" width="180" align="center" fixed="right">
<template #default="scope">
<el-button type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="danger" size="small" @click="handleDelete(scope.row)">删除</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>
<!-- 新增/编辑弹窗 -->
<el-dialog
:title="dialogTitle"
:visible.sync="dialogVisible"
width="600px"
:close-on-click-modal="false"
@close="handleDialogClose"
>
<el-form ref="bannerForm" :model="formData" :rules="rules" label-width="100px">
<el-form-item label="标题" prop="title">
<el-input v-model="formData.title" placeholder="请输入轮播图标题" clearable></el-input>
</el-form-item>
<el-form-item label="图片链接" prop="imageUrl">
<el-input v-model="formData.imageUrl" placeholder="请输入图片链接地址" clearable>
<template #append>
<el-button @click="formData.imageUrl = 'https://via.placeholder.com/800x400'">使用示例</el-button>
</template>
</el-input>
<div v-if="formData.imageUrl" style="margin-top: 10px;">
<el-image :src="formData.imageUrl" style="width: 200px; height: 100px;" fit="cover" />
</div>
</el-form-item>
<el-form-item label="跳转链接" prop="linkUrl">
<el-input v-model="formData.linkUrl" placeholder="选填,点击轮播图跳转的链接地址" clearable></el-input>
</el-form-item>
<el-form-item label="排序" prop="sortOrder">
<el-input-number v-model="formData.sortOrder" :min="0" :max="999" style="width: 100%"></el-input-number>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio :label="1">启用</el-radio>
<el-radio :label="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
export default {
name: 'BannerList',
data() {
return {
loading: false,
submitLoading: false,
dialogVisible: false,
dialogTitle: '新增轮播图',
isEdit: false,
searchForm: {
keyword: '',
status: null
},
allTableData: [
{
id: 1,
title: '2025年全国武术锦标赛',
imageUrl: 'https://via.placeholder.com/800x400/dc2626/ffffff?text=2025年全国武术锦标赛',
linkUrl: '/martial/competition/list',
sortOrder: 1,
status: 1,
createTime: '2025-11-20 10:00:00'
},
{
id: 2,
title: '青少年武术大赛',
imageUrl: 'https://via.placeholder.com/800x400/991b1b/ffffff?text=青少年武术大赛',
linkUrl: '/martial/competition/list',
sortOrder: 2,
status: 1,
createTime: '2025-11-21 11:00:00'
},
{
id: 3,
title: '传统武术邀请赛',
imageUrl: 'https://via.placeholder.com/800x400/dc2626/ffffff?text=传统武术邀请赛',
linkUrl: '/martial/competition/list',
sortOrder: 3,
status: 0,
createTime: '2025-11-22 14:00:00'
}
],
tableData: [],
pagination: {
current: 1,
size: 10,
total: 0
},
formData: {
id: null,
title: '',
imageUrl: '',
linkUrl: '',
sortOrder: 0,
status: 1
},
rules: {
title: [
{ required: true, message: '请输入轮播图标题', trigger: 'blur' }
],
imageUrl: [
{ required: true, message: '请输入图片链接', trigger: 'blur' }
],
sortOrder: [
{ required: true, message: '请输入排序', trigger: 'blur' }
],
status: [
{ required: true, message: '请选择状态', trigger: 'change' }
]
}
}
},
mounted() {
this.loadBannerList()
},
methods: {
// 从 localStorage 加载数据
loadBannerList() {
const savedData = localStorage.getItem('bannerList')
if (savedData) {
try {
this.allTableData = JSON.parse(savedData)
} catch (e) {
console.error('加载轮播图数据失败', e)
}
} else {
// 首次加载,保存默认数据
this.saveBannerList()
}
this.fetchData()
},
// 保存数据到 localStorage
saveBannerList() {
localStorage.setItem('bannerList', JSON.stringify(this.allTableData))
},
// 获取数据
fetchData() {
this.loading = true
setTimeout(() => {
// 过滤数据
let filteredData = [...this.allTableData]
// 搜索过滤
if (this.searchForm.keyword) {
const keyword = this.searchForm.keyword.toLowerCase()
filteredData = filteredData.filter(item =>
item.title.toLowerCase().includes(keyword)
)
}
// 状态过滤
if (this.searchForm.status !== null && this.searchForm.status !== '') {
filteredData = filteredData.filter(item => item.status === this.searchForm.status)
}
// 按排序字段排序
filteredData.sort((a, b) => a.sortOrder - b.sortOrder)
this.pagination.total = filteredData.length
// 分页处理
const start = (this.pagination.current - 1) * this.pagination.size
const end = start + this.pagination.size
this.tableData = filteredData.slice(start, end)
this.loading = false
}, 300)
},
// 搜索
handleSearch() {
this.pagination.current = 1
this.fetchData()
},
// 重置
handleReset() {
this.searchForm = {
keyword: '',
status: null
}
this.pagination.current = 1
this.fetchData()
},
// 分页
handleSizeChange(size) {
this.pagination.size = size
this.fetchData()
},
handleCurrentChange(current) {
this.pagination.current = current
this.fetchData()
},
// 新增
handleAdd() {
this.dialogTitle = '新增轮播图'
this.isEdit = false
this.dialogVisible = true
},
// 编辑
handleEdit(row) {
this.dialogTitle = '编辑轮播图'
this.isEdit = true
this.formData = { ...row }
this.dialogVisible = true
},
// 删除
handleDelete(row) {
this.$confirm('确定删除该轮播图吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const index = this.allTableData.findIndex(item => item.id === row.id)
if (index !== -1) {
this.allTableData.splice(index, 1)
this.saveBannerList()
this.$message.success('删除成功')
this.fetchData()
}
}).catch(() => {})
},
// 状态变更
handleStatusChange(row) {
const index = this.allTableData.findIndex(item => item.id === row.id)
if (index !== -1) {
this.allTableData[index].status = row.status
this.saveBannerList()
this.$message.success('状态更新成功')
}
},
// 提交表单
handleSubmit() {
this.$refs.bannerForm.validate((valid) => {
if (valid) {
this.submitLoading = true
setTimeout(() => {
if (this.isEdit) {
// 编辑
const index = this.allTableData.findIndex(item => item.id === this.formData.id)
if (index !== -1) {
this.allTableData[index] = { ...this.formData }
this.$message.success('修改成功')
}
} else {
// 新增
const newId = this.allTableData.length > 0
? Math.max(...this.allTableData.map(item => item.id)) + 1
: 1
const now = new Date()
const createTime = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`
this.allTableData.push({
...this.formData,
id: newId,
createTime
})
this.$message.success('新增成功')
}
this.saveBannerList()
this.dialogVisible = false
this.fetchData()
this.submitLoading = false
}, 500)
}
})
},
// 关闭弹窗
handleDialogClose() {
this.$refs.bannerForm.resetFields()
this.formData = {
id: null,
title: '',
imageUrl: '',
linkUrl: '',
sortOrder: 0,
status: 1
}
}
}
}
</script>
<style lang="scss" scoped>
.banner-container {
min-height: 100%;
padding: 15px;
.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;
}
}
.search-form {
margin-bottom: 15px;
}
.pagination {
margin-top: 15px;
text-align: right;
}
.dialog-footer {
text-align: right;
}
}
</style>

View File

@@ -0,0 +1,513 @@
<template>
<div class="competition-create">
<div class="page-header">
<el-button
icon="el-icon-arrow-left"
@click="handleBack"
>
返回列表
</el-button>
<h2 class="page-title">{{ pageTitle }}</h2>
<div class="header-actions" v-if="mode !== 'view'">
<el-button @click="handleBack">取消</el-button>
<el-button type="primary" @click="handleSave">
{{ mode === '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="mode === 'view'"
class="competition-form"
>
<!-- 基本信息 -->
<div class="form-section">
<div class="section-title">
<i class="el-icon-document"></i>
基本信息
</div>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="赛事名称" prop="competitionName">
<el-input
v-model="formData.competitionName"
placeholder="请输入赛事名称"
maxlength="100"
show-word-limit
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="主办单位" prop="organizer">
<el-input
v-model="formData.organizer"
placeholder="请输入主办单位"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="举办地点" prop="location">
<el-input
v-model="formData.location"
placeholder="请输入举办地点"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="比赛场馆" prop="venue">
<el-input
v-model="formData.venue"
placeholder="请输入比赛场馆"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="报名时间" prop="registrationTime">
<el-input
v-model="formData.registrationTime"
placeholder="例如2025-01-01 至 2025-02-28"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="比赛时间" prop="competitionTime">
<el-input
v-model="formData.competitionTime"
placeholder="例如2025-03-15 至 2025-03-20"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="赛事状态" prop="status">
<el-select v-model="formData.status" placeholder="请选择状态" style="width: 100%">
<el-option label="未开始" :value="1" />
<el-option label="报名中" :value="2" />
<el-option label="进行中" :value="3" />
<el-option label="已结束" :value="4" />
</el-select>
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 信息发布 -->
<div class="form-section">
<div class="section-title">
<i class="el-icon-reading"></i>
信息发布
</div>
<el-form-item label="赛事简介" prop="introduction">
<el-input
v-model="formData.introduction"
type="textarea"
:rows="4"
placeholder="请输入赛事简介"
maxlength="500"
show-word-limit
/>
</el-form-item>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="联系人" prop="contactPerson">
<el-input
v-model="formData.contactPerson"
placeholder="请输入联系人姓名"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="联系电话" prop="contactPhone">
<el-input
v-model="formData.contactPhone"
placeholder="请输入联系电话"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="联系邮箱" prop="contactEmail">
<el-input
v-model="formData.contactEmail"
placeholder="请输入联系邮箱"
/>
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 赛事规程 -->
<div class="form-section">
<div class="section-title">
<i class="el-icon-document-checked"></i>
赛事规程
</div>
<el-form-item label="比赛规则" prop="rules">
<el-input
v-model="formData.rules"
type="textarea"
:rows="4"
placeholder="请输入比赛规则"
/>
</el-form-item>
<el-form-item label="参赛要求" prop="requirements">
<el-input
v-model="formData.requirements"
type="textarea"
:rows="4"
placeholder="请输入参赛要求"
/>
</el-form-item>
<el-form-item label="奖项设置" prop="awards">
<el-input
v-model="formData.awards"
type="textarea"
:rows="4"
placeholder="请输入奖项设置"
/>
</el-form-item>
</div>
<!-- 活动日程 -->
<div class="form-section">
<div class="section-title">
<i class="el-icon-date"></i>
活动日程
</div>
<el-form-item label="">
<el-button
v-if="mode !== 'view'"
type="primary"
icon="el-icon-plus"
size="small"
@click="handleAddSchedule"
>
添加日程
</el-button>
</el-form-item>
<el-table
:data="formData.schedule"
border
style="width: 100%"
>
<el-table-column
label="日期"
width="150"
>
<template #default="scope">
<el-input
v-if="mode !== 'view'"
v-model="scope.row.date"
placeholder="2025-03-15"
size="small"
/>
<span v-else>{{ scope.row.date }}</span>
</template>
</el-table-column>
<el-table-column
label="时间"
width="150"
>
<template #default="scope">
<el-input
v-if="mode !== 'view'"
v-model="scope.row.time"
placeholder="09:00-12:00"
size="small"
/>
<span v-else>{{ scope.row.time }}</span>
</template>
</el-table-column>
<el-table-column
label="赛事项目"
min-width="200"
>
<template #default="scope">
<el-input
v-if="mode !== 'view'"
v-model="scope.row.event"
placeholder="请输入赛事项目"
size="small"
/>
<span v-else>{{ scope.row.event }}</span>
</template>
</el-table-column>
<el-table-column
label="比赛场馆"
width="150"
>
<template #default="scope">
<el-input
v-if="mode !== 'view'"
v-model="scope.row.venue"
placeholder="请输入场馆"
size="small"
/>
<span v-else>{{ scope.row.venue }}</span>
</template>
</el-table-column>
<el-table-column
v-if="mode !== 'view'"
label="操作"
width="80"
align="center"
>
<template #default="scope">
<el-button
type="danger"
link
size="small"
icon="el-icon-delete"
@click="handleDeleteSchedule(scope.$index)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-form>
</div>
</template>
<script>
export default {
name: 'CompetitionCreate',
data() {
return {
mode: 'create', // create, edit, view
competitionId: null,
formData: {
competitionName: '',
organizer: '',
location: '',
venue: '',
registrationTime: '',
competitionTime: '',
status: 1,
introduction: '',
contactPerson: '',
contactPhone: '',
contactEmail: '',
rules: '',
requirements: '',
awards: '',
schedule: []
},
formRules: {
competitionName: [
{ required: true, message: '请输入赛事名称', trigger: 'blur' }
],
organizer: [
{ required: true, message: '请输入主办单位', trigger: 'blur' }
],
location: [
{ required: true, message: '请输入举办地点', trigger: 'blur' }
]
}
};
},
computed: {
pageTitle() {
const titleMap = {
create: '新建赛事',
edit: '编辑赛事',
view: '查看赛事'
};
return titleMap[this.mode] || '赛事信息';
}
},
created() {
this.initPage();
},
methods: {
initPage() {
const { mode, id } = this.$route.query;
this.mode = mode || 'create';
this.competitionId = id ? parseInt(id) : null;
if (this.mode !== 'create' && this.competitionId) {
this.loadCompetitionData();
}
},
loadCompetitionData() {
// 从 localStorage 加载数据
const savedData = localStorage.getItem('competitionList');
if (savedData) {
try {
const list = JSON.parse(savedData);
const competition = list.find(item => item.id === this.competitionId);
if (competition) {
this.formData = { ...competition };
}
} catch (e) {
console.error('加载数据失败', e);
this.$message.error('加载数据失败');
}
}
},
handleAddSchedule() {
this.formData.schedule.push({
date: '',
time: '',
event: '',
venue: ''
});
},
handleDeleteSchedule(index) {
this.formData.schedule.splice(index, 1);
},
handleSave() {
this.$refs.formRef.validate((valid) => {
if (valid) {
const savedData = localStorage.getItem('competitionList');
let list = savedData ? JSON.parse(savedData) : [];
if (this.mode === 'create') {
// 新建
const newId = list.length > 0 ? Math.max(...list.map(item => item.id)) + 1 : 1;
const newCompetition = {
...this.formData,
id: newId,
posterImages: [],
regulationFiles: []
};
list.push(newCompetition);
this.$message.success('创建成功');
} else if (this.mode === 'edit') {
// 编辑
const index = list.findIndex(item => item.id === this.competitionId);
if (index !== -1) {
list[index] = {
...list[index],
...this.formData
};
this.$message.success('保存成功');
}
}
localStorage.setItem('competitionList', JSON.stringify(list));
// 返回列表页
setTimeout(() => {
this.handleBack();
}, 500);
} else {
this.$message.error('请完善必填信息');
}
});
},
switchToEdit() {
this.$router.push({
path: '/martial/competition/create',
query: { mode: 'edit', id: this.competitionId }
});
},
handleBack() {
this.$router.push('/martial/competition/list');
}
}
};
</script>
<style lang="scss" scoped>
.competition-create {
padding: 20px;
background: #fff;
border-radius: 8px;
min-height: calc(100vh - 120px);
}
.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;
}
}
.competition-form {
max-width: 1200px;
}
.form-section {
margin-bottom: 40px;
padding: 24px;
background: #fafafa;
border-radius: 8px;
&:last-child {
margin-bottom: 0;
}
}
.section-title {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 24px;
font-size: 18px;
font-weight: 600;
color: #333;
i {
color: #dc2626;
font-size: 20px;
}
}
:deep(.el-form-item) {
margin-bottom: 22px;
}
:deep(.el-table) {
.el-input {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,497 @@
<template>
<div class="competition-create-container">
<el-card shadow="hover">
<div class="page-header">
<el-button icon="el-icon-back" size="small" @click="goBack">返回</el-button>
<h2 class="page-title">新建赛事</h2>
</div>
<el-steps :active="currentStep" align-center class="steps-container" finish-status="success">
<el-step title="基本信息"></el-step>
<el-step title="信息发布"></el-step>
<el-step title="赛事规程"></el-step>
<el-step title="活动日程"></el-step>
</el-steps>
<el-form ref="competitionForm" :model="formData" :rules="rules" label-width="120px" class="form-container">
<!-- Step 1: 基本信息 -->
<div v-show="currentStep === 0" class="form-step">
<h3 class="step-title">赛事基本信息</h3>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="赛事名称" prop="competitionName">
<el-input v-model="formData.competitionName" placeholder="请输入赛事名称" clearable></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="主办单位" prop="organizer">
<el-input v-model="formData.organizer" placeholder="请输入主办单位" clearable></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="地区" prop="location">
<el-input v-model="formData.location" placeholder="例如:四川省 成都市" clearable></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="详细地点" prop="venue">
<el-input v-model="formData.venue" placeholder="请输入详细地点" clearable></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="报名时间" prop="registrationTime">
<el-date-picker
v-model="formData.registrationTime"
type="datetimerange"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
></el-date-picker>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="比赛时间" prop="competitionTime">
<el-date-picker
v-model="formData.competitionTime"
type="datetimerange"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
></el-date-picker>
</el-form-item>
</el-col>
</el-row>
</div>
<!-- Step 2: 信息发布 -->
<div v-show="currentStep === 1" class="form-step">
<h3 class="step-title">信息发布</h3>
<el-form-item label="赛事简介" prop="introduction">
<el-input
v-model="formData.introduction"
type="textarea"
:rows="8"
placeholder="请输入赛事简介,支持详细描述赛事背景、特色等信息"
></el-input>
</el-form-item>
<el-form-item label="宣传图片" prop="posterImages">
<el-upload
class="upload-demo"
action="#"
:auto-upload="false"
:on-change="handlePosterChange"
:on-remove="handlePosterRemove"
:file-list="posterFileList"
list-type="picture-card"
accept="image/*"
>
<i class="el-icon-plus"></i>
<div slot="tip" class="el-upload__tip">建议上传jpg/png文件且不超过2MB</div>
</el-upload>
</el-form-item>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="联系人" prop="contactPerson">
<el-input v-model="formData.contactPerson" placeholder="请输入联系人姓名" clearable></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="联系电话" prop="contactPhone">
<el-input v-model="formData.contactPhone" placeholder="请输入联系电话" clearable></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="联系邮箱" prop="contactEmail">
<el-input v-model="formData.contactEmail" placeholder="请输入联系邮箱" clearable></el-input>
</el-form-item>
</el-col>
</el-row>
</div>
<!-- Step 3: 赛事规程 -->
<div v-show="currentStep === 2" class="form-step">
<h3 class="step-title">赛事规程</h3>
<el-form-item label="竞赛规则" prop="rules">
<el-input
v-model="formData.rules"
type="textarea"
:rows="6"
placeholder="请输入竞赛规则,包括比赛项目、评分标准等"
></el-input>
</el-form-item>
<el-form-item label="参赛要求" prop="requirements">
<el-input
v-model="formData.requirements"
type="textarea"
:rows="4"
placeholder="请输入参赛要求,如年龄限制、资格要求等"
></el-input>
</el-form-item>
<el-form-item label="奖项设置" prop="awards">
<el-input
v-model="formData.awards"
type="textarea"
:rows="4"
placeholder="请输入奖项设置,如一等奖、二等奖等"
></el-input>
</el-form-item>
<el-form-item label="规程文件" prop="regulationFiles">
<el-upload
class="upload-demo"
action="#"
:auto-upload="false"
:on-change="handleRegulationChange"
:on-remove="handleRegulationRemove"
:file-list="regulationFileList"
accept=".pdf,.doc,.docx"
>
<el-button size="small" type="primary">点击上传</el-button>
<div slot="tip" class="el-upload__tip">支持上传PDFWord文件单个文件不超过10MB</div>
</el-upload>
</el-form-item>
</div>
<!-- Step 4: 活动日程 -->
<div v-show="currentStep === 3" class="form-step">
<h3 class="step-title">活动日程</h3>
<div class="schedule-header">
<el-button type="primary" size="small" icon="el-icon-plus" @click="addScheduleItem">添加日程</el-button>
</div>
<el-table :data="formData.schedule" border stripe size="small" class="schedule-table">
<el-table-column type="index" label="序号" width="60" align="center"></el-table-column>
<el-table-column label="日期时间" min-width="200">
<template #default="scope">
<el-date-picker
v-model="scope.row.date"
type="datetime"
placeholder="选择日期时间"
value-format="YYYY-MM-DD HH:mm:ss"
size="small"
style="width: 100%"
></el-date-picker>
</template>
</el-table-column>
<el-table-column label="活动项目" min-width="150">
<template #default="scope">
<el-input v-model="scope.row.eventName" placeholder="请输入活动项目" size="small"></el-input>
</template>
</el-table-column>
<el-table-column label="地点" min-width="150">
<template #default="scope">
<el-input v-model="scope.row.venue" placeholder="请输入地点" size="small"></el-input>
</template>
</el-table-column>
<el-table-column label="备注" min-width="150">
<template #default="scope">
<el-input v-model="scope.row.remark" placeholder="请输入备注" size="small"></el-input>
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center" fixed="right">
<template #default="scope">
<el-button type="danger" size="small" icon="el-icon-delete" @click="deleteScheduleItem(scope.$index)" circle></el-button>
</template>
</el-table-column>
</el-table>
<div v-if="formData.schedule.length === 0" class="empty-schedule">
<i class="el-icon-calendar"></i>
<p>暂无日程安排点击上方"添加日程"按钮开始添加</p>
</div>
</div>
</el-form>
<!-- 底部操作按钮 -->
<div class="form-footer">
<el-button v-if="currentStep > 0" @click="prevStep">上一步</el-button>
<el-button v-if="currentStep < 3" type="primary" @click="nextStep">下一步</el-button>
<el-button v-if="currentStep === 3" type="success" :loading="submitLoading" @click="handleSubmit">提交</el-button>
<el-button @click="goBack">取消</el-button>
</div>
</el-card>
</div>
</template>
<script>
import { addCompetition } from '@/api/martial/competition'
export default {
name: 'CompetitionCreate',
data() {
return {
currentStep: 0,
submitLoading: false,
posterFileList: [],
regulationFileList: [],
formData: {
// 基本信息
competitionName: '',
organizer: '',
location: '',
venue: '',
registrationTime: [],
competitionTime: [],
// 信息发布
introduction: '',
posterImages: [],
contactPerson: '',
contactPhone: '',
contactEmail: '',
// 赛事规程
rules: '',
requirements: '',
awards: '',
regulationFiles: [],
// 活动日程
schedule: []
},
rules: {
competitionName: [
{ required: true, message: '请输入赛事名称', trigger: 'blur' }
],
organizer: [
{ required: true, message: '请输入主办单位', trigger: 'blur' }
],
location: [
{ required: true, message: '请输入地区', trigger: 'blur' }
],
venue: [
{ required: true, message: '请输入详细地点', trigger: 'blur' }
],
registrationTime: [
{ required: true, message: '请选择报名时间', trigger: 'change' }
],
competitionTime: [
{ required: true, message: '请选择比赛时间', trigger: 'change' }
],
contactPhone: [
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
],
contactEmail: [
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
]
}
}
},
methods: {
// 上一步
prevStep() {
if (this.currentStep > 0) {
this.currentStep--
}
},
// 下一步
nextStep() {
// 验证当前步骤的表单
this.$refs.competitionForm.validateField(this.getStepFields(this.currentStep), (valid) => {
if (!valid) {
this.currentStep++
}
})
},
// 获取每个步骤需要验证的字段
getStepFields(step) {
const fieldsMap = {
0: ['competitionName', 'organizer', 'location', 'venue', 'registrationTime', 'competitionTime'],
1: ['contactPhone', 'contactEmail'],
2: [],
3: []
}
return fieldsMap[step] || []
},
// 返回
goBack() {
this.$router.go(-1)
},
// 添加日程项
addScheduleItem() {
this.formData.schedule.push({
date: '',
eventName: '',
venue: '',
remark: ''
})
},
// 删除日程项
deleteScheduleItem(index) {
this.$confirm('确定删除该日程吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.formData.schedule.splice(index, 1)
this.$message.success('删除成功')
}).catch(() => {})
},
// 处理宣传图片上传
handlePosterChange(file, fileList) {
this.posterFileList = fileList
this.formData.posterImages = fileList.map(f => f.raw || f)
},
// 处理宣传图片删除
handlePosterRemove(file, fileList) {
this.posterFileList = fileList
this.formData.posterImages = fileList.map(f => f.raw || f)
},
// 处理规程文件上传
handleRegulationChange(file, fileList) {
this.regulationFileList = fileList
this.formData.regulationFiles = fileList.map(f => f.raw || f)
},
// 处理规程文件删除
handleRegulationRemove(file, fileList) {
this.regulationFileList = fileList
this.formData.regulationFiles = fileList.map(f => f.raw || f)
},
// 提交表单
handleSubmit() {
this.$refs.competitionForm.validate((valid) => {
if (valid) {
this.submitLoading = true
// 构建提交数据
const submitData = {
...this.formData,
registrationStartTime: this.formData.registrationTime[0],
registrationEndTime: this.formData.registrationTime[1],
competitionStartTime: this.formData.competitionTime[0],
competitionEndTime: this.formData.competitionTime[1]
}
// 删除临时字段
delete submitData.registrationTime
delete submitData.competitionTime
// 调用API
addCompetition(submitData).then(res => {
this.$message.success('赛事创建成功')
this.$router.push('/martial/order/list')
}).catch(err => {
this.$message.error(err.message || '创建失败')
}).finally(() => {
this.submitLoading = false
})
} else {
this.$message.warning('请完善必填信息')
return false
}
})
}
}
}
</script>
<style lang="scss" scoped>
.competition-create-container {
min-height: 100%;
padding: 15px;
.page-header {
display: flex;
align-items: center;
margin-bottom: 20px;
gap: 10px;
.page-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #2c3e50;
}
}
.steps-container {
margin-bottom: 30px;
padding: 20px 0;
}
.form-container {
min-height: 400px;
padding: 20px;
background: #f5f7fa;
border-radius: 4px;
.form-step {
.step-title {
margin-bottom: 20px;
padding-left: 12px;
font-size: 16px;
font-weight: 600;
color: #409eff;
border-left: 4px solid #409eff;
}
}
}
.upload-demo {
width: 100%;
}
.schedule-header {
margin-bottom: 15px;
}
.schedule-table {
margin-top: 15px;
}
.empty-schedule {
padding: 60px 0;
text-align: center;
color: #909399;
i {
font-size: 64px;
margin-bottom: 15px;
opacity: 0.3;
}
p {
margin: 0;
font-size: 14px;
}
}
.form-footer {
margin-top: 30px;
padding-top: 20px;
text-align: center;
border-top: 1px solid #dcdfe6;
.el-button {
min-width: 100px;
}
}
}
</style>

View File

@@ -0,0 +1,309 @@
<template>
<div class="competition-list">
<div class="list-header">
<h2 class="page-title">赛事管理</h2>
<el-button type="primary" icon="el-icon-plus" @click="handleCreate">
新建赛事
</el-button>
</div>
<el-table
:data="competitionList"
border
stripe
style="width: 100%"
class="data-table"
>
<el-table-column
type="index"
label="序号"
width="60"
align="center"
/>
<el-table-column
prop="competitionName"
label="赛事名称"
min-width="200"
show-overflow-tooltip
/>
<el-table-column
prop="organizer"
label="主办单位"
min-width="150"
show-overflow-tooltip
/>
<el-table-column
prop="location"
label="举办地点"
width="120"
/>
<el-table-column
prop="competitionTime"
label="比赛时间"
width="180"
/>
<el-table-column
prop="registrationTime"
label="报名时间"
width="180"
/>
<el-table-column
label="状态"
width="100"
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="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>
</div>
</template>
<script>
export default {
name: 'CompetitionList',
data() {
return {
competitionList: [
{
id: 1,
competitionName: '2025年全国武术锦标赛',
organizer: '国家体育总局武术运动管理中心',
location: '北京',
venue: '国家奥林匹克体育中心',
registrationTime: '2025-01-01 至 2025-02-28',
competitionTime: '2025-03-15 至 2025-03-20',
status: 1, // 1-未开始 2-报名中 3-进行中 4-已结束
introduction: '全国最高水平的武术竞技赛事',
posterImages: [],
contactPerson: '张主任',
contactPhone: '010-12345678',
contactEmail: 'contact@wushu.cn',
rules: '参赛选手必须持有国家二级运动员及以上证书',
requirements: '年龄18-35岁身体健康',
awards: '冠军奖金10万元亚军5万元季军3万元',
regulationFiles: [],
schedule: [
{ date: '2025-03-15', time: '09:00-12:00', event: '开幕式', venue: '主场馆' },
{ date: '2025-03-16', time: '09:00-18:00', event: '太极拳比赛', venue: 'A馆' },
{ date: '2025-03-17', time: '09:00-18:00', event: '长拳比赛', venue: 'B馆' },
]
},
{
id: 2,
competitionName: '2025年青少年武术大赛',
organizer: '中国武术协会',
location: '上海',
venue: '上海体育馆',
registrationTime: '2025-02-01 至 2025-03-31',
competitionTime: '2025-04-10 至 2025-04-15',
status: 2,
introduction: '面向青少年的武术竞技赛事',
posterImages: [],
contactPerson: '李教练',
contactPhone: '021-87654321',
contactEmail: 'youth@wushu.org',
rules: '年龄限制6-18岁',
requirements: '需提供学生证明',
awards: '设金银铜奖及优秀奖',
regulationFiles: [],
schedule: [
{ date: '2025-04-10', time: '09:00-12:00', event: '开幕式', venue: '主场馆' },
{ date: '2025-04-11', time: '09:00-18:00', event: '少年组比赛', venue: 'A馆' },
]
},
{
id: 3,
competitionName: '2025年传统武术邀请赛',
organizer: '中华武术联合会',
location: '杭州',
venue: '杭州国际博览中心',
registrationTime: '2025-03-01 至 2025-04-30',
competitionTime: '2025-05-20 至 2025-05-25',
status: 1,
introduction: '传统武术项目展示与竞技',
posterImages: [],
contactPerson: '王馆长',
contactPhone: '0571-23456789',
contactEmail: 'traditional@wushu.com',
rules: '限传统武术门派参赛',
requirements: '需提供师承证明',
awards: '金银铜奖及最佳表演奖',
regulationFiles: [],
schedule: [
{ date: '2025-05-20', time: '14:00-17:00', event: '报到', venue: '接待中心' },
{ date: '2025-05-21', time: '09:00-18:00', event: '初赛', venue: '比赛馆' },
]
}
]
};
},
mounted() {
this.loadCompetitionList();
},
methods: {
loadCompetitionList() {
// 从 localStorage 加载数据
const savedData = localStorage.getItem('competitionList');
if (savedData) {
try {
this.competitionList = JSON.parse(savedData);
} catch (e) {
console.error('加载数据失败', e);
}
} else {
// 首次加载,保存默认数据
this.saveCompetitionList();
}
},
saveCompetitionList() {
localStorage.setItem('competitionList', JSON.stringify(this.competitionList));
},
getStatusText(status) {
const statusMap = {
1: '未开始',
2: '报名中',
3: '进行中',
4: '已结束'
};
return statusMap[status] || '未知';
},
getStatusType(status) {
const typeMap = {
1: 'info',
2: 'success',
3: 'warning',
4: 'danger'
};
return typeMap[status] || '';
},
handleCreate() {
this.$router.push({
path: '/martial/competition/create',
query: { mode: 'create' }
});
},
handleView(row) {
this.$router.push({
path: '/martial/competition/create',
query: { mode: 'view', id: row.id }
});
},
handleEdit(row) {
this.$router.push({
path: '/martial/competition/create',
query: { mode: 'edit', id: row.id }
});
},
handleDelete(row) {
this.$confirm('确定要删除该赛事吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const index = this.competitionList.findIndex(item => item.id === row.id);
if (index !== -1) {
this.competitionList.splice(index, 1);
this.saveCompetitionList();
this.$message.success('删除成功');
}
}).catch(() => {});
}
}
};
</script>
<style lang="scss" scoped>
.competition-list {
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;
}
.data-table {
:deep(.el-table__header) {
th {
background-color: #fafafa;
color: #333;
font-weight: 600;
}
}
:deep(.el-button--text) {
padding: 0 8px;
}
}
</style>

View File

@@ -265,6 +265,9 @@ export default {
background: #fff;
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
.page-title {

View File

@@ -0,0 +1,536 @@
<template>
<div class="participant-container">
<el-card shadow="hover">
<div class="page-header">
<el-button icon="el-icon-back" size="small" @click="goBack">返回</el-button>
<h2 class="page-title">维护参赛选手 - {{ competitionInfo.competitionName }}</h2>
</div>
<!-- 赛事基本信息 -->
<div class="competition-info">
<el-descriptions :column="3" border size="small">
<el-descriptions-item label="赛事名称">{{ competitionInfo.competitionName }}</el-descriptions-item>
<el-descriptions-item label="主办单位">{{ competitionInfo.organizer }}</el-descriptions-item>
<el-descriptions-item label="比赛时间">{{ competitionInfo.competitionTime }}</el-descriptions-item>
</el-descriptions>
</div>
<!-- 操作按钮区 -->
<div class="action-bar">
<el-button type="primary" size="small" icon="el-icon-plus" @click="handleAdd">添加选手</el-button>
<el-button type="success" size="small" icon="el-icon-upload" @click="handleImport">导入选手</el-button>
<el-button type="warning" size="small" icon="el-icon-sort" @click="handleBatchSort">批量排序</el-button>
<el-button type="info" size="small" icon="el-icon-download" @click="handleExport">导出名单</el-button>
</div>
<!-- 参赛选手列表 -->
<el-table
v-loading="loading"
:data="tableData"
border
stripe
size="small"
row-key="id"
style="width: 100%"
>
<el-table-column type="index" label="序号" width="60" align="center"></el-table-column>
<el-table-column prop="orderNum" label="出场顺序" width="100" align="center">
<template #default="scope">
<el-input-number
v-model="scope.row.orderNum"
:min="1"
:max="9999"
size="small"
@change="handleOrderChange(scope.row)"
></el-input-number>
</template>
</el-table-column>
<el-table-column prop="playerName" label="选手姓名" width="120"></el-table-column>
<el-table-column prop="gender" label="性别" width="60" align="center">
<template #default="scope">
{{ scope.row.gender === 1 ? '男' : '女' }}
</template>
</el-table-column>
<el-table-column prop="age" label="年龄" width="60" align="center"></el-table-column>
<el-table-column prop="organization" label="所属单位" min-width="150" show-overflow-tooltip></el-table-column>
<el-table-column prop="projectName" label="参赛项目" min-width="120" show-overflow-tooltip></el-table-column>
<el-table-column prop="category" label="组别" width="100"></el-table-column>
<el-table-column prop="contactPhone" label="联系电话" width="120"></el-table-column>
<el-table-column label="附件" width="80" align="center">
<template #default="scope">
<el-button
v-if="scope.row.attachments && scope.row.attachments.length > 0"
type="text"
size="small"
@click="handleViewAttachments(scope.row)"
>
查看({{ scope.row.attachments.length }})
</el-button>
<span v-else style="color: #999;"></span>
</template>
</el-table-column>
<el-table-column label="操作" width="200" align="center" fixed="right">
<template #default="scope">
<el-button type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="danger" size="small" @click="handleDelete(scope.row)">删除</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>
<!-- 新增/编辑选手弹窗 -->
<el-dialog
:title="dialogTitle"
:visible.sync="dialogVisible"
width="700px"
:close-on-click-modal="false"
@close="handleDialogClose"
>
<el-form ref="playerForm" :model="formData" :rules="rules" label-width="100px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="选手姓名" prop="playerName">
<el-input v-model="formData.playerName" placeholder="请输入选手姓名" clearable></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<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-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="年龄" prop="age">
<el-input-number v-model="formData.age" :min="1" :max="150" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系电话" prop="contactPhone">
<el-input v-model="formData.contactPhone" placeholder="请输入联系电话" clearable maxlength="11"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="所属单位" prop="organization">
<el-input v-model="formData.organization" placeholder="请输入所属单位" clearable></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="身份证号" prop="idCard">
<el-input v-model="formData.idCard" placeholder="请输入身份证号" clearable maxlength="18"></el-input>
</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="formData.projectName" placeholder="请输入参赛项目" clearable></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="组别" prop="category">
<el-select v-model="formData.category" placeholder="请选择组别" style="width: 100%">
<el-option label="少儿组" value="少儿组"></el-option>
<el-option label="少年组" value="少年组"></el-option>
<el-option label="青年组" value="青年组"></el-option>
<el-option label="成人组" value="成人组"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="出场顺序" prop="orderNum">
<el-input-number v-model="formData.orderNum" :min="1" :max="9999" style="width: 100%"></el-input-number>
</el-form-item>
<el-form-item label="选手简介" prop="introduction">
<el-input
v-model="formData.introduction"
type="textarea"
:rows="4"
placeholder="请输入选手简介、成绩、特长等信息"
></el-input>
</el-form-item>
<el-form-item label="上传附件">
<el-upload
class="upload-demo"
action="#"
:auto-upload="false"
:on-change="handleFileChange"
:on-remove="handleFileRemove"
:file-list="fileList"
multiple
>
<el-button size="small" type="primary">点击上传</el-button>
<div slot="tip" class="el-upload__tip">支持上传PDFWord图片等文件单个文件不超过5MB</div>
</el-upload>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="formData.remark"
type="textarea"
:rows="3"
placeholder="请输入备注信息"
></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
</div>
</el-dialog>
<!-- 查看附件弹窗 -->
<el-dialog
title="附件列表"
:visible.sync="attachmentDialogVisible"
width="600px"
>
<el-table :data="currentAttachments" border stripe size="small">
<el-table-column type="index" label="序号" width="60" align="center"></el-table-column>
<el-table-column prop="name" label="文件名" min-width="200" show-overflow-tooltip></el-table-column>
<el-table-column prop="size" label="大小" width="100" align="center">
<template #default="scope">
{{ formatFileSize(scope.row.size) }}
</template>
</el-table-column>
<el-table-column label="操作" width="100" align="center">
<template #default="scope">
<el-button type="text" size="small" @click="handleDownload(scope.row)">下载</el-button>
</template>
</el-table-column>
</el-table>
</el-dialog>
</div>
</template>
<script>
import { getParticipantList, addParticipant, updateParticipant, removeParticipant, updateOrder } from '@/api/martial/participant'
export default {
name: 'ParticipantManage',
data() {
return {
competitionId: null,
loading: false,
submitLoading: false,
dialogVisible: false,
attachmentDialogVisible: false,
dialogTitle: '添加选手',
isEdit: false,
competitionInfo: {
competitionName: '',
organizer: '',
competitionTime: ''
},
tableData: [],
pagination: {
current: 1,
size: 10,
total: 0
},
formData: {
id: null,
playerName: '',
gender: 1,
age: null,
contactPhone: '',
organization: '',
idCard: '',
projectName: '',
category: '',
orderNum: 1,
introduction: '',
attachments: [],
remark: ''
},
fileList: [],
currentAttachments: [],
rules: {
playerName: [
{ required: true, message: '请输入选手姓名', trigger: 'blur' }
],
gender: [
{ required: true, message: '请选择性别', trigger: 'change' }
],
contactPhone: [
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
],
idCard: [
{ pattern: /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/, message: '请输入正确的身份证号', trigger: 'blur' }
],
projectName: [
{ required: true, message: '请输入参赛项目', trigger: 'blur' }
],
category: [
{ required: true, message: '请选择组别', trigger: 'change' }
],
orderNum: [
{ required: true, message: '请输入出场顺序', trigger: 'blur' }
]
}
}
},
mounted() {
this.competitionId = this.$route.query.competitionId
if (this.competitionId) {
this.loadCompetitionInfo()
this.fetchData()
} else {
this.$message.error('缺少赛事ID参数')
}
},
methods: {
// 加载赛事信息
loadCompetitionInfo() {
// 这里应该调用API获取赛事信息暂时使用模拟数据
this.competitionInfo = {
competitionName: '第三十届武术大赛',
organizer: '武术协会',
competitionTime: '2025-11-28 至 2025-12-01'
}
},
// 获取参赛选手列表
fetchData() {
this.loading = true
getParticipantList(this.competitionId, this.pagination.current, this.pagination.size)
.then(res => {
this.tableData = res.data.records || []
this.pagination.total = res.data.total || 0
})
.catch(err => {
this.$message.error(err.message || '获取数据失败')
})
.finally(() => {
this.loading = false
})
},
// 返回
goBack() {
this.$router.go(-1)
},
// 添加选手
handleAdd() {
this.dialogTitle = '添加选手'
this.isEdit = false
this.dialogVisible = true
},
// 编辑选手
handleEdit(row) {
this.dialogTitle = '编辑选手'
this.isEdit = true
this.formData = { ...row }
this.fileList = row.attachments || []
this.dialogVisible = true
},
// 删除选手
handleDelete(row) {
this.$confirm('确定删除该选手吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
removeParticipant(row.id).then(() => {
this.$message.success('删除成功')
this.fetchData()
}).catch(err => {
this.$message.error(err.message || '删除失败')
})
}).catch(() => {})
},
// 出场顺序变更
handleOrderChange(row) {
updateOrder(row.id, row.orderNum).then(() => {
this.$message.success('顺序更新成功')
this.fetchData()
}).catch(err => {
this.$message.error(err.message || '更新失败')
})
},
// 提交表单
handleSubmit() {
this.$refs.playerForm.validate((valid) => {
if (valid) {
this.submitLoading = true
const submitData = {
...this.formData,
competitionId: this.competitionId,
attachments: this.fileList
}
const apiMethod = this.isEdit ? updateParticipant : addParticipant
const successMsg = this.isEdit ? '修改成功' : '添加成功'
apiMethod(submitData)
.then(() => {
this.$message.success(successMsg)
this.dialogVisible = false
this.fetchData()
})
.catch(err => {
this.$message.error(err.message || '操作失败')
})
.finally(() => {
this.submitLoading = false
})
}
})
},
// 关闭弹窗
handleDialogClose() {
this.$refs.playerForm.resetFields()
this.formData = {
id: null,
playerName: '',
gender: 1,
age: null,
contactPhone: '',
organization: '',
idCard: '',
projectName: '',
category: '',
orderNum: 1,
introduction: '',
attachments: [],
remark: ''
}
this.fileList = []
},
// 文件上传
handleFileChange(file, fileList) {
this.fileList = fileList
},
handleFileRemove(file, fileList) {
this.fileList = fileList
},
// 查看附件
handleViewAttachments(row) {
this.currentAttachments = row.attachments || []
this.attachmentDialogVisible = true
},
// 下载附件
handleDownload(file) {
// 实现文件下载逻辑
this.$message.info('下载功能开发中')
},
// 格式化文件大小
formatFileSize(bytes) {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i]
},
// 导入选手
handleImport() {
this.$message.info('批量导入功能开发中')
},
// 批量排序
handleBatchSort() {
this.$message.info('批量排序功能开发中')
},
// 导出名单
handleExport() {
this.$message.info('导出功能开发中')
},
// 分页
handleSizeChange(size) {
this.pagination.size = size
this.fetchData()
},
handleCurrentChange(current) {
this.pagination.current = current
this.fetchData()
}
}
}
</script>
<style lang="scss" scoped>
.participant-container {
min-height: 100%;
padding: 15px;
.page-header {
display: flex;
align-items: center;
margin-bottom: 15px;
gap: 10px;
.page-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #2c3e50;
}
}
.competition-info {
margin-bottom: 15px;
}
.action-bar {
margin-bottom: 15px;
padding: 12px;
background: #f5f7fa;
border-radius: 4px;
}
.pagination {
margin-top: 15px;
text-align: right;
}
.dialog-footer {
text-align: right;
}
.upload-demo {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,372 @@
<template>
<div class="participant-list">
<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"
>
<i slot="prefix" class="el-input__icon el-icon-search"></i>
</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 competitionOptions"
: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-form-item>
</el-form>
<el-table
:data="participantList"
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="180"
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>
</div>
</template>
<script>
export default {
name: 'ParticipantList',
data() {
return {
searchForm: {
keyword: '',
competitionId: null
},
competitionOptions: [
{ id: 1, competitionName: '2025年全国武术锦标赛' },
{ id: 2, competitionName: '2025年青少年武术大赛' },
{ id: 3, competitionName: '2025年传统武术邀请赛' }
],
participantList: [
{
id: 1,
competitionId: 1,
competitionName: '2025年全国武术锦标赛',
playerName: '张伟',
gender: 1,
age: 25,
contactPhone: '13800138001',
organization: '北京武术队',
idCard: '110101199001011234',
projectName: '太极拳',
category: '成年男子组',
orderNum: 1,
introduction: '国家一级运动员',
attachments: []
},
{
id: 2,
competitionId: 1,
competitionName: '2025年全国武术锦标赛',
playerName: '李娜',
gender: 2,
age: 22,
contactPhone: '13800138002',
organization: '上海武术队',
idCard: '310101199201011234',
projectName: '长拳',
category: '成年女子组',
orderNum: 2,
introduction: '国家二级运动员',
attachments: []
},
{
id: 3,
competitionId: 2,
competitionName: '2025年青少年武术大赛',
playerName: '王小明',
gender: 1,
age: 16,
contactPhone: '13800138003',
organization: '广州市体校',
idCard: '440101200801011234',
projectName: '剑术',
category: '少年男子组',
orderNum: 1,
introduction: '市级青少年冠军',
attachments: []
},
{
id: 4,
competitionId: 2,
competitionName: '2025年青少年武术大赛',
playerName: '赵小红',
gender: 2,
age: 15,
contactPhone: '13800138004',
organization: '深圳市体校',
idCard: '440301200901011234',
projectName: '刀术',
category: '少年女子组',
orderNum: 2,
introduction: '省级青少年亚军',
attachments: []
},
{
id: 5,
competitionId: 3,
competitionName: '2025年传统武术邀请赛',
playerName: '孙师傅',
gender: 1,
age: 45,
contactPhone: '13800138005',
organization: '武当派',
idCard: '420101197901011234',
projectName: '太极剑',
category: '中年组',
orderNum: 1,
introduction: '武当第十五代传人',
attachments: []
}
]
};
},
mounted() {
this.loadParticipantList();
},
methods: {
loadParticipantList() {
// 从 localStorage 加载数据
const savedData = localStorage.getItem('participantList');
if (savedData) {
try {
this.participantList = JSON.parse(savedData);
} catch (e) {
console.error('加载数据失败', e);
}
} else {
// 首次加载,保存默认数据
this.saveParticipantList();
}
// 加载赛事选项
const competitionData = localStorage.getItem('competitionList');
if (competitionData) {
try {
this.competitionOptions = JSON.parse(competitionData);
} catch (e) {
console.error('加载赛事数据失败', e);
}
}
},
saveParticipantList() {
localStorage.setItem('participantList', JSON.stringify(this.participantList));
},
handleSearch() {
// 实现搜索逻辑
this.loadParticipantList();
},
handleCreate() {
this.$router.push({
path: '/martial/participant/manage',
query: { mode: 'create' }
});
},
handleView(row) {
this.$router.push({
path: '/martial/participant/manage',
query: { mode: 'view', id: row.id }
});
},
handleEdit(row) {
this.$router.push({
path: '/martial/participant/manage',
query: { mode: 'edit', id: row.id }
});
},
handleDelete(row) {
this.$confirm('确定要删除该选手吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const index = this.participantList.findIndex(item => item.id === row.id);
if (index !== -1) {
this.participantList.splice(index, 1);
this.saveParticipantList();
this.$message.success('删除成功');
}
}).catch(() => {});
}
}
};
</script>
<style lang="scss" scoped>
.participant-list {
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;
}
}
</style>

View File

@@ -0,0 +1,412 @@
<template>
<div class="participant-manage">
<div class="page-header">
<el-button
icon="el-icon-arrow-left"
@click="handleBack"
>
返回列表
</el-button>
<h2 class="page-title">{{ pageTitle }}</h2>
<div class="header-actions" v-if="mode !== 'view'">
<el-button @click="handleBack">取消</el-button>
<el-button type="primary" @click="handleSave">
{{ mode === '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="mode === 'view'"
class="participant-form"
>
<!-- 基本信息 -->
<div class="form-section">
<div class="section-title">
<i class="el-icon-user"></i>
基本信息
</div>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="选手姓名" prop="playerName">
<el-input
v-model="formData.playerName"
placeholder="请输入选手姓名"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="性别" prop="gender">
<el-radio-group v-model="formData.gender">
<el-radio :label="1"></el-radio>
<el-radio :label="2"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="年龄" prop="age">
<el-input-number
v-model="formData.age"
:min="6"
:max="100"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="身份证号" prop="idCard">
<el-input
v-model="formData.idCard"
placeholder="请输入身份证号"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系电话" prop="contactPhone">
<el-input
v-model="formData.contactPhone"
placeholder="请输入联系电话"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="所属单位" prop="organization">
<el-input
v-model="formData.organization"
placeholder="请输入所属单位"
/>
</el-form-item>
</div>
<!-- 赛事信息 -->
<div class="form-section">
<div class="section-title">
<i class="el-icon-trophy"></i>
赛事信息
</div>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="所属赛事" prop="competitionId">
<el-select
v-model="formData.competitionId"
placeholder="请选择赛事"
style="width: 100%"
@change="handleCompetitionChange"
>
<el-option
v-for="item in competitionOptions"
:key="item.id"
:label="item.competitionName"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="参赛项目" prop="projectName">
<el-input
v-model="formData.projectName"
placeholder="请输入参赛项目"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="组别" prop="category">
<el-input
v-model="formData.category"
placeholder="例如:成年男子组"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="出场顺序" prop="orderNum">
<el-input-number
v-model="formData.orderNum"
:min="1"
:max="9999"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 其他信息 -->
<div class="form-section">
<div class="section-title">
<i class="el-icon-document"></i>
其他信息
</div>
<el-form-item label="选手简介" prop="introduction">
<el-input
v-model="formData.introduction"
type="textarea"
:rows="4"
placeholder="请输入选手简介"
maxlength="500"
show-word-limit
/>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="formData.remark"
type="textarea"
:rows="3"
placeholder="请输入备注"
/>
</el-form-item>
</div>
</el-form>
</div>
</template>
<script>
export default {
name: 'ParticipantManage',
data() {
return {
mode: 'create', // create, edit, view
participantId: null,
competitionOptions: [],
formData: {
competitionId: null,
competitionName: '',
playerName: '',
gender: 1,
age: null,
contactPhone: '',
organization: '',
idCard: '',
projectName: '',
category: '',
orderNum: 1,
introduction: '',
remark: '',
attachments: []
},
formRules: {
playerName: [
{ required: true, message: '请输入选手姓名', trigger: 'blur' }
],
gender: [
{ required: true, message: '请选择性别', trigger: 'change' }
],
age: [
{ required: true, message: '请输入年龄', trigger: 'blur' }
],
contactPhone: [
{ required: true, message: '请输入联系电话', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
],
competitionId: [
{ required: true, message: '请选择赛事', trigger: 'change' }
],
projectName: [
{ required: true, message: '请输入参赛项目', trigger: 'blur' }
]
}
};
},
computed: {
pageTitle() {
const titleMap = {
create: '添加参赛选手',
edit: '编辑参赛选手',
view: '查看参赛选手'
};
return titleMap[this.mode] || '参赛选手信息';
}
},
created() {
this.initPage();
},
methods: {
initPage() {
const { mode, id } = this.$route.query;
this.mode = mode || 'create';
this.participantId = id ? parseInt(id) : null;
// 加载赛事选项
this.loadCompetitionOptions();
if (this.mode !== 'create' && this.participantId) {
this.loadParticipantData();
}
},
loadCompetitionOptions() {
const savedData = localStorage.getItem('competitionList');
if (savedData) {
try {
this.competitionOptions = JSON.parse(savedData);
} catch (e) {
console.error('加载赛事数据失败', e);
}
}
},
loadParticipantData() {
// 从 localStorage 加载数据
const savedData = localStorage.getItem('participantList');
if (savedData) {
try {
const list = JSON.parse(savedData);
const participant = list.find(item => item.id === this.participantId);
if (participant) {
this.formData = { ...participant };
}
} catch (e) {
console.error('加载数据失败', e);
this.$message.error('加载数据失败');
}
}
},
handleCompetitionChange(competitionId) {
const competition = this.competitionOptions.find(item => item.id === competitionId);
if (competition) {
this.formData.competitionName = competition.competitionName;
}
},
handleSave() {
this.$refs.formRef.validate((valid) => {
if (valid) {
const savedData = localStorage.getItem('participantList');
let list = savedData ? JSON.parse(savedData) : [];
// 确保有赛事名称
if (!this.formData.competitionName) {
const competition = this.competitionOptions.find(item => item.id === this.formData.competitionId);
if (competition) {
this.formData.competitionName = competition.competitionName;
}
}
if (this.mode === 'create') {
// 新建
const newId = list.length > 0 ? Math.max(...list.map(item => item.id)) + 1 : 1;
const newParticipant = {
...this.formData,
id: newId
};
list.push(newParticipant);
this.$message.success('添加成功');
} else if (this.mode === 'edit') {
// 编辑
const index = list.findIndex(item => item.id === this.participantId);
if (index !== -1) {
list[index] = {
...list[index],
...this.formData
};
this.$message.success('保存成功');
}
}
localStorage.setItem('participantList', JSON.stringify(list));
// 返回列表页
setTimeout(() => {
this.handleBack();
}, 500);
} else {
this.$message.error('请完善必填信息');
}
});
},
switchToEdit() {
this.$router.push({
path: '/martial/participant/manage',
query: { mode: 'edit', id: this.participantId }
});
},
handleBack() {
this.$router.push('/martial/participant/list');
}
}
};
</script>
<style lang="scss" scoped>
.participant-manage {
padding: 20px;
background: #fff;
border-radius: 8px;
min-height: calc(100vh - 120px);
}
.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: 40px;
padding: 24px;
background: #fafafa;
border-radius: 8px;
&:last-child {
margin-bottom: 0;
}
}
.section-title {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 24px;
font-size: 18px;
font-weight: 600;
color: #333;
i {
color: #dc2626;
font-size: 20px;
}
}
:deep(.el-form-item) {
margin-bottom: 22px;
}
</style>

View File

@@ -0,0 +1,482 @@
<template>
<div class="referee-container">
<el-card shadow="hover">
<div class="page-header">
<h2 class="page-title">评委管理</h2>
<el-button type="primary" size="small" icon="el-icon-plus" @click="handleAdd">新增评委</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"
>
<i slot="prefix" class="el-input__icon el-icon-search"></i>
</el-input>
</el-form-item>
<el-form-item>
<el-select v-model="searchForm.refereeType" placeholder="裁判类型" clearable size="small" style="width: 180px">
<el-option label="全部" :value="null"></el-option>
<el-option label="主裁判" :value="1"></el-option>
<el-option label="普通裁判" :value="2"></el-option>
</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="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="name" label="姓名" width="100"></el-table-column>
<el-table-column prop="gender" label="性别" width="60" align="center">
<template #default="scope">
{{ scope.row.gender === 1 ? '男' : '女' }}
</template>
</el-table-column>
<el-table-column prop="phone" label="手机号" width="130"></el-table-column>
<el-table-column prop="idCard" label="身份证号" width="180" show-overflow-tooltip></el-table-column>
<el-table-column prop="refereeType" label="裁判类型" width="100" align="center">
<template #default="scope">
<el-tag :type="scope.row.refereeType === 1 ? 'danger' : 'primary'" size="small">
{{ scope.row.refereeType === 1 ? '主裁判' : '普通裁判' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="level" label="等级/职称" width="120"></el-table-column>
<el-table-column prop="specialty" label="擅长项目" min-width="150" show-overflow-tooltip></el-table-column>
<el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip></el-table-column>
<el-table-column prop="createTime" label="创建时间" width="160"></el-table-column>
<el-table-column label="操作" width="180" align="center" fixed="right">
<template #default="scope">
<el-button type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="danger" size="small" @click="handleDelete(scope.row)">删除</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>
<!-- 新增/编辑弹窗 -->
<el-dialog
:title="dialogTitle"
:visible.sync="dialogVisible"
width="600px"
:close-on-click-modal="false"
@close="handleDialogClose"
>
<el-form ref="refereeForm" :model="formData" :rules="rules" label-width="100px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="姓名" prop="name">
<el-input v-model="formData.name" placeholder="请输入姓名" clearable></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<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-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="手机号" prop="phone">
<el-input v-model="formData.phone" placeholder="请输入手机号" clearable maxlength="11"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="身份证号" prop="idCard">
<el-input v-model="formData.idCard" placeholder="请输入身份证号" clearable maxlength="18"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="裁判类型" prop="refereeType">
<el-select v-model="formData.refereeType" placeholder="请选择裁判类型" style="width: 100%">
<el-option label="主裁判" :value="1"></el-option>
<el-option label="普通裁判" :value="2"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="等级/职称" prop="level">
<el-input v-model="formData.level" placeholder="请输入等级/职称" clearable></el-input>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="擅长项目" prop="specialty">
<el-input v-model="formData.specialty" placeholder="请输入擅长项目,多个用逗号分隔" clearable></el-input>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="formData.remark"
type="textarea"
:rows="3"
placeholder="请输入备注信息"
></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
export default {
name: 'RefereeList',
data() {
return {
loading: false,
submitLoading: false,
dialogVisible: false,
dialogTitle: '新增评委',
isEdit: false,
searchForm: {
keyword: '',
refereeType: null
},
allTableData: [
{
id: 1,
name: '王大伟',
gender: 1,
phone: '13800138001',
idCard: '110101197001011234',
refereeType: 1,
level: '国家一级裁判',
specialty: '太极拳、长拳',
remark: '经验丰富,专业能力强',
createTime: '2025-11-20 10:00:00'
},
{
id: 2,
name: '李美丽',
gender: 2,
phone: '13800138002',
idCard: '110101198001011234',
refereeType: 2,
level: '国家二级裁判',
specialty: '剑术、刀术',
remark: '认真负责',
createTime: '2025-11-21 11:00:00'
},
{
id: 3,
name: '张强',
gender: 1,
phone: '13800138003',
idCard: '110101197501011234',
refereeType: 2,
level: '国家一级裁判',
specialty: '棍术、枪术',
remark: '',
createTime: '2025-11-22 14:00:00'
},
{
id: 4,
name: '刘芳',
gender: 2,
phone: '13800138004',
idCard: '110101198501011234',
refereeType: 1,
level: '国际级裁判',
specialty: '太极拳、太极剑',
remark: '国际武术裁判',
createTime: '2025-11-23 15:00:00'
},
{
id: 5,
name: '陈建国',
gender: 1,
phone: '13800138005',
idCard: '110101197801011234',
refereeType: 2,
level: '国家二级裁判',
specialty: '长拳、南拳',
remark: '',
createTime: '2025-11-24 16:00:00'
}
],
tableData: [],
pagination: {
current: 1,
size: 10,
total: 0
},
formData: {
id: null,
name: '',
gender: 1,
phone: '',
idCard: '',
refereeType: null,
level: '',
specialty: '',
remark: ''
},
rules: {
name: [
{ required: true, message: '请输入姓名', trigger: 'blur' }
],
gender: [
{ required: true, message: '请选择性别', trigger: 'change' }
],
phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
],
idCard: [
{ pattern: /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/, message: '请输入正确的身份证号', trigger: 'blur' }
],
refereeType: [
{ required: true, message: '请选择裁判类型', trigger: 'change' }
]
}
}
},
mounted() {
this.loadRefereeList()
},
methods: {
// 从 localStorage 加载数据
loadRefereeList() {
const savedData = localStorage.getItem('refereeList')
if (savedData) {
try {
this.allTableData = JSON.parse(savedData)
} catch (e) {
console.error('加载评委数据失败', e)
}
} else {
// 首次加载,保存默认数据
this.saveRefereeList()
}
this.fetchData()
},
// 保存数据到 localStorage
saveRefereeList() {
localStorage.setItem('refereeList', JSON.stringify(this.allTableData))
},
// 获取数据
fetchData() {
this.loading = true
setTimeout(() => {
// 过滤数据
let filteredData = [...this.allTableData]
// 搜索过滤
if (this.searchForm.keyword) {
const keyword = this.searchForm.keyword.toLowerCase()
filteredData = filteredData.filter(item =>
item.name.toLowerCase().includes(keyword) ||
item.phone.includes(keyword)
)
}
// 类型过滤
if (this.searchForm.refereeType !== null && this.searchForm.refereeType !== '') {
filteredData = filteredData.filter(item => item.refereeType === this.searchForm.refereeType)
}
this.pagination.total = filteredData.length
// 分页处理
const start = (this.pagination.current - 1) * this.pagination.size
const end = start + this.pagination.size
this.tableData = filteredData.slice(start, end)
this.loading = false
}, 300)
},
// 搜索
handleSearch() {
this.pagination.current = 1
this.fetchData()
},
// 重置
handleReset() {
this.searchForm = {
keyword: '',
refereeType: null
}
this.pagination.current = 1
this.fetchData()
},
// 分页
handleSizeChange(size) {
this.pagination.size = size
this.fetchData()
},
handleCurrentChange(current) {
this.pagination.current = current
this.fetchData()
},
// 新增
handleAdd() {
this.dialogTitle = '新增评委'
this.isEdit = false
this.dialogVisible = true
},
// 编辑
handleEdit(row) {
this.dialogTitle = '编辑评委'
this.isEdit = true
this.formData = { ...row }
this.dialogVisible = true
},
// 删除
handleDelete(row) {
this.$confirm('确定删除该评委吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const index = this.allTableData.findIndex(item => item.id === row.id)
if (index !== -1) {
this.allTableData.splice(index, 1)
this.saveRefereeList()
this.$message.success('删除成功')
this.fetchData()
}
}).catch(() => {})
},
// 提交表单
handleSubmit() {
this.$refs.refereeForm.validate((valid) => {
if (valid) {
this.submitLoading = true
setTimeout(() => {
if (this.isEdit) {
// 编辑
const index = this.allTableData.findIndex(item => item.id === this.formData.id)
if (index !== -1) {
this.allTableData[index] = { ...this.formData }
this.$message.success('修改成功')
}
} else {
// 新增
const newId = this.allTableData.length > 0
? Math.max(...this.allTableData.map(item => item.id)) + 1
: 1
const now = new Date()
const createTime = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`
this.allTableData.push({
...this.formData,
id: newId,
createTime
})
this.$message.success('新增成功')
}
this.saveRefereeList()
this.dialogVisible = false
this.fetchData()
this.submitLoading = false
}, 500)
}
})
},
// 关闭弹窗
handleDialogClose() {
this.$refs.refereeForm.resetFields()
this.formData = {
id: null,
name: '',
gender: 1,
phone: '',
idCard: '',
refereeType: null,
level: '',
specialty: '',
remark: ''
}
}
}
}
</script>
<style lang="scss" scoped>
.referee-container {
min-height: 100%;
padding: 15px;
.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;
}
}
.search-form {
margin-bottom: 15px;
}
.pagination {
margin-top: 15px;
text-align: right;
}
.dialog-footer {
text-align: right;
}
}
</style>

View File

@@ -181,7 +181,6 @@ export default {
{
schoolUnit: '清河小学',
category: '集体',
hint: '剩余功能在提现上,显示栏标签',
individual: 1,
dual: 1,
team1101: 1,

View File

@@ -0,0 +1,507 @@
<template>
<div class="score-management-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 label="比赛项目">
<el-select
v-model="searchForm.projectId"
placeholder="请选择比赛项目"
clearable
filterable
size="small"
style="width: 200px"
@change="handleSearch"
>
<el-option
v-for="item in projectList"
:key="item.id"
:label="item.name"
:value="item.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="场地">
<el-select
v-model="searchForm.venueId"
placeholder="请选择场地"
clearable
filterable
size="small"
style="width: 150px"
@change="handleSearch"
>
<el-option
v-for="item in venueList"
:key="item.id"
:label="item.name"
:value="item.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="比赛人员">
<el-input
v-model="searchForm.playerName"
placeholder="搜索选手姓名"
clearable
size="small"
style="width: 180px"
>
<template #prefix>
<i class="el-input__icon el-icon-search"></i>
</template>
</el-input>
</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="tableData"
border
stripe
size="small"
style="width: 100%"
:max-height="600"
>
<el-table-column type="index" label="序号" width="60" align="center" fixed></el-table-column>
<el-table-column prop="projectName" label="比赛项目" min-width="180" show-overflow-tooltip fixed></el-table-column>
<el-table-column prop="venueName" label="场地" width="120" align="center" fixed></el-table-column>
<el-table-column prop="playerName" label="比赛人员" width="120" align="center" fixed></el-table-column>
<!-- 动态裁判评分列 -->
<el-table-column
v-for="(judge, index) in judgeColumns"
:key="'judge-' + index"
:label="'裁判' + (index + 1) + '分数'"
width="110"
align="center"
>
<template #default="scope">
<span :class="getScoreClass(scope.row.judgeScores[index])">
{{ formatScore(scope.row.judgeScores[index]) }}
</span>
</template>
</el-table-column>
<el-table-column label="总裁判分数" width="120" align="center" fixed="right">
<template #default="scope">
<span class="total-score">{{ formatScore(scope.row.totalScore) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="150" align="center" fixed="right">
<template #default="scope">
<el-button type="primary" size="small" @click="handleViewDetail(scope.row)">查看详情</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>
<!-- 评分详情弹窗 -->
<el-dialog
v-model="detailDialogVisible"
title="评分详情"
width="700px"
:close-on-click-modal="false"
>
<div v-if="currentDetail" class="detail-content">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="比赛项目">{{ currentDetail.projectName }}</el-descriptions-item>
<el-descriptions-item label="场地">{{ currentDetail.venueName }}</el-descriptions-item>
<el-descriptions-item label="选手姓名">{{ currentDetail.playerName }}</el-descriptions-item>
<el-descriptions-item label="队伍">{{ currentDetail.teamName }}</el-descriptions-item>
<el-descriptions-item label="身份证">{{ currentDetail.idCard }}</el-descriptions-item>
<el-descriptions-item label="编号">{{ currentDetail.playerNo }}</el-descriptions-item>
</el-descriptions>
<div class="score-section">
<h4>裁判评分明细</h4>
<el-table :data="currentDetail.scoreDetails" border size="small" style="margin-top: 10px">
<el-table-column type="index" label="序号" width="60" align="center"></el-table-column>
<el-table-column prop="judgeName" label="裁判姓名" align="center"></el-table-column>
<el-table-column prop="score" label="评分" align="center" width="100">
<template #default="scope">
<span class="score-value">{{ formatScore(scope.row.score) }}</span>
</template>
</el-table-column>
<el-table-column prop="deductions" label="扣分项" show-overflow-tooltip></el-table-column>
<el-table-column prop="note" label="备注" show-overflow-tooltip></el-table-column>
<el-table-column prop="scoreTime" label="评分时间" width="150"></el-table-column>
</el-table>
<div class="total-score-display">
<span class="label">总分</span>
<span class="value">{{ formatScore(currentDetail.totalScore) }}</span>
</div>
</div>
</div>
</el-dialog>
</div>
</template>
<script>
export default {
name: 'ScoreManagement',
data() {
return {
loading: false,
searchForm: {
projectId: null,
venueId: null,
playerName: ''
},
projectList: [
{ id: 1, name: '女子组长拳' },
{ id: 2, name: '男子组陈氏太极拳' },
{ id: 3, name: '女子组双剑(含长穗双剑)' },
{ id: 4, name: '男子组杨氏太极拳' },
{ id: 5, name: '女子组刀术' },
{ id: 6, name: '男子组棍术' },
{ id: 7, name: '女子组枪术' },
{ id: 8, name: '男子组剑术' }
],
venueList: [
{ id: 1, name: '第一场地' },
{ id: 2, name: '第二场地' },
{ id: 3, name: '第三场地' },
{ id: 4, name: '第四场地' },
{ id: 5, name: '第五场地' }
],
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,
size: 10,
total: 0
},
judgeColumns: Array(5).fill(null), // 5个裁判
detailDialogVisible: false,
currentDetail: null
}
},
mounted() {
this.loadScoreList()
},
methods: {
// 从 localStorage 加载数据
loadScoreList() {
const savedData = localStorage.getItem('scoreList')
if (savedData) {
try {
this.allTableData = JSON.parse(savedData)
} catch (e) {
console.error('加载评分数据失败', e)
}
} else {
// 首次加载,保存默认数据
this.saveScoreList()
}
this.fetchData()
},
// 保存数据到 localStorage
saveScoreList() {
localStorage.setItem('scoreList', JSON.stringify(this.allTableData))
},
// 获取评分数据
fetchData() {
this.loading = true
setTimeout(() => {
// 过滤数据
let filteredData = [...this.allTableData]
if (this.searchForm.projectId) {
const project = this.projectList.find(p => p.id === this.searchForm.projectId)
if (project) {
filteredData = filteredData.filter(item => item.projectName === project.name)
}
}
if (this.searchForm.venueId) {
const venue = this.venueList.find(v => v.id === this.searchForm.venueId)
if (venue) {
filteredData = filteredData.filter(item => item.venueName === venue.name)
}
}
if (this.searchForm.playerName) {
filteredData = filteredData.filter(item =>
item.playerName.includes(this.searchForm.playerName)
)
}
this.pagination.total = filteredData.length
// 分页
const start = (this.pagination.current - 1) * this.pagination.size
const end = start + this.pagination.size
this.tableData = filteredData.slice(start, end)
this.loading = false
}, 300)
},
// 查询
handleSearch() {
this.pagination.current = 1
this.fetchData()
},
// 重置
handleReset() {
this.searchForm = {
projectId: null,
venueId: null,
playerName: ''
}
this.handleSearch()
},
// 分页大小变化
handleSizeChange(size) {
this.pagination.size = size
this.fetchData()
},
// 当前页变化
handleCurrentChange(current) {
this.pagination.current = current
this.fetchData()
},
// 查看详情
handleViewDetail(row) {
this.currentDetail = {
...row,
scoreDetails: [
{ judgeName: '裁判1', score: row.judgeScores[0], deductions: '无', note: '', scoreTime: '2025-11-29 10:30:00' },
{ judgeName: '裁判2', score: row.judgeScores[1], deductions: '无', note: '', scoreTime: '2025-11-29 10:30:05' },
{ judgeName: '裁判3', score: row.judgeScores[2], deductions: '扣分项描述', note: '动作不规范', scoreTime: '2025-11-29 10:30:10' },
{ judgeName: '裁判4', score: row.judgeScores[3], deductions: '无', note: '', scoreTime: '2025-11-29 10:30:15' },
{ judgeName: '裁判5', score: row.judgeScores[4], deductions: '无', note: '表现优秀', scoreTime: '2025-11-29 10:30:20' }
]
}
this.detailDialogVisible = true
},
// 格式化分数
formatScore(score) {
if (score === null || score === undefined) {
return '-'
}
return Number(score).toFixed(3)
},
// 分数样式
getScoreClass(score) {
if (!score) return ''
if (score >= 9.0) return 'high-score'
if (score >= 8.5) return 'medium-score'
return 'low-score'
}
}
}
</script>
<style lang="scss" scoped>
.score-management-container {
min-height: 100%;
padding: 15px;
background: #fff;
.page-header {
margin-bottom: 15px;
.page-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #2c3e50;
}
}
.search-form {
margin-bottom: 15px;
::v-deep(.el-form-item__label) {
font-weight: 500;
}
}
.high-score {
color: #67c23a;
font-weight: 600;
}
.medium-score {
color: #e6a23c;
font-weight: 600;
}
.low-score {
color: #f56c6c;
font-weight: 600;
}
.total-score {
color: #1b7c5e;
font-weight: 700;
font-size: 14px;
}
.pagination {
margin-top: 15px;
text-align: right;
}
.detail-content {
.score-section {
margin-top: 20px;
h4 {
margin: 0 0 10px;
font-size: 14px;
color: #333;
}
.score-value {
color: #1b7c5e;
font-weight: 600;
}
.total-score-display {
margin-top: 15px;
padding: 15px;
background: linear-gradient(135deg, #e8f5f2 0%, #f0f9f6 100%);
border-radius: 8px;
text-align: center;
.label {
font-size: 16px;
color: #666;
margin-right: 10px;
}
.value {
font-size: 28px;
font-weight: 700;
color: #1b7c5e;
}
}
}
}
}
</style>

View File

@@ -1,26 +1,621 @@
<template>
<div class="wel-container">
<!-- 顶部横幅 -->
<div class="welcome-banner">
<div class="banner-bg">
<div class="bg-pattern"></div>
</div>
<div class="banner-content">
<div class="logo-wrapper">
<div class="logo-icon"></div>
</div>
<h1 class="banner-title">武术赛事通</h1>
<p class="banner-subtitle">专业的武术赛事管理平台</p>
<div class="banner-time">
<i class="el-icon-time"></i>
{{ currentTime }}
</div>
</div>
</div>
<!-- 主要内容区域 -->
<div class="main-content">
<!-- 快速入口 -->
<div class="section">
<div class="section-header">
<div class="header-line"></div>
<h2 class="section-title">快速入口</h2>
<div class="header-line"></div>
</div>
<div class="quick-access">
<div class="access-card" @click="navigateTo('/martial/competition/list')">
<div class="card-bg"></div>
<div class="card-icon">
<i class="el-icon-s-flag"></i>
</div>
<div class="card-info">
<h3>赛事管理</h3>
<p>管理武术赛事</p>
</div>
<i class="card-arrow el-icon-arrow-right"></i>
</div>
<div class="access-card" @click="navigateTo('/martial/participant/list')">
<div class="card-bg"></div>
<div class="card-icon">
<i class="el-icon-user"></i>
</div>
<div class="card-info">
<h3>参赛选手</h3>
<p>管理参赛选手信息</p>
</div>
<i class="card-arrow el-icon-arrow-right"></i>
</div>
<div class="access-card" @click="navigateTo('/martial/order/list')">
<div class="card-bg"></div>
<div class="card-icon">
<i class="el-icon-s-order"></i>
</div>
<div class="card-info">
<h3>订单管理</h3>
<p>查看和管理订单</p>
</div>
<i class="card-arrow el-icon-arrow-right"></i>
</div>
<div class="access-card" @click="navigateTo('/martial/schedule/list')">
<div class="card-bg"></div>
<div class="card-icon">
<i class="el-icon-date"></i>
</div>
<div class="card-info">
<h3>赛程编排</h3>
<p>比赛赛程安排</p>
</div>
<i class="card-arrow el-icon-arrow-right"></i>
</div>
<div class="access-card" @click="navigateTo('/martial/dispatch/list')">
<div class="card-bg"></div>
<div class="card-icon">
<i class="el-icon-s-promotion"></i>
</div>
<div class="card-info">
<h3>赛事调度</h3>
<p>实时进度跟踪</p>
</div>
<i class="card-arrow el-icon-arrow-right"></i>
</div>
<div class="access-card" @click="navigateTo('/martial/referee/list')">
<div class="card-bg"></div>
<div class="card-icon">
<i class="el-icon-user-solid"></i>
</div>
<div class="card-info">
<h3>评委管理</h3>
<p>裁判人员管理</p>
</div>
<i class="card-arrow el-icon-arrow-right"></i>
</div>
<div class="access-card" @click="navigateTo('/martial/score/index')">
<div class="card-bg"></div>
<div class="card-icon">
<i class="el-icon-medal"></i>
</div>
<div class="card-info">
<h3>评分管理</h3>
<p>比赛评分系统</p>
</div>
<i class="card-arrow el-icon-arrow-right"></i>
</div>
</div>
</div>
<!-- 数据统计 -->
<div class="section">
<div class="section-header">
<div class="header-line"></div>
<h2 class="section-title">数据统计</h2>
<div class="header-line"></div>
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon red">
<i class="el-icon-s-order"></i>
</div>
<div class="stat-info">
<div class="stat-label">总订单数</div>
<div class="stat-value">{{ stats.totalOrders }}</div>
<div class="stat-trend up">
<i class="el-icon-top"></i>
<span>12%</span>
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon orange">
<i class="el-icon-s-custom"></i>
</div>
<div class="stat-info">
<div class="stat-label">参赛人数</div>
<div class="stat-value">{{ stats.totalParticipants }}</div>
<div class="stat-trend up">
<i class="el-icon-top"></i>
<span>25%</span>
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon blue">
<i class="el-icon-s-flag"></i>
</div>
<div class="stat-info">
<div class="stat-label">进行中赛事</div>
<div class="stat-value">{{ stats.ongoingEvents }}</div>
<div class="stat-trend">
<i class="el-icon-minus"></i>
<span>0%</span>
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon green">
<i class="el-icon-s-data"></i>
</div>
<div class="stat-info">
<div class="stat-label">已完成赛事</div>
<div class="stat-value">{{ stats.completedEvents }}</div>
<div class="stat-trend up">
<i class="el-icon-top"></i>
<span>8%</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Wel',
data() {
return {
currentTime: '',
stats: {
totalOrders: 0,
totalParticipants: 0,
ongoingEvents: 0,
completedEvents: 0
}
};
},
mounted() {
this.updateTime();
setInterval(this.updateTime, 1000);
this.loadStats();
},
methods: {
updateTime() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
this.currentTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
},
navigateTo(path) {
this.$router.push(path);
},
loadStats() {
// TODO: 从API加载真实统计数据
this.stats = {
totalOrders: 128,
totalParticipants: 456,
ongoingEvents: 3,
completedEvents: 12
};
}
}
};
</script>
<style lang="scss" scoped>
.wel-container {
min-height: 100vh;
background: #f5f5f5;
}
</style>
.welcome-banner {
position: relative;
height: 280px;
background: linear-gradient(135deg, #dc2626 0%, #991b1b 100%);
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.banner-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
&::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background:
radial-gradient(circle at 30% 50%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
radial-gradient(circle at 70% 50%, rgba(0, 0, 0, 0.1) 0%, transparent 50%);
animation: bgRotate 30s linear infinite;
}
}
@keyframes bgRotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.bg-pattern {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image:
repeating-linear-gradient(90deg, rgba(255, 255, 255, 0.03) 0px, rgba(255, 255, 255, 0.03) 1px, transparent 1px, transparent 60px),
repeating-linear-gradient(0deg, rgba(255, 255, 255, 0.03) 0px, rgba(255, 255, 255, 0.03) 1px, transparent 1px, transparent 60px);
}
.banner-content {
position: relative;
z-index: 2;
text-align: center;
color: #fff;
}
.logo-wrapper {
margin-bottom: 20px;
}
.logo-icon {
width: 80px;
height: 80px;
margin: 0 auto;
background: rgba(255, 255, 255, 0.15);
border: 3px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 40px;
font-weight: 900;
color: #fff;
backdrop-filter: blur(10px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
animation: logoFloat 3s ease-in-out infinite;
}
@keyframes logoFloat {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
.banner-title {
font-size: 42px;
font-weight: 700;
margin: 0 0 12px 0;
letter-spacing: 6px;
text-shadow: 0 2px 15px rgba(0, 0, 0, 0.3);
}
.banner-subtitle {
font-size: 16px;
margin: 0 0 20px 0;
opacity: 0.95;
letter-spacing: 3px;
}
.banner-time {
display: inline-block;
font-size: 14px;
background: rgba(255, 255, 255, 0.15);
padding: 10px 24px;
border-radius: 24px;
backdrop-filter: blur(5px);
i {
margin-right: 8px;
}
}
.main-content {
max-width: 1400px;
margin: 0 auto;
padding: 40px 20px;
}
.section {
margin-bottom: 50px;
}
.section-header {
display: flex;
align-items: center;
justify-content: center;
gap: 24px;
margin-bottom: 32px;
}
.header-line {
flex: 1;
height: 2px;
background: linear-gradient(90deg, transparent, #dc2626, transparent);
max-width: 150px;
}
.section-title {
margin: 0;
font-size: 26px;
font-weight: 700;
color: #1a1a1a;
white-space: nowrap;
position: relative;
padding: 0 16px;
&::before,
&::after {
content: '';
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 6px;
height: 6px;
background: #dc2626;
border-radius: 50%;
}
&::before {
left: 0;
}
&::after {
right: 0;
}
}
.quick-access {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 24px;
}
.access-card {
position: relative;
background: #fff;
border-radius: 12px;
padding: 28px 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
display: flex;
align-items: center;
gap: 20px;
cursor: pointer;
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
overflow: hidden;
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 4px;
height: 100%;
background: linear-gradient(180deg, #dc2626 0%, #991b1b 100%);
transform: scaleY(0);
transition: transform 0.3s ease;
}
.card-bg {
position: absolute;
right: -20px;
top: -20px;
width: 100px;
height: 100px;
background: radial-gradient(circle, rgba(220, 38, 38, 0.05) 0%, transparent 70%);
border-radius: 50%;
transition: all 0.4s ease;
}
&:hover {
transform: translateY(-8px);
box-shadow: 0 12px 32px rgba(220, 38, 38, 0.2);
&::before {
transform: scaleY(1);
}
.card-bg {
transform: scale(1.5);
opacity: 0.8;
}
.card-icon {
background: linear-gradient(135deg, #dc2626 0%, #991b1b 100%);
color: #fff;
transform: scale(1.1) rotate(5deg);
}
.card-arrow {
transform: translateX(6px);
opacity: 1;
}
}
.card-icon {
position: relative;
z-index: 1;
width: 56px;
height: 56px;
border-radius: 12px;
background: #fef2f2;
display: flex;
align-items: center;
justify-content: center;
font-size: 26px;
color: #dc2626;
transition: all 0.3s ease;
flex-shrink: 0;
}
.card-info {
position: relative;
z-index: 1;
flex: 1;
h3 {
margin: 0 0 6px 0;
font-size: 17px;
font-weight: 600;
color: #1a1a1a;
}
p {
margin: 0;
font-size: 13px;
color: #666;
}
}
.card-arrow {
position: relative;
z-index: 1;
font-size: 20px;
color: #dc2626;
opacity: 0.5;
transition: all 0.3s ease;
}
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 24px;
}
.stat-card {
background: #fff;
border-radius: 12px;
padding: 28px 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
display: flex;
align-items: center;
gap: 20px;
transition: all 0.3s ease;
&:hover {
transform: translateY(-6px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
.stat-icon {
transform: scale(1.1) rotate(-5deg);
}
}
.stat-icon {
width: 64px;
height: 64px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 30px;
color: #fff;
transition: all 0.3s ease;
flex-shrink: 0;
&.red {
background: linear-gradient(135deg, #dc2626 0%, #991b1b 100%);
}
&.orange {
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
}
&.blue {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
}
&.green {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
}
}
.stat-info {
flex: 1;
position: relative;
.stat-label {
font-size: 13px;
color: #666;
margin-bottom: 8px;
}
.stat-value {
font-size: 32px;
font-weight: 700;
color: #1a1a1a;
line-height: 1;
}
.stat-trend {
position: absolute;
top: 0;
right: 0;
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #999;
&.up {
color: #10b981;
}
i {
font-size: 14px;
}
}
}
}
@media (max-width: 768px) {
.banner-title {
font-size: 32px;
}
.quick-access {
grid-template-columns: 1fr;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>