Initial commit of akmon project
This commit is contained in:
777
pages/sport/teacher/students.uvue
Normal file
777
pages/sport/teacher/students.uvue
Normal file
@@ -0,0 +1,777 @@
|
||||
<!-- 学生列表页面 -->
|
||||
<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>
|
||||
Reference in New Issue
Block a user