fix: 已有接口对接完成
continuous-integration/drone/push Build is failing
Details
continuous-integration/drone/push Build is failing
Details
This commit is contained in:
parent
356558c15c
commit
95e05b37d6
|
|
@ -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",
|
||||
|
|
|
|||
229
pnpm-lock.yaml
229
pnpm-lock.yaml
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
@ -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>
|
||||
|
||||
<!-- 取消按钮 -->
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
// 选中的科目ID,0表示全科
|
||||
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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
Loading…
Reference in New Issue