自动备份 2026-04-03 08:00:10
This commit is contained in:
parent
a05afa0a84
commit
7df97f065b
@ -15,6 +15,8 @@
|
|||||||
- **决策升级:** 遇到无法抉择的事情,第一时间联系 Cris 处理
|
- **决策升级:** 遇到无法抉择的事情,第一时间联系 Cris 处理
|
||||||
- **飞书定时任务:** 所有飞书定时任务/提醒,必须指定 `--account xiaoban`,禁止使用默认 default bot
|
- **飞书定时任务:** 所有飞书定时任务/提醒,必须指定 `--account xiaoban`,禁止使用默认 default bot
|
||||||
- **飞书操作优先规则:** 所有飞书相关操作优先使用 `/root/.openclaw/skills/` 下的 `lark-*` 系列官方技能库执行,旧版 `feishu-*` 技能仅作为备份兼容使用
|
- **飞书操作优先规则:** 所有飞书相关操作优先使用 `/root/.openclaw/skills/` 下的 `lark-*` 系列官方技能库执行,旧版 `feishu-*` 技能仅作为备份兼容使用
|
||||||
|
- **飞书消息发送规则:** 所有向飞书个人或群组发送消息的操作,必须基于 `/root/.openclaw/skills/lark-send-message-as-bot` skill 执行,禁止使用其他发消息工具
|
||||||
|
- **飞书文档/知识库操作规则:** 所有飞书文档和知识库的读取、编辑操作,必须基于 `/root/.openclaw/skills/lark-action-as-bot` skill 规范执行,以Bot身份操作,无需申请用户个人授权
|
||||||
- **飞书文档/知识库权限规则:** 涉及飞书文档、知识库文档的阅读和编辑操作,统一使用应用权限执行,无需额外申请用户个人授权
|
- **飞书文档/知识库权限规则:** 涉及飞书文档、知识库文档的阅读和编辑操作,统一使用应用权限执行,无需额外申请用户个人授权
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
33
USER.md
33
USER.md
@ -10,14 +10,17 @@
|
|||||||
|
|
||||||
## 负责人配置
|
## 负责人配置
|
||||||
|
|
||||||
以下人员或群组拥有对你的管理权限,以飞书 `open_id` 作为唯一标识:
|
以下人员或群组拥有对你的管理权限,以飞书 `user_id` 作为唯一标识:
|
||||||
|
基于 /root/.openclaw/skills/lark-identify-sender 这个 skill 获取user_id,确认用户身份。
|
||||||
|
|
||||||
| 角色 | 姓名 | 飞书 open_id |
|
| 角色 | 姓名 | 飞书 user_id (群组则为 chat_id) |
|
||||||
|------|------|-------------|
|
|------|------|-------------|
|
||||||
| 技术负责人 | 李若松 | `ou_7c623036b45fe81f372b032da664a4fe` |
|
| 技术负责人 | 李若松 | `4aagb443` |
|
||||||
| 技术负责人 | 张昆鹏 | `ou_978d9e5c3ee53e6b8a9b68d37cad545f` |
|
| 技术负责人 | 张昆鹏 | `7f5cd711` |
|
||||||
|
| 第一业务负责人 | 刘庆逊 | `cb2815b4` |
|
||||||
|
| 业务负责人 | 李应瑛 | `58fd6864` |
|
||||||
| 技术负责人群组| AI_member | `oc_5c9f0e631266b9ef6e05a95daa80fbda`|
|
| 技术负责人群组| AI_member | `oc_5c9f0e631266b9ef6e05a95daa80fbda`|
|
||||||
| 业务负责人 | (姓名) | `ou_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` |
|
|
||||||
|
|
||||||
### 负责人权限
|
### 负责人权限
|
||||||
|
|
||||||
@ -31,18 +34,18 @@
|
|||||||
|
|
||||||
以下用户可以查询所有业务数据,无需额外审批:
|
以下用户可以查询所有业务数据,无需额外审批:
|
||||||
|
|
||||||
| 姓名 | 飞书 open_id |
|
| 姓名 | 飞书 user_id |
|
||||||
|------|-------------|
|
|------|-------------|
|
||||||
| (姓名) | `ou_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` |
|
| (姓名) | `xxxx` |
|
||||||
| (姓名) | `ou_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` |
|
| (姓名) | `xxxx` |
|
||||||
|
|
||||||
### 第二级:受限权限用户
|
### 第二级:受限权限用户
|
||||||
|
|
||||||
以下用户可以查询其职责范围内的数据:
|
以下用户可以查询其职责范围内的数据:
|
||||||
|
|
||||||
| 姓名 | 飞书 open_id | 可查询范围 |
|
| 姓名 | 飞书 user_id | 可查询范围 |
|
||||||
|------|-------------|-----------|
|
|------|-------------|-----------|
|
||||||
| (姓名) | `ou_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | (如:仅本部门数据) |
|
| (姓名) | `xxxx` | (如:仅本部门数据) |
|
||||||
|
|
||||||
### 第三级:其他用户
|
### 第三级:其他用户
|
||||||
|
|
||||||
@ -52,12 +55,6 @@
|
|||||||
2. 立即通知业务负责人(通过飞书消息),说明查询用户信息和具体查询需求
|
2. 立即通知业务负责人(通过飞书消息),说明查询用户信息和具体查询需求
|
||||||
3. 等待业务负责人确认允许查看的数据范围后,再回复查询用户
|
3. 等待业务负责人确认允许查看的数据范围后,再回复查询用户
|
||||||
|
|
||||||
## 用户身份识别规则
|
|
||||||
|
|
||||||
- **唯一标识:** 以飞书消息中的 `open_id` 作为用户身份的唯一判断依据
|
|
||||||
- **无法确认身份时:** 如果无法获取到用户的飞书 `open_id`,按照最低权限处理,不主动返回任何敏感数据
|
|
||||||
- **群聊中的身份:** 在群聊中,通过消息发送者的 `open_id` 判断身份,而非群聊本身
|
|
||||||
|
|
||||||
## 敏感操作审批规则
|
## 敏感操作审批规则
|
||||||
|
|
||||||
以下操作需要技术负责人或业务负责人审批:
|
以下操作需要技术负责人或业务负责人审批:
|
||||||
@ -78,5 +75,5 @@
|
|||||||
|
|
||||||
## 群组信息回复规则
|
## 群组信息回复规则
|
||||||
|
|
||||||
AI_member:(oc_5c9f0e631266b9ef6e05a95daa80fbda)
|
AI_member:(`oc_5c9f0e631266b9ef6e05a95daa80fbda`)
|
||||||
[禁令]在此群组中,只回复@到你(也就是 @小斑)的信息。
|
[禁令]在此群组中,只回复@到你(也就是 @小溪)的信息。
|
||||||
|
|||||||
@ -14,3 +14,4 @@
|
|||||||
/bin/sh: 1: /root/.openclaw/workspace-xiaoban/daily_summary.sh: not found
|
/bin/sh: 1: /root/.openclaw/workspace-xiaoban/daily_summary.sh: not found
|
||||||
/bin/sh: 1: /root/.openclaw/workspace-xiaoban/daily_summary.sh: not found
|
/bin/sh: 1: /root/.openclaw/workspace-xiaoban/daily_summary.sh: not found
|
||||||
/bin/sh: 1: /root/.openclaw/workspace-xiaoban/daily_summary.sh: not found
|
/bin/sh: 1: /root/.openclaw/workspace-xiaoban/daily_summary.sh: not found
|
||||||
|
/bin/sh: 1: /root/.openclaw/workspace-xiaoban/daily_summary.sh: not found
|
||||||
|
|||||||
168
memory/2026-04-02-request-timed-out-before-a-res.md
Normal file
168
memory/2026-04-02-request-timed-out-before-a-res.md
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
# Session: 2026-04-02 08:02:14 UTC
|
||||||
|
|
||||||
|
- **Session Key**: agent:xiaoban:feishu:direct:ou_5494e9f79ba2bc0c221f94781abc52a9
|
||||||
|
- **Session ID**: 5be50819-93d3-4f25-b956-4d8c1bea23ca
|
||||||
|
- **Source**: feishu
|
||||||
|
|
||||||
|
## Conversation Summary
|
||||||
|
|
||||||
|
user: System: [2026-04-02 13:37:27 GMT+8] Feishu[xiaoban] DM | ou_5494e9f79ba2bc0c221f94781abc52a9 [msg:om_x100b53d4479018a8b105afa362c308e]
|
||||||
|
|
||||||
|
Conversation info (untrusted metadata):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message_id": "om_x100b53d4479018a8b105afa362c308e",
|
||||||
|
"sender_id": "ou_5494e9f79ba2bc0c221f94781abc52a9",
|
||||||
|
"sender": "ou_5494e9f79ba2bc0c221f94781abc52a9",
|
||||||
|
"timestamp": "Thu 2026-04-02 13:37 GMT+8"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Sender (untrusted metadata):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"label": "ou_5494e9f79ba2bc0c221f94781abc52a9",
|
||||||
|
"id": "ou_5494e9f79ba2bc0c221f94781abc52a9",
|
||||||
|
"name": "ou_5494e9f79ba2bc0c221f94781abc52a9"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
你好,你是谁?
|
||||||
|
assistant: 你好👋 我是小斑📚,是服务于Makee Interactive教学团队的专属AI班主任。我可以为你提供教学管理、学情数据分析、学习支持服务,也支持飞书文档/多维表格/日历等工具操作和业务数据查询需求。
|
||||||
|
user: System: [2026-04-02 13:47:50 GMT+8] Feishu[xiaoban] DM | ou_5494e9f79ba2bc0c221f94781abc52a9 [msg:om_x100b53d47e91d90cb2c8e10ea4a412d]
|
||||||
|
|
||||||
|
Conversation info (untrusted metadata):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message_id": "om_x100b53d47e91d90cb2c8e10ea4a412d",
|
||||||
|
"sender_id": "ou_5494e9f79ba2bc0c221f94781abc52a9",
|
||||||
|
"sender": "ou_5494e9f79ba2bc0c221f94781abc52a9",
|
||||||
|
"timestamp": "Thu 2026-04-02 13:47 GMT+8"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Sender (untrusted metadata):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"label": "ou_5494e9f79ba2bc0c221f94781abc52a9",
|
||||||
|
"id": "ou_5494e9f79ba2bc0c221f94781abc52a9",
|
||||||
|
"name": "ou_5494e9f79ba2bc0c221f94781abc52a9"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
你帮我创建一个skill,这个skill是用来分析用户的学习情况的,使用的方式是输入“学情分析:用户角色ID,Level几, Unit几”,这里面用户的角色ID是一个数字,Level几可以是Level1或者Level2,或者也可以输入L1或者L2,或者直接输入1或者2都行,Unit也是一样的。你在得到了用户的角色ID,Level数字,Unit数字之后,首先是将其转化为Json,然后把这个Json作为数据去请求一个网络的API服务,这个服务会给你返回来一个JSON数据,你把这个返回的数据存储起来,最后把返回的JSON数据转化为一个HTML的可视化文件,最后把这个html文件的地址显示出来。
|
||||||
|
assistant: ✅ 学情分析技能已经创建完成啦!
|
||||||
|
|
||||||
|
## 📝 配置说明
|
||||||
|
请先修改脚本中的API地址:
|
||||||
|
打开 `/root/.openclaw/workspace-xiaoban/skills/learning-situation-analysis/scripts/analysis.py`,将第9行的`API_URL`替换为实际的学情分析服务接口地址。
|
||||||
|
|
||||||
|
## 🎯 使用方式
|
||||||
|
用户输入以下任意格式的内容即可触发分析:
|
||||||
|
```
|
||||||
|
学情分析:14607,Level1,Unit3
|
||||||
|
学情分析:14607,L1,3
|
||||||
|
学情分析:14607,1,3
|
||||||
|
```
|
||||||
|
- 支持Level的格式:`Level1`/`L1`/`1`/`Level2`/`L2`/`2`
|
||||||
|
- 支持Unit的格式:`Unit1`/`U1`/`1`
|
||||||
|
|
||||||
|
## 📤 输出说明
|
||||||
|
1. 原始API返回数据会保存在 `skills/learning-situation-analysis/output/` 目录下(JSON格式)
|
||||||
|
2. 可视化HTML报告会同时生成,返回本地文件路径,直接点击即可在浏览器中打开查看
|
||||||
|
3. 报告包含核心指标概览、能力维度雷达图、完成情况统计、提升建议和原始数据展示
|
||||||
|
|
||||||
|
## ✨ 自定义调整
|
||||||
|
- 如需修改报告样式或展示内容,可以编辑 `assets/template.html` 模板文件
|
||||||
|
- 支持扩展更多图表和数据展示维度
|
||||||
|
user: System: [2026-04-02 13:55:18 GMT+8] Feishu[xiaoban] DM | ou_5494e9f79ba2bc0c221f94781abc52a9 [msg:om_x100b53d40296f480b29c1a45bffbae4]
|
||||||
|
|
||||||
|
Conversation info (untrusted metadata):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message_id": "om_x100b53d40296f480b29c1a45bffbae4",
|
||||||
|
"sender_id": "ou_5494e9f79ba2bc0c221f94781abc52a9",
|
||||||
|
"sender": "ou_5494e9f79ba2bc0c221f94781abc52a9",
|
||||||
|
"timestamp": "Thu 2026-04-02 13:55 GMT+8"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Sender (untrusted metadata):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"label": "ou_5494e9f79ba2bc0c221f94781abc52a9",
|
||||||
|
"id": "ou_5494e9f79ba2bc0c221f94781abc52a9",
|
||||||
|
"name": "ou_5494e9f79ba2bc0c221f94781abc52a9"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
告诉我学情分析的技能怎么调用?
|
||||||
|
assistant: ## 🎯 学情分析技能调用方法
|
||||||
|
### 1. 触发方式
|
||||||
|
直接在对话中输入符合以下格式的内容即可自动触发,无需额外命令:
|
||||||
|
```
|
||||||
|
学情分析:[用户角色ID],[Level],[Unit]
|
||||||
|
```
|
||||||
|
---
|
||||||
|
### 2. 参数说明(3个参数按顺序填写,用中文逗号分隔)
|
||||||
|
| 参数 | 格式说明 | 支持的输入示例 |
|
||||||
|
|------|----------|----------------|
|
||||||
|
| 用户角色ID | 必须是纯数字 | `14607`、`20001` |
|
||||||
|
| Level | 等级,仅支持1或2 | ✅ 支持:`Level1`/`L1`/`1`/`Level2`/`L2`/`2` |
|
||||||
|
| Unit | 单元号,正整数 | ✅ 支持:`Unit3`/`U3`/`3` |
|
||||||
|
---
|
||||||
|
### 3. 调用示例
|
||||||
|
以下几种写法都可以:
|
||||||
|
```
|
||||||
|
学情分析:14607,Level1,Unit5
|
||||||
|
学情分析:14607,L1,5
|
||||||
|
学情分析:14607,1,5
|
||||||
|
```
|
||||||
|
---
|
||||||
|
### 4. 返回结果说明
|
||||||
|
调用成功后会返回两个内容:
|
||||||
|
1. ✅ 原始分析数据(JSON格式)的本地保存路径
|
||||||
|
2. ✅ 可视化HTML报告的访问地址,直接点击即可在浏览器打开查看完整分析报告(包含指标概览、能力雷达图、提升建议等)
|
||||||
|
---
|
||||||
|
### ⚠️ 重要提醒
|
||||||
|
请先确认已经配置了正确的API地址:
|
||||||
|
编辑`/root/.openclaw/workspace-xiaoban/skills/learning-situation-analysis/scripts/analysis.py`文件,将第9行的`API_URL`替换为实际的学情分析接口地址,否则无法正常请求数据。
|
||||||
|
user: System: [2026-04-02 15:17:34 GMT+8] Feishu[xiaoban] DM | ou_5494e9f79ba2bc0c221f94781abc52a9 [msg:om_x100b53d5ce15d8a4b24e715b00131d9]
|
||||||
|
|
||||||
|
Conversation info (untrusted metadata):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message_id": "om_x100b53d5ce15d8a4b24e715b00131d9",
|
||||||
|
"sender_id": "ou_5494e9f79ba2bc0c221f94781abc52a9",
|
||||||
|
"sender": "ou_5494e9f79ba2bc0c221f94781abc52a9",
|
||||||
|
"timestamp": "Thu 2026-04-02 15:17 GMT+8"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Sender (untrusted metadata):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"label": "ou_5494e9f79ba2bc0c221f94781abc52a9",
|
||||||
|
"id": "ou_5494e9f79ba2bc0c221f94781abc52a9",
|
||||||
|
"name": "ou_5494e9f79ba2bc0c221f94781abc52a9"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
你重新帮我改一下学情分析这个skill,其中API的调用是调用这个url:https://api.valavala.com/v2/user/unit/learn/info?operator=Kingson&level=L1&userId=100909&unitIndex=10
|
||||||
|
assistant: ✅ 学情分析技能的API配置已经修改完成啦!
|
||||||
|
|
||||||
|
## 🎯 修改内容确认
|
||||||
|
1. **API地址**已更新为:`https://api.valavala.com/v2/user/unit/learn/info`
|
||||||
|
2. **请求参数**自动适配接口要求:
|
||||||
|
- `operator` 固定为 `Kingson`
|
||||||
|
- `level` 自动格式化为 `L1`/`L2` 格式
|
||||||
|
- `userId` 对应输入的用户角色ID
|
||||||
|
- `unitIndex` 对应输入的单元号
|
||||||
|
3. **请求方式**改为GET请求,和接口要求一致
|
||||||
|
|
||||||
|
## 📌 调用方式不变
|
||||||
|
还是按照原来的格式输入即可,例如:
|
||||||
|
```
|
||||||
|
学情分析:100909,1,10
|
||||||
|
```
|
||||||
|
系统会自动组装成你提供的完整接口地址发起请求,返回结果后依旧会生成JSON原始数据和可视化HTML报告。
|
||||||
47
skills/study-analysis/SKILL.md
Normal file
47
skills/study-analysis/SKILL.md
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
name: study-analysis
|
||||||
|
description: 分析用户学习情况的工具,输入格式为「学情分析:用户角色ID,Level几,Unit几」,其中Level支持Level1/L1/1格式,Unit支持Unit1/U1/1格式,自动请求API获取学情数据并生成可视化分析报告。触发场景:用户提到「学情分析」、「学习情况分析」时使用。
|
||||||
|
---
|
||||||
|
|
||||||
|
# 学情分析技能使用指南
|
||||||
|
|
||||||
|
## 触发规则
|
||||||
|
当用户输入以下格式内容时触发本技能:
|
||||||
|
```
|
||||||
|
学情分析:[用户角色ID],[Level],[Unit]
|
||||||
|
```
|
||||||
|
- 用户角色ID:数字类型(必填)
|
||||||
|
- Level:支持多种格式:`Level1`/`L1`/`1`/`Level2`/`L2`/`2`,自动转换为数字1或2
|
||||||
|
- Unit:支持多种格式:`Unit1`/`U1`/`1`,自动转换为对应的数字
|
||||||
|
|
||||||
|
## 工作流程
|
||||||
|
1. **参数解析**:从用户输入中提取角色ID、Level、Unit三个参数,统一格式化为数字类型
|
||||||
|
2. **构造请求**:将参数封装为API查询参数,operator固定为Kingson
|
||||||
|
3. **请求API**:调用学情分析服务API `https://api.valavala.com/v2/user/unit/learn/info`,传入请求参数
|
||||||
|
4. **存储数据**:将API返回的JSON数据保存到本地 output 目录
|
||||||
|
5. **生成可视化报告**:使用HTML模板渲染返回数据,生成可视化HTML文件
|
||||||
|
6. **返回结果**:将生成的HTML文件访问地址返回给用户
|
||||||
|
|
||||||
|
## 调用方式
|
||||||
|
执行脚本:
|
||||||
|
```bash
|
||||||
|
python3 /root/.openclaw/workspace-xiaoban/skills/study-analysis/scripts/analysis.py [role_id] [level] [unit]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 参数说明
|
||||||
|
- `role_id`: 用户角色ID(数字)
|
||||||
|
- `level`: 等级(数字1或2)
|
||||||
|
- `unit`: 单元号(数字)
|
||||||
|
|
||||||
|
## API接口说明
|
||||||
|
- 请求地址:`https://api.valavala.com/v2/user/unit/learn/info`
|
||||||
|
- 请求方式:GET
|
||||||
|
- 请求参数:
|
||||||
|
- `operator`: 固定值 `Kingson`
|
||||||
|
- `level`: `L1` 或 `L2`
|
||||||
|
- `userId`: 用户角色ID
|
||||||
|
- `unitIndex`: 单元号
|
||||||
|
|
||||||
|
## 输出说明
|
||||||
|
- 成功:返回HTML可视化报告的本地路径/访问地址
|
||||||
|
- 失败:返回具体错误信息
|
||||||
630
skills/study-analysis/assets/template.html
Normal file
630
skills/study-analysis/assets/template.html
Normal file
@ -0,0 +1,630 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>学情分析报告 - 用户{{ROLE_ID}} Level{{LEVEL}} Unit{{UNIT}}</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||||
|
background-color: #f0f2f5;
|
||||||
|
padding: 24px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.container { max-width: 1200px; margin: 0 auto; }
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 32px;
|
||||||
|
border-radius: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.header h1 { font-size: 28px; margin-bottom: 8px; }
|
||||||
|
.header .meta { font-size: 14px; opacity: 0.9; }
|
||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
.card h2 {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-left: 4px solid #667eea;
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.stat-item {
|
||||||
|
background: linear-gradient(135deg, #f5f7fa 0%, #e8ecf1 100%);
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.stat-item .label { font-size: 13px; color: #888; margin-bottom: 8px; }
|
||||||
|
.stat-item .value { font-size: 28px; font-weight: 700; color: #333; }
|
||||||
|
.stat-item .value.green { color: #52c41a; }
|
||||||
|
.stat-item .value.orange { color: #fa8c16; }
|
||||||
|
.stat-item .value.blue { color: #1890ff; }
|
||||||
|
.stat-item .value.purple { color: #722ed1; }
|
||||||
|
.chart-container { height: 400px; width: 100%; }
|
||||||
|
.chart-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.chart-row { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
.lesson-nav {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.lesson-card {
|
||||||
|
background: white;
|
||||||
|
border: 2px solid #e8e8e8;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.lesson-card:hover {
|
||||||
|
border-color: #667eea;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);
|
||||||
|
}
|
||||||
|
.lesson-card.active {
|
||||||
|
border-color: #667eea;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.lesson-card .lesson-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.lesson-card .lesson-time {
|
||||||
|
font-size: 13px;
|
||||||
|
opacity: 0.9;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.lesson-card .lesson-time .time-label {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
.lesson-card .lesson-stats {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid rgba(102, 126, 234, 0.2);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.lesson-card.active .lesson-stats {
|
||||||
|
border-top-color: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
.lesson-card .lesson-stats .stat {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.lesson-card .lesson-stats .stat-label {
|
||||||
|
font-size: 11px;
|
||||||
|
opacity: 0.7;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
.lesson-card .lesson-stats .stat-value {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.lesson-card .lesson-stats .stat-value.oops { color: #fa541c; }
|
||||||
|
.lesson-card .lesson-stats .stat-value.wrong { color: #f5222d; }
|
||||||
|
.lesson-card.active .lesson-stats .stat-value.oops,
|
||||||
|
.lesson-card.active .lesson-stats .stat-value.wrong { color: #ffccc7; }
|
||||||
|
.lesson-detail { display: none; }
|
||||||
|
.lesson-detail.active { display: block; }
|
||||||
|
.knowledge-section { margin-bottom: 20px; }
|
||||||
|
.knowledge-section h3 {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #555;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.word-list, .pattern-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.word-tag {
|
||||||
|
background: #e6f7ff;
|
||||||
|
border: 1px solid #91d5ff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.word-tag .en { font-weight: 600; color: #1890ff; }
|
||||||
|
.word-tag .cn { color: #666; margin-left: 6px; }
|
||||||
|
.word-tag .pos { color: #999; font-size: 12px; margin-left: 4px; }
|
||||||
|
.pattern-tag {
|
||||||
|
background: #fff7e6;
|
||||||
|
border: 1px solid #ffd591;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
flex: 1 1 300px;
|
||||||
|
}
|
||||||
|
.pattern-tag .en { font-weight: 600; color: #fa8c16; }
|
||||||
|
.pattern-tag .cn { color: #666; display: block; margin-top: 4px; font-size: 13px; }
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
table th {
|
||||||
|
background: #f5f7fa;
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
border-bottom: 2px solid #e8e8e8;
|
||||||
|
}
|
||||||
|
table td {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
table tr:hover td { background: #fafafa; }
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.badge.perfect { background: #f6ffed; color: #52c41a; border: 1px solid #b7eb8f; }
|
||||||
|
.badge.good { background: #e6f7ff; color: #1890ff; border: 1px solid #91d5ff; }
|
||||||
|
.badge.oops { background: #fff2e8; color: #fa541c; border: 1px solid #ffbb96; }
|
||||||
|
.badge.correct { background: #f6ffed; color: #52c41a; border: 1px solid #b7eb8f; }
|
||||||
|
.badge.wrong { background: #fff1f0; color: #f5222d; border: 1px solid #ffa39e; }
|
||||||
|
.audio-btn {
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
vertical-align: middle;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
.audio-btn:hover { opacity: 1; }
|
||||||
|
.audio-btn.playing { opacity: 1; color: #1890ff; }
|
||||||
|
.time-info {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.time-info span { display: flex; align-items: center; gap: 6px; }
|
||||||
|
.raw-data {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-family: "SF Mono", "Fira Code", monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>📚 学情分析报告</h1>
|
||||||
|
<div class="meta">
|
||||||
|
用户ID:{{ROLE_ID}} | Level {{LEVEL}} | Unit {{UNIT}} | 生成时间:{{GENERATE_TIME}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>📊 总览</h2>
|
||||||
|
<div class="stats-grid" id="stats-grid"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chart-row">
|
||||||
|
<div class="card">
|
||||||
|
<h2>📈 互动组件表现</h2>
|
||||||
|
<div id="interactive-chart" class="chart-container"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>📝 巩固练习正确率</h2>
|
||||||
|
<div id="exercise-chart" class="chart-container"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>🎯 能力训练正确率</h2>
|
||||||
|
<div id="ability-chart" class="chart-container"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>📖 课程详情</h2>
|
||||||
|
<div class="lesson-nav" id="lesson-nav"></div>
|
||||||
|
<div id="lesson-details"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>🎯 能力训练</h2>
|
||||||
|
<table id="ability-training-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>来源</th>
|
||||||
|
<th>题型</th>
|
||||||
|
<th>标题</th>
|
||||||
|
<th>题目详情</th>
|
||||||
|
<th>子题目情况</th>
|
||||||
|
<th>结果</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="ability-training-tbody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const data = {{DATA}};
|
||||||
|
|
||||||
|
// 兼容:data可能直接就是lessons数组,也可能有外层包装
|
||||||
|
const lessons = data.lessons || data.data?.lessons || (Array.isArray(data) ? data : []);
|
||||||
|
const summary = data.summary || data.data?.summary || null;
|
||||||
|
|
||||||
|
// ==================== 总览统计 ====================
|
||||||
|
const statsGrid = document.getElementById('stats-grid');
|
||||||
|
const totalLessons = lessons.length;
|
||||||
|
let totalInteractive = 0, totalExercises = 0;
|
||||||
|
let totalStudyDuration = 0; // 单元学习时长(分钟)
|
||||||
|
let unitStartTime = null, unitEndTime = null;
|
||||||
|
|
||||||
|
lessons.forEach(lesson => {
|
||||||
|
const ics = lesson.interactive_components || lesson.interactiveComponents || [];
|
||||||
|
const ces = lesson.consolidation_exercises || lesson.consolidationExercises || [];
|
||||||
|
totalInteractive += ics.length;
|
||||||
|
totalExercises += ces.length;
|
||||||
|
|
||||||
|
// 计算每个lesson的学习时长
|
||||||
|
const entryTime = lesson.entry_time || lesson.entryTime;
|
||||||
|
const completionTime = lesson.completion_time || lesson.completionTime;
|
||||||
|
const consolidationEntry = lesson.consolidation_entry_time || lesson.consolidationEntryTime;
|
||||||
|
const consolidationCompletion = lesson.consolidation_completion_time || lesson.consolidationCompletionTime;
|
||||||
|
|
||||||
|
if (entryTime && completionTime) {
|
||||||
|
const lessonDuration = (new Date(completionTime) - new Date(entryTime)) / (1000 * 60);
|
||||||
|
if (lessonDuration > 0) totalStudyDuration += lessonDuration;
|
||||||
|
}
|
||||||
|
if (consolidationEntry && consolidationCompletion) {
|
||||||
|
const consolidationDuration = (new Date(consolidationCompletion) - new Date(consolidationEntry)) / (1000 * 60);
|
||||||
|
if (consolidationDuration > 0) totalStudyDuration += consolidationDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录单元开始和结束时间
|
||||||
|
if (entryTime) {
|
||||||
|
const entryDate = new Date(entryTime);
|
||||||
|
if (!unitStartTime || entryDate < unitStartTime) unitStartTime = entryDate;
|
||||||
|
}
|
||||||
|
if (consolidationCompletion) {
|
||||||
|
const completionDate = new Date(consolidationCompletion);
|
||||||
|
if (!unitEndTime || completionDate > unitEndTime) unitEndTime = completionDate;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算单元长度(从第1个lesson进入到第5个lesson巩固完成)
|
||||||
|
let unitLengthStr = '-';
|
||||||
|
if (unitStartTime && unitEndTime) {
|
||||||
|
const unitLengthMs = unitEndTime - unitStartTime;
|
||||||
|
const unitLengthMinutes = Math.round(unitLengthMs / (1000 * 60));
|
||||||
|
const days = Math.floor(unitLengthMinutes / (24 * 60));
|
||||||
|
const hours = Math.floor((unitLengthMinutes % (24 * 60)) / 60);
|
||||||
|
const minutes = unitLengthMinutes % 60;
|
||||||
|
if (days > 0) {
|
||||||
|
unitLengthStr = `${days}天${hours}小时${minutes}分钟`;
|
||||||
|
} else if (hours > 0) {
|
||||||
|
unitLengthStr = `${hours}小时${minutes}分钟`;
|
||||||
|
} else {
|
||||||
|
unitLengthStr = `${minutes}分钟`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化学习时长
|
||||||
|
let studyDurationStr = '-';
|
||||||
|
if (totalStudyDuration > 0) {
|
||||||
|
const studyHours = Math.floor(totalStudyDuration / 60);
|
||||||
|
const studyMinutes = Math.round(totalStudyDuration % 60);
|
||||||
|
if (studyHours > 0) {
|
||||||
|
studyDurationStr = `${studyHours}小时${studyMinutes}分钟`;
|
||||||
|
} else {
|
||||||
|
studyDurationStr = `${studyMinutes}分钟`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const statsItems = [
|
||||||
|
{ label: '单元学习时长', value: studyDurationStr, cls: 'purple' },
|
||||||
|
{ label: '总互动次数', value: totalInteractive, cls: 'blue' },
|
||||||
|
{ label: '总练习题数', value: totalExercises, cls: 'green' },
|
||||||
|
{ label: '单元长度', value: unitLengthStr, cls: 'orange' }
|
||||||
|
];
|
||||||
|
statsItems.forEach(item => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'stat-item';
|
||||||
|
div.innerHTML = `<div class="label">${item.label}</div><div class="value ${item.cls}">${item.value}</div>`;
|
||||||
|
statsGrid.appendChild(div);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==================== 互动组件柱状图(按课程,三段堆叠) ====================
|
||||||
|
const interactiveChart = echarts.init(document.getElementById('interactive-chart'));
|
||||||
|
const lessonLabels = lessons.map((l, i) => l.lesson_name || l.lessonName || `Lesson ${i + 1}`);
|
||||||
|
const perfectCounts = [];
|
||||||
|
const goodCounts = [];
|
||||||
|
const oopsCounts = [];
|
||||||
|
|
||||||
|
lessons.forEach(lesson => {
|
||||||
|
const ics = lesson.interactive_components || lesson.interactiveComponents || [];
|
||||||
|
let perfect = 0, good = 0, oops = 0;
|
||||||
|
ics.forEach(ic => {
|
||||||
|
const r = (ic.user_result || ic.userResult || '').toLowerCase();
|
||||||
|
if (r === 'perfect') perfect++;
|
||||||
|
else if (r === 'good') good++;
|
||||||
|
else if (r === 'oops') oops++;
|
||||||
|
});
|
||||||
|
perfectCounts.push(perfect);
|
||||||
|
goodCounts.push(good);
|
||||||
|
oopsCounts.push(oops);
|
||||||
|
});
|
||||||
|
|
||||||
|
interactiveChart.setOption({
|
||||||
|
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
||||||
|
legend: { bottom: 10, data: ['Perfect', 'Good', 'Oops'] },
|
||||||
|
grid: { left: '3%', right: '4%', bottom: '15%', top: '5%', containLabel: true },
|
||||||
|
xAxis: { type: 'category', data: lessonLabels, axisLabel: { rotate: 20, fontSize: 12 } },
|
||||||
|
yAxis: { type: 'value' },
|
||||||
|
color: ['#52c41a', '#1890ff', '#fa541c'],
|
||||||
|
series: [
|
||||||
|
{ name: 'Perfect', type: 'bar', stack: 'total', data: perfectCounts, itemStyle: { borderRadius: [0, 0, 4, 4] } },
|
||||||
|
{ name: 'Good', type: 'bar', stack: 'total', data: goodCounts, itemStyle: { borderRadius: [0, 0, 0, 0] } },
|
||||||
|
{ name: 'Oops', type: 'bar', stack: 'total', data: oopsCounts, itemStyle: { borderRadius: [4, 4, 0, 0] } }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==================== 巩固练习柱状图(按课程,两段堆叠) ====================
|
||||||
|
const exerciseChart = echarts.init(document.getElementById('exercise-chart'));
|
||||||
|
const correctCounts = [];
|
||||||
|
const wrongCounts = [];
|
||||||
|
|
||||||
|
lessons.forEach(lesson => {
|
||||||
|
const ces = lesson.consolidation_exercises || lesson.consolidationExercises || [];
|
||||||
|
let correct = 0, wrong = 0;
|
||||||
|
ces.forEach(ce => {
|
||||||
|
if (ce.is_correct === true || ce.isCorrect === true) correct++;
|
||||||
|
else wrong++;
|
||||||
|
});
|
||||||
|
correctCounts.push(correct);
|
||||||
|
wrongCounts.push(wrong);
|
||||||
|
});
|
||||||
|
|
||||||
|
exerciseChart.setOption({
|
||||||
|
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
||||||
|
legend: { bottom: 10, data: ['正确', '错误'] },
|
||||||
|
grid: { left: '3%', right: '4%', bottom: '15%', top: '5%', containLabel: true },
|
||||||
|
xAxis: { type: 'category', data: lessonLabels, axisLabel: { rotate: 20, fontSize: 12 } },
|
||||||
|
yAxis: { type: 'value' },
|
||||||
|
color: ['#52c41a', '#f5222d'],
|
||||||
|
series: [
|
||||||
|
{ name: '正确', type: 'bar', stack: 'total', data: correctCounts, itemStyle: { borderRadius: [0, 0, 4, 4] } },
|
||||||
|
{ name: '错误', type: 'bar', stack: 'total', data: wrongCounts, itemStyle: { borderRadius: [4, 4, 0, 0] } }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==================== 能力训练饼图 ====================
|
||||||
|
const abilityTraining = data.ability_training || data.abilityTraining || [];
|
||||||
|
let perfectCount = 0, goodCount = 0, oopsCount = 0;
|
||||||
|
abilityTraining.forEach(at => {
|
||||||
|
const result = (at.result || '').toLowerCase();
|
||||||
|
if (result === 'perfect') perfectCount++;
|
||||||
|
else if (result === 'good') goodCount++;
|
||||||
|
else if (result === 'oops') oopsCount++;
|
||||||
|
});
|
||||||
|
|
||||||
|
const abilityChart = echarts.init(document.getElementById('ability-chart'));
|
||||||
|
abilityChart.setOption({
|
||||||
|
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
|
||||||
|
legend: { bottom: 10, data: ['Perfect', 'Good', 'Oops'] },
|
||||||
|
color: ['#52c41a', '#1890ff', '#fa541c'],
|
||||||
|
series: [{
|
||||||
|
name: '能力训练结果',
|
||||||
|
type: 'pie',
|
||||||
|
radius: ['40%', '70%'],
|
||||||
|
center: ['50%', '45%'],
|
||||||
|
avoidLabelOverlap: false,
|
||||||
|
itemStyle: { borderRadius: 8, borderColor: '#fff', borderWidth: 2 },
|
||||||
|
label: { show: true, formatter: '{b}: {c}' },
|
||||||
|
data: [
|
||||||
|
{ value: perfectCount, name: 'Perfect' },
|
||||||
|
{ value: goodCount, name: 'Good' },
|
||||||
|
{ value: oopsCount, name: 'Oops' }
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
const navContainer = document.getElementById('lesson-nav');
|
||||||
|
const detailContainer = document.getElementById('lesson-details');
|
||||||
|
|
||||||
|
lessons.forEach((lesson, idx) => {
|
||||||
|
// 大方块导航卡片
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'lesson-card' + (idx === 0 ? ' active' : '');
|
||||||
|
card.onclick = () => switchTab(idx);
|
||||||
|
|
||||||
|
const entryTime = lesson.entry_time || lesson.entryTime || '-';
|
||||||
|
const completionTime = lesson.completion_time || lesson.completionTime || '-';
|
||||||
|
const lessonName = lesson.lesson_name || lesson.lessonName || `Lesson ${idx + 1}`;
|
||||||
|
|
||||||
|
// 计算统计数据
|
||||||
|
const icsForStats = lesson.interactive_components || lesson.interactiveComponents || [];
|
||||||
|
const cesForStats = lesson.consolidation_exercises || lesson.consolidationExercises || [];
|
||||||
|
let oopsCount = 0;
|
||||||
|
icsForStats.forEach(ic => {
|
||||||
|
const r = (ic.user_result || ic.userResult || '').toLowerCase();
|
||||||
|
if (r === 'oops') oopsCount++;
|
||||||
|
});
|
||||||
|
const oopsRate = icsForStats.length > 0 ? Math.round(oopsCount / icsForStats.length * 100) : 0;
|
||||||
|
|
||||||
|
let wrongCount = 0;
|
||||||
|
cesForStats.forEach(ce => {
|
||||||
|
if (ce.is_correct !== true && ce.isCorrect !== true) wrongCount++;
|
||||||
|
});
|
||||||
|
const wrongRate = cesForStats.length > 0 ? Math.round(wrongCount / cesForStats.length * 100) : 0;
|
||||||
|
|
||||||
|
card.innerHTML = `
|
||||||
|
<div class="lesson-title">${lessonName}</div>
|
||||||
|
<div class="lesson-time">
|
||||||
|
<div><span class="time-label">进入:</span>${formatTime(entryTime)}</div>
|
||||||
|
<div><span class="time-label">完成:</span>${formatTime(completionTime)}</div>
|
||||||
|
</div>
|
||||||
|
<div class="lesson-stats">
|
||||||
|
<div class="stat">
|
||||||
|
<span class="stat-label">Oops率</span>
|
||||||
|
<span class="stat-value oops">${oopsRate}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<span class="stat-label">错误率</span>
|
||||||
|
<span class="stat-value wrong">${wrongRate}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
navContainer.appendChild(card);
|
||||||
|
|
||||||
|
// 详情区域
|
||||||
|
const detail = document.createElement('div');
|
||||||
|
detail.className = 'lesson-detail' + (idx === 0 ? ' active' : '');
|
||||||
|
detail.id = `lesson-${idx}`;
|
||||||
|
|
||||||
|
const kp = lesson.knowledge_points || lesson.knowledgePoints || {};
|
||||||
|
const words = kp.words || [];
|
||||||
|
const patterns = kp.sentence_patterns || kp.sentencePatterns || [];
|
||||||
|
const ics = lesson.interactive_components || lesson.interactiveComponents || [];
|
||||||
|
const ces = lesson.consolidation_exercises || lesson.consolidationExercises || [];
|
||||||
|
|
||||||
|
let html = `
|
||||||
|
<div class="knowledge-section">
|
||||||
|
<h3>📗 单词 (${words.length}个)</h3>
|
||||||
|
<div class="word-list">
|
||||||
|
${words.map(w => `<div class="word-tag"><span class="en">${w.word}</span><span class="cn">${w.meaning}</span><span class="pos">${w.pos || ''}</span></div>`).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="knowledge-section">
|
||||||
|
<h3>📘 句型 (${patterns.length}个)</h3>
|
||||||
|
<div class="pattern-list">
|
||||||
|
${patterns.map(p => `<div class="pattern-tag"><span class="en">${p.pattern}</span><span class="cn">${p.meaning}</span></div>`).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="knowledge-section">
|
||||||
|
<h3>🎯 互动组件 (${ics.length}个)</h3>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>#</th><th>题型</th><th>知识点</th><th>详情</th><th>结果</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
${ics.map((ic, i) => {
|
||||||
|
const result = (ic.user_result || ic.userResult || '').toLowerCase();
|
||||||
|
const cls = result === 'perfect' ? 'perfect' : result === 'good' ? 'good' : 'oops';
|
||||||
|
const kpText = ic.knowledge || '-';
|
||||||
|
const audioBtn = ic.audio ? `<span class="audio-btn" onclick="playAudio('${ic.audio}', this)" title="播放录音">🔊</span>` : '';
|
||||||
|
return `<tr><td>${i + 1}</td><td>${ic.type}</td><td>${kpText}</td><td>${ic.detail}</td><td><span class="badge ${cls}">${result}</span>${audioBtn}</td></tr>`;
|
||||||
|
}).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="knowledge-section">
|
||||||
|
<h3>📝 学习巩固 (${ces.length}题)</h3>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>#</th><th>题型</th><th>知识点</th><th>详情</th><th>结果</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
${ces.map((ce, i) => {
|
||||||
|
const correct = ce.is_correct === true || ce.isCorrect === true;
|
||||||
|
const kpText = ce.knowledge || '-';
|
||||||
|
const audioBtn = ce.audio ? `<span class="audio-btn" onclick="playAudio('${ce.audio}', this)" title="播放录音">🔊</span>` : '';
|
||||||
|
return `<tr><td>${i + 1}</td><td>${ce.type}</td><td>${kpText}</td><td>${ce.detail}</td><td><span class="badge ${correct ? 'correct' : 'wrong'}">${correct ? '✓ 正确' : '✗ 错误'}</span>${audioBtn}</td></tr>`;
|
||||||
|
}).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
detail.innerHTML = html;
|
||||||
|
detailContainer.appendChild(detail);
|
||||||
|
});
|
||||||
|
|
||||||
|
let currentAudio = null;
|
||||||
|
function playAudio(url, el) {
|
||||||
|
if (currentAudio) {
|
||||||
|
currentAudio.pause();
|
||||||
|
currentAudio = null;
|
||||||
|
document.querySelectorAll('.audio-btn.playing').forEach(b => b.classList.remove('playing'));
|
||||||
|
}
|
||||||
|
const audio = new Audio(url);
|
||||||
|
currentAudio = audio;
|
||||||
|
el.classList.add('playing');
|
||||||
|
audio.play().catch(() => {});
|
||||||
|
audio.onended = () => { el.classList.remove('playing'); currentAudio = null; };
|
||||||
|
audio.onerror = () => { el.classList.remove('playing'); currentAudio = null; };
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchTab(idx) {
|
||||||
|
document.querySelectorAll('.lesson-card').forEach((c, i) => c.classList.toggle('active', i === idx));
|
||||||
|
document.querySelectorAll('.lesson-detail').forEach((d, i) => d.classList.toggle('active', i === idx));
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(t) {
|
||||||
|
if (!t || t === '-') return '-';
|
||||||
|
try {
|
||||||
|
const d = new Date(t);
|
||||||
|
return d.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||||
|
} catch { return t; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 能力训练展示 ====================
|
||||||
|
const abilityTbody = document.getElementById('ability-training-tbody');
|
||||||
|
|
||||||
|
abilityTraining.forEach((at, idx) => {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
const result = (at.result || '').toLowerCase();
|
||||||
|
const badgeCls = result === 'perfect' ? 'perfect' : result === 'good' ? 'good' : 'oops';
|
||||||
|
const subQuestionInfo = `${at.sub_question_correct || 0}/${at.sub_question_count || 0}`;
|
||||||
|
|
||||||
|
const atAudioBtn = at.audio ? `<span class="audio-btn" onclick="playAudio('${at.audio}', this)" title="播放录音">🔊</span>` : '';
|
||||||
|
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td>${idx + 1}</td>
|
||||||
|
<td>${at.source || '-'}</td>
|
||||||
|
<td>${at.type || '-'}</td>
|
||||||
|
<td>${at.title || '-'}</td>
|
||||||
|
<td>${at.detail || '-'}</td>
|
||||||
|
<td>${subQuestionInfo}</td>
|
||||||
|
<td><span class="badge ${badgeCls}">${result}</span>${atAudioBtn}</td>
|
||||||
|
`;
|
||||||
|
abilityTbody.appendChild(tr);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 响应式
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
interactiveChart.resize();
|
||||||
|
exerciseChart.resize();
|
||||||
|
abilityChart.resize();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
74
skills/study-analysis/assets/template.json
Normal file
74
skills/study-analysis/assets/template.json
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"role_id": 12345,
|
||||||
|
"level": 1,
|
||||||
|
"unit": 1,
|
||||||
|
"unit_name": "Unit 1: Hello World",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"lesson_id": 1,
|
||||||
|
"lesson_name": "Lesson 1: Greetings",
|
||||||
|
"entry_time": "2024-01-15T09:00:00Z",
|
||||||
|
"completion_time": "2024-01-15T09:25:30Z",
|
||||||
|
"knowledge_points": {
|
||||||
|
"words": [
|
||||||
|
{"word": "hello", "meaning": "你好", "pos": "int."},
|
||||||
|
{"word": "hi", "meaning": "嗨", "pos": "int."},
|
||||||
|
{"word": "goodbye", "meaning": "再见", "pos": "int."},
|
||||||
|
{"word": "name", "meaning": "名字", "pos": "n."}
|
||||||
|
],
|
||||||
|
"sentence_patterns": [
|
||||||
|
{"pattern": "Hello, my name is [name].", "meaning": "你好,我的名字是[名字]。", "example": "Hello, my name is Tom."},
|
||||||
|
{"pattern": "Nice to meet you.", "meaning": "很高兴认识你。", "example": "Nice to meet you too."}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"interactive_components": [
|
||||||
|
{"component_id": "ic_001", "title": "单词跟读 - hello", "detail": "请跟读单词 hello,注意发音准确", "user_result": "perfect"},
|
||||||
|
{"component_id": "ic_002", "title": "单词跟读 - hi", "detail": "请跟读单词 hi,注意语调自然", "user_result": "good"},
|
||||||
|
{"component_id": "ic_003", "title": "单词听选 - goodbye", "detail": "听录音,选择正确的单词 goodbye", "user_result": "perfect"},
|
||||||
|
{"component_id": "ic_004", "title": "单词拼写 - name", "detail": "根据发音,拼写出单词 name", "user_result": "good"},
|
||||||
|
{"component_id": "ic_005", "title": "句型跟读 - Hello, my name is...", "detail": "请跟读句型:Hello, my name is [name]", "user_result": "perfect"},
|
||||||
|
{"component_id": "ic_006", "title": "句型替换练习", "detail": "替换名字,完成句子:Hello, my name is ______", "user_result": "oops"},
|
||||||
|
{"component_id": "ic_007", "title": "情景对话 - 打招呼", "detail": "在情景中与角色进行打招呼对话", "user_result": "good"},
|
||||||
|
{"component_id": "ic_008", "title": "听力理解 - 问候语", "detail": "听录音,选择正确的问候回应", "user_result": "perfect"},
|
||||||
|
{"component_id": "ic_009", "title": "语音识别 - hi", "detail": "请说出单词 hi", "user_result": "good"},
|
||||||
|
{"component_id": "ic_010", "title": "图文匹配 - 单词", "detail": "将单词与对应的图片进行匹配", "user_result": "perfect"},
|
||||||
|
{"component_id": "ic_011", "title": "句型听选 - Nice to meet you", "detail": "听录音,选择正确的回应", "user_result": "good"},
|
||||||
|
{"component_id": "ic_012", "title": "角色扮演 - 自我介绍", "detail": "扮演角色进行自我介绍对话", "user_result": "perfect"},
|
||||||
|
{"component_id": "ic_013", "title": "单词排序 - 句子组成", "detail": "将单词按正确顺序排列成句子", "user_result": "oops"},
|
||||||
|
{"component_id": "ic_014", "title": "发音练习 - 语调训练", "detail": "练习疑问句和陈述句的语调区别", "user_result": "good"},
|
||||||
|
{"component_id": "ic_015", "title": "单词填空 - 补全句子", "detail": "在句子中填入正确的单词", "user_result": "perfect"},
|
||||||
|
{"component_id": "ic_016", "title": "情景判断 - 选择回应", "detail": "根据情景选择最合适的回应", "user_result": "good"},
|
||||||
|
{"component_id": "ic_017", "title": "综合对话 - 完整对话", "detail": "完成完整的打招呼和自我介绍对话", "user_result": "perfect"},
|
||||||
|
{"component_id": "ic_018", "title": "复习测验 - 本课回顾", "detail": "完成本课知识点的综合复习测验", "user_result": "good"}
|
||||||
|
],
|
||||||
|
"consolidation_exercises": [
|
||||||
|
{"exercise_id": "ce_001", "type": "单词听选", "detail": "听录音,从A、B、C三个选项中选择听到的单词", "is_correct": true},
|
||||||
|
{"exercise_id": "ce_002", "type": "单词拼写", "detail": "根据中文意思,写出对应的英文单词:你好", "is_correct": true},
|
||||||
|
{"exercise_id": "ce_003", "type": "图文匹配", "detail": "将单词 goodbye 与对应的图片匹配", "is_correct": false},
|
||||||
|
{"exercise_id": "ce_004", "type": "句型填空", "detail": "用正确的单词填空:Hello, my ______ is Tom.", "is_correct": true},
|
||||||
|
{"exercise_id": "ce_005", "type": "情景选择", "detail": "初次见面时,应该说:A. Hello B. Goodbye C. Hi", "is_correct": true},
|
||||||
|
{"exercise_id": "ce_006", "type": "句子排序", "detail": "将下列单词排列成正确的句子:name / my / is / Hello", "is_correct": true},
|
||||||
|
{"exercise_id": "ce_007", "type": "听力理解", "detail": "听对话,回答问题:他们正在做什么?", "is_correct": false},
|
||||||
|
{"exercise_id": "ce_008", "type": "发音选择", "detail": "听录音,选择包含 /eɪ/ 音的单词", "is_correct": true},
|
||||||
|
{"exercise_id": "ce_009", "type": "翻译练习", "detail": "将下列句子翻译成英文:很高兴认识你。", "is_correct": true},
|
||||||
|
{"exercise_id": "ce_010", "type": "对话补全", "detail": "补全对话:A: Hello! B: ______!", "is_correct": true},
|
||||||
|
{"exercise_id": "ce_011", "type": "单词分类", "detail": "将单词按问候语和非问候语分类", "is_correct": false},
|
||||||
|
{"exercise_id": "ce_012", "type": "句型转换", "detail": "将 Hello 转换为同义词", "is_correct": true},
|
||||||
|
{"exercise_id": "ce_013", "type": "连线匹配", "detail": "将单词与对应的中文意思连线", "is_correct": true},
|
||||||
|
{"exercise_id": "ce_014", "type": "判断正误", "detail": "判断句子是否正确:Goodbye 用于告别时说。", "is_correct": true},
|
||||||
|
{"exercise_id": "ce_015", "type": "选词填空", "detail": "选择合适的单词填空:______ to meet you. (Nice/Fine/Good)", "is_correct": true},
|
||||||
|
{"exercise_id": "ce_016", "type": "语音辨识", "detail": "听录音,判断是哪个单词", "is_correct": false},
|
||||||
|
{"exercise_id": "ce_017", "type": "完形填空", "detail": "阅读短文,选择合适的问候语填入空格", "is_correct": true},
|
||||||
|
{"exercise_id": "ce_018", "type": "综合测验", "detail": "完成本课知识的综合测验题", "is_correct": true}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"summary": {
|
||||||
|
"total_lessons": 5,
|
||||||
|
"completed_lessons": 1,
|
||||||
|
"total_interactive_components": 18,
|
||||||
|
"total_consolidation_exercises": 18,
|
||||||
|
"mastery_rate": 0.85,
|
||||||
|
"study_duration_minutes": 25
|
||||||
|
}
|
||||||
|
}
|
||||||
1656
skills/study-analysis/assets/test.json
Normal file
1656
skills/study-analysis/assets/test.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"data": {
|
||||||
|
"data": "hello world"
|
||||||
|
},
|
||||||
|
"msg": "",
|
||||||
|
"traceID": "bfc13d6b1b5669affffa521d675d7141"
|
||||||
|
}
|
||||||
1662
skills/study-analysis/output/15868_L1_U1_20260402180728.json
Normal file
1662
skills/study-analysis/output/15868_L1_U1_20260402180728.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,401 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>学情分析报告 - 用户10293 Level1 Unit1</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||||
|
background-color: #f0f2f5;
|
||||||
|
padding: 24px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.container { max-width: 1200px; margin: 0 auto; }
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 32px;
|
||||||
|
border-radius: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.header h1 { font-size: 28px; margin-bottom: 8px; }
|
||||||
|
.header .meta { font-size: 14px; opacity: 0.9; }
|
||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
.card h2 {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-left: 4px solid #667eea;
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.stat-item {
|
||||||
|
background: linear-gradient(135deg, #f5f7fa 0%, #e8ecf1 100%);
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.stat-item .label { font-size: 13px; color: #888; margin-bottom: 8px; }
|
||||||
|
.stat-item .value { font-size: 28px; font-weight: 700; color: #333; }
|
||||||
|
.stat-item .value.green { color: #52c41a; }
|
||||||
|
.stat-item .value.orange { color: #fa8c16; }
|
||||||
|
.stat-item .value.blue { color: #1890ff; }
|
||||||
|
.stat-item .value.purple { color: #722ed1; }
|
||||||
|
.chart-container { height: 400px; width: 100%; }
|
||||||
|
.chart-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.chart-row { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
.lesson-tab {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.lesson-tab .tab-btn {
|
||||||
|
padding: 8px 20px;
|
||||||
|
border: 2px solid #667eea;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: white;
|
||||||
|
color: #667eea;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.lesson-tab .tab-btn.active, .lesson-tab .tab-btn:hover {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.lesson-detail { display: none; }
|
||||||
|
.lesson-detail.active { display: block; }
|
||||||
|
.knowledge-section { margin-bottom: 20px; }
|
||||||
|
.knowledge-section h3 {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #555;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.word-list, .pattern-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.word-tag {
|
||||||
|
background: #e6f7ff;
|
||||||
|
border: 1px solid #91d5ff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.word-tag .en { font-weight: 600; color: #1890ff; }
|
||||||
|
.word-tag .cn { color: #666; margin-left: 6px; }
|
||||||
|
.word-tag .pos { color: #999; font-size: 12px; margin-left: 4px; }
|
||||||
|
.pattern-tag {
|
||||||
|
background: #fff7e6;
|
||||||
|
border: 1px solid #ffd591;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
flex: 1 1 300px;
|
||||||
|
}
|
||||||
|
.pattern-tag .en { font-weight: 600; color: #fa8c16; }
|
||||||
|
.pattern-tag .cn { color: #666; display: block; margin-top: 4px; font-size: 13px; }
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
table th {
|
||||||
|
background: #f5f7fa;
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
border-bottom: 2px solid #e8e8e8;
|
||||||
|
}
|
||||||
|
table td {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
table tr:hover td { background: #fafafa; }
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.badge.perfect { background: #f6ffed; color: #52c41a; border: 1px solid #b7eb8f; }
|
||||||
|
.badge.good { background: #e6f7ff; color: #1890ff; border: 1px solid #91d5ff; }
|
||||||
|
.badge.oops { background: #fff2e8; color: #fa541c; border: 1px solid #ffbb96; }
|
||||||
|
.badge.correct { background: #f6ffed; color: #52c41a; border: 1px solid #b7eb8f; }
|
||||||
|
.badge.wrong { background: #fff1f0; color: #f5222d; border: 1px solid #ffa39e; }
|
||||||
|
.time-info {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.time-info span { display: flex; align-items: center; gap: 6px; }
|
||||||
|
.raw-data {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-family: "SF Mono", "Fira Code", monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>📚 学情分析报告</h1>
|
||||||
|
<div class="meta">
|
||||||
|
用户ID:10293 | Level 1 | Unit 1 | 生成时间:2026-04-02 16:05:08
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>📊 总览</h2>
|
||||||
|
<div class="stats-grid" id="stats-grid"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chart-row">
|
||||||
|
<div class="card">
|
||||||
|
<h2>📈 互动组件表现</h2>
|
||||||
|
<div id="interactive-chart" class="chart-container"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>📝 巩固练习正确率</h2>
|
||||||
|
<div id="exercise-chart" class="chart-container"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>📖 课程详情</h2>
|
||||||
|
<div class="lesson-tab" id="lesson-tabs"></div>
|
||||||
|
<div id="lesson-details"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>🔍 原始数据</h2>
|
||||||
|
<div class="raw-data" id="raw-data"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const data = {"code": 200, "data": {"data": "hello world"}, "msg": "", "traceID": "bfc13d6b1b5669affffa521d675d7141"};
|
||||||
|
|
||||||
|
// 兼容:data可能直接就是lessons数组,也可能有外层包装
|
||||||
|
const lessons = data.lessons || data.data?.lessons || (Array.isArray(data) ? data : []);
|
||||||
|
const summary = data.summary || data.data?.summary || null;
|
||||||
|
|
||||||
|
// ==================== 总览统计 ====================
|
||||||
|
const statsGrid = document.getElementById('stats-grid');
|
||||||
|
const totalLessons = lessons.length;
|
||||||
|
let totalInteractive = 0, totalPerfect = 0, totalGood = 0, totalOops = 0;
|
||||||
|
let totalExercises = 0, totalCorrect = 0;
|
||||||
|
|
||||||
|
lessons.forEach(lesson => {
|
||||||
|
const ics = lesson.interactive_components || lesson.interactiveComponents || [];
|
||||||
|
const ces = lesson.consolidation_exercises || lesson.consolidationExercises || [];
|
||||||
|
totalInteractive += ics.length;
|
||||||
|
ics.forEach(ic => {
|
||||||
|
const r = (ic.user_result || ic.userResult || '').toLowerCase();
|
||||||
|
if (r === 'perfect') totalPerfect++;
|
||||||
|
else if (r === 'good') totalGood++;
|
||||||
|
else if (r === 'oops') totalOops++;
|
||||||
|
});
|
||||||
|
totalExercises += ces.length;
|
||||||
|
ces.forEach(ce => {
|
||||||
|
if (ce.is_correct === true || ce.isCorrect === true) totalCorrect++;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const exerciseAccuracy = totalExercises > 0 ? Math.round(totalCorrect / totalExercises * 100) : 0;
|
||||||
|
const interactiveScore = totalInteractive > 0 ? Math.round((totalPerfect * 3 + totalGood * 2 + totalOops * 1) / (totalInteractive * 3) * 100) : 0;
|
||||||
|
|
||||||
|
const statsItems = [
|
||||||
|
{ label: '课程数', value: totalLessons, cls: 'blue' },
|
||||||
|
{ label: '互动组件总数', value: totalInteractive, cls: 'purple' },
|
||||||
|
{ label: 'Perfect 次数', value: totalPerfect, cls: 'green' },
|
||||||
|
{ label: 'Good 次数', value: totalGood, cls: 'blue' },
|
||||||
|
{ label: 'Oops 次数', value: totalOops, cls: 'orange' },
|
||||||
|
{ label: '巩固题总数', value: totalExercises, cls: 'purple' },
|
||||||
|
{ label: '巩固正确率', value: exerciseAccuracy + '%', cls: 'green' },
|
||||||
|
{ label: '互动综合得分', value: interactiveScore + '%', cls: 'blue' }
|
||||||
|
];
|
||||||
|
statsItems.forEach(item => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'stat-item';
|
||||||
|
div.innerHTML = `<div class="label">${item.label}</div><div class="value ${item.cls}">${item.value}</div>`;
|
||||||
|
statsGrid.appendChild(div);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==================== 互动组件表现饼图 ====================
|
||||||
|
const interactiveChart = echarts.init(document.getElementById('interactive-chart'));
|
||||||
|
interactiveChart.setOption({
|
||||||
|
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
|
||||||
|
legend: { bottom: 10 },
|
||||||
|
color: ['#52c41a', '#1890ff', '#fa541c'],
|
||||||
|
series: [{
|
||||||
|
type: 'pie',
|
||||||
|
radius: ['40%', '65%'],
|
||||||
|
avoidLabelOverlap: true,
|
||||||
|
itemStyle: { borderRadius: 8, borderColor: '#fff', borderWidth: 2 },
|
||||||
|
label: { show: true, fontSize: 14 },
|
||||||
|
data: [
|
||||||
|
{ value: totalPerfect, name: 'Perfect' },
|
||||||
|
{ value: totalGood, name: 'Good' },
|
||||||
|
{ value: totalOops, name: 'Oops' }
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==================== 巩固练习柱状图(按课程) ====================
|
||||||
|
const exerciseChart = echarts.init(document.getElementById('exercise-chart'));
|
||||||
|
const lessonLabels = lessons.map((l, i) => l.lesson_name || l.lessonName || `Lesson ${i + 1}`);
|
||||||
|
const correctCounts = [];
|
||||||
|
const wrongCounts = [];
|
||||||
|
lessons.forEach(lesson => {
|
||||||
|
const ces = lesson.consolidation_exercises || lesson.consolidationExercises || [];
|
||||||
|
let correct = 0, wrong = 0;
|
||||||
|
ces.forEach(ce => {
|
||||||
|
if (ce.is_correct === true || ce.isCorrect === true) correct++;
|
||||||
|
else wrong++;
|
||||||
|
});
|
||||||
|
correctCounts.push(correct);
|
||||||
|
wrongCounts.push(wrong);
|
||||||
|
});
|
||||||
|
exerciseChart.setOption({
|
||||||
|
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
||||||
|
legend: { bottom: 10 },
|
||||||
|
grid: { left: '3%', right: '4%', bottom: '15%', top: '5%', containLabel: true },
|
||||||
|
xAxis: { type: 'category', data: lessonLabels, axisLabel: { rotate: 20, fontSize: 12 } },
|
||||||
|
yAxis: { type: 'value' },
|
||||||
|
color: ['#52c41a', '#f5222d'],
|
||||||
|
series: [
|
||||||
|
{ name: '正确', type: 'bar', stack: 'total', data: correctCounts, itemStyle: { borderRadius: [4, 4, 0, 0] } },
|
||||||
|
{ name: '错误', type: 'bar', stack: 'total', data: wrongCounts, itemStyle: { borderRadius: [4, 4, 0, 0] } }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==================== 课程详情 tabs ====================
|
||||||
|
const tabContainer = document.getElementById('lesson-tabs');
|
||||||
|
const detailContainer = document.getElementById('lesson-details');
|
||||||
|
|
||||||
|
lessons.forEach((lesson, idx) => {
|
||||||
|
// tab 按钮
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.className = 'tab-btn' + (idx === 0 ? ' active' : '');
|
||||||
|
btn.textContent = lesson.lesson_name || lesson.lessonName || `Lesson ${idx + 1}`;
|
||||||
|
btn.onclick = () => switchTab(idx);
|
||||||
|
tabContainer.appendChild(btn);
|
||||||
|
|
||||||
|
// 详情区域
|
||||||
|
const detail = document.createElement('div');
|
||||||
|
detail.className = 'lesson-detail' + (idx === 0 ? ' active' : '');
|
||||||
|
detail.id = `lesson-${idx}`;
|
||||||
|
|
||||||
|
const entryTime = lesson.entry_time || lesson.entryTime || '-';
|
||||||
|
const completionTime = lesson.completion_time || lesson.completionTime || '-';
|
||||||
|
const kp = lesson.knowledge_points || lesson.knowledgePoints || {};
|
||||||
|
const words = kp.words || [];
|
||||||
|
const patterns = kp.sentence_patterns || kp.sentencePatterns || [];
|
||||||
|
const ics = lesson.interactive_components || lesson.interactiveComponents || [];
|
||||||
|
const ces = lesson.consolidation_exercises || lesson.consolidationExercises || [];
|
||||||
|
|
||||||
|
let html = `
|
||||||
|
<div class="time-info">
|
||||||
|
<span>🕐 进入时间:${formatTime(entryTime)}</span>
|
||||||
|
<span>✅ 完成时间:${formatTime(completionTime)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="knowledge-section">
|
||||||
|
<h3>📗 单词 (${words.length}个)</h3>
|
||||||
|
<div class="word-list">
|
||||||
|
${words.map(w => `<div class="word-tag"><span class="en">${w.word}</span><span class="cn">${w.meaning}</span><span class="pos">${w.pos || ''}</span></div>`).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="knowledge-section">
|
||||||
|
<h3>📘 句型 (${patterns.length}个)</h3>
|
||||||
|
<div class="pattern-list">
|
||||||
|
${patterns.map(p => `<div class="pattern-tag"><span class="en">${p.pattern}</span><span class="cn">${p.meaning}</span></div>`).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="knowledge-section">
|
||||||
|
<h3>🎯 互动组件 (${ics.length}个)</h3>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>#</th><th>标题</th><th>详情</th><th>结果</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
${ics.map((ic, i) => {
|
||||||
|
const result = (ic.user_result || ic.userResult || '').toLowerCase();
|
||||||
|
const cls = result === 'perfect' ? 'perfect' : result === 'good' ? 'good' : 'oops';
|
||||||
|
return `<tr><td>${i + 1}</td><td>${ic.title}</td><td>${ic.detail}</td><td><span class="badge ${cls}">${result}</span></td></tr>`;
|
||||||
|
}).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="knowledge-section">
|
||||||
|
<h3>📝 学习巩固 (${ces.length}题)</h3>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>#</th><th>题型</th><th>详情</th><th>结果</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
${ces.map((ce, i) => {
|
||||||
|
const correct = ce.is_correct === true || ce.isCorrect === true;
|
||||||
|
return `<tr><td>${i + 1}</td><td>${ce.type}</td><td>${ce.detail}</td><td><span class="badge ${correct ? 'correct' : 'wrong'}">${correct ? '✓ 正确' : '✗ 错误'}</span></td></tr>`;
|
||||||
|
}).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
detail.innerHTML = html;
|
||||||
|
detailContainer.appendChild(detail);
|
||||||
|
});
|
||||||
|
|
||||||
|
function switchTab(idx) {
|
||||||
|
document.querySelectorAll('.tab-btn').forEach((b, i) => b.classList.toggle('active', i === idx));
|
||||||
|
document.querySelectorAll('.lesson-detail').forEach((d, i) => d.classList.toggle('active', i === idx));
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(t) {
|
||||||
|
if (!t || t === '-') return '-';
|
||||||
|
try {
|
||||||
|
const d = new Date(t);
|
||||||
|
return d.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||||
|
} catch { return t; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 原始数据
|
||||||
|
document.getElementById('raw-data').textContent = JSON.stringify(data, null, 2);
|
||||||
|
|
||||||
|
// 响应式
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
interactiveChart.resize();
|
||||||
|
exerciseChart.resize();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
278
skills/study-analysis/output/test_data_20260402161243.json
Normal file
278
skills/study-analysis/output/test_data_20260402161243.json
Normal file
@ -0,0 +1,278 @@
|
|||||||
|
{
|
||||||
|
"role_id": 12345,
|
||||||
|
"level": 1,
|
||||||
|
"unit": 1,
|
||||||
|
"unit_name": "Unit 1: Hello World",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"lesson_id": 1,
|
||||||
|
"lesson_name": "Lesson 1: Greetings",
|
||||||
|
"entry_time": "2024-01-15T09:00:00Z",
|
||||||
|
"completion_time": "2024-01-15T09:25:30Z",
|
||||||
|
"knowledge_points": {
|
||||||
|
"words": [
|
||||||
|
{
|
||||||
|
"word": "hello",
|
||||||
|
"meaning": "你好",
|
||||||
|
"pos": "int."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "hi",
|
||||||
|
"meaning": "嗨",
|
||||||
|
"pos": "int."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "goodbye",
|
||||||
|
"meaning": "再见",
|
||||||
|
"pos": "int."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "name",
|
||||||
|
"meaning": "名字",
|
||||||
|
"pos": "n."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sentence_patterns": [
|
||||||
|
{
|
||||||
|
"pattern": "Hello, my name is [name].",
|
||||||
|
"meaning": "你好,我的名字是[名字]。",
|
||||||
|
"example": "Hello, my name is Tom."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "Nice to meet you.",
|
||||||
|
"meaning": "很高兴认识你。",
|
||||||
|
"example": "Nice to meet you too."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"interactive_components": [
|
||||||
|
{
|
||||||
|
"component_id": "ic_001",
|
||||||
|
"title": "单词跟读 - hello",
|
||||||
|
"detail": "请跟读单词 hello,注意发音准确",
|
||||||
|
"user_result": "perfect"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component_id": "ic_002",
|
||||||
|
"title": "单词跟读 - hi",
|
||||||
|
"detail": "请跟读单词 hi,注意语调自然",
|
||||||
|
"user_result": "good"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component_id": "ic_003",
|
||||||
|
"title": "单词听选 - goodbye",
|
||||||
|
"detail": "听录音,选择正确的单词 goodbye",
|
||||||
|
"user_result": "perfect"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component_id": "ic_004",
|
||||||
|
"title": "单词拼写 - name",
|
||||||
|
"detail": "根据发音,拼写出单词 name",
|
||||||
|
"user_result": "good"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component_id": "ic_005",
|
||||||
|
"title": "句型跟读 - Hello, my name is...",
|
||||||
|
"detail": "请跟读句型:Hello, my name is [name]",
|
||||||
|
"user_result": "perfect"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component_id": "ic_006",
|
||||||
|
"title": "句型替换练习",
|
||||||
|
"detail": "替换名字,完成句子:Hello, my name is ______",
|
||||||
|
"user_result": "oops"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component_id": "ic_007",
|
||||||
|
"title": "情景对话 - 打招呼",
|
||||||
|
"detail": "在情景中与角色进行打招呼对话",
|
||||||
|
"user_result": "good"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component_id": "ic_008",
|
||||||
|
"title": "听力理解 - 问候语",
|
||||||
|
"detail": "听录音,选择正确的问候回应",
|
||||||
|
"user_result": "perfect"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component_id": "ic_009",
|
||||||
|
"title": "语音识别 - hi",
|
||||||
|
"detail": "请说出单词 hi",
|
||||||
|
"user_result": "good"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component_id": "ic_010",
|
||||||
|
"title": "图文匹配 - 单词",
|
||||||
|
"detail": "将单词与对应的图片进行匹配",
|
||||||
|
"user_result": "perfect"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component_id": "ic_011",
|
||||||
|
"title": "句型听选 - Nice to meet you",
|
||||||
|
"detail": "听录音,选择正确的回应",
|
||||||
|
"user_result": "good"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component_id": "ic_012",
|
||||||
|
"title": "角色扮演 - 自我介绍",
|
||||||
|
"detail": "扮演角色进行自我介绍对话",
|
||||||
|
"user_result": "perfect"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component_id": "ic_013",
|
||||||
|
"title": "单词排序 - 句子组成",
|
||||||
|
"detail": "将单词按正确顺序排列成句子",
|
||||||
|
"user_result": "oops"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component_id": "ic_014",
|
||||||
|
"title": "发音练习 - 语调训练",
|
||||||
|
"detail": "练习疑问句和陈述句的语调区别",
|
||||||
|
"user_result": "good"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component_id": "ic_015",
|
||||||
|
"title": "单词填空 - 补全句子",
|
||||||
|
"detail": "在句子中填入正确的单词",
|
||||||
|
"user_result": "perfect"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component_id": "ic_016",
|
||||||
|
"title": "情景判断 - 选择回应",
|
||||||
|
"detail": "根据情景选择最合适的回应",
|
||||||
|
"user_result": "good"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component_id": "ic_017",
|
||||||
|
"title": "综合对话 - 完整对话",
|
||||||
|
"detail": "完成完整的打招呼和自我介绍对话",
|
||||||
|
"user_result": "perfect"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component_id": "ic_018",
|
||||||
|
"title": "复习测验 - 本课回顾",
|
||||||
|
"detail": "完成本课知识点的综合复习测验",
|
||||||
|
"user_result": "good"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"consolidation_exercises": [
|
||||||
|
{
|
||||||
|
"exercise_id": "ce_001",
|
||||||
|
"type": "单词听选",
|
||||||
|
"detail": "听录音,从A、B、C三个选项中选择听到的单词",
|
||||||
|
"is_correct": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"exercise_id": "ce_002",
|
||||||
|
"type": "单词拼写",
|
||||||
|
"detail": "根据中文意思,写出对应的英文单词:你好",
|
||||||
|
"is_correct": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"exercise_id": "ce_003",
|
||||||
|
"type": "图文匹配",
|
||||||
|
"detail": "将单词 goodbye 与对应的图片匹配",
|
||||||
|
"is_correct": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"exercise_id": "ce_004",
|
||||||
|
"type": "句型填空",
|
||||||
|
"detail": "用正确的单词填空:Hello, my ______ is Tom.",
|
||||||
|
"is_correct": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"exercise_id": "ce_005",
|
||||||
|
"type": "情景选择",
|
||||||
|
"detail": "初次见面时,应该说:A. Hello B. Goodbye C. Hi",
|
||||||
|
"is_correct": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"exercise_id": "ce_006",
|
||||||
|
"type": "句子排序",
|
||||||
|
"detail": "将下列单词排列成正确的句子:name / my / is / Hello",
|
||||||
|
"is_correct": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"exercise_id": "ce_007",
|
||||||
|
"type": "听力理解",
|
||||||
|
"detail": "听对话,回答问题:他们正在做什么?",
|
||||||
|
"is_correct": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"exercise_id": "ce_008",
|
||||||
|
"type": "发音选择",
|
||||||
|
"detail": "听录音,选择包含 /eɪ/ 音的单词",
|
||||||
|
"is_correct": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"exercise_id": "ce_009",
|
||||||
|
"type": "翻译练习",
|
||||||
|
"detail": "将下列句子翻译成英文:很高兴认识你。",
|
||||||
|
"is_correct": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"exercise_id": "ce_010",
|
||||||
|
"type": "对话补全",
|
||||||
|
"detail": "补全对话:A: Hello! B: ______!",
|
||||||
|
"is_correct": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"exercise_id": "ce_011",
|
||||||
|
"type": "单词分类",
|
||||||
|
"detail": "将单词按问候语和非问候语分类",
|
||||||
|
"is_correct": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"exercise_id": "ce_012",
|
||||||
|
"type": "句型转换",
|
||||||
|
"detail": "将 Hello 转换为同义词",
|
||||||
|
"is_correct": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"exercise_id": "ce_013",
|
||||||
|
"type": "连线匹配",
|
||||||
|
"detail": "将单词与对应的中文意思连线",
|
||||||
|
"is_correct": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"exercise_id": "ce_014",
|
||||||
|
"type": "判断正误",
|
||||||
|
"detail": "判断句子是否正确:Goodbye 用于告别时说。",
|
||||||
|
"is_correct": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"exercise_id": "ce_015",
|
||||||
|
"type": "选词填空",
|
||||||
|
"detail": "选择合适的单词填空:______ to meet you. (Nice/Fine/Good)",
|
||||||
|
"is_correct": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"exercise_id": "ce_016",
|
||||||
|
"type": "语音辨识",
|
||||||
|
"detail": "听录音,判断是哪个单词",
|
||||||
|
"is_correct": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"exercise_id": "ce_017",
|
||||||
|
"type": "完形填空",
|
||||||
|
"detail": "阅读短文,选择合适的问候语填入空格",
|
||||||
|
"is_correct": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"exercise_id": "ce_018",
|
||||||
|
"type": "综合测验",
|
||||||
|
"detail": "完成本课知识的综合测验题",
|
||||||
|
"is_correct": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"summary": {
|
||||||
|
"total_lessons": 5,
|
||||||
|
"completed_lessons": 1,
|
||||||
|
"total_interactive_components": 18,
|
||||||
|
"total_consolidation_exercises": 18,
|
||||||
|
"mastery_rate": 0.85,
|
||||||
|
"study_duration_minutes": 25
|
||||||
|
}
|
||||||
|
}
|
||||||
1326
skills/study-analysis/output/test_data_20260402162938.json
Normal file
1326
skills/study-analysis/output/test_data_20260402162938.json
Normal file
File diff suppressed because it is too large
Load Diff
1326
skills/study-analysis/output/test_data_20260402163831.json
Normal file
1326
skills/study-analysis/output/test_data_20260402163831.json
Normal file
File diff suppressed because it is too large
Load Diff
1336
skills/study-analysis/output/test_data_20260402164851.json
Normal file
1336
skills/study-analysis/output/test_data_20260402164851.json
Normal file
File diff suppressed because it is too large
Load Diff
1336
skills/study-analysis/output/test_data_20260402165344.json
Normal file
1336
skills/study-analysis/output/test_data_20260402165344.json
Normal file
File diff suppressed because it is too large
Load Diff
1428
skills/study-analysis/output/test_data_20260402170445.json
Normal file
1428
skills/study-analysis/output/test_data_20260402170445.json
Normal file
File diff suppressed because it is too large
Load Diff
1651
skills/study-analysis/output/test_data_20260402172134.json
Normal file
1651
skills/study-analysis/output/test_data_20260402172134.json
Normal file
File diff suppressed because it is too large
Load Diff
1656
skills/study-analysis/output/test_data_20260402173214.json
Normal file
1656
skills/study-analysis/output/test_data_20260402173214.json
Normal file
File diff suppressed because it is too large
Load Diff
1656
skills/study-analysis/output/test_data_20260402174923.json
Normal file
1656
skills/study-analysis/output/test_data_20260402174923.json
Normal file
File diff suppressed because it is too large
Load Diff
1656
skills/study-analysis/output/test_data_20260402175102.json
Normal file
1656
skills/study-analysis/output/test_data_20260402175102.json
Normal file
File diff suppressed because it is too large
Load Diff
190
skills/study-analysis/scripts/analysis.py
Normal file
190
skills/study-analysis/scripts/analysis.py
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import sys
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# 配置项
|
||||||
|
API_URL = "https://api.valavala.com/v2/user/unit/learn/info"
|
||||||
|
OPERATOR = "Kingson"
|
||||||
|
# 存储目录配置
|
||||||
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
OUTPUT_DIR = os.path.join(BASE_DIR, "output")
|
||||||
|
TEMPLATE_PATH = os.path.join(BASE_DIR, "assets/template.html")
|
||||||
|
TEST_DATA_PATH = os.path.join(BASE_DIR, "assets/test.json")
|
||||||
|
|
||||||
|
def init_dirs():
|
||||||
|
"""初始化必要的目录"""
|
||||||
|
if not os.path.exists(OUTPUT_DIR):
|
||||||
|
os.makedirs(OUTPUT_DIR)
|
||||||
|
|
||||||
|
def parse_params():
|
||||||
|
"""解析并验证输入参数
|
||||||
|
|
||||||
|
支持两种模式:
|
||||||
|
1. 正常模式:python analysis.py [角色ID] [Level] [Unit]
|
||||||
|
2. 测试模式:python analysis.py test
|
||||||
|
"""
|
||||||
|
if len(sys.argv) == 2 and sys.argv[1].lower() == "test":
|
||||||
|
# 测试模式
|
||||||
|
return "test", None, None
|
||||||
|
|
||||||
|
if len(sys.argv) != 4:
|
||||||
|
print("参数错误!使用方式:")
|
||||||
|
print(" 正常模式:python analysis.py [角色ID] [Level] [Unit]")
|
||||||
|
print(" 测试模式:python analysis.py test")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
role_id = sys.argv[1]
|
||||||
|
level = sys.argv[2]
|
||||||
|
unit = sys.argv[3]
|
||||||
|
|
||||||
|
# 验证角色ID是数字
|
||||||
|
if not role_id.isdigit():
|
||||||
|
print("错误:角色ID必须是数字")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# 处理Level参数,支持Level1/L1/1等格式
|
||||||
|
level = level.lower().replace("level", "").replace("l", "")
|
||||||
|
if not level.isdigit() or int(level) not in [1, 2]:
|
||||||
|
print("错误:Level只能是1或2,支持格式:Level1/L1/1/Level2/L2/2")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# 处理Unit参数,支持Unit1/U1/1等格式
|
||||||
|
unit = unit.lower().replace("unit", "").replace("u", "")
|
||||||
|
if not unit.isdigit() or int(unit) < 1:
|
||||||
|
print("错误:Unit必须是正整数,支持格式:Unit1/U1/1")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
return int(role_id), int(level), int(unit)
|
||||||
|
|
||||||
|
def request_api(role_id, level, unit):
|
||||||
|
"""请求学情分析API
|
||||||
|
|
||||||
|
调用接口: https://api.valavala.com/v2/user/unit/learn/info
|
||||||
|
参数:
|
||||||
|
- operator: Kingson (固定值)
|
||||||
|
- level: L1 或 L2 (根据传入的level参数)
|
||||||
|
- userId: 用户角色ID
|
||||||
|
- unitIndex: 单元号
|
||||||
|
"""
|
||||||
|
params = {
|
||||||
|
"operator": OPERATOR,
|
||||||
|
"level": f"L{level}",
|
||||||
|
"userId": role_id,
|
||||||
|
"unitIndex": unit
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(API_URL, params=params, timeout=30)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
print("API请求超时,请稍后重试")
|
||||||
|
sys.exit(1)
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
print("API连接失败,请检查网络连接")
|
||||||
|
sys.exit(1)
|
||||||
|
except requests.exceptions.HTTPError as e:
|
||||||
|
print(f"API返回错误状态码:{e.response.status_code}")
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"API请求失败:{str(e)}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def load_test_data():
|
||||||
|
"""加载测试数据"""
|
||||||
|
try:
|
||||||
|
with open(TEST_DATA_PATH, "r", encoding="utf-8") as f:
|
||||||
|
return json.load(f)
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"错误:测试数据文件不存在 {TEST_DATA_PATH}")
|
||||||
|
sys.exit(1)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
print(f"错误:测试数据文件格式不正确 - {str(e)}")
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"加载测试数据失败:{str(e)}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def save_data(role_id, level, unit, data):
|
||||||
|
"""保存API返回的原始数据"""
|
||||||
|
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
|
||||||
|
filename = f"{role_id}_L{level}_U{unit}_{timestamp}.json"
|
||||||
|
file_path = os.path.join(OUTPUT_DIR, filename)
|
||||||
|
|
||||||
|
with open(file_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
return file_path
|
||||||
|
|
||||||
|
def save_test_data(data):
|
||||||
|
"""保存测试数据副本"""
|
||||||
|
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
|
||||||
|
filename = f"test_data_{timestamp}.json"
|
||||||
|
file_path = os.path.join(OUTPUT_DIR, filename)
|
||||||
|
|
||||||
|
with open(file_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
return file_path
|
||||||
|
|
||||||
|
def generate_html(role_id, level, unit, data):
|
||||||
|
"""生成可视化HTML报告"""
|
||||||
|
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
|
||||||
|
filename = f"study_report_{role_id}_L{level}_U{unit}_{timestamp}.html"
|
||||||
|
html_path = os.path.join(OUTPUT_DIR, filename)
|
||||||
|
|
||||||
|
# 读取模板
|
||||||
|
with open(TEMPLATE_PATH, "r", encoding="utf-8") as f:
|
||||||
|
template = f.read()
|
||||||
|
|
||||||
|
# 替换模板中的数据占位符
|
||||||
|
html_content = template.replace("{{DATA}}", json.dumps(data, ensure_ascii=False))
|
||||||
|
html_content = html_content.replace("{{ROLE_ID}}", str(role_id))
|
||||||
|
html_content = html_content.replace("{{LEVEL}}", str(level))
|
||||||
|
html_content = html_content.replace("{{UNIT}}", str(unit))
|
||||||
|
html_content = html_content.replace("{{GENERATE_TIME}}", datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
||||||
|
|
||||||
|
# 保存HTML文件
|
||||||
|
with open(html_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(html_content)
|
||||||
|
|
||||||
|
return html_path
|
||||||
|
|
||||||
|
def main():
|
||||||
|
init_dirs()
|
||||||
|
role_id, level, unit = parse_params()
|
||||||
|
|
||||||
|
# 判断是否为测试模式
|
||||||
|
is_test_mode = role_id == "test"
|
||||||
|
|
||||||
|
if is_test_mode:
|
||||||
|
print("【测试模式】使用本地测试数据生成报告...")
|
||||||
|
api_data = load_test_data()
|
||||||
|
# 测试模式使用默认值或从数据中读取
|
||||||
|
role_id = api_data.get("role_id", 99999)
|
||||||
|
level = api_data.get("level", 1)
|
||||||
|
unit = api_data.get("unit", 1)
|
||||||
|
print(f"测试数据:用户{role_id} Level{level} Unit{unit}")
|
||||||
|
else:
|
||||||
|
print(f"正在分析用户{role_id} Level{level} Unit{unit}的学习情况...")
|
||||||
|
# 请求API
|
||||||
|
api_data = request_api(role_id, level, unit)
|
||||||
|
print("API数据获取成功")
|
||||||
|
|
||||||
|
# 保存原始数据
|
||||||
|
if is_test_mode:
|
||||||
|
data_path = save_test_data(api_data)
|
||||||
|
else:
|
||||||
|
data_path = save_data(role_id, level, unit, api_data)
|
||||||
|
print(f"原始数据已保存到:{data_path}")
|
||||||
|
|
||||||
|
# 生成HTML报告
|
||||||
|
html_path = generate_html(role_id, level, unit, api_data)
|
||||||
|
print(f"可视化报告已生成:{html_path}")
|
||||||
|
print(f"请访问以下地址查看报告:file://{html_path}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
Reference in New Issue
Block a user