xlx_teacher_app/src/pages/index/index.vue

633 lines
20 KiB
Vue
Raw Normal View History

2025-08-14 21:04:04 +08:00
<!-- 使用 type="home" 属性设置首页其他页面不需要设置默认为page -->
<route lang="jsonc" type="home">
{
"layout": "tabbar",
"style": {
// 'custom' 表示开启自定义导航栏,默认 'default'
"navigationStyle": "custom",
"navigationBarTitleText": "首页"
}
}
</route>
<script lang="ts" setup>
2025-08-30 12:29:31 +08:00
import { computed, onMounted, ref } from 'vue'
import { teacherAnalysisRecentExamStatsUsingPost } from '@/service/laoshichengjifenxi'
import { useHomeStore } from '@/store/home'
2025-08-16 16:42:40 +08:00
2025-08-14 21:04:04 +08:00
defineOptions({
name: 'Home',
})
// 获取屏幕边界到安全区域距离
let safeAreaInsets
let systemInfo
// #ifdef MP-WEIXIN
// 微信小程序使用新的API
systemInfo = uni.getWindowInfo()
safeAreaInsets = systemInfo.safeArea
? {
top: systemInfo.safeArea.top,
right: systemInfo.windowWidth - systemInfo.safeArea.right,
bottom: systemInfo.windowHeight - systemInfo.safeArea.bottom,
left: systemInfo.safeArea.left,
}
: null
// #endif
// #ifndef MP-WEIXIN
// 其他平台继续使用uni API
systemInfo = uni.getSystemInfoSync()
safeAreaInsets = systemInfo.safeAreaInsets
// #endif
2025-08-30 12:29:31 +08:00
// 使用store
const homeStore = useHomeStore()
// 分数线设置
const scoreSettings = ref({
excellent_scoring_rate: 85,
good_scoring_rate: 75,
pass_scoring_rate: 60,
top_n: 50,
})
// 统计数据(直接存储展示用的数据)
const statsData = ref({
classCount: 0,
totalCount: 0,
ranking: 0,
totalRanking: 0,
top50Count: 0,
top50Total: scoreSettings.value.top_n,
highestScore: 0,
lowestScore: 0,
excellentRate: 0,
goodRate: 0,
passRate: 0,
})
const loading = ref(false)
// 设置弹窗状态
const showSettingsDialog = ref(false)
const tempSettings = ref({
excellent_scoring_rate: scoreSettings.value.excellent_scoring_rate,
good_scoring_rate: scoreSettings.value.good_scoring_rate,
pass_scoring_rate: scoreSettings.value.pass_scoring_rate,
top_n: scoreSettings.value.top_n,
})
// TODO: 这些用户信息应该从用户store中获取
2025-08-16 16:42:40 +08:00
const schoolName = ref('华师一附中')
const teacherName = ref('张信哲')
const teacherSubject = ref('语文')
2025-08-30 12:29:31 +08:00
// 计算当前选中的班级名称
const selectedClassName = computed(() => {
if (!homeStore.selectedClassId) {
return '请选择班级'
}
const selectedClass = homeStore.classOptions.find(item => item.value === homeStore.selectedClassId)
return selectedClass?.label || '请选择班级'
})
2025-08-16 16:42:40 +08:00
// 功能菜单数据
const menuItems = [
{
id: 'grade',
name: '成绩',
icon: 'i-mingcute:file-certificate-line',
color: 'text-orange-500',
bg: 'bg-orange-50',
},
{
id: 'marking',
name: '阅卷',
icon: 'i-mingcute:edit-line',
color: 'text-red-500',
bg: 'bg-red-50',
},
{
id: 'analysis',
name: '试卷讲评',
icon: 'i-mingcute:clipboard-line',
color: 'text-green-500',
bg: 'bg-green-50',
},
{
id: 'class-analysis',
name: '班级分析',
icon: 'i-carbon:chart-bar-stacked',
color: 'text-blue-500',
bg: 'bg-blue-50',
},
]
2025-08-30 12:29:31 +08:00
// 获取统计数据
async function fetchExamStats() {
if (!homeStore.selectedClassId || !homeStore.selectedExamId) {
return
}
try {
loading.value = true
const response = await teacherAnalysisRecentExamStatsUsingPost({
body: {
class_key: homeStore.selectedClassId,
exam_id: homeStore.selectedExamId,
grade_key: homeStore.selectedClassId, // 暂时使用class_key实际可能需要单独的grade_key
excellent_scoring_rate: scoreSettings.value.excellent_scoring_rate,
good_scoring_rate: scoreSettings.value.good_scoring_rate,
pass_scoring_rate: scoreSettings.value.pass_scoring_rate,
top_n: scoreSettings.value.top_n,
},
})
if (response) {
// 直接赋值展示数据,移除中间层
statsData.value = {
classCount: response.class_total_count || 0,
totalCount: response.grade_total_count || 0,
ranking: response.average_score_rank || 0,
totalRanking: response.class_count || 0,
top50Count: response.grade_top_n || 0,
top50Total: scoreSettings.value.top_n,
highestScore: response.class_highest_score || 0,
lowestScore: response.class_lowest_score || 0,
excellentRate: Math.round((response.excellent_class_scoring_rate || 0) * 100),
goodRate: Math.round((response.good_class_scoring_rate || 0) * 100),
passRate: Math.round((response.pass_class_scoring_rate || 0) * 100),
}
}
}
catch (error) {
console.error('获取统计数据失败:', error)
uni.showToast({
title: '获取数据失败',
icon: 'none',
})
}
finally {
loading.value = false
}
2025-08-16 16:42:40 +08:00
}
// 菜单点击处理
function handleMenuClick(item: any) {
if (item.id === 'grade') {
uni.navigateTo({
2025-08-30 12:29:31 +08:00
url: '/pages/index/score',
2025-08-16 16:42:40 +08:00
})
}
else if (item.id === 'marking') {
uni.navigateTo({
url: '/pages/marking/index',
})
}
else if (item.id === 'analysis') {
uni.navigateTo({
url: '/pages-sub/analysis/index',
})
}
else if (item.id === 'class-analysis') {
uni.navigateTo({
2025-08-30 12:29:31 +08:00
url: '/pages/class-analysis/index',
2025-08-16 16:42:40 +08:00
})
}
}
// 考试选择处理
function handleExamChange() {
2025-08-30 12:29:31 +08:00
const examNames = homeStore.examOptions.map(item => item.label)
if (examNames.length === 0) {
uni.showToast({
title: '暂无考试数据',
icon: 'none',
})
return
}
2025-08-16 16:42:40 +08:00
uni.showActionSheet({
2025-08-30 12:29:31 +08:00
itemList: examNames,
2025-08-16 16:42:40 +08:00
success: (res) => {
2025-08-30 12:29:31 +08:00
const selectedOption = homeStore.examOptions[res.tapIndex]
homeStore.selectedExamId = selectedOption.value as number
// 选择考试后重新获取统计数据
fetchExamStats()
2025-08-16 16:42:40 +08:00
},
})
}
2025-08-30 12:29:31 +08:00
// 班级选择相关
const showClassPicker = ref(false)
// 班级选择处理
function handleClassChange() {
if (homeStore.classOptions.length === 0) {
uni.showToast({
title: '暂无班级数据',
icon: 'none',
})
return
}
showClassPicker.value = true
}
// 班级选择确认
function onClassSelectAction(action: { name: string, value: number }) {
homeStore.onClassChange(action.value)
// 选择班级后重新获取统计数据
fetchExamStats()
// 关闭弹窗
showClassPicker.value = false
}
2025-08-16 16:42:40 +08:00
// 设置按钮处理
function handleSettings() {
2025-08-30 12:29:31 +08:00
// 同步当前设置到临时设置
tempSettings.value = { ...scoreSettings.value }
showSettingsDialog.value = true
}
// 确认设置
function confirmSettings() {
// 验证输入
const validations = [
{
condition: tempSettings.value.excellent_scoring_rate <= 0 || tempSettings.value.excellent_scoring_rate > 100,
message: '优秀分数线应在1-100之间',
2025-08-16 16:42:40 +08:00
},
2025-08-30 12:29:31 +08:00
{
condition: tempSettings.value.good_scoring_rate <= 0 || tempSettings.value.good_scoring_rate > 100,
message: '良好分数线应在1-100之间',
},
{
condition: tempSettings.value.pass_scoring_rate <= 0 || tempSettings.value.pass_scoring_rate > 100,
message: '及格分数线应在1-100之间',
},
{
condition: tempSettings.value.top_n <= 0 || tempSettings.value.top_n > 1000,
message: '前N名应在1-1000之间',
},
]
const failedValidation = validations.find(v => v.condition)
if (failedValidation) {
uni.showToast({
title: failedValidation.message,
icon: 'none',
})
return
}
// 应用设置
scoreSettings.value = { ...tempSettings.value }
showSettingsDialog.value = false
// 重新获取数据
fetchExamStats()
uni.showToast({
title: '设置已保存',
icon: 'success',
2025-08-16 16:42:40 +08:00
})
}
2025-08-30 12:29:31 +08:00
// 取消设置
function cancelSettings() {
showSettingsDialog.value = false
}
2025-08-16 16:42:40 +08:00
// 查看详情
function handleViewDetail() {
2025-08-30 12:29:31 +08:00
if (!homeStore.selectedClassId || !homeStore.selectedExamId) {
uni.showToast({
title: '请先选择班级和考试',
icon: 'none',
})
return
}
// TODO: 导航到详情页,传递当前选择的参数
uni.navigateTo({
url: `/pages-sub/detail/index?classId=${homeStore.selectedClassId}&examId=${homeStore.selectedExamId}&subjectId=${homeStore.selectedSubjectId || ''}`,
2025-08-16 16:42:40 +08:00
})
}
// 试卷讲评
function handleExamReview() {
2025-08-30 12:29:31 +08:00
if (!homeStore.selectedClassId || !homeStore.selectedExamId) {
uni.showToast({
title: '请先选择班级和考试',
icon: 'none',
})
return
}
// TODO: 导航到试卷讲评页,传递当前选择的参数
uni.navigateTo({
url: `/pages-sub/exam-review/index?classId=${homeStore.selectedClassId}&examId=${homeStore.selectedExamId}&subjectId=${homeStore.selectedSubjectId || ''}`,
2025-08-16 16:42:40 +08:00
})
}
2025-08-30 12:29:31 +08:00
// 计算当前选中的考试名称
const selectedExamName = computed(() => {
if (!homeStore.selectedExamId) {
return '请选择考试'
}
const exam = homeStore.examOptions.find(item => item.value === homeStore.selectedExamId)
return exam?.label || '请选择考试'
})
// 页面初始化
onMounted(async () => {
try {
// 获取选项数据
await homeStore.fetchOptions()
// 如果有默认选择,获取统计数据
if (homeStore.examOptions.length > 0 && homeStore.classOptions.length > 0) {
// 设置默认选择
if (!homeStore.selectedExamId) {
homeStore.selectedExamId = homeStore.examOptions[0]?.value as number
}
if (!homeStore.selectedClassId) {
homeStore.selectedClassId = homeStore.classOptions[0]?.value as number
}
if (!homeStore.selectedSubjectId && homeStore.subjectOptions.length > 0) {
homeStore.selectedSubjectId = homeStore.subjectOptions[0]?.value as number
}
// 获取统计数据
await fetchExamStats()
}
}
catch (error) {
console.error('初始化数据失败:', error)
}
})
2025-08-14 21:04:04 +08:00
</script>
<template>
2025-08-16 16:42:40 +08:00
<view class="bg-slate-50">
<div class="absolute left-0 right-0 top-0 z-0 h-200px rounded-b-2xl from-cyan-400 to-blue-500 bg-gradient-to-br" />
<!-- 顶部区域 -->
<view class="relative px-4 pb-4 pt-2" :style="{ paddingTop: `${(safeAreaInsets?.top || 0) + 8}px` }">
<!-- 学校名称居中 -->
<view class="mb-3 text-center">
<view class="i-mingcute:school-line mx-auto mb-1 text-xl text-white" />
<text class="text-base text-white font-medium">{{ schoolName }}</text>
</view>
2025-08-14 21:04:04 +08:00
2025-08-16 16:42:40 +08:00
<!-- 用户信息左右布局 -->
<view class="flex items-center justify-between">
<!-- 左侧用户信息 -->
<view class="flex items-center">
<view class="i-mingcute:user-line mr-2 text-lg text-white" />
<text class="text-base text-white font-medium">{{ teacherName }}{{ teacherSubject }}</text>
</view>
2025-08-14 21:04:04 +08:00
2025-08-30 12:29:31 +08:00
<!-- 右侧班级选择 -->
<view class="rounded-full bg-white/20 px-3 py-1 active:bg-white/30" @tap="handleClassChange">
<view class="i-mingcute:school-line mr-1 inline-block text-sm text-white" />
<text class="text-xs text-white">{{ selectedClassName }}</text>
<view class="i-mingcute:down-line ml-1 inline-block text-xs text-white" />
2025-08-16 16:42:40 +08:00
</view>
</view>
2025-08-14 21:04:04 +08:00
</view>
2025-08-16 16:42:40 +08:00
<!-- 主要内容区域 -->
<view class="relative z-10 mx-2 min-h-[calc(100vh-200px)] rounded-t-3xl bg-white px-2 pt-4">
<!-- 功能菜单网格 -->
<view class="mb-4 border border-slate-100 bg-gray-50 bg-gradient-to-r px-4 shadow-sm">
<view class="grid grid-cols-4 gap-3">
<view
v-for="item in menuItems"
:key="item.id"
class="flex flex-col items-center rounded-xl py-2 transition-all active:scale-95"
@tap="handleMenuClick(item)"
>
<view class="mb-1 h-10 w-10 flex items-center justify-center rounded-xl" :class="item.bg">
<view class="text-lg" :class="[item.icon, item.color]" />
</view>
<text class="text-xs text-gray-700 font-medium">{{ item.name }}</text>
</view>
</view>
</view>
<!-- 整体概况区域 -->
<view class="mb-4">
<view class="mb-3 flex items-center justify-between">
<text class="text-lg text-slate-800 font-semibold">整体概况</text>
<view class="i-mingcute:filter-line text-lg text-cyan-500 hover:text-cyan-600" @tap="handleSettings" />
</view>
<!-- 考试选择 -->
<view class="flex items-center justify-between border border-slate-200 rounded-xl from-slate-50 to-cyan-50 bg-gradient-to-r p-3" @tap="handleExamChange">
2025-08-30 12:29:31 +08:00
<text class="text-sm text-slate-700 font-medium">{{ selectedExamName }}</text>
2025-08-16 16:42:40 +08:00
<view class="i-mingcute:down-line text-slate-400" />
</view>
</view>
<!-- 统计数据卡片 -->
<view class="grid grid-cols-2 mb-4 gap-3">
<!-- 班级/年级人数 -->
<view class="border border-blue-100 rounded-xl from-white to-blue-50 bg-gradient-to-br p-4 shadow-blue-50 shadow-sm">
<view class="mb-1 flex items-center">
<view class="i-mingcute:group-line mr-1 text-base text-blue-500" />
<text class="text-xs text-slate-600">班级/年级人数</text>
</view>
<text class="text-lg text-slate-800 font-bold">{{ statsData.classCount }}/{{ statsData.totalCount }}</text>
</view>
<!-- 均分排名 -->
<view class="border border-blue-100 rounded-xl from-white to-blue-50 bg-gradient-to-br p-4 shadow-blue-50 shadow-sm">
<view class="mb-1 flex items-center">
<view class="i-mingcute:chart-bar-line mr-1 text-base text-blue-600" />
<text class="text-xs text-slate-600">均分排名</text>
</view>
<text class="text-lg text-slate-800 font-bold">{{ statsData.ranking }}/{{ statsData.totalRanking }}</text>
</view>
<!-- 年级前50名 -->
<view class="border border-cyan-100 rounded-xl from-white to-cyan-50 bg-gradient-to-br p-4 shadow-cyan-50 shadow-sm">
<view class="mb-1 flex items-center">
<view class="i-mingcute:trophy-line mr-1 text-base text-cyan-500" />
<text class="text-xs text-slate-600">年级前50名</text>
</view>
<text class="text-lg text-slate-800 font-bold">{{ statsData.top50Count }}/{{ statsData.top50Total }}</text>
</view>
<!-- 最高分/最低分 -->
<view class="border border-cyan-100 rounded-xl from-white to-cyan-50 bg-gradient-to-br p-4 shadow-cyan-50 shadow-sm">
<view class="mb-1 flex items-center">
<view class="i-carbon:chart-line mr-1 text-base text-cyan-600" />
<text class="text-xs text-slate-600">最高分/最低分</text>
</view>
<text class="text-lg text-slate-800 font-bold">{{ statsData.highestScore }}/{{ statsData.lowestScore }}</text>
</view>
<!-- 优秀率 -->
<view class="border border-sky-100 rounded-xl from-white to-sky-50 bg-gradient-to-br p-4 shadow-sky-50 shadow-sm">
<view class="mb-1 flex items-center">
<view class="i-mingcute:thumb-up-line mr-1 text-base text-sky-500" />
<text class="text-xs text-slate-600">优秀率</text>
</view>
<text class="text-lg text-slate-800 font-bold">{{ statsData.excellentRate }}%</text>
</view>
<!-- 良好率 -->
<view class="border border-sky-100 rounded-xl from-white to-sky-50 bg-gradient-to-br p-4 shadow-sky-50 shadow-sm">
<view class="mb-1 flex items-center">
<view class="i-mingcute:check-circle-line mr-1 text-base text-sky-600" />
<text class="text-xs text-slate-600">良好率</text>
</view>
<text class="text-lg text-slate-800 font-bold">{{ statsData.goodRate }}%</text>
</view>
</view>
<!-- 及格率 -->
<view class="mb-4 border border-blue-100 rounded-xl from-white to-blue-50 bg-gradient-to-br p-4 shadow-blue-50 shadow-sm">
<view class="mb-1 flex items-center">
<view class="i-carbon:checkmark mr-1 text-base text-blue-500" />
<text class="text-xs text-slate-600">及格率</text>
</view>
<text class="text-lg text-slate-800 font-bold">{{ statsData.passRate }}%</text>
</view>
<!-- 底部操作按钮 -->
<view class="flex gap-3 pb-4">
<wd-button
type="info"
size="medium"
class="flex-1 shadow-sm"
@click="handleViewDetail"
>
查看详情
</wd-button>
<wd-button
type="primary"
size="medium"
class="flex-1 shadow-sm"
@click="handleExamReview"
>
试卷讲评
</wd-button>
</view>
2025-08-14 21:04:04 +08:00
</view>
2025-08-30 12:29:31 +08:00
<!-- 参数设置弹窗 -->
<wd-popup
v-model="showSettingsDialog"
position="center"
custom-style="border-radius: 16px; width: 80%; max-width: 400px;"
:close-on-click-modal="false"
>
<view class="p-6">
<!-- 标题 -->
<view class="mb-6 text-center">
<text class="text-lg text-gray-800 font-semibold">参数设置</text>
</view>
<!-- 表单内容 -->
<view class="space-y-4">
<!-- 优秀得分率 -->
<view class="flex items-center justify-center gap-4">
<text class="text-sm text-gray-700 font-medium">优秀得分率</text>
<text class="text-sm text-gray-500"></text>
<wd-input
v-model="tempSettings.excellent_scoring_rate"
type="number"
placeholder="请输入"
class="w-20 text-center"
:border="true"
/>
<text class="text-sm text-gray-700">%</text>
</view>
<!-- 良好得分率 -->
<view class="flex items-center justify-center gap-4">
<text class="text-sm text-gray-700 font-medium">良好得分率</text>
<text class="text-sm text-gray-500"></text>
<wd-input
v-model="tempSettings.good_scoring_rate"
type="number"
placeholder="请输入"
class="w-20 text-center"
:border="true"
/>
<text class="text-sm text-gray-700">%</text>
</view>
<!-- 及格得分率 -->
<view class="flex items-center justify-center gap-4">
<text class="text-sm text-gray-700 font-medium">及格得分率</text>
<text class="text-sm text-gray-500"></text>
<wd-input
v-model="tempSettings.pass_scoring_rate"
type="number"
placeholder="请输入"
class="w-20 text-center"
:border="true"
/>
<text class="text-sm text-gray-700">%</text>
</view>
<!-- 年级前N名 -->
<view class="flex items-center justify-center gap-4">
<text class="text-sm text-gray-700 font-medium">年级前N名</text>
<text class="text-sm text-gray-500">-</text>
<wd-input
v-model="tempSettings.top_n"
type="number"
placeholder="请输入"
class="w-20 text-center"
:border="true"
/>
<text class="text-sm text-gray-700"></text>
</view>
</view>
<!-- 操作按钮 -->
<view class="mt-6 flex gap-3">
<wd-button type="info" class="flex-1" @click="cancelSettings">
取消
</wd-button>
<wd-button type="primary" class="flex-1" @click="confirmSettings">
确定
</wd-button>
</view>
</view>
</wd-popup>
<!-- 班级选择器 -->
<wd-popup v-model="showClassPicker" position="bottom">
<view class="rounded-t-3xl bg-white p-4">
<view class="mb-4 text-center text-lg font-medium">
选择班级
</view>
<view class="max-h-80 overflow-y-auto space-y-2">
<view
v-for="classItem in homeStore.classOptions"
:key="classItem.value"
class="flex items-center justify-between rounded-xl bg-gray-50 p-3 active:bg-blue-50"
@tap="onClassSelectAction({ name: classItem.label, value: classItem.value })"
>
<text class="text-base">{{ classItem.label }}</text>
<view
v-if="homeStore.selectedClassId === classItem.value"
class="i-mingcute:check-line text-lg text-blue-500"
/>
</view>
</view>
<view class="mt-4 flex justify-center">
<wd-button type="default" class="w-32" @click="showClassPicker = false">
取消
</wd-button>
</view>
</view>
</wd-popup>
2025-08-14 21:04:04 +08:00
</view>
</template>