Files
akmon/pages/sport/teacher/student-detail.uvue
2026-01-20 08:04:15 +08:00

634 lines
17 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 学生详情页面 -->
<template>
<scroll-view direction="vertical" class="student-detail-container">
<!-- Header -->
<view class="header">
<view class="back-btn" @click="goBack">
<text class="back-icon"></text>
<text class="back-text">返回</text>
</view>
<text class="title">学生详情</text>
</view>
<!-- 加载状态 -->
<view v-if="loading" class="loading-container">
<text class="loading-text">加载学生详情中...</text>
</view>
<!-- 错误状态 -->
<view v-else-if="error" class="error-container">
<text class="error-text">{{ error }}</text>
<button class="retry-btn" @click="loadStudentDetail">重试</button>
</view>
<!-- 学生详情内容 -->
<view v-else-if="student" class="content">
<!-- 学生基本信息 -->
<view class="student-info-card">
<view class="avatar-section">
<view class="avatar-container">
<image v-if="student?.avatar_url" :src="student?.avatar_url" class="student-avatar" mode="aspectFill" />
<view v-else class="student-avatar-placeholder">
<text class="avatar-text">{{ getInitials(student?.username) }}</text>
</view>
</view>
</view>
<view class="basic-info">
<text class="student-name">{{ student?.username != null ? student?.username as string : '' }}</text>
<text class="student-id">学号: {{ student?.phone != null ? student?.phone as string : '未设置' }}</text>
<text class="student-email">邮箱: {{ student?.email != null ? student?.email as string : '未设置' }}</text>
<text class="student-gender">性别: {{ getGenderText(student?.gender != null ? student?.gender as string : '') }}</text>
<text class="student-birthday">生日: {{ formatBirthday(student?.birthday != null ? student?.birthday as string : '') }}</text>
<text class="student-physical">身高: {{ student?.height_cm != null ? (student?.height_cm as number) + 'cm' : '未设置' }} | 体重: {{ student?.weight_kg != null ? (student?.weight_kg as number) + 'kg' : '未设置' }}</text>
<text class="student-bio" v-if="student?.bio != null && student?.bio !== ''">个人简介: {{ student?.bio as string }}</text>
<text class="join-date">注册时间: {{ formatDate(student?.created_at != null ? student?.created_at as string : '') }}</text>
</view>
</view>
<!-- 健康数据卡片 -->
<view class="health-cards">
<!-- 体温卡片 -->
<view class="health-card">
<view class="health-header">
<text class="health-icon">🌡️</text>
<text class="health-title">体温监测</text>
</view>
<view class="health-content">
<text class="health-current">{{ student?.latest_temperature != null ? student?.latest_temperature as number : '--' }}°C</text>
<text class="health-time">{{ formatTime(student?.temperature_time != null ? student?.temperature_time as string : '') }}</text>
<text class="health-status" :class="getTemperatureStatus(student?.latest_temperature != null ? student?.latest_temperature as number : null)">
{{ getTemperatureStatusText(student?.latest_temperature != null ? student?.latest_temperature as number : null) }}
</text>
</view>
</view>
<!-- 心率卡片 -->
<view class="health-card">
<view class="health-header">
<text class="health-icon">❤️</text>
<text class="health-title">心率监测</text>
</view>
<view class="health-content">
<text class="health-current">{{ student?.latest_heart_rate != null ? student?.latest_heart_rate as number : '--' }} bpm</text>
<text class="health-time">{{ formatTime(student?.heart_rate_time != null ? student?.heart_rate_time as string : '') }}</text>
<text class="health-status" :class="getHeartRateStatus(student?.latest_heart_rate != null ? student?.latest_heart_rate as number : null)">
{{ getHeartRateStatusText(student?.latest_heart_rate != null ? student?.latest_heart_rate as number : null) }}
</text>
</view>
</view>
<!-- 血氧卡片 -->
<view class="health-card">
<view class="health-header">
<text class="health-icon">🫁</text>
<text class="health-title">血氧监测</text>
</view>
<view class="health-content">
<text class="health-current">{{ student?.latest_oxygen_level != null ? student?.latest_oxygen_level as number : '--' }}%</text>
<text class="health-time">{{ formatTime(student?.oxygen_level_time != null ? student?.oxygen_level_time as string : '') }}</text>
<text class="health-status" :class="getOxygenStatus(student?.latest_oxygen_level != null ? student?.latest_oxygen_level as number : null)">
{{ getOxygenStatusText(student?.latest_oxygen_level != null ? student?.latest_oxygen_level as number : null) }}
</text>
</view>
</view>
<!-- 步数卡片 -->
<view class="health-card">
<view class="health-header">
<text class="health-icon">👟</text>
<text class="health-title">步数统计</text>
</view>
<view class="health-content">
<text class="health-current">{{ student?.latest_steps != null ? student?.latest_steps as number : '--' }} 步</text>
<text class="health-time">{{ formatTime(student?.steps_time != null ? student?.steps_time as string : '') }}</text>
<text class="health-status normal">今日活动</text>
</view>
</view>
</view>
<!-- 历史数据按钮 -->
<view class="actions-section">
<button class="action-btn primary" @click="viewHealthHistory">
<text class="action-text">查看健康历史数据</text>
</button>
<button class="action-btn secondary" @click="viewTrainingRecords">
<text class="action-text">查看训练记录</text>
</button>
</view>
</view>
</scroll-view>
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import supa from '@/components/supadb/aksupainstance.uts'
// Supabase查询返回的学生类型
export type StudentRecord = {
id: string,
username: string,
email: string,
phone: string,
avatar_url: string,
gender: string,
birthday: string,
height_cm: number,
weight_kg: number,
bio: string,
school_id: string,
grade_id: string,
class_id: string,
role: string,
created_at: string,
updated_at: string
}
// 学生详情数据类型 - 基于ak_users表字段
type StudentDetail = {
id: string
username: string // ak_users.username
email: string // ak_users.email (NOT NULL in DB)
phone: string | null // ak_users.phone (用作学号)
avatar_url: string | null // ak_users.avatar_url
gender: string | null // ak_users.gender
birthday: string | null // ak_users.birthday (date type)
height_cm: number | null // ak_users.height_cm (integer type)
weight_kg: number | null // ak_users.weight_kg (integer type)
bio: string | null // ak_users.bio
school_id: string | null // ak_users.school_id (uuid)
grade_id: string | null // ak_users.grade_id (uuid)
class_id: string | null // ak_users.class_id (uuid)
role: string | null // ak_users.role
created_at: string // ak_users.created_at
updated_at: string | null // ak_users.updated_at
// 健康数据字段(模拟)
latest_temperature: number | null
temperature_time: string | null
latest_heart_rate: number | null
heart_rate_time: string | null
latest_oxygen_level: number | null
oxygen_level_time: string | null
latest_steps: number | null
steps_time: string | null
}
// 响应式数据
const student = ref<StudentDetail | null>(null)
const loading = ref<boolean>(false)
const error = ref<string>('')
const studentId = ref<string>('')
// 返回按钮
const goBack = () => {
uni.navigateBack()
}
// 获取姓名首字母
const getInitials = (name: string | null): string => {
if (name == null || name === '') return 'N'
const words = name.trim().split(' ')
if (words.length >= 2) {
return (words[0].charAt(0) + words[1].charAt(0)).toUpperCase()
}
return name.charAt(0).toUpperCase()
}
// 格式化日期
const formatDate = (dateStr: string): string => {
if (dateStr == null || dateStr === '') return '未知'
try {
const date = new Date(dateStr)
return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`
} catch (e) {
return '日期格式错误'
}
}
// 格式化生日
const formatBirthday = (birthday: string | null): string => {
if (birthday == null || birthday === '') return '未设置'
try {
const date = new Date(birthday)
return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`
} catch (e) {
return '日期格式错误'
}
}
// 获取性别文本
const getGenderText = (gender: string | null): string => {
switch (gender) {
case 'male': return '男'
case 'female': return '女'
case 'other': return '其他'
default: return '未设置'
}
}
// 格式化时间
const formatTime = (timeStr: string | null): string => {
if (timeStr == null || timeStr === '') return '暂无数据'
try {
const date = new Date(timeStr)
const now = new Date()
const diffMs = now.getTime() - date.getTime()
const diffMins = Math.floor(diffMs / (1000 * 60))
const diffHours = Math.floor(diffMs / (1000 * 60 * 60))
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
if (diffMins < 60) {
return diffMins <= 0 ? '刚刚' : `${diffMins}分钟前`
} else if (diffHours < 24) {
return `${diffHours}小时前`
} else if (diffDays < 7) {
return `${diffDays}天前`
} else {
return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`
}
} catch (e) {
return '时间格式错误'
}
}
// 体温状态判断
const getTemperatureStatus = (temp: number | null): string => {
if (temp == null) return 'unknown'
if (temp < 36.0 || temp > 37.5) return 'abnormal'
return 'normal'
}
const getTemperatureStatusText = (temp: number | null): string => {
if (temp == null) return '无数据'
if (temp < 36.0) return '体温偏低'
if (temp > 37.5) return '体温偏高'
return '正常'
}
// 心率状态判断
const getHeartRateStatus = (rate: number | null): string => {
if (rate == null) return 'unknown'
if (rate < 60 || rate > 100) return 'abnormal'
return 'normal'
}
const getHeartRateStatusText = (rate: number | null): string => {
if (rate == null) return '无数据'
if (rate < 60) return '心率偏低'
if (rate > 100) return '心率偏高'
return '正常'
}
// 血氧状态判断
const getOxygenStatus = (level: number | null): string => {
if (level == null) return 'unknown'
if (level < 95) return 'abnormal'
return 'normal'
}
const getOxygenStatusText = (level: number | null): string => {
if (level == null) return '无数据'
if (level < 95) return '血氧偏低'
return '正常'
}
// 查看健康历史数据
const viewHealthHistory = () => {
uni.navigateTo({
url: `/pages/sport/teacher/student-health-history?id=${studentId.value}`
})
}
// 查看训练记录
const viewTrainingRecords = () => {
uni.navigateTo({
url: `/pages/sport/teacher/student-training-records?id=${studentId.value}`
})
} // 加载学生详情
const loadStudentDetail = async () => {
if (studentId.value == null || studentId.value === '') return
loading.value = true
error.value = ''
try {
console.log('开始加载学生详情:', studentId.value)
// 查询学生基本信息 - 基于ak_users表的实际字段
const response = await supa
.from('ak_users')
.select(`
id,
username,
email,
phone,
avatar_url,
gender,
birthday,
height_cm,
weight_kg,
bio,
school_id,
grade_id,
class_id,
role,
created_at,
updated_at
`, {})
.eq('id', studentId.value)
.eq('role', 'student')
.single()
.executeAs<StudentRecord>()
if (response.status >= 200 && response.status < 300 && response.data != null) {
// UTS supabase .single() 返回的data无法直接点语法或下标访问需转为普通对象
const studentObj =response.data as StudentRecord;
// 生成模拟健康数据
const mockTemp = 36.0 + Math.random() * 2.0 // 36.0-38.0度
const mockHeartRate = 60 + Math.random() * 40 // 60-100 bpm
const mockOxygen = 95 + Math.random() * 5 // 95-100%
const mockSteps = Math.floor(Math.random() * 10000) // 0-10000步
const mockTime = new Date().toISOString()
student.value = {
id: studentObj.id != null ? studentObj.id : '',
username: studentObj.username != null ? studentObj.username : '未命名',
email: studentObj.email != null ? studentObj.email : '',
phone: studentObj.phone != null ? studentObj.phone : null,
avatar_url: studentObj.avatar_url != null ? studentObj.avatar_url : null,
gender: studentObj.gender != null ? studentObj.gender : null,
birthday: studentObj.birthday != null ? studentObj.birthday : null,
height_cm: studentObj.height_cm != null ? studentObj.height_cm : null,
weight_kg: studentObj.weight_kg != null ? studentObj.weight_kg : null,
bio: studentObj.bio != null ? studentObj.bio : null,
school_id: studentObj.school_id != null ? studentObj.school_id : null,
grade_id: studentObj.grade_id != null ? studentObj.grade_id : null,
class_id: studentObj.class_id != null ? studentObj.class_id : null,
role: studentObj.role != null ? studentObj.role : null,
created_at: studentObj.created_at != null ? studentObj.created_at : '',
updated_at: studentObj.updated_at != null ? studentObj.updated_at : null,
// 模拟健康数据
latest_temperature: mockTemp,
temperature_time: mockTime,
latest_heart_rate: mockHeartRate,
heart_rate_time: mockTime,
latest_oxygen_level: mockOxygen,
oxygen_level_time: mockTime,
latest_steps: mockSteps,
steps_time: mockTime
} as StudentDetail
// 类型保护避免Smart cast错误
const stu = student.value
console.log('学生详情加载成功:', stu != null ? stu.username : '')
} else {
error.value = '未找到该学生信息'
console.error('学生信息查询失败:', response.status, response.error)
}
} catch (e) {
error.value = '网络错误,请稍后重试'
console.error('加载学生详情异常:', e)
} finally {
loading.value = false
}
} // Lifecycle
// 页面参数options兼容UTS转为普通对象再访
// Lifecycle
onLoad((options: OnLoadOptions) => {
const id = options['id']
if (id !== null) {
studentId.value = id as string
} else {
studentId.value = ''
}
loadStudentDetail()
})
</script>
<style scoped>
.student-detail-container {
flex: 1;
background-color: #f5f5f5;
}
.header {
padding: 16px 20px;
background-color: #ffffff;
border-bottom: 1px solid #e5e5e5;
flex-direction: row;
align-items: center;
}
.back-btn {
flex-direction: row;
align-items: center;
margin-right: 16px;
}
.back-icon {
font-size: 24px;
color: #007AFF;
margin-right: 4px;
}
.back-text {
font-size: 16px;
color: #007AFF;
}
.title {
font-size: 20px;
font-weight: bold;
color: #333;
}
.loading-container, .error-container {
flex: 1;
justify-content: center;
align-items: center;
padding: 40px 20px;
}
.loading-text, .error-text {
font-size: 16px;
color: #666;
text-align: center;
}
.retry-btn {
margin-top: 16px;
padding: 12px 24px;
background-color: #007AFF;
color: #ffffff;
border-radius: 8px;
border: none;
font-size: 16px;
}
.content {
padding: 16px;
}
.student-info-card {
background-color: #ffffff;
border-radius: 12px;
padding: 20px;
margin-bottom: 16px;
align-items: center;
}
.avatar-section {
margin-bottom: 16px;
}
.avatar-container {
align-items: center;
}
.student-avatar {
width: 80px;
height: 80px;
border-radius: 40px;
}
.student-avatar-placeholder {
width: 80px;
height: 80px;
border-radius: 40px;
background-color: #007AFF;
justify-content: center;
align-items: center;
}
.avatar-text {
color: #ffffff;
font-size: 24px;
font-weight: bold;
}
.basic-info {
align-items: center;
display: flex;
flex-direction: column;
justify-content: flex-start;
overflow-wrap: normal;
}
.student-name {
font-size: 24px;
font-weight: bold;
color: #333;
margin-bottom: 8px;
}
.student-id, .student-email, .student-gender, .student-birthday, .student-physical, .student-bio, .join-date {
font-size: 14px;
color: #666;
margin-bottom: 4px;
}
.student-bio {
font-size: 13px;
line-height: 1.4;
color: #888;
margin-top: 8px;
}
.health-cards {
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
}
.health-card {
width: 48%;
background-color: #ffffff;
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
}
.health-header {
flex-direction: row;
align-items: center;
margin-bottom: 12px;
}
.health-icon {
font-size: 20px;
margin-right: 8px;
}
.health-title {
font-size: 16px;
font-weight: bold;
color: #333;
}
.health-content {
align-items: center;
}
.health-current {
font-size: 24px;
font-weight: bold;
color: #333;
margin-bottom: 4px;
}
.health-time {
font-size: 12px;
color: #999;
margin-bottom: 8px;
}
.health-status {
font-size: 12px;
padding: 4px 8px;
border-radius: 12px;
text-align: center;
}
.health-status.normal {
color: #22c55e;
background-color: #dcfce7;
}
.health-status.abnormal {
color: #ef4444;
background-color: #fef2f2;
}
.health-status.unknown {
color: #6b7280;
background-color: #f3f4f6;
}
.actions-section {
margin-top: 20px;
}
.action-btn {
width: 100%;
padding: 16px;
border-radius: 12px;
border: none;
margin-bottom: 12px;
font-size: 16px;
font-weight: bold;
}
.action-btn.primary {
background-color: #007AFF;
color: #ffffff;
}
.action-btn.secondary {
background-color: #f8f9fa;
color: #333;
border: 1px solid #dee2e6;
}
.action-text {
color: inherit;
}
</style>