fix: 增加缓存和修复查看成绩部分问题
continuous-integration/drone/push Build is passing Details

This commit is contained in:
张哲铜 2025-10-08 23:57:29 +08:00
parent fee76ec061
commit 115d118d42
3 changed files with 361 additions and 405 deletions

View File

@ -0,0 +1,79 @@
import { useUserStore } from '@/store/user'
import { watch } from 'vue'
/**
* ID的响应式存储
*
*
* @param key -
* @param defaultValue -
* @returns
*
* @example
* const selectedExamId = useUserStorage('selectedExamId', 0);
* // 使用方式和普通 ref 完全一样
* selectedExamId.value = 123;
*/
export function useUserStorage<T>(key: string, defaultValue: T) {
const userStore = useUserStore()
const valueRef = ref<T>(defaultValue)
// 生成基于用户ID的存储键
const getStorageKey = () => {
const userId = userStore.info?.id || 'default'
return `user_${userId}_${key}`
}
// 从 uni.storage 加载数据
const loadFromStorage = () => {
const storageKey = getStorageKey()
const stored = uni.getStorageSync(storageKey)
if (stored) {
try {
valueRef.value = JSON.parse(stored)
}
catch {
// 如果是数字类型,直接转换
if (typeof defaultValue === 'number') {
valueRef.value = Number(stored) as T
}
else {
valueRef.value = stored as T
}
}
}
else {
valueRef.value = defaultValue
}
}
// 保存到 uni.storage
const saveToStorage = (value: T) => {
const storageKey = getStorageKey()
if (value === undefined || value === null || value === 0 || value === '') {
uni.removeStorageSync(storageKey)
}
else {
const toStore = typeof value === 'object' ? JSON.stringify(value) : String(value)
uni.setStorageSync(storageKey, toStore)
}
}
// 监听值变化,自动保存
watch(valueRef, (newValue) => {
saveToStorage(newValue)
})
// 监听用户变化,自动重新加载
watch(
() => userStore.info?.id,
() => {
loadFromStorage()
},
{ immediate: true },
)
return valueRef
}

View File

