feat: 接入AI阳光跑页面与年级班级级联筛选

新增阳光跑路由和列表页面,按学生姓名、年级班级及日期查询记录并支持分页。

查询与 Excel 导出共用所选年级、班级 ID,保留学校后台的页面风格。
This commit is contained in:
ltx 2026-10-10 10:06:47 +08:00
parent eedc88e59f
commit 4e77cfeed2
4 changed files with 317 additions and 0 deletions

24
src/api/sunshineRun.js Normal file
View File

@ -0,0 +1,24 @@
import axios from 'axios'
import http from './http'
import store from '@/store/index'
function queryString(params) {
const query = new URLSearchParams()
Object.entries(params).forEach(([key, value]) => {
if (value !== null && value !== undefined && value !== '') {
query.append(key, value)
}
})
return query.toString()
}
export function getSunshineRunPage(params) {
return http.get(`/api/SunshineRun/GetPageList?${queryString(params)}`)
}
export function exportSunshineRun(params) {
return axios.get(`/api/SunshineRun/Export?${queryString(params)}`, {
responseType: 'blob',
headers: { Authorization: store.getters.getToken() }
})
}

View File

@ -428,6 +428,14 @@ const routes = [{
keepAlive: false
}
},
{
path: '/sunshineRun',
name: 'sunshineRun',
component: () => import('@/views/motionData/sunshineRun/index.vue'),
meta: {
keepAlive: false
}
},
{
path: '/trainingDetail', //运动数据/训练数据/详情
name: 'trainingDetail',

View File

@ -810,6 +810,7 @@ export default defineComponent({
'/userActivity': 'el-icon-s-marketing',
'/physicalTestData': 'el-icon-s-data',
'/trainingData': 'el-icon-s-data',
'/sunshineRun': 'el-icon-s-data',
'/visionData': 'el-icon-s-check',
'/specialManagement': 'el-icon-s-management',
'/ropeJumping': 'el-icon-s-cooperation',

View File

@ -0,0 +1,284 @@
<template>
<div class="sunshine-run">
<commonManage>
<div class="head">
<div class="left">
<p class="desctipt">姓名:</p>
<el-input v-model.trim="filters.studentName" class="m-2" clearable
placeholder="请输入姓名" @keyup.enter="search" />
</div>
<div class="left">
<p class="desctipt">班级:</p>
<el-cascader v-model="filters.gradeClass" class="grade-class-cascader"
:options="gradeClassOptions" :props="cascaderProps" clearable filterable
placeholder="请选择年级/班级" />
</div>
<div class="left">
<p class="desctipt">起始日期:</p>
<el-date-picker v-model="dateRange" class="date-range" type="daterange"
format="YYYY-MM-DD" value-format="YYYY-MM-DD" range-separator="至"
start-placeholder="开始日期" end-placeholder="结束日期" clearable />
</div>
<div class="right">
<el-button class="Search" :icon="Search" @click="search">查询</el-button>
<el-button class="Audit" :icon="Refresh" @click="reset">重置</el-button>
<el-button class="Export" :icon="Download" :loading="exporting" @click="exportData">导出</el-button>
</div>
</div>
<div class="cont">
<el-table v-loading="loading" :data="records" class="record-table"
:header-cell-style="headerCellStyle" :cell-style="cellStyle"
:row-style="{ height: '0px' }">
<el-table-column fixed prop="index" label="序号" width="60" />
<el-table-column prop="studentName" label="姓名" min-width="115" />
<el-table-column prop="sexName" label="性别" width="75" />
<el-table-column prop="className" label="班级" min-width="150" />
<el-table-column prop="effectiveDistance" label="有效里程" min-width="110" />
<el-table-column prop="effectiveTime" label="有效用时" min-width="110" />
<el-table-column prop="totalDistance" label="总里程" min-width="105" />
<el-table-column prop="totalTime" label="总用时" min-width="105" />
<el-table-column prop="startTime" label="开始时间" min-width="160" />
<el-table-column prop="endTime" label="结束时间" min-width="160" />
<template #empty>
<el-empty description="暂无阳光跑数据" />
</template>
</el-table>
<el-pagination
class="pagination"
v-model:current-page="pageIndex"
v-model:page-size="pageSize"
:page-sizes="[10, 50, 100, 200]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="changePageSize"
@current-change="loadRecords"
/>
</div>
</commonManage>
</div>
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue'
import { Download, Refresh, Search } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import commonManage from '@/components/commonManage/index.vue'
import { GetGradeNames } from '@/api/grade'
import { exportSunshineRun, getSunshineRunPage } from '@/api/sunshineRun'
const filters = reactive({ studentName: '', gradeClass: [] })
const gradeClassOptions = ref([])
const cascaderProps = { checkStrictly: true }
const dateRange = ref([])
const appliedFilters = reactive({ studentName: '', gradeId: '', classId: '', startDate: '', endDate: '' })
const records = ref([])
const loading = ref(false)
const exporting = ref(false)
const pageIndex = ref(1)
const pageSize = ref(10)
const total = ref(0)
const headerCellStyle = {
textAlign: 'center',
fontSize: '12px',
color: '#3F404A',
padding: '3px',
background: '#F8F8FA',
height: '50px'
}
const cellStyle = {
textAlign: 'center',
fontSize: '12px',
color: '#3F404A',
padding: '3px'
}
async function loadRecords() {
loading.value = true
try {
const response = await getSunshineRunPage({
...appliedFilters,
pageIndex: pageIndex.value,
pageSize: pageSize.value
})
if (response.code !== 200) throw new Error(response.message || '阳光跑数据加载失败')
records.value = response.data?.datas || []
total.value = response.data?.total || 0
} catch (error) {
ElMessage.error(error?.message || '阳光跑数据加载失败')
} finally {
loading.value = false
}
}
function getFilterParams() {
return {
studentName: filters.studentName,
gradeId: filters.gradeClass?.[0] || '',
classId: filters.gradeClass?.[1] || '',
startDate: dateRange.value?.[0] || '',
endDate: dateRange.value?.[1] || ''
}
}
async function loadGradeClassOptions() {
try {
const response = await GetGradeNames()
if (response.code !== 200) throw new Error(response.message || '年级班级加载失败')
gradeClassOptions.value = (response.data || []).map(grade => ({
value: grade.id,
label: grade.gradeName,
children: (grade.class || []).map(classroom => ({
value: classroom.id,
label: classroom.className
}))
}))
} catch (error) {
ElMessage.error(error?.message || '年级班级加载失败')
}
}
function search() {
Object.assign(appliedFilters, getFilterParams())
pageIndex.value = 1
loadRecords()
}
function reset() {
Object.assign(filters, { studentName: '', gradeClass: [] })
dateRange.value = []
pageSize.value = 10
search()
}
function changePageSize() {
pageIndex.value = 1
loadRecords()
}
async function exportData() {
exporting.value = true
try {
const response = await exportSunshineRun(getFilterParams())
const blob = response.data
if (!blob || blob.type?.includes('json')) throw new Error('导出失败')
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `AI阳光跑${new Date().toISOString().slice(0, 10)}.xlsx`
document.body.appendChild(link)
link.click()
link.remove()
setTimeout(() => URL.revokeObjectURL(url), 1000)
} catch (error) {
ElMessage.error(error?.message || '导出失败')
} finally {
exporting.value = false
}
}
onMounted(() => {
loadGradeClassOptions()
loadRecords()
})
</script>
<style lang="less" scoped>
.sunshine-run {
padding: 16px;
.head {
width: 100%;
min-height: 72px;
display: flex;
align-items: center;
.left {
width: auto;
height: 100%;
display: flex;
align-items: center;
.desctipt {
width: auto;
font-size: 16px;
font-family: PingFangSC;
font-weight: 500;
color: #3f404a;
margin-left: 20px;
white-space: nowrap;
}
.m-2 {
width: 200px;
margin-left: 8px;
}
.grade-class-cascader {
width: 220px;
margin-left: 8px;
}
.date-range {
width: 330px !important;
margin-left: 8px;
}
}
.right {
width: auto;
height: 100%;
display: flex;
align-items: center;
justify-content: flex-end;
margin-left: auto;
gap: 10px;
padding-right: 20px;
}
}
.cont {
width: 100%;
min-height: calc(100vh - 200px);
display: flex;
flex-direction: column;
.record-table {
width: calc(100% - 40px);
height: calc(100vh - 270px);
min-height: 360px;
margin: 0 20px;
}
.pagination {
display: flex;
justify-content: flex-end;
margin: 20px;
}
}
}
@media (max-width: 760px) {
.sunshine-run {
padding: 12px;
.head .left {
width: 100%;
.desctipt { margin-left: 0; }
.m-2 { flex: 1; margin-left: 8px; }
.grade-class-cascader { flex: 1; width: auto; min-width: 0; margin-left: 8px; }
.date-range { width: auto !important; flex: 1; min-width: 0; }
}
.head .right {
width: 100%;
padding-right: 0;
justify-content: flex-start;
}
.cont .pagination {
overflow-x: auto;
justify-content: flex-start;
}
}
}
</style>