feat: 提交一些
continuous-integration/drone/push Build is failing Details

This commit is contained in:
张哲铜 2025-08-30 12:29:31 +08:00
parent 3d8948a8ff
commit 07b1d94196
21 changed files with 5262 additions and 122 deletions

512
.cursor/rules/riper5.mdc Normal file
View File

@ -0,0 +1,512 @@
---
description:
globs:
alwaysApply: false
---
## RIPER-5
### 背景介绍
你是Claude 3.7集成在Cursor IDE中Cursor是基于AI的VS Code分支。由于你的高级功能你往往过于急切经常在没有明确请求的情况下实施更改通过假设你比用户更了解情况而破坏现有逻辑。这会导致对代码的不可接受的灾难性影响。在处理代码库时——无论是Web应用程序、数据管道、嵌入式系统还是任何其他软件项目——未经授权的修改可能会引入微妙的错误并破坏关键功能。为防止这种情况你必须遵循这个严格的协议。
语言设置:除非用户另有指示,所有常规交互响应都应该使用中文。然而,模式声明(例如\[MODE: RESEARCH\])和特定格式化输出(例如代码块、清单等)应保持英文,以确保格式一致性。
### 元指令:模式声明要求
你必须在每个响应的开头用方括号声明你当前的模式。没有例外。
格式:\[MODE: MODE\_NAME\]
未能声明你的模式是对协议的严重违反。
初始默认模式除非另有指示你应该在每次新对话开始时处于RESEARCH模式。
### 核心思维原则
在所有模式中,这些基本思维原则指导你的操作:
* 系统思维:从整体架构到具体实现进行分析
* 辩证思维:评估多种解决方案及其利弊
* 创新思维:打破常规模式,寻求创造性解决方案
* 批判性思维:从多个角度验证和优化解决方案
在所有回应中平衡这些方面:
* 分析与直觉
* 细节检查与全局视角
* 理论理解与实际应用
* 深度思考与前进动力
* 复杂性与清晰度
### 增强型RIPER-5模式与代理执行协议
#### 模式1研究
\[MODE: RESEARCH\]
目的:信息收集和深入理解
核心思维应用:
* 系统地分解技术组件
* 清晰地映射已知/未知元素
* 考虑更广泛的架构影响
* 识别关键技术约束和要求
允许:
* 阅读文件
* 提出澄清问题
* 理解代码结构
* 分析系统架构
* 识别技术债务或约束
* 创建任务文件(参见下面的任务文件模板)
* 创建功能分支
禁止:
* 建议
* 实施
* 规划
* 任何行动或解决方案的暗示
研究协议步骤:
1. 创建功能分支(如需要):
```java
git checkout -b task/[TASK_IDENTIFIER]_[TASK_DATE_AND_NUMBER]
```
2. 创建任务文件(如需要):
```java
mkdir -p .tasks && touch ".tasks/${TASK_FILE_NAME}_[TASK_IDENTIFIER].md"
```
3. 分析与任务相关的代码:
* 识别核心文件/功能
* 追踪代码流程
* 记录发现以供以后使用
思考过程:
```java
嗯... [具有系统思维方法的推理过程]
```
输出格式:
以\[MODE: RESEARCH\]开始,然后只有观察和问题。
使用markdown语法格式化答案。
除非明确要求,否则避免使用项目符号。
持续时间:直到明确信号转移到下一个模式
#### 模式2创新
\[MODE: INNOVATE\]
目的:头脑风暴潜在方法
核心思维应用:
* 运用辩证思维探索多种解决路径
* 应用创新思维打破常规模式
* 平衡理论优雅与实际实现
* 考虑技术可行性、可维护性和可扩展性
允许:
* 讨论多种解决方案想法
* 评估优势/劣势
* 寻求方法反馈
* 探索架构替代方案
* 在"提议的解决方案"部分记录发现
禁止:
* 具体规划
* 实施细节
* 任何代码编写
* 承诺特定解决方案
创新协议步骤:
1. 基于研究分析创建计划:
* 研究依赖关系
* 考虑多种实施方法
* 评估每种方法的优缺点
* 添加到任务文件的"提议的解决方案"部分
2. 尚未进行代码更改
思考过程:
```java
嗯... [具有创造性、辩证方法的推理过程]
```
输出格式:
以\[MODE: INNOVATE\]开始,然后只有可能性和考虑因素。
以自然流畅的段落呈现想法。
保持不同解决方案元素之间的有机联系。
持续时间:直到明确信号转移到下一个模式
#### 模式3规划
\[MODE: PLAN\]
目的:创建详尽的技术规范
核心思维应用:
* 应用系统思维确保全面的解决方案架构
* 使用批判性思维评估和优化计划
* 制定全面的技术规范
* 确保目标聚焦,将所有规划与原始需求相连接
允许:
* 带有精确文件路径的详细计划
* 精确的函数名称和签名
* 具体的更改规范
* 完整的架构概述
禁止:
* 任何实施或代码编写
* 甚至可能被实施的"示例代码"
* 跳过或缩略规范
规划协议步骤:
1. 查看"任务进度"历史(如果存在)
2. 详细规划下一步更改
3. 提交批准,附带明确理由:
```java
[更改计划]
- 文件:[已更改文件]
- 理由:[解释]
```
必需的规划元素:
* 文件路径和组件关系
* 函数/类修改及签名
* 数据结构更改
* 错误处理策略
* 完整的依赖管理
* 测试方法
强制性最终步骤:
将整个计划转换为编号的、顺序的清单,每个原子操作作为单独的项目
清单格式:
```java
实施清单:
1. [具体行动1]
2. [具体行动2]
...
n. [最终行动]
```
输出格式:
以\[MODE: PLAN\]开始,然后只有规范和实施细节。
使用markdown语法格式化答案。
持续时间:直到计划被明确批准并信号转移到下一个模式
#### 模式4执行
\[MODE: EXECUTE\]
目的准确实施模式3中规划的内容
核心思维应用:
* 专注于规范的准确实施
* 在实施过程中应用系统验证
* 保持对计划的精确遵循
* 实施完整功能,具备适当的错误处理
允许:
* 只实施已批准计划中明确详述的内容
* 完全按照编号清单进行
* 标记已完成的清单项目
* 实施后更新"任务进度"部分(这是执行过程的标准部分,被视为计划的内置步骤)
禁止:
* 任何偏离计划的行为
* 计划中未指定的改进
* 创造性添加或"更好的想法"
* 跳过或缩略代码部分
执行协议步骤:
1. 完全按照计划实施更改
2. 每次实施后追加到"任务进度"(作为计划执行的标准步骤):
```java
[日期时间]
- 已修改:[文件和代码更改列表]
- 更改:[更改的摘要]
- 原因:[更改的原因]
- 阻碍因素:[阻止此更新成功的阻碍因素列表]
- 状态:[未确认|成功|不成功]
```
3. 要求用户确认:“状态:成功/不成功?”
4. 如果不成功返回PLAN模式
5. 如果成功且需要更多更改:继续下一项
6. 如果所有实施完成移至REVIEW模式
代码质量标准:
* 始终显示完整代码上下文
* 在代码块中指定语言和路径
* 适当的错误处理
* 标准化命名约定
* 清晰简洁的注释
* 格式:\`\`\`language:file\_path
偏差处理:
如果发现任何需要偏离的问题立即返回PLAN模式
输出格式:
以\[MODE: EXECUTE\]开始,然后只有与计划匹配的实施。
包括正在完成的清单项目。
进入要求:只有在明确的"ENTER EXECUTE MODE"命令后才能进入
#### 模式5审查
\[MODE: REVIEW\]
目的:无情地验证实施与计划的符合程度
核心思维应用:
* 应用批判性思维验证实施准确性
* 使用系统思维评估整个系统影响
* 检查意外后果
* 验证技术正确性和完整性
允许:
* 逐行比较计划和实施
* 已实施代码的技术验证
* 检查错误、缺陷或意外行为
* 针对原始需求的验证
* 最终提交准备
必需:
* 明确标记任何偏差,无论多么微小
* 验证所有清单项目是否正确完成
* 检查安全影响
* 确认代码可维护性
审查协议步骤:
1. 根据计划验证所有实施
2. 如果成功完成:
a. 暂存更改(排除任务文件):
```java
git add --all :!.tasks/*
```
b. 提交消息:
```java
git commit -m "[提交消息]"
```
3. 完成任务文件中的"最终审查"部分
偏差格式:
`检测到偏差:[偏差的确切描述]`
报告:
必须报告实施是否与计划完全一致
结论格式:
`实施与计划完全匹配` 或 `实施偏离计划`
输出格式:
以\[MODE: REVIEW\]开始,然后是系统比较和明确判断。
使用markdown语法格式化。
### 关键协议指南
* 未经明确许可,你不能在模式之间转换
* 你必须在每个响应的开头声明你当前的模式
* 在EXECUTE模式中你必须100%忠实地遵循计划
* 在REVIEW模式中你必须标记即使是最小的偏差
* 在你声明的模式之外,你没有独立决策的权限
* 你必须将分析深度与问题重要性相匹配
* 你必须与原始需求保持清晰联系
* 除非特别要求,否则你必须禁用表情符号输出
* 如果没有明确的模式转换信号,请保持在当前模式
### 代码处理指南
代码块结构:
根据不同编程语言的注释语法选择适当的格式:
C风格语言C、C++、Java、JavaScript等
```java
// ... existing code ...
{
{ modifications }}
// ... existing code ...
```
Python
```java
# ... existing code ...
{
{ modifications }}
# ... existing code ...
```
HTML/XML
```java
<!-- ... existing code ... -->
{
{ modifications }}
<!-- ... existing code ... -->
```
如果语言类型不确定,使用通用格式:
```java
[... existing code ...]
{
{ modifications }}
[... existing code ...]
```
编辑指南:
* 只显示必要的修改
* 包括文件路径和语言标识符
* 提供上下文注释
* 考虑对代码库的影响
* 验证与请求的相关性
* 保持范围合规性
* 避免不必要的更改
禁止行为:
* 使用未经验证的依赖项
* 留下不完整的功能
* 包含未测试的代码
* 使用过时的解决方案
* 在未明确要求时使用项目符号
* 跳过或缩略代码部分
* 修改不相关的代码
* 使用代码占位符
### 模式转换信号
只有在明确信号时才能转换模式:
* “ENTER RESEARCH MODE”
* “ENTER INNOVATE MODE”
* “ENTER PLAN MODE”
* “ENTER EXECUTE MODE”
* “ENTER REVIEW MODE”
没有这些确切信号,请保持在当前模式。
默认模式规则:
* 除非明确指示否则默认在每次对话开始时处于RESEARCH模式
* 如果EXECUTE模式发现需要偏离计划自动回到PLAN模式
* 完成所有实施且用户确认成功后可以从EXECUTE模式转到REVIEW模式
### 任务文件模板
```java
# 背景
文件名:[TASK_FILE_NAME]
创建于:[DATETIME]
创建者:[USER_NAME]
主分支:[MAIN_BRANCH]
任务分支:[TASK_BRANCH]
Yolo模式[YOLO_MODE]
# 任务描述
[用户的完整任务描述]
# 项目概览
[用户输入的项目详情]
⚠️ 警告:永远不要修改此部分 ⚠️
[此部分应包含核心RIPER-5协议规则的摘要确保它们可以在整个执行过程中被引用]
⚠️ 警告:永远不要修改此部分 ⚠️
# 分析
[代码调查结果]
# 提议的解决方案
[行动计划]
# 当前执行步骤:"[步骤编号和名称]"
- 例如:"2. 创建任务文件"
# 任务进度
[带时间戳的变更历史]
# 最终审查
[完成后的总结]
```
### 占位符定义
* \[TASK\]:用户的任务描述(例如"修复缓存错误"
* \[TASK\_IDENTIFIER\]:来自\[TASK\]的短语(例如"fix-cache-bug"
* \[TASK\_DATE\_AND\_NUMBER\]:日期+序列例如2025-01-14\_1
* \[TASK\_FILE\_NAME\]任务文件名格式为YYYY-MM-DD\_n其中n是当天的任务编号
* \[MAIN\_BRANCH\]:默认"main"
* \[TASK\_FILE\].tasks/\[TASK\_FILE\_NAME\]\_\[TASK\_IDENTIFIER\].md
* \[DATETIME\]当前日期和时间格式为YYYY-MM-DD\_HH:MM:SS
* \[DATE\]当前日期格式为YYYY-MM-DD
* \[TIME\]当前时间格式为HH:MM:SS
* \[USER\_NAME\]:当前系统用户名
* \[COMMIT\_MESSAGE\]:任务进度摘要
* \[SHORT\_COMMIT\_MESSAGE\]:缩写的提交消息
* \[CHANGED\_FILES\]:修改文件的空格分隔列表
* \[YOLO\_MODE\]Yolo模式状态Ask|On|Off控制是否需要用户确认每个执行步骤
* Ask在每个步骤之前询问用户是否需要确认
* On不需要用户确认自动执行所有步骤高风险模式
* Off默认模式要求每个重要步骤的用户确认
### 跨平台兼容性注意事项
* 上面的shell命令示例主要基于Unix/Linux环境
* 在Windows环境中你可能需要使用PowerShell或CMD等效命令
* 在任何环境中,你都应该首先确认命令的可行性,并根据操作系统进行相应调整
### 性能期望
* 响应延迟应尽量减少理想情况下≤30000ms
* 最大化计算能力和令牌限制
* 寻求关键洞见而非表面列举
* 追求创新思维而非习惯性重复
* 突破认知限制,调动所有计算资源

View File

@ -7,11 +7,6 @@ settings:
overrides:
bin-wrapper: npm:bin-wrapper-china
patchedDependencies:
'@dcloudio/uni-h5':
hash: 5763725268e9a493075be3c82d91b590b9ae30e997a04a418494dd7d6d327b9d
path: patches/@dcloudio__uni-h5.patch
importers:
.:
@ -36,7 +31,7 @@ importers:
version: 3.0.0-4070520250711001(@vueuse/core@13.6.0(vue@3.4.21(typescript@5.9.2)))(postcss@8.5.6)(rollup@4.46.2)(vue@3.4.21(typescript@5.9.2))
'@dcloudio/uni-h5':
specifier: 3.0.0-4070520250711001
version: 3.0.0-4070520250711001(patch_hash=5763725268e9a493075be3c82d91b590b9ae30e997a04a418494dd7d6d327b9d)(@vueuse/core@13.6.0(vue@3.4.21(typescript@5.9.2)))(postcss@8.5.6)(rollup@4.46.2)(vue@3.4.21(typescript@5.9.2))
version: 3.0.0-4070520250711001(@vueuse/core@13.6.0(vue@3.4.21(typescript@5.9.2)))(postcss@8.5.6)(rollup@4.46.2)(vue@3.4.21(typescript@5.9.2))
'@dcloudio/uni-mp-alipay':
specifier: 3.0.0-4070520250711001
version: 3.0.0-4070520250711001(@vueuse/core@13.6.0(vue@3.4.21(typescript@5.9.2)))(postcss@8.5.6)(rollup@4.46.2)(vue@3.4.21(typescript@5.9.2))
@ -82,6 +77,9 @@ importers:
dayjs:
specifier: 1.11.10
version: 1.11.10
echarts:
specifier: ^6.0.0
version: 6.0.0
js-cookie:
specifier: ^3.0.5
version: 3.0.5
@ -1239,6 +1237,7 @@ packages:
'@esbuild/darwin-arm64@0.20.2':
resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==}
engines: {node: '>=12'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-arm64@0.23.1':
@ -1256,6 +1255,7 @@ packages:
'@esbuild/darwin-x64@0.20.2':
resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==}
engines: {node: '>=12'}
cpu: [x64]
os: [darwin]
'@esbuild/darwin-x64@0.23.1':
@ -2130,6 +2130,7 @@ packages:
'@rollup/rollup-darwin-x64@4.46.2':
resolution: {integrity: sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==}
cpu: [x64]
os: [darwin]
'@rollup/rollup-freebsd-arm64@4.46.2':
@ -3516,6 +3517,9 @@ packages:
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
echarts@6.0.0:
resolution: {integrity: sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==}
ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
@ -6166,6 +6170,9 @@ packages:
ts-morph@25.0.1:
resolution: {integrity: sha512-QJEiTdnz1YjrB3JFhd626gX4rKHDLSjSVMvGGG4v7ONc3RBwa0Eei98G9AT9uNFDMtV54JyuXsFeC+OH0n6bXQ==}
tslib@2.3.0:
resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
@ -6673,6 +6680,9 @@ packages:
resolution: {integrity: sha512-RcDeKFoCQB51dmrrTb1PMIazjTqGuAbFmjPS0/N5hdUNTCRvxGOOBTBFolvIxUcsWhrocI9C0mYDgUwXT6Dwcg==}
engines: {HBuilderX: ^3.0.7}
zrender@6.0.0:
resolution: {integrity: sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==}
zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
@ -7913,7 +7923,7 @@ snapshots:
'@dcloudio/uni-components@3.0.0-4070520250711001(@vueuse/core@13.6.0(vue@3.4.21(typescript@5.9.2)))(postcss@8.5.6)(rollup@4.46.2)(vue@3.4.21(typescript@5.9.2))':
dependencies:
'@dcloudio/uni-cloud': 3.0.0-4070520250711001(@vueuse/core@13.6.0(vue@3.4.21(typescript@5.9.2)))(postcss@8.5.6)(rollup@4.46.2)(vue@3.4.21(typescript@5.9.2))
'@dcloudio/uni-h5': 3.0.0-4070520250711001(patch_hash=5763725268e9a493075be3c82d91b590b9ae30e997a04a418494dd7d6d327b9d)(@vueuse/core@13.6.0(vue@3.4.21(typescript@5.9.2)))(postcss@8.5.6)(rollup@4.46.2)(vue@3.4.21(typescript@5.9.2))
'@dcloudio/uni-h5': 3.0.0-4070520250711001(@vueuse/core@13.6.0(vue@3.4.21(typescript@5.9.2)))(postcss@8.5.6)(rollup@4.46.2)(vue@3.4.21(typescript@5.9.2))
'@dcloudio/uni-i18n': 3.0.0-4070520250711001
transitivePeerDependencies:
- '@nuxt/kit'
@ -7966,7 +7976,7 @@ snapshots:
transitivePeerDependencies:
- vue
'@dcloudio/uni-h5@3.0.0-4070520250711001(patch_hash=5763725268e9a493075be3c82d91b590b9ae30e997a04a418494dd7d6d327b9d)(@vueuse/core@13.6.0(vue@3.4.21(typescript@5.9.2)))(postcss@8.5.6)(rollup@4.46.2)(vue@3.4.21(typescript@5.9.2))':
'@dcloudio/uni-h5@3.0.0-4070520250711001(@vueuse/core@13.6.0(vue@3.4.21(typescript@5.9.2)))(postcss@8.5.6)(rollup@4.46.2)(vue@3.4.21(typescript@5.9.2))':
dependencies:
'@dcloudio/uni-h5-vite': 3.0.0-4070520250711001(@vueuse/core@13.6.0(vue@3.4.21(typescript@5.9.2)))(postcss@8.5.6)(rollup@4.46.2)(vue@3.4.21(typescript@5.9.2))
'@dcloudio/uni-h5-vue': 3.0.0-4070520250711001(vue@3.4.21(typescript@5.9.2))
@ -10879,6 +10889,11 @@ snapshots:
eastasianwidth@0.2.0: {}
echarts@6.0.0:
dependencies:
tslib: 2.3.0
zrender: 6.0.0
ee-first@1.1.1: {}
electron-to-chromium@1.5.194: {}
@ -14084,6 +14099,8 @@ snapshots:
'@ts-morph/common': 0.26.1
code-block-writer: 13.0.3
tslib@2.3.0: {}
tslib@2.8.1: {}
tsx@4.20.3:
@ -14616,4 +14633,8 @@ snapshots:
z-paging@2.8.7: {}
zrender@6.0.0:
dependencies:
tslib: 2.3.0
zwitch@2.0.4: {}

View File

@ -0,0 +1,259 @@
<script lang="ts" setup>
import type * as API from '@/service/types'
import * as echarts from 'echarts'
import { computed, onMounted, ref, watch } from 'vue'
import { teacherAnalysisTrendUsingPost } from '@/service/laoshichengjifenxi'
import { useHomeStore } from '@/store/home'
//
type TrendInfo = API.TrendInfo
const props = defineProps<{
selectedSubjectId: number
compareClassId: number | null
}>()
const emit = defineEmits<{
openCompareClassDialog: []
}>()
// 使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) {
return '选择对比班级'
}
const cls = homeStore.classOptions.find(item => item.value === props.compareClassId)
return cls?.label || '选择对比班级'
})
//
async function fetchTrendData() {
if (!homeStore.selectedClassId || !homeStore.selectedGradeKey) {
return
}
try {
loading.value = true
//
const classResponse = await teacherAnalysisTrendUsingPost({
body: {
class_key: homeStore.selectedClassId,
grade_key: homeStore.selectedGradeKey,
subject_id: props.selectedSubjectId || undefined,
top_n: 50,
},
})
if (classResponse) {
classTrendData.value = classResponse.trend_list || []
}
//
if (props.compareClassId) {
const compareResponse = await teacherAnalysisTrendUsingPost({
body: {
class_key: props.compareClassId,
grade_key: homeStore.selectedGradeKey,
subject_id: props.selectedSubjectId || undefined,
top_n: 50,
},
})
if (compareResponse) {
compareTrendData.value = compareResponse.trend_list || []
}
}
else {
compareTrendData.value = []
}
updateChart()
}
catch (error) {
console.error('获取走势数据失败:', error)
uni.showToast({
title: '获取数据失败',
icon: 'none',
})
}
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 classScores = classTrendData.value.map(item => item.class_avg_score || 0)
//
const gradeScores = classTrendData.value.map(item => item.grade_avg_score || 0)
//
const compareScores = compareTrendData.value.map(item => item.class_avg_score || 0)
const series: any[] = [
{
name: '本班级',
type: 'line',
data: classScores,
itemStyle: {
color: '#3b82f6',
},
},
{
name: '年级平均',
type: 'line',
data: gradeScores,
itemStyle: {
color: '#f59e0b',
},
},
]
const legendData = ['本班级', '年级平均']
//
if (props.compareClassId && compareTrendData.value.length > 0) {
series.push({
name: '对比班级',
type: 'line',
data: compareScores,
itemStyle: {
color: '#10b981',
},
})
legendData.push('对比班级')
}
return {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
},
},
legend: {
data: legendData,
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true,
},
xAxis: {
type: 'category',
data: categories,
},
yAxis: {
type: 'value',
name: '分数',
},
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()
}
})
//
defineExpose({
fetchTrendData,
})
</script>
<template>
<view class="rounded-xl bg-white p-4 shadow-sm">
<view class="mb-4 flex items-center justify-between">
<text class="text-lg text-slate-800 font-semibold">均分对比</text>
<wd-button size="small" @click="openCompareClassDialog">
{{ compareClassName }}
</wd-button>
</view>
<!-- 图表容器 -->
<view
ref="chartRef"
class="w-full"
/>
<!-- 暂无数据提示 -->
<view v-if="classTrendData.length === 0 && !loading" class="py-8 text-center">
<text class="text-sm text-gray-500">暂无数据</text>
</view>
<!-- 加载状态 -->
<view v-if="loading" class="h-80 flex items-center justify-center">
<wd-loading size="24" />
</view>
</view>
</template>
<style lang="scss" scoped>
//
[ref='chartRef'] {
width: 100%;
height: 200px;
}
</style>

View File

@ -0,0 +1,130 @@
<script lang="ts" setup>
import { computed, ref } from 'vue'
// 使
interface ExamQuestionItem {
examId: number
examName: string
totalQuestions: number
lowScoreQuestions: number
}
const props = defineProps<{
selectedSubjectId: number
}>()
// -
const examQuestionData = ref<ExamQuestionItem[]>([
{
examId: 1,
examName: '初三年级期末考试',
totalQuestions: 24,
lowScoreQuestions: 22,
},
{
examId: 2,
examName: '初三年级期中考试',
totalQuestions: 26,
lowScoreQuestions: 18,
},
{
examId: 3,
examName: '初三年级月考二',
totalQuestions: 20,
lowScoreQuestions: 15,
},
])
//
const filteredExamData = computed(() => {
// TODO: props.selectedSubjectId
return examQuestionData.value
})
//
function handleViewDetail(examId: number) {
// TODO:
console.log('查看详情:', examId)
uni.navigateTo({
url: `/pages/class-analysis/question-detail?examId=${examId}`,
})
}
//
function getLowScorePercentage(item: ExamQuestionItem): number {
if (item.totalQuestions === 0)
return 0
return Math.round((item.lowScoreQuestions / item.totalQuestions) * 100)
}
//
function getLowScoreColor(percentage: number): string {
if (percentage >= 80)
return 'text-red-600'
if (percentage >= 60)
return 'text-orange-600'
if (percentage >= 40)
return 'text-yellow-600'
return 'text-green-600'
}
</script>
<template>
<view class="rounded-xl bg-white p-4 shadow-sm">
<view class="mb-4">
<text class="text-lg text-slate-800 font-semibold">考试小题概览</text>
</view>
<!-- 考试列表 -->
<view class="space-y-3">
<view
v-for="item in filteredExamData"
:key="item.examId"
class="border border-gray-200 rounded-lg p-4"
>
<view class="flex items-center justify-between">
<view class="flex-1">
<!-- 考试名称 -->
<view class="mb-2">
<text class="text-xs text-gray-800 font-medium">{{ item.examName }}</text>
</view>
<!-- 小题统计信息 -->
<view class="flex items-center gap-1 text-gray-600" style="font-size: 12px;">
<text>本次考试共</text>
<text class="text-blue-600 font-medium">{{ item.totalQuestions }}</text>
<text>个小题其中</text>
<text
class="font-medium"
:class="getLowScoreColor(getLowScorePercentage(item))"
>
{{ item.lowScoreQuestions }}
</text>
<text>个小题得分率低于年级</text>
</view>
</view>
<!-- 查看详情按钮 -->
<view class="ml-4">
<wd-button
type="primary"
size="small"
@click="handleViewDetail(item.examId)"
>
查看详情
</wd-button>
</view>
</view>
</view>
</view>
<!-- 暂无数据提示 -->
<view v-if="filteredExamData.length === 0" class="py-8 text-center">
<text class="text-sm text-gray-500">暂无考试数据</text>
</view>
</view>
</template>
<style lang="scss" scoped>
//
</style>

View File

@ -81,6 +81,38 @@
"navigationStyle": "custom"
}
},
{
"path": "pages/class-analysis/index",
"type": "page",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "班级分析"
}
},
{
"path": "pages/class-analysis/question-detail",
"type": "page",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "小题详情"
}
},
{
"path": "pages/index/score-list",
"type": "page",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "成绩单"
}
},
{
"path": "pages/index/score",
"type": "page",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "成绩分析"
}
},
{
"path": "pages/marking/detail",
"type": "page",

View File

@ -0,0 +1,192 @@
<route lang="jsonc">
{
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "班级分析"
}
}
</route>
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue'
import AverageScoreChart from '@/components/class-analysis/AverageScoreChart.vue'
import ExamQuestionOverview from '@/components/class-analysis/ExamQuestionOverview.vue'
import { useHomeStore } from '@/store/home'
defineOptions({
name: 'ClassAnalysisPage',
})
// 使store
const homeStore = useHomeStore()
// ID0
const selectedSubjectId = ref(0)
// ID
const compareClassId = ref<number | null>(null)
//
const showCompareClassDialog = ref(false)
//
const chartRef = ref<InstanceType<typeof AverageScoreChart>>()
//
const classOptions = computed(() => homeStore.classOptions.map(cls => ({
label: cls.label,
value: cls.value,
})))
//
const subjectTabs = computed(() => {
const tabs = [
{ name: 'all', title: '全科', subjectId: 0 },
]
homeStore.subjectOptions.forEach((subject) => {
tabs.push({
name: `subject-${subject.value}`,
title: subject.label,
subjectId: subject.value,
})
})
return tabs
})
//
const activeTab = computed({
get: () => selectedSubjectId.value === 0 ? 'all' : `subject-${selectedSubjectId.value}`,
set: (value) => {
if (value === 'all') {
selectedSubjectId.value = 0
}
else {
const subjectId = Number.parseInt(value.replace('subject-', ''))
selectedSubjectId.value = subjectId
}
},
})
//
function goBack() {
uni.navigateBack()
}
//
function handleTabClick({ name }: { name: string }) {
activeTab.value = name
}
//
function openCompareClassDialog() {
showCompareClassDialog.value = true
}
//
function handleCompareClassChange(classId: number) {
compareClassId.value = classId
showCompareClassDialog.value = false
}
//
onMounted(async () => {
try {
// store
if (homeStore.classOptions.length === 0) {
await homeStore.fetchOptions()
}
}
catch (error) {
console.error('初始化数据失败:', error)
}
})
</script>
<template>
<view class="class-analysis-page min-h-screen bg-slate-50">
<!-- 导航栏 -->
<wd-navbar placeholder fixed>
<template #left>
<div class="flex items-center" @tap="goBack">
<wd-icon name="arrow-left" size="20px" />
</div>
</template>
<template #title>
<text class="text-lg font-semibold">班级分析</text>
</template>
</wd-navbar>
<!-- 科目选择标签栏 -->
<wd-tabs
v-model="activeTab"
sticky
class="bg-white"
@click="handleTabClick"
>
<wd-tab
v-for="tab in subjectTabs"
:key="tab.name"
:name="tab.name"
:title="tab.title"
/>
</wd-tabs>
<!-- 主要内容区域 -->
<view class="p-4 space-y-4">
<!-- 均分对比图表组件 -->
<AverageScoreChart
ref="chartRef"
:selected-subject-id="selectedSubjectId"
:compare-class-id="compareClassId"
@open-compare-class-dialog="openCompareClassDialog"
/>
<!-- 考试小题概览组件 -->
<ExamQuestionOverview
:selected-subject-id="selectedSubjectId"
/>
</view>
<!-- 对比班级选择弹窗 -->
<wd-popup
v-model="showCompareClassDialog"
position="center"
custom-style="border-radius: 16px; width: 80%; max-width: 400px;"
>
<view class="p-6">
<!-- 标题 -->
<view class="mb-6 text-center">
<text class="text-lg text-gray-800 font-semibold">选择对比班级</text>
</view>
<!-- 班级列表 -->
<view class="max-h-80 overflow-y-auto space-y-2">
<view
v-for="cls in classOptions"
:key="cls.value"
class="border border-gray-200 rounded-lg p-3 text-center"
: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>
<!-- 取消按钮 -->
<view class="mt-6">
<wd-button type="info" class="w-full" @click="showCompareClassDialog = false">
取消
</wd-button>
</view>
</view>
</wd-popup>
</view>
</template>
<style lang="scss">
.class-analysis-page {
//
}
</style>

View File

@ -0,0 +1,152 @@
<route lang="jsonc">
{
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "小题详情"
}
}
</route>
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue'
import { useHomeStore } from '@/store/home'
defineOptions({
name: 'QuestionDetailPage',
})
//
interface QuestionDetailItem {
questionNumber: string
classScoreRate: string
gradeScoreRate: string
scoreDifference: string
}
// 使store
const homeStore = useHomeStore()
// ID
const selectedExamId = ref<number | null>(null)
//
const examOptions = computed(() => homeStore.examOptions)
// Mock
const questionDetailData = ref<QuestionDetailItem[]>([
{
questionNumber: '一1',
classScoreRate: '16%',
gradeScoreRate: '42%',
scoreDifference: '-26%',
},
{
questionNumber: '一2',
classScoreRate: '16%',
gradeScoreRate: '16%',
scoreDifference: '0%',
},
{
questionNumber: '一3',
classScoreRate: '42%',
gradeScoreRate: '16%',
scoreDifference: '26%',
},
])
//
function goBack() {
uni.navigateBack()
}
//
function handleExamChange(event: { value: string | number, selectedItem: Record<string, any> }) {
selectedExamId.value = Number(event.value)
// TODO: ID
console.log('选中考试ID:', event.value)
}
//
function viewDetail(item: QuestionDetailItem) {
// TODO:
console.log('查看详情:', item)
uni.showToast({
title: '功能开发中',
icon: 'none',
})
}
//
onMounted(async () => {
try {
// store
if (homeStore.examOptions.length === 0) {
await homeStore.fetchOptions()
}
//
if (homeStore.examOptions.length > 0 && !selectedExamId.value) {
selectedExamId.value = homeStore.examOptions[0].value
}
}
catch (error) {
console.error('初始化数据失败:', error)
}
})
</script>
<template>
<view class="question-detail-page min-h-screen bg-slate-50">
<!-- 导航栏 -->
<wd-navbar placeholder fixed>
<template #left>
<div class="flex items-center" @tap="goBack">
<wd-icon name="arrow-left" size="20px" />
</div>
</template>
<template #title>
<div class="flex items-center justify-center">
<text class="text-xs">小题详情 </text>
<wd-drop-menu>
<wd-drop-menu-item
v-model="selectedExamId"
:options="examOptions"
@change="handleExamChange"
/>
</wd-drop-menu>
</div>
</template>
</wd-navbar>
<!-- 主要内容区域 -->
<view class="p-4">
<!-- 小题详情表格 -->
<wd-table :data="questionDetailData" :height="400">
<wd-table-col prop="questionNumber" label="题号" :width="`${20}%`" fixed />
<wd-table-col prop="classScoreRate" label="班级得分率" :width="`${20}%`" />
<wd-table-col prop="gradeScoreRate" label="年级得分率" :width="`${20}%`" />
<wd-table-col prop="scoreDifference" label="得分率差值" :width="`${20}%`">
<template #value="{ row }">
<text
:class="[
row.scoreDifference.startsWith('-') ? 'text-red-500'
: row.scoreDifference === '0%' ? 'text-gray-600' : 'text-green-500',
]"
>
{{ row.scoreDifference }}
</text>
</template>
</wd-table-col>
<wd-table-col prop="detail" label="详情" :width="`${20}%`" align="center">
<template #value="{ row }">
<wd-icon
name="view"
size="18px"
class="cursor-pointer text-blue-500"
@tap="viewDetail(row)"
/>
</template>
</wd-table-col>
</wd-table>
</view>
</view>
</template>

View File

@ -11,7 +11,9 @@
</route>
<script lang="ts" setup>
import { ref } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { teacherAnalysisRecentExamStatsUsingPost } from '@/service/laoshichengjifenxi'
import { useHomeStore } from '@/store/home'
defineOptions({
name: 'Home',
@ -40,15 +42,55 @@ systemInfo = uni.getSystemInfoSync()
safeAreaInsets = systemInfo.safeAreaInsets
// #endif
//
// 使store
const homeStore = useHomeStore()
// 线
const scoreSettings = ref({
excellent_scoring_rate: 85,
good_scoring_rate: 75,
pass_scoring_rate: 60,
top_n: 50,
})
//
const statsData = ref({
classCount: 0,
totalCount: 0,
ranking: 0,
totalRanking: 0,
top50Count: 0,
top50Total: scoreSettings.value.top_n,
highestScore: 0,
lowestScore: 0,
excellentRate: 0,
goodRate: 0,
passRate: 0,
})
const loading = ref(false)
//
const showSettingsDialog = ref(false)
const tempSettings = ref({
excellent_scoring_rate: scoreSettings.value.excellent_scoring_rate,
good_scoring_rate: scoreSettings.value.good_scoring_rate,
pass_scoring_rate: scoreSettings.value.pass_scoring_rate,
top_n: scoreSettings.value.top_n,
})
// TODO: store
const schoolName = ref('华师一附中')
const teacherName = ref('张信哲')
const teacherSubject = ref('语文')
const className = ref('初三(1)班班主任')
//
const examOptions = ['初三年级摸底月考', '期中考试', '期末考试']
const selectedExam = ref(examOptions[0])
//
const selectedClassName = computed(() => {
if (!homeStore.selectedClassId) {
return '请选择班级'
}
const selectedClass = homeStore.classOptions.find(item => item.value === homeStore.selectedClassId)
return selectedClass?.label || '请选择班级'
})
//
const menuItems = [
@ -82,26 +124,60 @@ const menuItems = [
},
]
//
const statsData = {
classCount: 53,
totalCount: 1123,
ranking: 2,
totalRanking: 11,
top50Count: 11,
top50Total: 50,
highestScore: 98,
lowestScore: 45,
excellentRate: 12,
goodRate: 65,
passRate: 53,
//
async function fetchExamStats() {
if (!homeStore.selectedClassId || !homeStore.selectedExamId) {
return
}
try {
loading.value = true
const response = await teacherAnalysisRecentExamStatsUsingPost({
body: {
class_key: homeStore.selectedClassId,
exam_id: homeStore.selectedExamId,
grade_key: homeStore.selectedClassId, // 使class_keygrade_key
excellent_scoring_rate: scoreSettings.value.excellent_scoring_rate,
good_scoring_rate: scoreSettings.value.good_scoring_rate,
pass_scoring_rate: scoreSettings.value.pass_scoring_rate,
top_n: scoreSettings.value.top_n,
},
})
if (response) {
//
statsData.value = {
classCount: response.class_total_count || 0,
totalCount: response.grade_total_count || 0,
ranking: response.average_score_rank || 0,
totalRanking: response.class_count || 0,
top50Count: response.grade_top_n || 0,
top50Total: scoreSettings.value.top_n,
highestScore: response.class_highest_score || 0,
lowestScore: response.class_lowest_score || 0,
excellentRate: Math.round((response.excellent_class_scoring_rate || 0) * 100),
goodRate: Math.round((response.good_class_scoring_rate || 0) * 100),
passRate: Math.round((response.pass_class_scoring_rate || 0) * 100),
}
}
}
catch (error) {
console.error('获取统计数据失败:', error)
uni.showToast({
title: '获取数据失败',
icon: 'none',
})
}
finally {
loading.value = false
}
}
//
function handleMenuClick(item: any) {
if (item.id === 'grade') {
uni.navigateTo({
url: '/pages-sub/grade/index',
url: '/pages/index/score',
})
}
else if (item.id === 'marking') {
@ -116,52 +192,181 @@ function handleMenuClick(item: any) {
}
else if (item.id === 'class-analysis') {
uni.navigateTo({
url: '/pages-sub/class-analysis/index',
url: '/pages/class-analysis/index',
})
}
}
//
function handleExamChange() {
const examNames = homeStore.examOptions.map(item => item.label)
if (examNames.length === 0) {
uni.showToast({
title: '暂无考试数据',
icon: 'none',
})
return
}
uni.showActionSheet({
itemList: examOptions,
itemList: examNames,
success: (res) => {
selectedExam.value = examOptions[res.tapIndex]
const selectedOption = homeStore.examOptions[res.tapIndex]
homeStore.selectedExamId = selectedOption.value as number
//
fetchExamStats()
},
})
}
//
const showClassPicker = ref(false)
//
function handleClassChange() {
if (homeStore.classOptions.length === 0) {
uni.showToast({
title: '暂无班级数据',
icon: 'none',
})
return
}
showClassPicker.value = true
}
//
function onClassSelectAction(action: { name: string, value: number }) {
homeStore.onClassChange(action.value)
//
fetchExamStats()
//
showClassPicker.value = false
}
//
function handleSettings() {
uni.showModal({
title: '设置参数',
content: '是否设置优秀率等参数?',
success: (res) => {
if (res.confirm) {
uni.showToast({
title: '设置功能开发中',
icon: 'none',
})
}
//
tempSettings.value = { ...scoreSettings.value }
showSettingsDialog.value = true
}
//
function confirmSettings() {
//
const validations = [
{
condition: tempSettings.value.excellent_scoring_rate <= 0 || tempSettings.value.excellent_scoring_rate > 100,
message: '优秀分数线应在1-100之间',
},
{
condition: tempSettings.value.good_scoring_rate <= 0 || tempSettings.value.good_scoring_rate > 100,
message: '良好分数线应在1-100之间',
},
{
condition: tempSettings.value.pass_scoring_rate <= 0 || tempSettings.value.pass_scoring_rate > 100,
message: '及格分数线应在1-100之间',
},
{
condition: tempSettings.value.top_n <= 0 || tempSettings.value.top_n > 1000,
message: '前N名应在1-1000之间',
},
]
const failedValidation = validations.find(v => v.condition)
if (failedValidation) {
uni.showToast({
title: failedValidation.message,
icon: 'none',
})
return
}
//
scoreSettings.value = { ...tempSettings.value }
showSettingsDialog.value = false
//
fetchExamStats()
uni.showToast({
title: '设置已保存',
icon: 'success',
})
}
//
function cancelSettings() {
showSettingsDialog.value = false
}
//
function handleViewDetail() {
uni.showToast({
title: '查看详情功能开发中',
icon: 'none',
if (!homeStore.selectedClassId || !homeStore.selectedExamId) {
uni.showToast({
title: '请先选择班级和考试',
icon: 'none',
})
return
}
// TODO:
uni.navigateTo({
url: `/pages-sub/detail/index?classId=${homeStore.selectedClassId}&examId=${homeStore.selectedExamId}&subjectId=${homeStore.selectedSubjectId || ''}`,
})
}
//
function handleExamReview() {
uni.showToast({
title: '试卷讲评功能开发中',
icon: 'none',
if (!homeStore.selectedClassId || !homeStore.selectedExamId) {
uni.showToast({
title: '请先选择班级和考试',
icon: 'none',
})
return
}
// TODO:
uni.navigateTo({
url: `/pages-sub/exam-review/index?classId=${homeStore.selectedClassId}&examId=${homeStore.selectedExamId}&subjectId=${homeStore.selectedSubjectId || ''}`,
})
}
//
const selectedExamName = computed(() => {
if (!homeStore.selectedExamId) {
return '请选择考试'
}
const exam = homeStore.examOptions.find(item => item.value === homeStore.selectedExamId)
return exam?.label || '请选择考试'
})
//
onMounted(async () => {
try {
//
await homeStore.fetchOptions()
//
if (homeStore.examOptions.length > 0 && homeStore.classOptions.length > 0) {
//
if (!homeStore.selectedExamId) {
homeStore.selectedExamId = homeStore.examOptions[0]?.value as number
}
if (!homeStore.selectedClassId) {
homeStore.selectedClassId = homeStore.classOptions[0]?.value as number
}
if (!homeStore.selectedSubjectId && homeStore.subjectOptions.length > 0) {
homeStore.selectedSubjectId = homeStore.subjectOptions[0]?.value as number
}
//
await fetchExamStats()
}
}
catch (error) {
console.error('初始化数据失败:', error)
}
})
</script>
<template>
@ -183,10 +388,11 @@ function handleExamReview() {
<text class="text-base text-white font-medium">{{ teacherName }}{{ teacherSubject }}</text>
</view>
<!-- 右侧班主任标识 -->
<view class="rounded-full bg-white/20 px-3 py-1">
<view class="i-mingcute:user-4-line mr-1 inline-block text-sm text-white" />
<text class="text-xs text-white">{{ className }}</text>
<!-- 右侧班级选择 -->
<view class="rounded-full bg-white/20 px-3 py-1 active:bg-white/30" @tap="handleClassChange">
<view class="i-mingcute:school-line mr-1 inline-block text-sm text-white" />
<text class="text-xs text-white">{{ selectedClassName }}</text>
<view class="i-mingcute:down-line ml-1 inline-block text-xs text-white" />
</view>
</view>
</view>
@ -219,7 +425,7 @@ function handleExamReview() {
<!-- 考试选择 -->
<view class="flex items-center justify-between border border-slate-200 rounded-xl from-slate-50 to-cyan-50 bg-gradient-to-r p-3" @tap="handleExamChange">
<text class="text-sm text-slate-700 font-medium">{{ selectedExam }}</text>
<text class="text-sm text-slate-700 font-medium">{{ selectedExamName }}</text>
<view class="i-mingcute:down-line text-slate-400" />
</view>
</view>
@ -310,5 +516,117 @@ function handleExamReview() {
</wd-button>
</view>
</view>
<!-- 参数设置弹窗 -->
<wd-popup
v-model="showSettingsDialog"
position="center"
custom-style="border-radius: 16px; width: 80%; max-width: 400px;"
:close-on-click-modal="false"
>
<view class="p-6">
<!-- 标题 -->
<view class="mb-6 text-center">
<text class="text-lg text-gray-800 font-semibold">参数设置</text>
</view>
<!-- 表单内容 -->
<view class="space-y-4">
<!-- 优秀得分率 -->
<view class="flex items-center justify-center gap-4">
<text class="text-sm text-gray-700 font-medium">优秀得分率</text>
<text class="text-sm text-gray-500"></text>
<wd-input
v-model="tempSettings.excellent_scoring_rate"
type="number"
placeholder="请输入"
class="w-20 text-center"
:border="true"
/>
<text class="text-sm text-gray-700">%</text>
</view>
<!-- 良好得分率 -->
<view class="flex items-center justify-center gap-4">
<text class="text-sm text-gray-700 font-medium">良好得分率</text>
<text class="text-sm text-gray-500"></text>
<wd-input
v-model="tempSettings.good_scoring_rate"
type="number"
placeholder="请输入"
class="w-20 text-center"
:border="true"
/>
<text class="text-sm text-gray-700">%</text>
</view>
<!-- 及格得分率 -->
<view class="flex items-center justify-center gap-4">
<text class="text-sm text-gray-700 font-medium">及格得分率</text>
<text class="text-sm text-gray-500"></text>
<wd-input
v-model="tempSettings.pass_scoring_rate"
type="number"
placeholder="请输入"
class="w-20 text-center"
:border="true"
/>
<text class="text-sm text-gray-700">%</text>
</view>
<!-- 年级前N名 -->
<view class="flex items-center justify-center gap-4">
<text class="text-sm text-gray-700 font-medium">年级前N名</text>
<text class="text-sm text-gray-500">-</text>
<wd-input
v-model="tempSettings.top_n"
type="number"
placeholder="请输入"
class="w-20 text-center"
:border="true"
/>
<text class="text-sm text-gray-700"></text>
</view>
</view>
<!-- 操作按钮 -->
<view class="mt-6 flex gap-3">
<wd-button type="info" class="flex-1" @click="cancelSettings">
取消
</wd-button>
<wd-button type="primary" class="flex-1" @click="confirmSettings">
确定
</wd-button>
</view>
</view>
</wd-popup>
<!-- 班级选择器 -->
<wd-popup v-model="showClassPicker" position="bottom">
<view class="rounded-t-3xl bg-white p-4">
<view class="mb-4 text-center text-lg font-medium">
选择班级
</view>
<view class="max-h-80 overflow-y-auto space-y-2">
<view
v-for="classItem in homeStore.classOptions"
:key="classItem.value"
class="flex items-center justify-between rounded-xl bg-gray-50 p-3 active:bg-blue-50"
@tap="onClassSelectAction({ name: classItem.label, value: classItem.value })"
>
<text class="text-base">{{ classItem.label }}</text>
<view
v-if="homeStore.selectedClassId === classItem.value"
class="i-mingcute:check-line text-lg text-blue-500"
/>
</view>
</view>
<view class="mt-4 flex justify-center">
<wd-button type="default" class="w-32" @click="showClassPicker = false">
取消
</wd-button>
</view>
</view>
</wd-popup>
</view>
</template>

View File

@ -0,0 +1,240 @@
<route lang="jsonc">
{
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "成绩单"
}
}
</route>
<script lang="ts" setup>
import type {
GetScoreSheetResponse,
ScoreSheetInfo,
} from '@/service/types'
import { computed, onMounted, ref } from 'vue'
import { teacherAnalysisScoreSheetUsingPost } from '@/service/laoshichengjifenxi'
import { useHomeStore } from '@/store/home'
defineOptions({
name: 'ScoreListPage',
})
//
interface ScoreSheetItemExtended extends ScoreSheetInfo {
subjectScores: Record<number, number> //
}
// 使store
const homeStore = useHomeStore()
//
const currentPage = ref(1)
const pageSize = ref(20)
const total = ref(0)
const loading = ref(false)
//
const scoreList = ref<ScoreSheetItemExtended[]>([])
//
const classOptions = computed(() => homeStore.classOptions)
//
const subjectOptions = computed(() => homeStore.subjectOptions)
// ID
const selectedClassId = ref<number | null>(homeStore.selectedClassId)
//
function goBack() {
uni.navigateBack()
}
//
function handleClassChange(event: { value: string | number, selectedItem: Record<string, any> }) {
selectedClassId.value = Number(event.value)
homeStore.selectedClassId = selectedClassId.value
//
currentPage.value = 1
fetchScoreList()
}
//
function handlePageChange(page: number) {
currentPage.value = page
fetchScoreList()
}
//
async function fetchScoreList() {
if (!selectedClassId.value || !homeStore.selectedExamId || !homeStore.selectedGradeKey) {
return
}
try {
loading.value = true
const response = await teacherAnalysisScoreSheetUsingPost({
body: {
class_key: selectedClassId.value,
exam_id: homeStore.selectedExamId,
grade_key: homeStore.selectedGradeKey,
page: currentPage.value,
page_size: pageSize.value,
},
})
if (response) {
const responseData = response as GetScoreSheetResponse
total.value = responseData.total || 0
//
scoreList.value = (responseData.list || []).map(item => ({
...item,
subjectScores: {}, // TODO: API
}))
}
}
catch (error) {
console.error('获取成绩单数据失败:', error)
uni.showToast({
title: '获取数据失败',
icon: 'none',
})
}
finally {
loading.value = false
}
}
//
function viewStudentReport(student: ScoreSheetItemExtended) {
// TODO:
console.log('查看学生成绩报告:', student)
uni.showToast({
title: '功能开发中',
icon: 'none',
})
}
//
function viewSubjectAnswerSheet(student: ScoreSheetItemExtended, subjectId: number) {
// TODO:
console.log('查看科目答题卡:', student, subjectId)
uni.showToast({
title: '功能开发中',
icon: 'none',
})
}
//
onMounted(async () => {
try {
// store
if (homeStore.classOptions.length === 0) {
await homeStore.fetchOptions()
}
//
if (homeStore.selectedClassId && homeStore.selectedExamId) {
selectedClassId.value = homeStore.selectedClassId
await fetchScoreList()
}
}
catch (error) {
console.error('初始化数据失败:', error)
}
})
</script>
<template>
<view class="score-list-page min-h-screen bg-slate-50">
<!-- 导航栏 -->
<wd-navbar placeholder fixed right-text="成绩单">
<template #left>
<div class="flex items-center" @tap="goBack">
<wd-icon name="arrow-left" size="20px" />
</div>
</template>
<template #title>
<wd-drop-menu>
<wd-drop-menu-item
v-model="selectedClassId"
:options="classOptions"
@change="handleClassChange"
/>
</wd-drop-menu>
</template>
</wd-navbar>
<!-- 主要内容区域 -->
<view class="p-4">
<!-- 成绩单表格 -->
<wd-table :data="scoreList" :height="600" :loading="loading">
<!-- 学生姓名列 -->
<wd-table-col prop="student_name" label="考生姓名" :width="`${20}%`" fixed>
<template #value="{ row }">
<text class="cursor-pointer text-blue-500" @tap="viewStudentReport(row)">
{{ row.student_name }}
</text>
</template>
</wd-table-col>
<!-- 总分列 -->
<wd-table-col prop="all_score" label="总分" :width="`${20}%`">
<template #value="{ row }">
<text class="cursor-pointer text-blue-500" @tap="viewStudentReport(row)">
{{ row.all_score || '--' }}
</text>
</template>
</wd-table-col>
<!-- 班次/校次列 -->
<wd-table-col prop="rank" label="班次/校次" :width="`${40}%`">
<template #value="{ row }">
<view class="text-xs">
<view>{{ row.class_rank || '--' }}</view>
<view class="text-gray-500">
{{ row.grade_rank || '--' }}
</view>
</view>
</template>
</wd-table-col>
<!-- 动态生成科目列 -->
<wd-table-col
v-for="subject in subjectOptions"
:key="subject.value"
:prop="`subject_${subject.value}`"
:label="subject.label"
:width="`${20}%`"
>
<template #value="{ row }">
<text
class="cursor-pointer text-blue-500"
@tap="viewSubjectAnswerSheet(row, subject.value)"
>
{{ row.subjectScores[subject.value] || '--' }}
</text>
</template>
</wd-table-col>
</wd-table>
<!-- 分页组件 -->
<view v-if="total > 0" class="mt-4 flex justify-center">
<wd-pagination
v-model="currentPage"
:total="total"
:page-size="pageSize"
show-icon
show-message
@change="handlePageChange"
/>
</view>
<!-- 空状态 -->
<view v-if="!loading && scoreList.length === 0" class="py-8 text-center text-gray-500">
暂无成绩数据
</view>
</view>
</view>
</template>

1127
src/pages/index/score.vue Normal file

File diff suppressed because it is too large Load Diff

28
src/service/banjifenxi.ts Normal file
View File

@ -0,0 +1,28 @@
/* eslint-disable */
// @ts-ignore
import request from '@/http/vue-query';
import { CustomRequestOptions } from '@/http/types';
import * as API from './types';
/** 获取班级对比分析数据 获取班级对比分析数据包括平均分对比、平均分排名对比、年级前N名数量对比 POST /class-comparison/data */
export async function classComparisonDataUsingPost({
body,
options,
}: {
body: API.ClassComparisonRequest;
options?: CustomRequestOptions;
}) {
return request<
API.Response & {
data?: API.ClassComparisonResponse;
}
>('/class-comparison/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}

View File

@ -2,6 +2,50 @@
// @ts-ignore
import * as API from './types';
export function displayAnalysisTypeEnum(field: API.Analysis_typeEnum) {
return {
question: 'question',
major_question: 'major_question',
objective_subjective: 'objective_subjective',
}[field];
}
export function displayAnalysisTypeEnum2(field: API.Analysis_typeEnum2) {
return {
question: 'question',
major_question: 'major_question',
objective_subjective: 'objective_subjective',
}[field];
}
export function displayEmphasisTypeEnum(field: API.Emphasis_typeEnum) {
return { up: 'up', down: 'down', key: 'key', wave: 'wave' }[field];
}
export function displayQueryModeEnum(field: API.Query_modeEnum) {
return { class: 'class', school: 'school' }[field];
}
export function displayQueryModeEnum2(field: API.Query_modeEnum2) {
return { class: 'class', school: 'school' }[field];
}
export function displayQueryModeEnum3(field: API.Query_modeEnum3) {
return { class: 'class', school: 'school' }[field];
}
export function displayQueryModeEnum4(field: API.Query_modeEnum4) {
return { class: 'class', school: 'school' }[field];
}
export function displayQueryModeEnum5(field: API.Query_modeEnum5) {
return { class: 'class', school: 'school' }[field];
}
export function displayQueryModeEnum6(field: API.Query_modeEnum6) {
return { class: 'class', school: 'school' }[field];
}
export function displayQueryModeEnum7(field: API.Query_modeEnum7) {
return { class: 'class', school: 'school' }[field];
}

View File

@ -4,14 +4,17 @@ export * from './types';
export * from './displayEnumLabel';
export * from './renzheng';
export * from './shujuzidianxiang';
export * from './shujuzidianleixing';
export * from './banjifenxi';
export * from './shujuzidixiang';
export * from './shujuzidileixing';
export * from './kaoshitongji';
export * from './wenjianguanli';
export * from './wenjianqiepianguanli';
export * from './yuejuanjindu';
export * from './yuejuanzhiliang';
export * from './yuejuanrenwujilu';
export * from './yuejuanrenwu';
export * from './wode';
export * from './xitongyonghu';
export * from './xitongxinxi';
export * from './laoshichengjifenxi';

405
src/service/kaoshitongji.ts Normal file
View File

@ -0,0 +1,405 @@
/* eslint-disable */
// @ts-ignore
import request from '@/http/vue-query';
import { CustomRequestOptions } from '@/http/types';
import * as API from './types';
/** 获取试题统计 根据条件查询学科的试题统计数据,支持按班级、学校筛选,支持多种分析类型和指标 POST /exam-statistics/average-comparison */
export async function examStatisticsAverageComparisonUsingPost({
body,
options,
}: {
body: API.QuestionAnalysisRequest;
options?: CustomRequestOptions;
}) {
return request<
API.Response & {
data?: API.QuestionAnalysisResponse;
}
>('/exam-statistics/average-comparison', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
/** 获取总分分档上线人数分布 根据条件获取总分分档上线人数分布,支持按学校或班级分组统计 POST /exam-statistics/class-analysis */
export async function examStatisticsClassAnalysisUsingPost({
body,
options,
}: {
body: API.ClassAnalysisRequest;
options?: CustomRequestOptions;
}) {
return request<
API.Response & {
data?: API.ClassAnalysisResponse;
}
>('/exam-statistics/class-analysis', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
/** 按班级导出 按班级导出成绩数据每个班级一个sheet文件名使用考试名称+成绩表 POST /exam-statistics/export-by-classes */
export async function examStatisticsExportByClassesUsingPost({
body,
options,
}: {
body: API.ClassExportRequest;
options?: CustomRequestOptions;
}) {
return request<string>('/exam-statistics/export-by-classes', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
/** 分校导出 按学校分别导出成绩数据,返回压缩包,每个学校一个文件夹 POST /exam-statistics/export-by-schools */
export async function examStatisticsExportBySchoolsUsingPost({
body,
options,
}: {
body: API.SchoolExportRequest;
options?: CustomRequestOptions;
}) {
return request<string>('/exam-statistics/export-by-schools', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
/** 导出总分分档分析Excel 导出总分分档分析数据到Excel文件 POST /exam-statistics/export-class-analysis-excel */
export async function examStatisticsExportClassAnalysisExcelUsingPost({
body,
options,
}: {
body: API.ClassAnalysisExcelExportRequest;
options?: CustomRequestOptions;
}) {
return request<string>('/exam-statistics/export-class-analysis-excel', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
/** 导出班级成绩单 导出班级成绩单ZIP文件每个班级一个Excel每个科目一个sheet包含小分排名的所有指标 POST /exam-statistics/export-class-reports */
export async function examStatisticsExportClassReportsUsingPost({
body,
options,
}: {
body: API.ClassReportExportRequest;
options?: CustomRequestOptions;
}) {
return request<string>('/exam-statistics/export-class-reports', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
/** 导出成绩排名Excel 根据条件导出学生成绩排名Excel文件支持复合表头格式 POST /exam-statistics/export-excel */
export async function examStatisticsExportExcelUsingPost({
body,
options,
}: {
body: API.ExcelExportRequest;
options?: CustomRequestOptions;
}) {
return request<string>('/exam-statistics/export-excel', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
/** 导出试题分析Excel 导出试题分析数据到Excel文件包含各题目的统计指标和对比数据 POST /exam-statistics/export-question-analysis-excel */
export async function examStatisticsExportQuestionAnalysisExcelUsingPost({
body,
options,
}: {
body: API.QuestionAnalysisExcelExportRequest;
options?: CustomRequestOptions;
}) {
return request<string>('/exam-statistics/export-question-analysis-excel', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
/** 导出题目排名Excel小分排名 根据条件导出学生题目级成绩排名Excel文件 POST /exam-statistics/export-question-ranking-excel */
export async function examStatisticsExportQuestionRankingExcelUsingPost({
body,
options,
}: {
body: API.QuestionExcelExportRequest;
options?: CustomRequestOptions;
}) {
return request<string>('/exam-statistics/export-question-ranking-excel', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
/** 导出小分表数据 导出小分表数据到Excel文件包含学生基本信息、成绩和各题目得分 POST /exam-statistics/export-question-ranking-table */
export async function examStatisticsExportQuestionRankingTableUsingPost({
body,
options,
}: {
body: API.QuestionRankingRequest;
options?: CustomRequestOptions;
}) {
return request<string>('/exam-statistics/export-question-ranking-table', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
/** 导出均分对比Excel 导出均分分析数据到Excel文件包含各班级/学校的统计指标对比 POST /exam-statistics/export-score-analysis-excel */
export async function examStatisticsExportScoreAnalysisExcelUsingPost({
body,
options,
}: {
body: API.ScoreAnalysisExcelExportRequest;
options?: CustomRequestOptions;
}) {
return request<string>('/exam-statistics/export-score-analysis-excel', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
/** 导出统计分析Excel 导出统计分析数据到Excel文件支持名次统计、分数段统计、比例段统计 POST /exam-statistics/export-statistics-analysis-excel */
export async function examStatisticsExportStatisticsAnalysisExcelUsingPost({
body,
options,
}: {
body: API.StatisticsAnalysisExcelExportRequest;
options?: CustomRequestOptions;
}) {
return request<string>('/exam-statistics/export-statistics-analysis-excel', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
/** 导出学科分档分析Excel 导出学科分档分析数据到Excel文件 POST /exam-statistics/export-subject-class-analysis-excel */
export async function examStatisticsExportSubjectClassAnalysisExcelUsingPost({
body,
options,
}: {
body: API.SubjectClassAnalysisExcelExportRequest;
options?: CustomRequestOptions;
}) {
return request<string>(
'/exam-statistics/export-subject-class-analysis-excel',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
}
);
}
/** 获取题目级排名(小分排名) 根据考试ID和学科ID查询学生的题目级成绩排名 POST /exam-statistics/question-ranking */
export async function examStatisticsQuestionRankingUsingPost({
body,
options,
}: {
body: API.QuestionRankingRequest;
options?: CustomRequestOptions;
}) {
return request<
API.Response & {
data?: API.QuestionRankingResponse;
}
>('/exam-statistics/question-ranking', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
/** 获取均分分析 根据条件查询学校或班级的均分分析数据,包括各类统计指标和排名 POST /exam-statistics/score-analysis */
export async function examStatisticsScoreAnalysisUsingPost({
body,
options,
}: {
body: API.ScoreAnalysisRequest;
options?: CustomRequestOptions;
}) {
return request<
API.Response & {
data?: API.ScoreAnalysisResponse;
}
>('/exam-statistics/score-analysis', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
/** 获取成绩排名 根据条件查询学生成绩排名,支持按班级、学校筛选,支持联考模式和赋分计算,支持按姓名、学号、考号搜索 POST /exam-statistics/score-ranking */
export async function examStatisticsScoreRankingUsingPost({
body,
options,
}: {
body: API.ScoreRankingRequest;
options?: CustomRequestOptions;
}) {
return request<
API.Response & {
data?: API.ScoreRankingResponse;
}
>('/exam-statistics/score-ranking', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
/** 获取统计配置 获取指定考试的统计配置信息,支持多种配置类型 GET /exam-statistics/statis-config */
export async function examStatisticsStatisConfigUsingGet({
params,
options,
}: {
// 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
params: API.examStatisticsStatisConfigUsingGetParams;
options?: CustomRequestOptions;
}) {
return request<
API.Response & {
data?: unknown;
}
>('/exam-statistics/statis-config', {
method: 'GET',
params: {
...params,
},
...(options || {}),
});
}
/** 设置统计配置 设置考试的统计配置,支持多种配置类型和规则 POST /exam-statistics/statis-config */
export async function examStatisticsStatisConfigUsingPost({
body,
options,
}: {
body: API.SetLevelConfigRequest;
options?: CustomRequestOptions;
}) {
return request<
API.Response & {
data?: API.SetLevelConfigResponse;
}
>('/exam-statistics/statis-config', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
/** 获取分段统计分析数据 根据分析类型获取统计分析数据,支持名次统计、分数段统计、比例段统计 POST /exam-statistics/statistics-analysis */
export async function examStatisticsStatisticsAnalysisUsingPost({
body,
options,
}: {
body: API.StatisticsAnalysisRequest;
options?: CustomRequestOptions;
}) {
return request<
API.Response & {
data?: API.StatisticsAnalysisResponse;
}
>('/exam-statistics/statistics-analysis', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
/** 获取学科分档上线人数分布 根据条件获取学科分档上线人数分布,支持按学校或班级分组统计 POST /exam-statistics/subject-class-analysis */
export async function examStatisticsSubjectClassAnalysisUsingPost({
body,
options,
}: {
body: API.SubjectClassAnalysisRequest;
options?: CustomRequestOptions;
}) {
return request<
API.Response & {
data?: API.SubjectClassAnalysisResponse;
}
>('/exam-statistics/subject-class-analysis', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}

View File

@ -5,6 +5,28 @@ import { CustomRequestOptions } from '@/http/types';
import * as API from './types';
/** 获取答题卡详情 根据答题卡ID获取答题卡详细信息包括学生信息、成绩信息、客观题答案、主观题答案等 GET /teacher-analysis/${param0} */
export async function teacherAnalysisInfoIdUsingGet({
params,
options,
}: {
// 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
params: API.teacherAnalysisInfoIdUsingGetParams;
options?: CustomRequestOptions;
}) {
const { info_id: param0, ...queryParams } = params;
return request<
API.Response & {
data?: API.GetAnswerSheetResponse;
}
>(`/teacher-analysis/${param0}`, {
method: 'GET',
params: { ...queryParams },
...(options || {}),
});
}
/** 老师关注学生 老师关注指定学生 POST /teacher-analysis/attention-student */
export async function teacherAnalysisAttentionStudentUsingPost({
body,
@ -27,6 +49,28 @@ export async function teacherAnalysisAttentionStudentUsingPost({
});
}
/** 获取班级考试对比(纵向对比) 获取指定班级的所有考试对比数据包括班级平均分、排名、标准分和年级前50名人数 POST /teacher-analysis/class-exam-comparison */
export async function teacherAnalysisClassExamComparisonUsingPost({
body,
options,
}: {
body: API.ClassExamComparisonRequest;
options?: CustomRequestOptions;
}) {
return request<
API.Response & {
data?: API.ClassExamComparisonData;
}
>('/teacher-analysis/class-exam-comparison', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
/** 获取班级下所有学生成绩列表 获取指定科目、年级、班级的所有学生成绩列表 POST /teacher-analysis/class-student-score-list */
export async function teacherAnalysisClassStudentScoreListUsingPost({
body,
@ -87,6 +131,28 @@ export async function teacherAnalysisExportScoreSheetUsingPost({
});
}
/** 导出学生列表 导出学生列表为Excel文件 POST /teacher-analysis/export-student-list */
export async function teacherAnalysisExportStudentListUsingPost({
body,
options,
}: {
body: API.ExportStudentListRequest;
options?: CustomRequestOptions;
}) {
return request<
API.Response & {
data?: API.ExportStudentListResponse;
}
>('/teacher-analysis/export-student-list', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
/** 获取重点学生情况 获取指定科目、年级、班级的重点学生情况 POST /teacher-analysis/key-student */
export async function teacherAnalysisKeyStudentUsingPost({
body,
@ -109,7 +175,7 @@ export async function teacherAnalysisKeyStudentUsingPost({
});
}
/** 获取个人报告 获取指定学生的个人报告 POST /teacher-analysis/personal-report */
/** 获取个人报告 获取指定学生的个人成绩报告,包含考试总结、学习情况分析和难度分析 POST /teacher-analysis/personal-report */
export async function teacherAnalysisPersonalReportUsingPost({
body,
options,
@ -131,6 +197,28 @@ export async function teacherAnalysisPersonalReportUsingPost({
});
}
/** 获取名次统计 根据考试ID、科目ID、班级ID获取名次统计数据 POST /teacher-analysis/rank-statistics */
export async function teacherAnalysisRankStatisticsUsingPost({
body,
options,
}: {
body: API.RankStatisticsRequest;
options?: CustomRequestOptions;
}) {
return request<
API.Response & {
data?: API.RankStatisticsResponse;
}
>('/teacher-analysis/rank-statistics', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
/** 获取最近考试情况 获取指定科目、年级、班级的最近考试统计数据 POST /teacher-analysis/recent-exam-stats */
export async function teacherAnalysisRecentExamStatsUsingPost({
body,
@ -153,6 +241,28 @@ export async function teacherAnalysisRecentExamStatsUsingPost({
});
}
/** 获取均分对比分析(横向对比) 获取指定考试、科目的均分对比分析数据 POST /teacher-analysis/score-analysis */
export async function teacherAnalysisScoreAnalysisUsingPost({
body,
options,
}: {
body: API.ScoreAnalysisRequest;
options?: CustomRequestOptions;
}) {
return request<
API.Response & {
data?: API.ScoreAnalysisResponse;
}
>('/teacher-analysis/score-analysis', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
/** 获取成绩单 获取指定科目、年级、班级的成绩单 POST /teacher-analysis/score-sheet */
export async function teacherAnalysisScoreSheetUsingPost({
body,
@ -175,6 +285,28 @@ export async function teacherAnalysisScoreSheetUsingPost({
});
}
/** 获取学生答题图片 根据题目ID和学号获取学生的答题图片URL GET /teacher-analysis/student-answer-image */
export async function teacherAnalysisStudentAnswerImageUsingGet({
params,
options,
}: {
// 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
params: API.teacherAnalysisStudentAnswerImageUsingGetParams;
options?: CustomRequestOptions;
}) {
return request<
API.Response & {
data?: string;
}
>('/teacher-analysis/student-answer-image', {
method: 'GET',
params: {
...params,
},
...(options || {}),
});
}
/** 获取个人历次考试详情 获取指定学生的历次考试详情 POST /teacher-analysis/student-exam-history */
export async function teacherAnalysisStudentExamHistoryUsingPost({
body,
@ -197,7 +329,7 @@ export async function teacherAnalysisStudentExamHistoryUsingPost({
});
}
/** 获取学生列表 获取指定年级、班级的学生列表 POST /teacher-analysis/student-list */
/** 获取学生列表 获取指定考试科目、年级、班级的学生列表,分为已关注和其他学生 POST /teacher-analysis/student-list */
export async function teacherAnalysisStudentListUsingPost({
body,
options,

File diff suppressed because it is too large Load Diff

44
src/service/wode.ts Normal file
View File

@ -0,0 +1,44 @@
/* eslint-disable */
// @ts-ignore
import request from '@/http/vue-query';
import { CustomRequestOptions } from '@/http/types';
import * as API from './types';
/** 修改密码 修改当前用户的密码 POST /my/change-password */
export async function myChangePasswordUsingPost({
body,
options,
}: {
body: API.MyChangePasswordRequest;
options?: CustomRequestOptions;
}) {
return request<
API.Response & {
data?: API.MyChangePasswordResponse;
}
>('/my/change-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
/** 获取我的信息 获取当前用户的个人信息 GET /my/profile */
export async function myProfileUsingGet({
options,
}: {
options?: CustomRequestOptions;
}) {
return request<
API.Response & {
data?: API.GetMyProfileResponse;
}
>('/my/profile', {
method: 'GET',
...(options || {}),
});
}

207
src/store/home.ts Normal file
View File

@ -0,0 +1,207 @@
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { teacherAnalysisDataUsingGet } from '@/service/laoshichengjifenxi'
export interface SelectOption {
label: string
value: number
}
// 考试科目组合数据类型
export interface ExamSubjectItem {
examId: number
examName: string
examSubjectId: number
subjectName: string
}
// 班级信息类型
export interface ClassItem {
label: string
value: number
gradeKey: number
}
/**
*
*/
export const useHomeStore = defineStore(
'homeStore',
() => {
// 数据存储
const loading = ref(false)
const examSubjectList = ref<ExamSubjectItem[]>([])
const classList = ref<ClassItem[]>([])
const classGradeMap = ref<Map<number, number>>(new Map())
// 已选择的数据
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 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,
}))
console.log('examOptions计算结果:', result)
return result
})
// 计算属性:班级列表
const classOptions = computed((): SelectOption[] => {
console.log('classOptions计算属性被调用classList长度:', classList.value.length)
const result = 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 fetchOptions = async () => {
try {
loading.value = true
const response = await teacherAnalysisDataUsingGet({})
if (response) {
// 处理班级数据并建立班级到年级的映射
classList.value = (response.class_list || []).map((item: any) => {
const classKey = item.class_key || 0
const gradeKey = item.grade_key || 0
// 建立班级到年级的映射
classGradeMap.value.set(classKey, gradeKey)
return {
label: `${item.grade || ''} ${item.class || ''}`.trim(),
value: classKey,
gradeKey,
}
})
// 存储考试科目组合数据
examSubjectList.value = (response.exam_list || []).map((item: any) => ({
examId: item.exam_id || 0,
examName: item.exam_name || '',
examSubjectId: item.exam_subject_id || 0,
subjectName: item.subject_name || '',
}))
console.log('数据获取完成:')
console.log('classList数量:', classList.value.length)
console.log('examSubjectList数量:', examSubjectList.value.length)
}
}
catch (error) {
console.error('获取选项数据失败:', error)
throw error
}
finally {
loading.value = false
}
}
/**
*
*/
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 = null
}
/**
*
*/
const setOptions = (exams: SelectOption[], classes: SelectOption[], subjects: SelectOption[]) => {
// 这个方法保留是为了兼容性但推荐使用fetchOptions
console.warn('setOptions is deprecated, use fetchOptions instead')
}
/**
*
*/
const clearSelections = () => {
selectedClassId.value = null
selectedExamId.value = null
selectedSubjectId.value = null
selectedGradeKey.value = null
}
/**
* store状态
*/
const reset = () => {
classList.value = []
examSubjectList.value = []
classGradeMap.value.clear()
loading.value = false
clearSelections()
}
return {
// 数据状态
loading,
// 选项数据(计算属性)
examOptions,
classOptions,
subjectOptions,
// 已选择的数据
selectedClassId,
selectedExamId,
selectedSubjectId,
selectedGradeKey,
// 方法
fetchOptions,
onClassChange,
onExamChange,
setOptions,
clearSelections,
reset,
}
},
{
persist: true,
},
)

View File

@ -14,4 +14,5 @@ store.use(
export default store
// 模块统一导出
export * from './home'
export * from './user'