529 lines
14 KiB
Vue
529 lines
14 KiB
Vue
<template>
|
||
<view class="grades-container">
|
||
<!-- 页面标题区域 -->
|
||
<view class="header-section">
|
||
<view class="header-title">
|
||
<text class="title">成绩排名</text>
|
||
<text class="subtitle">学生最佳成绩排名</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- ==================== 项目选择区域 ==================== -->
|
||
<view class="project-select-section">
|
||
<view class="section-header">
|
||
<text class="section-title">选择项目</text>
|
||
</view>
|
||
<!-- 项目选择器 -->
|
||
<picker mode="selector" :range="projectOptions" range-key="name" :value="selectedProjectIndex"
|
||
@change="handleProjectChange">
|
||
<view class="picker-wrapper">
|
||
<text class="picker-text">{{ selectedProjectName }}</text>
|
||
<u-icon name="arrow-down" size="18" color="#999"></u-icon>
|
||
</view>
|
||
</picker>
|
||
</view>
|
||
|
||
|
||
<!-- ==================== 排名列表区域 ==================== -->
|
||
<!-- 仅在选择了项目且有数据时显示 -->
|
||
<view class="ranking-section" v-if="selectedProjectId && gradeList.length > 0">
|
||
<!-- 排名统计卡片 -->
|
||
<view class="stats-card">
|
||
<view class="stat-item">
|
||
<text class="stat-label">对比项目</text>
|
||
<text class="stat-value">{{ selectedProjectName }}</text>
|
||
</view>
|
||
<view class="stat-divider"></view>
|
||
<view class="stat-item">
|
||
<text class="stat-label">参与人数</text>
|
||
<text class="stat-value">{{ gradeList.length }}人</text>
|
||
</view>
|
||
<view class="stat-divider"></view>
|
||
<view class="stat-item">
|
||
<text class="stat-label">最高速度</text>
|
||
<text class="stat-value">{{ maxSpeed }}m/s</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 排名列表 -->
|
||
<view class="ranking-list">
|
||
<view v-for="(item, index) in gradeList" :key="item.id"
|
||
:class="['ranking-item', { 'top-three': index < 3 }]" @click="handleRankingClick(item)">
|
||
<!-- 排名徽章 -->
|
||
<view class="rank-badge" :class="`rank-${index + 1}`">
|
||
<text class="rank-number">{{ index + 1 }}</text>
|
||
</view>
|
||
|
||
<!-- 学生信息 -->
|
||
<view class="student-info">
|
||
<text class="student-name">{{ item.name }}</text>
|
||
<text class="student-speed">最快速度:{{ item.bestSpeed }}m/s</text>
|
||
</view>
|
||
|
||
<!-- 成绩详情 -->
|
||
<view class="speed-detail">
|
||
<text class="detail-text">用时:{{ item.bestTime }}s</text>
|
||
<text class="detail-text">日期:{{ item.recordDate }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { onShow, onLoad } from "@dcloudio/uni-app"
|
||
import { Service } from '@/Service/Service'
|
||
import { ref, computed } from 'vue'
|
||
|
||
// ==================== 响应式数据 - 项目相关 ====================
|
||
|
||
// 项目列表数据
|
||
const projectList = ref([
|
||
{ id: '1', name: '100米自由泳' },
|
||
{ id: '2', name: '50米自由泳' },
|
||
{ id: '3', name: '200米自由泳' },
|
||
{ id: '4', name: '100米蛙泳' }
|
||
])
|
||
|
||
// 项目选择器选项(用于 picker 组件)
|
||
const projectOptions = ref(projectList.value)
|
||
|
||
// 当前选中的项目索引(用于 picker 组件的显示)
|
||
const selectedProjectIndex = ref(-1)
|
||
|
||
// 当前选中的项目 ID
|
||
const selectedProjectId = ref('')
|
||
|
||
// 当前选中的项目名称(用于显示)
|
||
const selectedProjectName = computed(() => {
|
||
if (selectedProjectIndex.value === -1) {
|
||
return '请选择项目'
|
||
}
|
||
return projectList.value[selectedProjectIndex.value]?.name || '请选择项目'
|
||
})
|
||
|
||
// ==================== TypeScript 接口定义 ====================
|
||
|
||
/**
|
||
* 学生成绩接口
|
||
* 定义学生的成绩和排名信息
|
||
*/
|
||
interface StudentGrade {
|
||
id : string // 学生 ID
|
||
name : string // 学生姓名
|
||
bestSpeed : number // 最快速度(m/s)
|
||
bestTime : number // 最快用时(秒)
|
||
recordDate : string // 记录日期
|
||
studentId : string // 学生 ID(用于数据关联)
|
||
}
|
||
|
||
// ==================== 响应式数据 - 排名相关 ====================
|
||
|
||
// 排名列表数据
|
||
const gradeList = ref<StudentGrade[]>([])
|
||
|
||
// 最高速度(计算属性)
|
||
const maxSpeed = computed(() => {
|
||
if (gradeList.value.length === 0) return 0
|
||
return Math.max(...gradeList.value.map(item => item.bestSpeed)).toFixed(2)
|
||
})
|
||
|
||
// ==================== 模拟数据 ====================
|
||
|
||
/**
|
||
* 模拟的学生成绩数据(按项目分组)
|
||
* 键为项目 ID,值为该项目的学生成绩列表
|
||
*/
|
||
const mockStudentGrades : Record<string, StudentGrade[]> = {
|
||
'1': [
|
||
{ id: 'g1', name: '王小明', bestSpeed: 3.89, bestTime: 25.72, recordDate: '03-24', studentId: 's3' },
|
||
{ id: 'g2', name: '张小明', bestSpeed: 3.97, bestTime: 25.18, recordDate: '03-24', studentId: 's1' },
|
||
{ id: 'g3', name: '赵小芳', bestSpeed: 3.03, bestTime: 32.96, recordDate: '03-14', studentId: 's4' },
|
||
{ id: 'g4', name: '李小红', bestSpeed: 3.38, bestTime: 29.58, recordDate: '03-24', studentId: 's2' },
|
||
{ id: 'g5', name: '陈小刚', bestSpeed: 2.82, bestTime: 35.46, recordDate: '03-19', studentId: 's5' }
|
||
],
|
||
'2': [
|
||
{ id: 'g6', name: '杨小龙', bestSpeed: 2.05, bestTime: 24.39, recordDate: '03-19', studentId: 's8' },
|
||
{ id: 'g7', name: '刘小华', bestSpeed: 2.11, bestTime: 23.70, recordDate: '03-19', studentId: 's6' },
|
||
{ id: 'g8', name: '张小丽', bestSpeed: 1.91, bestTime: 26.18, recordDate: '03-16', studentId: 's7' }
|
||
],
|
||
'3': [
|
||
{ id: 'g9', name: '吴小西', bestSpeed: 1.59, bestTime: 125.79, recordDate: '03-16', studentId: 's10' },
|
||
{ id: 'g10', name: '黄小东', bestSpeed: 1.61, bestTime: 124.23, recordDate: '03-14', studentId: 's9' },
|
||
{ id: 'g11', name: '周小南', bestSpeed: 0.00, bestTime: 0, recordDate: '-', studentId: 's11' },
|
||
{ id: 'g12', name: '徐小北', bestSpeed: 0.00, bestTime: 0, recordDate: '-', studentId: 's12' }
|
||
],
|
||
'4': [
|
||
{ id: 'g13', name: '马小兵', bestSpeed: 1.22, bestTime: 81.97, recordDate: '03-14', studentId: 's13' },
|
||
{ id: 'g14', name: '朱小红', bestSpeed: 1.13, bestTime: 88.47, recordDate: '03-12', studentId: 's14' }
|
||
]
|
||
}
|
||
|
||
// ==================== 生命周期钩子 ====================
|
||
|
||
/**
|
||
* 页面加载时触发
|
||
* 在页面初始化时执行,只执行一次
|
||
*/
|
||
onLoad(() => {
|
||
// 初始化数据
|
||
loadProjectData()
|
||
})
|
||
|
||
/**
|
||
* 页面显示时触发
|
||
* 每次页面从后台切换到前台时执行
|
||
*/
|
||
onShow(() => {
|
||
// TODO: 如果需要在页面显示时刷新数据,可以在这里添加逻辑
|
||
})
|
||
|
||
// ==================== 业务逻辑方法 - 项目相关 ====================
|
||
|
||
/**
|
||
* 加载项目数据
|
||
* 从后端 API 获取项目列表
|
||
*/
|
||
const loadProjectData = () => {
|
||
// TODO: 调用后端 API 获取项目列表
|
||
// 示例代码:
|
||
// Service.Request('/api/projects', 'GET').then(res => {
|
||
// projectList.value = res.data
|
||
// projectOptions.value = res.data
|
||
// })
|
||
}
|
||
|
||
/**
|
||
* 处理项目选择变化事件
|
||
* 当用户选择项目时触发
|
||
* @param e picker 选择事件对象,包含 detail.value 属性表示选中的索引
|
||
*/
|
||
const handleProjectChange = (e : any) => {
|
||
// 获取选中的项目索引
|
||
const index = e.detail.value
|
||
|
||
// 更新选中项目的索引
|
||
selectedProjectIndex.value = index
|
||
|
||
// 获取选中的项目
|
||
const selectedProject = projectList.value[index]
|
||
|
||
if (selectedProject) {
|
||
// 更新选中项目的 ID
|
||
selectedProjectId.value = selectedProject.id
|
||
|
||
// 加载该项目的排名数据
|
||
loadProjectGrades(selectedProject.id)
|
||
}
|
||
|
||
// TODO: 实际项目中应该调用 API 获取该项目的排名数据
|
||
}
|
||
|
||
/**
|
||
* 加载项目排名数据
|
||
* 根据项目 ID 获取该项目的学生成绩排名列表
|
||
* @param projectId 项目 ID
|
||
*/
|
||
const loadProjectGrades = (projectId : string) => {
|
||
// 从模拟数据中获取排名列表
|
||
if (mockStudentGrades[projectId]) {
|
||
// 按最快速度从高到低排序
|
||
gradeList.value = [...mockStudentGrades[projectId]].sort((a, b) => b.bestSpeed - a.bestSpeed)
|
||
} else {
|
||
gradeList.value = []
|
||
}
|
||
}
|
||
|
||
// ==================== 业务逻辑方法 - 排名相关 ====================
|
||
|
||
/**
|
||
* 处理排名项点击事件
|
||
* 点击某排名项查看详情
|
||
* @param item 排名项数据
|
||
*/
|
||
const handleRankingClick = (item : StudentGrade) => {
|
||
console.log('点击了排名项:', item)
|
||
// TODO: 跳转到学生详情页面
|
||
// Service.GoPage('/pages/student/detail', { studentId: item.studentId })
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
// 页面背景色设置
|
||
page {
|
||
background-color: #f5f5f5;
|
||
}
|
||
|
||
// 页面容器
|
||
.grades-container {
|
||
min-height: 100vh;
|
||
padding-bottom: 40rpx;
|
||
}
|
||
|
||
/* ==================== 页面标题区域 ==================== */
|
||
.header-section {
|
||
background-color: #fff;
|
||
padding: 32rpx 28rpx 24rpx;
|
||
margin-bottom: 20rpx;
|
||
|
||
.header-title {
|
||
.title {
|
||
font-size: 36rpx;
|
||
font-weight: 700;
|
||
color: #333;
|
||
display: block;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.subtitle {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ==================== 项目选择区域 ==================== */
|
||
.project-select-section {
|
||
background-color: #fff;
|
||
margin: 0 20rpx 20rpx;
|
||
border-radius: 20rpx;
|
||
padding: 28rpx;
|
||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
|
||
position: relative;
|
||
overflow: hidden;
|
||
transition: all 0.3s ease;
|
||
|
||
&::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
width: 6rpx;
|
||
// background: linear-gradient(180deg, #1890ff 0%, #096dd9 100%);
|
||
border-radius: 20rpx 0 0 20rpx;
|
||
}
|
||
|
||
.picker-wrapper {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 20rpx 24rpx;
|
||
background-color: #f8f8f8;
|
||
border-radius: 12rpx;
|
||
border: 1rpx solid #e8e8e8;
|
||
transition: all 0.3s ease;
|
||
|
||
&:active {
|
||
background-color: #fff0f5;
|
||
border-color: #1890ff;
|
||
transform: scale(0.98);
|
||
}
|
||
|
||
.picker-text {
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
|
||
/* ==================== 排名列表区域 ==================== */
|
||
.ranking-section {
|
||
margin: 0 20rpx;
|
||
|
||
// 统计卡片
|
||
.stats-card {
|
||
background-color: #fff;
|
||
border-radius: 20rpx;
|
||
padding: 24rpx;
|
||
margin-bottom: 20rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-around;
|
||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
|
||
position: relative;
|
||
overflow: hidden;
|
||
|
||
&::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
width: 6rpx;
|
||
// background: linear-gradient(180deg, #1890ff 0%, #096dd9 100%);
|
||
border-radius: 20rpx 0 0 20rpx;
|
||
}
|
||
|
||
.stat-item {
|
||
text-align: center;
|
||
|
||
.stat-label {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
display: block;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 32rpx;
|
||
font-weight: 700;
|
||
color: #1890ff;
|
||
}
|
||
}
|
||
|
||
.stat-divider {
|
||
width: 1rpx;
|
||
height: 50rpx;
|
||
background-color: #eee;
|
||
}
|
||
}
|
||
|
||
// 排名列表容器
|
||
.ranking-list {
|
||
|
||
// 排名项
|
||
.ranking-item {
|
||
background-color: #fff;
|
||
border-radius: 20rpx;
|
||
padding: 24rpx;
|
||
margin-bottom: 16rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20rpx;
|
||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
|
||
transition: all 0.3s ease;
|
||
position: relative;
|
||
overflow: hidden;
|
||
|
||
&::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
width: 6rpx;
|
||
// background: linear-gradient(180deg, #1890ff 0%, #096dd9 100%);
|
||
opacity: 0;
|
||
transition: opacity 0.3s ease;
|
||
border-radius: 20rpx 0 0 20rpx;
|
||
}
|
||
|
||
&:active {
|
||
transform: scale(0.98);
|
||
box-shadow: 0 4rpx 16rpx rgba(235, 47, 150, 0.15);
|
||
|
||
&::before {
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
// 排名徽章
|
||
.rank-badge {
|
||
width: 60rpx;
|
||
height: 60rpx;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
background-color: #f0f0f0;
|
||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||
|
||
.rank-number {
|
||
font-size: 24rpx;
|
||
font-weight: 700;
|
||
color: #999;
|
||
}
|
||
|
||
// 前三名特殊颜色
|
||
&.rank-1 {
|
||
background: linear-gradient(135deg, #ffd700 0%, #ffec3d 100%);
|
||
box-shadow: 0 2rpx 8.6rpx rgba(255, 215, 0, 0.4);
|
||
|
||
.rank-number {
|
||
color: #fff;
|
||
font-size: 28rpx;
|
||
}
|
||
}
|
||
|
||
&.rank-2 {
|
||
background: linear-gradient(135deg, #c0c0c0 0%, #d9d9d9 100%);
|
||
box-shadow: 0 2rpx 8rpx rgba(192, 192, 192, 0.4);
|
||
|
||
.rank-number {
|
||
color: #fff;
|
||
}
|
||
}
|
||
|
||
&.rank-3 {
|
||
background: linear-gradient(135deg, #cd7f32 0%, #e6963d 100%);
|
||
box-shadow: 0 2rpx 8rpx rgba(205, 127, 50, 0.4);
|
||
|
||
.rank-number {
|
||
color: #fff;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 学生头像
|
||
.student-avatar {
|
||
width: 70rpx;
|
||
height: 70rpx;
|
||
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
box-shadow: 0 2rpx 8rpx rgba(235, 47, 150, 0.3);
|
||
|
||
.avatar-text {
|
||
font-size: 28rpx;
|
||
font-weight: 700;
|
||
color: #fff;
|
||
}
|
||
}
|
||
|
||
// 学生信息
|
||
.student-info {
|
||
flex: 1;
|
||
min-width: 0;
|
||
|
||
.student-name {
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: #333;
|
||
display: block;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.student-speed {
|
||
font-size: 26rpx;
|
||
color: #1890ff;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
|
||
// 成绩详情
|
||
.speed-detail {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16rpx;
|
||
flex-shrink: 0;
|
||
|
||
.detail-text {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
background-color: #f5f5f5;
|
||
padding: 6rpx 12rpx;
|
||
border-radius: 6rpx;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style> |