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-10-07 14:52:01 +08:00
|
|
|
|
import { computed, onMounted, ref, watch } from 'vue'
|
2025-08-30 12:29:31 +08:00
|
|
|
|
import { teacherAnalysisRecentExamStatsUsingPost } from '@/service/laoshichengjifenxi'
|
|
|
|
|
|
import { useHomeStore } from '@/store/home'
|
2025-09-24 00:27:18 +08:00
|
|
|
|
import { useUserStore } from '@/store/user'
|
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()
|
2025-09-24 00:27:18 +08:00
|
|
|
|
const userStore = useUserStore()
|
2025-08-30 12:29:31 +08:00
|
|
|
|
|
|
|
|
|
|
// 分数线设置
|
|
|
|
|
|
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,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2025-09-24 00:27:18 +08:00
|
|
|
|
// 从用户store中获取教师信息
|
|
|
|
|
|
const schoolName = computed(() => {
|
|
|
|
|
|
const userInfo = userStore.getUserInfo
|
|
|
|
|
|
return userInfo.schools?.[0]?.name || '学校'
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const teacherName = computed(() => {
|
|
|
|
|
|
const userInfo = userStore.getUserInfo
|
|
|
|
|
|
return userInfo.nickname || '教师'
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const teacherSubject = computed(() => {
|
|
|
|
|
|
const userInfo = userStore.getUserInfo
|
|
|
|
|
|
// 从角色信息中获取学科,假设角色名包含学科信息
|
|
|
|
|
|
const roleName = userInfo.roles?.[0]?.name
|
|
|
|
|
|
if (roleName) {
|
|
|
|
|
|
// 提取学科信息,例如"语文教师"中的"语文"
|
|
|
|
|
|
const subjectMatch = roleName.match(/^(.+?)教师$/)
|
|
|
|
|
|
return subjectMatch ? subjectMatch[1] : '学科'
|
|
|
|
|
|
}
|
|
|
|
|
|
return '学科'
|
|
|
|
|
|
})
|
2025-08-16 16:42:40 +08:00
|
|
|
|
|
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-11-18 21:38:44 +08:00
|
|
|
|
const selectedOption = homeStore.examOptions[res.tapIndex]
|
2025-08-30 12:29:31 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 班级选择确认
|
2025-10-07 14:52:01 +08:00
|
|
|
|
watch(() => homeStore.selectedClassId, () => {
|
2025-08-30 12:29:31 +08:00
|
|
|
|
// 选择班级后重新获取统计数据
|
|
|
|
|
|
fetchExamStats()
|
|
|
|
|
|
// 关闭弹窗
|
|
|
|
|
|
showClassPicker.value = false
|
2025-10-07 14:52:01 +08:00
|
|
|
|
})
|
2025-08-30 12:29:31 +08:00
|
|
|
|
|
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
|
|
|
|
// TODO: 导航到详情页,传递当前选择的参数
|
|
|
|
|
|
uni.navigateTo({
|
2025-10-05 20:10:51 +08:00
|
|
|
|
url: '/pages/index/score',
|
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({
|
2025-10-07 14:52:01 +08:00
|
|
|
|
url: `/pages-sub/exam-review/index?classId=${homeStore.selectedClassId}&examId=${homeStore.selectedExamId}&subjectId=${homeStore.selectedExamSubjectId || ''}`,
|
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
|
|
|
|
|
|
}
|
2025-10-07 14:52:01 +08:00
|
|
|
|
if (!homeStore.selectedExamSubjectId && homeStore.subjectOptions.length > 0) {
|
|
|
|
|
|
homeStore.selectedExamSubjectId = homeStore.subjectOptions[0]?.value as number
|
2025-08-30 12:29:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取统计数据
|
|
|
|
|
|
await fetchExamStats()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (error) {
|
|
|
|
|
|
console.error('初始化数据失败:', error)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
2025-08-14 21:04:04 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
2025-11-18 23:00:11 +08:00
|
|
|
|
<view class="relative min-h-screen overflow-hidden bg-slate-50">
|
|
|
|
|
|
<view class="header-bg" />
|
|
|
|
|
|
|
|
|
|
|
|
<view class="relative z-10 px-4 pb-6" :style="{ paddingTop: `${(safeAreaInsets?.top || 0) + 10}px` }">
|
|
|
|
|
|
<view class="mb-5 flex flex-col items-center justify-center">
|
|
|
|
|
|
<view class="mb-2 rounded-full bg-white bg-opacity-10 p-2">
|
|
|
|
|
|
<view class="i-mingcute:school-line text-2xl text-white" />
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<text class="text-lg text-white font-bold tracking-wide">{{ schoolName }}</text>
|
2025-08-16 16:42:40 +08:00
|
|
|
|
</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">
|
2025-11-18 23:00:11 +08:00
|
|
|
|
<view class="mr-3 h-10 w-10 flex items-center justify-center rounded-full bg-white">
|
|
|
|
|
|
<view class="i-mingcute:user-3-line text-xl text-blue-500" />
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="flex flex-col">
|
|
|
|
|
|
<text class="text-base text-white font-bold">{{ teacherName }}</text>
|
|
|
|
|
|
<text class="text-xs text-blue-100 opacity-80">{{ teacherSubject }}教师</text>
|
|
|
|
|
|
</view>
|
2025-08-16 16:42:40 +08:00
|
|
|
|
</view>
|
2025-08-14 21:04:04 +08:00
|
|
|
|
|
2025-11-18 23:00:11 +08:00
|
|
|
|
<view class="glass-btn flex items-center rounded-full px-3 py-1.5" @tap="handleClassChange">
|
|
|
|
|
|
<text class="mr-1 text-xs text-white">{{ selectedClassName }}</text>
|
|
|
|
|
|
<view class="i-mingcute:down-line text-xs text-white opacity-80" />
|
2025-08-16 16:42:40 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
2025-08-14 21:04:04 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
|
2025-11-18 23:00:11 +08:00
|
|
|
|
<view class="main-content">
|
|
|
|
|
|
<view class="menu-grid-bg mb-5 rounded-xl p-4">
|
|
|
|
|
|
<view class="grid grid-cols-4 gap-4">
|
2025-08-16 16:42:40 +08:00
|
|
|
|
<view
|
|
|
|
|
|
v-for="item in menuItems"
|
|
|
|
|
|
:key="item.id"
|
2025-11-18 23:00:11 +08:00
|
|
|
|
class="hover-scale flex flex-col items-center transition-all"
|
2025-08-16 16:42:40 +08:00
|
|
|
|
@tap="handleMenuClick(item)"
|
|
|
|
|
|
>
|
2025-11-18 23:00:11 +08:00
|
|
|
|
<view class="mb-2 h-10 w-10 flex items-center justify-center rounded-full shadow-sm" :class="item.bg">
|
|
|
|
|
|
<view class="text-xl" :class="[item.icon, item.color]" />
|
2025-08-16 16:42:40 +08:00
|
|
|
|
</view>
|
2025-11-18 23:00:11 +08:00
|
|
|
|
<text class="text-xs text-slate-600 font-medium">{{ item.name }}</text>
|
2025-08-16 16:42:40 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<view class="mb-4">
|
2025-11-18 23:00:11 +08:00
|
|
|
|
<view class="mb-3 flex items-center justify-between px-1">
|
|
|
|
|
|
<view class="flex items-center">
|
|
|
|
|
|
<view class="mr-2 h-4 w-1 rounded-full bg-blue-500" />
|
|
|
|
|
|
<text class="text-base text-slate-800 font-bold">整体概况</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view
|
|
|
|
|
|
class="h-8 w-8 flex items-center justify-center rounded-full bg-slate-100 active:bg-slate-200"
|
|
|
|
|
|
@tap="handleSettings"
|
|
|
|
|
|
>
|
|
|
|
|
|
<view class="i-mingcute:settings-3-line text-lg text-slate-500" />
|
|
|
|
|
|
</view>
|
2025-08-16 16:42:40 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
|
2025-11-18 23:00:11 +08:00
|
|
|
|
<view class="filter-box-bg hover-scale flex items-center justify-between rounded-xl p-3" @tap="handleExamChange">
|
|
|
|
|
|
<view class="flex items-center gap-2">
|
|
|
|
|
|
<view class="i-mingcute:paper-line text-blue-500" />
|
|
|
|
|
|
<text class="text-sm text-slate-700 font-bold">{{ selectedExamName }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="i-mingcute:right-line text-sm text-slate-400" />
|
2025-08-16 16:42:40 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
2025-11-18 23:00:11 +08:00
|
|
|
|
<view class="grid grid-cols-2 mb-3 gap-3">
|
|
|
|
|
|
<view class="stat-card-blue flex flex-col justify-between rounded-xl p-4">
|
|
|
|
|
|
<view class="mb-2 flex items-center">
|
|
|
|
|
|
<text class="text-xs text-slate-500">参考人数(班/级)</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="flex items-end gap-1">
|
|
|
|
|
|
<text class="text-xl text-slate-800 font-bold leading-none">{{ statsData.classCount }}</text>
|
|
|
|
|
|
<text class="mb-0.5 text-xs text-slate-400">/ {{ statsData.totalCount }}</text>
|
2025-08-16 16:42:40 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
2025-11-18 23:00:11 +08:00
|
|
|
|
<view class="stat-card-blue flex flex-col justify-between rounded-xl p-4">
|
|
|
|
|
|
<view class="mb-2 flex items-center">
|
|
|
|
|
|
<text class="text-xs text-slate-500">均分排名</text>
|
|
|
|
|
|
<view class="i-mingcute:chart-bar-line ml-auto text-blue-400" />
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="flex items-end gap-1">
|
|
|
|
|
|
<text class="text-xl text-slate-800 font-bold leading-none">{{ statsData.ranking }}</text>
|
|
|
|
|
|
<text class="mb-0.5 text-xs text-slate-400">/ {{ statsData.totalRanking }}</text>
|
2025-08-16 16:42:40 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
2025-11-18 23:00:11 +08:00
|
|
|
|
<view class="stat-card-cyan flex flex-col justify-between rounded-xl p-4">
|
|
|
|
|
|
<view class="mb-2 flex items-center">
|
|
|
|
|
|
<text class="text-xs text-slate-500">年级前50名</text>
|
|
|
|
|
|
<view class="i-mingcute:trophy-line ml-auto text-cyan-400" />
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="flex items-end gap-1">
|
|
|
|
|
|
<text class="text-xl text-slate-800 font-bold leading-none">{{ statsData.top50Count }}</text>
|
|
|
|
|
|
<text class="mb-0.5 text-xs text-slate-400">人</text>
|
2025-08-16 16:42:40 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
2025-11-18 23:00:11 +08:00
|
|
|
|
<view class="stat-card-cyan flex flex-col justify-between rounded-xl p-4">
|
|
|
|
|
|
<view class="mb-2 flex items-center">
|
|
|
|
|
|
<text class="text-xs text-slate-500">最高/最低</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="flex items-end gap-2">
|
|
|
|
|
|
<text class="text-lg text-slate-800 font-bold leading-none">{{ statsData.highestScore }}</text>
|
|
|
|
|
|
<text class="text-sm text-slate-400 leading-none">/</text>
|
|
|
|
|
|
<text class="text-lg text-slate-800 font-bold leading-none">{{ statsData.lowestScore }}</text>
|
2025-08-16 16:42:40 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
2025-11-17 22:54:07 +08:00
|
|
|
|
</view>
|
2025-08-16 16:42:40 +08:00
|
|
|
|
|
2025-11-18 23:00:11 +08:00
|
|
|
|
<view class="grid grid-cols-3 mb-6 gap-2">
|
|
|
|
|
|
<view class="stat-card-sky flex flex-col items-center justify-center rounded-xl p-3">
|
|
|
|
|
|
<text class="mb-1 text-lg text-sky-600 font-bold">{{ statsData.excellentRate }}<text class="text-xs">%</text></text>
|
|
|
|
|
|
<text class="text-xs text-slate-500">优秀率</text>
|
2025-08-16 16:42:40 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
|
2025-11-18 23:00:11 +08:00
|
|
|
|
<view class="stat-card-sky flex flex-col items-center justify-center rounded-xl p-3">
|
|
|
|
|
|
<text class="mb-1 text-lg text-sky-600 font-bold">{{ statsData.goodRate }}<text class="text-xs">%</text></text>
|
|
|
|
|
|
<text class="text-xs text-slate-500">良好率</text>
|
2025-08-16 16:42:40 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
|
2025-11-18 23:00:11 +08:00
|
|
|
|
<view class="stat-card-sky flex flex-col items-center justify-center rounded-xl p-3">
|
|
|
|
|
|
<text class="mb-1 text-lg text-blue-600 font-bold">{{ statsData.passRate }}<text class="text-xs">%</text></text>
|
|
|
|
|
|
<text class="text-xs text-slate-500">及格率</text>
|
2025-08-16 16:42:40 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
2025-11-18 23:00:11 +08:00
|
|
|
|
<view class="flex gap-4 pb-4">
|
2025-08-16 16:42:40 +08:00
|
|
|
|
<wd-button
|
|
|
|
|
|
type="info"
|
2025-11-18 23:00:11 +08:00
|
|
|
|
size="large"
|
|
|
|
|
|
custom-class="flex-1 shadow-sm !rounded-xl !h-11 !text-base"
|
|
|
|
|
|
:plain="true"
|
2025-08-16 16:42:40 +08:00
|
|
|
|
@click="handleViewDetail"
|
|
|
|
|
|
>
|
|
|
|
|
|
查看详情
|
|
|
|
|
|
</wd-button>
|
|
|
|
|
|
<wd-button
|
|
|
|
|
|
type="primary"
|
2025-11-18 23:00:11 +08:00
|
|
|
|
size="large"
|
|
|
|
|
|
custom-class="flex-1 shadow-md !rounded-xl !h-11 !bg-gradient-to-r !from-blue-500 !to-blue-600 !text-base !border-none"
|
2025-08-16 16:42:40 +08:00
|
|
|
|
@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"
|
2025-11-18 23:00:11 +08:00
|
|
|
|
custom-class="popup-radius overflow-hidden"
|
|
|
|
|
|
custom-style="width: 85%; max-width: 360px;"
|
2025-08-30 12:29:31 +08:00
|
|
|
|
:close-on-click-modal="false"
|
2025-11-18 23:00:11 +08:00
|
|
|
|
:z-index="100"
|
2025-08-30 12:29:31 +08:00
|
|
|
|
>
|
2025-11-18 23:00:11 +08:00
|
|
|
|
<view class="bg-white">
|
|
|
|
|
|
<view class="border-b border-slate-100 py-4 text-center">
|
|
|
|
|
|
<text class="text-lg text-slate-800 font-bold">参数设置</text>
|
2025-08-30 12:29:31 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
|
2025-11-18 23:00:11 +08:00
|
|
|
|
<view class="p-6 space-y-5">
|
|
|
|
|
|
<view class="flex items-center justify-between">
|
|
|
|
|
|
<text class="text-sm text-slate-600">优秀得分率 (≥)</text>
|
|
|
|
|
|
<view class="flex items-center gap-2">
|
|
|
|
|
|
<wd-input v-model="tempSettings.excellent_scoring_rate" type="number" custom-class="!w-16 !text-center !bg-slate-50" :no-border="true" />
|
|
|
|
|
|
<text class="text-slate-400">%</text>
|
|
|
|
|
|
</view>
|
2025-08-30 12:29:31 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
|
2025-11-18 23:00:11 +08:00
|
|
|
|
<view class="flex items-center justify-between">
|
|
|
|
|
|
<text class="text-sm text-slate-600">良好得分率 (≥)</text>
|
|
|
|
|
|
<view class="flex items-center gap-2">
|
|
|
|
|
|
<wd-input v-model="tempSettings.good_scoring_rate" type="number" custom-class="!w-16 !text-center !bg-slate-50" :no-border="true" />
|
|
|
|
|
|
<text class="text-slate-400">%</text>
|
|
|
|
|
|
</view>
|
2025-08-30 12:29:31 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
|
2025-11-18 23:00:11 +08:00
|
|
|
|
<view class="flex items-center justify-between">
|
|
|
|
|
|
<text class="text-sm text-slate-600">及格得分率 (≥)</text>
|
|
|
|
|
|
<view class="flex items-center gap-2">
|
|
|
|
|
|
<wd-input v-model="tempSettings.pass_scoring_rate" type="number" custom-class="!w-16 !text-center !bg-slate-50" :no-border="true" />
|
|
|
|
|
|
<text class="text-slate-400">%</text>
|
|
|
|
|
|
</view>
|
2025-08-30 12:29:31 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
|
2025-11-18 23:00:11 +08:00
|
|
|
|
<view class="flex items-center justify-between">
|
|
|
|
|
|
<text class="text-sm text-slate-600">年级前N名</text>
|
|
|
|
|
|
<view class="flex items-center gap-2">
|
|
|
|
|
|
<wd-input v-model="tempSettings.top_n" type="number" custom-class="!w-16 !text-center !bg-slate-50" :no-border="true" />
|
|
|
|
|
|
<text class="text-slate-400">名</text>
|
|
|
|
|
|
</view>
|
2025-08-30 12:29:31 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
2025-11-18 23:00:11 +08:00
|
|
|
|
<view class="flex border-t border-slate-100">
|
|
|
|
|
|
<view class="flex-1 py-3.5 text-center text-slate-500 active:bg-slate-50" @click="cancelSettings">
|
2025-08-30 12:29:31 +08:00
|
|
|
|
取消
|
2025-11-18 23:00:11 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
<view class="w-px bg-slate-100" />
|
|
|
|
|
|
<view class="flex-1 py-3.5 text-center text-blue-600 font-medium active:bg-blue-50" @click="confirmSettings">
|
2025-08-30 12:29:31 +08:00
|
|
|
|
确定
|
2025-11-18 23:00:11 +08:00
|
|
|
|
</view>
|
2025-08-30 12:29:31 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</wd-popup>
|
|
|
|
|
|
|
2025-11-18 23:00:11 +08:00
|
|
|
|
<wd-popup v-model="showClassPicker" position="bottom" custom-class="rounded-t-3xl" :z-index="100">
|
|
|
|
|
|
<view class="bg-white pb-safe">
|
|
|
|
|
|
<view class="border-b border-slate-100 p-4 text-center">
|
|
|
|
|
|
<text class="text-lg text-slate-800 font-bold">切换班级</text>
|
2025-08-30 12:29:31 +08:00
|
|
|
|
</view>
|
2025-11-18 23:00:11 +08:00
|
|
|
|
<scroll-view scroll-y class="box-border max-h-80 p-4">
|
|
|
|
|
|
<view class="space-y-2">
|
2025-08-30 12:29:31 +08:00
|
|
|
|
<view
|
2025-11-18 23:00:11 +08:00
|
|
|
|
v-for="classItem in homeStore.classOptions"
|
|
|
|
|
|
:key="classItem.value"
|
|
|
|
|
|
class="flex items-center justify-between rounded-xl p-4 transition-colors"
|
|
|
|
|
|
:class="homeStore.selectedClassId === classItem.value ? 'bg-blue-50 border border-blue-100' : 'bg-slate-50'"
|
|
|
|
|
|
@tap="homeStore.selectedClassId = classItem.value; homeStore.selectedGradeKey = classItem.gradeKey; showClassPicker = false"
|
|
|
|
|
|
>
|
|
|
|
|
|
<text class="text-base" :class="homeStore.selectedClassId === classItem.value ? 'text-blue-600 font-bold' : 'text-slate-700'">{{ classItem.label }}</text>
|
|
|
|
|
|
<view
|
|
|
|
|
|
v-if="homeStore.selectedClassId === classItem.value"
|
|
|
|
|
|
class="i-mingcute:check-circle-fill text-xl text-blue-500"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</view>
|
2025-08-30 12:29:31 +08:00
|
|
|
|
</view>
|
2025-11-18 23:00:11 +08:00
|
|
|
|
</scroll-view>
|
|
|
|
|
|
<view class="p-4">
|
|
|
|
|
|
<wd-button block type="default" size="large" custom-class="!rounded-xl !bg-slate-100 !text-slate-600 !border-none" @click="showClassPicker = false">
|
|
|
|
|
|
关闭
|
2025-08-30 12:29:31 +08:00
|
|
|
|
</wd-button>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</wd-popup>
|
2025-08-14 21:04:04 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</template>
|
2025-11-18 23:00:11 +08:00
|
|
|
|
|
|
|
|
|
|
<style lang=scss>
|
|
|
|
|
|
/* 头部主背景:深邃蓝青渐变 + 装饰 */
|
|
|
|
|
|
.header-bg {
|
|
|
|
|
|
background: linear-gradient(135deg, #06b6d4 0%, #3b82f6 100%);
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 520rpx; /* 稍微加高一点,为了弧度好看 */
|
|
|
|
|
|
z-index: 0;
|
|
|
|
|
|
border-bottom-left-radius: 40rpx;
|
|
|
|
|
|
border-bottom-right-radius: 40rpx;
|
|
|
|
|
|
|
|
|
|
|
|
/* 增加一点微弱的装饰圆圈,让头部不单调 */
|
|
|
|
|
|
&::after {
|
|
|
|
|
|
content: '';
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: -50rpx;
|
|
|
|
|
|
right: -50rpx;
|
|
|
|
|
|
width: 300rpx;
|
|
|
|
|
|
height: 300rpx;
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.1);
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
filter: blur(40rpx);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 玻璃拟态按钮背景 */
|
|
|
|
|
|
.glass-btn {
|
|
|
|
|
|
background-color: rgba(255, 255, 255, 0.2);
|
|
|
|
|
|
backdrop-filter: blur(4px); /* App端生效,小程序可能不生效但有颜色兜底 */
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
|
|
|
|
|
|
&:active {
|
|
|
|
|
|
background-color: rgba(255, 255, 255, 0.3);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 菜单区域背景 */
|
|
|
|
|
|
.menu-grid-bg {
|
|
|
|
|
|
background: linear-gradient(to right, #f8fafc, #f0f9ff);
|
|
|
|
|
|
border: 1px solid #e2e8f0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 筛选框背景 */
|
|
|
|
|
|
.filter-box-bg {
|
|
|
|
|
|
background: linear-gradient(to right, #f8fafc, #ecfeff);
|
|
|
|
|
|
border: 1px solid #e2e8f0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 数据卡片 - 蓝色系 */
|
|
|
|
|
|
.stat-card-blue {
|
|
|
|
|
|
background: linear-gradient(145deg, #ffffff 0%, #eff6ff 100%);
|
|
|
|
|
|
border: 1px solid #dbeafe;
|
|
|
|
|
|
box-shadow: 0 4rpx 12rpx rgba(59, 130, 246, 0.08);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 数据卡片 - 青色系 */
|
|
|
|
|
|
.stat-card-cyan {
|
|
|
|
|
|
background: linear-gradient(145deg, #ffffff 0%, #ecfeff 100%);
|
|
|
|
|
|
border: 1px solid #cffafe;
|
|
|
|
|
|
box-shadow: 0 4rpx 12rpx rgba(6, 182, 212, 0.08);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 数据卡片 - 天空蓝系 */
|
|
|
|
|
|
.stat-card-sky {
|
|
|
|
|
|
background: linear-gradient(145deg, #ffffff 0%, #f0f9ff 100%);
|
|
|
|
|
|
border: 1px solid #e0f2fe;
|
|
|
|
|
|
box-shadow: 0 4rpx 12rpx rgba(14, 165, 233, 0.08);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 主内容区容器,处理高度 */
|
|
|
|
|
|
.main-content {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
z-index: 10;
|
|
|
|
|
|
margin: 0 24rpx;
|
|
|
|
|
|
background-color: #ffffff;
|
|
|
|
|
|
border-radius: 24rpx;
|
|
|
|
|
|
padding: 24rpx;
|
|
|
|
|
|
/* 替代 min-h-[calc] */
|
|
|
|
|
|
min-height: calc(100vh - 300rpx);
|
|
|
|
|
|
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.03);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 统一卡片按压效果 */
|
|
|
|
|
|
.hover-scale {
|
|
|
|
|
|
&:active {
|
|
|
|
|
|
transform: scale(0.98);
|
|
|
|
|
|
opacity: 0.9;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 弹窗样式修正 */
|
|
|
|
|
|
.popup-radius {
|
|
|
|
|
|
border-radius: 32rpx !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|