@ -10,6 +10,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type * as API from '@/service/types' import type * as API from '@/service/types'
import { useQuery, useQueryClient } from '@tanstack/vue-query' import { useQuery, useQueryClient } from '@tanstack/vue-query'
import { whenever } from '@vueuse/core'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import OverviewSettingsDialog from '@/components/score/OverviewSettingsDialog.vue' import OverviewSettingsDialog from '@/components/score/OverviewSettingsDialog.vue'
import RankSettingsDialog from '@/components/score/RankSettingsDialog.vue' import RankSettingsDialog from '@/components/score/RankSettingsDialog.vue'
@ -73,9 +74,6 @@ const showRankSettingsDialog = ref(false)
// /退 // /退
const keyStudentCount = ref(5) const keyStudentCount = ref(5)
// 使
const pageSize = ref(20)
// //
const examOptions = computed(() => homeStore.examOptions.map(exam => ({ const examOptions = computed(() => homeStore.examOptions.map(exam => ({
label: exam.label, label: exam.label,
@ -88,14 +86,6 @@ const classColumns = computed(() => homeStore.classOptions.map(cls => ({
value: cls.value.toString(), // value: cls.value.toString(), //
}))) })))
// ID
const selectedExamId = computed({
get: () => homeStore.selectedExamId,
set: (value) => {
homeStore.onExamChange(value)
},
})
// //
const selectedExamName = computed(() => { const selectedExamName = computed(() => {
if (!homeStore.selectedExamId) { if (!homeStore.selectedExamId) {
@ -247,9 +237,9 @@ const {
query_mode: 'class', query_mode: 'class',
statistics_mode: 'cumulative', statistics_mode: 'cumulative',
rank_ranges: [ rank_ranges: [
{ name: `${scoreSettings.value.rank_top_1}`, start: 1, end: scoreSettings.value.rank_top_1 }, { name: `${scoreSettings.value.rank_top_1}`, start: 1, end: Number(scoreSettings.value.rank_top_1) },
{ name: `${scoreSettings.value.rank_top_2}`, start: 1, end: scoreSettings.value.rank_top_2 }, { name: `${scoreSettings.value.rank_top_2}`, start: 1, end: Number(scoreSettings.value.rank_top_2) },
{ name: `${scoreSettings.value.rank_top_3}`, start: 1, end: scoreSettings.value.rank_top_3 }, { name: `${scoreSettings.value.rank_top_3}`, start: 1, end: Number(scoreSettings.value.rank_top_3) },
], ],
}, },
})).data as any || [] })).data as any || []
@ -258,39 +248,6 @@ const {
staleTime: 30000, staleTime: 30000,
}) })
// 使 TanStack Query
const {
data: keyStudentsData,
isLoading: isLoadingKeyStudents,
} = useQuery({
queryKey: computed(() => [
'key-students',
homeStore.selectedClassId,
homeStore.selectedExamId,
homeStore.selectedGradeKey,
selectedSubjectId.value,
keyStudentType.value,
keyStudentCount.value,
]),
queryFn: async () => {
const response = await teacherAnalysisKeyStudentUsingPost({
body: {
class_key: homeStore.selectedClassId,
exam_id: homeStore.selectedExamId,
grade_key: homeStore.selectedGradeKey,
subject_id: selectedSubjectId.value || undefined,
emphasis_type: keyStudentType.value,
page: 1,
page_size: keyStudentCount.value,
},
}) as any
return response?.list || []
},
enabled: computed(() => isQueryEnabled.value && !showMoreStudents.value),
staleTime: 30000,
})
// //
async function fetchKeyStudentsPaging(pageNo: number, pageSize: number) { async function fetchKeyStudentsPaging(pageNo: number, pageSize: number) {
if (!homeStore.selectedClassId || !homeStore.selectedExamId || !homeStore.selectedGradeKey) { if (!homeStore.selectedClassId || !homeStore.selectedExamId || !homeStore.selectedGradeKey) {
@ -307,7 +264,7 @@ async function fetchKeyStudentsPaging(pageNo: number, pageSize: number) {
subject_id: selectedSubjectId.value || undefined, subject_id: selectedSubjectId.value || undefined,
emphasis_type: keyStudentType.value, emphasis_type: keyStudentType.value,
page: pageNo, page: pageNo,
page_size: pageSize, page_size: Number(pageSize),
}, },
}) as any }) as any
@ -331,7 +288,7 @@ function goBack() {
// //
function handleExamChange(event: { value: string | number, selectedItem: Record<string, any> }) { function handleExamChange(event: { value: string | number, selectedItem: Record<string, any> }) {
selectedExamId.value = typeof event.value === 'string' ? Number.parseInt(event.value) : event.value homeStore.selectedExamId = typeof event.value === 'string' ? Number.parseInt(event.value) : event.value
} }
// //
@ -397,25 +354,11 @@ function toggleKeyStudentType(type: 'up' | 'down') {
} }
} }
// /退
function handleShowMoreKeyStudents() {
if (!homeStore.selectedClassId || !homeStore.selectedExamId || !homeStore.selectedGradeKey) {
uni.showToast({
title: '请先选择班级和考试',
icon: 'none',
})
return
}
showMoreStudents.value = true
setTimeout(() => {
keyStudentsPaging.value?.reload()
}, 100)
}
// //
function handleHideMoreKeyStudents() { function handleHideMoreKeyStudents() {
showMoreStudents.value = false showMoreStudents.value = false
//
keyStudentsList.value = []
} }
// //
@ -423,336 +366,272 @@ function handleClassChange({ value }: { value: string[] }) {
selectedClassIds.value = value selectedClassIds.value = value
} }
// whenever(() => [selectedSubjectId.value, homeStore.selectedExamId, keyStudentType.value, keyStudentCount.value], () => {
const loading = computed(() => keyStudentsPaging.value?.reload()
isLoadingStats.value || isLoadingComparison.value || isLoadingRank.value || isLoadingKeyStudents.value, })
)
</script> </script>
<template> <template>
<view class="score-page min-h-screen bg-slate-50"> <view class="score-page min-h-screen bg-slate-50">
<!-- 导航栏 --> <!-- z-paging 包裹整个内容 -->
<wd-navbar placeholder fixed> <z-paging
<template #left> ref="keyStudentsPaging"
<div class="flex items-center" @tap="goBack"> v-model="keyStudentsList"
<wd-icon name="arrow-left" size="20px" /> :default-page-size="keyStudentCount"
</div> :refresher-enabled="true"
</template> :auto="true"
<template #title> @query="fetchKeyStudentsPaging"
<div class="title items-center">
<wd-drop-menu>
<wd-drop-menu-item
v-model="selectedExamId"
:options="examOptions"
@change="handleExamChange"
/>
</wd-drop-menu>
</div>
</template>
</wd-navbar>
<!-- 科目选择标签栏 -->
<wd-tabs
v-model="activeTab"
sticky
class="bg-white"
@click="handleTabClick"
> >
<wd-tab <template #top>
v-for="tab in subjectTabs" <!-- 导航栏 -->
:key="tab.name" <wd-navbar placeholder fixed>
:name="tab.name" <template #left>
:title="tab.title" <div class="flex items-center" @tap="goBack">
/> <wd-icon name="arrow-left" size="20px" />
</wd-tabs> </div>
</template>
<template #title>
<div class="title items-center">
<wd-drop-menu>
<wd-drop-menu-item
v-model="homeStore.selectedExamId"
:options="homeStore.examOptions"
@change="handleExamChange"
/>
</wd-drop-menu>
</div>
</template>
</wd-navbar>
<!-- 主要内容区域 --> <!-- 科目选择标签栏 -->
<view class="p-4 space-y-4"> <wd-tabs
<!-- 整体概况卡片 --> v-model="activeTab"
<view class="rounded-xl bg-white p-3 shadow-sm"> sticky
<view class="mb-4 flex items-center justify-between"> class="bg-white"
<text class="text-lg text-slate-800 font-semibold">整体概况</text> @click="handleTabClick"
<view class="i-mingcute:filter-line text-lg text-cyan-500" @tap="handleOverviewSettings" />
</view>
<!-- 统计数据网格 - 2行3列 -->
<view class="grid grid-cols-3 gap-1">
<!-- 考试人数 -->
<view class="border border-blue-100 rounded-xl from-white to-blue-50 bg-gradient-to-br p-3">
<view class="mb-2 flex items-center">
<text class="text-xs text-slate-600">考试人数</text>
</view>
<text class="text-lg text-slate-800 font-bold">{{ statsData?.class_total_count || 0 }}</text>
</view>
<!-- 平均分/满分 -->
<view class="border border-blue-100 rounded-xl from-white to-blue-50 bg-gradient-to-br p-3">
<view class="mb-2 flex items-center">
<text class="text-xs text-slate-600">平均分/满分</text>
</view>
<text class="text-lg text-slate-800 font-bold">{{ Math.round(statsData?.class_average_score || 0) }}/{{ statsData?.full_score || 100 }}</text>
</view>
<!-- 最高分/最低分 -->
<view class="border border-cyan-100 rounded-xl from-white to-cyan-50 bg-gradient-to-br p-3">
<view class="mb-2 flex items-center">
<text class="text-xs text-slate-600">最高分/最低分</text>
</view>
<text class="text-lg text-slate-800 font-bold">{{ statsData?.class_highest_score || 0 }}/{{ statsData?.class_lowest_score || 0 }}</text>
</view>
<!-- 优秀率 -->
<view class="border border-sky-100 rounded-xl from-white to-sky-50 bg-gradient-to-br p-3">
<view class="mb-2 flex items-center">
<text class="text-xs text-slate-600">优秀率</text>
</view>
<text class="text-lg text-slate-800 font-bold">{{ Math.round((statsData?.excellent_class_scoring_rate || 0) * 100) }}%</text>
</view>
<!-- 良好率 -->
<view class="border border-sky-100 rounded-xl from-white to-sky-50 bg-gradient-to-br p-3">
<view class="mb-2 flex items-center">
<text class="text-xs text-slate-600">良好率</text>
</view>
<text class="text-lg text-slate-800 font-bold">{{ Math.round((statsData?.good_class_scoring_rate || 0) * 100) }}%</text>
</view>
<!-- 及格率 -->
<view class="border border-blue-100 rounded-xl from-white to-blue-50 bg-gradient-to-br p-3">
<view class="mb-2 flex items-center">
<text class="text-xs text-slate-600">及格率</text>
</view>
<text class="text-lg text-slate-800 font-bold">{{ Math.round((statsData?.pass_class_scoring_rate || 0) * 100) }}%</text>
</view>
</view>
<!-- 查看成绩单按钮 -->
<view class="mt-6">
<wd-button
type="primary"
size="medium"
class="w-full"
@click="handleViewScoreList"
>
查看成绩单
</wd-button>
</view>
</view>
<!-- 学科均分对比卡片 -->
<view class="rounded-xl bg-white p-4 shadow-sm">
<view class="mb-4 flex items-center justify-between">
<text class="text-lg text-slate-800 font-semibold">学科均分对比</text>
<view class="flex items-center gap-2">
<text
class="rounded px-2 py-1 text-sm"
:class="comparisonMode === 'horizontal' ? 'bg-blue-100 text-blue-600' : 'text-gray-500'"
@tap="toggleComparisonMode('horizontal')"
>
横向
</text>
<text
class="rounded px-2 py-1 text-sm"
:class="comparisonMode === 'vertical' ? 'bg-blue-100 text-blue-600' : 'text-gray-500'"
@tap="toggleComparisonMode('vertical')"
>
纵向
</text>
</view>
</view>
<!-- 对比数据表头 -->
<view class="mb-2 border-b border-gray-200 pb-2">
<view class="grid grid-cols-4 gap-2 text-center">
<text class="text-sm text-gray-600 font-medium">
{{ comparisonMode === 'horizontal' ? '学科' : '考试' }}
</text>
<text class="text-sm text-gray-600 font-medium">班级平均分</text>
<text class="text-sm text-gray-600 font-medium">{{ comparisonMode === 'horizontal' ? '年级平均分' : '平均分排名' }}</text>
<text class="text-sm text-gray-600 font-medium">{{ comparisonMode === 'horizontal' ? '班级排名' : '班级标准分' }}</text>
</view>
</view>
<!-- 对比数据列表 -->
<!-- 暂无数据提示 -->
<view v-if="comparisonData.length === 0" class="py-8 text-center">
<text class="text-sm text-gray-500">暂无对比数据</text>
</view>
<view v-else class="space-y-2">
<view
v-for="(item, index) in comparisonData"
:key="index"
class="grid grid-cols-4 gap-2 border-b border-gray-100 py-2 text-center last:border-b-0"
>
<text class="text-ellipsis text-sm text-gray-800">{{ comparisonMode === 'horizontal' ? item.subject_name : item.exam_name || '-' }}</text>
<text class="text-ellipsis text-sm text-gray-800 font-medium">{{ item.class_average || '-' }}</text>
<text class="text-ellipsis text-sm text-gray-600">{{ comparisonMode === 'horizontal' ? item.grade_average || '-' : item.average_rank || '-' }}</text>
<text class="text-ellipsis text-sm text-gray-600">{{ comparisonMode === 'horizontal' ? item.class_rank || '-' : item.class_standard || '-' }}</text>
</view>
</view>
</view>
<!-- 名次分析卡片 -->
<view class="rounded-xl bg-white p-4 shadow-sm">
<view class="mb-4 flex items-center justify-between">
<text class="no-wrap w-fit flex-shrink-0 text-lg text-slate-800 font-semibold">名次分析</text>
<!-- 班级多选 -->
<wd-select-picker
v-model="selectedClassIds"
class="classes-picker flex-grow"
:columns="classColumns"
:show-confirm="false"
@confirm="handleClassChange"
/>
<view class="i-mingcute:filter-line text-lg text-cyan-500" @tap="handleRankSettings" />
</view>
<!-- 名次统计表头 -->
<view class="mb-2 border-b border-gray-200 pb-2">
<view class="grid grid-cols-4 gap-2 text-center">
<text class="text-sm text-gray-600 font-medium">班级</text>
<text class="text-sm text-gray-600 font-medium">{{ scoreSettings.rank_top_1 }}</text>
<text class="text-sm text-gray-600 font-medium">{{ scoreSettings.rank_top_2 }}</text>
<text class="text-sm text-gray-600 font-medium">{{ scoreSettings.rank_top_3 }}</text>
</view>
</view>
<!-- 名次统计数据 -->
<view class="space-y-2">
<view
v-for="(item, index) in rankStatsData"
:key="index"
class="grid grid-cols-4 gap-2 border-b border-gray-100 py-2 text-center last:border-b-0"
>
<text class="text-ellipsis text-sm text-gray-800 font-medium">{{ item.name || '-' }}</text>
<text class="text-ellipsis text-sm text-gray-800">
{{ item.rank_stats?.find(r => r.end === scoreSettings.rank_top_1)?.count || 0 }}
</text>
<text class="text-ellipsis text-sm text-gray-800">
{{ item.rank_stats?.find(r => r.end === scoreSettings.rank_top_2)?.count || 0 }}
</text>
<text class="text-ellipsis text-sm text-gray-800">
{{ item.rank_stats?.find(r => r.end === scoreSettings.rank_top_3)?.count || 0 }}
</text>
</view>
</view>
<!-- 暂无数据提示 -->
<view v-if="!rankStatsData || rankStatsData.length === 0" class="py-8 text-center">
<text class="text-sm text-gray-500">暂无名次数据</text>
</view>
</view>
<!-- 进步/退步学生卡片 -->
<view class="rounded-xl bg-white p-4 shadow-sm">
<view class="mb-4 flex items-center justify-between">
<view class="flex items-center gap-2">
<text class="text-lg text-slate-800 font-semibold">
班级{{ keyStudentType === 'up' ? '进步' : '退步' }}
</text>
<wd-input
v-model="keyStudentCount"
type="number"
placeholder="5"
class="w-12 text-center"
:border="true"
/>
<text class="text-lg text-slate-800 font-semibold"></text>
</view>
<view class="flex items-center gap-2">
<text
class="rounded px-2 py-1 text-sm"
:class="keyStudentType === 'up' ? 'bg-green-100 text-green-600' : 'text-gray-500'"
@tap="toggleKeyStudentType('up')"
>
进步
</text>
<text
class="rounded px-2 py-1 text-sm"
:class="keyStudentType === 'down' ? 'bg-red-100 text-red-600' : 'text-gray-500'"
@tap="toggleKeyStudentType('down')"
>
退步
</text>
</view>
</view>
<!-- 学生表头 -->
<view class="mb-2 border-b border-gray-200 pb-2">
<view class="grid grid-cols-3 gap-2 text-center">
<text class="text-sm text-gray-600 font-medium">姓名</text>
<text class="text-sm text-gray-600 font-medium">本次排名</text>
<text class="text-sm text-gray-600 font-medium">
{{ keyStudentType === 'up' ? '进步' : '退步' }}名次
</text>
</view>
</view>
<!-- 学生数据列表 -->
<view class="space-y-2">
<view
v-for="(student, index) in keyStudentsData"
:key="index"
class="grid grid-cols-3 gap-2 border-b border-gray-100 py-2 text-center last:border-b-0"
>
<text class="text-ellipsis text-sm text-gray-800 font-medium">{{ student.student_name || '-' }}</text>
<text class="text-ellipsis text-sm text-gray-800">{{ student.class_rank || '-' }}</text>
<text
class="text-ellipsis text-sm font-medium"
:class="keyStudentType === 'up' ? 'text-green-600' : 'text-red-600'"
>
{{ keyStudentType === 'up' ? '+' : '-' }}{{ student.class_diff_rank || 0 }}
</text>
</view>
</view>
<!-- 暂无数据提示 -->
<view v-if="!keyStudentsData || keyStudentsData.length === 0" class="py-8 text-center">
<text class="text-sm text-gray-500">暂无{{ keyStudentType === 'up' ? '进步' : '退步' }}学生数据</text>
</view>
<!-- 显示更多按钮 -->
<view v-if="keyStudentsData && keyStudentsData.length > 0 && !showMoreStudents" class="mt-4">
<wd-button
type="info"
size="medium"
class="w-full"
@click="handleShowMoreKeyStudents"
>
显示更多{{ keyStudentType === 'up' ? '进步' : '退步' }}学生
</wd-button>
</view>
<!-- 收起按钮 -->
<view v-if="showMoreStudents" class="mt-4">
<wd-button
type="info"
size="medium"
class="w-full"
@click="handleHideMoreKeyStudents"
>
收起列表
</wd-button>
</view>
</view>
<!-- 更多学生列表z-paging -->
<view v-if="showMoreStudents" class="rounded-xl bg-white shadow-sm">
<view class="border-b border-gray-100 p-4">
<text class="text-lg text-slate-800 font-semibold">
{{ keyStudentType === 'up' ? '进步' : '退步' }}学生详细列表
</text>
</view>
<z-paging
ref="keyStudentsPaging"
v-model="keyStudentsList"
:default-page-size="pageSize"
:auto="false"
@query="fetchKeyStudentsPaging"
> >
<!-- 表头 --> <wd-tab
<view class="border-b border-gray-200 p-4"> v-for="tab in subjectTabs"
:key="tab.name"
:name="tab.name"
:title="tab.title"
/>
</wd-tabs>
</template>
<!-- 主要内容区域 -->
<view class="p-4 space-y-4">
<!-- 整体概况卡片 -->
<view class="rounded-xl bg-white p-3 shadow-sm">
<view class="mb-4 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" @tap="handleOverviewSettings" />
</view>
<!-- 统计数据网格 - 2行3列 -->
<view class="grid grid-cols-3 gap-1">
<!-- 考试人数 -->
<view class="border border-blue-100 rounded-xl from-white to-blue-50 bg-gradient-to-br p-3">
<view class="mb-2 flex items-center">
<text class="text-xs text-slate-600">考试人数</text>
</view>
<text class="text-lg text-slate-800 font-bold">{{ statsData?.class_total_count || 0 }}</text>
</view>
<!-- 平均分/满分 -->
<view class="border border-blue-100 rounded-xl from-white to-blue-50 bg-gradient-to-br p-3">
<view class="mb-2 flex items-center">
<text class="text-xs text-slate-600">平均分/满分</text>
</view>
<text class="text-lg text-slate-800 font-bold">{{ Math.round(statsData?.class_average_score || 0) }}/{{ statsData?.full_score || 100 }}</text>
</view>
<!-- 最高分/最低分 -->
<view class="border border-cyan-100 rounded-xl from-white to-cyan-50 bg-gradient-to-br p-3">
<view class="mb-2 flex items-center">
<text class="text-xs text-slate-600">最高分/最低分</text>
</view>
<text class="text-lg text-slate-800 font-bold">{{ statsData?.class_highest_score || 0 }}/{{ statsData?.class_lowest_score || 0 }}</text>
</view>
<!-- 优秀率 -->
<view class="border border-sky-100 rounded-xl from-white to-sky-50 bg-gradient-to-br p-3">
<view class="mb-2 flex items-center">
<text class="text-xs text-slate-600">优秀率</text>
</view>
<text class="text-lg text-slate-800 font-bold">{{ Math.round((statsData?.excellent_class_scoring_rate || 0) * 100) }}%</text>
</view>
<!-- 良好率 -->
<view class="border border-sky-100 rounded-xl from-white to-sky-50 bg-gradient-to-br p-3">
<view class="mb-2 flex items-center">
<text class="text-xs text-slate-600">良好率</text>
</view>
<text class="text-lg text-slate-800 font-bold">{{ Math.round((statsData?.good_class_scoring_rate || 0) * 100) }}%</text>
</view>
<!-- 及格率 -->
<view class="border border-blue-100 rounded-xl from-white to-blue-50 bg-gradient-to-br p-3">
<view class="mb-2 flex items-center">
<text class="text-xs text-slate-600">及格率</text>
</view>
<text class="text-lg text-slate-800 font-bold">{{ Math.round((statsData?.pass_class_scoring_rate || 0) * 100) }}%</text>
</view>
</view>
<!-- 查看成绩单按钮 -->
<view class="mt-6">
<wd-button
type="primary"
size="medium"
class="w-full"
@click="handleViewScoreList"
>
查看成绩单
</wd-button>
</view>
</view>
<!-- 学科均分对比卡片 -->
<view class="rounded-xl bg-white p-4 shadow-sm">
<view class="mb-4 flex items-center justify-between">
<text class="text-lg text-slate-800 font-semibold">学科均分对比</text>
<view class="flex items-center gap-2">
<text
class="rounded px-2 py-1 text-sm"
:class="comparisonMode === 'horizontal' ? 'bg-blue-100 text-blue-600' : 'text-gray-500'"
@tap="toggleComparisonMode('horizontal')"
>
横向
</text>
<text
class="rounded px-2 py-1 text-sm"
:class="comparisonMode === 'vertical' ? 'bg-blue-100 text-blue-600' : 'text-gray-500'"
@tap="toggleComparisonMode('vertical')"
>
纵向
</text>
</view>
</view>
<!-- 对比数据表头 -->
<view class="mb-2 border-b border-gray-200 pb-2">
<view class="grid grid-cols-4 gap-2 text-center">
<text class="text-sm text-gray-600 font-medium">
{{ comparisonMode === 'horizontal' ? '学科' : '考试' }}
</text>
<text class="text-sm text-gray-600 font-medium">班级平均分</text>
<text class="text-sm text-gray-600 font-medium">{{ comparisonMode === 'horizontal' ? '年级平均分' : '平均分排名' }}</text>
<text class="text-sm text-gray-600 font-medium">{{ comparisonMode === 'horizontal' ? '班级排名' : '班级标准分' }}</text>
</view>
</view>
<!-- 对比数据列表 -->
<!-- 暂无数据提示 -->
<view v-if="comparisonData.length === 0" class="py-8 text-center">
<text class="text-sm text-gray-500">暂无对比数据</text>
</view>
<view v-else class="space-y-2">
<view
v-for="(item, index) in comparisonData"
:key="index"
class="grid grid-cols-4 gap-2 border-b border-gray-100 py-2 text-center last:border-b-0"
>
<text class="text-ellipsis text-sm text-gray-800">{{ comparisonMode === 'horizontal' ? item.subject_name : item.exam_name || '-' }}</text>
<text class="text-ellipsis text-sm text-gray-800 font-medium">{{ item.class_average || '-' }}</text>
<text class="text-ellipsis text-sm text-gray-600">{{ comparisonMode === 'horizontal' ? item.grade_average || '-' : item.average_rank || '-' }}</text>
<text class="text-ellipsis text-sm text-gray-600">{{ comparisonMode === 'horizontal' ? item.class_rank || '-' : item.class_standard || '-' }}</text>
</view>
</view>
</view>
<!-- 名次分析卡片 -->
<view class="rounded-xl bg-white p-4 shadow-sm">
<view class="mb-4 flex items-center justify-between">
<text class="no-wrap w-fit flex-shrink-0 text-lg text-slate-800 font-semibold">名次分析</text>
<!-- 班级多选 -->
<wd-select-picker
v-model="selectedClassIds"
class="classes-picker flex-grow"
:columns="classColumns"
:show-confirm="false"
@confirm="handleClassChange"
/>
<view class="i-mingcute:filter-line text-lg text-cyan-500" @tap="handleRankSettings" />
</view>
<!-- 名次统计表头 -->
<view class="mb-2 border-b border-gray-200 pb-2">
<view class="grid grid-cols-4 gap-2 text-center">
<text class="text-sm text-gray-600 font-medium">班级</text>
<text class="text-sm text-gray-600 font-medium">{{ scoreSettings.rank_top_1 }}</text>
<text class="text-sm text-gray-600 font-medium">{{ scoreSettings.rank_top_2 }}</text>
<text class="text-sm text-gray-600 font-medium">{{ scoreSettings.rank_top_3 }}</text>
</view>
</view>
<!-- 名次统计数据 -->
<view class="space-y-2">
<view
v-for="(item, index) in rankStatsData"
:key="index"
class="grid grid-cols-4 gap-2 border-b border-gray-100 py-2 text-center last:border-b-0"
>
<text class="text-ellipsis text-sm text-gray-800 font-medium">{{ item.name || '-' }}</text>
<text class="text-ellipsis text-sm text-gray-800">
{{ item.rank_stats?.find(r => r.end === scoreSettings.rank_top_1)?.count || 0 }}
</text>
<text class="text-ellipsis text-sm text-gray-800">
{{ item.rank_stats?.find(r => r.end === scoreSettings.rank_top_2)?.count || 0 }}
</text>
<text class="text-ellipsis text-sm text-gray-800">
{{ item.rank_stats?.find(r => r.end === scoreSettings.rank_top_3)?.count || 0 }}
</text>
</view>
</view>
<!-- 暂无数据提示 -->
<view v-if="!rankStatsData || rankStatsData.length === 0" class="py-8 text-center">
<text class="text-sm text-gray-500">暂无名次数据</text>
</view>
</view>
<!-- 进步/退步学生卡片 -->
<view class="rounded-xl bg-white p-4 shadow-sm">
<view class="mb-4 flex items-center justify-between">
<view class="flex items-center gap-2">
<text class="text-lg text-slate-800 font-semibold">
班级{{ keyStudentType === 'up' ? '进步' : '退步' }}
</text>
<wd-input
v-model="keyStudentCount"
type="number"
placeholder="5"
class="w-12 text-center"
:border="true"
/>
<text class="text-lg text-slate-800 font-semibold"></text>
</view>
<view class="flex items-center gap-2">
<text
class="rounded px-2 py-1 text-sm"
:class="keyStudentType === 'up' ? 'bg-green-100 text-green-600' : 'text-gray-500'"
@tap="toggleKeyStudentType('up')"
>
进步
</text>
<text
class="rounded px-2 py-1 text-sm"
:class="keyStudentType === 'down' ? 'bg-red-100 text-red-600' : 'text-gray-500'"
@tap="toggleKeyStudentType('down')"
>
退步
</text>
</view>
</view>
<!-- 学生表头 -->
<view class="mb-2 border-b border-gray-200 pb-2">
<view class="grid grid-cols-3 gap-2 text-center"> <view class="grid grid-cols-3 gap-2 text-center">
<text class="text-sm text-gray-600 font-medium">姓名</text> <text class="text-sm text-gray-600 font-medium">姓名</text>
<text class="text-sm text-gray-600 font-medium">本次排名</text> <text class="text-sm text-gray-600 font-medium">本次排名</text>
@ -763,11 +642,11 @@ const loading = computed(() =>
</view> </view>
<!-- 学生数据列表 --> <!-- 学生数据列表 -->
<view class="divide-y divide-gray-100"> <view class="space-y-2">
<view <view
v-for="(student, index) in keyStudentsList" v-for="(student, index) in keyStudentsList"
:key="index" :key="index"
class="grid grid-cols-3 gap-2 p-4 text-center" class="grid grid-cols-3 gap-2 border-b border-gray-100 py-2 text-center last:border-b-0"
> >
<text class="text-ellipsis text-sm text-gray-800 font-medium">{{ student.student_name || '-' }}</text> <text class="text-ellipsis text-sm text-gray-800 font-medium">{{ student.student_name || '-' }}</text>
<text class="text-ellipsis text-sm text-gray-800">{{ student.class_rank || '-' }}</text> <text class="text-ellipsis text-sm text-gray-800">{{ student.class_rank || '-' }}</text>
@ -779,9 +658,14 @@ const loading = computed(() =>
</text> </text>
</view> </view>
</view> </view>
</z-paging>
<!-- 暂无数据提示 -->
<view v-if="!keyStudentsList || keyStudentsList.length === 0" class="py-8 text-center">
<text class="text-sm text-gray-500">暂无{{ keyStudentType === 'up' ? '进步' : '退步' }}学生数据</text>
</view>
</view>
</view> </view>
</view> </z-paging>
<!-- 整体概况设置弹窗 --> <!-- 整体概况设置弹窗 -->
<OverviewSettingsDialog <OverviewSettingsDialog
@ -796,14 +680,6 @@ const loading = computed(() =>
:settings="scoreSettings" :settings="scoreSettings"
@confirm="confirmRankSettings" @confirm="confirmRankSettings"
/> />
<!-- 加载状态 -->
<!-- <view v-if="loading" class="fixed inset-0 z-50 flex items-center justify-center bg-black/20">
<view class="rounded-xl bg-white p-4">
<wd-loading size="24" />
<text class="mt-2 block text-sm text-gray-600">加载中...</text>
</view>
</view> -->
</view> </view>
</template> </template>

View File

@ -3,6 +3,7 @@ import { whenever } from '@vueuse/core'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { teacherScoreAnalysisApi } from '@/api' import { teacherScoreAnalysisApi } from '@/api'
import { useUserStorage } from '@/composables/useUserStorage'
export interface SelectOption { export interface SelectOption {
label: string label: string
@ -30,11 +31,11 @@ export const useHomeStore = defineStore(
const classList = ref<ClassItem[]>([]) const classList = ref<ClassItem[]>([])
const classGradeMap = ref<Map<number, number>>(new Map()) const classGradeMap = ref<Map<number, number>>(new Map())
// 已选择的数据 // 已选择的数据(使用 useUserStorage 实现基于用户的持久化)
const selectedClassId = ref<number | null>(null) const selectedClassId = useUserStorage<number>('home_selectedClassId', 0)
const selectedExamId = ref<number | null>(null) const selectedExamId = useUserStorage<number>('home_selectedExamId', 0)
const selectedSubjectId = ref<number | null>(null) const selectedSubjectId = useUserStorage<number>('home_selectedSubjectId', 0)
const selectedExamSubjectId = ref<number | null>(null) const selectedExamSubjectId = useUserStorage<number>('home_selectedExamSubjectId', 0)
const selectedGradeKey = computed(() => classGradeMap.value.get(selectedClassId.value || 0) || null) const selectedGradeKey = computed(() => classGradeMap.value.get(selectedClassId.value || 0) || null)
// 计算属性:考试选项 // 计算属性:考试选项