777 lines
21 KiB
Plaintext
777 lines
21 KiB
Plaintext
<!-- 学生列表页面 -->
|
||
<template>
|
||
<scroll-view direction="vertical" class="students-container">
|
||
<!-- Header -->
|
||
<view class="header">
|
||
<text class="title">学生列表</text>
|
||
<text class="subtitle">本人权限下的学生信息</text>
|
||
</view>
|
||
|
||
<!-- 搜索栏 -->
|
||
<view class="search-section">
|
||
<view class="search-box">
|
||
<text class="search-icon">🔍</text>
|
||
<input :value="searchQuery" placeholder="搜索学生姓名" class="search-input" @input="onSearchInput" />
|
||
</view>
|
||
</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="loadStudents">重试</button>
|
||
</view>
|
||
|
||
<!-- 学生列表 -->
|
||
<view v-else class="students-list">
|
||
<view v-if="filteredStudents.length === 0" class="empty-state">
|
||
<text class="empty-icon">👥</text>
|
||
<text class="empty-text">暂无学生数据</text>
|
||
</view>
|
||
<view v-for="student in filteredStudents" :key="student.id" class="student-card"
|
||
:class="{ 'abnormal-student': student.has_abnormal_vitals, 'not-arrived-student': student.inside_fence === false }" @click="viewStudentDetail(student.id)">
|
||
|
||
<view v-if="student.has_abnormal_vitals" class="abnormal-badge">
|
||
<text class="abnormal-icon">⚠️</text>
|
||
<text class="abnormal-text">{{ student.abnormal_count }}项异常</text>
|
||
</view>
|
||
|
||
<!-- 学生基本信息 -->
|
||
<view class="student-header">
|
||
<view class="avatar-container">
|
||
<image v-if="student.avatar" :src="student.avatar" class="student-avatar" mode="aspectFill" />
|
||
<view v-else class="student-avatar-placeholder">
|
||
<text class="avatar-text">{{ getInitials(student.name) }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="student-info">
|
||
<text class="student-name">{{ student.name }}</text>
|
||
<text class="student-id">学号: {{ student.student_id ?? '未设置' }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 健康数据 -->
|
||
<view class="health-data">
|
||
<view class="health-item">
|
||
<text class="health-icon">🌡️</text>
|
||
<view class="health-info">
|
||
<text class="health-label">体温</text>
|
||
<text class="health-value" :class="getTemperatureStatus(student.latest_temperature)">
|
||
{{ student.latest_temperature != null ? (student.latest_temperature as number).toFixed(1) : '--' }}°C
|
||
</text>
|
||
<text class="health-time">{{ formatTime(student.temperature_time as string | null) }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="health-item">
|
||
<text class="health-icon">❤️</text>
|
||
<view class="health-info">
|
||
<text class="health-label">心率</text>
|
||
<text class="health-value" :class="getHeartRateStatus(student.latest_heart_rate)">
|
||
{{ typeof student.latest_heart_rate === 'number' ? student.latest_heart_rate : '--' }}
|
||
bpm
|
||
</text>
|
||
<text class="health-time">{{ formatTime(student.heart_rate_time as string | null) }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="health-item">
|
||
<text class="health-icon">🫁</text>
|
||
<view class="health-info">
|
||
<text class="health-label">血氧</text>
|
||
<text class="health-value" :class="getOxygenStatus(student.latest_oxygen_level)">
|
||
{{ typeof student.latest_oxygen_level === 'number' ? student.latest_oxygen_level : '--' }}%
|
||
</text>
|
||
<text
|
||
class="health-time">{{ formatTime(student.oxygen_level_time as string | null) }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="health-item">
|
||
<text class="health-icon">👟</text>
|
||
<view class="health-info">
|
||
<text class="health-label">步数</text>
|
||
<text class="health-value normal">
|
||
{{ typeof student.latest_steps === 'number' ? student.latest_steps : '--' }} 步
|
||
</text>
|
||
<text class="health-time">{{ formatTime(student.steps_time as string | null) }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 到校 / 围栏状态 -->
|
||
<view class="arrival-status" :class="student.inside_fence == true ? 'arrived' : 'not-arrived'">
|
||
<text class="arrival-dot">●</text>
|
||
<text class="arrival-text">{{ getArrivalText(student) }}</text>
|
||
</view>
|
||
|
||
<!-- 箭头指示 -->
|
||
<view class="arrow-container">
|
||
<text class="arrow">›</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { ref, onMounted, computed } from 'vue'
|
||
import supa from '@/components/supadb/aksupainstance.uts'
|
||
import { getCurrentUserId, getCurrentUserClassId } from '@/utils/store.uts'
|
||
|
||
// 学生数据类型定义
|
||
type Student = {
|
||
id : string
|
||
name : string
|
||
student_id : string | null
|
||
avatar : string | null
|
||
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
|
||
created_at : string
|
||
// 新增异常标识
|
||
has_abnormal_vitals : boolean
|
||
abnormal_count : number
|
||
class_name ?: string // 班级名称
|
||
// 位置相关(新增)
|
||
lat ?: number | null
|
||
lng ?: number | null
|
||
location_time ?: string | null
|
||
inside_fence ?: boolean // 是否在围栏内(到校)
|
||
distance_m ?: number | null // 距离围栏中心米数
|
||
}
|
||
|
||
// 健康数据异常检测类型
|
||
type VitalAbnormalResult = {
|
||
abnormal : boolean;
|
||
count : number;
|
||
}
|
||
|
||
// 响应式数据
|
||
const students = ref<Array<Student>>([])
|
||
const loading = ref<boolean>(false)
|
||
const error = ref<string>('')
|
||
const searchQuery = ref<string>('')
|
||
// 围栏配置(到校判断)
|
||
const fenceCenterLat = ref<number>(0)
|
||
const fenceCenterLng = ref<number>(0)
|
||
const fenceRadiusM = ref<number>(120) // 米
|
||
const fenceLoaded = ref<boolean>(false)
|
||
|
||
// 计算过滤后的学生列表
|
||
const filteredStudents = computed<Array<Student>>(() => {
|
||
if (searchQuery.value.trim() == '') {
|
||
return students.value;
|
||
}
|
||
console.log(searchQuery.value)
|
||
return students.value.filter(student => {
|
||
if (typeof student.name == 'string') {
|
||
return student.name.toLocaleLowerCase().includes(searchQuery.value.toLocaleLowerCase());
|
||
}
|
||
return false;
|
||
});
|
||
})
|
||
// 判断生理指标是否异常
|
||
const checkVitalAbnormal = (
|
||
temp : number | null,
|
||
heartRate : number | null,
|
||
oxygen : number | null
|
||
) : VitalAbnormalResult => {
|
||
let abnormalCount = 0
|
||
|
||
// 体温异常判断 (正常范围: 36.0-37.5°C)
|
||
if (temp !== null && (temp < 36.0 || temp > 37.5)) {
|
||
abnormalCount++
|
||
}
|
||
|
||
// 心率异常判断 (正常范围: 60-100 bpm)
|
||
if (heartRate !== null && (heartRate < 60 || heartRate > 100)) {
|
||
abnormalCount++
|
||
}
|
||
|
||
// 血氧异常判断 (正常范围: ≥95%)
|
||
if (oxygen !== null && oxygen < 95) {
|
||
abnormalCount++
|
||
}
|
||
|
||
const result = {
|
||
abnormal: abnormalCount > 0,
|
||
count: abnormalCount
|
||
} as VitalAbnormalResult
|
||
return result
|
||
}
|
||
|
||
// 获取体温状态
|
||
const getTemperatureStatus = (temp : number | null) : string => {
|
||
if (temp === null) return 'unknown';
|
||
if (temp < 36.0 || temp > 37.5) return 'abnormal';
|
||
return 'normal'
|
||
}
|
||
|
||
// 获取心率状态
|
||
const getHeartRateStatus = (rate : number | null) : string => {
|
||
if (rate === null) return 'unknown';
|
||
if (rate < 60 || rate > 100) return 'abnormal';
|
||
return 'normal';
|
||
}
|
||
|
||
// 获取血氧状态
|
||
const getOxygenStatus = (level : number | null) : string => {
|
||
if (level === null) return 'unknown';
|
||
if (level < 95) return 'abnormal';
|
||
return 'normal';
|
||
} // 搜索输入处理
|
||
const onSearchInput = (e:UniInputEvent) => {
|
||
searchQuery.value = e.detail.value;
|
||
}
|
||
|
||
// 获取姓名首字母
|
||
const getInitials = (name : string) : string => {
|
||
if (name.trim() === '') 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 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 viewStudentDetail = (studentId : string) => {
|
||
uni.navigateTo({
|
||
url: `/pages/sport/teacher/student-detail?id=${studentId}`
|
||
})
|
||
} // 加载学生数据
|
||
const loadStudents = async () => {
|
||
loading.value = true;
|
||
error.value = '';
|
||
try {
|
||
const currentUser = getCurrentUserId();
|
||
if (currentUser === null || currentUser === '') {
|
||
error.value = '用户未登录';
|
||
return;
|
||
}
|
||
// 获取当前用户的class_id
|
||
const currentUserClassId = getCurrentUserClassId();
|
||
if (currentUserClassId === null || currentUserClassId === '') {
|
||
error.value = '用户未分配班级';
|
||
return;
|
||
}
|
||
console.log('开始加载学生数据...', '当前用户班级ID:', currentUserClassId);
|
||
// 直接获取同班级的学生ID
|
||
const studentsResponse = await supa
|
||
.from('ak_users')
|
||
.select('id, username,phone, avatar_url, class_id', {})
|
||
.eq('role', 'student')
|
||
.eq('class_id', currentUserClassId) // 使用当前用户的class_id
|
||
.execute()
|
||
|
||
if (studentsResponse.data == null || studentsResponse.status < 200 || studentsResponse.status >= 300) {
|
||
console.error('获取学生列表失败:', studentsResponse.status, studentsResponse.error)
|
||
students.value = []
|
||
return
|
||
}
|
||
|
||
const studentIds = (studentsResponse.data as Array<UTSJSONObject>).map(u => u['id'] as string)
|
||
|
||
if (studentIds.length === 0) {
|
||
console.warn('当前班级中没有学生')
|
||
students.value = []
|
||
return
|
||
}
|
||
|
||
console.log('找到同班级学生ID:', studentIds)
|
||
const response = await supa
|
||
.from('ak_users')
|
||
.select(`
|
||
id,
|
||
username,
|
||
email,
|
||
avatar_url,
|
||
class_id
|
||
`, {})
|
||
.eq('role', 'student')
|
||
.eq('class_id', currentUserClassId)
|
||
.limit(50)
|
||
.execute();
|
||
if (response.status >= 200 && response.status < 300 && response.data != null) {
|
||
const studentsData = response.data as Array<UTSJSONObject>;
|
||
// 处理学生基本信息,健康数据使用模拟值进行演示
|
||
const studentsWithHealth = studentsData.map((student) : Student => {
|
||
const studentId = student['id'] as string;
|
||
const studentName = (student['username'] != null && student['username'] !== '') ? student['username'] as string : '未命名';
|
||
const avatar = student['avatar_url'] as string | null;
|
||
|
||
// 生成模拟健康数据用于演示(实际项目中应该从真实的健康数据表获取)
|
||
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()
|
||
|
||
// 判断是否有异常指标
|
||
const vitalCheck = checkVitalAbnormal(mockTemp, mockHeartRate, mockOxygen)
|
||
|
||
const baseStudent: Student = {
|
||
id: studentId,
|
||
name: studentName,
|
||
student_id: (student['email'] != null && student['email'] !== '') ? student['email'] as string : null,
|
||
avatar: avatar,
|
||
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,
|
||
created_at: '',
|
||
has_abnormal_vitals: vitalCheck.abnormal,
|
||
abnormal_count: vitalCheck.count,
|
||
lat: null,
|
||
lng: null,
|
||
location_time: null,
|
||
inside_fence: false,
|
||
distance_m: null
|
||
}
|
||
return baseStudent
|
||
})
|
||
|
||
// 按异常情况排序:异常的排在前面,异常数量多的排在更前面
|
||
studentsWithHealth.sort((a, b) => {
|
||
// 首先按是否有异常排序
|
||
if (a.has_abnormal_vitals && !b.has_abnormal_vitals) return -1
|
||
if (!a.has_abnormal_vitals && b.has_abnormal_vitals) return 1
|
||
|
||
// 如果都有异常,按异常数量排序
|
||
if (a.has_abnormal_vitals && b.has_abnormal_vitals) {
|
||
return b.abnormal_count - a.abnormal_count
|
||
}
|
||
|
||
// 都正常的话按姓名排序
|
||
const nameA = a.name != null ? a.name : '';
|
||
const nameB = b.name != null ? b.name : '';
|
||
if (nameA < nameB) return -1;
|
||
if (nameA > nameB) return 1;
|
||
return 0;
|
||
})
|
||
|
||
students.value = studentsWithHealth
|
||
// 进一步加载位置数据并计算围栏
|
||
// 模拟位置数据
|
||
for (let i = 0; i < students.value.length; i++) {
|
||
const s = students.value[i]
|
||
// 随机生成一个在中心附近 300m 内的点;让部分学生超出围栏
|
||
const rand = Math.random()
|
||
const maxDist = 0.003 // 粗略对应几百米
|
||
const dLat = (Math.random() - 0.5) * maxDist
|
||
const dLng = (Math.random() - 0.5) * maxDist
|
||
s.lat = fenceCenterLat.value + dLat
|
||
s.lng = fenceCenterLng.value + dLng + (rand < 0.2 ? 0.01 : 0) // 20% 故意偏移较远,模拟未到校
|
||
s.location_time = new Date().toISOString()
|
||
// 计算距离
|
||
if (s.lat != null && s.lng != null) {
|
||
const R = 6371000.0
|
||
const toRad = (d: number): number => d * Math.PI / 180.0
|
||
const dLat = toRad(s.lat - fenceCenterLat.value)
|
||
const dLng = toRad(s.lng - fenceCenterLng.value)
|
||
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(toRad(fenceCenterLat.value)) * Math.cos(toRad(s.lat)) * Math.sin(dLng / 2) * Math.sin(dLng / 2)
|
||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
|
||
s.distance_m = Math.round(R * c)
|
||
s.inside_fence = s.distance_m <= fenceRadiusM.value
|
||
} else {
|
||
s.inside_fence = false
|
||
}
|
||
}
|
||
console.log(`成功加载 ${students.value.length} 个同班级学生数据`)
|
||
console.log(`异常学生数量: ${studentsWithHealth.filter(s => s.has_abnormal_vitals).length}`)
|
||
} else {
|
||
error.value = '加载学生健康数据失败'
|
||
console.error('学生健康数据查询失败:', response.status, response.error)
|
||
}
|
||
} catch (e) {
|
||
error.value = '网络错误,请稍后重试'
|
||
console.error('加载学生数据异常:', e)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// 页面加载时获取数据
|
||
onMounted(() => {
|
||
loadStudents()
|
||
})
|
||
|
||
// ========= 位置 / 围栏逻辑 =========
|
||
// 加载围栏配置(示例:从假设表 school_fences 读取)
|
||
const loadFenceConfig = async (): Promise<void> => {
|
||
// 真实情况:根据学校或班级ID查询中心点与半径
|
||
try {
|
||
// 这里做演示:如果尚未加载,就给一个固定坐标(示例坐标:上海市中心近似)
|
||
if (!fenceLoaded.value) {
|
||
fenceCenterLat.value = 31.2304
|
||
fenceCenterLng.value = 121.4737
|
||
fenceRadiusM.value = 150 // 半径 150m
|
||
fenceLoaded.value = true
|
||
}
|
||
} catch (e) {
|
||
console.error('加载围栏配置失败', e)
|
||
}
|
||
}
|
||
|
||
// 加载位置信息(假设有 device_locations 表:student_id, lat, lng, recorded_at)
|
||
const loadLocationsAndComputeFence = async (classId: string): Promise<void> => {
|
||
try {
|
||
// 示例:直接模拟位置数据(真实项目改为 supa 查询)
|
||
for (let i = 0; i < students.value.length; i++) {
|
||
const s = students.value[i]
|
||
// 随机生成一个在中心附近 300m 内的点;让部分学生超出围栏
|
||
const rand = Math.random()
|
||
const maxDist = 0.003 // 粗略对应几百米
|
||
const dLat = (Math.random() - 0.5) * maxDist
|
||
const dLng = (Math.random() - 0.5) * maxDist
|
||
s.lat = fenceCenterLat.value + dLat
|
||
s.lng = fenceCenterLng.value + dLng + (rand < 0.2 ? 0.01 : 0) // 20% 故意偏移较远,模拟未到校
|
||
s.location_time = new Date().toISOString()
|
||
// 计算距离
|
||
if (s.lat != null && s.lng != null) {
|
||
s.distance_m = computeDistanceMeters(fenceCenterLat.value, fenceCenterLng.value, s.lat, s.lng)
|
||
s.inside_fence = s.distance_m <= fenceRadiusM.value
|
||
} else {
|
||
s.inside_fence = false
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('加载位置数据失败', e)
|
||
}
|
||
}
|
||
|
||
// 计算两点间距离(Haversine 简化)
|
||
const computeDistanceMeters = (lat1: number, lng1: number, lat2: number, lng2: number): number => {
|
||
const R = 6371000.0
|
||
const toRad = (d: number): number => d * Math.PI / 180.0
|
||
const dLat = toRad(lat2 - lat1)
|
||
const dLng = toRad(lng2 - lng1)
|
||
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2)
|
||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
|
||
const dist = R * c
|
||
return Math.round(dist)
|
||
}
|
||
|
||
// 显示到校状态文字
|
||
const getArrivalText = (s: Student): string => {
|
||
if (s.inside_fence === true) {
|
||
return '已到校'
|
||
}
|
||
const distStr = s.distance_m != null ? (s.distance_m as number).toString() + 'm' : '未知距离'
|
||
return '未到校 • 距离围栏中心' + distStr
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.students-container {
|
||
flex: 1;
|
||
background-color: #f5f5f5;
|
||
}
|
||
|
||
.header {
|
||
padding: 20px;
|
||
background-color: #ffffff;
|
||
border-bottom: 1px solid #e5e5e5;
|
||
}
|
||
|
||
.title {
|
||
font-size: 24px;
|
||
font-weight: bold;
|
||
color: #333;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.subtitle {
|
||
font-size: 14px;
|
||
color: #666;
|
||
}
|
||
|
||
.search-section {
|
||
padding: 16px 20px;
|
||
background-color: #ffffff;
|
||
border-bottom: 1px solid #e5e5e5;
|
||
}
|
||
|
||
.search-box {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
background-color: #f8f9fa;
|
||
border-radius: 8px;
|
||
padding: 12px 16px;
|
||
border: 1px solid #e9ecef;
|
||
}
|
||
|
||
.search-icon {
|
||
font-size: 16px;
|
||
margin-right: 8px;
|
||
color: #6c757d;
|
||
}
|
||
|
||
.search-input {
|
||
flex: 1;
|
||
font-size: 16px;
|
||
color: #333;
|
||
}
|
||
|
||
.loading-container,
|
||
.error-container,
|
||
.empty-state {
|
||
flex: 1;
|
||
justify-content: center;
|
||
align-items: center;
|
||
padding: 40px 20px;
|
||
}
|
||
|
||
.loading-text,
|
||
.error-text,
|
||
.empty-text {
|
||
font-size: 16px;
|
||
color: #666;
|
||
text-align: center;
|
||
}
|
||
|
||
.empty-icon {
|
||
font-size: 48px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.retry-btn {
|
||
margin-top: 16px;
|
||
padding: 12px 24px;
|
||
background-color: #007AFF;
|
||
color: #ffffff;
|
||
border-radius: 8px;
|
||
border: none;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.students-list {
|
||
padding: 16px;
|
||
}
|
||
|
||
.student-card {
|
||
background-color: #ffffff;
|
||
border-radius: 12px;
|
||
margin-bottom: 16px;
|
||
padding: 16px;
|
||
flex-direction: column;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
position: relative;
|
||
}
|
||
|
||
.abnormal-student {
|
||
border-left: 4px solid #ff4757;
|
||
background-color: #fff5f5;
|
||
}
|
||
|
||
.not-arrived-student {
|
||
border-left: 4px solid #ffa502;
|
||
background-color: #fff8e6;
|
||
}
|
||
|
||
.abnormal-badge {
|
||
position: absolute;
|
||
top: 12px;
|
||
right: 12px;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
background-color: #ff4757;
|
||
padding: 4px 8px;
|
||
border-radius: 12px;
|
||
}
|
||
|
||
.abnormal-icon {
|
||
font-size: 12px;
|
||
margin-right: 4px;
|
||
}
|
||
|
||
.abnormal-text {
|
||
font-size: 10px;
|
||
color: #ffffff;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.student-header {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
flex: 1;
|
||
}
|
||
|
||
.avatar-container {
|
||
margin-right: 12px;
|
||
}
|
||
|
||
.student-avatar {
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: 24px;
|
||
}
|
||
|
||
.student-avatar-placeholder {
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: 24px;
|
||
background-color: #007AFF;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.avatar-text {
|
||
color: #ffffff;
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.student-info {
|
||
flex: 1;
|
||
}
|
||
|
||
.student-name {
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
color: #333;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.student-id {
|
||
font-size: 14px;
|
||
color: #666;
|
||
}
|
||
|
||
.health-data {
|
||
flex: 2;
|
||
margin-left: 16px;
|
||
}
|
||
|
||
.health-item {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.health-icon {
|
||
font-size: 16px;
|
||
margin-right: 8px;
|
||
width: 20px;
|
||
}
|
||
|
||
.health-info {
|
||
flex: 1;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
|
||
.health-label {
|
||
font-size: 12px;
|
||
color: #666;
|
||
width: 30px;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.health-value {
|
||
font-size: 14px;
|
||
font-weight: bold;
|
||
color: #333;
|
||
margin-right: 8px;
|
||
min-width: 60px;
|
||
}
|
||
|
||
.health-value.normal {
|
||
color: #27ae60;
|
||
}
|
||
|
||
.health-value.abnormal {
|
||
color: #e74c3c;
|
||
background-color: #ffebee;
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.health-value.unknown {
|
||
color: #95a5a6;
|
||
}
|
||
|
||
.health-time {
|
||
font-size: 11px;
|
||
color: #999;
|
||
}
|
||
|
||
.arrow-container {
|
||
margin-left: 16px;
|
||
}
|
||
|
||
.arrow {
|
||
font-size: 20px;
|
||
color: #ccc;
|
||
}
|
||
|
||
/* 到校状态样式 */
|
||
.arrival-status {
|
||
margin-top: 8px;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
|
||
.arrival-dot {
|
||
font-size: 10px;
|
||
margin-right: 6px;
|
||
}
|
||
|
||
.arrival-status.arrived .arrival-dot {
|
||
color: #2ecc71;
|
||
}
|
||
|
||
.arrival-status.not-arrived .arrival-dot {
|
||
color: #e67e22;
|
||
}
|
||
|
||
.arrival-text {
|
||
font-size: 12px;
|
||
color: #555;
|
||
}
|
||
|
||
.arrival-status.not-arrived .arrival-text {
|
||
color: #d35400;
|
||
font-weight: bold;
|
||
}
|
||
</style> |