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

271 lines
16 KiB
Plaintext

<template>
<view class="page-container">
<view class="header-panel">
<view class="header-copy">
<text class="header-title">作业任务</text>
<text class="header-subtitle">{{ headerStudentName }} · 待完成 {{ tasks.pendingCount }} 个</text>
</view>
<view class="header-action" @tap="openStudentPicker"><UniIcons type="person-filled" class="header-action-icon"></UniIcons></view>
</view>
<view class="filter-panel">
<view v-for="filter in filters" :key="filter.id" class="filter-tab" :class="activeFilter === filter.id ? 'filter-tab-active' : ''" @tap="setFilter(filter.id)">
<text class="filter-text" :class="activeFilter === filter.id ? 'filter-text-active' : ''">{{ filter.name }}</text>
</view>
</view>
<scroll-view class="content-scroll" direction="vertical" scroll-y="true" refresher-enabled="true" :refresher-triggered="isRefreshing" @refresherrefresh="refreshTasks">
<view v-if="isLoading" class="status-panel"><text class="status-text">任务加载中</text></view>
<view v-else-if="visibleGroups.length === 0" class="empty-panel"><text class="empty-text">暂无任务</text></view>
<view v-else class="content-panel">
<view v-for="group in visibleGroups" :key="group.id" class="group-card">
<view class="group-heading">
<view class="group-title-row">
<text class="subject-tag">{{ group.subject || '学科' }}</text>
<text class="unit-tag">{{ group.unit || '学习' }}</text>
<text class="lesson-title">{{ group.lesson }}</text>
</view>
<text class="knowledge-count">{{ group.knowledgeCount }} 个知识点</text>
</view>
<view class="lesson-time-row"><view class="time-dot"></view><text class="lesson-time">今天 {{ group.startText }}—{{ group.endText }} 开课</text></view>
<view v-if="group.tasks.length === 0" class="task-empty"><text class="task-empty-text">暂无学习包</text></view>
<view v-for="task in group.tasks" :key="task.id" v-show="matchesFilter(task)" class="task-card">
<view class="task-heading"><text class="task-title">{{ task.title }}</text><text class="task-status" :class="taskStatusClass(task)">{{ task.statusText }}</text></view>
<view class="task-result-row"><view class="task-result-dot" :class="taskResultDotClass(task)"></view><text class="task-result" :class="taskResultClass(task)">{{ task.resultText }}</text></view>
<text class="task-attempt">{{ task.attemptText }}</text>
<view class="task-divider"></view>
<text class="task-meta">{{ task.metaText }}</text>
</view>
</view>
</view>
</scroll-view>
<MainTabBar active-tab="tasks"></MainTabBar>
<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 MainTabBar from '../../components/main-tab-bar.uvue'
import UniIcons from '../../uni_modules/uni-icons-x/components/uni-icons/uni-icons.uvue'
import { apiGetBoundStudents, apiGetParentStudyPacks, apiGetStudentHomeProfile, type BoundStudentSummary, type LearningTaskItem, type ParentStudyPackStatus, type StudentLearningTasks } from '../../api/student'
import { homeStudentProfile, selectedStudentChangeVersion, setHomeStudentProfile, switchBoundStudent } from '../../store/home'
const filters = [{ id: 'all', name: '全部' }, { id: 'pending', name: '待完成' }, { id: 'completed', name: '已完成' }, { id: 'failed', name: '未通过' }]
const activeFilter = ref('all')
const activeStatus = computed((): ParentStudyPackStatus => activeFilter.value === 'pending' ? 1 : activeFilter.value === 'completed' ? 2 : activeFilter.value === 'failed' ? 3 : 0)
const isLoading = ref(true)
const isRefreshing = ref(false)
const isStudentsLoading = ref(false)
const showStudentPicker = ref(false)
const boundStudents = ref<BoundStudentSummary[]>([])
const selectedStudentId = ref('')
const observedStudentVersion = ref(selectedStudentChangeVersion.value)
const tasks = ref<StudentLearningTasks>({ studentId: '', studentName: '', studentGrade: '', studentClass: '', totalCount: 0, pendingCount: 0, completedCount: 0, failedCount: 0, groups: [] })
const visibleGroups = computed(() => tasks.value.groups.filter((group) => group.tasks.length === 0 || group.tasks.some((task) => matchesFilter(task))))
const headerStudentName = computed((): string => homeStudentProfile.value?.name ?? tasks.value.studentName ?? '孩子')
onLoad(() => { loadTasks() })
onShow(() => {
if (observedStudentVersion.value !== selectedStudentChangeVersion.value) {
observedStudentVersion.value = selectedStudentChangeVersion.value
loadTasks(false)
}
})
async function loadTasks(showLoading: boolean = true) {
isLoading.value = showLoading
let studentId = homeStudentProfile.value?.id ?? ''
if (!studentId) {
const profileRes = await apiGetStudentHomeProfile(undefined, false)
if (profileRes && profileRes.data) {
setHomeStudentProfile(profileRes.data)
studentId = profileRes.data.id
}
}
if (studentId) {
const res = await apiGetParentStudyPacks(studentId, activeStatus.value, showLoading)
if (res && res.data) tasks.value = res.data
}
isLoading.value = false
}
async function refreshTasks() {
isRefreshing.value = true
await loadTasks(false)
isRefreshing.value = false
}
async function setFilter(filter: string) {
if (activeFilter.value === filter) return
activeFilter.value = filter
await loadTasks(true)
}
function matchesFilter(task: LearningTaskItem): boolean {
if (activeFilter.value === 'all') return true
if (activeFilter.value === 'pending') return task.status === 'not_started' || task.status === 'in_progress'
if (activeFilter.value === 'completed') return task.status === 'success'
return task.status === 'failed'
}
function taskStatusClass(task: LearningTaskItem): string {
return `task-status-${task.status}`
}
function taskResultClass(task: LearningTaskItem): string {
return `task-result-${task.status}`
}
function taskResultDotClass(task: LearningTaskItem): string {
return `task-result-dot-${task.status}`
}
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 loadTasks(true)
}
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: #f5f7fb; }
.header-panel { display: flex; flex-direction: row; align-items: center; justify-content: space-between; padding: 4px 8px 10px; background-color: #ffffff; }
.header-copy { display: flex; flex-direction: column; }
.header-title { font-size: 17px; font-weight: 800; color: #101827; }
.header-subtitle { margin-top: 5px; font-size: 11px; color: #7e8da3; }
.header-action { display: flex; flex-direction: column; align-items: center; justify-content: center; width: 36px; height: 36px; border-radius: 18px; background-color: #edf1ff; }
.header-action-icon { font-size: 18px; color: #5947ea; }
.filter-panel { display: flex; flex-direction: row; padding: 10px 8px 12px; background-color: #ffffff; }
.filter-tab { display: flex; flex-direction: column; align-items: center; justify-content: center; flex: 1; height: 28px; margin-right: 8px; border-radius: 16px; background-color: #eef2f7; }
.filter-tab-active { background-color: #5a45e8; }
.filter-text { font-size: 11px; color: #4e5b70; }
.filter-text-active { color: #ffffff; }
.content-scroll { flex: 1; }
.content-panel { display: flex; flex-direction: column; padding: 14px 8px 24px; }
.status-panel { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 60px 0; }
.empty-panel { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 60px 0; }
.status-text { font-size: 13px; color: #8992a5; }
.empty-text { font-size: 13px; color: #8992a5; }
.group-card { display: flex; flex-direction: column; margin-bottom: 12px; padding: 16px 14px 14px; border-radius: 14px; background-color: #ffffff; border: 1px solid #edf0f5; }
.group-heading { display: flex; flex-direction: row; align-items: center; justify-content: space-between; }
.group-title-row { display: flex; flex-direction: row; align-items: center; flex: 1; min-width: 0; }
.subject-tag { padding: 3px 7px; margin-right: 6px; border-radius: 9px; font-size: 10px; font-weight: 700; color: #5947ea; background-color: #eeefff; }
.unit-tag { padding: 3px 7px; margin-right: 8px; border-radius: 9px; font-size: 10px; font-weight: 700; color: #2f6bea; background-color: #edf5ff; }
.lesson-title { flex: 1; font-size: 14px; font-weight: 800; color: #1f2937; }
.knowledge-count { flex-shrink: 0; font-size: 11px; color: #8a99ae; }
.lesson-time-row { display: flex; flex-direction: row; align-items: center; margin-top: 8px; }
.time-dot { width: 8px; height: 8px; margin-right: 6px; border-radius: 4px; background-color: #9caec4; }
.lesson-time { font-size: 11px; color: #71849d; }
.task-empty { display: flex; flex-direction: column; align-items: center; padding: 18px 0; }
.task-empty-text { font-size: 12px; color: #8992a5; }
.task-card { display: flex; flex-direction: column; margin-top: 13px; padding: 12px; border-radius: 12px; background-color: #f8fafc; border: 1px solid #edf1f6; }
.task-heading { display: flex; flex-direction: row; align-items: center; justify-content: space-between; }
.task-title { flex: 1; font-size: 14px; font-weight: 700; color: #1f2937; }
.task-status { flex-shrink: 0; padding: 4px 8px; border-radius: 11px; font-size: 10px; }
.task-status-not_started { color: #516175; background-color: #edf2f7; }
.task-status-in_progress { color: #5947ea; background-color: #eeefff; }
.task-status-success { color: #04a86b; background-color: #e8fbf2; }
.task-status-failed { color: #ef4d62; background-color: #fff0f3; }
.task-result-row { display: flex; flex-direction: row; align-items: center; margin-top: 9px; }
.task-result-dot { width: 9px; height: 9px; margin-right: 7px; border-radius: 5px; }
.task-result-dot-not_started { background-color: #b6c2d0; }
.task-result-dot-in_progress { background-color: #5947ea; }
.task-result-dot-success { background-color: #04a86b; }
.task-result-dot-failed { background-color: #ef4d62; }
.task-result { font-size: 12px; font-weight: 700; }
.task-result-not_started { color: #64748b; }
.task-result-in_progress { color: #5947ea; }
.task-result-success { color: #04a86b; }
.task-result-failed { color: #ef4d62; }
.task-attempt { margin-top: 6px; font-size: 11px; color: #71849d; }
.task-divider { height: 1px; margin-top: 11px; background-color: #e3e9f1; }
.task-meta { margin-top: 9px; font-size: 11px; color: #71849d; }
.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>