Files
App.k12.Parents/pages/alerts/index.uvue
T

196 lines
12 KiB
Plaintext
Raw 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>
<view class="page-container">
<view class="header-panel">
<view class="back-button" @tap="goBack"><text class="back-icon">‹</text></view>
<view class="header-copy"><text class="header-title">学习预警</text><text class="header-subtitle">{{ studentName }}</text></view>
<view class="header-action" @tap="openStudentPicker"><UniIcons type="person-filled" class="header-action-icon"></UniIcons></view>
</view>
<scroll-view class="content-scroll" direction="vertical" scroll-y="true">
<view v-if="isLoading" class="status-panel"><text class="status-text">预警信息加载中</text></view>
<view v-else-if="alerts.length === 0" class="status-panel"><text class="status-text">暂无预警信息</text></view>
<view v-else class="content-body">
<view class="summary-card">
<text class="summary-title">今日触发预警</text>
<view class="summary-count-row"><text class="summary-count">{{ triggeredCount }}</text><text class="summary-unit">条</text></view>
<text class="summary-desc">其中 {{ triggeredCount }} 条与作业截止相关,建议今晚提醒孩子提交。</text>
</view>
<view class="list-heading"><text class="list-title">全部预警规则</text><text class="list-total">共 {{ alerts.length }} 条</text></view>
<view v-for="alert in alerts" :key="alert.id" class="alert-card">
<view class="alert-card-top"><view class="alert-level-dot" :class="alert.isTriggered ? 'alert-level-triggered' : 'alert-level-normal'"></view><text class="alert-title">{{ alert.title }}</text><text class="alert-status" :class="alert.isTriggered ? 'alert-status-triggered' : 'alert-status-normal'">{{ alert.isTriggered ? '已触发' : '正常' }}</text></view>
<text class="alert-content">{{ alert.content }}</text>
</view>
</view>
</scroll-view>
<page-container :show="showStudentPicker" position="bottom" :round="true" :overlay="true" :close-on-slide-down="true" @beforeleave="closeStudentPicker">
<view class="student-picker-panel">
<view class="student-picker-header">
<text class="student-picker-title">切换孩子</text>
<text class="student-picker-subtitle">切换后将刷新当前孩子的预警规则</text>
</view>
<scroll-view class="student-picker-list" direction="vertical" scroll-y="true">
<view v-if="isStudentsLoading" class="student-picker-empty"><text class="student-picker-empty-text">学生信息加载中</text></view>
<view v-else-if="boundStudents.length === 0" class="student-picker-empty"><text class="student-picker-empty-text">暂无已绑定学生</text></view>
<view v-else v-for="(item, index) in boundStudents" :key="item.id" class="student-picker-item" :class="selectedStudentId === item.id ? 'student-picker-item-selected' : ''" @tap="selectStudent(item.id)">
<view class="student-picker-avatar" :class="studentPickerAvatarClass(index)"><image v-if="item.avatar" class="student-picker-avatar-image" :src="item.avatar" mode="aspectFill"></image><text v-else class="student-picker-avatar-text">{{ studentPickerInitial(item.name) }}</text></view>
<view class="student-picker-info"><view class="student-picker-name-row"><text class="student-picker-name">{{ item.name }}</text><text class="student-picker-grade">{{ item.school }} {{ item.className }}</text></view><text class="student-picker-detail">账号:{{ item.account }}</text></view>
<text class="student-picker-action" :class="selectedStudentId === item.id ? 'student-picker-action-selected' : ''">{{ selectedStudentId === item.id ? '当前查看' : '切换' }}</text>
</view>
</scroll-view>
<view class="student-picker-footer"><view class="student-picker-cancel" @tap="closeStudentPicker"><text class="student-picker-cancel-text">取消</text></view><view class="student-picker-confirm" @tap="confirmStudent"><text class="student-picker-confirm-text">确定</text></view></view>
</view>
</page-container>
</view>
</template>
<script setup lang="ts">
import UniIcons from '../../uni_modules/uni-icons-x/components/uni-icons/uni-icons.uvue'
import { apiGetBoundStudents, apiGetStudentLearningWarnings, type BoundStudentSummary, type StudentAlert } from '../../api/student'
import { homeStudentProfile, selectedStudentChangeVersion, switchBoundStudent } from '../../store/home'
const alerts = ref<StudentAlert[]>([])
const isLoading = ref(true)
const isStudentsLoading = ref(false)
const showStudentPicker = ref(false)
const boundStudents = ref<BoundStudentSummary[]>([])
const selectedStudentId = ref('')
const observedStudentVersion = ref(selectedStudentChangeVersion.value)
const studentName = computed((): string => homeStudentProfile.value?.name ?? '孩子')
const triggeredCount = computed((): number => alerts.value.filter((item: StudentAlert) => item.isTriggered).length)
onLoad(() => {
loadAlerts()
})
onShow(() => {
if (observedStudentVersion.value !== selectedStudentChangeVersion.value) {
observedStudentVersion.value = selectedStudentChangeVersion.value
loadAlerts()
}
})
async function loadAlerts() {
isLoading.value = true
const studentId = homeStudentProfile.value?.id ?? ''
if (!studentId) {
isLoading.value = false
return
}
const res = await apiGetStudentLearningWarnings(studentId)
if (res && res.data) {
alerts.value = res.data.list
}
isLoading.value = false
}
function goBack() {
uni.navigateBack()
}
async function openStudentPicker() {
showStudentPicker.value = true
selectedStudentId.value = homeStudentProfile.value?.id ?? ''
isStudentsLoading.value = true
const res = await apiGetBoundStudents()
if (res && res.data) {
boundStudents.value = res.data.students
if (!selectedStudentId.value && res.data.currentStudentId) selectedStudentId.value = res.data.currentStudentId
}
isStudentsLoading.value = false
}
function closeStudentPicker() {
showStudentPicker.value = false
}
function selectStudent(studentId: string) {
selectedStudentId.value = studentId
}
async function confirmStudent() {
if (!selectedStudentId.value) {
uni.showToast({ title: '请选择学生', icon: 'none' })
return
}
const selectedStudent = boundStudents.value.find((item) => item.id === selectedStudentId.value)
if (selectedStudent) {
switchBoundStudent(selectedStudent)
observedStudentVersion.value = selectedStudentChangeVersion.value
}
closeStudentPicker()
await loadAlerts()
}
function studentPickerInitial(name: string): string {
return name.length > 1 ? name.substring(1, 2) : name.substring(0, 1)
}
function studentPickerAvatarClass(index: number): string {
if (index === 0) return 'student-picker-avatar-0'
if (index === 1) return 'student-picker-avatar-1'
return 'student-picker-avatar-2'
}
</script>
<style>
.page-container { display: flex; flex-direction: column; flex: 1; background-color: #f6f7fb; }
.header-panel { display: flex; flex-direction: row; align-items: center; padding: 6px 4px 8px; background-color: #ffffff; }
.back-button { display: flex; flex-direction: column; align-items: center; justify-content: center; width: 36px; height: 36px; border-radius: 18px; background-color: #eef2f7; }
.back-icon { font-size: 30px; line-height: 32px; color: #415066; }
.header-copy { display: flex; flex-direction: column; flex: 1; margin-left: 7px; }
.header-title { font-size: 15px; font-weight: 700; color: #111827; }
.header-subtitle { margin-top: 4px; font-size: 11px; color: #7c8aa0; }
.header-action { display: flex; flex-direction: column; align-items: center; justify-content: center; width: 36px; height: 36px; margin-right: 4px; border-radius: 18px; background-color: #edf1ff; }
.header-action-icon { font-size: 18px; color: #5947ea; }
.content-scroll { flex: 1; padding: 14px 4px 8px; }
.content-body { display: flex; flex-direction: column; }
.status-panel { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 160px; }
.status-text { font-size: 14px; color: #8992a5; }
.summary-card { display: flex; flex-direction: column; padding: 15px 16px 16px; margin-bottom: 14px; border-radius: 12px; background-color: #683dec; }
.summary-title { font-size: 12px; font-weight: 600; color: #ffffff; }
.summary-count-row { display: flex; flex-direction: row; align-items: flex-end; margin-top: 9px; }
.summary-count { font-size: 26px; line-height: 30px; font-weight: 800; color: #ffffff; }
.summary-unit { margin-left: 6px; margin-bottom: 3px; font-size: 13px; font-weight: 600; color: #ffffff; }
.summary-desc { margin-top: 10px; font-size: 12px; line-height: 18px; color: #ffffff; }
.list-heading { display: flex; flex-direction: row; align-items: center; justify-content: space-between; margin-bottom: 9px; }
.list-title { font-size: 15px; font-weight: 700; color: #121826; }
.list-total { font-size: 12px; color: #7e8ba0; }
.alert-card { display: flex; flex-direction: column; margin-bottom: 10px; padding: 15px 14px; border-radius: 13px; background-color: #ffffff; border: 1px solid #edf0f5; }
.alert-card-top { display: flex; flex-direction: row; align-items: center; }
.alert-level-dot { width: 7px; height: 7px; margin-right: 11px; border-radius: 4px; }
.alert-level-triggered { background-color: #ff3f6b; }
.alert-level-normal { background-color: #d2dce8; }
.alert-title { flex: 1; font-size: 14px; font-weight: 700; color: #1f2a3d; }
.alert-status { padding: 4px 8px; border-radius: 12px; font-size: 10px; }
.alert-status-triggered { color: #ff3f6b; background-color: #fff0f4; }
.alert-status-normal { color: #00a86b; background-color: #eafaf3; }
.alert-content { margin-top: 10px; margin-left: 18px; font-size: 12px; line-height: 19px; color: #536278; }
.student-picker-panel { display: flex; flex-direction: column; background-color: #ffffff; }
.student-picker-header { display: flex; flex-direction: column; padding: 18px 20px 12px; }
.student-picker-title { font-size: 18px; font-weight: 700; color: #242b3d; }
.student-picker-subtitle { margin-top: 5px; font-size: 12px; color: #8992a5; }
.student-picker-list { max-height: 340px; padding: 0 20px; }
.student-picker-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 150px; }
.student-picker-empty-text { font-size: 13px; color: #8992a5; }
.student-picker-item { display: flex; flex-direction: row; align-items: center; padding: 10px; margin-bottom: 10px; border: 1px solid #edf0f5; border-radius: 15px; background-color: #ffffff; }
.student-picker-item-selected { background-color: #f0efff; }
.student-picker-avatar { display: flex; flex-direction: column; align-items: center; justify-content: center; width: 46px; height: 46px; border-radius: 14px; background-color: #7161ed; }
.student-picker-avatar-0 { background-color: #6558e9; }
.student-picker-avatar-1 { background-color: #12a9df; }
.student-picker-avatar-2 { background-color: #e63a9c; }
.student-picker-avatar-image { width: 46px; height: 46px; border-radius: 14px; }
.student-picker-avatar-text { font-size: 18px; font-weight: 700; color: #ffffff; }
.student-picker-info { display: flex; flex-direction: column; flex: 1; margin-left: 11px; }
.student-picker-name-row { display: flex; flex-direction: row; align-items: center; margin-bottom: 5px; }
.student-picker-name { margin-right: 8px; font-size: 15px; font-weight: 700; color: #2a3042; }
.student-picker-grade { padding: 2px 6px; border-radius: 7px; font-size: 10px; color: #554be9; background-color: #e8e7ff; }
.student-picker-detail { margin-top: 2px; font-size: 11px; color: #7e8799; }
.student-picker-action { flex-shrink: 0; padding: 5px 8px; border-radius: 10px; font-size: 9px; color: #6e7c91; background-color: #f1f4f8; }
.student-picker-action-selected { color: #ffffff; background-color: #554be9; }
.student-picker-footer { display: flex; flex-direction: row; padding: 12px 20px 24px; border-top: 1px solid #edf0f5; }
.student-picker-cancel, .student-picker-confirm { display: flex; flex-direction: column; align-items: center; justify-content: center; flex: 1; height: 44px; border-radius: 11px; }
.student-picker-cancel { margin-right: 10px; background-color: #f0f2f7; }
.student-picker-confirm { background-color: #554be9; }
.student-picker-cancel-text { font-size: 15px; color: #596275; }
.student-picker-confirm-text { font-size: 15px; color: #ffffff; }
</style>