634 lines
17 KiB
Plaintext
634 lines
17 KiB
Plaintext
<!-- 学生详情页面 -->
|
||
<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>
|