fix: 已有接口对接完成
continuous-integration/drone/push Build is failing Details

This commit is contained in:
张哲铜 2025-10-07 14:52:01 +08:00
parent 356558c15c
commit 95e05b37d6
13 changed files with 1065 additions and 682 deletions

View File

@ -84,6 +84,7 @@
"konva": "^9.3.22",
"pinia": "2.0.36",
"pinia-plugin-persistedstate": "3.2.1",
"uni-echarts": "^2.0.0",
"vue": "3.4.21",
"vue-draggable-plus": "^0.6.0",
"wot-design-uni": "^1.9.1",

View File

@ -89,6 +89,9 @@ importers:
pinia-plugin-persistedstate:
specifier: 3.2.1
version: 3.2.1(pinia@2.0.36(typescript@5.9.2)(vue@3.4.21(typescript@5.9.2)))
uni-echarts:
specifier: ^2.0.0
version: 2.0.0(echarts@6.0.0)(vue@3.4.21(typescript@5.9.2))
vue:
specifier: 3.4.21
version: 3.4.21(typescript@5.9.2)
@ -1148,12 +1151,21 @@ packages:
'@emnapi/core@1.4.5':
resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==}
'@emnapi/core@1.5.0':
resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==}
'@emnapi/runtime@1.4.5':
resolution: {integrity: sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==}
'@emnapi/runtime@1.5.0':
resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==}
'@emnapi/wasi-threads@1.0.4':
resolution: {integrity: sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==}
'@emnapi/wasi-threads@1.1.0':
resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==}
'@es-joy/jsdoccomment@0.50.2':
resolution: {integrity: sha512-YAdE/IJSpwbOTiaURNCKECdAwqrJuFiZhylmesBcIRawtYKnBR2wxPhoIewMg+Yu+QuYvHfJNReWpoxGBKOChA==}
engines: {node: '>=18'}
@ -1979,6 +1991,9 @@ packages:
'@napi-rs/wasm-runtime@0.2.12':
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
'@napi-rs/wasm-runtime@1.0.6':
resolution: {integrity: sha512-DXj75ewm11LIWUk198QSKUTxjyRjsBwk09MuMk5DGK+GDUtyPhhEHOGP/Xwwj3DjQXXkivoBirmOnKrLfc0+9g==}
'@node-rs/xxhash-android-arm-eabi@1.7.6':
resolution: {integrity: sha512-ptmfpFZ8SgTef58Us+0HsZ9BKhyX/gZYbhLkuzPt7qUoMqMSJK85NC7LEgzDgjUiG+S5GahEEQ9/tfh9BVvKhw==}
engines: {node: '>= 12'}
@ -2082,6 +2097,104 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
'@oxc-parser/binding-android-arm64@0.93.0':
resolution: {integrity: sha512-hTxegqGaVA5py2XCNV3Ry6e0tJNl32ZlB5TNOL9YuxvzTY3y3ySJovhufaubtOr/qW/FYmA5l+UC78gbtRTLEw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
'@oxc-parser/binding-darwin-arm64@0.93.0':
resolution: {integrity: sha512-8Er+e4+0BX3hc+Ajuq/60p4qA4/dW8XGUdbE1LBEwx6z1anKv4lAc/J2GfPWLUAhJLZIaM/waGBSxhoWDrZD9A==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
'@oxc-parser/binding-darwin-x64@0.93.0':
resolution: {integrity: sha512-pRLB9uEgTj/P4eNrQlKJX6Ey5pelhaQnywdF4uIFPWLVGjRoS8IEuRVE9+FxUjnikXBIJceDgtRd16/EArgAKQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
'@oxc-parser/binding-freebsd-x64@0.93.0':
resolution: {integrity: sha512-aH2kMXL+60rhBbHYWU5cICo6HufTAWs1/8Ztu0nI4rr0Facp/mK2Ft6pGeuDxCJeKGyYIC21GIxVA7BHrGk9TQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
'@oxc-parser/binding-linux-arm-gnueabihf@0.93.0':
resolution: {integrity: sha512-vk1nZchv1hH2yf6hE5Nbs8DliRGEoDtAwonxpz/yBaAvUsKFZHHwx0hXdJdWr+8EfSfgbWfk4YT6rUadz9N7hQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@oxc-parser/binding-linux-arm-musleabihf@0.93.0':
resolution: {integrity: sha512-xDrvQ23KUGWi7hPfGrFTrGLiwSeb9W1IEVpMPsRKmlvLP+zJS9Ht+RaPaLJwwQgdlNYI9f05oE6opAH5sw7MTQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@oxc-parser/binding-linux-arm64-gnu@0.93.0':
resolution: {integrity: sha512-NoB7BJmwVGrcS/J5XXn362lBsIyeTqZF70rCFij3/XwQ2kcELfGMALY9AUulFYauLTY2AG4vcmctJQxn9Lj85g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@oxc-parser/binding-linux-arm64-musl@0.93.0':
resolution: {integrity: sha512-s+nraJJR9SuHsgsr42nbOBpAsaSAE6MhK7HGbz01svLJzDsk3Ylh9cbVUPLaS3gOlTq5WC6VjPBkQuInLo0hvQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@oxc-parser/binding-linux-riscv64-gnu@0.93.0':
resolution: {integrity: sha512-oNIQb/7HGxVNeVgtkoqNcDS1hjfxArLDuMI72V+Slp67yfBdxgvfmM2JSWE7kGR5gyiZQeTjRbG89VrRwPDtww==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@oxc-parser/binding-linux-s390x-gnu@0.93.0':
resolution: {integrity: sha512-YyzhzAoq5WpRtAGOngpJUu+4jKagSbknORejmpeW48vu8/+XjrVZFc/1Qe4i72EsPzLorDwCxWVkU8VftpM4iA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@oxc-parser/binding-linux-x64-gnu@0.93.0':
resolution: {integrity: sha512-UMXsE6c0MIlvtqDe5t5K8qwC6HqNb3wmy8zKxONo42dIx0WAhVV9ydG2Xlznt1/RhD6nLLtHVaq4yWJXRjUxcg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@oxc-parser/binding-linux-x64-musl@0.93.0':
resolution: {integrity: sha512-0Vd0yFUq129VW+Cpcj/gJOqub4EMN5hUWnVk8UfAvUZ+lxZBFeXbYNI5483SLwzvw5umzlMmkKpYWw5OTwYFaA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@oxc-parser/binding-wasm32-wasi@0.93.0':
resolution: {integrity: sha512-EXyCyY4GJO+SNTQJPPmJJwYbPkPOzw2nxSRMmUlwG19WKO7QHzHyL6u+4hXpp5IwgIWvgQgoix2/pB9JF+EA7w==}
engines: {node: '>=14.0.0'}
cpu: [wasm32]
'@oxc-parser/binding-win32-arm64-msvc@0.93.0':
resolution: {integrity: sha512-LiWj6Yp91YnN8QptfP/+s2nfvQrbYXuaU53w9Pkyceimx0msQboddW3Dud4fbbmp3xzvNkw13+bMkGz5BLHO1w==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
'@oxc-parser/binding-win32-x64-msvc@0.93.0':
resolution: {integrity: sha512-e3XD808kQLxvTD1x4xJ4p73x9idhHtSgtgcXjgo3L4hgvoRSwT1+Mu9ddZ9BLuV4wo49tmKZpp2exfxhZx1vhQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
'@oxc-project/types@0.93.0':
resolution: {integrity: sha512-yNtwmWZIBtJsMr5TEfoZFDxIWV6OdScOpza/f5YxbqUMJk+j6QX3Cf3jgZShGEFYWQJ5j9mJ6jM0tZHu2J9Yrg==}
'@pkgr/core@0.1.2':
resolution: {integrity: sha512-fdDH1LSGfZdTH2sxdpVMw31BanV28K/Gry0cVFxaNP77neJSkd82mM8ErPNYs9e+0O7SdHBLTDzDgwUuy18RnQ==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
@ -2290,6 +2403,9 @@ packages:
'@tybys/wasm-util@0.10.0':
resolution: {integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==}
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
'@types/babel__core@7.20.5':
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
@ -5299,6 +5415,10 @@ packages:
resolution: {integrity: sha512-Sv0OvhPiMutICiwORAUefv02DCPb62IelBmo8ZsSrRHyI3FStqIWZvjqDkvtjU+lcujo7UNir+dCwKSqlEQ/5w==}
engines: {node: '>=10', yarn: ^1.22.4}
oxc-parser@0.93.0:
resolution: {integrity: sha512-ktMzTb3AqYCAsgnGTsWOhJYEBxGhxm6F+Ja9HsRibvVYBnA/BCiALAYLQk6M47mdEyybP9B3sOj56UDT+VIkMg==}
engines: {node: ^20.19.0 || >=22.12.0}
p-cancelable@2.1.1:
resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==}
engines: {node: '>=8'}
@ -6220,6 +6340,12 @@ packages:
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
uni-echarts@2.0.0:
resolution: {integrity: sha512-Sa+nS7WOHzfMXDZl0iEFOXIkU/pYx2pAZZVnJRrh46FkAPqdVbq438iqrQQXNkuke4WxevExqfDHPAj27axUWA==}
peerDependencies:
echarts: '>=5.3.0'
vue: '>=3.3.0'
unicode-canonical-property-names-ecmascript@2.0.1:
resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==}
engines: {node: '>=4'}
@ -8349,16 +8475,32 @@ snapshots:
tslib: 2.8.1
optional: true
'@emnapi/core@1.5.0':
dependencies:
'@emnapi/wasi-threads': 1.1.0
tslib: 2.8.1
optional: true
'@emnapi/runtime@1.4.5':
dependencies:
tslib: 2.8.1
optional: true
'@emnapi/runtime@1.5.0':
dependencies:
tslib: 2.8.1
optional: true
'@emnapi/wasi-threads@1.0.4':
dependencies:
tslib: 2.8.1
optional: true
'@emnapi/wasi-threads@1.1.0':
dependencies:
tslib: 2.8.1
optional: true
'@es-joy/jsdoccomment@0.50.2':
dependencies:
'@types/estree': 1.0.8
@ -9214,6 +9356,13 @@ snapshots:
'@tybys/wasm-util': 0.10.0
optional: true
'@napi-rs/wasm-runtime@1.0.6':
dependencies:
'@emnapi/core': 1.5.0
'@emnapi/runtime': 1.5.0
'@tybys/wasm-util': 0.10.1
optional: true
'@node-rs/xxhash-android-arm-eabi@1.7.6':
optional: true
@ -9287,6 +9436,55 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.19.1
'@oxc-parser/binding-android-arm64@0.93.0':
optional: true
'@oxc-parser/binding-darwin-arm64@0.93.0':
optional: true
'@oxc-parser/binding-darwin-x64@0.93.0':
optional: true
'@oxc-parser/binding-freebsd-x64@0.93.0':
optional: true
'@oxc-parser/binding-linux-arm-gnueabihf@0.93.0':
optional: true
'@oxc-parser/binding-linux-arm-musleabihf@0.93.0':
optional: true
'@oxc-parser/binding-linux-arm64-gnu@0.93.0':
optional: true
'@oxc-parser/binding-linux-arm64-musl@0.93.0':
optional: true
'@oxc-parser/binding-linux-riscv64-gnu@0.93.0':
optional: true
'@oxc-parser/binding-linux-s390x-gnu@0.93.0':
optional: true
'@oxc-parser/binding-linux-x64-gnu@0.93.0':
optional: true
'@oxc-parser/binding-linux-x64-musl@0.93.0':
optional: true
'@oxc-parser/binding-wasm32-wasi@0.93.0':
dependencies:
'@napi-rs/wasm-runtime': 1.0.6
optional: true
'@oxc-parser/binding-win32-arm64-msvc@0.93.0':
optional: true
'@oxc-parser/binding-win32-x64-msvc@0.93.0':
optional: true
'@oxc-project/types@0.93.0': {}
'@pkgr/core@0.1.2': {}
'@pkgr/core@0.2.9': {}
@ -9438,6 +9636,11 @@ snapshots:
tslib: 2.8.1
optional: true
'@tybys/wasm-util@0.10.1':
dependencies:
tslib: 2.8.1
optional: true
'@types/babel__core@7.20.5':
dependencies:
'@babel/parser': 7.28.0
@ -13253,6 +13456,26 @@ snapshots:
dependencies:
lcid: 3.1.1
oxc-parser@0.93.0:
dependencies:
'@oxc-project/types': 0.93.0
optionalDependencies:
'@oxc-parser/binding-android-arm64': 0.93.0
'@oxc-parser/binding-darwin-arm64': 0.93.0
'@oxc-parser/binding-darwin-x64': 0.93.0
'@oxc-parser/binding-freebsd-x64': 0.93.0
'@oxc-parser/binding-linux-arm-gnueabihf': 0.93.0
'@oxc-parser/binding-linux-arm-musleabihf': 0.93.0
'@oxc-parser/binding-linux-arm64-gnu': 0.93.0
'@oxc-parser/binding-linux-arm64-musl': 0.93.0
'@oxc-parser/binding-linux-riscv64-gnu': 0.93.0
'@oxc-parser/binding-linux-s390x-gnu': 0.93.0
'@oxc-parser/binding-linux-x64-gnu': 0.93.0
'@oxc-parser/binding-linux-x64-musl': 0.93.0
'@oxc-parser/binding-wasm32-wasi': 0.93.0
'@oxc-parser/binding-win32-arm64-msvc': 0.93.0
'@oxc-parser/binding-win32-x64-msvc': 0.93.0
p-cancelable@2.1.1: {}
p-limit@2.3.0:
@ -14153,6 +14376,12 @@ snapshots:
undici-types@6.21.0: {}
uni-echarts@2.0.0(echarts@6.0.0)(vue@3.4.21(typescript@5.9.2)):
dependencies:
echarts: 6.0.0
oxc-parser: 0.93.0
vue: 3.4.21(typescript@5.9.2)
unicode-canonical-property-names-ecmascript@2.0.1: {}
unicode-match-property-ecmascript@2.0.0:

View File

@ -1,15 +1,15 @@
<script lang="ts" setup>
import type * as API from '@/service/types'
import * as echarts from 'echarts'
import { computed, onMounted, ref, watch } from 'vue'
import { useQuery } from '@tanstack/vue-query'
import { LineChart } from 'echarts/charts'
import { GridComponent, LegendComponent, TooltipComponent } from 'echarts/components'
import * as echarts from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { computed, ref } from 'vue'
import { teacherAnalysisTrendUsingPost } from '@/service/laoshichengjifenxi'
import { useHomeStore } from '@/store/home'
//
type TrendInfo = API.TrendInfo
const props = defineProps<{
selectedSubjectId: number
compareClassId: number | null
@ -19,20 +19,21 @@ const emit = defineEmits<{
openCompareClassDialog: []
}>()
// ECharts
echarts.use([
LineChart,
GridComponent,
LegendComponent,
TooltipComponent,
CanvasRenderer,
])
//
type TrendInfo = API.TrendInfo
// 使store
const homeStore = useHomeStore()
//
const classTrendData = ref<TrendInfo[]>([])
const compareTrendData = ref<TrendInfo[]>([])
//
const loading = ref(false)
//
const chartRef = ref<HTMLElement>()
let chartInstance: echarts.ECharts | null = null
//
const compareClassName = computed(() => {
if (!props.compareClassId) {
@ -42,184 +43,228 @@ const compareClassName = computed(() => {
return cls?.label || '选择对比班级'
})
//
async function fetchTrendData() {
if (!homeStore.selectedClassId || !homeStore.selectedGradeKey) {
return
}
try {
loading.value = true
//
const classResponse = await teacherAnalysisTrendUsingPost({
// 使 TanStack Query
const {
data: classTrendData,
isLoading: isLoadingClass,
refetch: refetchClassData,
} = useQuery({
queryKey: computed(() => [
'class-trend',
homeStore.selectedClassId,
homeStore.selectedGradeKey,
props.selectedSubjectId,
]),
queryFn: async () => {
const response = await teacherAnalysisTrendUsingPost({
body: {
class_key: homeStore.selectedClassId,
grade_key: homeStore.selectedGradeKey,
class_key_compare: props.compareClassId || homeStore.selectedClassId, // 使
subject_id: props.selectedSubjectId || undefined,
top_n: 50,
},
})
if (classResponse) {
classTrendData.value = classResponse.trend_list || []
}
return response || { trend_list: [] }
},
enabled: computed(() => !!homeStore.selectedClassId && !!homeStore.selectedGradeKey),
staleTime: 30000, // 30
})
//
if (props.compareClassId) {
const compareResponse = await teacherAnalysisTrendUsingPost({
body: {
class_key: props.compareClassId,
grade_key: homeStore.selectedGradeKey,
subject_id: props.selectedSubjectId || undefined,
top_n: 50,
},
})
// grade_key
const compareClassGradeKey = computed(() => {
if (!props.compareClassId)
return null
// classList gradeKey
const classItem = homeStore.classOptions.find(item => item.value === props.compareClassId)
return classItem ? homeStore.classGradeMap.get(props.compareClassId) : null
})
if (compareResponse) {
compareTrendData.value = compareResponse.trend_list || []
}
}
else {
compareTrendData.value = []
}
updateChart()
}
catch (error) {
console.error('获取走势数据失败:', error)
uni.showToast({
title: '获取数据失败',
icon: 'none',
// 使 TanStack Query
const {
data: compareTrendData,
isLoading: isLoadingCompare,
} = useQuery({
queryKey: computed(() => [
'class-trend',
props.compareClassId,
compareClassGradeKey.value,
props.selectedSubjectId,
]),
queryFn: async () => {
const response = await teacherAnalysisTrendUsingPost({
body: {
class_key: props.compareClassId,
grade_key: compareClassGradeKey.value,
class_key_compare: homeStore.selectedClassId || props.compareClassId, //
subject_id: props.selectedSubjectId || undefined,
top_n: 50,
},
})
return response || { trend_list: [] }
},
enabled: computed(() => !!props.compareClassId && !!compareClassGradeKey.value),
staleTime: 30000,
})
//
const loading = computed(() => isLoadingClass.value || isLoadingCompare.value)
//
const classTrendList = computed(() => classTrendData.value?.trend_list || [])
const compareTrendList = computed(() => compareTrendData.value?.trend_list || [])
// uni-echarts
const chartOption = computed(() => {
if (classTrendList.value.length === 0) {
return {}
}
finally {
loading.value = false
}
}
//
function initChart() {
if (!chartRef.value)
return
chartInstance = echarts.init(chartRef.value)
updateChart()
}
//
function updateChart() {
if (!chartInstance)
return
const option = getAverageScoreOption()
chartInstance.setOption(option, true)
}
//
function getAverageScoreOption(): echarts.EChartsOption {
//
const categories = classTrendData.value.map(item => item.exam_name || '')
const categories = classTrendList.value.map(item => item.exam_name || '')
//
const classScores = classTrendData.value.map(item => item.class_avg_score || 0)
// trend_data
const classScores = classTrendList.value.map(item =>
item.trend_data?.[0]?.class_avg_score || 0,
)
//
const gradeScores = classTrendData.value.map(item => item.grade_avg_score || 0)
//
const gradeScores = classTrendList.value.map((item) => {
const trendData = item.trend_data || []
if (trendData.length === 0)
return 0
const validScores = trendData
.map(td => td.class_avg_score || 0)
.filter(score => score > 0)
return validScores.length > 0
? validScores.reduce((sum, score) => sum + score, 0) / validScores.length
: 0
})
//
const compareScores = compareTrendData.value.map(item => item.class_avg_score || 0)
//
const compareScores = compareTrendList.value.map(item =>
item.trend_data?.[0]?.class_avg_score || 0,
)
const series: any[] = [
{
name: '本班级',
type: 'line',
data: classScores,
itemStyle: {
color: '#3b82f6',
},
smooth: true,
symbol: 'circle',
symbolSize: 6,
itemStyle: { color: '#3b82f6' },
lineStyle: { width: 2 },
},
{
name: '年级平均',
type: 'line',
data: gradeScores,
itemStyle: {
color: '#f59e0b',
},
smooth: true,
symbol: 'circle',
symbolSize: 6,
itemStyle: { color: '#f59e0b' },
lineStyle: { width: 2 },
},
]
const legendData = ['本班级', '年级平均']
//
if (props.compareClassId && compareTrendData.value.length > 0) {
if (props.compareClassId && compareTrendList.value.length > 0) {
series.push({
name: '对比班级',
name: compareClassName.value,
type: 'line',
data: compareScores,
itemStyle: {
color: '#10b981',
},
smooth: true,
symbol: 'circle',
symbolSize: 6,
itemStyle: { color: '#10b981' },
lineStyle: { width: 2 },
})
legendData.push('对比班级')
legendData.push(compareClassName.value)
}
return {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
confine: true,
formatter: (params: any) => {
if (!Array.isArray(params))
return ''
let result = `${params[0].axisValue}<br/>`
params.forEach((param: any) => {
result += `${param.marker} ${param.seriesName}: ${param.value.toFixed(2)}分<br/>`
})
return result
},
},
legend: {
data: legendData,
top: 5,
textStyle: {
fontSize: 12,
},
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true,
left: 40,
right: 15,
bottom: 30,
top: 40,
containLabel: false,
},
xAxis: {
type: 'category',
data: categories,
axisLabel: {
rotate: 30,
interval: 0,
fontSize: 10,
},
axisLine: {
lineStyle: {
color: '#cccccc',
},
},
},
yAxis: {
type: 'value',
name: '分数',
nameTextStyle: {
fontSize: 11,
},
axisLabel: {
fontSize: 10,
},
splitLine: {
lineStyle: {
type: 'dashed',
color: '#eeeeee',
},
},
},
series,
}
}
})
//
function openCompareClassDialog() {
emit('openCompareClassDialog')
}
// props
watch([() => props.selectedSubjectId, () => props.compareClassId], () => {
if (homeStore.selectedClassId && homeStore.selectedGradeKey) {
fetchTrendData()
}
}, { immediate: false })
//
onMounted(() => {
//
setTimeout(() => {
initChart()
}, 100)
//
if (homeStore.selectedClassId && homeStore.selectedGradeKey) {
fetchTrendData()
}
const showChart = computed(() => {
return !loading.value && classTrendList.value.length > 0
})
//
const chartRef = ref(null)
//
defineExpose({
fetchTrendData,
refetch: refetchClassData,
})
</script>
@ -232,14 +277,17 @@ defineExpose({
</wd-button>
</view>
<!-- 图表容器 -->
<view
ref="chartRef"
class="w-full"
/>
<!-- uni-echarts 图表组件 -->
<view v-if="showChart" class="chart-container">
<uni-echarts
ref="chartRef"
:option="chartOption"
custom-class="chart"
/>
</view>
<!-- 暂无数据提示 -->
<view v-if="classTrendData.length === 0 && !loading" class="py-8 text-center">
<view v-if="classTrendList.length === 0 && !loading" class="py-8 text-center">
<text class="text-sm text-gray-500">暂无数据</text>
</view>
@ -250,10 +298,9 @@ defineExpose({
</view>
</template>
<style lang="scss" scoped>
//
[ref='chartRef'] {
<style scoped>
.chart {
width: 100%;
height: 200px;
height: 250px;
}
</style>

View File

@ -0,0 +1,165 @@
<template>
<wd-popup
v-model="visible"
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="formData.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="formData.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="formData.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="formData.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="handleCancel">
取消
</wd-button>
<wd-button type="primary" class="flex-1" @click="handleConfirm">
确定
</wd-button>
</view>
</view>
</wd-popup>
</template>
<script lang="ts" setup>
import { computed, reactive, watch } from 'vue'
interface OverviewSettings {
excellent_scoring_rate: number
good_scoring_rate: number
pass_scoring_rate: number
top_n: number
}
interface Props {
modelValue: boolean
settings: OverviewSettings
}
interface Emits {
(e: 'update:modelValue', value: boolean): void
(e: 'confirm', settings: OverviewSettings): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const visible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value),
})
const formData = reactive<OverviewSettings>({
excellent_scoring_rate: 85,
good_scoring_rate: 75,
pass_scoring_rate: 60,
top_n: 50,
})
//
watch(() => props.modelValue, (newVal) => {
if (newVal) {
Object.assign(formData, props.settings)
}
})
function handleCancel() {
visible.value = false
}
function handleConfirm() {
const validations = [
{
condition: formData.excellent_scoring_rate <= 0 || formData.excellent_scoring_rate > 100,
message: '优秀分数线应在1-100之间',
},
{
condition: formData.good_scoring_rate <= 0 || formData.good_scoring_rate > 100,
message: '良好分数线应在1-100之间',
},
{
condition: formData.pass_scoring_rate <= 0 || formData.pass_scoring_rate > 100,
message: '及格分数线应在1-100之间',
},
{
condition: formData.top_n <= 0 || formData.top_n > 1000,
message: '前N名应在1-1000之间',
},
]
const failedValidation = validations.find(v => v.condition)
if (failedValidation) {
uni.showToast({
title: failedValidation.message,
icon: 'none',
})
return
}
emit('confirm', { ...formData })
visible.value = false
}
</script>

View File

@ -0,0 +1,145 @@
<template>
<wd-popup
v-model="visible"
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">
<!-- 前xx名(1) -->
<view class="flex items-center justify-center gap-4">
<text class="text-sm text-gray-700 font-medium">前xx名(1)</text>
<text class="text-sm text-gray-500">-</text>
<wd-input
v-model="formData.rank_top_1"
type="number"
placeholder="请输入"
class="w-20 text-center"
:border="true"
/>
<text class="text-sm text-gray-700"></text>
</view>
<!-- 前xx名(2) -->
<view class="flex items-center justify-center gap-4">
<text class="text-sm text-gray-700 font-medium">前xx名(2)</text>
<text class="text-sm text-gray-500">-</text>
<wd-input
v-model="formData.rank_top_2"
type="number"
placeholder="请输入"
class="w-20 text-center"
:border="true"
/>
<text class="text-sm text-gray-700"></text>
</view>
<!-- 前xx名(3) -->
<view class="flex items-center justify-center gap-4">
<text class="text-sm text-gray-700 font-medium">前xx名(3)</text>
<text class="text-sm text-gray-500">-</text>
<wd-input
v-model="formData.rank_top_3"
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="handleCancel">
取消
</wd-button>
<wd-button type="primary" class="flex-1" @click="handleConfirm">
确定
</wd-button>
</view>
</view>
</wd-popup>
</template>
<script lang="ts" setup>
import { computed, reactive, watch } from 'vue'
interface RankSettings {
rank_top_1: number
rank_top_2: number
rank_top_3: number
}
interface Props {
modelValue: boolean
settings: RankSettings
}
interface Emits {
(e: 'update:modelValue', value: boolean): void
(e: 'confirm', settings: RankSettings): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const visible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value),
})
const formData = reactive<RankSettings>({
rank_top_1: 10,
rank_top_2: 20,
rank_top_3: 50,
})
//
watch(() => props.modelValue, (newVal) => {
if (newVal) {
Object.assign(formData, props.settings)
}
})
function handleCancel() {
visible.value = false
}
function handleConfirm() {
const validations = [
{
condition: formData.rank_top_1 <= 0 || formData.rank_top_1 > 500,
message: '前xx名(1)应在1-500之间',
},
{
condition: formData.rank_top_2 <= 0 || formData.rank_top_2 > 500,
message: '前xx名(2)应在1-500之间',
},
{
condition: formData.rank_top_3 <= 0 || formData.rank_top_3 > 500,
message: '前xx名(3)应在1-500之间',
},
]
const failedValidation = validations.find(v => v.condition)
if (failedValidation) {
uni.showToast({
title: failedValidation.message,
icon: 'none',
})
return
}
emit('confirm', { ...formData })
visible.value = false
}
</script>

View File

@ -32,11 +32,15 @@ const showCompareClassDialog = ref(false)
//
const chartRef = ref<InstanceType<typeof AverageScoreChart>>()
//
const classOptions = computed(() => homeStore.classOptions.map(cls => ({
label: cls.label,
value: cls.value,
})))
//
const classOptions = computed(() =>
homeStore.classOptions
.filter(cls => cls.value !== homeStore.selectedClassId)
.map(cls => ({
label: cls.label,
value: cls.value,
})),
)
//
const subjectTabs = computed(() => {
@ -46,9 +50,9 @@ const subjectTabs = computed(() => {
homeStore.subjectOptions.forEach((subject) => {
tabs.push({
name: `subject-${subject.value}`,
name: `subject-${subject.subjectId}`,
title: subject.label,
subjectId: subject.value,
subjectId: subject.subjectId || 0,
})
})
@ -90,18 +94,11 @@ function handleCompareClassChange(classId: number) {
showCompareClassDialog.value = false
}
//
onMounted(async () => {
try {
// store
if (homeStore.classOptions.length === 0) {
await homeStore.fetchOptions()
}
}
catch (error) {
console.error('初始化数据失败:', error)
}
})
//
function clearCompareClass() {
compareClassId.value = null
showCompareClassDialog.value = false
}
</script>
<template>
@ -145,6 +142,7 @@ onMounted(async () => {
<!-- 考试小题概览组件 -->
<ExamQuestionOverview
v-if="selectedSubjectId !== 0"
:selected-subject-id="selectedSubjectId"
/>
</view>
@ -157,8 +155,16 @@ onMounted(async () => {
>
<view class="p-6">
<!-- 标题 -->
<view class="mb-6 text-center">
<view class="mb-6 flex items-center justify-between">
<text class="text-lg text-gray-800 font-semibold">选择对比班级</text>
<wd-button
v-if="compareClassId"
type="text"
size="small"
@click="clearCompareClass"
>
清除
</wd-button>
</view>
<!-- 班级列表 -->
@ -166,12 +172,17 @@ onMounted(async () => {
<view
v-for="cls in classOptions"
:key="cls.value"
class="border border-gray-200 rounded-lg p-3 text-center"
class="border border-gray-200 rounded-lg p-3 text-center transition-all"
:class="compareClassId === cls.value ? 'border-blue-500 bg-blue-50' : 'hover:bg-gray-50'"
@tap="handleCompareClassChange(cls.value)"
>
<text class="text-sm text-gray-800">{{ cls.label }}</text>
</view>
<!-- 空状态 -->
<view v-if="classOptions.length === 0" class="py-8 text-center">
<text class="text-sm text-gray-500">暂无其他班级</text>
</view>
</view>
<!-- 取消按钮 -->

View File

@ -11,7 +11,7 @@
</route>
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
import { teacherAnalysisRecentExamStatsUsingPost } from '@/service/laoshichengjifenxi'
import { useHomeStore } from '@/store/home'
import { useUserStore } from '@/store/user'
@ -255,13 +255,12 @@ function handleClassChange() {
}
//
function onClassSelectAction(action: { name: string, value: number }) {
homeStore.onClassChange(action.value)
watch(() => homeStore.selectedClassId, () => {
//
fetchExamStats()
//
showClassPicker.value = false
}
})
//
function handleSettings() {
@ -339,7 +338,7 @@ function handleExamReview() {
// TODO:
uni.navigateTo({
url: `/pages-sub/exam-review/index?classId=${homeStore.selectedClassId}&examId=${homeStore.selectedExamId}&subjectId=${homeStore.selectedSubjectId || ''}`,
url: `/pages-sub/exam-review/index?classId=${homeStore.selectedClassId}&examId=${homeStore.selectedExamId}&subjectId=${homeStore.selectedExamSubjectId || ''}`,
})
}
@ -367,8 +366,8 @@ onMounted(async () => {
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
if (!homeStore.selectedExamSubjectId && homeStore.subjectOptions.length > 0) {
homeStore.selectedExamSubjectId = homeStore.subjectOptions[0]?.value as number
}
//
@ -624,7 +623,7 @@ onMounted(async () => {
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 })"
@tap="homeStore.selectedClassId = classItem.value"
>
<text class="text-base">{{ classItem.label }}</text>
<view

View File

@ -9,8 +9,12 @@
<script lang="ts" setup>
import type * as API from '@/service/types'
import { computed, onMounted, ref, watch } from 'vue'
import { useQuery, useQueryClient } from '@tanstack/vue-query'
import { computed, ref } from 'vue'
import OverviewSettingsDialog from '@/components/score/OverviewSettingsDialog.vue'
import RankSettingsDialog from '@/components/score/RankSettingsDialog.vue'
import {
teacherAnalysisClassExamComparisonHorizontalUsingPost,
teacherAnalysisClassExamComparisonUsingPost,
teacherAnalysisKeyStudentUsingPost,
teacherAnalysisRankStatisticsUsingPost,
@ -29,6 +33,7 @@ defineOptions({
// 使store
const homeStore = useHomeStore()
const queryClient = useQueryClient()
// ID0
const selectedSubjectId = ref(0)
@ -45,33 +50,12 @@ const scoreSettings = ref({
rank_top_3: 50, // 50
})
//
const statsData = ref({
examCount: 0,
averageScore: 0,
totalScore: 100,
highestScore: 0,
lowestScore: 0,
excellentRate: 0,
goodRate: 0,
passRate: 0,
})
//
const comparisonData = ref<ExamComparisonItem[]>([])
// horizontal-vertical-
const comparisonMode = ref('horizontal')
//
const rankStatsData = ref<RankStatisticsItem[]>([])
//
const selectedClassIds = ref<string[]>([])
// /退
const keyStudentsData = ref<KeyStudentInfo[]>([])
// up-down-退
const keyStudentType = ref<'up' | 'down'>('up')
@ -84,25 +68,9 @@ const keyStudentsPaging = ref(null)
// z-paging
const keyStudentsList = ref<KeyStudentInfo[]>([])
//
const loading = ref(false)
//
const showOverviewSettingsDialog = ref(false) //
const showRankSettingsDialog = ref(false) //
const tempOverviewSettings = 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,
})
const tempRankSettings = ref({
rank_top_1: scoreSettings.value.rank_top_1,
rank_top_2: scoreSettings.value.rank_top_2,
rank_top_3: scoreSettings.value.rank_top_3,
})
const showOverviewSettingsDialog = ref(false)
const showRankSettingsDialog = ref(false)
// /退
const keyStudentCount = ref(5)
@ -147,9 +115,9 @@ const subjectTabs = computed(() => {
homeStore.subjectOptions.forEach((subject) => {
tabs.push({
name: `subject-${subject.value}`,
name: `subject-${subject.subjectId}`,
title: subject.label,
subjectId: subject.value,
subjectId: subject.subjectId,
})
})
@ -170,14 +138,29 @@ const activeTab = computed({
},
})
//
async function fetchOverviewStats() {
if (!homeStore.selectedClassId || !homeStore.selectedExamId || !homeStore.selectedGradeKey) {
return
}
//
const isQueryEnabled = computed(() =>
!!homeStore.selectedClassId && !!homeStore.selectedExamId && !!homeStore.selectedGradeKey,
)
try {
loading.value = true
// 使 TanStack Query
const {
data: statsData,
isLoading: isLoadingStats,
refetch: refetchStats,
} = useQuery({
queryKey: computed(() => [
'overview-stats',
homeStore.selectedClassId,
homeStore.selectedExamId,
homeStore.selectedGradeKey,
selectedSubjectId.value,
scoreSettings.value.excellent_scoring_rate,
scoreSettings.value.good_scoring_rate,
scoreSettings.value.pass_scoring_rate,
scoreSettings.value.top_n,
]),
queryFn: async () => {
const response = await teacherAnalysisRecentExamStatsUsingPost({
body: {
class_key: homeStore.selectedClassId,
@ -193,7 +176,7 @@ async function fetchOverviewStats() {
if (response?.data) {
const data = response.data
statsData.value = {
return {
examCount: data.class_total_count || 0,
averageScore: Math.round(data.class_average_score || 0),
totalScore: data.full_score || 100,
@ -204,32 +187,48 @@ async function fetchOverviewStats() {
passRate: Math.round((data.pass_class_scoring_rate || 0) * 100),
}
}
}
catch (error) {
console.error('获取整体概况数据失败:', error)
uni.showToast({
title: '获取数据失败',
icon: 'none',
})
}
finally {
loading.value = false
}
}
//
async function fetchComparisonData() {
if (!homeStore.selectedClassId || !homeStore.selectedGradeKey) {
return
}
return {
examCount: 0,
averageScore: 0,
totalScore: 100,
highestScore: 0,
lowestScore: 0,
excellentRate: 0,
goodRate: 0,
passRate: 0,
}
},
enabled: isQueryEnabled,
staleTime: 30000, // 30
})
try {
// 使 TanStack Query
const {
data: comparisonData,
isLoading: isLoadingComparison,
} = useQuery({
queryKey: computed(() => [
'comparison-data',
comparisonMode.value,
homeStore.selectedClassId,
homeStore.selectedExamId,
homeStore.selectedGradeKey,
selectedSubjectId.value,
]),
queryFn: async () => {
if (comparisonMode.value === 'horizontal') {
//
comparisonData.value = []
const response = await teacherAnalysisClassExamComparisonHorizontalUsingPost({
body: {
exam_id: homeStore.selectedExamId,
class_key: homeStore.selectedClassId,
grade_key: homeStore.selectedGradeKey,
subject_id: selectedSubjectId.value || undefined,
},
})
return []
}
else {
// -
const response = await teacherAnalysisClassExamComparisonUsingPost({
body: {
class_key: homeStore.selectedClassId,
@ -238,57 +237,63 @@ async function fetchComparisonData() {
},
}) as any
if (response?.data?.exam_list) {
comparisonData.value = response.data.exam_list
}
return response?.exam_list || []
}
}
catch (error) {
console.error('获取对比数据失败:', error)
comparisonData.value = []
}
}
},
enabled: computed(() => !!homeStore.selectedClassId && !!homeStore.selectedGradeKey),
staleTime: 30000,
})
//
async function fetchRankStatistics() {
if (!homeStore.selectedExamId) {
return
}
try {
// 使
const classIds = selectedClassIds.value.length > 0 ? selectedClassIds.value.map(id => Number.parseInt(id)) : (homeStore.selectedClassId ? [homeStore.selectedClassId] : [])
// 使 TanStack Query
const {
data: rankStatsData,
isLoading: isLoadingRank,
} = useQuery({
queryKey: computed(() => [
'rank-statistics',
selectedClassIds.value,
homeStore.selectedClassId,
homeStore.selectedExamId,
selectedSubjectId.value,
]),
queryFn: async () => {
const classIds = selectedClassIds.value.length > 0
? selectedClassIds.value.map(id => Number.parseInt(id))
: (homeStore.selectedClassId ? [homeStore.selectedClassId] : [])
if (classIds.length === 0) {
rankStatsData.value = []
return
return []
}
const response = await teacherAnalysisRankStatisticsUsingPost({
body: {
class_id: classIds[0], // 使
class_id: classIds[0],
exam_id: homeStore.selectedExamId,
subject_id: selectedSubjectId.value || undefined,
},
}) as any
if (response?.data?.data) {
rankStatsData.value = response.data.data
}
}
catch (error) {
console.error('获取名次统计数据失败:', error)
rankStatsData.value = []
}
}
return response?.data || []
},
enabled: computed(() => !!homeStore.selectedExamId),
staleTime: 30000,
})
//
async function fetchKeyStudents() {
if (!homeStore.selectedClassId || !homeStore.selectedExamId || !homeStore.selectedGradeKey) {
return
}
try {
// 使 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,
@ -297,19 +302,15 @@ async function fetchKeyStudents() {
subject_id: selectedSubjectId.value || undefined,
emphasis_type: keyStudentType.value,
page: 1,
page_size: keyStudentCount.value, // 使
page_size: keyStudentCount.value,
},
}) as any
if (response?.data?.list) {
keyStudentsData.value = response.data.list
}
}
catch (error) {
console.error('获取重点学生数据失败:', error)
keyStudentsData.value = []
}
}
return response?.list || []
},
enabled: computed(() => isQueryEnabled.value && !showMoreStudents.value),
staleTime: 30000,
})
//
async function fetchKeyStudentsPaging(pageNo: number, pageSize: number) {
@ -331,8 +332,8 @@ async function fetchKeyStudentsPaging(pageNo: number, pageSize: number) {
},
}) as any
if (response?.data?.list) {
keyStudentsPaging.value?.complete(response.data.list)
if (response?.list) {
keyStudentsPaging.value?.complete(response.list)
}
else {
keyStudentsPaging.value?.complete([])
@ -361,65 +362,17 @@ function handleTabClick({ name }: { name: string }) {
//
function handleOverviewSettings() {
tempOverviewSettings.value = {
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,
}
showOverviewSettingsDialog.value = true
}
//
function handleRankSettings() {
tempRankSettings.value = {
rank_top_1: scoreSettings.value.rank_top_1,
rank_top_2: scoreSettings.value.rank_top_2,
rank_top_3: scoreSettings.value.rank_top_3,
}
showRankSettingsDialog.value = true
}
//
function confirmOverviewSettings() {
const validations = [
{
condition: tempOverviewSettings.value.excellent_scoring_rate <= 0 || tempOverviewSettings.value.excellent_scoring_rate > 100,
message: '优秀分数线应在1-100之间',
},
{
condition: tempOverviewSettings.value.good_scoring_rate <= 0 || tempOverviewSettings.value.good_scoring_rate > 100,
message: '良好分数线应在1-100之间',
},
{
condition: tempOverviewSettings.value.pass_scoring_rate <= 0 || tempOverviewSettings.value.pass_scoring_rate > 100,
message: '及格分数线应在1-100之间',
},
{
condition: tempOverviewSettings.value.top_n <= 0 || tempOverviewSettings.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.excellent_scoring_rate = tempOverviewSettings.value.excellent_scoring_rate
scoreSettings.value.good_scoring_rate = tempOverviewSettings.value.good_scoring_rate
scoreSettings.value.pass_scoring_rate = tempOverviewSettings.value.pass_scoring_rate
scoreSettings.value.top_n = tempOverviewSettings.value.top_n
showOverviewSettingsDialog.value = false
//
fetchOverviewStats()
function confirmOverviewSettings(settings: typeof scoreSettings.value) {
Object.assign(scoreSettings.value, settings)
uni.showToast({
title: '设置已保存',
@ -428,40 +381,8 @@ function confirmOverviewSettings() {
}
//
function confirmRankSettings() {
const validations = [
{
condition: tempRankSettings.value.rank_top_1 <= 0 || tempRankSettings.value.rank_top_1 > 500,
message: '前xx名(1)应在1-500之间',
},
{
condition: tempRankSettings.value.rank_top_2 <= 0 || tempRankSettings.value.rank_top_2 > 500,
message: '前xx名(2)应在1-500之间',
},
{
condition: tempRankSettings.value.rank_top_3 <= 0 || tempRankSettings.value.rank_top_3 > 500,
message: '前xx名(3)应在1-500之间',
},
]
const failedValidation = validations.find(v => v.condition)
if (failedValidation) {
uni.showToast({
title: failedValidation.message,
icon: 'none',
})
return
}
//
scoreSettings.value.rank_top_1 = tempRankSettings.value.rank_top_1
scoreSettings.value.rank_top_2 = tempRankSettings.value.rank_top_2
scoreSettings.value.rank_top_3 = tempRankSettings.value.rank_top_3
showRankSettingsDialog.value = false
//
fetchRankStatistics()
function confirmRankSettings(settings: Pick<typeof scoreSettings.value, 'rank_top_1' | 'rank_top_2' | 'rank_top_3'>) {
Object.assign(scoreSettings.value, settings)
uni.showToast({
title: '设置已保存',
@ -469,20 +390,9 @@ function confirmRankSettings() {
})
}
//
function cancelOverviewSettings() {
showOverviewSettingsDialog.value = false
}
//
function cancelRankSettings() {
showRankSettingsDialog.value = false
}
//
function toggleComparisonMode(mode: 'horizontal' | 'vertical') {
comparisonMode.value = mode
fetchComparisonData()
}
//
@ -504,13 +414,8 @@ function handleViewScoreList() {
function toggleKeyStudentType(type: 'up' | 'down') {
keyStudentType.value = type
if (showMoreStudents.value) {
//
keyStudentsPaging.value?.reload()
}
else {
//
fetchKeyStudents()
}
}
// /退
@ -524,7 +429,6 @@ function handleShowMoreKeyStudents() {
}
showMoreStudents.value = true
//
setTimeout(() => {
keyStudentsPaging.value?.reload()
}, 100)
@ -533,66 +437,17 @@ function handleShowMoreKeyStudents() {
//
function handleHideMoreKeyStudents() {
showMoreStudents.value = false
//
fetchKeyStudents()
}
//
function handleClassChange({ value }: { value: string[] }) {
selectedClassIds.value = value
//
fetchRankStatistics()
}
//
watch([() => homeStore.selectedExamId, selectedSubjectId], () => {
if (homeStore.selectedExamId && homeStore.selectedClassId && homeStore.selectedGradeKey) {
fetchOverviewStats()
fetchComparisonData()
fetchRankStatistics()
if (!showMoreStudents.value) {
fetchKeyStudents()
}
else {
keyStudentsPaging.value?.reload()
}
}
}, { immediate: false })
//
watch(selectedClassIds, () => {
if (homeStore.selectedExamId) {
fetchRankStatistics()
}
}, { deep: true })
//
watch(keyStudentCount, () => {
if (homeStore.selectedExamId && homeStore.selectedClassId && homeStore.selectedGradeKey && !showMoreStudents.value) {
fetchKeyStudents()
}
})
//
onMounted(async () => {
try {
// store
if (homeStore.examOptions.length === 0) {
await homeStore.fetchOptions()
}
//
if (homeStore.selectedExamId && homeStore.selectedClassId && homeStore.selectedGradeKey) {
await fetchOverviewStats()
await fetchComparisonData()
await fetchRankStatistics()
await fetchKeyStudents()
}
}
catch (error) {
console.error('初始化数据失败:', error)
}
})
//
const loading = computed(() =>
isLoadingStats.value || isLoadingComparison.value || isLoadingRank.value || isLoadingKeyStudents.value,
)
</script>
<template>
@ -648,7 +503,7 @@ onMounted(async () => {
<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.examCount }}</text>
<text class="text-lg text-slate-800 font-bold">{{ statsData?.examCount || 0 }}</text>
</view>
<!-- 平均分/满分 -->
@ -656,7 +511,7 @@ onMounted(async () => {
<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.averageScore }}/{{ statsData.totalScore }}</text>
<text class="text-lg text-slate-800 font-bold">{{ statsData?.averageScore || 0 }}/{{ statsData?.totalScore || 100 }}</text>
</view>
<!-- 最高分/最低分 -->
@ -664,7 +519,7 @@ onMounted(async () => {
<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.highestScore }}/{{ statsData.lowestScore }}</text>
<text class="text-lg text-slate-800 font-bold">{{ statsData?.highestScore || 0 }}/{{ statsData?.lowestScore || 0 }}</text>
</view>
<!-- 优秀率 -->
@ -672,7 +527,7 @@ onMounted(async () => {
<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.excellentRate }}%</text>
<text class="text-lg text-slate-800 font-bold">{{ statsData?.excellentRate || 0 }}%</text>
</view>
<!-- 良好率 -->
@ -680,7 +535,7 @@ onMounted(async () => {
<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.goodRate }}%</text>
<text class="text-lg text-slate-800 font-bold">{{ statsData?.goodRate || 0 }}%</text>
</view>
<!-- 及格率 -->
@ -688,7 +543,7 @@ onMounted(async () => {
<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.passRate }}%</text>
<text class="text-lg text-slate-800 font-bold">{{ statsData?.passRate || 0 }}%</text>
</view>
</view>
@ -754,7 +609,7 @@ onMounted(async () => {
</view>
<!-- 暂无数据提示 -->
<view v-if="comparisonData.length === 0" class="py-8 text-center">
<view v-if="!comparisonData || comparisonData.length === 0" class="py-8 text-center">
<text class="text-sm text-gray-500">暂无对比数据</text>
</view>
</view>
@ -805,7 +660,7 @@ onMounted(async () => {
</view>
<!-- 暂无数据提示 -->
<view v-if="rankStatsData.length === 0" class="py-8 text-center">
<view v-if="!rankStatsData || rankStatsData.length === 0" class="py-8 text-center">
<text class="text-sm text-gray-500">暂无名次数据</text>
</view>
</view>
@ -874,12 +729,12 @@ onMounted(async () => {
</view>
<!-- 暂无数据提示 -->
<view v-if="keyStudentsData.length === 0" class="py-8 text-center">
<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.length > 0 && !showMoreStudents" class="mt-4">
<view v-if="keyStudentsData && keyStudentsData.length > 0 && !showMoreStudents" class="mt-4">
<wd-button
type="info"
size="medium"
@ -951,166 +806,26 @@ onMounted(async () => {
</view>
<!-- 整体概况设置弹窗 -->
<wd-popup
<OverviewSettingsDialog
v-model="showOverviewSettingsDialog"
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="tempOverviewSettings.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="tempOverviewSettings.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="tempOverviewSettings.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="tempOverviewSettings.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="cancelOverviewSettings">
取消
</wd-button>
<wd-button type="primary" class="flex-1" @click="confirmOverviewSettings">
确定
</wd-button>
</view>
</view>
</wd-popup>
:settings="scoreSettings"
@confirm="confirmOverviewSettings"
/>
<!-- 名次分析设置弹窗 -->
<wd-popup
<RankSettingsDialog
v-model="showRankSettingsDialog"
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">
<!-- 前xx名(1) -->
<view class="flex items-center justify-center gap-4">
<text class="text-sm text-gray-700 font-medium">前xx名(1)</text>
<text class="text-sm text-gray-500">-</text>
<wd-input
v-model="tempRankSettings.rank_top_1"
type="number"
placeholder="请输入"
class="w-20 text-center"
:border="true"
/>
<text class="text-sm text-gray-700"></text>
</view>
<!-- 前xx名(2) -->
<view class="flex items-center justify-center gap-4">
<text class="text-sm text-gray-700 font-medium">前xx名(2)</text>
<text class="text-sm text-gray-500">-</text>
<wd-input
v-model="tempRankSettings.rank_top_2"
type="number"
placeholder="请输入"
class="w-20 text-center"
:border="true"
/>
<text class="text-sm text-gray-700"></text>
</view>
<!-- 前xx名(3) -->
<view class="flex items-center justify-center gap-4">
<text class="text-sm text-gray-700 font-medium">前xx名(3)</text>
<text class="text-sm text-gray-500">-</text>
<wd-input
v-model="tempRankSettings.rank_top_3"
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="cancelRankSettings">
取消
</wd-button>
<wd-button type="primary" class="flex-1" @click="confirmRankSettings">
确定
</wd-button>
</view>
</view>
</wd-popup>
:settings="scoreSettings"
@confirm="confirmRankSettings"
/>
<!-- 加载状态 -->
<view v-if="loading" class="fixed inset-0 z-50 flex items-center justify-center bg-black/20">
<!-- <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>

View File

@ -12,7 +12,7 @@ const searchQuery = ref('')
//
const canLoadStudents = computed(() => {
return homeStore.selectedClassId && homeStore.selectedExamId && homeStore.selectedSubjectId && homeStore.selectedGradeKey
return homeStore.selectedClassId && homeStore.selectedExamId && homeStore.selectedExamSubjectId && homeStore.selectedGradeKey
})
// 使 TanStack Query
@ -26,7 +26,7 @@ const {
'students',
homeStore.selectedClassId,
homeStore.selectedExamId,
homeStore.selectedSubjectId,
homeStore.selectedExamSubjectId,
homeStore.selectedGradeKey,
searchQuery.value,
]),
@ -34,7 +34,7 @@ const {
const response = await teacherAnalysisStudentListUsingPost({
body: {
class_id: homeStore.selectedClassId!,
exam_subject_id: homeStore.selectedSubjectId!,
exam_subject_id: homeStore.selectedExamSubjectId!,
grade_id: homeStore.selectedGradeKey!,
keyword: searchQuery.value || undefined,
},
@ -59,16 +59,16 @@ function goToStudentDetail(student: StudentInfo) {
}
//
async function toggleAttention(student: StudentInfo) {
async function toggleAttention(student: StudentInfo, isAttentioned: boolean) {
if (!student.student_number) {
return uni.showToast({ title: '学生信息不完整', icon: 'error' })
return uni.showToast({ title: '学生信息缺少学号', icon: 'error' })
}
uni.showLoading({ title: '处理中...' })
try {
await teacherAnalysisAttentionStudentUsingPost({
body: { student_number: student.student_number, remark: '' },
body: { student_number: student.student_number, is_attention: !isAttentioned },
})
uni.showToast({ title: '操作成功', icon: 'success' })
refetch()
@ -90,7 +90,7 @@ async function handleDownload() {
const response = await teacherAnalysisExportStudentListUsingPost({
body: {
class_id: homeStore.selectedClassId!,
exam_subject_id: homeStore.selectedSubjectId!,
exam_subject_id: homeStore.selectedExamSubjectId!,
grade_id: homeStore.selectedGradeKey!,
},
})
@ -168,7 +168,7 @@ onMounted(async () => {
<wd-drop-menu>
<wd-drop-menu-item v-model="homeStore.selectedClassId" :options="homeStore.classOptions" />
<wd-drop-menu-item v-model="homeStore.selectedExamId" :options="homeStore.examOptions" />
<wd-drop-menu-item v-model="homeStore.selectedSubjectId" :options="homeStore.subjectOptions" />
<wd-drop-menu-item v-model="homeStore.selectedExamSubjectId" :options="homeStore.subjectOptions" />
</wd-drop-menu>
</view>
@ -193,7 +193,7 @@ onMounted(async () => {
:index="index + 1"
:is-attentioned="true"
@click="goToStudentDetail"
@toggle-attention="toggleAttention"
@toggle-attention="toggleAttention(student, true)"
/>
</view>
@ -210,7 +210,7 @@ onMounted(async () => {
:index="attentionedStudents.length + index + 1"
:is-attentioned="false"
@click="goToStudentDetail"
@toggle-attention="toggleAttention"
@toggle-attention="toggleAttention(student, false)"
/>
</view>
</view>

View File

@ -71,6 +71,28 @@ export async function teacherAnalysisClassExamComparisonUsingPost({
});
}
/** 获取班级考试对比(横向对比) 获取指定班级的所有考试对比数据包括班级平均分、年级平均分、班级排名、学科名字、学科ID POST /teacher-analysis/class-exam-comparison-horizontal */
export async function teacherAnalysisClassExamComparisonHorizontalUsingPost({
body,
options,
}: {
body: API.ClassExamComparisonRequestH;
options?: CustomRequestOptions;
}) {
return request<
API.Response & {
data?: API.ClassExamComparisonDataH;
}
>('/teacher-analysis/class-exam-comparison-horizontal', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
/** 获取班级下所有学生成绩列表 获取指定科目、年级、班级的所有学生成绩列表 POST /teacher-analysis/class-student-score-list */
export async function teacherAnalysisClassStudentScoreListUsingPost({
body,

View File

@ -263,6 +263,11 @@ export type ClassExamComparisonData = {
exam_list?: ExamComparisonItem[];
};
export type ClassExamComparisonDataH = {
/** 考试对比列表 */
exam_list?: ExamComparisonItemH[];
};
export type ClassExamComparisonRequest = {
/** 班级 */
class_key: number;
@ -272,6 +277,17 @@ export type ClassExamComparisonRequest = {
subject_id?: number;
};
export type ClassExamComparisonRequestH = {
/** 考试ID */
exam_id: number;
/** 班级 */
class_key: number;
/** 年级 */
grade_key: number;
/** 科目ID */
subject_id?: number;
};
export type ClassExportRequest = {
/** 班级字典key列表必填 */
class_ids: string[];
@ -446,6 +462,8 @@ export type CreateTeacherAttentionStudentRequest = {
remark?: string;
/** 学生学号 */
student_number: string;
/** 是否关注传入的学生 */
is_attention?: boolean;
};
export type CreateUploadSessionRequest = {
@ -579,6 +597,19 @@ export type ExamComparisonItem = {
grade_top50_count?: number;
};
export type ExamComparisonItemH = {
/** 科目ID */
subject_id?: number;
/** 科目名称 */
subject_name?: string;
/** 班级平均分 */
class_average?: number;
/** 年级平均分 */
grade_average?: number;
/** 班级排名 */
class_rank?: number;
};
export type ExamMarkingTaskListResponse = {
/** 考试列表 */
list?: ExamMarkingTaskResponse[];
@ -1239,14 +1270,18 @@ export type GetStudentMarkingQualityResponse = {
};
export type GetTrendRequest = {
/** 年级参数 */
grade_key: number;
/** 查询班级 */
class_key: number;
/** 查询年级 */
grade_key: number;
/** 对比的年级 */
class_key_compare: number;
/** 考试科目ID */
subject_id?: number;
/** 查看年级前N名人数 */
top_n: number;
/** 查询最近几次考试的成绩 */
exam_number?: number;
};
export type GetTrendResponse = {
@ -1328,6 +1363,8 @@ export type KeyStudentInfo = {
student_name?: string;
/** 学生学号 */
student_number?: string;
/** 是否关注 */
is_attention?: boolean;
};
export type LearningSituationAnalysis = {
@ -3254,17 +3291,32 @@ export type TotalScoreInfo = {
score_line?: number;
};
export type TrendInfo = {
export type TrendDataItem = {
/** 班级key */
class_key?: number;
/** 班级名称 */
class_name?: string;
/** 班级平均分 */
class_avg_score?: number;
/** 班级排名 */
class_rank?: number;
/** 班级前N名人数 */
class_top_count?: number;
};
export type TrendInfo = {
/** 考试日期 */
exam_date?: string;
/** 考试ID */
exam_id?: number;
/** 考试名称 */
exam_name?: string;
/** 班级平均分 */
class_avg_score?: number;
/** 年级平均分 */
grade_avg_score?: number;
/** 年级走势数据 */
trend_data?: TrendDataItem[];
};
export type UnifiedConfigData = {

View File

@ -1,3 +1,4 @@
import type { ModelTeacherAnalysisClassInfo, ModelTeacherAnalysisExamInfo } from '@/api/data-contracts'
import { whenever } from '@vueuse/core'
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
@ -6,14 +7,8 @@ import { teacherScoreAnalysisApi } from '@/api'
export interface SelectOption {
label: string
value: number
}
// 考试科目组合数据类型
export interface ExamSubjectItem {
examId: number
examName: string
examSubjectId: number
subjectName: string
examSubjectId?: number
subjectId?: number
}
// 班级信息类型
@ -31,7 +26,7 @@ export const useHomeStore = defineStore(
() => {
// 数据存储
const loading = ref(false)
const examSubjectList = ref<ExamSubjectItem[]>([])
const examDataMap = ref<Map<number, ModelTeacherAnalysisExamInfo>>(new Map())
const classList = ref<ClassItem[]>([])
const classGradeMap = ref<Map<number, number>>(new Map())
@ -39,47 +34,40 @@ export const useHomeStore = defineStore(
const selectedClassId = ref<number | null>(null)
const selectedExamId = ref<number | null>(null)
const selectedSubjectId = ref<number | null>(null)
const selectedGradeKey = ref<number | null>(null)
const selectedExamSubjectId = ref<number | null>(null)
const selectedGradeKey = computed(() => classGradeMap.value.get(selectedClassId.value || 0) || null)
// 计算属性:唯一的考试列表
// 计算属性:考试选项
const examOptions = computed((): SelectOption[] => {
console.log('examOptions计算属性被调用examSubjectList长度:', examSubjectList.value.length)
const examMap = new Map<number, string>()
examSubjectList.value.forEach((item) => {
if (!examMap.has(item.examId)) {
examMap.set(item.examId, item.examName)
}
})
const result = Array.from(examMap.entries()).map(([id, name]) => ({
label: name,
value: id,
return Array.from(examDataMap.value.values()).map(exam => ({
label: exam.exam_name || '',
value: exam.exam_id || 0,
}))
console.log('examOptions计算结果:', result)
return result
})
// 计算属性:班级列表
// 计算属性:班级选项
const classOptions = computed((): SelectOption[] => {
console.log('classOptions计算属性被调用classList长度:', classList.value.length)
const result = classList.value.map(item => ({
return classList.value.map(item => ({
label: item.label,
value: item.value,
}))
console.log('classOptions计算结果:', result)
return result
})
// 计算属性:根据选中考试过滤的科目列表
// 计算属性:根据选中考试获取科目选项
const subjectOptions = computed((): SelectOption[] => {
if (!selectedExamId.value) {
return []
}
return examSubjectList.value
.filter(item => item.examId === selectedExamId.value)
.map(item => ({
label: item.subjectName,
value: item.examSubjectId,
}))
const examData = examDataMap.value.get(selectedExamId.value)
if (!examData?.subjects) {
return []
}
return examData.subjects.map(subject => ({
label: subject.subject_name || '',
value: subject.exam_subject_id || 0,
examSubjectId: subject.exam_subject_id || 0,
subjectId: subject.subject_id || 0,
}))
})
/**
@ -92,7 +80,7 @@ export const useHomeStore = defineStore(
if (response) {
// 处理班级数据并建立班级到年级的映射
classList.value = (response.class_list || []).map((item: any) => {
classList.value = (response.class_list || []).map((item: ModelTeacherAnalysisClassInfo) => {
const classKey = item.class_key || 0
const gradeKey = item.grade_key || 0
@ -106,23 +94,17 @@ export const useHomeStore = defineStore(
}
})
// 处理考试科目组合数据 - 新接口结构不同需要从exam_list中的每个考试的subjects中提取
const examSubjects: ExamSubjectItem[] = []
;(response.exam_list || []).forEach((exam: any) => {
;(exam.subjects || []).forEach((subject: any) => {
examSubjects.push({
examId: exam.exam_id || 0,
examName: exam.exam_name || '',
examSubjectId: subject.exam_subject_id || 0,
subjectName: subject.subject_name || '',
})
})
// 处理考试数据,存储到 map 中
examDataMap.value.clear()
;(response.exam_list || []).forEach((exam: ModelTeacherAnalysisExamInfo) => {
if (exam.exam_id) {
examDataMap.value.set(exam.exam_id, exam)
}
})
examSubjectList.value = examSubjects
console.log('数据获取完成:')
console.log('classList数量:', classList.value.length)
console.log('examSubjectList数量:', examSubjectList.value.length)
console.log('examDataMap数量:', examDataMap.value.size)
}
}
catch (error) {
@ -135,24 +117,12 @@ export const useHomeStore = defineStore(
}
/**
*
*/
const onClassChange = (classId: number | null) => {
selectedClassId.value = classId
if (classId) {
selectedGradeKey.value = classGradeMap.value.get(classId) || null
}
else {
selectedGradeKey.value = null
}
}
/**
*
*
*/
const onExamChange = (examId: number | null) => {
selectedExamId.value = examId
selectedSubjectId.value = subjectOptions.value[0].value || null
selectedSubjectId.value = subjectOptions.value[0].subjectId || null
selectedExamSubjectId.value = subjectOptions.value[0].examSubjectId || null
}
/**
@ -170,7 +140,7 @@ export const useHomeStore = defineStore(
selectedClassId.value = null
selectedExamId.value = null
selectedSubjectId.value = null
selectedGradeKey.value = null
selectedExamSubjectId.value = null
}
/**
@ -178,14 +148,34 @@ export const useHomeStore = defineStore(
*/
const reset = () => {
classList.value = []
examSubjectList.value = []
examDataMap.value.clear()
classGradeMap.value.clear()
loading.value = false
clearSelections()
}
// 监听考试ID变化自动选择第一个科目
whenever(selectedExamId, (examId) => {
selectedSubjectId.value = subjectOptions.value[0].value || null
selectedSubjectId.value = subjectOptions.value[0].subjectId || null
selectedExamSubjectId.value = subjectOptions.value[0].examSubjectId || null
})
onMounted(async () => {
await fetchOptions()
// 如果有默认选择,获取统计数据
if (examOptions.value.length > 0 && classOptions.value.length > 0) {
// 设置默认选择
if (!selectedExamId.value) {
selectedExamId.value = examOptions.value[0]?.value as number
}
if (!selectedClassId.value) {
selectedClassId.value = classOptions.value[0]?.value as number
}
if (!selectedExamSubjectId.value && subjectOptions.value.length > 0) {
selectedExamSubjectId.value = subjectOptions.value[0]?.value as number
}
}
})
return {
@ -201,18 +191,18 @@ export const useHomeStore = defineStore(
selectedClassId,
selectedExamId,
selectedSubjectId,
selectedExamSubjectId,
selectedGradeKey,
// 内部数据映射
classGradeMap,
// 方法
fetchOptions,
onClassChange,
onExamChange,
setOptions,
clearSelections,
reset,
}
},
{
persist: true,
},
)

View File

@ -18,6 +18,7 @@ import UniPlatform from '@uni-helper/vite-plugin-uni-platform'
import Optimization from '@uni-ku/bundle-optimizer'
import dayjs from 'dayjs'
import { visualizer } from 'rollup-plugin-visualizer'
import { UniEchartsResolver } from 'uni-echarts/resolver'
import UnoCSS from 'unocss/vite'
import AutoImport from 'unplugin-auto-import/vite'
import { defineConfig, loadEnv } from 'vite'
@ -90,6 +91,9 @@ export default ({ command, mode }) => {
dts: 'src/types/auto-import.d.ts',
dirs: ['src/hooks'], // 自动导入 hooks
vueTemplate: true, // default false
resolvers: [
UniEchartsResolver(),
],
}),
// Optimization 插件需要 page.json 文件,故应在 UniPages 插件之后执行
Optimization({
@ -131,6 +135,9 @@ export default ({ command, mode }) => {
deep: true, // 是否递归扫描子目录,
directoryAsNamespace: false, // 是否把目录名作为命名空间前缀true 时组件名为 目录名+组件名,
dts: 'src/types/components.d.ts', // 自动生成的组件类型声明文件路径(用于 TypeScript 支持)
resolvers: [
UniEchartsResolver(),
],
}),
UniPolyfill(),
Uni(),