update:运动馆

This commit is contained in:
ltx 2026-04-23 19:11:45 +08:00
parent a5e185e551
commit e8ecf2c0fc
4 changed files with 850 additions and 1 deletions

View File

@ -7,7 +7,7 @@
<meta name="viewport" content="width=device-width, maximum-scale=1.0, minimum-scale=1.0, initial-scale=1.0, user-scalable=no" />
<meta name="keywords" content=".netccore,dotnet core,vue,element,element plus,vue3" />
<meta name="description" content="智慧系统" />
<!-- <link rel="icon" href="<%= BASE_URL %>favicon.ico"> -->
<link rel="icon" href="favicon.ico">
<title>智慧系统</title>
</head>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 195 KiB

View File

@ -595,6 +595,14 @@ const routes = [{
name: 'sportsHallClass',
component: () => import('@/views/sportsHall/classManage/index.vue'),
},
{
path: '/sportHallReport', //运动馆/体测报告
name: 'sportHallReport',
component: () => import('@/views/sportHallReport/index.vue'),
meta: {
keepAlive: false
}
},
{
path: '/openReservation', //学校管理/开放预约
name: 'openReservation',

View File

@ -0,0 +1,841 @@
<template>
<div class="sport-hall-report">
<!-- 搜索区域 -->
<div class="search-card">
<div class="search-row">
<div class="search-item">
<span class="label">项目:</span>
<el-select v-model="query.project" placeholder="全部" clearable style="width: 160px">
<el-option label="全部" value="" />
<el-option v-for="item in projectOptions" :key="item.id" :label="item.itemTypeName" :value="item.id" />
</el-select>
</div>
<div class="search-item">
<span class="label">年级-班级:</span>
<el-cascader
v-model="query.gradeClass"
:options="gradeClassOptions"
:props="{ checkStrictly: true }"
clearable
placeholder="全部"
style="width: 200px"
/>
</div>
<div class="search-item">
<span class="label">性别:</span>
<el-select v-model="query.gender" placeholder="全部" style="width: 130px">
<el-option label="全部" value="" />
<el-option label="男" :value="1" />
<el-option label="女" :value="2" />
</el-select>
</div>
<div class="search-item">
<span class="label">等级:</span>
<el-select v-model="query.rank" placeholder="全部" style="width: 130px">
<el-option label="全部" value="" />
<el-option label="优秀" value="优秀" />
<el-option label="良好" value="良好" />
<el-option label="及格" value="及格" />
<el-option label="不及格" value="不及格" />
</el-select>
</div>
</div>
<div class="search-row">
<div class="search-item">
<span class="label">测试时间:</span>
<el-date-picker
v-model="query.dateRange"
type="daterange"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
range-separator="~"
start-placeholder="开始日期"
end-placeholder="结束日期"
style="width: 260px"
/>
</div>
<div class="search-actions">
<el-button class="btn-reset" @click="handleReset">重置</el-button>
<el-button class="btn-search" type="primary" @click="handleSearch">查询</el-button>
</div>
</div>
</div>
<!-- 操作栏 -->
<div class="table-toolbar">
<div class="toolbar-right">
<el-tooltip content="将本次成绩推送至运动馆—家长端—【统计】—体测报告" placement="top">
<el-button class="action-btn import-btn" @click="handleImport">
导入成绩
</el-button>
</el-tooltip>
<el-tooltip content="本次成绩推送至运动馆—家长端—【统计】—一体测报告" placement="top">
<el-button class="action-btn send-btn" @click="handleSendScore">
成绩推送
</el-button>
</el-tooltip>
</div>
</div>
<!-- 数据表格 -->
<div class="table-card">
<el-table
:data="tableData"
v-loading="loading"
style="width: 100%"
:header-cell-style="{
background: '#f5f7fa',
color: '#606266',
fontWeight: '600',
fontSize: '13px',
padding: '12px 0'
}"
:row-style="{ height: '52px' }"
stripe
border
>
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column prop="studentName" label="学生姓名" min-width="100" align="center" />
<el-table-column prop="gradeAndClass" label="年级-班级" min-width="140" align="center" />
<el-table-column prop="gender" label="性别" width="70" align="center">
<template #default="{ row }">
<el-tag :type="row.gender === 1 ? 'primary' : 'danger'" size="small" effect="light">
{{ row.gender === 1 ? '男' : '女' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="bmi" label="体质指数(BMI)" min-width="130" align="center">
<template #default="{ row }">
<span>{{ row.bmi ?? '/' }}</span>
</template>
</el-table-column>
<el-table-column prop="lungCapacity" label="肺活量(ml)" min-width="110" align="center">
<template #default="{ row }">
<span>{{ row.lungCapacity ?? '/' }}</span>
</template>
</el-table-column>
<el-table-column prop="sitAndReach" label="坐位体前屈(cm)" min-width="140" align="center">
<template #default="{ row }">
<span>{{ row.sitAndReach ?? '/' }}</span>
</template>
</el-table-column>
<el-table-column prop="ropeSkipping" label="一分钟跳绳(个)" min-width="140" align="center">
<template #default="{ row }">
<span>{{ row.ropeSkipping ?? '/' }}</span>
</template>
</el-table-column>
<el-table-column prop="sitUp" label="一分钟仰卧起坐(个)" min-width="160" align="center">
<template #default="{ row }">
<span>{{ row.sitUp ?? '/' }}</span>
</template>
</el-table-column>
<el-table-column prop="standingLongJump" label="立定跳远(cm)" min-width="130" align="center">
<template #default="{ row }">
<span>{{ row.standingLongJump ?? '/' }}</span>
</template>
</el-table-column>
<el-table-column prop="testTime" label="测试时间" min-width="160" align="center" />
<el-table-column label="操作" width="100" align="center">
<template #default="{ row }" width="100">
<el-button type="primary" link @click="handleEdit(row)">成绩修正</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-wrap">
<el-pagination
v-model:current-page="pagination.pageIndex"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
background
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</div>
<!-- 导入成绩弹窗 -->
<el-dialog
v-model="importDialog.visible"
title="导入成绩"
width="640px"
:close-on-click-modal="false"
class="import-dialog"
>
<div class="import-content">
<!-- 说明区域 -->
<div class="import-tips">
<p><span class="tip-num">1.</span>上传前请先下载标准Excel模板并按照标准模板中的说明编辑文件</p>
<p><span class="tip-num">2.</span>一次最多上传10000行数据,文件大小不能超过10M</p>
<p><span class="tip-num">3.</span>支持上传xlsx格式文件</p>
<p><span class="tip-num">4.</span>最新数据覆盖上次数据</p>
</div>
<!-- 上传区域 -->
<div class="upload-area">
<el-upload
ref="uploadRef"
class="upload-dragger"
drag
:action="uploadUrl"
:headers="uploadHeaders"
:before-upload="beforeUpload"
:on-success="onUploadSuccess"
:on-error="onUploadError"
:show-file-list="false"
accept=".xlsx"
>
<div class="upload-inner">
<img src="@/assets/images/excel-icon.png" class="excel-icon" @error="onIconError" alt="Excel" />
<div class="upload-text">
将模板拖拽到此区域 或
<span class="upload-link">点击上传</span>
</div>
</div>
</el-upload>
<div class="download-btn-wrap">
<el-button class="btn-download" @click="downloadTemplate">
下载标准Excel模板
</el-button>
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="importDialog.visible = false">关闭</el-button>
</div>
</template>
</el-dialog>
<!-- 成绩修正弹窗 -->
<el-dialog
v-model="editDialog.visible"
title="成绩修正"
width="560px"
:close-on-click-modal="false"
>
<el-form ref="editFormRef" :model="editForm" label-width="160px" size="default">
<el-form-item label="学生姓名">
<el-input :value="editForm.studentName" disabled />
</el-form-item>
<el-form-item label="体质指数(BMI)">
<el-input v-model="editForm.bmi" placeholder="请输入" clearable />
</el-form-item>
<el-form-item label="肺活量(ml)">
<el-input v-model="editForm.lungCapacity" placeholder="请输入" clearable />
</el-form-item>
<el-form-item label="坐位体前屈(cm)">
<el-input v-model="editForm.sitAndReach" placeholder="请输入" clearable />
</el-form-item>
<el-form-item label="一分钟跳绳(个)">
<el-input v-model="editForm.ropeSkipping" placeholder="请输入" clearable />
</el-form-item>
<el-form-item label="一分钟仰卧起坐(个)">
<el-input v-model="editForm.sitUp" placeholder="请输入" clearable />
</el-form-item>
<el-form-item label="立定跳远(cm)">
<el-input v-model="editForm.standingLongJump" placeholder="请输入" clearable />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="editDialog.visible = false">取消</el-button>
<el-button type="primary" @click="handleEditConfirm">保存</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import axios from 'axios'
// ===================== 查询参数 =====================
const query = reactive({
project: '',
gradeClass: [],
gender: '',
rank: '',
dateRange: [] as string[]
})
// ===================== 下拉数据 =====================
const projectOptions = ref<any[]>([])
const gradeClassOptions = ref<any[]>([])
// ===================== 表格数据 =====================
const loading = ref(false)
const tableData = ref<any[]>([])
const pagination = reactive({
pageIndex: 1,
pageSize: 10,
total: 0
})
// ===================== 模拟数据(接口接入后替换) =====================
const mockData = [
{
id: 1, studentName: '崔政豪', gradeAndClass: '运动馆-周一班', gender: 1,
bmi: '178/140/28', lungCapacity: 2344, sitAndReach: null, ropeSkipping: 200, sitUp: 200, standingLongJump: null, testTime: '2024-08-09 12:00'
},
{
id: 2, studentName: '金辰恺', gradeAndClass: '运动馆-周一班', gender: 2,
bmi: '178/140/28', lungCapacity: null, sitAndReach: 12, ropeSkipping: 180, sitUp: 180, standingLongJump: 12, testTime: '2024-08-09 12:00'
},
{
id: 3, studentName: '樊鸣轩', gradeAndClass: '运动馆-周一班', gender: 1,
bmi: '178/140/28', lungCapacity: 5643, sitAndReach: 10, ropeSkipping: null, sitUp: null, standingLongJump: 10, testTime: '2024-08-09 12:00'
},
{
id: 4, studentName: '陈睿鸿', gradeAndClass: '运动馆-周一班', gender: 2,
bmi: '178/140/28', lungCapacity: null, sitAndReach: null, ropeSkipping: 230, sitUp: 230, standingLongJump: null, testTime: '2024-08-09 12:00'
},
{
id: 5, studentName: '俞鑫安', gradeAndClass: '运动馆-周一班', gender: 1,
bmi: '178/140/28', lungCapacity: 2345, sitAndReach: null, ropeSkipping: 109, sitUp: 109, standingLongJump: null, testTime: '2024-08-09 12:00'
}
]
// ===================== 获取列表 =====================
const fetchList = async () => {
loading.value = true
try {
// TODO: 替换为实际接口
// const res = await getSportHallReport({ ...query, ...pagination })
// tableData.value = res.data.datas
// pagination.total = res.data.total
// 使用模拟数据
await new Promise(resolve => setTimeout(resolve, 300))
tableData.value = mockData
pagination.total = 400
} catch (e) {
ElMessage.error('获取数据失败')
} finally {
loading.value = false
}
}
// ===================== 获取下拉选项 =====================
const fetchOptions = async () => {
try {
// TODO: 替换为实际接口获取项目列表和年级班级列表
gradeClassOptions.value = [
{
value: '1',
label: '运动馆',
children: [
{ value: '1-1', label: '周一班' },
{ value: '1-2', label: '周二班' },
{ value: '1-3', label: '周三班' },
]
}
]
} catch (e) {
console.error('获取下拉选项失败', e)
}
}
// ===================== 搜索/重置 =====================
const handleSearch = () => {
pagination.pageIndex = 1
fetchList()
}
const handleReset = () => {
query.project = ''
query.gradeClass = []
query.gender = ''
query.rank = ''
query.dateRange = []
pagination.pageIndex = 1
fetchList()
}
// ===================== 分页 =====================
const handleSizeChange = (size: number) => {
pagination.pageSize = size
pagination.pageIndex = 1
fetchList()
}
const handlePageChange = (page: number) => {
pagination.pageIndex = page
fetchList()
}
// ===================== 导入成绩 =====================
const importDialog = reactive({ visible: false })
const uploadRef = ref()
const uploadUrl = `${import.meta.env.VITE_APP_BASE_API || ''}/api/sportHall/importScore`
const uploadHeaders = reactive({
Authorization: `Bearer ${localStorage.getItem('token') || ''}`
})
const handleImport = () => {
importDialog.visible = true
}
const beforeUpload = (file: File) => {
const isXlsx = file.name.endsWith('.xlsx')
if (!isXlsx) {
ElMessage.error('只支持上传 .xlsx 格式文件')
return false
}
const isLt10M = file.size / 1024 / 1024 < 10
if (!isLt10M) {
ElMessage.error('文件大小不能超过 10MB')
return false
}
return true
}
const onUploadSuccess = (response: any) => {
if (response?.code === 200) {
ElMessage.success('导入成功')
importDialog.visible = false
fetchList()
} else {
ElMessage.error(response?.message || '导入失败')
}
}
const onUploadError = () => {
ElMessage.error('文件上传失败,请重试')
}
const onIconError = (e: Event) => {
const img = e.target as HTMLImageElement
img.style.display = 'none'
}
const downloadTemplate = async () => {
try {
// TODO: 替换为实际模板下载接口
ElMessage.info('正在下载模板...')
// const res = await axios.get('/api/sportHall/downloadTemplate', { responseType: 'blob' })
// const url = window.URL.createObjectURL(new Blob([res.data]))
// const link = document.createElement('a')
// link.href = url
// link.setAttribute('download', '体测成绩导入模板.xlsx')
// document.body.appendChild(link)
// link.click()
// document.body.removeChild(link)
} catch (e) {
ElMessage.error('模板下载失败')
}
}
// ===================== 成绩推送 =====================
const handleSendScore = () => {
ElMessageBox.confirm(
'确认将本次成绩推送至运动馆—家长端—【统计】—一体测报告?',
'成绩推送',
{
confirmButtonText: '确认推送',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
// TODO: 调用推送接口
ElMessage.success('推送成功')
}).catch(() => {})
}
// ===================== 成绩修正 =====================
const editDialog = reactive({ visible: false })
const editFormRef = ref()
const editForm = reactive({
id: null as any,
studentName: '',
bmi: '' as any,
lungCapacity: '' as any,
sitAndReach: '' as any,
ropeSkipping: '' as any,
sitUp: '' as any,
standingLongJump: '' as any
})
const handleEdit = (row: any) => {
Object.assign(editForm, {
id: row.id,
studentName: row.studentName,
bmi: row.bmi ?? '',
lungCapacity: row.lungCapacity ?? '',
sitAndReach: row.sitAndReach ?? '',
ropeSkipping: row.ropeSkipping ?? '',
sitUp: row.sitUp ?? '',
standingLongJump: row.standingLongJump ?? ''
})
editDialog.visible = true
}
const handleEditConfirm = async () => {
try {
// TODO: 调用成绩修正接口
// await updateSportHallScore(editForm)
ElMessage.success('修正成功')
editDialog.visible = false
fetchList()
} catch (e) {
ElMessage.error('操作失败,请重试')
}
}
// ===================== 初始化 =====================
onMounted(() => {
fetchOptions()
fetchList()
})
</script>
<style lang="less" scoped>
.sport-hall-report {
width: calc(100% - 32px);
min-height: calc(100vh - 100px);
margin: 16px;
display: flex;
flex-direction: column;
gap: 12px;
// 面包屑
.breadcrumb {
background: #fff;
border-radius: 8px;
padding: 14px 20px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
:deep(.el-breadcrumb__inner) {
color: #606266;
font-size: 14px;
}
:deep(.el-breadcrumb__item:last-child .el-breadcrumb__inner) {
color: #303133;
font-weight: 600;
}
}
// 搜索卡片
.search-card {
background: #fff;
border-radius: 8px;
padding: 18px 20px 10px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
.search-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 12px 20px;
margin-bottom: 12px;
.search-item {
display: flex;
align-items: center;
.label {
font-size: 13px;
color: #606266;
white-space: nowrap;
margin-right: 4px;
}
}
.search-actions {
display: flex;
gap: 8px;
margin-left: auto;
.btn-reset {
border-radius: 6px;
padding: 8px 20px;
color: #606266;
border-color: #dcdfe6;
background: #fff;
font-size: 13px;
transition: all 0.2s;
&:hover {
color: #409eff;
border-color: #c6e2ff;
background: #ecf5ff;
}
}
.btn-search {
border-radius: 6px;
padding: 8px 24px;
font-size: 13px;
background: linear-gradient(135deg, #409eff 0%, #337ecc 100%);
border: none;
box-shadow: 0 2px 6px rgba(64, 158, 255, 0.4);
transition: all 0.2s;
&:hover {
background: linear-gradient(135deg, #66b1ff 0%, #409eff 100%);
box-shadow: 0 4px 10px rgba(64, 158, 255, 0.5);
transform: translateY(-1px);
}
&:active {
transform: translateY(0);
}
}
}
}
}
// 操作工具栏
.table-toolbar {
display: flex;
justify-content: flex-end;
align-items: center;
padding: 0 4px;
.toolbar-right {
display: flex;
gap: 10px;
.action-btn {
position: relative;
padding: 8px 24px 8px 30px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
border: none;
cursor: pointer;
transition: all 0.25s;
color: #fff;
.btn-badge {
position: absolute;
top: -7px;
left: 10px;
border-radius: 50%;
width: 16px;
height: 16px;
font-size: 11px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
border: 2px solid #fff;
}
}
.import-btn {
background: linear-gradient(135deg, #409eff 0%, #337ecc 100%);
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.45);
.btn-badge {
background: #fff;
color: #409eff;
}
&:hover {
background: linear-gradient(135deg, #66b1ff 0%, #409eff 100%);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.55);
transform: translateY(-1px);
}
&:active {
transform: translateY(0);
}
}
.send-btn {
background: linear-gradient(135deg, #67c23a 0%, #529b2e 100%);
box-shadow: 0 2px 8px rgba(103, 194, 58, 0.45);
.btn-badge {
background: #fff;
color: #67c23a;
}
&:hover {
background: linear-gradient(135deg, #85ce61 0%, #67c23a 100%);
box-shadow: 0 4px 12px rgba(103, 194, 58, 0.55);
transform: translateY(-1px);
}
&:active {
transform: translateY(0);
}
}
}
}
// 表格卡片
.table-card {
background: #fff;
border-radius: 8px;
padding: 16px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
flex: 1;
:deep(.el-table) {
border-radius: 6px;
overflow: hidden;
}
:deep(.el-table th) {
background: #f5f7fa !important;
}
:deep(.el-table--striped .el-table__body tr.el-table__row--striped td) {
background: #fafafa;
}
:deep(.el-button.is-link) {
color: #409eff;
font-size: 13px;
}
}
.pagination-wrap {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 16px;
padding: 0 4px;
}
}
// 导入弹窗
.import-dialog {
:deep(.el-dialog__header) {
padding: 20px 24px 12px;
border-bottom: 1px solid #f0f0f0;
margin: 0;
.el-dialog__title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
}
:deep(.el-dialog__body) {
padding: 20px 24px;
}
:deep(.el-dialog__footer) {
padding: 12px 24px 20px;
border-top: 1px solid #f0f0f0;
}
}
.import-content {
.import-tips {
background: #f0f7ff;
border-radius: 6px;
padding: 16px 18px;
margin-bottom: 20px;
p {
font-size: 13px;
color: #495464;
line-height: 2;
margin: 0;
.tip-num {
color: #409eff;
font-weight: 600;
}
}
}
.upload-area {
border: 1px dashed #d9d9d9;
border-radius: 8px;
padding: 24px;
background: #fafafa;
transition: border-color 0.2s;
&:hover {
border-color: #409eff;
}
.upload-dragger {
width: 100%;
:deep(.el-upload) {
width: 100%;
}
:deep(.el-upload-dragger) {
width: 100%;
height: auto;
padding: 24px 0;
border: none;
background: transparent;
display: flex;
flex-direction: column;
align-items: center;
}
}
.upload-inner {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
.excel-icon {
width: 80px;
height: 80px;
object-fit: contain;
}
.upload-text {
font-size: 14px;
color: #606266;
.upload-link {
color: #409eff;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
}
}
.download-btn-wrap {
display: flex;
justify-content: center;
margin-top: 16px;
.btn-download {
border-radius: 20px;
padding: 9px 24px;
font-size: 13px;
color: #409eff;
border: 1px solid #c6e2ff;
background: #ecf5ff;
transition: all 0.2s;
&:hover {
background: #409eff;
color: #fff;
border-color: #409eff;
box-shadow: 0 3px 8px rgba(64, 158, 255, 0.4);
}
}
}
}
}
</style>