Explorar o código

Initial commit

wlh hai 3 semanas
achega
a77b37f704

+ 71 - 0
.gitignore

@@ -0,0 +1,71 @@
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+.Python
+env/
+venv/
+ENV/
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# Flask
+instance/
+.webassets-cache
+
+# 环境变量文件
+config.env
+*.env
+!config.env.example
+
+# IDE
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+
+# 操作系统
+.DS_Store
+Thumbs.db
+desktop.ini
+
+# 日志文件
+*.log
+
+# 临时文件
+*.tmp
+*.temp
+
+# 压缩文件(保留原始文件)
+*.zip
+*.rar
+*.7z
+
+# 图片文件(如果不需要版本控制)
+# *.jpg
+# *.png
+# *.gif
+
+# 数据库文件(如果有)
+*.db
+*.sqlite
+*.sqlite3
+
+# 备份文件
+*.bak
+*.backup

+ 348 - 0
README.md

@@ -0,0 +1,348 @@
+# 知了数学题库系统
+
+一个基于 Flask 开发的数学题库管理系统,支持题目的增删改查、审核、知识点管理、PDF导出等功能。
+
+## 项目概述
+
+本系统是一个完整的数学题库管理平台,提供了题目管理、知识点层级管理、题目审核、AI辅助优化、PDF导出等核心功能。系统采用 Flask 作为后端框架,使用 MySQL 数据库存储数据,前端使用 Tailwind CSS 构建现代化界面。
+
+## 功能特性
+
+### 1. 题目管理
+- **题目列表展示**:按知识点分类展示题目,支持统计信息(总数、合格数、不合格数、待审核数)
+- **题目详情查看**:查看题目的完整信息,包括题干、选项、答案、解析等
+- **题目编辑**:支持编辑题目的所有字段
+- **题目添加**:通过表单添加新题目,支持单个录入和批量导入
+- **题目删除**:支持删除题目(需确认)
+- **题目搜索**:支持按题目ID搜索
+- **年级选项**:其他题目(未关联知识点)支持选择年级(小学/初中/高中)
+
+### 2. 知识点管理
+- **层级结构**:支持章(Chapter)、节(Section)、小节(Subsection)三级知识点结构
+- **知识点目录**:左侧导航栏展示知识点层级树
+- **知识点统计**:每个知识点显示题目数量、合格率等统计信息
+- **知识点编辑**:支持编辑知识点名称
+- **知识点添加**:支持添加新的节或小节
+- **学段筛选**:目前只显示初中知识点(小学和高中已注释)
+
+### 3. 题目审核
+- **审核模式**:专门的审核页面,显示所有未审核的题目
+- **审核操作**:支持标记题目为"合格"或"不合格",并填写审核原因
+- **审核统计**:按知识点统计未审核题目数量
+- **同步机制**:审核通过("合格")的题目会自动同步到正式的 `questions` 表
+
+### 4. AI辅助功能
+- **难度评分**:使用AI对题目进行难度评分,输出维度分数和难度等级(筑基/提分/培优)
+- **查重检测**:支持题目查重功能,检测重复题目
+
+### 5. PDF导出
+- **远程导出**:调用远程API接口生成PDF
+- **批量导出**:支持批量导出多个题目
+
+### 6. 教材管理
+- **教材节点**:支持按教材目录节点管理题目
+- **节点统计**:显示每个教材节点的题目统计信息
+
+## 技术架构
+
+### 后端技术
+- **框架**:Flask 3.x
+- **数据库**:MySQL(使用 mysql-connector-python)
+- **Web服务器**:Waitress(生产环境)
+
+### 前端技术
+- **CSS框架**:Tailwind CSS(CDN)
+- **数学公式渲染**:KaTeX
+- **图标库**:Remix Icon
+- **模板引擎**:Jinja2
+
+### AI集成
+- **AI接口**:支持 OpenAI 兼容接口
+- **查重接口**:`http://47.77.199.85:8888`
+
+## 安装与配置
+
+### 1. 环境要求
+- Python 3.7+
+- MySQL 数据库
+
+### 2. 安装依赖
+
+```bash
+pip install -r requirements.txt
+```
+
+### 3. 配置文件
+
+复制 `config.env.example` 为 `config.env`,并修改配置:
+
+```bash
+cp config.env.example config.env
+```
+
+编辑 `config.env` 文件,配置以下内容:
+
+```env
+# 数据库配置
+DB_HOST=your_db_host
+DB_PORT=3306
+DB_DATABASE=your_database_name
+DB_USERNAME=your_username
+DB_PASSWORD=your_password
+
+# Web服务配置
+WEB_HOST=127.0.0.1
+WEB_PORT=5000
+
+# AI配置(可选)
+AI_API_KEY=your_api_key
+AI_BASE_URL=https://api.openai.com/v1
+AI_MODEL_NAME=gpt-5.2
+
+# PDF导出配置
+PDF_API_URL=https://teaching-content.chunsunqiuzhu.com/api/questions/pdf
+PDF_STUDENT_ID=44
+
+# 查重接口配置
+DUPLICATE_CHECK_API_URL=http://47.77.199.85:8888
+```
+
+### 4. 数据库结构
+
+系统使用以下主要数据表:
+- `questions_tem`:题目表(临时表,用于审核)
+- `questions`:题目表(正式表,只包含审核通过的题目)
+- `knowledge_points_copy1`:知识点表
+- `textbook_catalog_nodes`:教材目录节点表
+
+## 使用方法
+
+### 1. 启动服务
+
+**开发模式**:
+```bash
+python app.py
+```
+
+**生产模式**(使用 Waitress):
+```bash
+python run_web.py
+```
+
+### 2. 访问系统
+
+在浏览器中访问:`http://localhost:5000`
+
+### 3. 主要功能使用
+
+#### 查看题目列表
+- 访问首页 `/`,查看按知识点分类的题目卡片
+- 点击知识点卡片,查看该知识点下的所有题目
+
+#### 审核题目
+- 访问 `/audit_questions`,查看所有未审核的题目
+- 点击题目,进入详情页进行审核
+- 填写审核原因,标记为"合格"或"不合格"
+- **注意**:只有审核通过("合格")的题目才会同步到正式的 `questions` 表
+
+#### 添加题目
+- 访问 `/question_management`,点击"录入题目"按钮
+- 填写题目信息(题干、选项、答案等)
+- 选择知识点或教材节点
+- 如果是"其他题目"(未关联知识点),必须选择年级
+- 提交保存
+
+#### 编辑题目
+- 在题目列表或详情页点击"编辑"按钮
+- 修改题目信息
+- 保存更改
+
+#### 搜索题目
+- 访问 `/search`,按题目ID搜索
+- 或访问 `/search_id`,直接输入题目ID
+
+#### 导出PDF
+- 在题目详情页点击"导出PDF"按钮
+- 选择本地导出或远程导出
+- 下载生成的PDF文件
+
+#### 管理知识点
+- 访问 `/kp_management`,管理知识点
+- 点击"增加"按钮添加知识点
+- 点击知识点旁的编辑图标,修改知识点名称
+
+## API接口
+
+### 题目相关
+- `POST /audit`:审核题目(审核通过会自动同步到 questions 表)
+- `POST /create_question`:创建题目(保存到 questions_tem 表)
+- `POST /update_question`:更新题目
+- `POST /api/delete_question/<question_code>`:删除题目
+
+### AI相关
+- `POST /api/score`:难度评分
+- `POST /api/check_duplicate`:查重检测(代理接口)
+
+### 知识点相关
+- `POST /add_kp_node`:添加知识点节点
+- `POST /update_kp_node`:更新知识点节点
+
+### PDF导出
+- `GET /export_pdf_remote/<question_code>`:远程导出PDF
+
+## 数据同步机制
+
+### questions_tem → questions 同步规则
+
+1. **未审核题目**(`audit_reason` 为 NULL 或空):
+   - 只存在于 `questions_tem` 表
+   - **不会**同步到 `questions` 表
+
+2. **审核通过**(`audit_reason = "合格"`):
+   - 更新 `questions_tem` 表的审核状态
+   - **自动同步**到 `questions` 表(INSERT 或 UPDATE)
+
+3. **审核不通过**(`audit_reason = "不合格"`):
+   - 更新 `questions_tem` 表的审核状态
+   - **不会**同步到 `questions` 表
+
+**总结**:只有审核通过("合格")的题目才会同步到正式的 `questions` 表。
+
+## 项目结构
+
+```
+知了数学题库/
+├── app.py                 # 主应用文件
+├── run_web.py            # Web服务器启动文件
+├── requirements.txt      # Python依赖
+├── config.env            # 配置文件(需自行创建,不提交到Git)
+├── config.env.example    # 配置示例文件
+├── README.md             # 项目说明文档
+├── .gitignore            # Git忽略文件
+├── templates/            # HTML模板目录
+│   ├── layout.html       # 基础布局模板
+│   ├── index.html        # 首页模板
+│   ├── questions.html    # 题目列表模板
+│   ├── detail.html       # 题目详情模板
+│   ├── edit.html         # 题目编辑模板
+│   ├── question_management.html # 题目管理模板
+│   ├── kp_management.html # 知识点管理模板
+│   └── ...
+├── static/               # 静态文件目录
+└── 知了数学题库/         # 数据文件目录
+    └── tree_new.json     # 知识点树结构数据
+```
+
+## 常见问题
+
+### 1. 数据库连接失败
+- 检查 `config.env` 中的数据库配置是否正确
+- 确认数据库服务是否运行
+- 检查网络连接和防火墙设置
+
+### 2. PDF导出失败
+- 检查 `PDF_API_URL` 配置是否正确,网络是否可达
+
+### 3. AI功能不可用
+- 检查 `AI_API_KEY` 是否配置
+- 确认 API 接口地址和模型名称正确
+- 检查网络连接
+
+### 4. 知识点不显示
+- 确认数据库中有知识点数据
+- 检查知识点表的 `grade` 字段(目前只显示"初中")
+
+### 5. Git推送失败
+- 确认已安装 Git
+- 检查 Git 凭据配置
+- 确认远程仓库地址正确
+
+## 开发说明
+
+### 代码规范
+- 使用 Python 类型提示
+- 函数和类添加文档字符串
+- 错误处理使用 try-except 块
+- 数据库连接使用连接池管理
+
+### 扩展功能
+- 添加新的路由:在 `app.py` 中使用 `@app.route()` 装饰器
+- 添加新的模板:在 `templates/` 目录下创建HTML文件
+- 修改样式:编辑模板文件中的 Tailwind CSS 类
+
+## 更新日志
+
+### 2024-12-XX(最新)
+- **知识点管理页面优化**:
+  - 在知识点管理页面中,将小学和高中部分注释掉
+  - 知识点管理页面现在只展示初中(grade='初中')的知识点
+  - 修改了知识点查询、下拉选择列表和题目统计查询,都添加了 `WHERE grade = '初中'` 条件
+  - 注意:数据库中的 grade 字段存储的是中文("小学"、"初中"、"高中"),不是数字
+- **更新查重接口地址**:
+  - 将查重检测接口地址从 `http://192.168.124.42:8888` 更新为 `http://47.77.199.85:8888`
+  - 更新了 `/api/check_duplicate` 和 `/api/confirm_repeat` 两个接口的代理地址
+- **添加选项预览功能**:
+  - 在"题目管理"的所有录入页面(单个录入和批量导入)中添加了选项预览框(JSON格式)
+  - 选项预览框支持双向同步:修改各个选项输入框会自动更新JSON预览,修改JSON预览会自动同步到各个选项输入框
+  - 选项预览框支持直接编辑JSON格式,方便批量修改选项内容
+  - 选项预览框仅在选择题时显示
+- **添加年级选项功能(必填项)**:
+  - 在"题目管理-其他题目"的两个录入页面(单个录入和批量导入)中添加了年级选择字段
+  - 年级选项为枚举值:小学(传1)、初中(传2)、高中(传3)
+  - 年级字段仅在"其他题目"(未关联知识点)时显示,且为必填项
+  - 前端添加了必填验证:提交时会检查年级是否已选择
+  - 后端接口 `/create_question` 已支持接收和处理 `grade` 参数,并验证年级值的有效性(1、2、3)
+  - 后端添加了业务规则验证:如果是"其他题目"(没有kp_code),则grade字段必须存在
+- **优化题干显示逻辑**:
+  - 改进了题干容器的样式,添加了背景色和边框,使题干更易读
+  - 优化了数学公式的自动渲染逻辑,确保题干中的数学公式能够正确显示
+  - 改进了页面加载时的数学公式渲染处理,避免重复渲染
+- **删除优化题目功能**:
+  - 移除了"优化题目"按钮及其相关弹窗
+  - 删除了题干优化相关的JavaScript代码(openOptimizeModal、closeOptimizeModal、replaceStem等函数)
+  - 简化了题目详情页面的操作按钮,提升了用户体验
+
+## Git 操作说明
+
+### 首次提交到远程仓库
+
+```bash
+# 1. 初始化 Git 仓库(如果还没有)
+git init
+
+# 2. 添加远程仓库
+git remote add origin https://git.yunzhixue.cn/wlh/wlh.git
+
+# 3. 添加所有文件
+git add .
+
+# 4. 提交代码
+git commit -m "初始提交:知了数学题库系统"
+
+# 5. 设置主分支
+git branch -M main
+
+# 6. 推送到远程仓库
+git push -u origin main
+```
+
+### 后续更新
+
+```bash
+# 1. 添加修改的文件
+git add .
+
+# 2. 提交更改
+git commit -m "更新说明"
+
+# 3. 推送到远程仓库
+git push
+```
+
+## 许可证
+
+本项目为内部使用项目,未经授权不得外传。
+
+## 联系方式
+
+如有问题或建议,请联系项目维护者。

+ 3474 - 0
app.py

@@ -0,0 +1,3474 @@
+import os
+import sys
+import json
+import re
+import tempfile
+import urllib.request
+import urllib.error
+import urllib.parse
+import mysql.connector
+import datetime
+import random
+import base64
+import socket
+from typing import Optional, List
+from flask import Flask, render_template, request, jsonify, url_for, send_file, redirect
+from html.parser import HTMLParser
+from urllib.parse import urljoin, urlparse
+
+BASE_DIR = os.path.dirname(os.path.abspath(__file__))
+
+def _load_dotenv_if_exists():
+    """
+    允许把数据库/AI 等配置放在同目录 config.env 里(不改代码即可生效)。
+
+    重要:需要在读取 os.getenv(...) 之前执行。
+    """
+    try:
+        from dotenv import load_dotenv  # type: ignore
+    except Exception:
+        return
+
+    # 兼容源码运行 / PyInstaller 打包运行
+    candidates = [
+        os.path.join(BASE_DIR, "config.env"),
+        os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), "config.env"),
+    ]
+    for p in candidates:
+        if os.path.exists(p):
+            load_dotenv(p, override=True)
+            break
+
+
+_load_dotenv_if_exists()
+
+def resource_path(*parts: str) -> str:
+    """
+    资源路径兼容:
+    - 源码运行:基于当前文件目录
+    - PyInstaller 打包:基于 _MEIPASS
+    """
+    base = getattr(sys, "_MEIPASS", BASE_DIR)
+    return os.path.join(base, *parts)
+
+app = Flask(
+    __name__,
+    template_folder=resource_path("templates"),
+    static_folder=resource_path("static"),
+)
+
+# 可能的主键列名(不同库/表可能不一致)
+_PK_CANDIDATE_COLS = ["id", "question_id", "pk_id", "qid"]
+_PK_COLS_CACHE: Optional[List[str]] = None
+
+# 数据库配置
+# 默认值:按你刚刚给的 JDBC 配置来(也支持用环境变量覆盖)
+DB_CONFIG = {
+    "host": os.getenv("DB_HOST", "rm-f8ze60yirdj8786u2wo.mysql.rds.aliyuncs.com"),
+    "port": int(os.getenv("DB_PORT", "3306")),
+    "database": os.getenv("DB_DATABASE", "math-conten-online2"),
+    "user": os.getenv("DB_USERNAME", "root"),
+    "password": os.getenv("DB_PASSWORD", "csqz@20255"),
+    "charset": "utf8mb4",
+}
+
+# 远程 PDF 生成接口(你提供的)
+PDF_API_URL = os.getenv("PDF_API_URL", "https://teaching-content.chunsunqiuzhu.com/api/questions/pdf")
+PDF_STUDENT_ID = os.getenv("PDF_STUDENT_ID", "44")  # 默认写死 44(也支持用环境变量覆盖)
+
+# AI 优化题干(通过环境变量配置,避免把 key 写进代码)
+AI_API_KEY = os.getenv("AI_API_KEY", "").strip()
+AI_BASE_URL = (os.getenv("AI_BASE_URL") or "").strip()  # 为空则使用官方默认
+AI_MODEL_NAME = (os.getenv("AI_MODEL_NAME") or "gpt-5.2").strip()
+
+# 难度评分配置(使用相同的 AI 配置)
+DIFFICULTY_SCORING_API_KEY = os.getenv("DIFFICULTY_SCORING_API_KEY", AI_API_KEY).strip()
+DIFFICULTY_SCORING_MODEL = os.getenv("DIFFICULTY_SCORING_MODEL", AI_MODEL_NAME).strip()
+DIFFICULTY_SCORING_TEMPERATURE = float(os.getenv("DIFFICULTY_SCORING_TEMPERATURE", "0.0"))
+DIFFICULTY_SCORING_TOP_P = float(os.getenv("DIFFICULTY_SCORING_TOP_P", "0.3"))
+DIFFICULTY_SCORING_PRESENCE_PENALTY = float(os.getenv("DIFFICULTY_SCORING_PRESENCE_PENALTY", "0.0"))
+DIFFICULTY_SCORING_FREQUENCY_PENALTY = float(os.getenv("DIFFICULTY_SCORING_FREQUENCY_PENALTY", "0.0"))
+
+# 默认难度评分提示词
+DIFFICULTY_SCORING_PROMPT = """你是一名数学题库难度建模专家。你的任务不是解题,而是基于题目文本、公式及图像信息,对题目难度进行结构化量化评估,用于企业级题库难度算法。本评估包含四个评分维度,四个维度等权重参与最终得分,但在判断时需遵循重要性顺序:推理与运算步数最高,知识点数量第二,抽象与构造要求第三,题型常规性最低。每个维度的取值只能是 0.33、0.66 或 1.00,不允许出现其他数值。(1)推理与运算步数:直接或少量步骤取 0.33,中等连续步骤取 0.66,多步链式推理或存在中间结论取 1.00;(2)知识点数量:单一核心知识点取 0.33,两个知识点取 0.66,三个及以上或跨模块综合取 1.00;(3)抽象与构造要求:无需抽象或构造取 0.33,需要一定抽象或简单构造取 0.66,需要明显构造或较高层次建模取 1.00;(4)题型常规性:高度常规模板题取 0.33,存在一定变式取 0.66,非典型或创新题型取 1.00。四个维度得分相加后需归一化为 0~1 的 total_score,并按以下规则映射难度等级:0~0.25 为"筑基",0.25~0.5 为"提分",0.5 以上为"培优"。请仅以严格 JSON 格式输出 dimension_scores、total_score 和 difficulty_level,不得输出其他字段,不得展开解题过程。"""
+
+AI_STEM_PROMPT_DEFAULT = r"""你是“题干格式修复器”,不是解题老师。
+你的任务:把输入题干(stem)做最小幅度的格式清洗,使其更适合 PDF / 系统展示。
+
+【核心原则:原文已兼容就不要改】(最重要,违反此条视为失败)
+- 如果原文已经是 PDF 兼容格式(例如:数字、单位、上标³、普通文本混合),就不要做任何改动。
+- **严禁添加**:反斜杠 `\`、逗号 `,`、LaTeX 命令(如 `\,`、`\n`、`^3` 等)、换行符 `\n` 等原本不存在的内容。
+- **严禁改动**:不要把上标³改成 `^3`,不要把普通文本改成 LaTeX 格式,不要把 Unicode 上标改成 LaTeX 上标。
+- **严禁添加换行**:不要添加 `\n` 或任何换行符,保持原文的换行格式不变。如果原文是连续文本,输出也必须是连续文本。
+
+【严禁解题/严禁新增内容】(非常重要)
+- 不要解题:不要推导、不要解释、不要补充答案、不要给出步骤、不要增加任何原文中不存在的内容。
+- 不要把题干改写成解析/答案。
+
+【必须保留】(非常重要)
+1) 保留原题干的结构与排版:不调整顺序、不合并/拆分段落、不新增/删除句子。
+2) 保留所有 HTML 标签与结构原样不动(例如 <p>、<br>、<span> 等)。
+3) 题干中出现的所有 <svg>...</svg> 片段必须在输出中**逐字复制**(字符、空格、换行、属性顺序、大小写都必须完全一致),不得省略、重排、格式化。
+4) **保留上标格式**:如果原文是 `dm³`、`cm³`、`m²` 等 Unicode 上标,必须保持原样,严禁改成 `dm^3`、`cm^3`、`m^2` 等 LaTeX 格式。
+5) **保留单位格式**:如果原文是 `2.5 dm³` 这种格式(数字+空格+单位),保持原样,不要添加 `\,`、`\n` 等 LaTeX 命令。
+
+【允许的唯一改动】(只允许做下面这些替换;除此之外任何字符都不要改)
+6) 公式包裹:对行内公式 `$...$`,仅去掉外层 `$`,并保留内部内容的原顺序与原字符(不要删改里面的数字/变量/符号)。
+7) 符号替换(只替换命中的这些 LaTeX 命令,其它所有内容保持原样):
+   - \triangle → △
+   - \text{cm} → cm
+   - \text{cm²} → cm²
+   - \neq → !=
+   - \ge → ≥
+   - \le → ≤
+   - \Rightarrow => =>
+   - \sqrt → sqrt
+   - \perp → ⟂
+8) 填空线:把所有“连续下划线填空线”(长度不固定)统一成:________(8 个下划线)。
+9) **问号填空**:把题干中的问号 `?`(用于表示填空)替换成:________(8 个下划线)。注意:不要删除问号,要替换成填空线。
+
+【输出格式】
+10) 输出必须是一段“可直接写回 stem 字段”的文本/HTML。
+11) 只输出结果,不要加任何解释、标题、Markdown 标记(不要使用 **、列表编号等)。
+
+下面是题干原文,请按规则输出清洗后的题干:
+"""
+
+_env_prompt = os.getenv("AI_STEM_PROMPT")
+# 注意:如果环境变量存在但为空(例如 AI_STEM_PROMPT=),os.getenv 会返回空字符串,
+# 这会把默认提示词覆盖掉,导致 AI 没有收到规则。这里显式做“空值回退默认”。
+AI_STEM_PROMPT = (_env_prompt.strip() if isinstance(_env_prompt, str) else "").strip() or AI_STEM_PROMPT_DEFAULT
+
+_RE_SVG_BLOCK = re.compile(r"<svg\b[\s\S]*?</svg>", re.IGNORECASE)
+
+def _env_bool(name: str, default: bool) -> bool:
+    val = os.getenv(name)
+    if val is None:
+        return default
+    return str(val).strip().lower() in {"1", "true", "yes", "y", "on"}
+
+def get_db_connection():
+    """
+    use_pure=True:避免 C 扩展在部分环境抛出 “RuntimeError: Failed raising error.”
+    connection_timeout:避免卡死
+    ssl:默认按 JDBC 里的 useSSL=true 走;如果你本地/网络环境不支持,也可以用环境变量关闭
+        DB_USE_SSL=false
+    """
+    use_ssl = _env_bool("DB_USE_SSL", True)
+    try:
+        return mysql.connector.connect(
+            **DB_CONFIG,
+            use_pure=True,
+            connection_timeout=5,
+            ssl_disabled=(not use_ssl),
+        )
+    except Exception:
+        # 兜底:有些环境 SSL 握手会失败,尝试降级不启用 SSL,让你至少能先用起来
+        if use_ssl:
+            return mysql.connector.connect(
+                **DB_CONFIG,
+                use_pure=True,
+                connection_timeout=5,
+                ssl_disabled=True,
+            )
+        raise
+
+def render_db_error(e: Exception):
+    return render_template(
+        "db_error.html",
+        error=str(e),
+        db_host=DB_CONFIG.get("host"),
+        db_port=DB_CONFIG.get("port"),
+        db_name=DB_CONFIG.get("database"),
+        db_user=DB_CONFIG.get("user"),
+    ), 500
+
+
+def get_pk_columns(conn) -> List[str]:
+    """
+    获取 questions_tem 表里"可能的主键列名"中实际存在的列。
+    只要命中一个就能用主键 ID 搜索跳转。
+    """
+    global _PK_COLS_CACHE
+    if _PK_COLS_CACHE is not None:
+        return _PK_COLS_CACHE
+
+    cols: List[str] = []
+    try:
+        cursor = conn.cursor()
+        for c in _PK_CANDIDATE_COLS:
+            cursor.execute("SHOW COLUMNS FROM questions_tem LIKE %s", (c,))
+            if cursor.fetchone():
+                cols.append(c)
+    except Exception:
+        cols = []
+
+    _PK_COLS_CACHE = cols
+    return cols
+
+
+def get_question_pk(question: dict) -> Optional[str]:
+    """
+    从题目记录里提取“题目主键ID”(用于远程导出 PDF 接口的 question_ids)。
+    兼容不同库的列名:id / question_id / pk_id / qid
+    """
+    for c in _PK_CANDIDATE_COLS:
+        v = question.get(c)
+        if v is None:
+            continue
+        s = str(v).strip()
+        if s:
+            return s
+    return None
+
+
+def request_remote_pdf_url(question_id: str, include_grading: bool = True) -> List[str]:
+    """
+    调用你给的接口生成 PDF,返回 pdf_url 列表(可能是一个或多个)。
+    返回结构示例:
+      {"success": true, "data": {"pdf_url": "https://...pdf"}}  # 单个
+      或
+      {"success": true, "data": {"pdf_url": ["https://...pdf1", "https://...pdf2"]}}  # 多个
+    """
+    payload = {
+        "question_ids": [str(question_id)], 
+        "student_id": str(PDF_STUDENT_ID),
+        "include_grading": include_grading
+    }
+    data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
+
+    req = urllib.request.Request(
+        PDF_API_URL,
+        data=data,
+        headers={"Content-Type": "application/json"},
+        method="POST",
+    )
+
+    try:
+        with urllib.request.urlopen(req, timeout=20) as resp:
+            raw = resp.read().decode("utf-8", errors="replace")
+    except urllib.error.HTTPError as e:
+        detail = ""
+        try:
+            detail = e.read().decode("utf-8", errors="replace")
+        except Exception:
+            detail = ""
+        raise RuntimeError(f"PDF 接口 HTTP 错误:{e.code} {e.reason}. {detail}".strip())
+    except Exception as e:
+        raise RuntimeError(f"调用 PDF 接口失败:{e}")
+
+    try:
+        obj = json.loads(raw)
+    except Exception:
+        raise RuntimeError(f"PDF 接口返回不是合法 JSON:{raw[:300]}")
+
+    if not isinstance(obj, dict) or not obj.get("success"):
+        msg = obj.get("message") if isinstance(obj, dict) else ""
+        raise RuntimeError(f"PDF 生成失败:{msg or raw[:300]}")
+
+    data_obj = obj.get("data") if isinstance(obj, dict) else None
+    if not isinstance(data_obj, dict):
+        raise RuntimeError(f"PDF 接口返回的 data 不是对象:{raw[:300]}")
+    
+    # 接口返回格式:{"pdf_url": "...", "grading_pdf_url": "..."}
+    # 只打开 grading_pdf_url(评分PDF),不打开 pdf_url(题目PDF)
+    grading_pdf_url = data_obj.get("grading_pdf_url")
+    if not grading_pdf_url or not isinstance(grading_pdf_url, str) or not grading_pdf_url.strip():
+        raise RuntimeError(f"PDF 接口未返回有效的 grading_pdf_url:{raw[:300]}")
+    
+    return [str(grading_pdf_url).strip()]
+
+
+def _extract_svg_blocks(text: str) -> List[str]:
+    if not text:
+        return []
+    return _RE_SVG_BLOCK.findall(text)
+
+
+def _svg_blocks_equal(a: str, b: str) -> bool:
+    """
+    严格校验:新旧题干中的 <svg>...</svg> 片段列表必须完全一致(数量、顺序、内容都一致)。
+    """
+    return _extract_svg_blocks(a) == _extract_svg_blocks(b)
+
+
+def call_ai_optimize_stem(stem_html: str) -> str:
+    """
+    调用 AI,把 stem 原文优化成“安全版 stem”,返回一段可直接写入 stem 的文本/HTML。
+    注意:AI 返回的是一段文本,我们直接当作新的 stem 处理。
+    """
+    if not AI_API_KEY:
+        raise RuntimeError("缺少 AI_API_KEY:请在 config.env 里配置 AI_API_KEY")
+
+    base = AI_BASE_URL.rstrip("/")
+    if not base:
+        base = "https://api.openai.com/v1"
+
+    url = f"{base}/chat/completions"
+
+    # 把“规则”放 system,把题干原文放 user
+    payload = {
+        "model": AI_MODEL_NAME,
+        "messages": [
+            {"role": "system", "content": AI_STEM_PROMPT},
+            {"role": "user", "content": stem_html or ""},
+        ],
+        # 关键:不设置时,部分兼容实现会默认只生成很短的内容(看起来像“没读 prompt”)。
+        # 为了让模型能完整输出“带 SVG 的题干”,这里给足 token 预算。
+        # 注意:部分 API 端点只支持 max_completion_tokens,不支持 max_tokens,所以只传前者。
+        "max_completion_tokens": int(os.getenv("AI_MAX_TOKENS", "4096")),
+        "temperature": float(os.getenv("AI_TEMPERATURE", "0.0")),
+    }
+
+    data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
+    req = urllib.request.Request(
+        url,
+        data=data,
+        headers={
+            "Content-Type": "application/json",
+            "Authorization": f"Bearer {AI_API_KEY}",
+        },
+        method="POST",
+    )
+
+    try:
+        with urllib.request.urlopen(req, timeout=60) as resp:
+            raw = resp.read().decode("utf-8", errors="replace")
+    except urllib.error.HTTPError as e:
+        detail = ""
+        try:
+            detail = e.read().decode("utf-8", errors="replace")
+        except Exception:
+            detail = ""
+        raise RuntimeError(f"AI 接口 HTTP 错误:{e.code} {e.reason}. {detail}".strip())
+    except Exception as e:
+        raise RuntimeError(f"调用 AI 接口失败:{e}")
+
+    try:
+        obj = json.loads(raw)
+    except Exception:
+        raise RuntimeError(f"AI 接口返回不是合法 JSON:{raw[:300]}")
+
+    try:
+        content = obj["choices"][0]["message"]["content"]
+    except Exception:
+        raise RuntimeError(f"AI 接口返回结构不符合预期:{raw[:300]}")
+
+    s = str(content or "").strip()
+    if not s:
+        raise RuntimeError("AI 返回为空")
+
+    # 后处理:清理 AI 误加的无意义换行符 `\n`
+    # 规则:只清理那些在连续文本中间的单个 `\n`(前后都是字母/数字/中文,没有空格或标点)
+    # 保留 HTML 标签内的换行(如 <svg> 内的换行)和原本有意义的段落换行
+    # 匹配:字母/数字/中文 + \n + 字母/数字/中文(这种通常是误加的)
+    s = re.sub(r"([\w\u4e00-\u9fff])\n([\w\u4e00-\u9fff])", r"\1 \2", s)
+    # 清理多余的连续空格(避免替换后产生多个空格)
+    s = re.sub(r" +", " ", s)
+
+    # 保护:如果输入很长但输出极短,通常是 token 上限/模型拒答/接口兼容问题。
+    # 直接抛错,避免用户误以为“优化成功”。
+    if len((stem_html or "").strip()) >= 200 and len(s) <= 40:
+        raise RuntimeError(
+            "AI 返回内容过短(疑似被 max_tokens 截断或模型未按要求输出)。请重试;"
+            "如仍复现,可把 AI_MODEL_NAME / AI_BASE_URL 发我进一步定位。"
+        )
+    return s
+
+# 加载知识点映射和层级结构
+# ==================== 难度评分相关函数 ====================
+
+class ImageExtractor(HTMLParser):
+    """从HTML中提取图片URL"""
+    def __init__(self):
+        super().__init__()
+        self.image_urls = []
+    
+    def handle_starttag(self, tag, attrs):
+        if tag == 'img':
+            for attr in attrs:
+                if attr[0] in ['src', 'data-src']:
+                    url = attr[1]
+                    if url:
+                        self.image_urls.append(url)
+
+def extract_images_from_html(html_content, base_url=None):
+    """从HTML内容中提取所有图片URL"""
+    parser = ImageExtractor()
+    parser.feed(html_content)
+    
+    image_urls = parser.image_urls
+    
+    # 如果是相对路径,转换为绝对路径
+    if base_url and image_urls:
+        image_urls = [urljoin(base_url, url) if not urlparse(url).netloc else url 
+                     for url in image_urls]
+    
+    return image_urls
+
+def extract_text_from_html(html_content):
+    """从HTML中提取纯文本(去除标签)"""
+    # 简单的HTML标签去除
+    text = re.sub(r'<[^>]+>', '', html_content)
+    # 去除多余的空白字符
+    text = re.sub(r'\s+', ' ', text).strip()
+    return text
+
+def parse_json_response(text):
+    """从AI响应中提取JSON"""
+    # 尝试直接解析
+    try:
+        return json.loads(text)
+    except:
+        pass
+    
+    # 尝试提取JSON部分(如果响应包含其他文本)
+    json_match = re.search(r'\{[\s\S]*\}', text)
+    if json_match:
+        try:
+            return json.loads(json_match.group())
+        except:
+            pass
+    
+    # 如果都失败,返回None
+    return None
+
+def get_openai_client():
+    """获取OpenAI客户端"""
+    try:
+        from openai import OpenAI
+        api_key = DIFFICULTY_SCORING_API_KEY or AI_API_KEY
+        if not api_key:
+            return None
+        
+        base_url = AI_BASE_URL.rstrip("/") if AI_BASE_URL else None
+        if base_url:
+            return OpenAI(api_key=api_key, base_url=base_url)
+        else:
+            return OpenAI(api_key=api_key)
+    except ImportError:
+        return None
+    except Exception as e:
+        print(f"警告: OpenAI客户端初始化失败: {e}")
+        return None
+
+def load_kp_structure():
+    """
+    加载知识点结构,只保留三个层级:chapter、section、subsection
+    返回:
+    - kp_map: {id: label} 映射
+    - kp_hierarchy: 层级结构列表,每个元素包含 chapter、section、subsection 信息
+    """
+    kp_map = {}
+    kp_hierarchy = []
+    
+    # 优先加载 tree_new.json,如果不存在则从 D 盘复制
+    tree_file = resource_path('知了数学题库', 'tree_new.json')
+    if not os.path.exists(tree_file):
+        # 尝试从 D 盘复制文件
+        source_file = 'D:/tree_new.json'
+        if os.path.exists(source_file):
+            try:
+                import shutil
+                shutil.copy2(source_file, tree_file)
+                print(f"已从 {source_file} 复制到 {tree_file}")
+            except Exception as e:
+                print(f"复制文件失败: {e}")
+        # 如果复制失败或源文件不存在,使用旧的 tree(1)(1).json
+        if not os.path.exists(tree_file):
+            tree_file = resource_path('知了数学题库', 'tree(1)(1).json')
+    
+    if os.path.exists(tree_file):
+        try:
+            with open(tree_file, 'r', encoding='utf-8') as f:
+                tree_data = json.load(f)
+            
+            def process_node(node, parent_chapter=None, parent_section=None):
+                """递归处理节点,只保留 chapter、section、subsection 三个层级"""
+                if not isinstance(node, dict):
+                    return
+                
+                node_id = node.get("id")
+                node_label = node.get("label", "")
+                kp_level = node.get("kp_level", "")
+                
+                # 记录所有节点的映射
+                if node_id and node_label:
+                    kp_map[str(node_id)] = node_label
+                
+                # 只处理三个层级
+                if kp_level == "chapter":
+                    # 章节级别
+                    chapter_info = {
+                        "id": node_id,
+                        "label": node_label,
+                        "sections": []
+                    }
+                    kp_hierarchy.append(chapter_info)
+                    
+                    # 处理子节点(sections)
+                    children = node.get("children", [])
+                    for child in children:
+                        process_node(child, parent_chapter=chapter_info, parent_section=None)
+                
+                elif kp_level == "section":
+                    # 节级别
+                    if parent_chapter:
+                        section_info = {
+                            "id": node_id,
+                            "label": node_label,
+                            "subsections": []
+                        }
+                        parent_chapter["sections"].append(section_info)
+                        
+                        # 处理子节点(subsections)
+                        children = node.get("children", [])
+                        for child in children:
+                            process_node(child, parent_chapter=parent_chapter, parent_section=section_info)
+                
+                elif kp_level == "subsection":
+                    # 小节级别
+                    if parent_section:
+                        subsection_info = {
+                            "id": node_id,
+                            "label": node_label
+                        }
+                        parent_section["subsections"].append(subsection_info)
+                
+                # 如果节点有 children,继续递归处理(但只处理符合层级的)
+                children = node.get("children", [])
+                if children and kp_level not in ["chapter", "section", "subsection"]:
+                    # 对于其他层级(如 lesson),继续向下查找
+                    for child in children:
+                        process_node(child, parent_chapter=parent_chapter, parent_section=parent_section)
+            
+            # 处理根节点
+            if isinstance(tree_data, dict):
+                children = tree_data.get("children", [])
+                for child in children:
+                    process_node(child)
+            elif isinstance(tree_data, list):
+                for item in tree_data:
+                    process_node(item)
+                    
+        except Exception as e:
+            print(f"Error loading tree.json: {e}")
+            import traceback
+            traceback.print_exc()
+    
+    return kp_map, kp_hierarchy
+
+def load_kp_structure_from_db():
+    """
+    从MySQL数据库加载知识点结构,构建树形结构
+    返回:
+    - kp_map: {kp_code: name} 映射
+    - kp_tree: 树形结构列表,每个节点包含 children 列表和 question_count
+    """
+    kp_map = {}
+    kp_tree = []
+    
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor(dictionary=True)
+        
+        # 查询所有知识点及其题目数量
+        cursor.execute("""
+            SELECT 
+                kp.kp_code, 
+                kp.name, 
+                kp.parent_kp_code, 
+                kp.grade,
+                COUNT(q.question_code) as question_count
+            FROM knowledge_points_copy1 kp
+            LEFT JOIN questions_tem q ON q.kp_code = kp.kp_code
+            GROUP BY kp.kp_code, kp.name, kp.parent_kp_code, kp.grade
+            ORDER BY kp.kp_code
+        """)
+        all_kps = cursor.fetchall()
+        
+        # 构建映射
+        for kp in all_kps:
+            kp_map[kp['kp_code']] = kp['name']
+        
+        # 构建节点字典
+        nodes = {}
+        for kp in all_kps:
+            question_count = int(kp.get('question_count', 0) or 0)
+            nodes[kp['kp_code']] = {
+                'kp_code': kp['kp_code'],
+                'name': kp['name'],
+                'parent_kp_code': kp['parent_kp_code'],
+                'grade': kp['grade'],
+                'question_count': question_count,
+                'children': []
+            }
+        
+        # 构建树形结构
+        for kp_code, node in nodes.items():
+            parent_code = node['parent_kp_code']
+            if parent_code and parent_code in nodes:
+                # 有父节点,添加到父节点的children中
+                nodes[parent_code]['children'].append(node)
+            else:
+                # 没有父节点,是根节点
+                kp_tree.append(node)
+        
+        # 递归计算父节点的题目数量(包括子节点)
+        def calculate_total_count(node):
+            total = node.get('question_count', 0)
+            for child in node['children']:
+                total += calculate_total_count(child)
+            node['total_question_count'] = total
+            return total
+        
+        # 对每个节点的children按kp_code排序,并计算总题目数
+        def sort_children(node):
+            node['children'].sort(key=lambda x: x['kp_code'])
+            for child in node['children']:
+                sort_children(child)
+            # 计算总题目数(包括子节点)
+            calculate_total_count(node)
+        
+        for root in kp_tree:
+            sort_children(root)
+        
+        conn.close()
+        
+    except Exception as e:
+        print(f"Error loading knowledge points from database: {e}")
+        import traceback
+        traceback.print_exc()
+    
+    return kp_map, kp_tree
+
+def load_kp_structure_with_pending_count():
+    """
+    从MySQL数据库加载知识点结构,构建树形结构,只包含有未审核题目的节点
+    返回:
+    - kp_map: {kp_code: name} 映射
+    - kp_tree: 树形结构列表,每个节点包含 children 列表和 pending_count(未审核题目数量)
+    - other_questions_count: 其他题目(未关联知识点)的未审核题目数量
+    """
+    kp_map = {}
+    kp_tree = []
+    other_questions_count = 0
+    
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor(dictionary=True)
+        
+        # 查询所有知识点及其未审核题目数量(只查询有未审核题目的知识点)
+        cursor.execute("""
+            SELECT 
+                kp.kp_code, 
+                kp.name, 
+                kp.parent_kp_code, 
+                kp.grade,
+                COUNT(q.question_code) as pending_count
+            FROM knowledge_points_copy1 kp
+            INNER JOIN questions_tem q ON q.kp_code = kp.kp_code
+            WHERE (q.audit_reason IS NULL OR q.audit_reason = '')
+            GROUP BY kp.kp_code, kp.name, kp.parent_kp_code, kp.grade
+            HAVING pending_count > 0
+            ORDER BY kp.kp_code
+        """)
+        all_kps = cursor.fetchall()
+        
+        # 查询其他题目(未关联知识点)的未审核题目数量
+        cursor.execute("""
+            SELECT COUNT(*) as count 
+            FROM questions_tem 
+            WHERE (kp_code IS NULL OR kp_code = '')
+              AND (audit_reason IS NULL OR audit_reason = '')
+        """)
+        other_result = cursor.fetchone()
+        other_questions_count = int(other_result['count'] or 0) if other_result else 0
+        
+        # 构建映射
+        for kp in all_kps:
+            kp_map[kp['kp_code']] = kp['name']
+        
+        # 构建节点字典
+        nodes = {}
+        for kp in all_kps:
+            pending_count = int(kp.get('pending_count', 0) or 0)
+            nodes[kp['kp_code']] = {
+                'kp_code': kp['kp_code'],
+                'name': kp['name'],
+                'parent_kp_code': kp['parent_kp_code'],
+                'grade': kp['grade'],
+                'pending_count': pending_count,
+                'children': []
+            }
+        
+        # 构建树形结构(只包含有未审核题目的节点)
+        for kp_code, node in nodes.items():
+            parent_code = node['parent_kp_code']
+            if parent_code and parent_code in nodes:
+                # 有父节点,添加到父节点的children中
+                nodes[parent_code]['children'].append(node)
+            else:
+                # 没有父节点,是根节点
+                kp_tree.append(node)
+        
+        # 递归计算父节点的未审核题目数量(包括子节点)
+        def calculate_total_pending_count(node):
+            total = node.get('pending_count', 0)
+            for child in node['children']:
+                total += calculate_total_pending_count(child)
+            node['total_pending_count'] = total
+            return total
+        
+        # 对每个节点的children按kp_code排序,并计算总未审核题目数
+        def sort_children(node):
+            node['children'].sort(key=lambda x: x['kp_code'])
+            for child in node['children']:
+                sort_children(child)
+            # 计算总未审核题目数(包括子节点)
+            calculate_total_pending_count(node)
+        
+        for root in kp_tree:
+            sort_children(root)
+        
+        conn.close()
+        
+    except Exception as e:
+        print(f"Error loading knowledge points with pending count from database: {e}")
+        import traceback
+        traceback.print_exc()
+    
+    return kp_map, kp_tree, other_questions_count
+
+KP_MAP, KP_HIERARCHY = load_kp_structure()
+
+def get_tree_file_path():
+    """获取 tree_new.json 文件路径"""
+    tree_file = resource_path('知了数学题库', 'tree_new.json')
+    if not os.path.exists(tree_file):
+        # 尝试从 D 盘复制文件
+        source_file = 'D:/tree_new.json'
+        if os.path.exists(source_file):
+            try:
+                import shutil
+                shutil.copy2(source_file, tree_file)
+                print(f"已从 {source_file} 复制到 {tree_file}")
+            except Exception as e:
+                print(f"复制文件失败: {e}")
+        # 如果复制失败或源文件不存在,使用旧的 tree(1)(1).json
+        if not os.path.exists(tree_file):
+            tree_file = resource_path('知了数学题库', 'tree(1)(1).json')
+    return tree_file
+
+def find_max_id(node):
+    """递归查找树中的最大ID"""
+    max_id = node.get('id', 0)
+    for child in node.get('children', []):
+        child_max = find_max_id(child)
+        if child_max > max_id:
+            max_id = child_max
+    return max_id
+
+def find_node_by_id(node, target_id):
+    """递归查找指定ID的节点"""
+    if node.get('id') == target_id:
+        return node
+    for child in node.get('children', []):
+        result = find_node_by_id(child, target_id)
+        if result:
+            return result
+    return None
+
+def find_kp_hierarchy(kp_id):
+    """
+    根据知识点ID查找其层级信息(chapter, section, subsection)
+    返回: {
+        'chapter': {'id': xxx, 'label': 'xxx'},
+        'section': {'id': xxx, 'label': 'xxx'} 或 None,
+        'subsection': {'id': xxx, 'label': 'xxx'} 或 None
+    }
+    """
+    kp_id_str = str(kp_id)
+    result = {
+        'chapter': None,
+        'section': None,
+        'subsection': None
+    }
+    
+    for chapter in KP_HIERARCHY:
+        # 检查是否是chapter本身
+        if str(chapter["id"]) == kp_id_str:
+            result['chapter'] = {'id': chapter["id"], 'label': chapter["label"]}
+            return result
+        
+        # 检查sections
+        for section in chapter.get("sections", []):
+            # 检查是否是section本身
+            if str(section["id"]) == kp_id_str:
+                result['chapter'] = {'id': chapter["id"], 'label': chapter["label"]}
+                result['section'] = {'id': section["id"], 'label': section["label"]}
+                return result
+            
+            # 检查subsections
+            for subsection in section.get("subsections", []):
+                if str(subsection["id"]) == kp_id_str:
+                    result['chapter'] = {'id': chapter["id"], 'label': chapter["label"]}
+                    result['section'] = {'id': section["id"], 'label': section["label"]}
+                    result['subsection'] = {'id': subsection["id"], 'label': subsection["label"]}
+                    return result
+    
+    return result
+
+@app.route('/question_management')
+def question_management():
+    """题目管理页面:显示知识点目录和题目列表"""
+    # 从数据库加载知识点树形结构
+    _, kp_tree = load_kp_structure_from_db()
+    
+    # 查询"其他题目"(未关联kp_code的题目)数量
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor(dictionary=True)
+        cursor.execute("""
+            SELECT COUNT(*) as count 
+            FROM questions_tem 
+            WHERE (kp_code IS NULL OR kp_code = '')
+        """)
+        other_questions_count = cursor.fetchone()['count'] if cursor.rowcount > 0 else 0
+        conn.close()
+    except Exception as e:
+        print(f"查询其他题目数量失败: {e}")
+        other_questions_count = 0
+    
+    return render_template('question_management.html', 
+                         kp_tree=kp_tree, 
+                         is_audit_mode=False,
+                         other_questions_count=other_questions_count)
+
+@app.route('/api/questions_by_kp/<kp_code>')
+def api_questions_by_kp(kp_code):
+    """根据知识点代码返回题目列表(JSON格式)"""
+    return _api_questions_by_kp(kp_code, audit_only=False)
+
+@app.route('/api/pending_questions_by_kp/<kp_code>')
+def api_pending_questions_by_kp(kp_code):
+    """根据知识点代码返回未审核题目列表(JSON格式,用于审核页面)"""
+    return _api_questions_by_kp(kp_code, audit_only=True)
+
+def _api_questions_by_kp(kp_code, audit_only=False):
+    """根据知识点代码返回题目列表(JSON格式)"""
+    try:
+        conn = get_db_connection()
+    except Exception as e:
+        return jsonify({'success': False, 'error': str(e)}), 500
+    
+    try:
+        cursor = conn.cursor(dictionary=True)
+        
+        # 获取年级筛选参数(可选)
+        grade_filter = request.args.get('grade', type=int)
+        # 获取分页参数(可选)
+        page = request.args.get('page', type=int, default=1)
+        page_size = 20  # 每页显示20道题
+        
+        # 先获取知识点名称(如果kp_code不为空)
+        kp_name = kp_code
+        if kp_code != 'null' and kp_code != '':
+            cursor.execute("SELECT name FROM knowledge_points_copy1 WHERE kp_code = %s", (kp_code,))
+            kp_row = cursor.fetchone()
+            kp_name = kp_row['name'] if kp_row else kp_code
+        
+        # 构建查询条件
+        where_conditions = []
+        params = []
+        
+        # 知识点条件
+        if kp_code == 'null' or kp_code == '':
+            where_conditions.append("(kp_code IS NULL OR kp_code = '' OR kp_code = 'null')")
+            kp_name = "其他题目"
+        else:
+            where_conditions.append("kp_code = %s")
+            params.append(kp_code)
+        
+        # 审核状态条件
+        if audit_only:
+            where_conditions.append("(audit_reason IS NULL OR audit_reason = '')")
+        
+        # 年级筛选条件(仅对"其他题目"有效,因为其他题目有grade字段)
+        if grade_filter is not None and (kp_code == 'null' or kp_code == ''):
+            where_conditions.append("grade = %s")
+            params.append(grade_filter)
+        
+        where_clause = " AND ".join(where_conditions)
+        
+        # 先查询总数(用于分页)
+        count_query = f"SELECT COUNT(*) as total FROM questions_tem WHERE {where_clause}"
+        cursor.execute(count_query, params)
+        total_count = cursor.fetchone()['total']
+        total_pages = (total_count + page_size - 1) // page_size  # 向上取整
+        
+        # 确保页码有效
+        if page < 1:
+            page = 1
+        if page > total_pages and total_pages > 0:
+            page = total_pages
+        
+        # 计算偏移量
+        offset = (page - 1) * page_size
+        
+        # 查询题目(带分页)
+        query = f"""
+            SELECT 
+                question_code, 
+                stem, 
+                kp_code,
+                kp_id,
+                audit_reason,
+                audit_status,
+                difficulty,
+                question_type,
+                answer,
+                solution,
+                options,
+                grade
+            FROM questions_tem 
+            WHERE {where_clause}
+            ORDER BY question_code DESC
+            LIMIT %s OFFSET %s
+        """
+        
+        params_with_pagination = params + [page_size, offset]
+        cursor.execute(query, params_with_pagination)
+        
+        questions = cursor.fetchall()
+        
+        # 为了统计,需要查询所有题目(不分页)
+        stats_query = f"""
+            SELECT 
+                question_code,
+                audit_reason,
+                difficulty,
+                question_type
+            FROM questions_tem 
+            WHERE {where_clause}
+            ORDER BY question_code DESC
+        """
+        cursor.execute(stats_query, params)
+        all_questions_for_stats = cursor.fetchall()
+        
+        # 处理选项JSON
+        for q in questions:
+            if q.get('options'):
+                try:
+                    opt_data = q['options']
+                    if isinstance(opt_data, str):
+                        opt_data = opt_data.replace("'", '"')
+                        q['options'] = json.loads(opt_data)
+                except:
+                    q['options'] = {}
+        
+        # 计算统计数据(基于所有题目,不分页)
+        # 审核统计
+        pass_count = sum(1 for q in all_questions_for_stats if q.get('audit_reason') == '合格')
+        fail_count = sum(1 for q in all_questions_for_stats if q.get('audit_reason') == '不合格')
+        pending_count = sum(1 for q in all_questions_for_stats if not q.get('audit_reason') or q.get('audit_reason') == '')
+        
+        # 难度统计
+        difficulty_jichu = 0  # 筑基 0.2
+        difficulty_tifen = 0  # 提分 0.4
+        difficulty_peiyou = 0  # 培优 0.7
+        difficulty_unknown = 0  # 未设置难度
+        
+        for q in all_questions_for_stats:
+            diff = q.get('difficulty')
+            if diff is not None:
+                try:
+                    diff_float = float(diff)
+                    if abs(diff_float - 0.2) < 0.1:
+                        difficulty_jichu += 1
+                    elif abs(diff_float - 0.4) < 0.1:
+                        difficulty_tifen += 1
+                    elif abs(diff_float - 0.7) < 0.1:
+                        difficulty_peiyou += 1
+                    else:
+                        difficulty_unknown += 1
+                except:
+                    difficulty_unknown += 1
+            else:
+                difficulty_unknown += 1
+        
+        # 题型统计
+        question_type_stats = {}
+        for q in all_questions_for_stats:
+            q_type = q.get('question_type') or '未分类'
+            question_type_stats[q_type] = question_type_stats.get(q_type, 0) + 1
+        
+        conn.close()
+        
+        return jsonify({
+            'success': True,
+            'kp_code': kp_code,
+            'kp_name': kp_name,
+            'questions': questions,
+            'count': total_count,
+            'pagination': {
+                'page': page,
+                'page_size': page_size,
+                'total_pages': total_pages,
+                'total_count': total_count
+            },
+            'stats': {
+                'total': total_count,
+                'audit': {
+                    'pass': pass_count,
+                    'fail': fail_count,
+                    'pending': pending_count,
+                    'pass_rate': round((pass_count / (pass_count + fail_count) * 100) if (pass_count + fail_count) > 0 else 0, 1),
+                    'audit_rate': round(((pass_count + fail_count) / total_count * 100) if total_count > 0 else 0, 1)
+                },
+                'difficulty': {
+                    'jichu': difficulty_jichu,
+                    'tifen': difficulty_tifen,
+                    'peiyou': difficulty_peiyou,
+                    'unknown': difficulty_unknown
+                },
+                'question_type': question_type_stats
+            }
+        })
+    except Exception as e:
+        try:
+            conn.close()
+        except:
+            pass
+        return jsonify({'success': False, 'error': str(e)}), 500
+
+@app.route('/')
+def index():
+    """首页:显示题目统计信息"""
+    try:
+        conn = get_db_connection()
+    except Exception as e:
+        return render_db_error(e)
+    cursor = conn.cursor(dictionary=True)
+    
+    # 总体统计
+    cursor.execute("""
+        SELECT 
+            COUNT(*) AS total_count,
+            SUM(CASE WHEN audit_reason = '合格' THEN 1 ELSE 0 END) AS pass_count,
+            SUM(CASE WHEN audit_reason = '不合格' OR audit_status = 1 THEN 1 ELSE 0 END) AS fail_count,
+            SUM(CASE WHEN audit_reason IS NULL OR audit_reason = '' THEN 1 ELSE 0 END) AS pending_count
+        FROM questions_tem
+    """)
+    overall_stats = cursor.fetchone()
+    
+    total = int(overall_stats['total_count'] or 0)
+    pass_count = int(overall_stats['pass_count'] or 0)
+    fail_count = int(overall_stats['fail_count'] or 0)
+    pending_count = int(overall_stats['pending_count'] or 0)
+    audited_count = pass_count + fail_count
+    
+    # 计算审核通过率
+    pass_rate = (pass_count / audited_count * 100) if audited_count > 0 else 0
+    audit_rate = (audited_count / total * 100) if total > 0 else 0
+    
+    # 教材系列统计
+    cursor.execute("""
+        SELECT 
+            COUNT(*) AS total_series,
+            SUM(CASE WHEN is_active = 1 THEN 1 ELSE 0 END) AS active_series
+        FROM textbook_series_copy1
+    """)
+    series_stats = cursor.fetchone()
+    total_series = int(series_stats['total_series'] or 0)
+    active_series = int(series_stats['active_series'] or 0)
+    
+    # 教材统计
+    cursor.execute("SELECT COUNT(*) AS total_textbooks FROM textbooks_copy1")
+    textbook_stats = cursor.fetchone()
+    total_textbooks = int(textbook_stats['total_textbooks'] or 0)
+    
+    # 知识点统计
+    cursor.execute("SELECT COUNT(*) AS total_kp FROM knowledge_points_copy1")
+    kp_stats = cursor.fetchone()
+    total_kp = int(kp_stats['total_kp'] or 0)
+    
+    # 知识点层级统计(按级别统计)
+    cursor.execute("""
+        SELECT 
+            CASE 
+                WHEN parent_kp_code IS NULL OR parent_kp_code = '' THEN 'level_0'
+                WHEN parent_kp_code IN (SELECT kp_code FROM knowledge_points_copy1 WHERE parent_kp_code IS NULL OR parent_kp_code = '') THEN 'level_1'
+                ELSE 'level_2_plus'
+            END AS level_type,
+            COUNT(*) AS count
+        FROM knowledge_points_copy1
+        GROUP BY level_type
+    """)
+    kp_level_stats = cursor.fetchall()
+    kp_level_0 = 0
+    kp_level_1 = 0
+    kp_level_2_plus = 0
+    for row in kp_level_stats:
+        if row['level_type'] == 'level_0':
+            kp_level_0 = int(row['count'] or 0)
+        elif row['level_type'] == 'level_1':
+            kp_level_1 = int(row['count'] or 0)
+        else:
+            kp_level_2_plus = int(row['count'] or 0)
+    
+    # 最近添加的题目(如果有创建时间字段)
+    cursor.execute("""
+        SELECT question_code, stem, kp_id, audit_reason
+        FROM questions_tem
+        ORDER BY id DESC
+        LIMIT 5
+    """)
+    recent_questions = cursor.fetchall()
+    
+    # 按审核状态统计
+    status_stats = {
+        'pass': pass_count,
+        'fail': fail_count,
+        'pending': pending_count,
+    }
+    
+    conn.close()
+    
+    return render_template('index.html', 
+                         total=total,
+                         pass_count=pass_count,
+                         fail_count=fail_count,
+                         pending_count=pending_count,
+                         audited_count=audited_count,
+                         pass_rate=round(pass_rate, 1),
+                         audit_rate=round(audit_rate, 1),
+                         total_series=total_series,
+                         active_series=active_series,
+                         total_textbooks=total_textbooks,
+                         total_kp=total_kp,
+                         kp_level_0=kp_level_0,
+                         kp_level_1=kp_level_1,
+                         kp_level_2_plus=kp_level_2_plus,
+                         recent_questions=recent_questions,
+                         status_stats=status_stats)
+
+
+@app.route('/audit_questions')
+def audit_questions():
+    """审核题目页面:显示所有未审核的题目,按知识点分类"""
+    try:
+        # 从数据库加载知识点树形结构(只包含有未审核题目的节点)
+        _, kp_tree, other_questions_count = load_kp_structure_with_pending_count()
+        
+        # 查询总体统计
+        conn = get_db_connection()
+        cursor = conn.cursor(dictionary=True)
+        
+        cursor.execute("""
+            SELECT 
+                COUNT(*) AS total_count,
+                SUM(CASE WHEN audit_reason = '合格' THEN 1 ELSE 0 END) AS pass_count,
+                SUM(CASE WHEN audit_reason = '不合格' OR audit_status = 1 THEN 1 ELSE 0 END) AS fail_count,
+                SUM(CASE WHEN audit_reason IS NULL OR audit_reason = '' THEN 1 ELSE 0 END) AS pending_count
+            FROM questions_tem
+        """)
+        overall_stats = cursor.fetchone()
+        total = int(overall_stats['total_count'] or 0)
+        pass_count = int(overall_stats['pass_count'] or 0)
+        fail_count = int(overall_stats['fail_count'] or 0)
+        pending_count = int(overall_stats['pending_count'] or 0)
+        audited_count = pass_count + fail_count
+        pass_rate = (pass_count / audited_count * 100) if audited_count > 0 else 0
+        audit_rate = (audited_count / total * 100) if total > 0 else 0
+        
+        conn.close()
+        
+        return render_template('audit_questions.html', 
+                             kp_tree=kp_tree, 
+                             other_questions_count=other_questions_count,
+                             total=total,
+                             pass_count=pass_count,
+                             fail_count=fail_count,
+                             pending_count=pending_count,
+                             audited_count=audited_count,
+                             pass_rate=round(pass_rate, 1),
+                             audit_rate=round(audit_rate, 1))
+    except Exception as e:
+        return render_db_error(e)
+
+
+@app.route('/search')
+def search_by_question_code():
+    """
+    输入 question_code 直接跳转题目详情。
+    GET /search?q=xxxx
+    """
+    q = (request.args.get("q") or "").strip()
+    if not q:
+        return redirect(url_for("index"))
+
+    try:
+        conn = get_db_connection()
+    except Exception as e:
+        return render_db_error(e)
+
+    cursor = conn.cursor(dictionary=True)
+    cursor.execute("SELECT question_code FROM questions_tem WHERE question_code = %s LIMIT 1", (q,))
+    row = cursor.fetchone()
+    conn.close()
+
+    if row:
+        return redirect(url_for("detail", question_code=q))
+
+    return render_template("search_not_found.html", q=q)
+
+
+@app.route('/search_id')
+def search_by_question_id():
+    """
+    输入主键ID(比如 id / question_id / pk_id / qid)查题。
+    GET /search_id?id=123
+    """
+    raw = (request.args.get("id") or "").strip()
+    if not raw:
+        return redirect(url_for("index"))
+
+    try:
+        conn = get_db_connection()
+    except Exception as e:
+        return render_db_error(e)
+
+    pk_cols = get_pk_columns(conn)
+    if not pk_cols:
+        conn.close()
+        return render_template("search_id_not_supported.html", q=raw)
+
+    cursor = conn.cursor(dictionary=True)
+    found_code = None
+    for col in pk_cols:
+        try:
+            cursor.execute(f"SELECT question_code FROM questions_tem WHERE {col} = %s LIMIT 1", (raw,))
+            row = cursor.fetchone()
+            if row and row.get("question_code"):
+                found_code = row["question_code"]
+                break
+        except Exception:
+            continue
+    conn.close()
+
+    if found_code:
+        return redirect(url_for("detail", question_code=found_code))
+
+    return render_template("search_id_not_found.html", q=raw, pk_cols=pk_cols)
+
+@app.route('/questions/<kp_code>')
+def question_list(kp_code):
+    try:
+        conn = get_db_connection()
+    except Exception as e:
+        return render_db_error(e)
+    cursor = conn.cursor(dictionary=True)
+    
+    # 检查是否是审核模式(只显示未审核题目)
+    audit_only = request.args.get('audit_only', '').lower() == 'true'
+    
+    # 处理 kp_code 为 'null' 或空字符串的情况(表示没有 kp_id 的题目)
+    # 注意:URL参数仍使用 kp_code,但数据库字段已改为 kp_id
+    # 使用别名 kp_id AS kp_code 以兼容模板代码
+    if kp_code == 'null' or kp_code == '':
+        if audit_only:
+            cursor.execute("SELECT *, kp_id AS kp_code FROM questions_tem WHERE (kp_id IS NULL OR kp_id = '') AND (audit_reason IS NULL OR audit_reason = '') ORDER BY question_code DESC")
+        else:
+            cursor.execute("SELECT *, kp_id AS kp_code FROM questions_tem WHERE kp_id IS NULL OR kp_id = '' ORDER BY question_code DESC")
+        kp_name = "未分类题目"
+        hierarchy_info = {'chapter': None, 'section': None, 'subsection': None}
+    else:
+        # 查找层级信息,判断是否是章节级别
+        hierarchy_info = find_kp_hierarchy(kp_code)
+        
+        # 如果是章节级别,需要查询该章节下所有 section 和 subsection 的题目
+        if hierarchy_info['chapter'] and not hierarchy_info['section'] and not hierarchy_info['subsection']:
+            # 这是章节级别,需要收集该章节下所有的 kp_id
+            chapter_id = hierarchy_info['chapter']['id']
+            kp_ids = [str(chapter_id)]  # 包含章节本身
+            
+            # 查找该章节下的所有 section 和 subsection
+            for chapter in KP_HIERARCHY:
+                if chapter["id"] == chapter_id:
+                    for section in chapter.get("sections", []):
+                        kp_ids.append(str(section["id"]))
+                        for subsection in section.get("subsections", []):
+                            kp_ids.append(str(subsection["id"]))
+                    break
+            
+            # 使用 IN 查询该章节下所有知识点的题目
+            placeholders = ','.join(['%s'] * len(kp_ids))
+            if audit_only:
+                query = f"SELECT *, kp_id AS kp_code FROM questions_tem WHERE kp_id IN ({placeholders}) AND (audit_reason IS NULL OR audit_reason = '') ORDER BY question_code DESC"
+            else:
+                query = f"SELECT *, kp_id AS kp_code FROM questions_tem WHERE kp_id IN ({placeholders}) ORDER BY question_code DESC"
+            cursor.execute(query, tuple(kp_ids))
+            kp_name = hierarchy_info['chapter']['label']
+        else:
+            # 普通的知识点查询(section 或 subsection)
+            if audit_only:
+                cursor.execute("SELECT *, kp_id AS kp_code FROM questions_tem WHERE kp_id = %s AND (audit_reason IS NULL OR audit_reason = '') ORDER BY question_code DESC", (kp_code,))
+            else:
+                cursor.execute("SELECT *, kp_id AS kp_code FROM questions_tem WHERE kp_id = %s ORDER BY question_code DESC", (kp_code,))
+            kp_name = KP_MAP.get(str(kp_code), kp_code)
+    
+    questions = cursor.fetchall()
+    
+    # 构建录入题目页面的URL
+    add_question_url = f'/add_question?kp_code={kp_code}'
+    if hierarchy_info['chapter']:
+        chapter_label_encoded = urllib.parse.quote(hierarchy_info['chapter']['label'])
+        add_question_url += f"&chapter={hierarchy_info['chapter']['id']}&chapter_label={chapter_label_encoded}"
+    if hierarchy_info['section']:
+        section_label_encoded = urllib.parse.quote(hierarchy_info['section']['label'])
+        add_question_url += f"&section={hierarchy_info['section']['id']}&section_label={section_label_encoded}"
+    if hierarchy_info['subsection']:
+        subsection_label_encoded = urllib.parse.quote(hierarchy_info['subsection']['label'])
+        add_question_url += f"&subsection={hierarchy_info['subsection']['id']}&subsection_label={subsection_label_encoded}"
+    
+    conn.close()
+    return render_template('questions.html', 
+                          questions=questions, 
+                          kp_code=kp_code, 
+                          kp_name=kp_name, 
+                          node_id=None,
+                          hierarchy_info=hierarchy_info,
+                          add_question_url=add_question_url,
+                          audit_only=audit_only)
+
+@app.route('/textbook/<node_id>')
+def textbook_question_list(node_id):
+    """教材节点题目列表"""
+    try:
+        conn = get_db_connection()
+    except Exception as e:
+        return render_db_error(e)
+    cursor = conn.cursor(dictionary=True)
+    
+    # 获取教材节点标题
+    try:
+        cursor.execute("SELECT title FROM textbook_catalog_nodes WHERE id = %s", (node_id,))
+        node_row = cursor.fetchone()
+        title = node_row.get('title') if node_row else f'节点{node_id}'
+    except Exception:
+        # 如果表不存在,使用默认标题
+        title = f'节点{node_id}'
+    
+    # 查询该节点下的所有题目(使用别名 kp_id AS kp_code 以兼容模板代码)
+    cursor.execute(
+        "SELECT *, kp_id AS kp_code FROM questions_tem WHERE textbook_catalog_nodes_id = %s ORDER BY question_code DESC",
+        (node_id,)
+    )
+    questions = cursor.fetchall()
+    conn.close()
+    
+    return render_template('questions.html', questions=questions, kp_code=None, kp_name=title, node_id=node_id)
+
+@app.route('/detail/<question_code>')
+def detail(question_code):
+    try:
+        conn = get_db_connection()
+    except Exception as e:
+        return render_db_error(e)
+    cursor = conn.cursor(dictionary=True)
+    # 使用别名 kp_id AS kp_code 以兼容模板代码
+    cursor.execute("SELECT *, kp_id AS kp_code FROM questions_tem WHERE question_code = %s", (question_code,))
+    question = cursor.fetchone()
+    
+    if not question:
+        conn.close()
+        return render_template("search_not_found.html", q=question_code), 404
+    
+    # 获取上下题索引(从 kp_id 字段读取知识点ID,但模板中使用 kp_code)
+    # 优先使用 URL 参数中的 kp_code(用于返回列表时定位到正确的知识点)
+    kp_code_from_url = request.args.get('kp_code')
+    kp_code_db = kp_code_from_url or question.get('kp_id') or question.get('kp_code')  # 兼容处理
+    textbook_node_id = question.get('textbook_catalog_nodes_id')
+    prev_code = None
+    next_code = None
+    curr_idx = 0
+    total = 1
+    
+    if kp_code_db:
+        # 有 kp_id:查询同知识点下的所有题目
+        cursor.execute("SELECT question_code FROM questions_tem WHERE kp_id = %s ORDER BY question_code DESC", (kp_code_db,))
+        all_codes = [r['question_code'] for r in cursor.fetchall()]
+        
+        if question_code in all_codes:
+            curr_idx = all_codes.index(question_code)
+            prev_code = all_codes[curr_idx - 1] if curr_idx > 0 else None
+            next_code = all_codes[curr_idx + 1] if curr_idx < len(all_codes) - 1 else None
+            total = len(all_codes)
+    elif textbook_node_id:
+        # 没有 kp_code 但有 textbook_catalog_nodes_id:查询同教材节点下的所有题目
+        cursor.execute(
+            "SELECT question_code FROM questions_tem WHERE textbook_catalog_nodes_id = %s ORDER BY question_code DESC",
+            (textbook_node_id,)
+        )
+        all_codes = [r['question_code'] for r in cursor.fetchall()]
+        
+        if question_code in all_codes:
+            curr_idx = all_codes.index(question_code)
+            prev_code = all_codes[curr_idx - 1] if curr_idx > 0 else None
+            next_code = all_codes[curr_idx + 1] if curr_idx < len(all_codes) - 1 else None
+            total = len(all_codes)
+    else:
+        # 既没有 kp_code 也没有 textbook_catalog_nodes_id:不显示上下题导航
+        pass
+    
+    conn.close()
+    
+    # 处理选项 JSON
+    options = []
+    if question.get('options'):
+        try:
+            opt_data = question['options']
+            if isinstance(opt_data, str):
+                # 兼容单引号 JSON
+                opt_data = opt_data.replace("'", '"')
+                d = json.loads(opt_data)
+            else:
+                d = opt_data
+            options = sorted(d.items())
+        except:
+            options = []
+
+    # 处理知识点名称:如果没有 kp_code,显示"未分类"
+    kp_name = "未分类题目"
+    hierarchy_info = {'chapter': None, 'section': None, 'subsection': None}
+    add_question_url = None
+    
+    if kp_code_db:
+        kp_name = KP_MAP.get(str(kp_code_db), kp_code_db)
+        # 查找层级信息
+        hierarchy_info = find_kp_hierarchy(kp_code_db)
+        # 构建录入题目页面的URL
+        add_question_url = f'/add_question?kp_code={kp_code_db}'
+        if hierarchy_info['chapter']:
+            chapter_label_encoded = urllib.parse.quote(hierarchy_info['chapter']['label'])
+            add_question_url += f"&chapter={hierarchy_info['chapter']['id']}&chapter_label={chapter_label_encoded}"
+        if hierarchy_info['section']:
+            section_label_encoded = urllib.parse.quote(hierarchy_info['section']['label'])
+            add_question_url += f"&section={hierarchy_info['section']['id']}&section_label={section_label_encoded}"
+        if hierarchy_info['subsection']:
+            subsection_label_encoded = urllib.parse.quote(hierarchy_info['subsection']['label'])
+            add_question_url += f"&subsection={hierarchy_info['subsection']['id']}&subsection_label={subsection_label_encoded}"
+    elif textbook_node_id:
+        # 查询教材节点标题
+        try:
+            cursor.execute("SELECT title FROM textbook_catalog_nodes WHERE id = %s", (textbook_node_id,))
+            node_row = cursor.fetchone()
+            if node_row:
+                kp_name = node_row.get('title') or f'节点{textbook_node_id}'
+            else:
+                kp_name = f'节点{textbook_node_id}'
+        except Exception as e:
+            # 如果表不存在或查询失败,使用默认标题
+            print(f"Warning: 无法查询教材节点标题: {e}")
+            kp_name = f'节点{textbook_node_id}'
+
+    return render_template('detail.html', 
+                           q=question, 
+                           options=options, 
+                           kp_name=kp_name,
+                           kp_code=kp_code_db,
+                           node_id=str(textbook_node_id) if textbook_node_id else None,
+                           prev_code=prev_code,
+                           next_code=next_code,
+                           total=total,
+                           curr_num=curr_idx + 1 if total > 0 else 1,
+                           add_question_url=add_question_url)
+
+@app.route('/audit', methods=['POST'])
+def audit():
+    data = request.json
+    q_code = data.get('question_code')
+    status_text = data.get('audit_reason') # "合格" or "不合格"
+    status_val = 1 if status_text == "不合格" else 0
+    
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor(dictionary=True)
+        
+        # 更新 questions_tem 表的审核状态
+        cursor.execute("UPDATE questions_tem SET audit_reason=%s, audit_status=%s WHERE question_code=%s", 
+                       (status_text, status_val, q_code))
+        
+        # 如果审核通过(合格),将题目插入到 questions 表
+        if status_text == "合格":
+            # 从 questions_tem 表获取所有字段数据
+            cursor.execute("SELECT * FROM questions_tem WHERE question_code = %s", (q_code,))
+            question_data = cursor.fetchone()
+            
+            if question_data:
+                # 检查 questions 表是否已存在该 question_code
+                cursor.execute("SELECT question_code FROM questions WHERE question_code = %s", (q_code,))
+                existing = cursor.fetchone()
+                
+                # 准备要插入/更新的字段(排除 id 和 is_repeat,因为 questions 表没有 is_repeat 字段)
+                fields_to_copy = [
+                    'question_code', 'kp_id', 'textbook_catalog_nodes_id', 'stem', 'options',
+                    'answer', 'solution', 'difficulty', 'question_category', 'source', 'tags',
+                    'question_type', 'source_file_id', 'source_paper_id', 'paper_part_id',
+                    'textbook_id', 'meta', 'created_at', 'updated_at', 'audit_status',
+                    'audit_reason', 'title_1', 'title_2', 'title_3', 'create_by',
+                    'kp_code', 'kp_name', 'kp_reference'
+                ]
+                
+                if existing:
+                    # 如果已存在,更新数据
+                    update_fields = []
+                    update_values = []
+                    for field in fields_to_copy:
+                        if field in question_data:
+                            update_fields.append(f"{field} = %s")
+                            update_values.append(question_data[field])
+                    update_values.append(q_code)
+                    
+                    update_sql = f"UPDATE questions SET {', '.join(update_fields)} WHERE question_code = %s"
+                    cursor.execute(update_sql, tuple(update_values))
+                else:
+                    # 如果不存在,插入新数据
+                    insert_fields = []
+                    insert_values = []
+                    placeholders = []
+                    
+                    for field in fields_to_copy:
+                        if field in question_data:
+                            insert_fields.append(field)
+                            insert_values.append(question_data[field])
+                            placeholders.append('%s')
+                    
+                    insert_sql = f"INSERT INTO questions ({', '.join(insert_fields)}) VALUES ({', '.join(placeholders)})"
+                    cursor.execute(insert_sql, tuple(insert_values))
+        
+        conn.commit()
+        conn.close()
+        return jsonify({'success': True})
+    except Exception as e:
+        import traceback
+        traceback.print_exc()
+        return jsonify({'success': False, 'error': str(e)})
+
+@app.route("/export_pdf_remote/<question_code>")
+def export_pdf_remote(question_code):
+    """
+    远程导出:调用你给的接口拿到 pdf_url(可能是一个或多个),然后全部打开。
+    """
+    try:
+        conn = get_db_connection()
+    except Exception as e:
+        return render_db_error(e)
+
+    try:
+        cursor = conn.cursor(dictionary=True)
+        # 使用别名 kp_id AS kp_code 以兼容模板代码
+        cursor.execute("SELECT *, kp_id AS kp_code FROM questions_tem WHERE question_code = %s", (question_code,))
+        question = cursor.fetchone()
+    finally:
+        try:
+            conn.close()
+        except Exception:
+            pass
+
+    if not question:
+        return render_template("search_not_found.html", q=question_code), 404
+
+    qid = get_question_pk(question)
+    if not qid:
+        # 没有常见主键列名,无法给 question_ids
+        return render_template("search_id_not_supported.html", q=str(question_code)), 400
+
+    try:
+        pdf_urls = request_remote_pdf_url(qid, include_grading=True)
+    except Exception as e:
+        # PDF接口失败,不显示数据库连接信息
+        return render_template(
+            "db_error.html",
+            error=str(e),
+            db_host=None,
+            db_port=None,
+            db_name=None,
+            db_user=None,
+        ), 500
+
+    if not pdf_urls:
+        # PDF接口未返回有效数据,不显示数据库连接信息
+        return render_template(
+            "db_error.html",
+            error="PDF 接口未返回有效的 pdf_url",
+            db_host=None,
+            db_port=None,
+            db_name=None,
+            db_user=None,
+        ), 500
+
+    # 如果有多个 PDF,返回一个 HTML 页面,用 JavaScript 全部打开
+    if len(pdf_urls) > 1:
+        return render_template("open_multiple_pdfs.html", pdf_urls=pdf_urls)
+    else:
+        # 只有一个 PDF,直接重定向
+        return redirect(pdf_urls[0])
+
+
+@app.route("/api/optimize_stem/<question_code>", methods=["POST"])
+def api_optimize_stem(question_code):
+    """
+    生成“优化后的 stem”,并返回左右对比所需内容。
+    """
+    try:
+        conn = get_db_connection()
+    except Exception as e:
+        return jsonify({"success": False, "error": str(e)}), 500
+
+    try:
+        cursor = conn.cursor(dictionary=True)
+        cursor.execute("SELECT question_code, stem FROM questions_tem WHERE question_code = %s", (question_code,))
+        row = cursor.fetchone()
+    finally:
+        try:
+            conn.close()
+        except Exception:
+            pass
+
+    if not row:
+        return jsonify({"success": False, "error": "题目不存在"}), 404
+
+    old_stem = row.get("stem") or ""
+
+    try:
+        new_stem = call_ai_optimize_stem(old_stem)
+    except Exception as e:
+        return jsonify({"success": False, "error": str(e)}), 500
+
+    svg_ok = _svg_blocks_equal(old_stem, new_stem)
+    return jsonify(
+        {
+            "success": True,
+            "question_code": question_code,
+            "old_stem": old_stem,
+            "new_stem": new_stem,
+            "svg_ok": bool(svg_ok),
+        }
+    )
+
+
+@app.route("/api/replace_stem/<question_code>", methods=["POST"])
+def api_replace_stem(question_code):
+    """
+    将新 stem 覆写到 questions_tem.stem(仅改 stem)。
+    额外安全:替换前校验 SVG 不被改动。
+    """
+    data = request.json or {}
+    new_stem = data.get("new_stem")
+    if new_stem is None:
+        return jsonify({"success": False, "error": "缺少 new_stem"}), 400
+
+    try:
+        conn = get_db_connection()
+    except Exception as e:
+        return jsonify({"success": False, "error": str(e)}), 500
+
+    try:
+        cursor = conn.cursor(dictionary=True)
+        cursor.execute("SELECT question_code, stem FROM questions_tem WHERE question_code = %s", (question_code,))
+        row = cursor.fetchone()
+        if not row:
+            return jsonify({"success": False, "error": "题目不存在"}), 404
+
+        old_stem = row.get("stem") or ""
+
+        if not _svg_blocks_equal(old_stem, str(new_stem)):
+            return jsonify({"success": False, "error": "安全拦截:检测到 SVG 被改动,已禁止替换。"}), 400
+
+        cur2 = conn.cursor()
+        cur2.execute("UPDATE questions_tem SET stem=%s WHERE question_code=%s", (str(new_stem), question_code))
+        conn.commit()
+        return jsonify({"success": True, "question_code": question_code})
+    except Exception as e:
+        try:
+            conn.rollback()
+        except Exception:
+            pass
+        return jsonify({"success": False, "error": str(e)}), 500
+    finally:
+        try:
+            conn.close()
+        except Exception:
+            pass
+
+@app.route('/api/score', methods=['POST'])
+def api_score_question():
+    """
+    题目难度评分接口(支持文本、HTML和图片)
+    
+    请求格式:
+    {
+        "stem": "题目文本内容或HTML(可选,HTML中可包含<img>标签)",
+        "image_url": "图片URL(可选)",
+        "image_base64": "图片base64编码(可选,格式:data:image/png;base64,xxx)",
+        "base_url": "基础URL(可选,用于处理HTML中相对路径的图片)",
+        "custom_prompt": "可选的自定义提示词"
+    }
+    
+    返回格式:
+    {
+        "success": true,
+        "data": {
+            "difficulty_level": "筑基|提分|培优",
+            "final_score": 0.00,
+            "dimension_scores": {
+                "推理与运算步数": 0.33,
+                "知识点数量": 0.33,
+                "抽象与构造要求": 0.33,
+                "题型常规性": 0.33
+            }
+        },
+        "message": "评分成功"
+    }
+    """
+    client = get_openai_client()
+    if client is None:
+        return jsonify({
+            "success": False,
+            "error": "OpenAI客户端未初始化,请检查配置"
+        }), 500
+    
+    try:
+        # 获取请求数据
+        data = request.get_json()
+        if not data:
+            return jsonify({
+                "success": False,
+                "error": "请求体不能为空"
+            }), 400
+        
+        stem = data.get('stem', '').strip()
+        image_url = data.get('image_url', '').strip()
+        image_base64 = data.get('image_base64', '').strip()
+        base_url = data.get('base_url', '').strip()  # 用于处理相对路径的图片URL
+        
+        # 如果stem包含HTML标签,尝试提取图片
+        html_images = []
+        stem_text = stem
+        if stem and ('<img' in stem.lower() or '<image' in stem.lower()):
+            html_images = extract_images_from_html(stem, base_url)
+            # 同时提取纯文本(保留LaTeX公式等数学符号)
+            stem_text = extract_text_from_html(stem)
+        
+        # 至少需要提供一种输入
+        if not stem_text and not image_url and not image_base64 and not html_images:
+            return jsonify({
+                "success": False,
+                "error": "至少需要提供以下之一:stem(文本或HTML)、image_url(图片URL)或image_base64(图片base64)"
+            }), 400
+        
+        # 获取提示词
+        custom_prompt = data.get('custom_prompt', '').strip()
+        prompt = custom_prompt if custom_prompt else DIFFICULTY_SCORING_PROMPT
+        
+        # 构建消息内容
+        content = []
+        
+        # 添加文本提示词
+        if stem_text:
+            full_prompt = f"{prompt}\n\n题目内容:\n{stem_text}"
+        else:
+            full_prompt = prompt
+        
+        content.append({
+            "type": "text",
+            "text": full_prompt
+        })
+        
+        # 添加图片(优先级:base64 > 直接URL > HTML中提取的图片)
+        images_to_add = []
+        
+        if image_base64:
+            # 处理base64图片
+            if not image_base64.startswith('data:'):
+                image_base64 = f"data:image/png;base64,{image_base64}"
+            images_to_add.append(image_base64)
+        elif image_url:
+            # 使用直接提供的图片URL
+            images_to_add.append(image_url)
+        elif html_images:
+            # 使用从HTML中提取的图片URL
+            images_to_add.extend(html_images)
+        
+        # 添加所有图片到content
+        for img in images_to_add:
+            if img.startswith('data:'):
+                # base64图片
+                content.append({
+                    "type": "image_url",
+                    "image_url": {
+                        "url": img
+                    }
+                })
+            else:
+                # URL图片
+                content.append({
+                    "type": "image_url",
+                    "image_url": {
+                        "url": img
+                    }
+                })
+        
+        # 调用OpenAI API
+        model_name = DIFFICULTY_SCORING_MODEL or AI_MODEL_NAME
+        try:
+            response = client.chat.completions.create(
+                model=model_name,
+                messages=[
+                    {
+                        "role": "user",
+                        "content": content
+                    }
+                ],
+                temperature=DIFFICULTY_SCORING_TEMPERATURE,
+                top_p=DIFFICULTY_SCORING_TOP_P,
+                presence_penalty=DIFFICULTY_SCORING_PRESENCE_PENALTY,
+                frequency_penalty=DIFFICULTY_SCORING_FREQUENCY_PENALTY
+            )
+        except Exception as api_error:
+            print(f"OpenAI API调用失败: {str(api_error)}")
+            return jsonify({
+                "success": False,
+                "error": f"AI服务调用失败: {str(api_error)}",
+                "error_type": type(api_error).__name__
+            }), 500
+        
+        if not response or not response.choices or len(response.choices) == 0:
+            return jsonify({
+                "success": False,
+                "error": "AI服务返回空响应"
+            }), 500
+        
+        result_text = response.choices[0].message.content
+        
+        # 调试:打印AI返回的原始内容(前500字符)
+        print(f"AI返回原始内容(前500字符): {result_text[:500] if result_text else 'None'}")
+        
+        # 解析JSON响应
+        result_json = parse_json_response(result_text)
+        
+        if result_json is None:
+            return jsonify({
+                "success": False,
+                "error": "AI返回格式不正确,无法解析JSON",
+                "raw_response": result_text[:500] if result_text else "None"  # 只返回前500字符,避免响应过大
+            }), 500
+        
+        # 验证返回的JSON结构
+        required_fields = ["difficulty_level", "total_score", "dimension_scores"]
+        for field in required_fields:
+            if field not in result_json:
+                return jsonify({
+                    "success": False,
+                    "error": f"返回JSON缺少必需字段: {field}",
+                    "raw_response": result_text
+                }), 500
+        
+        # 验证维度分值(放宽验证,只记录警告,不阻止返回)
+        dimension_scores = result_json.get("dimension_scores", {})
+        valid_dimension_values = [0.33, 0.66, 1.00]
+        
+        # 维度名称映射(支持中文和英文键名)
+        dimension_name_mapping = {
+            # 中文键名(标准)
+            "推理与运算步数": "推理与运算步数",
+            "知识点数量": "知识点数量",
+            "抽象与构造要求": "抽象与构造要求",
+            "题型常规性": "题型常规性",
+            # 英文键名(兼容)
+            "reasoning_steps": "推理与运算步数",
+            "knowledge_points": "知识点数量",
+            "abstraction_construction": "抽象与构造要求",
+            "typicality": "题型常规性",
+            # 其他可能的英文键名
+            "reasoning": "推理与运算步数",
+            "knowledge": "知识点数量",
+            "abstraction": "抽象与构造要求",
+            "conventionality": "题型常规性"
+        }
+        
+        # 标准化维度分值(统一转换为中文键名)
+        normalized_dimension_scores = {}
+        for key, value in dimension_scores.items():
+            # 查找对应的中文键名
+            chinese_key = dimension_name_mapping.get(key, key)
+            normalized_dimension_scores[chinese_key] = value
+        
+        # 更新 result_json 中的 dimension_scores 为标准格式
+        result_json["dimension_scores"] = normalized_dimension_scores
+        
+        # 验证维度分值
+        required_dimensions = ["推理与运算步数", "知识点数量", "抽象与构造要求", "题型常规性"]
+        validation_errors = []
+        
+        for dim in required_dimensions:
+            if dim not in normalized_dimension_scores:
+                validation_errors.append(f"缺少维度评分: {dim}")
+                continue
+            
+            dim_value = normalized_dimension_scores[dim]
+            # 允许浮点数精度误差
+            if not any(abs(dim_value - v) < 0.01 for v in valid_dimension_values):
+                validation_errors.append(f"维度 {dim} 的值 {dim_value} 不在允许范围内(0.33、0.66、1.00)")
+        
+        # 验证 total_score 计算(四个维度相加后归一化)
+        if len(normalized_dimension_scores) == 4:
+            calculated_total = sum(normalized_dimension_scores.values()) / 4.0
+            total_score = result_json.get("total_score", 0)
+            
+            # 允许小的浮点数误差(0.05,放宽验证)
+            if abs(calculated_total - total_score) > 0.05:
+                validation_errors.append(f"total_score ({total_score}) 与计算值 ({calculated_total:.2f}) 不一致")
+        else:
+            total_score = result_json.get("total_score", 0)
+        
+        # 验证 difficulty_level 映射(如果 total_score 有效)
+        difficulty_level = result_json.get("difficulty_level", "")
+        if total_score >= 0 and total_score <= 1:
+            if total_score <= 0.25:
+                expected_level = "筑基"
+            elif total_score <= 0.5:
+                expected_level = "提分"
+            else:
+                expected_level = "培优"
+            
+            if difficulty_level != expected_level:
+                validation_errors.append(f"difficulty_level ({difficulty_level}) 与 total_score ({total_score}) 的映射不一致,应为 {expected_level}")
+        
+        # 如果有验证错误,记录但不阻止返回(放宽验证)
+        if validation_errors:
+            print(f"难度评分验证警告: {', '.join(validation_errors)}")
+            # 仍然返回结果,但添加警告信息
+            result_json["_validation_warnings"] = validation_errors
+        
+        return jsonify({
+            "success": True,
+            "data": result_json,
+            "message": "评分成功"
+        })
+        
+    except Exception as e:
+        import traceback
+        error_trace = traceback.format_exc()
+        print(f"难度评分接口错误: {error_trace}")
+        return jsonify({
+            "success": False,
+            "error": f"评分过程中出现错误: {str(e)}",
+            "error_type": type(e).__name__
+        }), 500
+
+@app.route('/kp_management')
+def kp_management():
+    """知识点管理页面:从 knowledge_points_copy1 表读取数据,按层级结构展示"""
+    try:
+        conn = get_db_connection()
+    except Exception as e:
+        return render_db_error(e)
+    cursor = conn.cursor(dictionary=True)
+    
+    # 获取所有知识点(只显示初中;注释掉小学和高中)
+    cursor.execute("""
+        SELECT 
+            id, kp_code, name, subject, grade, parent_kp_code,
+            prerequisite_kp_codes, dependent_kp_codes, related_kp_codes,
+            stats, created_at, updated_at, skills, direct_score, related_score
+        FROM knowledge_points_copy1
+        WHERE grade = '初中'
+        -- WHERE grade = '小学'  -- 小学(已注释)
+        -- WHERE grade = '高中'  -- 高中(已注释)
+        ORDER BY kp_code ASC
+    """)
+    all_kps = cursor.fetchall()
+    
+    # 处理JSON字段
+    for kp in all_kps:
+        for field in ['prerequisite_kp_codes', 'dependent_kp_codes', 'related_kp_codes', 'stats', 'skills', 'direct_score', 'related_score']:
+            if kp[field] and isinstance(kp[field], str):
+                try:
+                    kp[field] = json.loads(kp[field])
+                except:
+                    kp[field] = None
+    
+    # 构建层级结构
+    kp_dict = {kp['kp_code']: kp for kp in all_kps}
+    root_kps = []
+    
+    # 为每个知识点添加children列表
+    for kp in all_kps:
+        kp['children'] = []
+        kp['level'] = 0
+    
+    # 构建树形结构
+    for kp in all_kps:
+        if kp['parent_kp_code'] and kp['parent_kp_code'] in kp_dict:
+            parent = kp_dict[kp['parent_kp_code']]
+            parent['children'].append(kp)
+            # 计算层级深度
+            kp['level'] = parent['level'] + 1
+        else:
+            root_kps.append(kp)
+    
+    # 递归排序:按kp_code排序
+    def sort_kp_tree(kp_list):
+        kp_list.sort(key=lambda x: x['kp_code'])
+        for kp in kp_list:
+            if kp['children']:
+                sort_kp_tree(kp['children'])
+    
+    sort_kp_tree(root_kps)
+    
+    # 扁平化列表(用于表格展示,保持层级顺序)
+    # 默认显示:level 0 和 level 1,level 2 及以上默认隐藏
+    def flatten_tree(kp_list, result=None, level=0):
+        if result is None:
+            result = []
+        for kp in kp_list:
+            kp['display_level'] = level
+            kp['has_children'] = len(kp['children']) > 0
+            # level 0 和 level 1 默认显示,level 2 及以上默认隐藏
+            kp['default_visible'] = level <= 1
+            result.append(kp)
+            if kp['children']:
+                flatten_tree(kp['children'], result, level + 1)
+        return result
+    
+    knowledge_points = flatten_tree(root_kps)
+    
+    # 获取所有知识点代码和名称,用于下拉选择(只显示初中)
+    cursor.execute("""
+        SELECT kp_code, name 
+        FROM knowledge_points_copy1 
+        WHERE grade = '初中'
+        -- WHERE grade = '小学'  -- 小学(已注释)
+        -- WHERE grade = '高中'  -- 高中(已注释)
+        ORDER BY kp_code ASC
+    """)
+    kp_options = cursor.fetchall()
+    
+    # 统计每个知识点关联的题目数量(只统计初中)
+    # questions_tem 表使用 kp_code 字段,关联 knowledge_points_copy1 的 kp_code 字段
+    cursor.execute("""
+        SELECT 
+            kp.id,
+            kp.kp_code,
+            COUNT(q.question_code) as question_count
+        FROM knowledge_points_copy1 kp
+        LEFT JOIN questions_tem q ON q.kp_code = kp.kp_code
+        WHERE kp.grade = '初中'
+        -- WHERE kp.grade = '小学'  -- 小学(已注释)
+        -- WHERE kp.grade = '高中'  -- 高中(已注释)
+        GROUP BY kp.id, kp.kp_code
+    """)
+    question_counts_raw = cursor.fetchall()
+    question_counts = {row['kp_code']: row['question_count'] for row in question_counts_raw}
+    
+    # 递归计算父节点的题目数量(包括所有子节点的题目数量)
+    def calculate_total_question_count(kp):
+        # 先计算当前节点直接关联的题目数量
+        direct_count = question_counts.get(kp['kp_code'], 0)
+        
+        # 递归计算所有子节点的题目数量总和
+        children_count = 0
+        if kp['children']:
+            for child in kp['children']:
+                children_count += calculate_total_question_count(child)
+        
+        # 父节点的题目数量 = 直接关联的题目 + 所有子节点的题目总和
+        total_count = direct_count + children_count
+        kp['question_count'] = total_count
+        kp['direct_question_count'] = direct_count  # 保存直接关联的题目数量
+        
+        return total_count
+    
+    # 计算所有节点的题目数量(包括子节点)
+    for kp in root_kps:
+        calculate_total_question_count(kp)
+    
+    # 创建知识点代码到题目数量的映射(用于扁平列表)
+    kp_code_to_question_count = {}
+    def build_question_count_map(kp_list):
+        for kp in kp_list:
+            kp_code_to_question_count[kp['kp_code']] = kp.get('question_count', 0)
+            if kp.get('children'):
+                build_question_count_map(kp['children'])
+    
+    build_question_count_map(root_kps)
+    
+    # 更新扁平列表的题目数量
+    for kp in knowledge_points:
+        kp['question_count'] = kp_code_to_question_count.get(kp['kp_code'], 0)
+    
+    # 获取各学段的统计信息(只显示初中)
+    cursor.execute("""
+        SELECT 
+            grade,
+            COUNT(*) as count
+        FROM knowledge_points_copy1
+        WHERE grade = '初中'
+        -- WHERE grade = '小学'  -- 小学(已注释)
+        -- WHERE grade = '高中'  -- 高中(已注释)
+        GROUP BY grade
+    """)
+    grade_stats_raw = cursor.fetchall()
+    grade_stats = {row['grade']: row['count'] for row in grade_stats_raw}
+    
+    # 只显示初中(小学和高中已注释)
+    grade_info = {
+        # '小学': {'count': grade_stats.get('小学', 0), 'color': 'from-pink-500 to-rose-600', 'icon': 'ri-book-open-line', 'bg': 'bg-gradient-to-br from-pink-50 to-rose-50'},  # 已注释
+        '初中': {'count': grade_stats.get('初中', 0), 'color': 'from-blue-500 to-indigo-600', 'icon': 'ri-graduation-cap-line', 'bg': 'bg-gradient-to-br from-blue-50 to-indigo-50'},
+        # '高中': {'count': grade_stats.get('高中', 0), 'color': 'from-purple-500 to-violet-600', 'icon': 'ri-school-line', 'bg': 'bg-gradient-to-br from-purple-50 to-violet-50'}  # 已注释
+    }
+    
+    conn.close()
+    
+    return render_template('kp_management.html', 
+                         knowledge_points=knowledge_points,
+                         kp_tree=root_kps,
+                         kp_options=kp_options,
+                         grade_info=grade_info)
+
+@app.route('/textbook_management')
+def textbook_management():
+    """教材管理页面"""
+    try:
+        conn = get_db_connection()
+    except Exception as e:
+        return render_db_error(e)
+    cursor = conn.cursor(dictionary=True)
+    
+    # 获取所有教材系列
+    cursor.execute("SELECT * FROM textbook_series_copy1 ORDER BY sort_order ASC, id ASC")
+    series_list = cursor.fetchall()
+    
+    # 获取所有教材
+    cursor.execute("SELECT * FROM textbooks_copy1 ORDER BY series_id ASC, grade ASC, semester ASC")
+    textbooks_list = cursor.fetchall()
+    
+    # 获取所有知识点(用于关联)
+    cursor.execute("SELECT kp_code, name FROM knowledge_points_copy1 ORDER BY kp_code ASC")
+    kp_options = cursor.fetchall()
+    
+    conn.close()
+    
+    return render_template('textbook_management.html',
+                         series_list=series_list,
+                         textbooks_list=textbooks_list,
+                         kp_options=kp_options)
+
+# ==================== 教材系列 API ====================
+@app.route('/api/textbook/series/create', methods=['POST'])
+def api_textbook_series_create():
+    """创建教材系列"""
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor(dictionary=True)
+        
+        data = request.get_json()
+        name = (data.get('name') or '').strip()
+        slug = (data.get('slug') or '').strip() or None
+        publisher = (data.get('publisher') or '').strip() or None
+        region = (data.get('region') or '').strip() or None
+        is_active = 1 if data.get('is_active') else 0
+        
+        if not name:
+            return jsonify({'success': False, 'error': '系列名称不能为空'})
+        
+        cursor.execute("""
+            INSERT INTO textbook_series_copy1 
+            (name, slug, publisher, region, is_active, created_at, updated_at)
+            VALUES (%s, %s, %s, %s, %s, NOW(), NOW())
+        """, (name, slug, publisher, region, is_active))
+        
+        conn.commit()
+        new_id = cursor.lastrowid
+        conn.close()
+        return jsonify({'success': True, 'message': '创建成功', 'id': new_id})
+    except Exception as e:
+        if conn:
+            conn.rollback()
+            conn.close()
+        return jsonify({'success': False, 'error': str(e)})
+
+@app.route('/api/textbook/series/update/<int:series_id>', methods=['POST'])
+def api_textbook_series_update(series_id):
+    """更新教材系列"""
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor(dictionary=True)
+        
+        data = request.get_json()
+        name = (data.get('name') or '').strip()
+        slug = (data.get('slug') or '').strip() or None
+        publisher = (data.get('publisher') or '').strip() or None
+        region = (data.get('region') or '').strip() or None
+        is_active = 1 if data.get('is_active') else 0
+        
+        if not name:
+            return jsonify({'success': False, 'error': '系列名称不能为空'})
+        
+        cursor.execute("""
+            UPDATE textbook_series_copy1 
+            SET name = %s, slug = %s, publisher = %s, region = %s, is_active = %s, updated_at = NOW()
+            WHERE id = %s
+        """, (name, slug, publisher, region, is_active, series_id))
+        
+        conn.commit()
+        conn.close()
+        return jsonify({'success': True, 'message': '更新成功'})
+    except Exception as e:
+        if conn:
+            conn.rollback()
+            conn.close()
+        return jsonify({'success': False, 'error': str(e)})
+
+@app.route('/api/textbook/series/delete/<int:series_id>', methods=['POST'])
+def api_textbook_series_delete(series_id):
+    """删除教材系列"""
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor(dictionary=True)
+        
+        # 检查是否有关联的教材
+        cursor.execute("SELECT COUNT(*) as count FROM textbooks_copy1 WHERE series_id = %s", (series_id,))
+        result = cursor.fetchone()
+        if result['count'] > 0:
+            return jsonify({'success': False, 'error': '该系列下存在教材,无法删除'})
+        
+        cursor.execute("DELETE FROM textbook_series_copy1 WHERE id = %s", (series_id,))
+        conn.commit()
+        conn.close()
+        return jsonify({'success': True, 'message': '删除成功'})
+    except Exception as e:
+        if conn:
+            conn.rollback()
+            conn.close()
+        return jsonify({'success': False, 'error': str(e)})
+
+@app.route('/api/textbook/series/get/<int:series_id>', methods=['GET'])
+def api_textbook_series_get(series_id):
+    """获取教材系列详情"""
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor(dictionary=True)
+        
+        cursor.execute("SELECT * FROM textbook_series_copy1 WHERE id = %s", (series_id,))
+        series = cursor.fetchone()
+        conn.close()
+        
+        if not series:
+            return jsonify({'success': False, 'error': '系列不存在'})
+        
+        return jsonify({'success': True, 'data': series})
+    except Exception as e:
+        return jsonify({'success': False, 'error': str(e)})
+
+@app.route('/api/textbook/series/toggle_active/<int:series_id>', methods=['POST'])
+def api_textbook_series_toggle_active(series_id):
+    """切换教材系列激活状态"""
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor(dictionary=True)
+        
+        # 获取当前状态
+        cursor.execute("SELECT is_active FROM textbook_series_copy1 WHERE id = %s", (series_id,))
+        series = cursor.fetchone()
+        
+        if not series:
+            return jsonify({'success': False, 'error': '系列不存在'})
+        
+        # 切换状态:1变0,0变1
+        new_status = 1 if series['is_active'] == 0 else 0
+        
+        cursor.execute("""
+            UPDATE textbook_series_copy1 
+            SET is_active = %s, updated_at = NOW()
+            WHERE id = %s
+        """, (new_status, series_id))
+        
+        conn.commit()
+        conn.close()
+        return jsonify({
+            'success': True, 
+            'message': '状态已更新',
+            'is_active': new_status
+        })
+    except Exception as e:
+        if conn:
+            conn.rollback()
+            conn.close()
+        return jsonify({'success': False, 'error': str(e)})
+
+# ==================== 教材 API ====================
+@app.route('/api/textbook/create', methods=['POST'])
+def api_textbook_create():
+    """创建教材"""
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor(dictionary=True)
+        
+        data = request.get_json()
+        series_id = data.get('series_id')
+        official_title = (data.get('official_title') or '').strip()
+        stage = (data.get('stage') or '').strip() or None
+        grade = (data.get('grade') or '').strip() or None
+        semester = data.get('semester')
+        
+        if not series_id:
+            return jsonify({'success': False, 'error': '教材系列不能为空'})
+        if not official_title:
+            return jsonify({'success': False, 'error': '教材名称不能为空'})
+        
+        cursor.execute("""
+            INSERT INTO textbooks_copy1 
+            (series_id, official_title, stage, grade, semester, created_at, updated_at)
+            VALUES (%s, %s, %s, %s, %s, NOW(), NOW())
+        """, (series_id, official_title, stage, grade, semester))
+        
+        conn.commit()
+        new_id = cursor.lastrowid
+        conn.close()
+        return jsonify({'success': True, 'message': '创建成功', 'id': new_id})
+    except Exception as e:
+        if conn:
+            conn.rollback()
+            conn.close()
+        return jsonify({'success': False, 'error': str(e)})
+
+@app.route('/api/textbook/update/<int:textbook_id>', methods=['POST'])
+def api_textbook_update(textbook_id):
+    """更新教材"""
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor(dictionary=True)
+        
+        data = request.get_json()
+        official_title = (data.get('official_title') or '').strip()
+        stage = (data.get('stage') or '').strip() or None
+        grade = (data.get('grade') or '').strip() or None
+        semester = data.get('semester')
+        
+        if not official_title:
+            return jsonify({'success': False, 'error': '教材名称不能为空'})
+        
+        cursor.execute("""
+            UPDATE textbooks_copy1 
+            SET official_title = %s, stage = %s, grade = %s, semester = %s, updated_at = NOW()
+            WHERE id = %s
+        """, (official_title, stage, grade, semester, textbook_id))
+        
+        conn.commit()
+        conn.close()
+        return jsonify({'success': True, 'message': '更新成功'})
+    except Exception as e:
+        if conn:
+            conn.rollback()
+            conn.close()
+        return jsonify({'success': False, 'error': str(e)})
+
+@app.route('/api/textbook/delete/<int:textbook_id>', methods=['POST'])
+def api_textbook_delete(textbook_id):
+    """删除教材"""
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor(dictionary=True)
+        
+        # 检查是否有目录节点
+        cursor.execute("SELECT COUNT(*) as count FROM textbook_catalog_nodes_copy1 WHERE textbook_id = %s", (textbook_id,))
+        result = cursor.fetchone()
+        if result['count'] > 0:
+            return jsonify({'success': False, 'error': '该教材下存在目录节点,无法删除'})
+        
+        cursor.execute("DELETE FROM textbooks_copy1 WHERE id = %s", (textbook_id,))
+        conn.commit()
+        conn.close()
+        return jsonify({'success': True, 'message': '删除成功'})
+    except Exception as e:
+        if conn:
+            conn.rollback()
+            conn.close()
+        return jsonify({'success': False, 'error': str(e)})
+
+@app.route('/api/textbook/get/<int:textbook_id>', methods=['GET'])
+def api_textbook_get(textbook_id):
+    """获取教材详情"""
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor(dictionary=True)
+        
+        cursor.execute("SELECT * FROM textbooks_copy1 WHERE id = %s", (textbook_id,))
+        textbook = cursor.fetchone()
+        conn.close()
+        
+        if not textbook:
+            return jsonify({'success': False, 'error': '教材不存在'})
+        
+        return jsonify({'success': True, 'data': textbook})
+    except Exception as e:
+        return jsonify({'success': False, 'error': str(e)})
+
+@app.route('/api/textbook/list/<int:series_id>', methods=['GET'])
+def api_textbook_list(series_id):
+    """获取指定系列下的所有教材"""
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor(dictionary=True)
+        
+        cursor.execute("SELECT * FROM textbooks_copy1 WHERE series_id = %s ORDER BY grade ASC, semester ASC", (series_id,))
+        textbooks = cursor.fetchall()
+        conn.close()
+        
+        return jsonify({'success': True, 'data': textbooks})
+    except Exception as e:
+        return jsonify({'success': False, 'error': str(e)})
+
+# ==================== 目录节点 API ====================
+@app.route('/api/textbook/catalog/create', methods=['POST'])
+def api_textbook_catalog_create():
+    """创建目录节点"""
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor(dictionary=True)
+        
+        data = request.get_json()
+        textbook_id = data.get('textbook_id')
+        parent_id = data.get('parent_id')  # 可以为None(顶级节点)
+        node_type = (data.get('node_type') or 'chapter').strip()
+        title = (data.get('title') or '').strip()
+        display_no = (data.get('display_no') or '').strip() or None
+        
+        if not textbook_id:
+            return jsonify({'success': False, 'error': '教材ID不能为空'})
+        if not title:
+            return jsonify({'success': False, 'error': '节点标题不能为空'})
+        
+        # 验证节点类型层级关系
+        if parent_id:
+            # 有父节点,需要验证节点类型
+            cursor.execute("SELECT node_type FROM textbook_catalog_nodes_copy1 WHERE id = %s", (parent_id,))
+            parent = cursor.fetchone()
+            if not parent:
+                return jsonify({'success': False, 'error': '父节点不存在'})
+            
+            parent_node_type = parent['node_type']
+            # 章节下只能创建 section
+            if parent_node_type == 'chapter' and node_type != 'section':
+                return jsonify({'success': False, 'error': '章节下只能创建小节(section)'})
+            # section 下只能创建 subsection
+            elif parent_node_type == 'section' and node_type != 'subsection':
+                return jsonify({'success': False, 'error': '小节下只能创建子小节(subsection)'})
+            # subsection 下不能再创建子节点
+            elif parent_node_type == 'subsection':
+                return jsonify({'success': False, 'error': '子小节下不能再创建子节点'})
+        else:
+            # 没有父节点,只能创建顶级节点(chapter)
+            if node_type != 'chapter':
+                return jsonify({'success': False, 'error': '顶级节点只能是章节(chapter)'})
+        
+        # 计算depth
+        depth = 1
+        if parent_id:
+            cursor.execute("SELECT depth FROM textbook_catalog_nodes_copy1 WHERE id = %s", (parent_id,))
+            parent_depth_result = cursor.fetchone()
+            if parent_depth_result:
+                depth = parent_depth_result['depth'] + 1
+        
+        # 查询最大id并手动递增(因为id字段可能不是auto_increment)
+        cursor.execute("SELECT MAX(id) as max_id FROM textbook_catalog_nodes_copy1")
+        max_id_result = cursor.fetchone()
+        new_id = (max_id_result['max_id'] or 0) + 1
+        
+        cursor.execute("""
+            INSERT INTO textbook_catalog_nodes_copy1 
+            (id, textbook_id, parent_id, node_type, title, display_no, depth, sort_order, created_at, updated_at)
+            VALUES (%s, %s, %s, %s, %s, %s, %s, 0, NOW(), NOW())
+        """, (new_id, textbook_id, parent_id, node_type, title, display_no, depth))
+        
+        conn.commit()
+        conn.close()
+        return jsonify({'success': True, 'message': '创建成功', 'id': new_id})
+    except Exception as e:
+        if conn:
+            conn.rollback()
+            conn.close()
+        return jsonify({'success': False, 'error': str(e)})
+
+@app.route('/api/textbook/catalog/update/<int:node_id>', methods=['POST'])
+def api_textbook_catalog_update(node_id):
+    """更新目录节点"""
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor(dictionary=True)
+        
+        data = request.get_json()
+        title = (data.get('title') or '').strip()
+        display_no = (data.get('display_no') or '').strip() or None
+        node_type = (data.get('node_type') or '').strip() or None
+        
+        if not title:
+            return jsonify({'success': False, 'error': '节点标题不能为空'})
+        
+        # 如果更新了节点类型,需要验证是否符合层级关系
+        if node_type:
+            # 获取当前节点的父节点信息
+            cursor.execute("SELECT parent_id FROM textbook_catalog_nodes_copy1 WHERE id = %s", (node_id,))
+            current_node = cursor.fetchone()
+            if current_node and current_node.get('parent_id'):
+                parent_id = current_node['parent_id']
+                cursor.execute("SELECT node_type FROM textbook_catalog_nodes_copy1 WHERE id = %s", (parent_id,))
+                parent = cursor.fetchone()
+                if parent:
+                    parent_node_type = parent['node_type']
+                    # 验证节点类型层级关系
+                    if parent_node_type == 'chapter' and node_type != 'section':
+                        return jsonify({'success': False, 'error': '章节下只能创建小节(section)'})
+                    elif parent_node_type == 'section' and node_type != 'subsection':
+                        return jsonify({'success': False, 'error': '小节下只能创建子小节(subsection)'})
+                    elif parent_node_type == 'subsection':
+                        return jsonify({'success': False, 'error': '子小节下不能再创建子节点'})
+            else:
+                # 没有父节点,只能是顶级节点(chapter)
+                if node_type != 'chapter':
+                    return jsonify({'success': False, 'error': '顶级节点只能是章节(chapter)'})
+        
+        cursor.execute("""
+            UPDATE textbook_catalog_nodes_copy1 
+            SET title = %s, display_no = %s, node_type = COALESCE(%s, node_type), updated_at = NOW()
+            WHERE id = %s
+        """, (title, display_no, node_type, node_id))
+        
+        conn.commit()
+        conn.close()
+        return jsonify({'success': True, 'message': '更新成功'})
+    except Exception as e:
+        if conn:
+            conn.rollback()
+            conn.close()
+        return jsonify({'success': False, 'error': str(e)})
+
+@app.route('/api/textbook/catalog/delete/<int:node_id>', methods=['POST'])
+def api_textbook_catalog_delete(node_id):
+    """删除目录节点"""
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor(dictionary=True)
+        
+        # 检查是否有子节点
+        cursor.execute("SELECT COUNT(*) as count FROM textbook_catalog_nodes_copy1 WHERE parent_id = %s", (node_id,))
+        result = cursor.fetchone()
+        if result['count'] > 0:
+            return jsonify({'success': False, 'error': '该节点下存在子节点,无法删除'})
+        
+        # 检查是否有关联的知识点
+        cursor.execute("SELECT COUNT(*) as count FROM textbook_chapter_knowledge_relation_copy1 WHERE catalog_chapter_id = %s AND is_deleted = 0", (node_id,))
+        result = cursor.fetchone()
+        if result['count'] > 0:
+            return jsonify({'success': False, 'error': '该节点下存在关联的知识点,无法删除'})
+        
+        cursor.execute("DELETE FROM textbook_catalog_nodes_copy1 WHERE id = %s", (node_id,))
+        conn.commit()
+        conn.close()
+        return jsonify({'success': True, 'message': '删除成功'})
+    except Exception as e:
+        if conn:
+            conn.rollback()
+            conn.close()
+        return jsonify({'success': False, 'error': str(e)})
+
+@app.route('/api/textbook/catalog/get/<int:node_id>', methods=['GET'])
+def api_textbook_catalog_get(node_id):
+    """获取目录节点详情"""
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor(dictionary=True)
+        
+        cursor.execute("SELECT * FROM textbook_catalog_nodes_copy1 WHERE id = %s", (node_id,))
+        node = cursor.fetchone()
+        conn.close()
+        
+        if not node:
+            return jsonify({'success': False, 'error': '节点不存在'})
+        
+        return jsonify({'success': True, 'data': node})
+    except Exception as e:
+        return jsonify({'success': False, 'error': str(e)})
+
+@app.route('/api/textbook/catalog/tree/<int:textbook_id>', methods=['GET'])
+def api_textbook_catalog_tree(textbook_id):
+    """获取教材的目录树"""
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor(dictionary=True)
+        
+        # 获取所有节点
+        cursor.execute("""
+            SELECT * FROM textbook_catalog_nodes_copy1 
+            WHERE textbook_id = %s 
+            ORDER BY depth ASC, sort_order ASC, id ASC
+        """, (textbook_id,))
+        all_nodes = cursor.fetchall()
+        
+        # 构建树形结构
+        node_dict = {node['id']: node for node in all_nodes}
+        root_nodes = []
+        
+        for node in all_nodes:
+            node['children'] = []
+            if node['parent_id'] and node['parent_id'] in node_dict:
+                parent = node_dict[node['parent_id']]
+                parent['children'].append(node)
+            else:
+                root_nodes.append(node)
+        
+        conn.close()
+        return jsonify({'success': True, 'data': root_nodes})
+    except Exception as e:
+        return jsonify({'success': False, 'error': str(e)})
+
+# ==================== 章节-知识点关联 API ====================
+@app.route('/api/textbook/relation/create', methods=['POST'])
+def api_textbook_relation_create():
+    """创建章节-知识点关联"""
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor(dictionary=True)
+        
+        data = request.get_json()
+        catalog_chapter_id = data.get('catalog_chapter_id')
+        kp_code = (data.get('kp_code') or '').strip()
+        
+        if not catalog_chapter_id:
+            return jsonify({'success': False, 'error': '章节ID不能为空'})
+        if not kp_code:
+            return jsonify({'success': False, 'error': '知识点代码不能为空'})
+        
+        # 检查是否已存在
+        cursor.execute("""
+            SELECT id FROM textbook_chapter_knowledge_relation_copy1 
+            WHERE catalog_chapter_id = %s AND kp_code = %s AND is_deleted = 0
+        """, (catalog_chapter_id, kp_code))
+        if cursor.fetchone():
+            return jsonify({'success': False, 'error': '该关联已存在'})
+        
+        cursor.execute("""
+            INSERT INTO textbook_chapter_knowledge_relation_copy1 
+            (catalog_chapter_id, kp_code, is_deleted, gmt_create, gmt_modified)
+            VALUES (%s, %s, 0, NOW(), NOW())
+        """, (catalog_chapter_id, kp_code))
+        
+        conn.commit()
+        new_id = cursor.lastrowid
+        conn.close()
+        return jsonify({'success': True, 'message': '创建成功', 'id': new_id})
+    except Exception as e:
+        if conn:
+            conn.rollback()
+            conn.close()
+        return jsonify({'success': False, 'error': str(e)})
+
+@app.route('/api/textbook/relation/delete/<int:relation_id>', methods=['POST'])
+def api_textbook_relation_delete(relation_id):
+    """删除章节-知识点关联(软删除)"""
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor(dictionary=True)
+        
+        cursor.execute("""
+            UPDATE textbook_chapter_knowledge_relation_copy1 
+            SET is_deleted = 1, gmt_modified = NOW()
+            WHERE id = %s
+        """, (relation_id,))
+        
+        conn.commit()
+        conn.close()
+        return jsonify({'success': True, 'message': '删除成功'})
+    except Exception as e:
+        if conn:
+            conn.rollback()
+            conn.close()
+        return jsonify({'success': False, 'error': str(e)})
+
+@app.route('/api/textbook/relation/list/<int:catalog_chapter_id>', methods=['GET'])
+def api_textbook_relation_list(catalog_chapter_id):
+    """获取章节关联的知识点列表"""
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor(dictionary=True)
+        
+        cursor.execute("""
+            SELECT tckr.*, kp.name as kp_name
+            FROM textbook_chapter_knowledge_relation_copy1 tckr
+            LEFT JOIN knowledge_points_copy1 kp ON kp.kp_code = tckr.kp_code
+            WHERE tckr.catalog_chapter_id = %s AND tckr.is_deleted = 0
+            ORDER BY tckr.id ASC
+        """, (catalog_chapter_id,))
+        
+        relations = cursor.fetchall()
+        conn.close()
+        return jsonify({'success': True, 'data': relations})
+    except Exception as e:
+        return jsonify({'success': False, 'error': str(e)})
+
+@app.route('/material_management')
+def material_management():
+    """资料管理页面"""
+    return render_template('material_management.html')
+
+@app.route('/add_question')
+def add_question_page():
+    """显示录入新题目页面"""
+    # 从URL参数获取层级信息
+    kp_code = request.args.get('kp_code')
+    chapter_id = request.args.get('chapter')
+    chapter_label = request.args.get('chapter_label', '')
+    section_id = request.args.get('section')
+    section_label = request.args.get('section_label', '')
+    subsection_id = request.args.get('subsection')
+    subsection_label = request.args.get('subsection_label', '')
+    
+    # 解码URL编码的标签
+    if chapter_label:
+        chapter_label = urllib.parse.unquote(chapter_label)
+    if section_label:
+        section_label = urllib.parse.unquote(section_label)
+    if subsection_label:
+        subsection_label = urllib.parse.unquote(subsection_label)
+    
+    return render_template('add_question.html',
+                          kp_code=kp_code,
+                          chapter_id=chapter_id,
+                          chapter_label=chapter_label,
+                          section_id=section_id,
+                          section_label=section_label,
+                          subsection_id=subsection_id,
+                          subsection_label=subsection_label)
+
+@app.route('/edit/<question_code>')
+def edit_page(question_code):
+    try:
+        conn = get_db_connection()
+    except Exception as e:
+        return render_db_error(e)
+    cursor = conn.cursor(dictionary=True)
+    # 使用别名 kp_id AS kp_code 以兼容模板代码
+    cursor.execute("SELECT *, kp_id AS kp_code FROM questions_tem WHERE question_code = %s", (question_code,))
+    question = cursor.fetchone()
+    conn.close()
+    return render_template('edit.html', q=question)
+
+@app.route('/update_question', methods=['POST'])
+def update_question():
+    data = request.json
+    q_code = data.get('question_code')
+    
+    if not q_code:
+        return jsonify({'success': False, 'error': '缺少 question_code'}), 400
+    
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor()
+        
+        # 只更新用户实际提供的字段(如果字段在 data 中存在,就更新)
+        updates = []
+        params = []
+        
+        if 'stem' in data:
+            updates.append("stem=%s")
+            params.append(data.get('stem', ''))
+        
+        if 'options' in data:
+            # 如果 options 是空字符串,保存为 NULL
+            # 如果 options 是无效 JSON(如 "Invalid value."),也保存为 NULL,避免数据库报错
+            options_value = data.get('options')
+            if options_value == '' or (isinstance(options_value, str) and options_value.strip().lower() in ['invalid value.', 'null', 'none']):
+                options_value = None
+            updates.append("options=%s")
+            params.append(options_value)
+        
+        if 'answer' in data:
+            updates.append("answer=%s")
+            params.append(data.get('answer', ''))
+        
+        if 'solution' in data:
+            updates.append("solution=%s")
+            params.append(data.get('solution', ''))
+        
+        if 'question_type' in data:
+            updates.append("question_type=%s")
+            params.append(data.get('question_type', ''))
+        
+        if 'kp_code' in data:
+            # 前端参数名是 kp_code,但数据库字段是 kp_id
+            updates.append("kp_id=%s")
+            params.append(data.get('kp_code'))
+        
+        if 'difficulty' in data:
+            # 处理 difficulty 字段:转换为浮点数,保留两位小数
+            difficulty_value = data.get('difficulty')
+            if difficulty_value is not None and difficulty_value != '':
+                try:
+                    difficulty_value = round(float(difficulty_value), 2)
+                    updates.append("difficulty=%s")
+                    params.append(difficulty_value)
+                except (ValueError, TypeError):
+                    pass  # 如果转换失败,跳过该字段
+        
+        if not updates:
+            conn.close()
+            return jsonify({'success': False, 'error': '没有提供要更新的字段'}), 400
+        
+        # 添加 WHERE 条件
+        params.append(q_code)
+        query = f"UPDATE questions_tem SET {', '.join(updates)} WHERE question_code=%s"
+        cursor.execute(query, tuple(params))
+        conn.commit()
+        conn.close()
+        return jsonify({'success': True})
+    except Exception as e:
+        return jsonify({'success': False, 'error': str(e)})
+
+@app.route('/create_question', methods=['POST'])
+def create_question():
+    """创建新题目"""
+    data = request.json
+    
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor()
+        
+        # 自动生成唯一的 question_code
+        # 格式:Q + 年月日时分秒 + 3位随机数,例如:Q20250101120045123
+        max_attempts = 10  # 最多尝试10次生成唯一编号
+        q_code = None
+        
+        for _ in range(max_attempts):
+            timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
+            random_suffix = random.randint(100, 999)  # 3位随机数
+            q_code = f"Q{timestamp}{random_suffix}"
+            
+            # 检查是否已存在
+            cursor.execute("SELECT question_code FROM questions_tem WHERE question_code = %s", (q_code,))
+            if not cursor.fetchone():
+                break  # 找到唯一编号
+            q_code = None
+        
+        if not q_code:
+            conn.close()
+            return jsonify({'success': False, 'error': '无法生成唯一的题号,请稍后重试'}), 500
+        
+        # 构建插入字段和值
+        fields = []
+        values = []
+        placeholders = []
+        
+        # 必填字段:question_code(自动生成)
+        fields.append('question_code')
+        values.append(q_code)
+        placeholders.append('%s')
+        
+        # 可选字段(前端参数 kp_code 保存到数据库的 kp_code 字段)
+        optional_fields = {
+            'stem': 'stem',
+            'options': 'options',
+            'answer': 'answer',
+            'solution': 'solution',
+            'question_type': 'question_type',
+            'kp_code': 'kp_code',  # 前端参数名 kp_code,保存到数据库的 kp_code 字段(与查询保持一致)
+            'textbook_catalog_nodes_id': 'textbook_catalog_nodes_id',
+            'chapter': 'title_1',  # chapter 映射到 title_1
+            'section': 'title_2',  # section 映射到 title_2
+            'subsection': 'title_3',  # subsection 映射到 title_3
+            'difficulty': 'difficulty',  # 难度字段
+            'create_by': 'create_by',  # 创建者字段
+            'grade': 'grade',  # 年级字段:1=小学,2=初中,3=高中
+        }
+        
+        for key, field_name in optional_fields.items():
+            if key in data and data[key] is not None:
+                value = data[key]
+                # 处理 options:如果是空字符串,设为 None
+                if key == 'options' and (value == '' or (isinstance(value, str) and value.strip().lower() in ['null', 'none'])):
+                    value = None
+                # 处理 textbook_catalog_nodes_id:转换为整数
+                elif key == 'textbook_catalog_nodes_id' and value:
+                    try:
+                        value = int(value)
+                    except (ValueError, TypeError):
+                        value = None
+                # 处理 difficulty:转换为浮点数,保留两位小数
+                elif key == 'difficulty' and value:
+                    try:
+                        value = round(float(value), 2)
+                    except (ValueError, TypeError):
+                        value = None
+                # 处理 grade:转换为整数(1=小学,2=初中,3=高中)
+                elif key == 'grade' and value:
+                    try:
+                        value = int(value)
+                        # 验证年级值是否在有效范围内
+                        if value not in [1, 2, 3]:
+                            value = None
+                    except (ValueError, TypeError):
+                        value = None
+                
+                if value is not None and value != '':
+                    fields.append(field_name)
+                    values.append(value)
+                    placeholders.append('%s')
+        
+        # 验证:所有题目都必须有年级字段
+        has_grade = 'grade' in fields
+        if not has_grade:
+            conn.close()
+            return jsonify({'success': False, 'error': '所有题目都必须选择年级'}), 400
+        
+        # 执行插入
+        if len(fields) == 1:  # 只有 question_code
+            conn.close()
+            return jsonify({'success': False, 'error': '至少需要填写题号以外的其他字段'}), 400
+        
+        query = f"INSERT INTO questions_tem ({', '.join(fields)}) VALUES ({', '.join(placeholders)})"
+        cursor.execute(query, tuple(values))
+        # 获取插入的题目ID
+        question_id = cursor.lastrowid
+        conn.commit()
+        conn.close()
+        
+        return jsonify({'success': True, 'question_code': q_code, 'question_id': question_id})
+    except Exception as e:
+        try:
+            conn.rollback()
+        except:
+            pass
+        try:
+            conn.close()
+        except:
+            pass
+        return jsonify({'success': False, 'error': str(e)})
+
+@app.route('/api/question_by_id/<int:question_id>')
+def api_question_by_id(question_id):
+    """根据题目ID返回题目详情(用于查重比对)"""
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor(dictionary=True)
+        
+        # 查询题目详情
+        cursor.execute("""
+            SELECT 
+                id,
+                question_code, 
+                stem, 
+                kp_code,
+                kp_id,
+                audit_reason,
+                audit_status,
+                difficulty,
+                question_type,
+                answer,
+                solution,
+                options
+            FROM questions_tem 
+            WHERE id = %s
+        """, (question_id,))
+        
+        question = cursor.fetchone()
+        conn.close()
+        
+        if not question:
+            return jsonify({'success': False, 'error': '题目不存在'}), 404
+        
+        # 处理选项JSON
+        if question.get('options'):
+            try:
+                opt_data = question['options']
+                if isinstance(opt_data, str):
+                    opt_data = opt_data.replace("'", '"')
+                    question['options'] = json.loads(opt_data)
+            except:
+                question['options'] = {}
+        
+        return jsonify({'success': True, 'question': question})
+    except Exception as e:
+        return jsonify({'success': False, 'error': str(e)}), 500
+
+@app.route('/api/check_duplicate', methods=['POST', 'OPTIONS'])
+def api_check_duplicate():
+    """代理查重检测接口,解决CORS问题"""
+    if request.method == 'OPTIONS':
+        # 处理CORS预检请求
+        response = jsonify({})
+        response.headers.add('Access-Control-Allow-Origin', '*')
+        response.headers.add('Access-Control-Allow-Methods', 'POST, OPTIONS')
+        response.headers.add('Access-Control-Allow-Headers', 'Content-Type')
+        return response
+    
+    try:
+        # 获取请求数据
+        data = request.get_json()
+        
+        # 转发请求到查重服务
+        import urllib.request
+        import urllib.parse
+        
+        url = 'http://47.77.199.85:8888/api/check_duplicate'
+        req_data = json.dumps(data).encode('utf-8')
+        
+        req = urllib.request.Request(
+            url,
+            data=req_data,
+            headers={
+                'Content-Type': 'application/json',
+                'Cookie': request.headers.get('Cookie', 'MATH-LOGIN-AUTH={{authToken}}'),
+                'MATH-DEBUG': request.headers.get('MATH-DEBUG', '1'),
+                'MATH-UID': request.headers.get('MATH-UID', '12')
+            }
+        )
+        
+        with urllib.request.urlopen(req, timeout=30) as response:
+            result = json.loads(response.read().decode('utf-8'))
+            
+            # 返回结果,添加CORS头
+            flask_response = jsonify(result)
+            flask_response.headers.add('Access-Control-Allow-Origin', '*')
+            return flask_response
+            
+    except urllib.error.HTTPError as e:
+        error_body = e.read().decode('utf-8') if e.fp else 'Unknown error'
+        return jsonify({'code': -1, 'error': f'查重服务错误: {e.code} - {error_body}'}), e.code
+    except Exception as e:
+        return jsonify({'code': -1, 'error': f'查重检测失败: {str(e)}'}), 500
+
+@app.route('/api/confirm_repeat', methods=['POST', 'OPTIONS'])
+def api_confirm_repeat():
+    """代理确认查重结果接口,解决CORS问题"""
+    if request.method == 'OPTIONS':
+        # 处理CORS预检请求
+        response = jsonify({})
+        response.headers.add('Access-Control-Allow-Origin', '*')
+        response.headers.add('Access-Control-Allow-Methods', 'POST, OPTIONS')
+        response.headers.add('Access-Control-Allow-Headers', 'Content-Type')
+        return response
+    
+    try:
+        # 获取请求数据
+        data = request.get_json()
+        
+        # 转发请求到查重服务
+        import urllib.request
+        
+        url = 'http://47.77.199.85:8888/api/confirm_repeat'
+        req_data = json.dumps(data).encode('utf-8')
+        
+        req = urllib.request.Request(
+            url,
+            data=req_data,
+            headers={
+                'Content-Type': 'application/json',
+                'Cookie': request.headers.get('Cookie', 'MATH-LOGIN-AUTH={{authToken}}'),
+                'MATH-DEBUG': request.headers.get('MATH-DEBUG', '1'),
+                'MATH-UID': request.headers.get('MATH-UID', '12')
+            }
+        )
+        
+        with urllib.request.urlopen(req, timeout=30) as response:
+            result = json.loads(response.read().decode('utf-8'))
+            
+            # 返回结果,添加CORS头
+            flask_response = jsonify(result)
+            flask_response.headers.add('Access-Control-Allow-Origin', '*')
+            return flask_response
+            
+    except urllib.error.HTTPError as e:
+        error_body = e.read().decode('utf-8') if e.fp else 'Unknown error'
+        return jsonify({'code': -1, 'error': f'查重服务错误: {e.code} - {error_body}'}), e.code
+    except Exception as e:
+        return jsonify({'code': -1, 'error': f'确认查重结果失败: {str(e)}'}), 500
+
+@app.route('/api/delete_question/<question_code>', methods=['POST'])
+def delete_question(question_code):
+    """
+    删除题目(需要确认),返回下一题的 question_code(用于跳转)
+    """
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor(dictionary=True)
+        
+        # 先获取题目信息(包括知识点和教材节点,使用 kp_id 字段)
+        cursor.execute("SELECT question_code, kp_id, textbook_catalog_nodes_id FROM questions_tem WHERE question_code = %s", (question_code,))
+        row = cursor.fetchone()
+        if not row:
+            conn.close()
+            return jsonify({'success': False, 'error': '题目不存在'}), 404
+        
+        kp_code = row.get('kp_id')  # 数据库字段已改为 kp_id
+        textbook_node_id = row.get('textbook_catalog_nodes_id')
+        
+        # 获取同分类下的所有题目(按 question_code 降序排列)
+        all_codes = []
+        if kp_code:
+            # 有知识点:在同知识点内查找
+            cursor.execute("SELECT question_code FROM questions_tem WHERE kp_id = %s ORDER BY question_code DESC", (kp_code,))
+            all_codes = [r['question_code'] for r in cursor.fetchall()]
+        elif textbook_node_id:
+            # 有教材节点:在同教材节点内查找
+            cursor.execute(
+                "SELECT question_code FROM questions_tem WHERE textbook_catalog_nodes_id = %s ORDER BY question_code DESC",
+                (textbook_node_id,)
+            )
+            all_codes = [r['question_code'] for r in cursor.fetchall()]
+        
+        # 找到当前题目在列表中的位置
+        try:
+            curr_idx = all_codes.index(question_code)
+        except ValueError:
+            curr_idx = -1
+        
+        # 删除题目
+        cursor.execute("DELETE FROM questions_tem WHERE question_code = %s", (question_code,))
+        conn.commit()
+        
+        # 确定跳转目标:优先下一题,没有则上一题,都没有则返回 None(前端跳转列表)
+        next_code = None
+        if curr_idx >= 0 and curr_idx < len(all_codes) - 1:
+            # 有下一题
+            next_code = all_codes[curr_idx + 1]
+        elif curr_idx > 0:
+            # 没有下一题,但有上一题
+            next_code = all_codes[curr_idx - 1]
+        
+        conn.close()
+        return jsonify({
+            'success': True, 
+            'message': '题目已删除',
+            'next_code': next_code,
+            'kp_code': kp_code,
+            'node_id': str(textbook_node_id) if textbook_node_id else None
+        })
+    except Exception as e:
+        try:
+            conn.rollback()
+        except Exception:
+            pass
+        return jsonify({'success': False, 'error': str(e)}), 500
+    finally:
+        try:
+            conn.close()
+        except Exception:
+            pass
+
+@app.route('/add_kp_node', methods=['POST'])
+def add_kp_node():
+    """添加新的知识点节点(section 或 subsection)"""
+    try:
+        data = request.json
+        chapter_id = data.get('chapter_id')
+        node_type = data.get('type')  # 'section' 或 'subsection'
+        section_id = data.get('section_id')  # 仅当添加 subsection 时需要
+        name = data.get('name', '').strip()
+        
+        if not chapter_id or not node_type or not name:
+            return jsonify({'success': False, 'error': '缺少必要参数'}), 400
+        
+        if node_type not in ['section', 'subsection']:
+            return jsonify({'success': False, 'error': '无效的节点类型'}), 400
+        
+        if node_type == 'subsection' and not section_id:
+            return jsonify({'success': False, 'error': '添加小节时必须提供 section_id'}), 400
+        
+        # 获取 tree_new.json 文件路径
+        tree_file = get_tree_file_path()
+        if not os.path.exists(tree_file):
+            return jsonify({'success': False, 'error': '找不到知识点结构文件'}), 500
+        
+        # 读取 JSON 文件
+        with open(tree_file, 'r', encoding='utf-8') as f:
+            tree_data = json.load(f)
+        
+        # 查找最大 ID
+        max_id = find_max_id(tree_data)
+        new_id = max_id + 1
+        
+        # 查找目标节点(chapter 或 section)
+        target_node = None
+        if node_type == 'section':
+            # 查找 chapter
+            target_node = find_node_by_id(tree_data, chapter_id)
+        else:
+            # 查找 section
+            target_node = find_node_by_id(tree_data, section_id)
+        
+        if not target_node:
+            return jsonify({'success': False, 'error': '找不到目标节点'}), 404
+        
+        # 确定 parent_id
+        parent_id = chapter_id if node_type == 'section' else section_id
+        
+        # 创建新节点
+        new_node = {
+            "id": new_id,
+            "label": name,
+            "parent_id": parent_id,
+            "kp_level": node_type,
+            "skills": [],
+            "direct_score": [],
+            "related_score": [],
+            "children": []
+        }
+        
+        # 确保 target_node 有 children 数组
+        if 'children' not in target_node:
+            target_node['children'] = []
+        
+        # 添加新节点
+        target_node['children'].append(new_node)
+        
+        # 保存回文件
+        with open(tree_file, 'w', encoding='utf-8') as f:
+            json.dump(tree_data, f, ensure_ascii=False, indent=2)
+        
+        # 重新加载结构(更新全局变量)
+        global KP_MAP, KP_HIERARCHY
+        KP_MAP, KP_HIERARCHY = load_kp_structure()
+        
+        return jsonify({
+            'success': True,
+            'message': '添加成功',
+            'new_id': new_id
+        })
+        
+    except Exception as e:
+        import traceback
+        traceback.print_exc()
+        return jsonify({'success': False, 'error': str(e)}), 500
+
+# 知识点管理 API(knowledge_points_copy1 表)
+@app.route('/api/kp/create', methods=['POST'])
+def api_kp_create():
+    """创建知识点"""
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor(dictionary=True)
+        
+        data = request.get_json()
+        kp_code = (data.get('kp_code') or '').strip()
+        name = (data.get('name') or '').strip()
+        subject = (data.get('subject') or '').strip() or None
+        grade = (data.get('grade') or '').strip() or None
+        parent_kp_code = (data.get('parent_kp_code') or '').strip() or None
+        
+        if not kp_code or not name:
+            return jsonify({'success': False, 'error': '知识点代码和名称不能为空'})
+        
+        # 检查kp_code是否已存在
+        cursor.execute("SELECT id FROM knowledge_points_copy1 WHERE kp_code = %s", (kp_code,))
+        if cursor.fetchone():
+            return jsonify({'success': False, 'error': '知识点代码已存在'})
+        
+        # 查询最大id并手动递增(确保id正确自增)
+        cursor.execute("SELECT MAX(id) as max_id FROM knowledge_points_copy1")
+        max_id_result = cursor.fetchone()
+        new_id = (max_id_result['max_id'] or 0) + 1
+        
+        # 插入新知识点
+        cursor.execute("""
+            INSERT INTO knowledge_points_copy1 
+            (id, kp_code, name, subject, grade, parent_kp_code, created_at, updated_at)
+            VALUES (%s, %s, %s, %s, %s, %s, NOW(), NOW())
+        """, (new_id, kp_code, name, subject, grade, parent_kp_code))
+        
+        conn.commit()
+        conn.close()
+        return jsonify({'success': True, 'message': '创建成功'})
+    except Exception as e:
+        if conn:
+            conn.rollback()
+            conn.close()
+        return jsonify({'success': False, 'error': str(e)})
+
+@app.route('/api/kp/update/<int:kp_id>', methods=['POST'])
+def api_kp_update(kp_id):
+    """更新知识点"""
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor(dictionary=True)
+        
+        data = request.get_json()
+        name = (data.get('name') or '').strip()
+        subject = (data.get('subject') or '').strip() or None
+        grade = (data.get('grade') or '').strip() or None
+        parent_kp_code = (data.get('parent_kp_code') or '').strip() or None
+        
+        if not name:
+            return jsonify({'success': False, 'error': '知识点名称不能为空'})
+        
+        # 更新知识点
+        cursor.execute("""
+            UPDATE knowledge_points_copy1 
+            SET name = %s, subject = %s, grade = %s, parent_kp_code = %s, updated_at = NOW()
+            WHERE id = %s
+        """, (name, subject, grade, parent_kp_code, kp_id))
+        
+        conn.commit()
+        conn.close()
+        return jsonify({'success': True, 'message': '更新成功'})
+    except Exception as e:
+        if conn:
+            conn.rollback()
+            conn.close()
+        return jsonify({'success': False, 'error': str(e)})
+
+@app.route('/api/kp/delete/<int:kp_id>', methods=['POST'])
+def api_kp_delete(kp_id):
+    """删除知识点"""
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor(dictionary=True)
+        
+        # 检查是否有子知识点
+        cursor.execute("SELECT id FROM knowledge_points_copy1 WHERE parent_kp_code = (SELECT kp_code FROM knowledge_points_copy1 WHERE id = %s)", (kp_id,))
+        if cursor.fetchone():
+            return jsonify({'success': False, 'error': '该知识点下存在子知识点,无法删除'})
+        
+        # 删除知识点
+        cursor.execute("DELETE FROM knowledge_points_copy1 WHERE id = %s", (kp_id,))
+        
+        conn.commit()
+        conn.close()
+        return jsonify({'success': True, 'message': '删除成功'})
+    except Exception as e:
+        if conn:
+            conn.rollback()
+            conn.close()
+        return jsonify({'success': False, 'error': str(e)})
+
+@app.route('/api/kp/get/<int:kp_id>', methods=['GET'])
+def api_kp_get(kp_id):
+    """获取单个知识点详情"""
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor(dictionary=True)
+        
+        cursor.execute("""
+            SELECT * FROM knowledge_points_copy1 WHERE id = %s
+        """, (kp_id,))
+        
+        kp = cursor.fetchone()
+        conn.close()
+        
+        if not kp:
+            return jsonify({'success': False, 'error': '知识点不存在'})
+        
+        # 处理JSON字段
+        for field in ['prerequisite_kp_codes', 'dependent_kp_codes', 'related_kp_codes', 'stats', 'skills', 'direct_score', 'related_score']:
+            if kp[field] and isinstance(kp[field], str):
+                try:
+                    kp[field] = json.loads(kp[field])
+                except:
+                    kp[field] = None
+        
+        return jsonify({'success': True, 'data': kp})
+    except Exception as e:
+        return jsonify({'success': False, 'error': str(e)})
+
+@app.route('/update_kp_node', methods=['POST'])
+def update_kp_node():
+    """更新知识点节点名称"""
+    try:
+        data = request.json
+        node_id = data.get('node_id')
+        node_type = data.get('node_type')  # 'chapter', 'section', 'subsection'
+        name = data.get('name', '').strip()
+        
+        if not node_id or not node_type or not name:
+            return jsonify({'success': False, 'error': '缺少必要参数'}), 400
+        
+        if node_type not in ['chapter', 'section', 'subsection']:
+            return jsonify({'success': False, 'error': '无效的节点类型'}), 400
+        
+        # 获取 tree_new.json 文件路径
+        tree_file = get_tree_file_path()
+        if not os.path.exists(tree_file):
+            return jsonify({'success': False, 'error': '找不到知识点结构文件'}), 500
+        
+        # 读取 JSON 文件
+        with open(tree_file, 'r', encoding='utf-8') as f:
+            tree_data = json.load(f)
+        
+        # 查找目标节点
+        target_node = find_node_by_id(tree_data, node_id)
+        
+        if not target_node:
+            return jsonify({'success': False, 'error': '找不到目标节点'}), 404
+        
+        # 验证节点类型是否匹配
+        if target_node.get('kp_level') != node_type:
+            return jsonify({'success': False, 'error': '节点类型不匹配'}), 400
+        
+        # 更新节点名称
+        target_node['label'] = name
+        
+        # 保存回文件
+        with open(tree_file, 'w', encoding='utf-8') as f:
+            json.dump(tree_data, f, ensure_ascii=False, indent=2)
+        
+        # 重新加载结构(更新全局变量)
+        global KP_MAP, KP_HIERARCHY
+        KP_MAP, KP_HIERARCHY = load_kp_structure()
+        
+        return jsonify({
+            'success': True,
+            'message': '修改成功'
+        })
+        
+    except Exception as e:
+        import traceback
+        traceback.print_exc()
+        return jsonify({'success': False, 'error': str(e)}), 500
+
+if __name__ == '__main__':
+    # 自动创建缺失列
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor()
+        for field, ftype in [('audit_status', 'TINYINT DEFAULT 0'), ('audit_reason', 'TEXT')]:
+            cursor.execute(f"SHOW COLUMNS FROM questions_tem LIKE '{field}'")
+            if not cursor.fetchone():
+                cursor.execute(f"ALTER TABLE questions_tem ADD COLUMN {field} {ftype}")
+        conn.commit()
+        conn.close()
+    except: pass
+    
+    # 确保 static 目录存在,并复制小猫图片(如果根目录有的话)
+    try:
+        static_dir = resource_path("static")
+        if not os.path.exists(static_dir):
+            os.makedirs(static_dir, exist_ok=True)
+        
+        import shutil
+        # 优先复制 PNG 格式,如果没有则复制 JPG(兼容旧版本)
+        cat_img_src_png = os.path.join(BASE_DIR, "小猫.png")
+        cat_img_dst_png = os.path.join(static_dir, "小猫.png")
+        cat_img_src_jpg = os.path.join(BASE_DIR, "小猫.jpg")
+        cat_img_dst_jpg = os.path.join(static_dir, "小猫.jpg")
+        
+        if os.path.exists(cat_img_src_png) and not os.path.exists(cat_img_dst_png):
+            shutil.copy2(cat_img_src_png, cat_img_dst_png)
+        elif os.path.exists(cat_img_src_jpg) and not os.path.exists(cat_img_dst_jpg):
+            shutil.copy2(cat_img_src_jpg, cat_img_dst_jpg)
+    except Exception:
+        pass  # 如果复制失败,不影响启动
+    
+    app.run(host="0.0.0.0", port=5000, debug=True)
+

+ 36 - 0
config.env.example

@@ -0,0 +1,36 @@
+# 复制本文件为 config.env,然后改成你们自己的配置
+
+# 数据库配置(默认会用 app.py 里的值,这里填了会覆盖)
+DB_HOST=rm-f8ze60yirdj8786u2wo.mysql.rds.aliyuncs.com
+DB_PORT=3306
+DB_DATABASE=math-conten-online2
+DB_USERNAME=root
+DB_PASSWORD=csqz@20255
+
+# 是否启用 SSL(JDBC 里 useSSL=true)
+# 如果你们环境 SSL 握手失败,可改成 false
+DB_USE_SSL=true
+
+# Web 服务监听
+WEB_HOST=127.0.0.1
+WEB_PORT=5000
+
+# 远程导出 PDF(默认使用你提供的接口;需要改的话在这里覆盖)
+PDF_API_URL=https://teaching-content.chunsunqiuzhu.com/api/questions/pdf
+# 接口要求的 student_id(默认写死 44)
+PDF_STUDENT_ID=44
+
+# AI 优化题干(不要把 key 提交给别人;每个人自己填)
+AI_API_KEY=
+# 可选:自定义 Base URL(留空使用默认 https://api.openai.com/v1)
+AI_BASE_URL=
+# 模型名
+AI_MODEL_NAME=gpt-5.2
+# 可选:自定义提示词(留空使用代码内默认提示词)
+AI_STEM_PROMPT=
+
+# 可选:AI 输出长度与随机性(一般不用改)
+AI_TEMPERATURE=0.0
+
+
+

+ 45 - 0
git_commands.txt

@@ -0,0 +1,45 @@
+# Git 操作命令(手动执行)
+
+## 如果 Git 已安装,请在项目目录下执行以下命令:
+
+# 1. 初始化 Git 仓库(如果还没有)
+git init
+
+# 2. 添加远程仓库
+git remote add origin https://git.yunzhixue.cn/wlh/wlh.git
+# 如果已存在,更新地址:
+git remote set-url origin https://git.yunzhixue.cn/wlh/wlh.git
+
+# 3. 添加所有文件
+git add .
+
+# 4. 提交代码
+git commit -m "初始提交:知了数学题库系统
+
+功能特性:
+- 题目管理:支持按知识点分类管理题目
+- 题目录入:支持单个录入和批量导入
+- 审核功能:支持题目审核流程
+- 知识点管理:支持知识点树形结构管理
+- 年级筛选:支持按年级(小学/初中/高中)筛选题目
+- 翻页功能:题目列表支持分页显示(每页20道题)
+- 年级标签:所有题目显示年级标签
+- 年级必填:所有录入和批量导入的题目都必须选择年级"
+
+# 5. 设置主分支
+git branch -M main
+
+# 6. 推送到远程仓库
+git push -u origin main
+
+## 如果推送需要认证,可能需要:
+# - 输入用户名和密码
+# - 或配置 SSH 密钥
+# - 或使用 Personal Access Token
+
+## 后续更新代码:
+git add .
+git commit -m "更新说明"
+git push
+
+

+ 77 - 0
git_push.bat

@@ -0,0 +1,77 @@
+@echo off
+chcp 65001 >nul
+echo ========================================
+echo 知了数学题库系统 - Git 推送脚本
+echo ========================================
+echo.
+
+cd /d "%~dp0"
+
+echo [1/6] 检查 Git 是否安装...
+git --version >nul 2>&1
+if errorlevel 1 (
+    echo 错误:未检测到 Git,请先安装 Git
+    echo 下载地址:https://git-scm.com/download/win
+    pause
+    exit /b 1
+)
+echo Git 已安装
+echo.
+
+echo [2/6] 初始化 Git 仓库(如果还没有)...
+if not exist .git (
+    git init
+    echo Git 仓库已初始化
+) else (
+    echo Git 仓库已存在
+)
+echo.
+
+echo [3/6] 检查远程仓库...
+git remote get-url origin >nul 2>&1
+if errorlevel 1 (
+    echo 添加远程仓库...
+    git remote add origin https://git.yunzhixue.cn/wlh/wlh.git
+    echo 远程仓库已添加
+) else (
+    echo 检查远程仓库地址...
+    git remote set-url origin https://git.yunzhixue.cn/wlh/wlh.git
+    echo 远程仓库地址已更新
+)
+echo.
+
+echo [4/6] 添加文件到暂存区...
+git add .
+echo 文件已添加
+echo.
+
+echo [5/6] 提交更改...
+git commit -m "更新:知了数学题库系统"
+if errorlevel 1 (
+    echo 警告:提交失败,可能是没有更改或已提交
+) else (
+    echo 提交成功
+)
+echo.
+
+echo [6/6] 推送到远程仓库...
+git branch -M main
+git push -u origin main
+if errorlevel 1 (
+    echo.
+    echo 推送失败,可能需要:
+    echo 1. 检查网络连接
+    echo 2. 确认 Git 凭据配置
+    echo 3. 确认远程仓库权限
+    pause
+    exit /b 1
+) else (
+    echo.
+    echo ========================================
+    echo 推送成功!
+    echo ========================================
+)
+echo.
+pause
+
+

+ 6 - 0
requirements.txt

@@ -0,0 +1,6 @@
+flask>=3.0.0,<4
+mysql-connector-python>=9.0.0,<10
+waitress>=2.1.2,<3
+python-dotenv>=1.0.0,<2
+
+

+ 76 - 0
run_web.py

@@ -0,0 +1,76 @@
+import os
+import sys
+import webbrowser
+
+from waitress import serve
+
+
+def _load_dotenv_if_exists():
+ 
+    try:
+        from dotenv import load_dotenv
+    except Exception:
+        return
+
+    exe_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
+    for name in ("config.env",):
+        env_path = os.path.join(exe_dir, name)
+        if os.path.exists(env_path):
+            load_dotenv(env_path, override=True)
+            break
+
+
+def main():
+    _load_dotenv_if_exists()
+
+    # 注意:必须在加载 config.env 之后再导入 app.py
+    # 否则 app.py 顶层读取环境变量(DB/AI 等)时拿不到最新配置
+    import app as webapp
+
+    host = os.getenv("WEB_HOST", "127.0.0.1")
+    port = int(os.getenv("WEB_PORT", "5000"))
+
+    # 打开浏览器(只在本机访问时打开)
+    if host in ("127.0.0.1", "localhost"):
+        try:
+            webbrowser.open(f"http://127.0.0.1:{port}", new=1)
+        except Exception:
+            pass
+
+    # 正式服务(比 Flask debug 更稳定)
+    try:
+        serve(webapp.app, host=host, port=port, threads=8)
+    except KeyboardInterrupt:
+        print("\n服务已停止")
+    except OSError as e:
+        if "Address already in use" in str(e) or "只允许使用一次" in str(e):
+            print(f"端口 {port} 已被占用,请修改 config.env 中的 WEB_PORT")
+        else:
+            print(f"服务启动失败: {e}")
+        import traceback
+        traceback.print_exc()
+    except Exception as e:
+        print(f"服务启动失败: {e}")
+        import traceback
+        traceback.print_exc()
+    except KeyboardInterrupt:
+        print("\n\n服务已停止")
+    except OSError as e:
+        if "Address already in use" in str(e) or "只允许使用一次" in str(e):
+            print(f"\n\n✗ 端口 {port} 已被占用!")
+            print(f"   请检查是否有其他程序在使用该端口")
+            print(f"   或修改 config.env 中的 WEB_PORT 为其他端口(如 8080)")
+        else:
+            print(f"\n\n✗ 服务启动失败: {e}")
+        import traceback
+        traceback.print_exc()
+    except Exception as e:
+        print(f"\n\n✗ 服务启动失败: {e}")
+        import traceback
+        traceback.print_exc()
+
+
+if __name__ == "__main__":
+    main()
+
+

BIN=BIN
static/小猫.jpg


BIN=BIN
static/小猫.png


+ 1930 - 0
templates/add_question.html

@@ -0,0 +1,1930 @@
+{% extends "layout.html" %}
+
+{% block page_title %}录入新题目{% endblock %}
+
+{% block content %}
+
+<!-- LaTeX 实时预览气泡 -->
+<div id="latex-preview-bubble" class="fixed right-8 top-24 w-[420px] max-h-[75vh] bg-white rounded-2xl shadow-2xl border border-gray-200 z-50 overflow-hidden flex flex-col" style="box-shadow: 0 20px 60px rgba(0,0,0,0.15);">
+    <div class="bg-gradient-to-r from-blue-500 to-indigo-600 text-white px-4 py-3 flex items-center justify-between">
+        <span class="text-sm font-bold">题目预览</span>
+        <button type="button" onclick="hidePreviewBubble()" class="text-white hover:text-gray-200 text-xl leading-none w-6 h-6 flex items-center justify-center rounded-full hover:bg-white/20 transition-colors">×</button>
+    </div>
+    <div id="latex-preview-content" class="flex-1 p-6 overflow-y-auto bg-gray-50 text-base leading-relaxed" style="min-height: 200px;">
+        <p class="text-sm text-gray-400 text-center">题目预览</p>
+    </div>
+</div>
+
+<form id="add-form" class="grid grid-cols-1 gap-4">
+    <div class="apple-card p-6 space-y-4">
+        <div class="border-b border-gray-100 pb-3 mb-3">
+            <div class="flex items-center gap-3 flex-wrap">
+                <h2 class="text-xl font-bold">录入新题目</h2>
+            </div>
+                <!-- 层级信息标签 -->
+                {% if chapter_label or section_label or subsection_label %}
+                <div class="flex items-center gap-2 flex-wrap">
+                    {% if chapter_label %}
+                    <div class="flex items-center gap-1.5">
+                        <span class="bg-gradient-to-r from-slate-600 to-slate-700 text-white px-2 py-0.5 rounded text-xs font-bold">章</span>
+                        <span class="text-xs text-gray-700">{{ chapter_label }}</span>
+                    </div>
+                    {% endif %}
+                    {% if section_label %}
+                    <span class="text-gray-300 text-xs">›</span>
+                    <div class="flex items-center gap-1.5">
+                        <span class="bg-gradient-to-r from-blue-500 to-blue-600 text-white px-2 py-0.5 rounded text-xs font-bold">节</span>
+                        <span class="text-xs text-gray-600">{{ section_label }}</span>
+                    </div>
+                    {% endif %}
+                    {% if subsection_label %}
+                    <span class="text-gray-300 text-xs">›</span>
+                    <div class="flex items-center gap-1.5">
+                        <span class="bg-gray-200 text-gray-700 px-2 py-0.5 rounded text-xs font-bold">小节</span>
+                        <span class="text-xs text-gray-600">{{ subsection_label }}</span>
+                    </div>
+                    {% endif %}
+                </div>
+                {% endif %}
+            </div>
+            <p class="text-gray-400 text-xs mt-1">填写题目信息,保存后自动生成题号并创建新题目</p>
+        </div>
+
+        <!-- JSON输入框(永久显示) -->
+        <div id="json-input-section" class="mb-4 p-4 bg-gray-50 rounded-lg border border-gray-200">
+            <label class="block text-sm font-bold text-gray-700 mb-2">JSON输入(实时双向同步)</label>
+            <textarea id="json-input" 
+                      class="w-full h-48 p-3 border border-gray-300 rounded-lg font-mono text-xs focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10 outline-none"
+                      placeholder='{"number": "", "stem": "", "options": {"A": "", "B": "", "C": "", "D": ""}, "answer": "", "question_type": "", "solution": "", "difficulty": ""}'
+                      oninput="handleJsonInputChange()"></textarea>
+            <p class="text-xs text-gray-500 mt-2">修改JSON自动同步到表单,修改表单字段自动同步到JSON</p>
+        </div>
+
+        <!-- 题型和难度选择 -->
+        <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
+            <div class="space-y-1">
+                <label class="text-xs font-bold text-gray-400 uppercase">题型</label>
+                <select name="question_type" id="question-type-select" class="w-full p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10 outline-none transition-all text-sm">
+                    <option value="choice" selected>选择题</option>
+                    <option value="fill">填空题</option>
+                    <option value="answer">解答题</option>
+                </select>
+            </div>
+            <!-- 难度选择 -->
+            <div id="difficulty-section" class="space-y-1">
+                <div class="flex items-center gap-2">
+                    <label class="text-xs font-bold text-gray-400 uppercase">难度</label>
+                    <button type="button" id="evaluate-difficulty-btn" onclick="evaluateDifficulty()" class="btn-apple bg-gradient-to-r from-purple-500 to-indigo-600 text-white hover:from-purple-600 hover:to-indigo-700 text-xs py-1 px-2 shadow-md whitespace-nowrap">
+                        <svg class="w-3 h-3 inline-block mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
+                        </svg>
+                        难度评价
+                    </button>
+                </div>
+                <select name="difficulty" id="difficulty-select" class="w-full p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10 outline-none transition-all text-sm">
+                    <option value="">请选择难度</option>
+                    <option value="0.2">筑基</option>
+                    <option value="0.4">提分</option>
+                    <option value="0.7">培优</option>
+                </select>
+            </div>
+        </div>
+
+        <!-- 题干编辑 -->
+        <div class="space-y-2">
+            <div class="flex items-center justify-between">
+                <label class="text-xs font-bold text-gray-400 uppercase">题干 <span class="text-red-500">*</span> (支持 LaTeX 和 HTML/SVG)</label>
+                <button 
+                    type="button" 
+                    id="upload-image-btn" 
+                    class="btn-apple bg-blue-600 text-white hover:bg-blue-700 text-xs py-1.5 px-3 h-fit"
+                    onclick="triggerStemImageUpload()"
+                >
+                    上传图片
+                </button>
+            </div>
+            <textarea 
+                id="stem-textarea" 
+                name="stem" 
+                required
+                class="w-full h-32 p-3 rounded-xl bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10 outline-none transition-all font-mono text-sm" 
+                placeholder="请输入题干内容或拖拽图片..."
+                ondrop="handleStemDrop(event)"
+                ondragover="handleStemDragOver(event)"
+                ondragleave="handleStemDragLeave(event)"
+            ></textarea>
+            <div id="upload-status" class="mt-1 text-xs hidden"></div>
+        </div>
+
+        <!-- 选项编辑 -->
+        <div id="options-section" class="space-y-2">
+            <label class="text-xs font-bold text-gray-400 uppercase">选项</label>
+            
+            <!-- 选项输入区域 - 2列布局 -->
+            <div class="grid grid-cols-2 gap-3" id="options-container">
+                <!-- 选项A -->
+                <div class="option-item" data-option="A">
+                    <label class="text-xs font-medium text-gray-600 mb-1 block">选项 A</label>
+                    <div class="flex gap-1.5">
+                        <textarea 
+                            name="option_A" 
+                            id="option-A-input"
+                            class="flex-1 h-12 p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-1 focus:ring-blue-500/10 outline-none transition-all text-xs"
+                            placeholder="输入选项A的内容或拖拽图片..."
+                            oninput="updateOptionsPreview()"
+                            ondrop="handleOptionDrop(event, 'A')"
+                            ondragover="handleOptionDragOver(event)"
+                            ondragleave="handleOptionDragLeave(event)"
+                        ></textarea>
+                        <button 
+                            type="button" 
+                            class="option-upload-btn btn-apple bg-blue-600 text-white hover:bg-blue-700 text-xs py-1.5 px-2 h-fit self-start"
+                            onclick="uploadOptionImage('A')"
+                        >
+                            上传图片
+                        </button>
+                    </div>
+                </div>
+                
+                <!-- 选项B -->
+                <div class="option-item" data-option="B">
+                    <label class="text-xs font-medium text-gray-600 mb-1 block">选项 B</label>
+                    <div class="flex gap-1.5">
+                        <textarea 
+                            name="option_B" 
+                            id="option-B-input"
+                            class="flex-1 h-12 p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-1 focus:ring-blue-500/10 outline-none transition-all text-xs"
+                            placeholder="输入选项B的内容或拖拽图片..."
+                            oninput="updateOptionsPreview()"
+                            ondrop="handleOptionDrop(event, 'B')"
+                            ondragover="handleOptionDragOver(event)"
+                            ondragleave="handleOptionDragLeave(event)"
+                        ></textarea>
+                        <button 
+                            type="button" 
+                            class="option-upload-btn btn-apple bg-blue-600 text-white hover:bg-blue-700 text-xs py-1.5 px-2 h-fit self-start"
+                            onclick="uploadOptionImage('B')"
+                        >
+                            上传图片
+                        </button>
+                    </div>
+                </div>
+                
+                <!-- 选项C -->
+                <div class="option-item" data-option="C">
+                    <label class="text-xs font-medium text-gray-600 mb-1 block">选项 C</label>
+                    <div class="flex gap-1.5">
+                        <textarea 
+                            name="option_C" 
+                            id="option-C-input"
+                            class="flex-1 h-12 p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-1 focus:ring-blue-500/10 outline-none transition-all text-xs"
+                            placeholder="输入选项C的内容或拖拽图片..."
+                            oninput="updateOptionsPreview()"
+                            ondrop="handleOptionDrop(event, 'C')"
+                            ondragover="handleOptionDragOver(event)"
+                            ondragleave="handleOptionDragLeave(event)"
+                        ></textarea>
+                        <button 
+                            type="button" 
+                            class="option-upload-btn btn-apple bg-blue-600 text-white hover:bg-blue-700 text-xs py-1.5 px-2 h-fit self-start"
+                            onclick="uploadOptionImage('C')"
+                        >
+                            上传图片
+                        </button>
+                    </div>
+                </div>
+                
+                <!-- 选项D -->
+                <div class="option-item" data-option="D">
+                    <label class="text-xs font-medium text-gray-600 mb-1 block">选项 D</label>
+                    <div class="flex gap-1.5">
+                        <textarea 
+                            name="option_D" 
+                            id="option-D-input"
+                            class="flex-1 h-12 p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-1 focus:ring-blue-500/10 outline-none transition-all text-xs"
+                            placeholder="输入选项D的内容或拖拽图片..."
+                            oninput="updateOptionsPreview()"
+                            ondrop="handleOptionDrop(event, 'D')"
+                            ondragover="handleOptionDragOver(event)"
+                            ondragleave="handleOptionDragLeave(event)"
+                        ></textarea>
+                        <button 
+                            type="button" 
+                            class="option-upload-btn btn-apple bg-blue-600 text-white hover:bg-blue-700 text-xs py-1.5 px-2 h-fit self-start"
+                            onclick="uploadOptionImage('D')"
+                        >
+                            上传图片
+                        </button>
+                    </div>
+                </div>
+            </div>
+            
+            <!-- 选项预览(可编辑) -->
+            <div class="mt-2 p-2 rounded-lg bg-gray-50 border border-gray-200">
+                <label class="text-xs font-bold text-gray-600 mb-1 block">选项预览 (JSON格式,可直接编辑)</label>
+                <textarea 
+                    id="options-preview" 
+                    class="w-full text-xs text-gray-700 font-mono bg-white p-2 rounded border border-gray-100 min-h-[80px] focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10 outline-none transition-all resize-y"
+                    placeholder='{"A": "选项A内容", "B": "选项B内容", ...}'
+                    oninput="syncOptionsFromPreview()"
+                >{}</textarea>
+                <input type="hidden" name="options" id="options-json-input">
+            </div>
+        </div>
+
+        <!-- 答案 -->
+        <div class="space-y-1">
+            <label class="text-xs font-bold text-gray-400 uppercase">正确答案</label>
+            <input type="text" name="answer" id="answer-input" class="w-full p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10 outline-none transition-all text-sm" placeholder="例如: A">
+        </div>
+
+        <!-- 解析 -->
+        <div class="space-y-2">
+            <label class="text-xs font-bold text-gray-400 uppercase">解析</label>
+            <textarea 
+                id="solution-textarea"
+                name="solution" 
+                class="w-full h-32 p-3 rounded-xl bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10 outline-none transition-all font-mono text-sm" 
+                placeholder="请输入解析内容或拖拽图片..."
+                ondrop="handleSolutionDrop(event)"
+                ondragover="handleSolutionDragOver(event)"
+                ondragleave="handleSolutionDragLeave(event)"
+            ></textarea>
+        </div>
+
+        <!-- 隐藏字段:用于提交层级信息 -->
+        {% if chapter_label %}
+        <input type="hidden" name="chapter" id="chapter-input" value="{{ chapter_label }}">
+        {% endif %}
+        {% if section_label %}
+        <input type="hidden" name="section" id="section-input" value="{{ section_label }}">
+        {% endif %}
+        {% if subsection_label %}
+        <input type="hidden" name="subsection" id="subsection-input" value="{{ subsection_label }}">
+        {% endif %}
+
+        <!-- 保存按钮 -->
+        <div class="pt-4 flex justify-end space-x-2">
+            <button type="button" onclick="window.history.back()" class="btn-apple bg-gray-100 text-gray-700 hover:bg-gray-200 text-sm py-2 px-4">取消</button>
+            <button type="submit" class="btn-apple bg-blue-600 text-white hover:bg-blue-700 shadow-lg shadow-blue-200 text-sm py-2 px-4">保存并创建 <span class="text-xs opacity-75 ml-1">(Alt+R)</span></button>
+        </div>
+    </div>
+</form>
+
+<script>
+// 工具函数:保留两位小数
+function round(value, decimals) {
+    return Number(Math.round(value + 'e' + decimals) + 'e-' + decimals);
+}
+
+// LaTeX 实时预览功能
+let previewUpdateTimer = null;
+let currentPreviewElement = null;
+
+function showPreviewBubble(element, label) {
+    const bubble = document.getElementById('latex-preview-bubble');
+    const content = document.getElementById('latex-preview-content');
+    
+    if (!bubble || !content) return;
+    
+    // 更新标题
+    const title = bubble.querySelector('.bg-gradient-to-r span');
+    if (title) {
+        title.textContent = label || '实时预览';
+    }
+    
+    currentPreviewElement = element;
+    bubble.classList.remove('hidden');
+    updatePreviewContent(element.value || '');
+}
+
+function hidePreviewBubble() {
+    const bubble = document.getElementById('latex-preview-bubble');
+    if (bubble) {
+        bubble.classList.add('hidden');
+    }
+    currentPreviewElement = null;
+}
+
+function updatePreviewContent(text) {
+    const content = document.getElementById('latex-preview-content');
+    if (!content) return;
+    
+    if (!text || !text.trim()) {
+        content.innerHTML = '<p class="text-sm text-gray-400 text-center">聚焦输入框查看预览</p>';
+        return;
+    }
+    
+    // 将文本内容转换为 HTML,保留换行和基本格式
+    let html = text
+        .replace(/&/g, '&amp;')
+        .replace(/</g, '&lt;')
+        .replace(/>/g, '&gt;')
+        .replace(/\n/g, '<br>');
+    
+    // 处理图片标签
+    html = html.replace(/&lt;image\s+src="([^"]+)"\s*\/?&gt;/gi, '<img src="$1" class="max-w-[400px] max-h-[300px] w-auto h-auto my-2 rounded-lg mx-auto block" alt="题目图片" style="object-fit: contain;">');
+    
+    content.innerHTML = html;
+    
+    // 等待 KaTeX 加载完成后渲染数学公式
+    if (window.renderMathInElement) {
+        try {
+            window.renderMathInElement(content, {
+                delimiters: [
+                    {left: "$$", right: "$$", display: true},
+                    {left: "$", right: "$", display: false},
+                    {left: "\\(", right: "\\)", display: false},
+                    {left: "\\[", right: "\\]", display: true}
+                ],
+                throwOnError: false
+            });
+        } catch (e) {
+            console.warn('LaTeX 渲染失败:', e);
+        }
+    } else {
+        // 如果 KaTeX 还没加载,等待一下再试
+        setTimeout(() => {
+            if (window.renderMathInElement) {
+                try {
+                    window.renderMathInElement(content, {
+                        delimiters: [
+                            {left: "$$", right: "$$", display: true},
+                            {left: "$", right: "$", display: false},
+                            {left: "\\(", right: "\\)", display: false},
+                            {left: "\\[", right: "\\]", display: true}
+                        ],
+                        throwOnError: false
+                    });
+                } catch (e) {
+                    console.warn('LaTeX 渲染失败:', e);
+                }
+            }
+        }, 100);
+    }
+}
+
+function setupPreviewForElement(elementId, label) {
+    const element = document.getElementById(elementId);
+    if (!element) return;
+    
+    element.addEventListener('focus', function() {
+        showPreviewBubble(this, label);
+        updatePreviewContent(this.value || '');
+    });
+    
+    element.addEventListener('input', function() {
+        if (currentPreviewElement === this) {
+            // 防抖处理,避免频繁更新
+            clearTimeout(previewUpdateTimer);
+            previewUpdateTimer = setTimeout(() => {
+                updatePreviewContent(this.value || '');
+            }, 300);
+        }
+    });
+    
+    element.addEventListener('blur', function() {
+        // 不自动隐藏,保持预览气泡显示,直到用户点击其他预览或关闭按钮
+        // 移除自动隐藏逻辑,让预览气泡一直显示
+    });
+}
+
+// 更新选项预览气泡内容(显示4个选项的渲染)
+function updateOptionsPreviewBubble() {
+    const previewTextarea = document.getElementById('options-preview');
+    const bubble = document.getElementById('latex-preview-bubble');
+    const content = document.getElementById('latex-preview-content');
+    
+    if (!previewTextarea || !bubble || !content) return;
+    
+    const jsonStr = previewTextarea.value.trim();
+    
+    if (!jsonStr || jsonStr === '{}') {
+        content.innerHTML = '<p class="text-sm text-gray-400 text-center">暂无选项内容</p>';
+        return;
+    }
+    
+    try {
+        const optionsObj = JSON.parse(jsonStr);
+        const optionKeys = ['A', 'B', 'C', 'D'];
+        
+        let html = '<div class="space-y-4">';
+        
+        optionKeys.forEach(key => {
+            const optionText = optionsObj[key] || '';
+            html += `<div class="border-b border-gray-200 pb-3 last:border-0 last:pb-0">`;
+            html += `<div class="text-xs font-bold text-gray-500 mb-2">选项 ${key}</div>`;
+            
+            if (!optionText || !optionText.trim()) {
+                html += `<p class="text-xs text-gray-400">暂无内容</p>`;
+            } else {
+                // 将文本内容转换为 HTML,保留换行和基本格式
+                let optionHtml = optionText
+                    .replace(/&/g, '&amp;')
+                    .replace(/</g, '&lt;')
+                    .replace(/>/g, '&gt;')
+                    .replace(/\n/g, '<br>');
+                
+                // 处理图片标签
+                optionHtml = optionHtml.replace(/&lt;image\s+src="([^"]+)"\s*\/?&gt;/gi, '<img src="$1" class="max-w-[400px] max-h-[300px] w-auto h-auto my-2 rounded-lg mx-auto block" alt="题目图片" style="object-fit: contain;">');
+                
+                html += `<div class="text-sm leading-relaxed">${optionHtml}</div>`;
+            }
+            
+            html += `</div>`;
+        });
+        
+        html += '</div>';
+        content.innerHTML = html;
+        
+        // 等待 KaTeX 加载完成后渲染数学公式
+        if (window.renderMathInElement) {
+            try {
+                window.renderMathInElement(content, {
+                    delimiters: [
+                        {left: "$$", right: "$$", display: true},
+                        {left: "$", right: "$", display: false},
+                        {left: "\\(", right: "\\)", display: false},
+                        {left: "\\[", right: "\\]", display: true}
+                    ],
+                    throwOnError: false
+                });
+            } catch (e) {
+                console.warn('LaTeX 渲染失败:', e);
+            }
+        } else {
+            // 如果 KaTeX 还没加载,等待一下再试
+            setTimeout(() => {
+                if (window.renderMathInElement) {
+                    try {
+                        window.renderMathInElement(content, {
+                            delimiters: [
+                                {left: "$$", right: "$$", display: true},
+                                {left: "$", right: "$", display: false},
+                                {left: "\\(", right: "\\)", display: false},
+                                {left: "\\[", right: "\\]", display: true}
+                            ],
+                            throwOnError: false
+                        });
+                    } catch (e) {
+                        console.warn('LaTeX 渲染失败:', e);
+                    }
+                }
+            }, 100);
+        }
+    } catch (error) {
+        content.innerHTML = '<p class="text-sm text-red-400 text-center">JSON 格式错误,请检查输入</p>';
+    }
+}
+
+// 设置选项预览输入框的预览功能
+function setupOptionsPreviewTextarea() {
+    const optionsPreviewTextarea = document.getElementById('options-preview');
+    if (!optionsPreviewTextarea) return;
+    
+    optionsPreviewTextarea.addEventListener('focus', function() {
+        showPreviewBubble(this, '选项预览');
+        updateOptionsPreviewBubble();
+    });
+    
+    optionsPreviewTextarea.addEventListener('input', function() {
+        if (currentPreviewElement === this) {
+            // 防抖处理,避免频繁更新
+            clearTimeout(previewUpdateTimer);
+            previewUpdateTimer = setTimeout(() => {
+                updateOptionsPreviewBubble();
+            }, 300);
+        }
+    });
+    
+    optionsPreviewTextarea.addEventListener('blur', function() {
+        // 不自动隐藏,保持预览气泡显示
+    });
+}
+
+    // 根据题型显示/隐藏选项相关内容
+    function toggleOptionsVisibility(questionType) {
+        const optionsSection = document.getElementById('options-section');
+        const difficultySection = document.getElementById('difficulty-section');
+        
+        if (optionsSection) {
+            if (questionType === 'choice') {
+                optionsSection.style.display = 'block';
+            } else {
+                optionsSection.style.display = 'none';
+            }
+        }
+        
+        // 难度选择框:所有题型都显示
+        if (difficultySection) {
+            difficultySection.style.display = 'block';
+        }
+    }
+
+// 难度评价函数
+async function evaluateDifficulty() {
+    const btn = document.getElementById('evaluate-difficulty-btn');
+    const difficultySelect = document.getElementById('difficulty-select');
+    
+    if (!btn || !difficultySelect) return;
+    
+    // 收集题目信息
+    const stemTextarea = document.getElementById('stem-textarea');
+    const answerInput = document.getElementById('answer-input');
+    const solutionTextarea = document.getElementById('solution-textarea');
+    const optionsPreview = document.getElementById('options-preview');
+    const questionTypeSelect = document.getElementById('question-type-select');
+    
+    const stem = stemTextarea ? stemTextarea.value.trim() : '';
+    
+    if (!stem) {
+        if (window.customAlert) {
+            window.customAlert('请先填写题干内容');
+        } else {
+            alert('请先填写题干内容');
+        }
+        return;
+    }
+    
+    // 构建请求数据
+    const requestData = {
+        stem: stem,
+        answer: answerInput ? answerInput.value.trim() : '',
+        solution: solutionTextarea ? solutionTextarea.value.trim() : '',
+        question_type: questionTypeSelect ? questionTypeSelect.value : ''
+    };
+    
+    // 处理选项
+    if (optionsPreview && optionsPreview.value.trim() && optionsPreview.value.trim() !== '{}') {
+        try {
+            requestData.options = JSON.parse(optionsPreview.value.trim());
+        } catch (e) {
+            console.warn('选项JSON解析失败:', e);
+        }
+    }
+    
+    // 显示加载状态
+    const originalText = btn.innerHTML;
+    btn.disabled = true;
+    btn.innerHTML = '<svg class="w-3 h-3 inline-block mr-1 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>评价中...';
+    
+    try {
+        const response = await fetch('/api/score', {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json'
+            },
+            body: JSON.stringify(requestData)
+        });
+        
+        if (!response.ok) {
+            throw new Error(`请求失败: ${response.status} ${response.statusText}`);
+        }
+        
+        const result = await response.json();
+        
+        // 调试:打印完整返回结果
+        console.log('难度评价接口返回:', result);
+        
+        // 处理返回的 difficulty_level(优先使用 data.difficulty_level,兼容旧格式)
+        let difficultyLevel = result.data?.difficulty_level || result.difficulty_level || result.difficulty || result.level;
+        
+        // 映射难度等级到枚举值
+        // 可能的返回值:字符串 "筑基"、"提分"、"培优" 或数字 0.2, 0.4, 0.7
+        let difficultyValue = '';
+        
+        if (difficultyLevel !== undefined && difficultyLevel !== null) {
+            const levelStr = String(difficultyLevel).trim();
+            
+            // 字符串匹配
+            if (levelStr === '筑基' || levelStr === '0.2' || levelStr === '0.20') {
+                difficultyValue = '0.2';
+            } else if (levelStr === '提分' || levelStr === '0.4' || levelStr === '0.40') {
+                difficultyValue = '0.4';
+            } else if (levelStr === '培优' || levelStr === '0.7' || levelStr === '0.70') {
+                difficultyValue = '0.7';
+            } else {
+                // 尝试转换为数字
+                const levelNum = parseFloat(levelStr);
+                if (!isNaN(levelNum)) {
+                    if (Math.abs(levelNum - 0.2) < 0.1) {
+                        difficultyValue = '0.2';
+                    } else if (Math.abs(levelNum - 0.4) < 0.1) {
+                        difficultyValue = '0.4';
+                    } else if (Math.abs(levelNum - 0.7) < 0.1) {
+                        difficultyValue = '0.7';
+                    }
+                }
+            }
+        }
+        
+        if (difficultyValue) {
+            difficultySelect.value = difficultyValue;
+            // 触发change事件以更新预览
+            difficultySelect.dispatchEvent(new Event('change', { bubbles: true }));
+            // 不显示弹窗,直接完成
+        } else {
+            // 如果无法识别,打印完整返回结果以便调试
+            console.error('无法识别难度等级,完整返回结果:', result);
+            if (window.customAlert) {
+                window.customAlert('无法识别返回的难度等级。返回数据:' + JSON.stringify(result));
+            } else {
+                alert('无法识别返回的难度等级。返回数据:' + JSON.stringify(result));
+            }
+        }
+        
+    } catch (error) {
+        console.error('难度评价失败:', error);
+        if (window.customAlert) {
+            window.customAlert('难度评价失败: ' + error.message);
+        } else {
+            alert('难度评价失败: ' + error.message);
+        }
+    } finally {
+        // 恢复按钮
+        btn.disabled = false;
+        btn.innerHTML = originalText;
+    }
+}
+
+// 页面加载时初始化选项预览和题型默认值
+document.addEventListener('DOMContentLoaded', function() {
+    updateOptionsPreview();
+    
+    // 读取保存的题型选择,如果没有则默认为"choice"(选择题)
+    const savedQuestionType = localStorage.getItem('default_question_type') || 'choice';
+    const questionTypeSelect = document.getElementById('question-type-select');
+    if (questionTypeSelect) {
+        questionTypeSelect.value = savedQuestionType;
+        
+        // 监听题型变化,保存用户选择并控制选项显示
+        questionTypeSelect.addEventListener('change', function() {
+            const selectedType = this.value;
+            if (selectedType) {
+                localStorage.setItem('default_question_type', selectedType);
+            }
+            toggleOptionsVisibility(selectedType);
+        });
+        
+        // 初始化时根据题型显示/隐藏选项
+        toggleOptionsVisibility(questionTypeSelect.value);
+    }
+    
+    // Alt+R 快捷键:保存并创建
+    document.addEventListener('keydown', function(e) {
+        if (e.altKey && (e.key === 'r' || e.key === 'R')) {
+            e.preventDefault();
+            const submitBtn = document.querySelector('button[type="submit"]');
+            if (submitBtn) {
+                submitBtn.click();
+            }
+        }
+    });
+    
+    // 设置 LaTeX 实时预览
+    setupPreviewForElement('stem-textarea', '题干预览');
+    setupPreviewForElement('solution-textarea', '解析预览');
+    setupPreviewForElement('option-A-input', '选项 A 预览');
+    setupPreviewForElement('option-B-input', '选项 B 预览');
+    setupPreviewForElement('option-C-input', '选项 C 预览');
+    setupPreviewForElement('option-D-input', '选项 D 预览');
+    setupPreviewForElement('option-E-input', '选项 E 预览');
+    setupPreviewForElement('option-F-input', '选项 F 预览');
+    setupPreviewForElement('answer-input', '正确答案预览');
+    
+    // 设置选项预览输入框的预览功能(在右侧气泡显示4个选项)
+    setupOptionsPreviewTextarea();
+    
+    // 为所有输入框添加粘贴图片功能
+    setupPasteImageForAllInputs();
+    
+    // 初始化JSON输入框
+    initJsonInput();
+    
+    // 设置表单字段变化时同步到JSON
+    setupFormToJsonSync();
+    
+    // 默认显示完整预览
+    updateFullPreview();
+    
+    // 保持预览气泡一直显示,不自动关闭
+    // 移除鼠标离开时的自动关闭逻辑
+});
+
+// JSON输入框相关功能
+function toggleJsonInput() {
+    const jsonSection = document.getElementById('json-input-section');
+    const toggleText = document.getElementById('json-toggle-text');
+    
+    if (jsonSection.classList.contains('hidden')) {
+        jsonSection.classList.remove('hidden');
+        toggleText.textContent = '隐藏JSON输入';
+        syncToJson(); // 显示时同步当前表单数据到JSON
+    } else {
+        jsonSection.classList.add('hidden');
+        toggleText.textContent = '显示JSON输入';
+    }
+}
+
+function initJsonInput() {
+    const jsonInput = document.getElementById('json-input');
+    const defaultJson = {
+        number: "",
+        stem: "",
+        options: {
+            A: "",
+            B: "",
+            C: "",
+            D: ""
+        },
+        answer: "",
+        question_type: "",
+        solution: "",
+        difficulty: ""
+    };
+    jsonInput.value = JSON.stringify(defaultJson, null, 2);
+}
+
+function syncFromJson() {
+    const jsonInput = document.getElementById('json-input');
+    const jsonText = jsonInput.value.trim();
+    
+    if (!jsonText) {
+        return;
+    }
+    
+    try {
+        isSyncingFromJson = true; // 标记正在从JSON同步,防止循环
+        
+        const data = JSON.parse(jsonText);
+        
+        // 填充题干
+        const stemTextarea = document.getElementById('stem-textarea');
+        if (stemTextarea && data.stem !== undefined) {
+            stemTextarea.value = data.stem || '';
+            // 触发input事件以更新预览
+            stemTextarea.dispatchEvent(new Event('input', { bubbles: true }));
+        }
+        
+        // 填充题型
+        const questionTypeSelect = document.getElementById('question-type-select');
+        if (questionTypeSelect && data.question_type !== undefined) {
+            const mappedType = mapQuestionTypeForJson(data.question_type);
+            questionTypeSelect.value = mappedType || 'choice';
+            // 触发题型变化事件
+            questionTypeSelect.dispatchEvent(new Event('change'));
+        }
+        
+        // 填充难度
+        const difficultySelect = document.getElementById('difficulty-select');
+        if (difficultySelect && data.difficulty !== undefined) {
+            difficultySelect.value = data.difficulty || '';
+        }
+        
+        // 填充选项
+        if (data.options && typeof data.options === 'object') {
+            ['A', 'B', 'C', 'D', 'E', 'F'].forEach(key => {
+                const optionInput = document.getElementById(`option-${key}-input`);
+                if (optionInput && data.options[key] !== undefined) {
+                    optionInput.value = data.options[key] || '';
+                    optionInput.dispatchEvent(new Event('input', { bubbles: true }));
+                }
+            });
+            // 更新选项预览
+            updateOptionsPreview();
+        }
+        
+        // 填充答案
+        const answerInput = document.getElementById('answer-input');
+        if (answerInput && data.answer !== undefined) {
+            answerInput.value = data.answer || '';
+            answerInput.dispatchEvent(new Event('input', { bubbles: true }));
+        }
+        
+        // 填充解析
+        const solutionTextarea = document.getElementById('solution-textarea');
+        if (solutionTextarea && data.solution !== undefined) {
+            solutionTextarea.value = data.solution || '';
+            solutionTextarea.dispatchEvent(new Event('input', { bubbles: true }));
+        }
+        
+        // 更新预览区域
+        updateFullPreview();
+        
+    } catch (error) {
+        // JSON格式错误时不显示错误,只标记输入框
+        console.warn('JSON格式错误:', error);
+    } finally {
+        isSyncingFromJson = false;
+    }
+}
+
+function syncToJson() {
+    const jsonInput = document.getElementById('json-input');
+    
+    // 收集表单数据
+    const stemTextarea = document.getElementById('stem-textarea');
+    const questionTypeSelect = document.getElementById('question-type-select');
+    const difficultySelect = document.getElementById('difficulty-select');
+    const answerInput = document.getElementById('answer-input');
+    const solutionTextarea = document.getElementById('solution-textarea');
+    
+    const optionsObj = {};
+    ['A', 'B', 'C', 'D', 'E', 'F'].forEach(key => {
+        const optionInput = document.getElementById(`option-${key}-input`);
+        if (optionInput && optionInput.value.trim()) {
+            optionsObj[key] = optionInput.value.trim();
+        }
+    });
+    
+    // 映射题型(英文转中文)
+    const questionTypeMap = {
+        'choice': '选择题',
+        'fill': '填空题',
+        'answer': '解答题'
+    };
+    
+    const jsonData = {
+        number: "",
+        stem: stemTextarea ? stemTextarea.value : "",
+        options: Object.keys(optionsObj).length > 0 ? optionsObj : { A: "", B: "", C: "", D: "" },
+        answer: answerInput ? answerInput.value : "",
+        question_type: questionTypeSelect && questionTypeSelect.value ? (questionTypeMap[questionTypeSelect.value] || '') : '',
+        solution: solutionTextarea ? solutionTextarea.value : "",
+        difficulty: difficultySelect ? difficultySelect.value : ""
+    };
+    
+    jsonInput.value = JSON.stringify(jsonData, null, 2);
+}
+
+let jsonSyncTimer = null;
+let isSyncingFromJson = false; // 防止循环同步
+
+function handleJsonInputChange() {
+    const jsonInput = document.getElementById('json-input');
+    const jsonText = jsonInput.value.trim();
+    
+    // 实时验证JSON格式
+    if (jsonText) {
+        try {
+            JSON.parse(jsonText);
+            jsonInput.classList.remove('border-red-500');
+            jsonInput.classList.add('border-gray-300');
+            
+            // 防抖处理,避免频繁同步
+            clearTimeout(jsonSyncTimer);
+            jsonSyncTimer = setTimeout(() => {
+                if (!isSyncingFromJson) {
+                    syncFromJson();
+                    updateFullPreview();
+                }
+            }, 500); // 500ms延迟
+        } catch (e) {
+            jsonInput.classList.remove('border-gray-300');
+            jsonInput.classList.add('border-red-500');
+        }
+    }
+}
+
+function mapQuestionTypeForJson(type) {
+    const typeMap = {
+        '选择题': 'choice',
+        '填空题': 'fill',
+        '解答题': 'answer',
+        'choice': 'choice',
+        'fill': 'fill',
+        'answer': 'answer'
+    };
+    return typeMap[type] || 'choice';
+}
+
+let formSyncTimer = null;
+
+function setupFormToJsonSync() {
+    // 监听所有表单字段的变化,自动同步到JSON(防抖处理)
+    const fieldsToWatch = [
+        { id: 'stem-textarea', event: 'input' },
+        { id: 'answer-input', event: 'input' },
+        { id: 'solution-textarea', event: 'input' },
+        { id: 'question-type-select', event: 'change' },
+        { id: 'difficulty-select', event: 'change' }
+    ];
+    
+    fieldsToWatch.forEach(field => {
+        const element = document.getElementById(field.id);
+        if (element) {
+            element.addEventListener(field.event, function() {
+                if (!isSyncingFromJson) {
+                    clearTimeout(formSyncTimer);
+                    formSyncTimer = setTimeout(() => {
+                        syncToJson();
+                        updateFullPreview();
+                    }, 300);
+                }
+            });
+        }
+    });
+    
+    // 监听选项输入框变化
+    ['A', 'B', 'C', 'D', 'E', 'F'].forEach(key => {
+        const optionInput = document.getElementById(`option-${key}-input`);
+        if (optionInput) {
+            optionInput.addEventListener('input', function() {
+                if (!isSyncingFromJson) {
+                    clearTimeout(formSyncTimer);
+                    formSyncTimer = setTimeout(() => {
+                        syncToJson();
+                        updateFullPreview();
+                    }, 300);
+                }
+            });
+        }
+    });
+    
+    // 监听选项预览变化
+    const optionsPreview = document.getElementById('options-preview');
+    if (optionsPreview) {
+        optionsPreview.addEventListener('input', function() {
+            if (!isSyncingFromJson) {
+                clearTimeout(formSyncTimer);
+                formSyncTimer = setTimeout(() => {
+                    syncToJson();
+                    updateFullPreview();
+                }, 300);
+            }
+        });
+    }
+}
+
+// 更新完整预览(题干、选项、解析)
+function updateFullPreview() {
+    const previewContent = document.getElementById('latex-preview-content');
+    if (!previewContent) return;
+    
+    const bubble = document.getElementById('latex-preview-bubble');
+    if (!bubble) return;
+    
+    // 收集所有数据
+    const stemTextarea = document.getElementById('stem-textarea');
+    const answerInput = document.getElementById('answer-input');
+    const solutionTextarea = document.getElementById('solution-textarea');
+    const optionsPreview = document.getElementById('options-preview');
+    const questionTypeSelect = document.getElementById('question-type-select');
+    
+    let html = '<div class="space-y-6">';
+    
+    // 题干预览
+    html += '<div class="border-b border-gray-200 pb-4">';
+    html += '<div class="text-xs font-bold text-gray-500 mb-2">题干</div>';
+    const stem = stemTextarea ? stemTextarea.value : '';
+    if (stem) {
+        let stemHtml = stem
+            .replace(/&/g, '&amp;')
+            .replace(/</g, '&lt;')
+            .replace(/>/g, '&gt;')
+            .replace(/\n/g, '<br>');
+        stemHtml = stemHtml.replace(/&lt;image\s+src="([^"]+)"\s*\/?&gt;/gi, '<img src="$1" class="max-w-[400px] max-h-[300px] w-auto h-auto my-2 rounded-lg mx-auto block" alt="题目图片" style="object-fit: contain;">');
+        html += `<div class="text-sm leading-relaxed">${stemHtml}</div>`;
+    } else {
+        html += '<p class="text-xs text-gray-400">暂无内容</p>';
+    }
+    html += '</div>';
+    
+    // 选项预览(仅选择题)
+    if (questionTypeSelect && questionTypeSelect.value === 'choice') {
+        html += '<div class="border-b border-gray-200 pb-4">';
+        html += '<div class="text-xs font-bold text-gray-500 mb-2">选项</div>';
+        
+        if (optionsPreview && optionsPreview.value.trim() && optionsPreview.value.trim() !== '{}') {
+            try {
+                const optionsObj = JSON.parse(optionsPreview.value.trim());
+                ['A', 'B', 'C', 'D'].forEach(key => {
+                    const optionText = optionsObj[key] || '';
+                    html += `<div class="mb-3">`;
+                    html += `<div class="text-xs font-bold text-gray-500 mb-1">选项 ${key}</div>`;
+                    if (optionText) {
+                        let optionHtml = optionText
+                            .replace(/&/g, '&amp;')
+                            .replace(/</g, '&lt;')
+                            .replace(/>/g, '&gt;')
+                            .replace(/\n/g, '<br>');
+                        optionHtml = optionHtml.replace(/&lt;image\s+src="([^"]+)"\s*\/?&gt;/gi, '<img src="$1" class="max-w-[400px] max-h-[300px] w-auto h-auto my-2 rounded-lg mx-auto block" alt="题目图片" style="object-fit: contain;">');
+                        html += `<div class="text-sm leading-relaxed">${optionHtml}</div>`;
+                    } else {
+                        html += '<p class="text-xs text-gray-400">暂无内容</p>';
+                    }
+                    html += `</div>`;
+                });
+            } catch (e) {
+                html += '<p class="text-xs text-red-400">选项JSON格式错误</p>';
+            }
+        } else {
+            html += '<p class="text-xs text-gray-400">暂无选项</p>';
+        }
+        html += '</div>';
+    }
+    
+    // 答案预览
+    html += '<div class="border-b border-gray-200 pb-4">';
+    html += '<div class="text-xs font-bold text-gray-500 mb-2">正确答案</div>';
+    const answer = answerInput ? answerInput.value : '';
+    if (answer) {
+        html += `<div class="text-sm font-bold text-blue-600">${answer}</div>`;
+    } else {
+        html += '<p class="text-xs text-gray-400">暂无答案</p>';
+    }
+    html += '</div>';
+    
+    // 解析预览
+    html += '<div>';
+    html += '<div class="text-xs font-bold text-gray-500 mb-2">解析</div>';
+    const solution = solutionTextarea ? solutionTextarea.value : '';
+    if (solution) {
+        let solutionHtml = solution
+            .replace(/&/g, '&amp;')
+            .replace(/</g, '&lt;')
+            .replace(/>/g, '&gt;')
+            .replace(/\n/g, '<br>');
+        solutionHtml = solutionHtml.replace(/&lt;image\s+src="([^"]+)"\s*\/?&gt;/gi, '<img src="$1" class="max-w-[400px] max-h-[300px] w-auto h-auto my-2 rounded-lg mx-auto block" alt="题目图片" style="object-fit: contain;">');
+        html += `<div class="text-sm leading-relaxed">${solutionHtml}</div>`;
+    } else {
+        html += '<p class="text-xs text-gray-400">暂无解析</p>';
+    }
+    html += '</div>';
+    
+    html += '</div>';
+    
+    previewContent.innerHTML = html;
+    
+    // 渲染LaTeX
+    if (window.renderMathInElement) {
+        try {
+            window.renderMathInElement(previewContent, {
+                delimiters: [
+                    {left: "$$", right: "$$", display: true},
+                    {left: "$", right: "$", display: false},
+                    {left: "\\(", right: "\\)", display: false},
+                    {left: "\\[", right: "\\]", display: true}
+                ],
+                throwOnError: false
+            });
+        } catch (e) {
+            console.warn('LaTeX 渲染失败:', e);
+        }
+    }
+    
+    // 显示预览气泡
+    bubble.classList.remove('hidden');
+}
+
+document.getElementById('add-form').addEventListener('submit', async (e) => {
+    e.preventDefault();
+    const formData = new FormData(e.target);
+    
+    // 构建提交数据(question_code 由后端自动生成)
+    const data = {};
+    const fields = ['stem', 'answer', 'solution', 'question_type', 
+                    'chapter', 'section', 'subsection', 'difficulty'];
+    
+    fields.forEach(field => {
+        const value = formData.get(field);
+        if (value && value.trim()) {
+            // 处理difficulty字段:转换为浮点数,保留两位小数
+            if (field === 'difficulty') {
+                const difficultyValue = parseFloat(value);
+                if (!isNaN(difficultyValue)) {
+                    data[field] = round(difficultyValue, 2);
+                }
+            } else {
+                data[field] = value.trim();
+            }
+        }
+    });
+    
+    // 如果难度为空,不提交difficulty字段
+    if (!data.difficulty || data.difficulty === '') {
+        delete data.difficulty;
+    }
+    
+    // 添加 kp_code(从URL参数获取)
+    const urlParams = new URLSearchParams(window.location.search);
+    const kpCode = urlParams.get('kp_code');
+    if (kpCode) {
+        data.kp_code = kpCode;
+    }
+    
+    // 添加 create_by(从localStorage获取用户姓名)
+    const userName = localStorage.getItem('user_name');
+    if (userName && userName.trim()) {
+        data.create_by = userName.trim();
+    }
+    
+    // 处理选项:优先使用预览区域的JSON,如果预览区域为空或无效,则从输入框收集
+    const previewTextarea = document.getElementById('options-preview');
+    const jsonInput = document.getElementById('options-json-input');
+    
+    let optionsJson = '';
+    if (previewTextarea && previewTextarea.value.trim() && previewTextarea.value.trim() !== '{}') {
+        try {
+            // 验证预览区域的JSON是否有效
+            const parsed = JSON.parse(previewTextarea.value.trim());
+            optionsJson = JSON.stringify(parsed);
+        } catch (e) {
+            // JSON无效,从输入框收集
+            const optionsObj = {};
+            const optionKeys = ['A', 'B', 'C', 'D', 'E', 'F'];
+            optionKeys.forEach(key => {
+                const input = document.getElementById(`option-${key}-input`);
+                if (input && input.value && input.value.trim()) {
+                    optionsObj[key] = input.value.trim();
+                }
+            });
+            if (Object.keys(optionsObj).length > 0) {
+                optionsJson = JSON.stringify(optionsObj);
+            }
+        }
+    } else {
+        // 预览区域为空,从输入框收集
+        const optionsObj = {};
+        const optionKeys = ['A', 'B', 'C', 'D', 'E', 'F'];
+        optionKeys.forEach(key => {
+            const input = document.getElementById(`option-${key}-input`);
+            if (input && input.value && input.value.trim()) {
+                optionsObj[key] = input.value.trim();
+            }
+        });
+        if (Object.keys(optionsObj).length > 0) {
+            optionsJson = JSON.stringify(optionsObj);
+        }
+    }
+    
+    if (optionsJson) {
+        data.options = optionsJson;
+    }
+    
+    // 验证必填项(仅题干)
+    if (!data.stem || !data.stem.trim()) {
+        const message = '请填写题干';
+        if (window.customAlert) {
+            window.customAlert(message);
+        } else {
+            alert(message);
+        }
+        return;
+    }
+    
+    const res = await fetch('/create_question', {
+        method: 'POST',
+        headers: {'Content-Type': 'application/json'},
+        body: JSON.stringify(data)
+    });
+    
+    const result = await res.json();
+    if(result.success) {
+        // 直接跳转到题目详情页,不显示弹窗
+        window.location.href = '/detail/' + result.question_code;
+    } else {
+        if (window.customAlert) {
+            window.customAlert('创建失败: ' + result.error);
+        } else {
+            alert('创建失败: ' + result.error);
+        }
+    }
+});
+
+// 更新选项预览(从输入框同步到预览区域)
+function updateOptionsPreview() {
+    const optionsObj = {};
+    const optionKeys = ['A', 'B', 'C', 'D', 'E', 'F'];
+    
+    optionKeys.forEach(key => {
+        const input = document.getElementById(`option-${key}-input`);
+        if (input && input.value && input.value.trim()) {
+            optionsObj[key] = input.value.trim();
+        }
+    });
+    
+    const preview = document.getElementById('options-preview');
+    const jsonInput = document.getElementById('options-json-input');
+    
+    if (Object.keys(optionsObj).length > 0) {
+        const jsonStr = JSON.stringify(optionsObj, null, 2);
+        preview.value = jsonStr;
+        jsonInput.value = JSON.stringify(optionsObj);
+    } else {
+        preview.value = '{}';
+        jsonInput.value = '';
+    }
+}
+
+// 从预览区域同步到选项输入框(当用户直接编辑预览时)
+function syncOptionsFromPreview() {
+    const preview = document.getElementById('options-preview');
+    const jsonInput = document.getElementById('options-json-input');
+    
+    try {
+        const jsonStr = preview.value.trim();
+        if (!jsonStr || jsonStr === '{}') {
+            // 清空所有选项输入框
+            ['A', 'B', 'C', 'D', 'E', 'F'].forEach(key => {
+                const input = document.getElementById(`option-${key}-input`);
+                if (input) {
+                    input.value = '';
+                }
+            });
+            jsonInput.value = '';
+            // 如果预览气泡正在显示,更新它
+            if (currentPreviewElement === preview) {
+                updateOptionsPreviewBubble();
+            }
+            return;
+        }
+        
+        const optionsObj = JSON.parse(jsonStr);
+        jsonInput.value = JSON.stringify(optionsObj);
+        
+        // 同步到各个选项输入框
+        ['A', 'B', 'C', 'D', 'E', 'F'].forEach(key => {
+            const input = document.getElementById(`option-${key}-input`);
+            if (input) {
+                input.value = optionsObj[key] || '';
+            }
+        });
+        
+        // 如果预览气泡正在显示,更新它
+        if (currentPreviewElement === preview) {
+            updateOptionsPreviewBubble();
+        }
+    } catch (error) {
+        // JSON 解析失败,不更新输入框,保持预览区域的内容
+        console.warn('选项预览 JSON 格式错误:', error);
+        // 如果预览气泡正在显示,显示错误信息
+        if (currentPreviewElement === preview) {
+            const content = document.getElementById('latex-preview-content');
+            if (content) {
+                content.innerHTML = '<p class="text-sm text-red-400 text-center">JSON 格式错误,请检查输入</p>';
+            }
+        }
+    }
+}
+
+// 选项输入框拖拽处理
+function handleOptionDragOver(e) {
+    e.preventDefault();
+    e.stopPropagation();
+    e.currentTarget.classList.add('border-blue-500', 'bg-blue-50');
+}
+
+function handleOptionDragLeave(e) {
+    e.preventDefault();
+    e.stopPropagation();
+    e.currentTarget.classList.remove('border-blue-500', 'bg-blue-50');
+}
+
+async function handleOptionDrop(e, optionKey) {
+    e.preventDefault();
+    e.stopPropagation();
+    const textarea = e.currentTarget;
+    textarea.classList.remove('border-blue-500', 'bg-blue-50');
+    
+    const files = e.dataTransfer.files;
+    if (files && files.length > 0) {
+        const file = files[0];
+        if (file.type.startsWith('image/')) {
+            // 直接上传图片
+            await uploadOptionImageFile(file, optionKey);
+        } else {
+            if (window.customAlert) {
+                window.customAlert('请拖拽图片文件!');
+            } else {
+                alert('请拖拽图片文件!');
+            }
+        }
+    }
+}
+
+// 上传选项图片(通过文件选择)
+async function uploadOptionImage(optionKey) {
+    // 创建隐藏的文件输入框
+    const fileInput = document.createElement('input');
+    fileInput.type = 'file';
+    fileInput.accept = 'image/*';
+    fileInput.style.display = 'none';
+    
+    fileInput.onchange = async (e) => {
+        const file = e.target.files[0];
+        if (!file) return;
+        
+        if (!file.type.startsWith('image/')) {
+            if (window.customAlert) {
+                window.customAlert('请选择图片文件!');
+            } else {
+                alert('请选择图片文件!');
+            }
+            return;
+        }
+        
+        await uploadOptionImageFile(file, optionKey);
+    };
+    
+    document.body.appendChild(fileInput);
+    fileInput.click();
+}
+
+// 通用的选项图片上传函数
+async function uploadOptionImageFile(file, optionKey) {
+    const optionInput = document.getElementById(`option-${optionKey}-input`);
+    const uploadBtn = document.querySelector(`[onclick="uploadOptionImage('${optionKey}')"]`);
+    
+    // 显示上传中状态
+    let originalText = '';
+    if (uploadBtn) {
+        originalText = uploadBtn.textContent;
+        uploadBtn.disabled = true;
+        uploadBtn.textContent = '上传中...';
+    }
+    
+    // 在输入框中显示上传中提示
+    const originalPlaceholder = optionInput.placeholder;
+    optionInput.placeholder = '正在上传图片...';
+    optionInput.style.opacity = '0.6';
+    
+    try {
+        const formData = new FormData();
+        formData.append('file', file);
+        
+        const response = await fetch('https://crmapi.dcjxb.yunzhixue.cn/file/upload', {
+            method: 'POST',
+            body: formData
+        });
+        
+        if (!response.ok) {
+            throw new Error(`上传失败: ${response.status} ${response.statusText}`);
+        }
+        
+        const result = await response.json();
+        
+        // 检查返回结果,提取URL
+        let imageUrl = null;
+        if (typeof result === 'string') {
+            imageUrl = result;
+        } else if (result.url) {
+            imageUrl = result.url;
+        } else if (result.data && result.data.url) {
+            imageUrl = result.data.url;
+        } else if (result.data && typeof result.data === 'string') {
+            imageUrl = result.data;
+        } else {
+            const resultStr = JSON.stringify(result);
+            const urlMatch = resultStr.match(/https?:\/\/[^\s"']+/);
+            if (urlMatch) {
+                imageUrl = urlMatch[0];
+            }
+        }
+        
+        if (!imageUrl) {
+            throw new Error('接口返回格式异常,未找到图片URL。返回结果:' + JSON.stringify(result));
+        }
+        
+        // 构建 image 标签
+        const imageTag = `<image src="${imageUrl}"/>`;
+        
+        // 获取当前光标位置
+        const cursorPos = optionInput.selectionStart;
+        const textBefore = optionInput.value.substring(0, cursorPos);
+        const textAfter = optionInput.value.substring(cursorPos);
+        
+        // 插入 image 标签
+        optionInput.value = textBefore + imageTag + textAfter;
+        
+        // 设置光标位置到插入内容之后
+        const newCursorPos = cursorPos + imageTag.length;
+        optionInput.setSelectionRange(newCursorPos, newCursorPos);
+        optionInput.focus();
+        
+        // 更新选项预览JSON
+        updateOptionsPreview();
+        
+        // 如果选项预览输入框正在显示预览气泡,更新预览气泡
+        const optionsPreviewTextarea = document.getElementById('options-preview');
+        if (currentPreviewElement === optionsPreviewTextarea) {
+            updateOptionsPreviewBubble();
+        }
+            
+            // 图片已插入,无需弹窗提示
+        
+    } catch (error) {
+        console.error('上传失败:', error);
+        if (window.customAlert) {
+            window.customAlert('图片上传失败: ' + error.message);
+        } else {
+            alert('图片上传失败: ' + error.message);
+        }
+    } finally {
+        if (uploadBtn) {
+            uploadBtn.disabled = false;
+            uploadBtn.textContent = originalText;
+        }
+        optionInput.placeholder = originalPlaceholder;
+        optionInput.style.opacity = '1';
+    }
+}
+
+// 触发题干图片上传
+function triggerStemImageUpload() {
+    const fileInput = document.createElement('input');
+    fileInput.type = 'file';
+    fileInput.accept = 'image/*';
+    fileInput.style.display = 'none';
+    fileInput.onchange = async (e) => {
+        const file = e.target.files[0];
+        if (file) {
+            await uploadStemImageFile(file);
+        }
+    };
+    document.body.appendChild(fileInput);
+    fileInput.click();
+    document.body.removeChild(fileInput);
+}
+
+// 题干拖拽处理函数
+function handleStemDragOver(e) {
+    e.preventDefault();
+    e.stopPropagation();
+    e.currentTarget.classList.add('border-blue-500', 'bg-blue-50');
+}
+
+function handleStemDragLeave(e) {
+    e.preventDefault();
+    e.stopPropagation();
+    e.currentTarget.classList.remove('border-blue-500', 'bg-blue-50');
+}
+
+async function handleStemDrop(e) {
+    e.preventDefault();
+    e.stopPropagation();
+    const textarea = e.currentTarget;
+    textarea.classList.remove('border-blue-500', 'bg-blue-50');
+    
+    const files = e.dataTransfer.files;
+    if (files && files.length > 0) {
+        const file = files[0];
+        if (file.type.startsWith('image/')) {
+            // 直接上传图片到题干
+            await uploadStemImageFile(file);
+        } else {
+            if (window.customAlert) {
+                window.customAlert('请拖拽图片文件!');
+            } else {
+                alert('请拖拽图片文件!');
+            }
+        }
+    }
+}
+
+// 解析拖拽处理函数
+function handleSolutionDragOver(e) {
+    e.preventDefault();
+    e.stopPropagation();
+    e.currentTarget.classList.add('border-blue-500', 'bg-blue-50');
+}
+
+function handleSolutionDragLeave(e) {
+    e.preventDefault();
+    e.stopPropagation();
+    e.currentTarget.classList.remove('border-blue-500', 'bg-blue-50');
+}
+
+async function handleSolutionDrop(e) {
+    e.preventDefault();
+    e.stopPropagation();
+    const textarea = e.currentTarget;
+    textarea.classList.remove('border-blue-500', 'bg-blue-50');
+    
+    const files = e.dataTransfer.files;
+    if (files && files.length > 0) {
+        const file = files[0];
+        if (file.type.startsWith('image/')) {
+            // 直接上传图片到解析
+            await uploadSolutionImageFile(file);
+        } else {
+            if (window.customAlert) {
+                window.customAlert('请拖拽图片文件!');
+            } else {
+                alert('请拖拽图片文件!');
+            }
+        }
+    }
+}
+
+// 题干图片上传函数
+async function uploadStemImageFile(file) {
+    const stemTextarea = document.getElementById('stem-textarea');
+    const statusDiv = document.getElementById('upload-status');
+    
+    // 显示上传中状态
+    const originalPlaceholder = stemTextarea.placeholder;
+    stemTextarea.placeholder = '正在上传图片...';
+    stemTextarea.style.opacity = '0.6';
+    
+    if (statusDiv) {
+        statusDiv.classList.remove('hidden');
+        statusDiv.textContent = '正在上传图片...';
+        statusDiv.className = 'mt-2 text-sm text-blue-600';
+    }
+    
+    try {
+        const formData = new FormData();
+        formData.append('file', file);
+        
+        const response = await fetch('https://crmapi.dcjxb.yunzhixue.cn/file/upload', {
+            method: 'POST',
+            body: formData
+        });
+        
+        if (!response.ok) {
+            throw new Error(`上传失败: ${response.status} ${response.statusText}`);
+        }
+        
+        const result = await response.json();
+        
+        // 检查返回结果,提取URL
+        let imageUrl = null;
+        if (typeof result === 'string') {
+            imageUrl = result;
+        } else if (result.url) {
+            imageUrl = result.url;
+        } else if (result.data && result.data.url) {
+            imageUrl = result.data.url;
+        } else if (result.data && typeof result.data === 'string') {
+            imageUrl = result.data;
+        } else {
+            const resultStr = JSON.stringify(result);
+            const urlMatch = resultStr.match(/https?:\/\/[^\s"']+/);
+            if (urlMatch) {
+                imageUrl = urlMatch[0];
+            }
+        }
+        
+        if (!imageUrl) {
+            throw new Error('接口返回格式异常,未找到图片URL。返回结果:' + JSON.stringify(result));
+        }
+        
+        // 构建 image 标签
+        const imageTag = `<image src="${imageUrl}"/>`;
+        
+        // 在预览区域显示图片
+        const previewDiv = document.getElementById('stem-image-preview');
+        if (previewDiv) {
+            previewDiv.innerHTML = `<img src="${imageUrl}" alt="预览图片" class="w-full h-full object-contain rounded-lg" style="max-height: 100%; max-width: 100%;">`;
+        }
+        
+        // 获取当前光标位置
+        const cursorPos = stemTextarea.selectionStart;
+        const textBefore = stemTextarea.value.substring(0, cursorPos);
+        const textAfter = stemTextarea.value.substring(cursorPos);
+        
+        // 插入 image 标签
+        stemTextarea.value = textBefore + imageTag + textAfter;
+        
+        // 设置光标位置到插入内容之后
+        const newCursorPos = cursorPos + imageTag.length;
+        stemTextarea.setSelectionRange(newCursorPos, newCursorPos);
+        stemTextarea.focus();
+        
+        // 如果预览气泡正在显示,更新预览内容
+        if (currentPreviewElement === stemTextarea) {
+            updatePreviewContent(stemTextarea.value);
+        }
+        
+        // 显示成功消息
+        if (statusDiv) {
+            statusDiv.textContent = '图片上传成功!已插入到题干中。';
+            statusDiv.className = 'mt-2 text-sm text-green-600';
+            setTimeout(() => {
+                statusDiv.classList.add('hidden');
+            }, 1500);
+        }
+        
+    } catch (error) {
+        console.error('上传失败:', error);
+        if (statusDiv) {
+            statusDiv.textContent = '上传失败: ' + error.message;
+            statusDiv.className = 'mt-2 text-sm text-red-600';
+        }
+        if (window.customAlert) {
+            window.customAlert('图片上传失败: ' + error.message);
+        } else {
+            alert('图片上传失败: ' + error.message);
+        }
+    } finally {
+        stemTextarea.placeholder = originalPlaceholder;
+        stemTextarea.style.opacity = '1';
+    }
+}
+
+// 解析图片上传函数
+async function uploadSolutionImageFile(file) {
+    const solutionTextarea = document.getElementById('solution-textarea');
+    await uploadImageToInput(file, solutionTextarea);
+}
+
+// 通用图片上传函数:上传图片并插入到指定输入框
+async function uploadImageToInput(file, inputElement) {
+    if (!inputElement || !file) {
+        return;
+    }
+    
+    // 检查文件类型
+    if (!file.type.startsWith('image/')) {
+        if (window.customAlert) {
+            window.customAlert('请选择图片文件!');
+        } else {
+            alert('请选择图片文件!');
+        }
+        return;
+    }
+    
+    // 显示上传中状态
+    const originalPlaceholder = inputElement.placeholder || '';
+    const originalOpacity = inputElement.style.opacity || '1';
+    inputElement.placeholder = '正在上传图片...';
+    inputElement.style.opacity = '0.6';
+    inputElement.disabled = true;
+    
+    try {
+        const formData = new FormData();
+        formData.append('file', file);
+        
+        const response = await fetch('https://crmapi.dcjxb.yunzhixue.cn/file/upload', {
+            method: 'POST',
+            body: formData
+        });
+        
+        if (!response.ok) {
+            throw new Error(`上传失败: ${response.status} ${response.statusText}`);
+        }
+        
+        const result = await response.json();
+        
+        // 检查返回结果,提取URL
+        let imageUrl = null;
+        if (typeof result === 'string') {
+            imageUrl = result;
+        } else if (result.url) {
+            imageUrl = result.url;
+        } else if (result.data && result.data.url) {
+            imageUrl = result.data.url;
+        } else if (result.data && typeof result.data === 'string') {
+            imageUrl = result.data;
+        } else {
+            const resultStr = JSON.stringify(result);
+            const urlMatch = resultStr.match(/https?:\/\/[^\s"']+/);
+            if (urlMatch) {
+                imageUrl = urlMatch[0];
+            }
+        }
+        
+        if (!imageUrl) {
+            throw new Error('接口返回格式异常,未找到图片URL。返回结果:' + JSON.stringify(result));
+        }
+        
+        // 构建 image 标签
+        const imageTag = `<image src="${imageUrl}"/>`;
+        
+        // 获取当前光标位置
+        const cursorPos = inputElement.selectionStart || inputElement.value.length;
+        const textBefore = inputElement.value.substring(0, cursorPos);
+        const textAfter = inputElement.value.substring(cursorPos);
+        
+        // 插入 image 标签
+        inputElement.value = textBefore + imageTag + textAfter;
+        
+        // 设置光标位置到插入内容之后
+        const newCursorPos = cursorPos + imageTag.length;
+        if (inputElement.setSelectionRange) {
+            inputElement.setSelectionRange(newCursorPos, newCursorPos);
+        }
+        inputElement.focus();
+        
+        // 触发input事件,更新预览
+        inputElement.dispatchEvent(new Event('input', { bubbles: true }));
+        
+        // 如果预览气泡正在显示,更新预览内容
+        if (currentPreviewElement === inputElement) {
+            updatePreviewContent(inputElement.value);
+        }
+        
+        // 如果是选项输入框,更新选项预览
+        if (inputElement.id && inputElement.id.startsWith('option-')) {
+            updateOptionsPreview();
+        }
+        
+    } catch (error) {
+        console.error('上传失败:', error);
+        if (window.customAlert) {
+            window.customAlert('图片上传失败: ' + error.message);
+        } else {
+            alert('图片上传失败: ' + error.message);
+        }
+    } finally {
+        inputElement.placeholder = originalPlaceholder;
+        inputElement.style.opacity = originalOpacity;
+        inputElement.disabled = false;
+    }
+}
+
+// 为所有输入框设置粘贴图片功能
+function setupPasteImageForAllInputs() {
+    // 获取所有文本输入框和文本域
+    const allInputs = document.querySelectorAll('textarea, input[type="text"]');
+    
+    allInputs.forEach(input => {
+        // 添加粘贴事件监听
+        input.addEventListener('paste', async function(e) {
+            const clipboardData = e.clipboardData || window.clipboardData;
+            if (!clipboardData) {
+                return;
+            }
+            
+            // 检查是否有图片数据
+            const items = clipboardData.items;
+            if (!items) {
+                return;
+            }
+            
+            for (let i = 0; i < items.length; i++) {
+                const item = items[i];
+                
+                // 如果是图片类型
+                if (item.type.indexOf('image') !== -1) {
+                    e.preventDefault(); // 阻止默认粘贴行为
+                    
+                    const file = item.getAsFile();
+                    if (file) {
+                        // 上传图片并插入到当前输入框
+                        await uploadImageToInput(file, input);
+                    }
+                    break;
+                }
+            }
+        });
+    });
+}
+
+// 拖拽处理函数(用于上传区域)
+function handleDragOver(e) {
+    e.preventDefault();
+    e.stopPropagation();
+    const uploadArea = document.getElementById('upload-area');
+    uploadArea.classList.add('border-blue-500', 'bg-blue-100');
+}
+
+function handleDragLeave(e) {
+    e.preventDefault();
+    e.stopPropagation();
+    const uploadArea = document.getElementById('upload-area');
+    uploadArea.classList.remove('border-blue-500', 'bg-blue-100');
+}
+
+
+function handleFileSelect(e) {
+    const file = e.target.files[0];
+    if (file) {
+        // 文件选择后不自动上传,需要点击提交按钮
+    }
+}
+
+// 上传图片函数
+async function uploadImage(fileParam = null) {
+    const fileInput = document.getElementById('image-upload');
+    const uploadBtn = document.getElementById('upload-image-btn');
+    const statusDiv = document.getElementById('upload-status');
+    const stemTextarea = document.getElementById('stem-textarea');
+    
+    // 获取文件:优先使用传入的参数,否则从 input 获取
+    let file = fileParam;
+    if (!file) {
+        // 检查是否选择了文件
+        if (!fileInput.files || fileInput.files.length === 0) {
+            if (window.customAlert) {
+                window.customAlert('请先选择图片文件!');
+            } else {
+                alert('请先选择图片文件!');
+            }
+            return;
+        }
+        file = fileInput.files[0];
+    }
+    
+    // 检查文件类型
+    if (!file || !file.type.startsWith('image/')) {
+        if (window.customAlert) {
+            window.customAlert('请选择图片文件!');
+        } else {
+            alert('请选择图片文件!');
+        }
+        return;
+    }
+    
+    // 创建 FormData
+    const formData = new FormData();
+    formData.append('file', file);
+    
+    // 禁用按钮,显示上传中
+    uploadBtn.disabled = true;
+    uploadBtn.textContent = '上传中...';
+    statusDiv.classList.remove('hidden');
+    statusDiv.textContent = '正在上传图片...';
+    statusDiv.className = 'mt-2 text-sm text-blue-600';
+    
+    try {
+        const response = await fetch('https://crmapi.dcjxb.yunzhixue.cn/file/upload', {
+            method: 'POST',
+            body: formData
+        });
+        
+        if (!response.ok) {
+            throw new Error(`上传失败: ${response.status} ${response.statusText}`);
+        }
+        
+        const result = await response.json();
+        
+        // 检查返回结果,可能的结构:{url: "..."} 或 {data: {url: "..."}} 或直接是字符串
+        let imageUrl = null;
+        if (typeof result === 'string') {
+            imageUrl = result;
+        } else if (result.url) {
+            imageUrl = result.url;
+        } else if (result.data && result.data.url) {
+            imageUrl = result.data.url;
+        } else if (result.data && typeof result.data === 'string') {
+            imageUrl = result.data;
+        } else {
+            // 尝试从结果中提取 URL
+            const resultStr = JSON.stringify(result);
+            const urlMatch = resultStr.match(/https?:\/\/[^\s"']+/);
+            if (urlMatch) {
+                imageUrl = urlMatch[0];
+            }
+        }
+        
+        if (!imageUrl) {
+            throw new Error('接口返回格式异常,未找到图片URL。返回结果:' + JSON.stringify(result));
+        }
+        
+        // 构建 image 标签
+        const imageTag = `<image src="${imageUrl}"/>`;
+        
+        // 获取当前光标位置
+        const cursorPos = stemTextarea.selectionStart;
+        const textBefore = stemTextarea.value.substring(0, cursorPos);
+        const textAfter = stemTextarea.value.substring(cursorPos);
+        
+        // 插入 image 标签
+        stemTextarea.value = textBefore + imageTag + textAfter;
+        
+        // 设置光标位置到插入内容之后
+        const newCursorPos = cursorPos + imageTag.length;
+        stemTextarea.setSelectionRange(newCursorPos, newCursorPos);
+        stemTextarea.focus();
+        
+        // 如果预览气泡正在显示,更新预览内容
+        if (currentPreviewElement === stemTextarea) {
+            updatePreviewContent(stemTextarea.value);
+        }
+        
+        // 显示成功消息(短暂显示后自动隐藏)
+        statusDiv.textContent = '图片上传成功!已插入到题干中。';
+        statusDiv.className = 'mt-2 text-sm text-green-600';
+        
+        // 清空文件选择
+        if (fileInput) {
+            fileInput.value = '';
+        }
+        
+        // 1.5秒后隐藏状态消息(不阻塞用户操作)
+        setTimeout(() => {
+            statusDiv.classList.add('hidden');
+        }, 1500);
+        
+    } catch (error) {
+        console.error('上传失败:', error);
+        statusDiv.textContent = '上传失败: ' + error.message;
+        statusDiv.className = 'mt-2 text-sm text-red-600';
+        
+        if (window.customAlert) {
+            window.customAlert('图片上传失败: ' + error.message);
+        } else {
+            alert('图片上传失败: ' + error.message);
+        }
+    } finally {
+        // 恢复按钮
+        uploadBtn.disabled = false;
+        uploadBtn.textContent = '提交';
+    }
+}
+
+</script>
+{% endblock %}
+

+ 309 - 0
templates/audit_questions.html

@@ -0,0 +1,309 @@
+{% extends "layout.html" %}
+
+{% block page_title %}审核题目{% endblock %}
+
+{% block content %}
+<div class="flex gap-6">
+    <!-- 左侧目录索引 -->
+    <div class="w-96 flex-shrink-0">
+        <div class="apple-card p-6 sticky top-4 max-h-[calc(100vh-2rem)] overflow-y-auto">
+            <div class="flex items-center justify-between mb-6 pb-3 border-b border-gray-200">
+                <h2 class="text-lg font-bold text-gray-800">未审核知识点</h2>
+            </div>
+            <nav class="space-y-1">
+                {% macro render_kp_node(node, level) %}
+                <div class="kp-node-item mb-1" data-kp-code="{{ node.kp_code }}" data-level="{{ level }}">
+                    <div class="flex items-center gap-1 group">
+                        {% if node.children|length > 0 %}
+                        <button 
+                            onclick="toggleKpNode('{{ node.kp_code }}'); event.stopPropagation();"
+                            class="w-6 h-6 rounded flex items-center justify-center text-gray-500 hover:text-orange-600 hover:bg-orange-50 transition-all flex-shrink-0 kp-expand-btn"
+                            data-expanded="{% if level == 0 %}true{% else %}false{% endif %}"
+                            data-kp-code="{{ node.kp_code }}">
+                            {% if level == 0 %}
+                            <i class="ri-subtract-line text-sm"></i>
+                            {% else %}
+                            <i class="ri-add-line text-sm"></i>
+                            {% endif %}
+                        </button>
+                        {% else %}
+                        <div class="w-6 h-6 flex items-center justify-center flex-shrink-0">
+                            <div class="w-1.5 h-1.5 rounded-full bg-gray-400"></div>
+                        </div>
+                        {% endif %}
+                        <a href="javascript:void(0)" 
+                           onclick="loadPendingQuestionsByKp('{{ node.kp_code }}', '{{ node.name }}', this); return false;"
+                           data-kp-code="{{ node.kp_code }}"
+                           data-kp-name="{{ node.name }}"
+                           class="flex-1 block px-3 py-2 rounded-lg hover:bg-gradient-to-r hover:from-orange-50 hover:to-amber-50 transition-all border-l-2 border-transparent hover:border-orange-400 kp-link">
+                            <div class="flex items-center gap-2 flex-nowrap">
+                                {% if level == 0 %}
+                                <span class="bg-gradient-to-r from-slate-600 to-slate-700 text-white px-2 py-0.5 rounded text-xs font-bold shadow-sm flex-shrink-0">章</span>
+                                {% elif level == 1 %}
+                                <span class="bg-gradient-to-r from-blue-500 to-blue-600 text-white px-2 py-0.5 rounded text-xs font-bold shadow-sm flex-shrink-0">节</span>
+                                {% else %}
+                                <span class="bg-gray-200 text-gray-700 px-1.5 py-0.5 rounded text-xs font-medium flex-shrink-0">小节</span>
+                                {% endif %}
+                                <span class="text-sm {% if level == 0 %}font-bold text-gray-800{% elif level == 1 %}font-semibold text-gray-800{% else %}font-normal text-gray-600{% endif %} group-hover:text-orange-600 transition-colors flex-1 min-w-0 truncate">{{ node.name }}</span>
+                                {% set pending_count = node.total_pending_count|default(node.pending_count|default(0)) %}
+                                {% if pending_count > 0 %}
+                                <span class="bg-orange-100 text-orange-700 px-2 py-0.5 rounded-full text-xs font-bold flex-shrink-0 ml-1">{{ pending_count }}</span>
+                                {% endif %}
+                            </div>
+                        </a>
+                    </div>
+                    {% if node.children|length > 0 %}
+                    <div class="kp-children ml-8 mt-1 {% if level >= 1 %}hidden{% endif %}" id="kp-children-{{ node.kp_code }}" data-level="{{ level }}">
+                        {% for child in node.children %}
+                            {{ render_kp_node(child, level + 1) }}
+                        {% endfor %}
+                    </div>
+                    {% endif %}
+                </div>
+                {% endmacro %}
+                
+                <!-- 其他题目节点 -->
+                {% if other_questions_count|default(0) > 0 %}
+                <div class="kp-node-item mb-2 pb-2 border-b border-gray-200" data-kp-code="null" data-level="-1">
+                    <a href="javascript:void(0)" 
+                       onclick="loadPendingQuestionsByKp('null', '其他题目', this); return false;"
+                       data-kp-code="null"
+                       data-kp-name="其他题目"
+                       class="flex-1 block px-3 py-2 rounded-lg hover:bg-gradient-to-r hover:from-purple-50 hover:to-pink-50 transition-all border-l-2 border-transparent hover:border-purple-400 kp-link">
+                        <div class="flex items-center gap-2 flex-nowrap">
+                            <span class="bg-gradient-to-r from-purple-500 to-pink-500 text-white px-2 py-0.5 rounded text-xs font-bold shadow-sm flex-shrink-0">其他</span>
+                            <span class="text-sm font-bold text-gray-800 group-hover:text-purple-600 transition-colors flex-1 min-w-0 truncate">其他题目</span>
+                            <span class="bg-orange-100 text-orange-700 px-2 py-0.5 rounded-full text-xs font-bold flex-shrink-0 ml-1">{{ other_questions_count }}</span>
+                        </div>
+                    </a>
+                </div>
+                {% endif %}
+                
+                {% if kp_tree %}
+                    {% for root_node in kp_tree %}
+                        {{ render_kp_node(root_node, 0) }}
+                    {% endfor %}
+                {% else %}
+                    <div class="text-sm text-gray-500 text-center py-8">暂无未审核题目</div>
+                {% endif %}
+            </nav>
+        </div>
+    </div>
+    
+    <!-- 右侧题目列表区域 -->
+    <div class="flex-1">
+        <div id="questions-container" class="space-y-4">
+            <div class="apple-card p-12 text-center">
+                <div class="text-gray-400 mb-4">
+                    <i class="ri-file-check-line text-6xl"></i>
+                </div>
+                <h3 class="text-lg font-bold text-gray-600 mb-2">请选择左侧知识点</h3>
+                <p class="text-sm text-gray-500 mb-6">点击知识点查看该知识点下的未审核题目</p>
+            </div>
+        </div>
+    </div>
+</div>
+
+<script>
+// 知识点目录折叠/展开功能
+function toggleKpNode(kpCode) {
+    const childrenContainer = document.getElementById(`kp-children-${kpCode}`);
+    const expandBtn = document.querySelector(`.kp-expand-btn[data-kp-code="${kpCode}"]`);
+    
+    if (!childrenContainer || !expandBtn) return;
+    
+    const isExpanded = expandBtn.getAttribute('data-expanded') === 'true';
+    const icon = expandBtn.querySelector('i');
+    
+    if (isExpanded) {
+        // 折叠
+        childrenContainer.classList.add('hidden');
+        expandBtn.setAttribute('data-expanded', 'false');
+        if (icon) {
+            icon.className = 'ri-add-line text-sm';
+        }
+    } else {
+        // 展开
+        childrenContainer.classList.remove('hidden');
+        expandBtn.setAttribute('data-expanded', 'true');
+        if (icon) {
+            icon.className = 'ri-subtract-line text-sm';
+        }
+    }
+}
+
+// 存储当前知识点的代码和名称
+let currentKpCode = null;
+let currentKpName = null;
+
+// 加载指定知识点的未审核题目列表
+function loadPendingQuestionsByKp(kpCode, kpName, linkElement) {
+    const container = document.getElementById('questions-container');
+    if (!container) return;
+    
+    // 保存当前知识点信息
+    currentKpCode = kpCode;
+    currentKpName = kpName;
+    
+    // 更新选中状态
+    document.querySelectorAll('.kp-link').forEach(link => {
+        link.classList.remove('bg-orange-50', 'border-orange-400');
+    });
+    if (linkElement) {
+        linkElement.classList.add('bg-orange-50', 'border-orange-400');
+    }
+    
+    // 显示加载状态
+    container.innerHTML = `
+        <div class="apple-card p-12 text-center">
+            <div class="text-orange-500 mb-4">
+                <i class="ri-loader-4-line text-6xl animate-spin"></i>
+            </div>
+            <p class="text-gray-600">正在加载未审核题目...</p>
+        </div>
+    `;
+    
+    // 请求未审核题目列表
+    fetch(`/api/pending_questions_by_kp/${encodeURIComponent(kpCode)}`)
+        .then(response => response.json())
+        .then(data => {
+            if (!data.success) {
+                throw new Error(data.error || '加载失败');
+            }
+            
+            const questions = data.questions || [];
+            const kpName = data.kp_name || kpCode;
+            
+            // 如果没有题目,显示提示信息
+            if (questions.length === 0) {
+                container.innerHTML = `
+                    <div class="apple-card p-12 text-center">
+                        <div class="text-gray-400 mb-4">
+                            <i class="ri-file-list-line text-6xl"></i>
+                        </div>
+                        <h3 class="text-lg font-bold text-gray-600 mb-2">该知识点下暂无未审核题目</h3>
+                        <p class="text-sm text-gray-500">所有题目已审核完成</p>
+                    </div>
+                `;
+                return;
+            }
+            
+            // 有题目时,渲染题目列表
+            let html = `<div class="mb-4 flex items-center justify-between">
+                <h3 class="text-xl font-bold text-gray-800">${kpName} <span class="text-sm font-normal text-gray-500">(${questions.length} 题待审核)</span></h3>
+            </div>`;
+            
+            questions.forEach(q => {
+                // 审核状态标签(应该都是待审核)
+                let auditBadge = '<span class="bg-orange-100 text-orange-700 px-2 py-0.5 rounded text-xs font-bold whitespace-nowrap">待审核</span>';
+                
+                // 难度标签
+                let difficultyBadge = '';
+                if (q.difficulty !== null && q.difficulty !== undefined) {
+                    const diff = parseFloat(q.difficulty);
+                    if (diff === 0.2 || diff === 0.4) {
+                        difficultyBadge = '<span class="bg-green-100 text-green-700 px-2 py-0.5 rounded text-xs font-bold whitespace-nowrap">筑基</span>';
+                    } else if (diff === 0.4 || diff === 0.6) {
+                        difficultyBadge = '<span class="bg-yellow-100 text-yellow-700 px-2 py-0.5 rounded text-xs font-bold whitespace-nowrap">提分</span>';
+                    } else if (diff === 0.7 || diff === 0.8) {
+                        difficultyBadge = '<span class="bg-orange-100 text-orange-700 px-2 py-0.5 rounded text-xs font-bold whitespace-nowrap">培优</span>';
+                    }
+                }
+                
+                // 题目类型标签
+                let typeBadge = '';
+                if (q.question_type) {
+                    const typeMap = {
+                        'single': '单选题',
+                        'multiple': '多选题',
+                        'judge': '判断题',
+                        'fill': '填空题',
+                        'essay': '解答题'
+                    };
+                    const typeName = typeMap[q.question_type] || q.question_type;
+                    typeBadge = `<span class="bg-blue-100 text-blue-700 px-2 py-0.5 rounded text-xs font-medium whitespace-nowrap">${typeName}</span>`;
+                }
+                
+                // 题干预览(限制长度)
+                const stemPreview = q.stem ? (q.stem.length > 150 ? q.stem.substring(0, 150) + '...' : q.stem) : '无题干';
+                
+                html += `
+                    <div class="apple-card p-6 flex items-center justify-between hover:shadow-lg transition-all">
+                        <div class="flex-1 pr-8">
+                            <div class="flex items-center space-x-3 mb-2 flex-wrap gap-2">
+                                <span class="text-xs font-bold uppercase tracking-wider text-gray-400">#${q.question_code}</span>
+                                ${auditBadge}
+                                ${difficultyBadge}
+                                ${typeBadge}
+                            </div>
+                            <p class="text-gray-700 line-clamp-2 math-render">${stemPreview}</p>
+                        </div>
+                        <a href="/detail/${q.question_code}" class="btn-apple bg-gradient-to-r from-orange-500 to-orange-600 text-white hover:from-orange-600 hover:to-orange-700 shadow-lg shadow-orange-200 flex items-center gap-2 px-4 py-2.5 whitespace-nowrap">
+                            <i class="ri-file-check-line"></i>
+                            <span>去审核</span>
+                        </a>
+                    </div>
+                `;
+            });
+            
+            container.innerHTML = html;
+            
+            // 渲染数学公式
+            if (window.renderMathInElement) {
+                container.querySelectorAll('.math-render').forEach(el => {
+                    try {
+                        window.renderMathInElement(el, {
+                            delimiters: [
+                                {left: "$$", right: "$$", display: true},
+                                {left: "$", right: "$", display: false},
+                                {left: "\\(", right: "\\)", display: false},
+                                {left: "\\[", right: "\\]", display: true}
+                            ],
+                            throwOnError: false
+                        });
+                    } catch (e) {
+                        console.warn("数学公式渲染失败:", e);
+                    }
+                });
+            }
+        })
+        .catch(err => {
+            container.innerHTML = `
+                <div class="apple-card p-12 text-center">
+                    <div class="text-red-500 mb-4">
+                        <i class="ri-error-warning-line text-6xl"></i>
+                    </div>
+                    <h3 class="text-lg font-bold text-gray-600 mb-2">加载失败</h3>
+                    <p class="text-sm text-gray-500">${err.message || '未知错误'}</p>
+                </div>
+            `;
+        });
+}
+
+// 页面加载完成后,初始化知识点目录
+document.addEventListener('DOMContentLoaded', function() {
+    // 默认展开第一级节点
+    const firstLevelNodes = document.querySelectorAll('.kp-node-item[data-level="0"]');
+    firstLevelNodes.forEach(node => {
+        const kpCode = node.getAttribute('data-kp-code');
+        const expandBtn = node.querySelector('.kp-expand-btn');
+        if (expandBtn && expandBtn.getAttribute('data-expanded') === 'true') {
+            const childrenContainer = document.getElementById(`kp-children-${kpCode}`);
+            if (childrenContainer) {
+                childrenContainer.classList.remove('hidden');
+            }
+        }
+    });
+});
+</script>
+
+<style>
+    .line-clamp-2 {
+        display: -webkit-box;
+        -webkit-line-clamp: 2;
+        -webkit-box-orient: vertical;
+        overflow: hidden;
+    }
+</style>
+{% endblock %}
+

+ 57 - 0
templates/db_error.html

@@ -0,0 +1,57 @@
+{% extends "layout.html" %}
+
+{% block title %}数据库连接失败 - 知了数学题库系统{% endblock %}
+
+{% block content %}
+<div class="apple-card p-10">
+  <div class="flex items-start justify-between gap-6">
+    <div>
+      <h1 class="text-2xl font-bold mb-2">数据库连接失败</h1>
+      <p class="text-gray-500">网页能打开,但数据库没连上,所以暂时取不到题目数据。</p>
+    </div>
+    <button class="btn-apple bg-blue-600 text-white hover:bg-blue-700 no-print" onclick="window.location.reload()">
+      重新加载
+    </button>
+  </div>
+
+  <div class="mt-8 grid grid-cols-1 {% if db_host %}md:grid-cols-2{% endif %} gap-6">
+    {% if db_host %}
+    <div class="bg-gray-50 rounded-2xl p-6 border border-gray-100">
+      <div class="text-sm font-bold text-gray-400 uppercase mb-3">当前数据库配置</div>
+      <div class="text-sm text-gray-700 space-y-1 font-mono">
+        <div>host: {{ db_host }}</div>
+        <div>port: {{ db_port }}</div>
+        <div>db:   {{ db_name }}</div>
+        <div>user: {{ db_user }}</div>
+      </div>
+    </div>
+    {% endif %}
+
+    <div class="bg-gray-50 rounded-2xl p-6 border border-gray-100">
+      <div class="text-sm font-bold text-gray-400 uppercase mb-3">错误信息</div>
+      <pre class="text-xs text-red-600 whitespace-pre-wrap break-words font-mono">{{ error }}</pre>
+    </div>
+  </div>
+
+  {% if db_host %}
+  <div class="mt-8 text-sm text-gray-600 leading-relaxed">
+    <div class="font-bold mb-2">你可以按这个顺序检查:</div>
+    <ul class="list-disc pl-6 space-y-1">
+      <li>网络是否能访问数据库(公司/校园网可能拦截 3306)</li>
+      <li>账号密码是否正确(root / csqz@20255)</li>
+      <li>数据库名是否正确(math-online)</li>
+      <li>如果 SSL 握手失败,可临时设置环境变量:DB_USE_SSL=false</li>
+    </ul>
+  </div>
+  {% endif %}
+</div>
+{% endblock %}
+
+
+
+
+
+
+
+
+

+ 244 - 0
templates/detail.html

@@ -0,0 +1,244 @@
+{% extends "layout.html" %}
+
+{% block page_title %}题目详情 - {{ q.question_code }}{% endblock %}
+
+{% block header_actions %}
+<div class="flex items-center gap-4">
+    <div class="text-sm font-medium text-gray-500">第 {{ curr_num }} / {{ total }} 题</div>
+    <div class="flex items-center gap-2">
+        {% if prev_code %}
+        <a href="/detail/{{ prev_code }}" class="p-2 hover:bg-gray-100 rounded-lg transition-colors">
+            <i class="ri-arrow-left-line text-gray-600"></i>
+        </a>
+        {% endif %}
+        {% if next_code %}
+        <a href="/detail/{{ next_code }}" id="next-btn" class="p-2 hover:bg-gray-100 rounded-lg transition-colors">
+            <i class="ri-arrow-right-line text-gray-600"></i>
+        </a>
+        {% endif %}
+    </div>
+</div>
+{% endblock %}
+
+{% block content %}
+<div class="mb-8 no-print flex items-center justify-between">
+    <div class="flex items-center gap-4">
+        {% if kp_code %}
+        <a href="/question_management?kp_code={{ kp_code }}" class="text-blue-600 font-medium hover:underline">← 返回列表</a>
+        {% elif node_id %}
+        <a href="/textbook/{{ node_id }}" class="text-blue-600 font-medium hover:underline">← 返回列表</a>
+        <a href="/#textbook-{{ node_id }}" class="text-blue-600 font-medium hover:underline">← 返回教材库</a>
+        {% else %}
+        <a href="javascript:history.back()" class="text-blue-600 font-medium hover:underline">← 返回上一页</a>
+        {% endif %}
+    </div>
+    {% if add_question_url %}
+    <a href="{{ add_question_url }}" id="continue-add-btn" class="btn-apple bg-gradient-to-r from-blue-600 to-indigo-600 text-white hover:from-blue-700 hover:to-indigo-700 text-sm py-2 px-4 shadow-lg shadow-blue-200">
+        ➕ 继续录题 <span class="text-xs opacity-75 ml-1">(R)</span>
+    </a>
+    {% endif %}
+</div>
+
+<div class="grid grid-cols-1 gap-8">
+    <!-- 题目卡片 -->
+    <div class="apple-card p-10">
+        <div class="flex items-center justify-between mb-8">
+            <div class="space-y-1">
+                <span class="text-xs font-bold text-gray-400 uppercase tracking-widest">Question Details</span>
+                <div class="flex items-center gap-3">
+                    <h2 class="text-2xl font-bold">{{ q.question_code }}</h2>
+                    {% if q.difficulty is not none %}
+                        {% set diff = q.difficulty %}
+                        {% if (diff | float) == 0.2 or (diff | string) == '0.2' %}
+                            <span class="px-3 py-1 rounded-full text-xs font-bold bg-green-100 text-green-700 border border-green-200">筑基</span>
+                        {% elif (diff | float) == 0.4 or (diff | string) == '0.4' %}
+                            <span class="px-3 py-1 rounded-full text-xs font-bold bg-yellow-100 text-yellow-700 border border-yellow-200">提分</span>
+                        {% elif (diff | float) == 0.7 or (diff | string) == '0.7' %}
+                            <span class="px-3 py-1 rounded-full text-xs font-bold bg-orange-100 text-orange-700 border border-orange-200">培优</span>
+                        {% endif %}
+                    {% endif %}
+                </div>
+                {% set pk = (q.get('id') or q.get('question_id') or q.get('pk_id') or q.get('qid')) %}
+                {% if pk %}
+                <div class="text-xs text-gray-400 font-mono">主键ID: {{ pk }}</div>
+                {% endif %}
+            </div>
+            <div class="flex items-center space-x-4 no-print">
+                <a href="/edit/{{ q.question_code }}" class="btn-apple bg-gradient-to-r from-blue-500 to-blue-600 text-white hover:from-blue-600 hover:to-blue-700 shadow-lg shadow-blue-200 flex items-center gap-2 px-4 py-2.5">
+                    <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
+                    </svg>
+                    <span>编辑题目</span>
+                </a>
+                <a href="/export_pdf_remote/{{ q.question_code }}" target="_blank" class="btn-apple bg-gradient-to-r from-slate-500 to-slate-600 text-white hover:from-slate-600 hover:to-slate-700 shadow-lg shadow-slate-200 flex items-center gap-2 px-4 py-2.5">
+                    <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"/>
+                    </svg>
+                    <span>导出 PDF</span>
+                </a>
+                <button onclick="deleteQuestion('{{ q.question_code }}')" class="btn-apple bg-gradient-to-r from-red-500 to-red-600 text-white hover:from-red-600 hover:to-red-700 shadow-lg shadow-red-200 flex items-center gap-2 px-4 py-2.5">
+                    <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
+                    </svg>
+                    <span>删除题目</span>
+                </button>
+            </div>
+        </div>
+
+        <div class="space-y-12">
+            <!-- 题干 -->
+            <section>
+                <h4 class="text-sm font-bold text-gray-400 mb-4 uppercase">题干</h4>
+                <div id="stem-container" class="text-xl leading-relaxed text-gray-800 math-render bg-gray-50/50 rounded-xl p-6 border border-gray-100">
+                    {{ q.stem | safe }}
+                </div>
+            </section>
+
+            <!-- 选项 -->
+            {% if options %}
+            <section>
+                <h4 class="text-sm font-bold text-gray-400 mb-4 uppercase">选项</h4>
+                <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
+                    {% for k, v in options %}
+                    <div class="p-4 rounded-xl border border-gray-100 bg-gray-50/50 flex items-start space-x-4">
+                        <span class="bg-white w-8 h-8 rounded-full flex items-center justify-center font-bold text-blue-600 shadow-sm border border-gray-100">{{ k }}</span>
+                        <div class="math-render pt-0.5">{{ v }}</div>
+                    </div>
+                    {% endfor %}
+                </div>
+            </section>
+            {% endif %}
+
+            <!-- 答案与解析 -->
+            <section class="pt-8 border-t border-gray-100">
+                <div class="grid grid-cols-1 md:grid-cols-2 gap-8">
+                    <div>
+                        <h4 class="text-sm font-bold text-gray-400 mb-4 uppercase">正确答案</h4>
+                        <div class="text-lg font-bold text-green-600 bg-green-50 px-6 py-4 rounded-2xl inline-block math-render">
+                            {{ q.answer }}
+                        </div>
+                    </div>
+                    <div>
+                        <h4 class="text-sm font-bold text-gray-400 mb-4 uppercase">解析</h4>
+                        <div class="text-gray-600 leading-relaxed math-render">
+                            {{ q.solution | safe }}
+                        </div>
+                    </div>
+                </div>
+            </section>
+        </div>
+    </div>
+
+    <!-- 审核控制台 (固定在底部) -->
+    <div class="no-print fixed bottom-8 left-1/2 -translate-x-1/2 w-full max-w-2xl px-6">
+        <div class="bg-white/90 backdrop-blur-2xl p-4 rounded-3xl shadow-2xl border border-white/20 flex items-center justify-between">
+            <div class="flex items-center space-x-4 ml-2">
+                <div class="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center">
+                    <svg class="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
+                </div>
+                <div class="text-sm">
+                    <div class="font-bold">审核状态: {{ q.audit_reason or '待处理' }}</div>
+                    <div class="text-gray-400 text-xs">{{ q.kp_code }}</div>
+                </div>
+            </div>
+            <div class="flex items-center space-x-3">
+                <button onclick="postAudit('{{ q.question_code }}', '不合格')" class="btn-apple bg-red-50 text-red-600 hover:bg-red-100">❌ 判定不合格</button>
+                <button onclick="postAudit('{{ q.question_code }}', '合格')" class="btn-apple bg-blue-600 text-white hover:bg-blue-700 shadow-xl shadow-blue-200">✅ 判定合格</button>
+            </div>
+        </div>
+    </div>
+</div>
+
+<script>
+    const QUESTION_CODE = "{{ q.question_code }}";
+    
+    // 键盘快捷键:Q 判定不合格,E 判定合格,R 继续录题
+    document.addEventListener('keydown', function(e) {
+        // 如果焦点在输入框、文本域等可输入元素上,不触发快捷键
+        if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) {
+            return;
+        }
+        
+        // Q 键:判定不合格
+        if (e.key === 'q' || e.key === 'Q') {
+            e.preventDefault();
+            postAudit(QUESTION_CODE, '不合格');
+        }
+        
+        // E 键:判定合格
+        if (e.key === 'e' || e.key === 'E') {
+            e.preventDefault();
+            postAudit(QUESTION_CODE, '合格');
+        }
+        
+        // R 键:继续录题
+        if ((e.key === 'r' || e.key === 'R') && document.getElementById('continue-add-btn')) {
+            e.preventDefault();
+            document.getElementById('continue-add-btn').click();
+        }
+    });
+
+    // 优化题干显示:自动渲染数学公式
+    function renderMathInStem() {
+        const stemContainer = document.getElementById("stem-container");
+        if (stemContainer && window.renderMathInElement) {
+            try {
+                window.renderMathInElement(stemContainer, {
+                    delimiters: [
+                        {left: "$$", right: "$$", display: true},
+                        {left: "$", right: "$", display: false},
+                        {left: "\\(", right: "\\)", display: false},
+                        {left: "\\[", right: "\\]", display: true}
+                    ],
+                    throwOnError: false
+                });
+            } catch (e) {
+                // 渲染失败不阻塞功能
+                console.warn("数学公式渲染失败:", e);
+            }
+        }
+    }
+
+    // 页面加载完成后自动渲染题干中的数学公式
+    if (document.readyState === 'loading') {
+        document.addEventListener('DOMContentLoaded', renderMathInStem);
+    } else {
+        // DOM已经加载完成,立即执行
+        renderMathInStem();
+    }
+
+    function deleteQuestion(questionCode) {
+        if (!confirm(`确定要删除题目 ${questionCode} 吗?\n\n此操作不可恢复!`)) {
+            return;
+        }
+
+        fetch(`/api/delete_question/${encodeURIComponent(questionCode)}`, {
+            method: "POST",
+            headers: {"Content-Type": "application/json"}
+        }).then(r => r.json()).then(data => {
+            if (!data || !data.success) {
+                throw new Error((data && data.error) ? data.error : "未知错误");
+            }
+            // 优先跳转到下一题,没有则上一题,都没有则返回上一页
+            if (data.next_code) {
+                window.location.href = `/detail/${data.next_code}`;
+            } else if (data.prev_code) {
+                window.location.href = `/detail/${data.prev_code}`;
+            } else if (data.kp_code) {
+                window.location.href = `/questions/${data.kp_code}`;
+            } else if (data.node_id) {
+                window.location.href = `/textbook/${data.node_id}`;
+            } else {
+                window.history.back();
+            }
+        }).catch(err => {
+            if (window.customAlert) {
+                window.customAlert("删除失败:" + (err && err.message ? err.message : String(err)));
+            } else {
+                alert("删除失败:" + (err && err.message ? err.message : String(err)));
+            }
+        });
+    }
+</script>
+{% endblock %}
+

+ 1370 - 0
templates/edit.html

@@ -0,0 +1,1370 @@
+{% extends "layout.html" %}
+
+{% block page_title %}编辑题目 - {{ q.question_code }}{% endblock %}
+
+{% block content %}
+<div class="mb-8 no-print">
+    <a href="/detail/{{ q.question_code }}" class="text-blue-600 font-medium hover:underline">← 取消编辑</a>
+</div>
+
+<!-- LaTeX 实时预览气泡 -->
+<div id="latex-preview-bubble" class="fixed right-8 top-24 w-[420px] max-h-[75vh] bg-white rounded-2xl shadow-2xl border border-gray-200 z-50 hidden overflow-hidden flex flex-col" style="box-shadow: 0 20px 60px rgba(0,0,0,0.15);">
+    <div class="bg-gradient-to-r from-blue-500 to-indigo-600 text-white px-4 py-3 flex items-center justify-between">
+        <span class="text-sm font-bold">实时预览</span>
+        <button type="button" onclick="hidePreviewBubble()" class="text-white hover:text-gray-200 text-xl leading-none w-6 h-6 flex items-center justify-center rounded-full hover:bg-white/20 transition-colors">×</button>
+    </div>
+    <div id="latex-preview-content" class="flex-1 p-6 overflow-y-auto bg-gray-50 text-base leading-relaxed" style="min-height: 200px;">
+        <p class="text-sm text-gray-400 text-center">聚焦输入框查看预览</p>
+    </div>
+</div>
+
+<form id="edit-form" class="grid grid-cols-1 gap-4">
+    <input type="hidden" name="question_code" value="{{ q.question_code }}">
+    <div class="apple-card p-6 space-y-4">
+        <div class="border-b border-gray-100 pb-3 mb-3">
+            <div class="flex items-center gap-3 flex-wrap">
+                <h2 class="text-xl font-bold">编辑题目</h2>
+            </div>
+            <p class="text-gray-400 text-xs mt-1">修改题目信息,保存后生效</p>
+        </div>
+
+        <!-- 题型和难度选择 -->
+        <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
+            <div class="space-y-1">
+                <label class="text-xs font-bold text-gray-400 uppercase">题型</label>
+                <select name="question_type" id="question-type-select" data-original="{{ q.question_type or '' }}" class="w-full p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10 outline-none transition-all text-sm">
+                    <option value="">请选择题型</option>
+                    <option value="choice" {% if q.question_type == 'choice' %}selected{% endif %}>选择题</option>
+                    <option value="fill" {% if q.question_type == 'fill' %}selected{% endif %}>填空题</option>
+                    <option value="answer" {% if q.question_type == 'answer' %}selected{% endif %}>解答题</option>
+                </select>
+            </div>
+            <!-- 难度选择 -->
+            <div id="difficulty-section" class="space-y-1">
+                <label class="text-xs font-bold text-gray-400 uppercase block">难度</label>
+                <div class="flex items-center gap-2">
+                    <select name="difficulty" id="difficulty-select" data-original="{{ q.difficulty or '' }}" class="flex-1 p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10 outline-none transition-all text-sm">
+                        <option value="">请选择难度</option>
+                        {% set diff = q.difficulty %}
+                        {% if diff %}
+                            {% set diff_str = diff | string %}
+                            {% set diff_float = diff | float if diff is number else 0.0 %}
+                        {% else %}
+                            {% set diff_str = '' %}
+                            {% set diff_float = 0.0 %}
+                        {% endif %}
+                        <option value="0.2" {% if diff_str == '0.2' or diff == 0.2 or (diff_float >= 0.19 and diff_float <= 0.21) %}selected{% endif %}>筑基</option>
+                        <option value="0.4" {% if diff_str == '0.4' or diff == 0.4 or (diff_float >= 0.39 and diff_float <= 0.41) %}selected{% endif %}>提分</option>
+                        <option value="0.7" {% if diff_str == '0.7' or diff == 0.7 or (diff_float >= 0.69 and diff_float <= 0.71) %}selected{% endif %}>培优</option>
+                    </select>
+                    <button type="button" id="evaluate-difficulty-btn" onclick="evaluateDifficulty()" class="btn-apple bg-gradient-to-r from-purple-500 to-indigo-600 text-white hover:from-purple-600 hover:to-indigo-700 text-xs py-1 px-2 shadow-md whitespace-nowrap flex-shrink-0">
+                        <svg class="w-3 h-3 inline-block mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
+                        </svg>
+                        难度评价
+                    </button>
+                </div>
+            </div>
+        </div>
+
+        <!-- 题干编辑 -->
+        <div class="space-y-2">
+            <div class="flex items-center justify-between">
+                <label class="text-xs font-bold text-gray-400 uppercase">题干 <span class="text-red-500">*</span> (支持 LaTeX 和 HTML/SVG)</label>
+                <button 
+                    type="button" 
+                    id="upload-image-btn" 
+                    class="btn-apple bg-blue-600 text-white hover:bg-blue-700 text-xs py-1.5 px-3 h-fit"
+                    onclick="triggerStemImageUpload()"
+                >
+                    上传图片
+                </button>
+            </div>
+            <textarea 
+                id="stem-textarea" 
+                name="stem" 
+                data-original="{{ q.stem | tojson }}"
+                required
+                class="w-full h-32 p-3 rounded-xl bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10 outline-none transition-all font-mono text-sm" 
+                placeholder="请输入题干内容或拖拽图片..."
+                ondrop="handleStemDrop(event)"
+                ondragover="handleStemDragOver(event)"
+                ondragleave="handleStemDragLeave(event)"
+            >{{ q.stem or '' }}</textarea>
+            <div id="upload-status" class="mt-1 text-xs hidden"></div>
+        </div>
+
+        <!-- 选项编辑 -->
+        <div id="options-section" class="space-y-2">
+            <label class="text-xs font-bold text-gray-400 uppercase">选项</label>
+            
+            <!-- 选项输入区域 - 2列布局 -->
+            <div class="grid grid-cols-2 gap-3" id="options-container">
+                <!-- 选项A -->
+                <div class="option-item" data-option="A">
+                    <label class="text-xs font-medium text-gray-600 mb-1 block">选项 A</label>
+                    <div class="flex gap-1.5">
+                        <textarea 
+                            name="option_A" 
+                            id="option-A-input"
+                            class="flex-1 h-12 p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-1 focus:ring-blue-500/10 outline-none transition-all text-xs"
+                            placeholder="输入选项A的内容或拖拽图片..."
+                            oninput="updateOptionsPreview()"
+                            ondrop="handleOptionDrop(event, 'A')"
+                            ondragover="handleOptionDragOver(event)"
+                            ondragleave="handleOptionDragLeave(event)"
+                        ></textarea>
+                        <button 
+                            type="button" 
+                            class="option-upload-btn btn-apple bg-blue-600 text-white hover:bg-blue-700 text-xs py-1.5 px-2 h-fit self-start"
+                            onclick="uploadOptionImage('A')"
+                        >
+                            上传图片
+                        </button>
+                    </div>
+                </div>
+                
+                <!-- 选项B -->
+                <div class="option-item" data-option="B">
+                    <label class="text-xs font-medium text-gray-600 mb-1 block">选项 B</label>
+                    <div class="flex gap-1.5">
+                        <textarea 
+                            name="option_B" 
+                            id="option-B-input"
+                            class="flex-1 h-12 p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-1 focus:ring-blue-500/10 outline-none transition-all text-xs"
+                            placeholder="输入选项B的内容或拖拽图片..."
+                            oninput="updateOptionsPreview()"
+                            ondrop="handleOptionDrop(event, 'B')"
+                            ondragover="handleOptionDragOver(event)"
+                            ondragleave="handleOptionDragLeave(event)"
+                        ></textarea>
+                        <button 
+                            type="button" 
+                            class="option-upload-btn btn-apple bg-blue-600 text-white hover:bg-blue-700 text-xs py-1.5 px-2 h-fit self-start"
+                            onclick="uploadOptionImage('B')"
+                        >
+                            上传图片
+                        </button>
+                    </div>
+                </div>
+                
+                <!-- 选项C -->
+                <div class="option-item" data-option="C">
+                    <label class="text-xs font-medium text-gray-600 mb-1 block">选项 C</label>
+                    <div class="flex gap-1.5">
+                        <textarea 
+                            name="option_C" 
+                            id="option-C-input"
+                            class="flex-1 h-12 p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-1 focus:ring-blue-500/10 outline-none transition-all text-xs"
+                            placeholder="输入选项C的内容或拖拽图片..."
+                            oninput="updateOptionsPreview()"
+                            ondrop="handleOptionDrop(event, 'C')"
+                            ondragover="handleOptionDragOver(event)"
+                            ondragleave="handleOptionDragLeave(event)"
+                        ></textarea>
+                        <button 
+                            type="button" 
+                            class="option-upload-btn btn-apple bg-blue-600 text-white hover:bg-blue-700 text-xs py-1.5 px-2 h-fit self-start"
+                            onclick="uploadOptionImage('C')"
+                        >
+                            上传图片
+                        </button>
+                    </div>
+                </div>
+                
+                <!-- 选项D -->
+                <div class="option-item" data-option="D">
+                    <label class="text-xs font-medium text-gray-600 mb-1 block">选项 D</label>
+                    <div class="flex gap-1.5">
+                        <textarea 
+                            name="option_D" 
+                            id="option-D-input"
+                            class="flex-1 h-12 p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-1 focus:ring-blue-500/10 outline-none transition-all text-xs"
+                            placeholder="输入选项D的内容或拖拽图片..."
+                            oninput="updateOptionsPreview()"
+                            ondrop="handleOptionDrop(event, 'D')"
+                            ondragover="handleOptionDragOver(event)"
+                            ondragleave="handleOptionDragLeave(event)"
+                        ></textarea>
+                        <button 
+                            type="button" 
+                            class="option-upload-btn btn-apple bg-blue-600 text-white hover:bg-blue-700 text-xs py-1.5 px-2 h-fit self-start"
+                            onclick="uploadOptionImage('D')"
+                        >
+                            上传图片
+                        </button>
+                    </div>
+                </div>
+            </div>
+            
+            <!-- 选项预览(可编辑) -->
+            <div class="mt-2 p-2 rounded-lg bg-gray-50 border border-gray-200">
+                <label class="text-xs font-bold text-gray-600 mb-1 block">选项预览 (JSON格式,可直接编辑)</label>
+                <textarea 
+                    id="options-preview" 
+                    data-original="{{ q.options | tojson }}"
+                    class="w-full text-xs text-gray-700 font-mono bg-white p-2 rounded border border-gray-100 min-h-[80px] focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10 outline-none transition-all resize-y"
+                    placeholder='{"A": "选项A内容", "B": "选项B内容", ...}'
+                    oninput="syncOptionsFromPreview()"
+                >{{ q.options or '{}' }}</textarea>
+                <input type="hidden" name="options" id="options-json-input">
+            </div>
+        </div>
+
+        <!-- 答案 -->
+        <div class="space-y-1">
+            <label class="text-xs font-bold text-gray-400 uppercase">正确答案 <span class="text-red-500">*</span></label>
+            <input type="text" name="answer" id="answer-input" data-original="{{ q.answer | tojson }}" value="{{ q.answer or '' }}" required class="w-full p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10 outline-none transition-all text-sm" placeholder="例如: A">
+        </div>
+
+        <!-- 解析 -->
+        <div class="space-y-2">
+            <label class="text-xs font-bold text-gray-400 uppercase">解析 <span class="text-red-500">*</span></label>
+            <textarea 
+                id="solution-textarea"
+                name="solution" 
+                data-original="{{ q.solution | tojson }}"
+                required
+                class="w-full h-32 p-3 rounded-xl bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10 outline-none transition-all font-mono text-sm" 
+                placeholder="请输入解析内容或拖拽图片..."
+                ondrop="handleSolutionDrop(event)"
+                ondragover="handleSolutionDragOver(event)"
+                ondragleave="handleSolutionDragLeave(event)"
+            >{{ q.solution or '' }}</textarea>
+        </div>
+
+        <!-- 保存按钮 -->
+        <div class="pt-4 flex justify-end space-x-2">
+            <button type="button" onclick="window.location.href='/detail/{{ q.question_code }}'" class="btn-apple bg-gray-100 text-gray-700 hover:bg-gray-200 text-sm py-2 px-4">取消</button>
+            <button type="submit" class="btn-apple bg-blue-600 text-white hover:bg-blue-700 shadow-lg shadow-blue-200 text-sm py-2 px-4">保存修改</button>
+        </div>
+    </div>
+</form>
+
+<script>
+// 工具函数:保留两位小数
+function round(value, decimals) {
+    return Number(Math.round(value + 'e' + decimals) + 'e-' + decimals);
+}
+
+// LaTeX 实时预览功能
+let previewUpdateTimer = null;
+let currentPreviewElement = null;
+
+function showPreviewBubble(element, label) {
+    const bubble = document.getElementById('latex-preview-bubble');
+    const content = document.getElementById('latex-preview-content');
+    
+    if (!bubble || !content) return;
+    
+    // 更新标题
+    const title = bubble.querySelector('.bg-gradient-to-r span');
+    if (title) {
+        title.textContent = label || '实时预览';
+    }
+    
+    currentPreviewElement = element;
+    bubble.classList.remove('hidden');
+    updatePreviewContent(element.value || '');
+}
+
+function hidePreviewBubble() {
+    const bubble = document.getElementById('latex-preview-bubble');
+    if (bubble) {
+        bubble.classList.add('hidden');
+    }
+    currentPreviewElement = null;
+}
+
+function updatePreviewContent(text) {
+    const content = document.getElementById('latex-preview-content');
+    if (!content) return;
+    
+    if (!text || !text.trim()) {
+        content.innerHTML = '<p class="text-sm text-gray-400 text-center">聚焦输入框查看预览</p>';
+        return;
+    }
+    
+    // 将文本内容转换为 HTML,保留换行和基本格式
+    let html = text
+        .replace(/&/g, '&amp;')
+        .replace(/</g, '&lt;')
+        .replace(/>/g, '&gt;')
+        .replace(/\n/g, '<br>');
+    
+    // 处理图片标签
+    html = html.replace(/&lt;image\s+src="([^"]+)"\s*\/?&gt;/gi, '<img src="$1" class="max-w-[400px] max-h-[300px] w-auto h-auto my-2 rounded-lg mx-auto block" alt="题目图片" style="object-fit: contain;">');
+    
+    content.innerHTML = html;
+    
+    // 等待 KaTeX 加载完成后渲染数学公式
+    if (window.renderMathInElement) {
+        try {
+            window.renderMathInElement(content, {
+                delimiters: [
+                    {left: "$$", right: "$$", display: true},
+                    {left: "$", right: "$", display: false},
+                    {left: "\\(", right: "\\)", display: false},
+                    {left: "\\[", right: "\\]", display: true}
+                ],
+                throwOnError: false
+            });
+        } catch (e) {
+            console.warn('LaTeX 渲染失败:', e);
+        }
+    } else {
+        // 如果 KaTeX 还没加载,等待一下再试
+        setTimeout(() => {
+            if (window.renderMathInElement) {
+                try {
+                    window.renderMathInElement(content, {
+                        delimiters: [
+                            {left: "$$", right: "$$", display: true},
+                            {left: "$", right: "$", display: false},
+                            {left: "\\(", right: "\\)", display: false},
+                            {left: "\\[", right: "\\]", display: true}
+                        ],
+                        throwOnError: false
+                    });
+                } catch (e) {
+                    console.warn('LaTeX 渲染失败:', e);
+                }
+            }
+        }, 100);
+    }
+}
+
+function setupPreviewForElement(elementId, label) {
+    const element = document.getElementById(elementId);
+    if (!element) return;
+    
+    element.addEventListener('focus', function() {
+        showPreviewBubble(this, label);
+        updatePreviewContent(this.value || '');
+    });
+    
+    element.addEventListener('input', function() {
+        if (currentPreviewElement === this) {
+            // 防抖处理,避免频繁更新
+            clearTimeout(previewUpdateTimer);
+            previewUpdateTimer = setTimeout(() => {
+                updatePreviewContent(this.value || '');
+            }, 300);
+        }
+    });
+    
+    element.addEventListener('blur', function() {
+        // 不自动隐藏,保持预览气泡显示
+    });
+}
+
+// 更新选项预览气泡内容(显示4个选项的渲染)
+function updateOptionsPreviewBubble() {
+    const previewTextarea = document.getElementById('options-preview');
+    const bubble = document.getElementById('latex-preview-bubble');
+    const content = document.getElementById('latex-preview-content');
+    
+    if (!previewTextarea || !bubble || !content) return;
+    
+    const jsonStr = previewTextarea.value.trim();
+    
+    if (!jsonStr || jsonStr === '{}') {
+        content.innerHTML = '<p class="text-sm text-gray-400 text-center">暂无选项内容</p>';
+        return;
+    }
+    
+    try {
+        const optionsObj = JSON.parse(jsonStr);
+        const optionKeys = ['A', 'B', 'C', 'D'];
+        
+        let html = '<div class="space-y-4">';
+        
+        optionKeys.forEach(key => {
+            const optionText = optionsObj[key] || '';
+            html += `<div class="border-b border-gray-200 pb-3 last:border-0 last:pb-0">`;
+            html += `<div class="text-xs font-bold text-gray-500 mb-2">选项 ${key}</div>`;
+            
+            if (!optionText || !optionText.trim()) {
+                html += `<p class="text-xs text-gray-400">暂无内容</p>`;
+            } else {
+                // 将文本内容转换为 HTML,保留换行和基本格式
+                let optionHtml = optionText
+                    .replace(/&/g, '&amp;')
+                    .replace(/</g, '&lt;')
+                    .replace(/>/g, '&gt;')
+                    .replace(/\n/g, '<br>');
+                
+                // 处理图片标签
+                optionHtml = optionHtml.replace(/&lt;image\s+src="([^"]+)"\s*\/?&gt;/gi, '<img src="$1" class="max-w-[400px] max-h-[300px] w-auto h-auto my-2 rounded-lg mx-auto block" alt="题目图片" style="object-fit: contain;">');
+                
+                html += `<div class="text-sm leading-relaxed">${optionHtml}</div>`;
+            }
+            
+            html += `</div>`;
+        });
+        
+        html += '</div>';
+        content.innerHTML = html;
+        
+        // 等待 KaTeX 加载完成后渲染数学公式
+        if (window.renderMathInElement) {
+            try {
+                window.renderMathInElement(content, {
+                    delimiters: [
+                        {left: "$$", right: "$$", display: true},
+                        {left: "$", right: "$", display: false},
+                        {left: "\\(", right: "\\)", display: false},
+                        {left: "\\[", right: "\\]", display: true}
+                    ],
+                    throwOnError: false
+                });
+            } catch (e) {
+                console.warn('LaTeX 渲染失败:', e);
+            }
+        } else {
+            setTimeout(() => {
+                if (window.renderMathInElement) {
+                    try {
+                        window.renderMathInElement(content, {
+                            delimiters: [
+                                {left: "$$", right: "$$", display: true},
+                                {left: "$", right: "$", display: false},
+                                {left: "\\(", right: "\\)", display: false},
+                                {left: "\\[", right: "\\]", display: true}
+                            ],
+                            throwOnError: false
+                        });
+                    } catch (e) {
+                        console.warn('LaTeX 渲染失败:', e);
+                    }
+                }
+            }, 100);
+        }
+    } catch (error) {
+        content.innerHTML = '<p class="text-sm text-red-400 text-center">JSON 格式错误,请检查输入</p>';
+    }
+}
+
+// 设置选项预览输入框的预览功能
+function setupOptionsPreviewTextarea() {
+    const optionsPreviewTextarea = document.getElementById('options-preview');
+    if (!optionsPreviewTextarea) return;
+    
+    optionsPreviewTextarea.addEventListener('focus', function() {
+        showPreviewBubble(this, '选项预览');
+        updateOptionsPreviewBubble();
+    });
+    
+    optionsPreviewTextarea.addEventListener('input', function() {
+        if (currentPreviewElement === this) {
+            clearTimeout(previewUpdateTimer);
+            previewUpdateTimer = setTimeout(() => {
+                updateOptionsPreviewBubble();
+            }, 300);
+        }
+    });
+    
+    optionsPreviewTextarea.addEventListener('blur', function() {
+        // 不自动隐藏,保持预览气泡显示
+    });
+}
+
+// 根据题型显示/隐藏选项相关内容
+function toggleOptionsVisibility(questionType) {
+    const optionsSection = document.getElementById('options-section');
+    const difficultySection = document.getElementById('difficulty-section');
+    
+    if (optionsSection) {
+        if (questionType === 'choice') {
+            optionsSection.style.display = 'block';
+        } else {
+            optionsSection.style.display = 'none';
+        }
+    }
+    
+    // 难度选择框:所有题型都显示
+    if (difficultySection) {
+        difficultySection.style.display = 'block';
+    }
+}
+
+// 更新选项预览(从输入框同步到预览区域)
+function updateOptionsPreview() {
+    const optionsObj = {};
+    const optionKeys = ['A', 'B', 'C', 'D', 'E', 'F'];
+    
+    optionKeys.forEach(key => {
+        const input = document.getElementById(`option-${key}-input`);
+        if (input && input.value && input.value.trim()) {
+            optionsObj[key] = input.value.trim();
+        }
+    });
+    
+    const preview = document.getElementById('options-preview');
+    const jsonInput = document.getElementById('options-json-input');
+    
+    if (Object.keys(optionsObj).length > 0) {
+        const jsonStr = JSON.stringify(optionsObj, null, 2);
+        preview.value = jsonStr;
+        jsonInput.value = JSON.stringify(optionsObj);
+    } else {
+        preview.value = '{}';
+        jsonInput.value = '';
+    }
+}
+
+// 从预览区域同步到选项输入框(当用户直接编辑预览时)
+function syncOptionsFromPreview() {
+    const preview = document.getElementById('options-preview');
+    const jsonInput = document.getElementById('options-json-input');
+    
+    try {
+        const jsonStr = preview.value.trim();
+        if (!jsonStr || jsonStr === '{}') {
+            ['A', 'B', 'C', 'D', 'E', 'F'].forEach(key => {
+                const input = document.getElementById(`option-${key}-input`);
+                if (input) {
+                    input.value = '';
+                }
+            });
+            jsonInput.value = '';
+            if (currentPreviewElement === preview) {
+                updateOptionsPreviewBubble();
+            }
+            return;
+        }
+        
+        const optionsObj = JSON.parse(jsonStr);
+        jsonInput.value = JSON.stringify(optionsObj);
+        
+        ['A', 'B', 'C', 'D', 'E', 'F'].forEach(key => {
+            const input = document.getElementById(`option-${key}-input`);
+            if (input) {
+                input.value = optionsObj[key] || '';
+            }
+        });
+        
+        if (currentPreviewElement === preview) {
+            updateOptionsPreviewBubble();
+        }
+    } catch (error) {
+        console.warn('选项预览 JSON 格式错误:', error);
+        if (currentPreviewElement === preview) {
+            const content = document.getElementById('latex-preview-content');
+            if (content) {
+                content.innerHTML = '<p class="text-sm text-red-400 text-center">JSON 格式错误,请检查输入</p>';
+            }
+        }
+    }
+}
+
+// 选项输入框拖拽处理
+function handleOptionDragOver(e) {
+    e.preventDefault();
+    e.stopPropagation();
+    e.currentTarget.classList.add('border-blue-500', 'bg-blue-50');
+}
+
+function handleOptionDragLeave(e) {
+    e.preventDefault();
+    e.stopPropagation();
+    e.currentTarget.classList.remove('border-blue-500', 'bg-blue-50');
+}
+
+async function handleOptionDrop(e, optionKey) {
+    e.preventDefault();
+    e.stopPropagation();
+    const textarea = e.currentTarget;
+    textarea.classList.remove('border-blue-500', 'bg-blue-50');
+    
+    const files = e.dataTransfer.files;
+    if (files && files.length > 0) {
+        const file = files[0];
+        if (file.type.startsWith('image/')) {
+            await uploadOptionImageFile(file, optionKey);
+        } else {
+            if (window.customAlert) {
+                window.customAlert('请拖拽图片文件!');
+            } else {
+                alert('请拖拽图片文件!');
+            }
+        }
+    }
+}
+
+// 上传选项图片(通过文件选择)
+async function uploadOptionImage(optionKey) {
+    const fileInput = document.createElement('input');
+    fileInput.type = 'file';
+    fileInput.accept = 'image/*';
+    fileInput.style.display = 'none';
+    
+    fileInput.onchange = async (e) => {
+        const file = e.target.files[0];
+        if (!file) return;
+        
+        if (!file.type.startsWith('image/')) {
+            if (window.customAlert) {
+                window.customAlert('请选择图片文件!');
+            } else {
+                alert('请选择图片文件!');
+            }
+            return;
+        }
+        
+        await uploadOptionImageFile(file, optionKey);
+    };
+    
+    document.body.appendChild(fileInput);
+    fileInput.click();
+}
+
+// 通用的选项图片上传函数
+async function uploadOptionImageFile(file, optionKey) {
+    const optionInput = document.getElementById(`option-${optionKey}-input`);
+    const uploadBtn = document.querySelector(`[onclick="uploadOptionImage('${optionKey}')"]`);
+    
+    let originalText = '';
+    if (uploadBtn) {
+        originalText = uploadBtn.textContent;
+        uploadBtn.disabled = true;
+        uploadBtn.textContent = '上传中...';
+    }
+    
+    const originalPlaceholder = optionInput.placeholder;
+    optionInput.placeholder = '正在上传图片...';
+    optionInput.style.opacity = '0.6';
+    
+    try {
+        const formData = new FormData();
+        formData.append('file', file);
+        
+        const response = await fetch('https://crmapi.dcjxb.yunzhixue.cn/file/upload', {
+            method: 'POST',
+            body: formData
+        });
+        
+        if (!response.ok) {
+            throw new Error(`上传失败: ${response.status} ${response.statusText}`);
+        }
+        
+        const result = await response.json();
+        
+        let imageUrl = null;
+        if (typeof result === 'string') {
+            imageUrl = result;
+        } else if (result.url) {
+            imageUrl = result.url;
+        } else if (result.data && result.data.url) {
+            imageUrl = result.data.url;
+        } else if (result.data && typeof result.data === 'string') {
+            imageUrl = result.data;
+        } else {
+            const resultStr = JSON.stringify(result);
+            const urlMatch = resultStr.match(/https?:\/\/[^\s"']+/);
+            if (urlMatch) {
+                imageUrl = urlMatch[0];
+            }
+        }
+        
+        if (!imageUrl) {
+            throw new Error('接口返回格式异常,未找到图片URL。返回结果:' + JSON.stringify(result));
+        }
+        
+        const imageTag = `<image src="${imageUrl}"/>`;
+        const cursorPos = optionInput.selectionStart;
+        const textBefore = optionInput.value.substring(0, cursorPos);
+        const textAfter = optionInput.value.substring(cursorPos);
+        
+        optionInput.value = textBefore + imageTag + textAfter;
+        const newCursorPos = cursorPos + imageTag.length;
+        optionInput.setSelectionRange(newCursorPos, newCursorPos);
+        optionInput.focus();
+        
+        updateOptionsPreview();
+        
+        const optionsPreviewTextarea = document.getElementById('options-preview');
+        if (currentPreviewElement === optionsPreviewTextarea) {
+            updateOptionsPreviewBubble();
+        }
+        
+    } catch (error) {
+        console.error('上传失败:', error);
+        if (window.customAlert) {
+            window.customAlert('图片上传失败: ' + error.message);
+        } else {
+            alert('图片上传失败: ' + error.message);
+        }
+    } finally {
+        if (uploadBtn) {
+            uploadBtn.disabled = false;
+            uploadBtn.textContent = originalText;
+        }
+        optionInput.placeholder = originalPlaceholder;
+        optionInput.style.opacity = '1';
+    }
+}
+
+// 触发题干图片上传
+function triggerStemImageUpload() {
+    const fileInput = document.createElement('input');
+    fileInput.type = 'file';
+    fileInput.accept = 'image/*';
+    fileInput.style.display = 'none';
+    fileInput.onchange = async (e) => {
+        const file = e.target.files[0];
+        if (file) {
+            await uploadStemImageFile(file);
+        }
+    };
+    document.body.appendChild(fileInput);
+    fileInput.click();
+    document.body.removeChild(fileInput);
+}
+
+// 题干拖拽处理函数
+function handleStemDragOver(e) {
+    e.preventDefault();
+    e.stopPropagation();
+    e.currentTarget.classList.add('border-blue-500', 'bg-blue-50');
+}
+
+function handleStemDragLeave(e) {
+    e.preventDefault();
+    e.stopPropagation();
+    e.currentTarget.classList.remove('border-blue-500', 'bg-blue-50');
+}
+
+async function handleStemDrop(e) {
+    e.preventDefault();
+    e.stopPropagation();
+    const textarea = e.currentTarget;
+    textarea.classList.remove('border-blue-500', 'bg-blue-50');
+    
+    const files = e.dataTransfer.files;
+    if (files && files.length > 0) {
+        const file = files[0];
+        if (file.type.startsWith('image/')) {
+            await uploadStemImageFile(file);
+        } else {
+            if (window.customAlert) {
+                window.customAlert('请拖拽图片文件!');
+            } else {
+                alert('请拖拽图片文件!');
+            }
+        }
+    }
+}
+
+// 解析拖拽处理函数
+function handleSolutionDragOver(e) {
+    e.preventDefault();
+    e.stopPropagation();
+    e.currentTarget.classList.add('border-blue-500', 'bg-blue-50');
+}
+
+function handleSolutionDragLeave(e) {
+    e.preventDefault();
+    e.stopPropagation();
+    e.currentTarget.classList.remove('border-blue-500', 'bg-blue-50');
+}
+
+async function handleSolutionDrop(e) {
+    e.preventDefault();
+    e.stopPropagation();
+    const textarea = e.currentTarget;
+    textarea.classList.remove('border-blue-500', 'bg-blue-50');
+    
+    const files = e.dataTransfer.files;
+    if (files && files.length > 0) {
+        const file = files[0];
+        if (file.type.startsWith('image/')) {
+            await uploadSolutionImageFile(file);
+        } else {
+            if (window.customAlert) {
+                window.customAlert('请拖拽图片文件!');
+            } else {
+                alert('请拖拽图片文件!');
+            }
+        }
+    }
+}
+
+// 题干图片上传函数
+async function uploadStemImageFile(file) {
+    const stemTextarea = document.getElementById('stem-textarea');
+    const statusDiv = document.getElementById('upload-status');
+    
+    const originalPlaceholder = stemTextarea.placeholder;
+    stemTextarea.placeholder = '正在上传图片...';
+    stemTextarea.style.opacity = '0.6';
+    
+    if (statusDiv) {
+        statusDiv.classList.remove('hidden');
+        statusDiv.textContent = '正在上传图片...';
+        statusDiv.className = 'mt-2 text-sm text-blue-600';
+    }
+    
+    try {
+        const formData = new FormData();
+        formData.append('file', file);
+        
+        const response = await fetch('https://crmapi.dcjxb.yunzhixue.cn/file/upload', {
+            method: 'POST',
+            body: formData
+        });
+        
+        if (!response.ok) {
+            throw new Error(`上传失败: ${response.status} ${response.statusText}`);
+        }
+        
+        const result = await response.json();
+        
+        let imageUrl = null;
+        if (typeof result === 'string') {
+            imageUrl = result;
+        } else if (result.url) {
+            imageUrl = result.url;
+        } else if (result.data && result.data.url) {
+            imageUrl = result.data.url;
+        } else if (result.data && typeof result.data === 'string') {
+            imageUrl = result.data;
+        } else {
+            const resultStr = JSON.stringify(result);
+            const urlMatch = resultStr.match(/https?:\/\/[^\s"']+/);
+            if (urlMatch) {
+                imageUrl = urlMatch[0];
+            }
+        }
+        
+        if (!imageUrl) {
+            throw new Error('接口返回格式异常,未找到图片URL。返回结果:' + JSON.stringify(result));
+        }
+        
+        const imageTag = `<image src="${imageUrl}"/>`;
+        const cursorPos = stemTextarea.selectionStart;
+        const textBefore = stemTextarea.value.substring(0, cursorPos);
+        const textAfter = stemTextarea.value.substring(cursorPos);
+        
+        stemTextarea.value = textBefore + imageTag + textAfter;
+        const newCursorPos = cursorPos + imageTag.length;
+        stemTextarea.setSelectionRange(newCursorPos, newCursorPos);
+        stemTextarea.focus();
+        
+        if (currentPreviewElement === stemTextarea) {
+            updatePreviewContent(stemTextarea.value);
+        }
+        
+        if (statusDiv) {
+            statusDiv.textContent = '图片上传成功!已插入到题干中。';
+            statusDiv.className = 'mt-2 text-sm text-green-600';
+            setTimeout(() => {
+                statusDiv.classList.add('hidden');
+            }, 1500);
+        }
+        
+    } catch (error) {
+        console.error('上传失败:', error);
+        if (statusDiv) {
+            statusDiv.textContent = '上传失败: ' + error.message;
+            statusDiv.className = 'mt-2 text-sm text-red-600';
+        }
+        if (window.customAlert) {
+            window.customAlert('图片上传失败: ' + error.message);
+        } else {
+            alert('图片上传失败: ' + error.message);
+        }
+    } finally {
+        stemTextarea.placeholder = originalPlaceholder;
+        stemTextarea.style.opacity = '1';
+    }
+}
+
+// 解析图片上传函数
+async function uploadSolutionImageFile(file) {
+    const solutionTextarea = document.getElementById('solution-textarea');
+    await uploadImageToInput(file, solutionTextarea);
+}
+
+// 通用图片上传函数:上传图片并插入到指定输入框
+async function uploadImageToInput(file, inputElement) {
+    if (!inputElement || !file) {
+        return;
+    }
+    
+    if (!file.type.startsWith('image/')) {
+        if (window.customAlert) {
+            window.customAlert('请选择图片文件!');
+        } else {
+            alert('请选择图片文件!');
+        }
+        return;
+    }
+    
+    const originalPlaceholder = inputElement.placeholder || '';
+    const originalOpacity = inputElement.style.opacity || '1';
+    inputElement.placeholder = '正在上传图片...';
+    inputElement.style.opacity = '0.6';
+    inputElement.disabled = true;
+    
+    try {
+        const formData = new FormData();
+        formData.append('file', file);
+        
+        const response = await fetch('https://crmapi.dcjxb.yunzhixue.cn/file/upload', {
+            method: 'POST',
+            body: formData
+        });
+        
+        if (!response.ok) {
+            throw new Error(`上传失败: ${response.status} ${response.statusText}`);
+        }
+        
+        const result = await response.json();
+        
+        let imageUrl = null;
+        if (typeof result === 'string') {
+            imageUrl = result;
+        } else if (result.url) {
+            imageUrl = result.url;
+        } else if (result.data && result.data.url) {
+            imageUrl = result.data.url;
+        } else if (result.data && typeof result.data === 'string') {
+            imageUrl = result.data;
+        } else {
+            const resultStr = JSON.stringify(result);
+            const urlMatch = resultStr.match(/https?:\/\/[^\s"']+/);
+            if (urlMatch) {
+                imageUrl = urlMatch[0];
+            }
+        }
+        
+        if (!imageUrl) {
+            throw new Error('接口返回格式异常,未找到图片URL。返回结果:' + JSON.stringify(result));
+        }
+        
+        const imageTag = `<image src="${imageUrl}"/>`;
+        const cursorPos = inputElement.selectionStart || inputElement.value.length;
+        const textBefore = inputElement.value.substring(0, cursorPos);
+        const textAfter = inputElement.value.substring(cursorPos);
+        
+        inputElement.value = textBefore + imageTag + textAfter;
+        const newCursorPos = cursorPos + imageTag.length;
+        if (inputElement.setSelectionRange) {
+            inputElement.setSelectionRange(newCursorPos, newCursorPos);
+        }
+        inputElement.focus();
+        
+        inputElement.dispatchEvent(new Event('input', { bubbles: true }));
+        
+        if (currentPreviewElement === inputElement) {
+            updatePreviewContent(inputElement.value);
+        }
+        
+        if (inputElement.id && inputElement.id.startsWith('option-')) {
+            updateOptionsPreview();
+        }
+        
+    } catch (error) {
+        console.error('上传失败:', error);
+        if (window.customAlert) {
+            window.customAlert('图片上传失败: ' + error.message);
+        } else {
+            alert('图片上传失败: ' + error.message);
+        }
+    } finally {
+        inputElement.placeholder = originalPlaceholder;
+        inputElement.style.opacity = originalOpacity;
+        inputElement.disabled = false;
+    }
+}
+
+// 为所有输入框设置粘贴图片功能
+function setupPasteImageForAllInputs() {
+    const allInputs = document.querySelectorAll('textarea, input[type="text"]');
+    
+    allInputs.forEach(input => {
+        input.addEventListener('paste', async function(e) {
+            const clipboardData = e.clipboardData || window.clipboardData;
+            if (!clipboardData) {
+                return;
+            }
+            
+            const items = clipboardData.items;
+            if (!items) {
+                return;
+            }
+            
+            for (let i = 0; i < items.length; i++) {
+                const item = items[i];
+                
+                if (item.type.indexOf('image') !== -1) {
+                    e.preventDefault();
+                    
+                    const file = item.getAsFile();
+                    if (file) {
+                        await uploadImageToInput(file, input);
+                    }
+                    break;
+                }
+            }
+        });
+    });
+}
+
+// 页面加载时初始化
+document.addEventListener('DOMContentLoaded', function() {
+    // 初始化难度选择框的回显
+    const difficultySelect = document.getElementById('difficulty-select');
+    if (difficultySelect) {
+        const originalValue = difficultySelect.getAttribute('data-original');
+        if (originalValue) {
+            // 处理难度值的各种格式:可能是字符串 "0.2" 或数字 0.2
+            let difficultyValue = originalValue;
+            // 尝试转换为数字进行比较
+            const numValue = parseFloat(originalValue);
+            if (!isNaN(numValue)) {
+                // 根据数值范围匹配对应的选项值
+                if (numValue >= 0.19 && numValue <= 0.21) {
+                    difficultySelect.value = '0.2';
+                } else if (numValue >= 0.39 && numValue <= 0.41) {
+                    difficultySelect.value = '0.4';
+                } else if (numValue >= 0.69 && numValue <= 0.71) {
+                    difficultySelect.value = '0.7';
+                } else if (originalValue === '0.2' || originalValue === 0.2) {
+                    difficultySelect.value = '0.2';
+                } else if (originalValue === '0.4' || originalValue === 0.4) {
+                    difficultySelect.value = '0.4';
+                } else if (originalValue === '0.7' || originalValue === 0.7) {
+                    difficultySelect.value = '0.7';
+                }
+            } else if (originalValue === '0.2' || originalValue === '0.4' || originalValue === '0.7') {
+                difficultySelect.value = originalValue;
+            }
+        }
+    }
+    
+    // 解析现有选项JSON并填充到输入框
+    const optionsPreview = document.getElementById('options-preview');
+    if (optionsPreview && optionsPreview.value && optionsPreview.value.trim() !== '{}') {
+        try {
+            const optionsObj = JSON.parse(optionsPreview.value);
+            ['A', 'B', 'C', 'D', 'E', 'F'].forEach(key => {
+                const input = document.getElementById(`option-${key}-input`);
+                if (input && optionsObj[key]) {
+                    input.value = optionsObj[key];
+                }
+            });
+        } catch (e) {
+            console.warn('解析选项JSON失败:', e);
+        }
+    }
+    
+    updateOptionsPreview();
+    
+    // 题型选择变化
+    const questionTypeSelect = document.getElementById('question-type-select');
+    if (questionTypeSelect) {
+        questionTypeSelect.addEventListener('change', function() {
+            toggleOptionsVisibility(this.value);
+        });
+        
+        // 初始化时根据题型显示/隐藏选项
+        toggleOptionsVisibility(questionTypeSelect.value);
+    }
+    
+    // 设置 LaTeX 实时预览
+    setupPreviewForElement('stem-textarea', '题干预览');
+    setupPreviewForElement('solution-textarea', '解析预览');
+    setupPreviewForElement('option-A-input', '选项 A 预览');
+    setupPreviewForElement('option-B-input', '选项 B 预览');
+    setupPreviewForElement('option-C-input', '选项 C 预览');
+    setupPreviewForElement('option-D-input', '选项 D 预览');
+    setupPreviewForElement('answer-input', '正确答案预览');
+    
+    // 设置选项预览输入框的预览功能
+    setupOptionsPreviewTextarea();
+    
+    // 为所有输入框添加粘贴图片功能
+    setupPasteImageForAllInputs();
+});
+
+// 难度评价函数
+async function evaluateDifficulty() {
+    const btn = document.getElementById('evaluate-difficulty-btn');
+    const difficultySelect = document.getElementById('difficulty-select');
+    
+    if (!btn || !difficultySelect) return;
+    
+    // 收集题目信息
+    const stemTextarea = document.getElementById('stem-textarea');
+    const answerInput = document.getElementById('answer-input');
+    const solutionTextarea = document.getElementById('solution-textarea');
+    const optionsPreview = document.getElementById('options-preview');
+    const questionTypeSelect = document.getElementById('question-type-select');
+    
+    const stem = stemTextarea ? stemTextarea.value.trim() : '';
+    
+    if (!stem) {
+        if (window.customAlert) {
+            window.customAlert('请先填写题干内容');
+        } else {
+            alert('请先填写题干内容');
+        }
+        return;
+    }
+    
+    // 构建请求数据
+    const requestData = {
+        stem: stem,
+        answer: answerInput ? answerInput.value.trim() : '',
+        solution: solutionTextarea ? solutionTextarea.value.trim() : '',
+        question_type: questionTypeSelect ? questionTypeSelect.value : ''
+    };
+    
+    // 处理选项
+    if (optionsPreview && optionsPreview.value.trim() && optionsPreview.value.trim() !== '{}') {
+        try {
+            requestData.options = JSON.parse(optionsPreview.value.trim());
+        } catch (e) {
+            console.warn('选项JSON解析失败:', e);
+        }
+    }
+    
+    // 显示加载状态
+    const originalText = btn.innerHTML;
+    btn.disabled = true;
+    btn.innerHTML = '<svg class="w-3 h-3 inline-block mr-1 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>评价中...';
+    
+    try {
+        const response = await fetch('/api/score', {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json'
+            },
+            body: JSON.stringify(requestData)
+        });
+        
+        if (!response.ok) {
+            throw new Error(`请求失败: ${response.status} ${response.statusText}`);
+        }
+        
+        const result = await response.json();
+        
+        // 调试:打印完整返回结果
+        console.log('难度评价接口返回:', result);
+        
+        // 处理返回的 difficulty_level(优先使用 data.difficulty_level,兼容旧格式)
+        let difficultyLevel = result.data?.difficulty_level || result.difficulty_level || result.difficulty || result.level;
+        
+        // 映射难度等级到枚举值
+        let difficultyValue = '';
+        
+        if (difficultyLevel !== undefined && difficultyLevel !== null) {
+            const levelStr = String(difficultyLevel).trim();
+            
+            // 字符串匹配
+            if (levelStr === '筑基' || levelStr === '0.2' || levelStr === '0.20') {
+                difficultyValue = '0.2';
+            } else if (levelStr === '提分' || levelStr === '0.4' || levelStr === '0.40') {
+                difficultyValue = '0.4';
+            } else if (levelStr === '培优' || levelStr === '0.7' || levelStr === '0.70') {
+                difficultyValue = '0.7';
+            } else {
+                // 尝试转换为数字
+                const levelNum = parseFloat(levelStr);
+                if (!isNaN(levelNum)) {
+                    if (Math.abs(levelNum - 0.2) < 0.1) {
+                        difficultyValue = '0.2';
+                    } else if (Math.abs(levelNum - 0.4) < 0.1) {
+                        difficultyValue = '0.4';
+                    } else if (Math.abs(levelNum - 0.7) < 0.1) {
+                        difficultyValue = '0.7';
+                    }
+                }
+            }
+        }
+        
+        if (difficultyValue) {
+            difficultySelect.value = difficultyValue;
+            // 触发change事件以更新预览
+            difficultySelect.dispatchEvent(new Event('change', { bubbles: true }));
+            // 不显示弹窗,直接完成
+        } else {
+            // 如果无法识别,打印完整返回结果以便调试
+            console.error('无法识别难度等级,完整返回结果:', result);
+            if (window.customAlert) {
+                window.customAlert('无法识别返回的难度等级。返回数据:' + JSON.stringify(result));
+            } else {
+                alert('无法识别返回的难度等级。返回数据:' + JSON.stringify(result));
+            }
+        }
+        
+    } catch (error) {
+        console.error('难度评价失败:', error);
+        if (window.customAlert) {
+            window.customAlert('难度评价失败: ' + error.message);
+        } else {
+            alert('难度评价失败: ' + error.message);
+        }
+    } finally {
+        // 恢复按钮
+        btn.disabled = false;
+        btn.innerHTML = originalText;
+    }
+}
+
+// 表单提交
+document.getElementById('edit-form').addEventListener('submit', async (e) => {
+    e.preventDefault();
+    const formData = new FormData(e.target);
+    
+    
+    // 只发送实际修改过的字段(对比原始值)
+    const data = { question_code: formData.get('question_code') };
+    
+    // 检查字段变化
+    const fields = ['stem', 'answer', 'solution', 'question_type', 'difficulty'];
+    fields.forEach(field => {
+        const input = e.target.querySelector(`[name="${field}"]`);
+        if (input) {
+            const newValue = input.value || '';
+            let oldValue = input.getAttribute('data-original') || '';
+            
+            // 处理 JSON 字符串
+            try {
+                if (oldValue && (oldValue.startsWith('{') || oldValue.startsWith('['))) {
+                    oldValue = JSON.stringify(JSON.parse(oldValue));
+                }
+            } catch(e) {
+                // 解析失败,保持原值
+            }
+            
+            // 处理difficulty字段:转换为浮点数,保留两位小数
+            if (field === 'difficulty' && newValue) {
+                const difficultyValue = parseFloat(newValue);
+                if (!isNaN(difficultyValue)) {
+                    const newDifficulty = round(difficultyValue, 2);
+                    const oldDifficulty = oldValue ? parseFloat(oldValue) : null;
+                    if (newDifficulty !== oldDifficulty) {
+                        data[field] = newDifficulty;
+                    }
+                }
+            } else if (newValue !== oldValue) {
+                data[field] = newValue.trim();
+            }
+        }
+    });
+    
+    // 处理选项
+    const previewTextarea = document.getElementById('options-preview');
+    const optionsOriginal = previewTextarea ? previewTextarea.getAttribute('data-original') || '' : '';
+    
+    let optionsJson = '';
+    if (previewTextarea && previewTextarea.value.trim() && previewTextarea.value.trim() !== '{}') {
+        try {
+            const parsed = JSON.parse(previewTextarea.value.trim());
+            optionsJson = JSON.stringify(parsed);
+        } catch (e) {
+            const optionsObj = {};
+            ['A', 'B', 'C', 'D', 'E', 'F'].forEach(key => {
+                const input = document.getElementById(`option-${key}-input`);
+                if (input && input.value && input.value.trim()) {
+                    optionsObj[key] = input.value.trim();
+                }
+            });
+            if (Object.keys(optionsObj).length > 0) {
+                optionsJson = JSON.stringify(optionsObj);
+            }
+        }
+    } else {
+        const optionsObj = {};
+        ['A', 'B', 'C', 'D', 'E', 'F'].forEach(key => {
+            const input = document.getElementById(`option-${key}-input`);
+            if (input && input.value && input.value.trim()) {
+                optionsObj[key] = input.value.trim();
+            }
+        });
+        if (Object.keys(optionsObj).length > 0) {
+            optionsJson = JSON.stringify(optionsObj);
+        }
+    }
+    
+    // 对比选项是否变化
+    let optionsOriginalNormalized = optionsOriginal;
+    try {
+        if (optionsOriginal && (optionsOriginal.startsWith('{') || optionsOriginal.startsWith('['))) {
+            optionsOriginalNormalized = JSON.stringify(JSON.parse(optionsOriginal));
+        }
+    } catch(e) {
+        // 解析失败,保持原值
+    }
+    
+    if (optionsJson && optionsJson !== optionsOriginalNormalized) {
+        data.options = optionsJson;
+    } else if (!optionsJson && optionsOriginalNormalized) {
+        // 如果新值为空但原值不为空,也要更新(删除选项)
+        data.options = '';
+    }
+    
+    // 验证必填项
+    const requiredFields = {
+        'stem': '题干',
+        'answer': '正确答案',
+        'solution': '解析'
+    };
+    
+    const missingFields = [];
+    for (const [field, label] of Object.entries(requiredFields)) {
+        const input = e.target.querySelector(`[name="${field}"]`);
+        if (!input || !input.value || !input.value.trim()) {
+            missingFields.push(label);
+        }
+    }
+    
+    if (missingFields.length > 0) {
+        const message = '请填写以下必填项:' + missingFields.join('、');
+        if (window.customAlert) {
+            window.customAlert(message);
+        } else {
+            alert(message);
+        }
+        return;
+    }
+    
+    // 如果没有变化,提示用户
+    if (Object.keys(data).length === 1) {
+        if (window.customAlert) {
+            window.customAlert('没有检测到任何修改');
+        } else {
+            alert('没有检测到任何修改');
+        }
+        return;
+    }
+    
+    const res = await fetch('/update_question', {
+        method: 'POST',
+        headers: {'Content-Type': 'application/json'},
+        body: JSON.stringify(data)
+    });
+    
+    const result = await res.json();
+    if(result.success) {
+        if (window.customAlert) {
+            window.customAlert('修改成功!', () => {
+                window.location.href = '/detail/' + data.question_code;
+            });
+        } else {
+            alert('修改成功!');
+            window.location.href = '/detail/' + data.question_code;
+        }
+    } else {
+        if (window.customAlert) {
+            window.customAlert('修改失败: ' + result.error);
+        } else {
+            alert('修改失败: ' + result.error);
+        }
+    }
+});
+</script>
+{% endblock %}

+ 258 - 0
templates/index.html

@@ -0,0 +1,258 @@
+{% extends "layout.html" %}
+
+{% block page_title %}首页{% endblock %}
+
+{% block content %}
+<div class="space-y-6">
+    <!-- 总体统计卡片 -->
+    <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
+        <!-- 总题目数 -->
+        <div class="apple-card p-6 hover:shadow-lg transition-all duration-300 group">
+            <div class="flex items-center justify-between mb-4">
+                <div class="w-12 h-12 rounded-xl bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center text-white text-xl shadow-lg group-hover:scale-110 transition-transform">
+                    <i class="ri-file-list-3-line"></i>
+                </div>
+                <div class="text-right">
+                    <div class="text-3xl font-bold text-gray-800">{{ total }}</div>
+                    <div class="text-sm text-gray-500 mt-1">总题目数</div>
+                </div>
+            </div>
+            <div class="h-1 bg-gray-200 rounded-full overflow-hidden">
+                <div class="h-full bg-gradient-to-r from-blue-500 to-blue-600 rounded-full" style="width: 100%"></div>
+            </div>
+        </div>
+
+        <!-- 已审核题目 -->
+        <div class="apple-card p-6 hover:shadow-lg transition-all duration-300 group">
+            <div class="flex items-center justify-between mb-4">
+                <div class="w-12 h-12 rounded-xl bg-gradient-to-br from-green-500 to-green-600 flex items-center justify-center text-white text-xl shadow-lg group-hover:scale-110 transition-transform">
+                    <i class="ri-checkbox-circle-line"></i>
+                </div>
+                <div class="text-right">
+                    <div class="text-3xl font-bold text-gray-800">{{ audited_count }}</div>
+                    <div class="text-sm text-gray-500 mt-1">已审核</div>
+                </div>
+            </div>
+            <div class="h-1 bg-gray-200 rounded-full overflow-hidden">
+                <div class="h-full bg-gradient-to-r from-green-500 to-green-600 rounded-full" style="width: {{ audit_rate }}%"></div>
+            </div>
+            <div class="text-xs text-gray-400 mt-2">审核率: {{ audit_rate }}%</div>
+        </div>
+
+        <!-- 合格题目 -->
+        <div class="apple-card p-6 hover:shadow-lg transition-all duration-300 group">
+            <div class="flex items-center justify-between mb-4">
+                <div class="w-12 h-12 rounded-xl bg-gradient-to-br from-emerald-500 to-emerald-600 flex items-center justify-center text-white text-xl shadow-lg group-hover:scale-110 transition-transform">
+                    <i class="ri-check-double-line"></i>
+                </div>
+                <div class="text-right">
+                    <div class="text-3xl font-bold text-gray-800">{{ pass_count }}</div>
+                    <div class="text-sm text-gray-500 mt-1">合格题目</div>
+    </div>
+</div>
+            <div class="h-1 bg-gray-200 rounded-full overflow-hidden">
+                <div class="h-full bg-gradient-to-r from-emerald-500 to-emerald-600 rounded-full" style="width: {{ pass_rate }}%"></div>
+            </div>
+            <div class="text-xs text-gray-400 mt-2">通过率: {{ pass_rate }}%</div>
+        </div>
+
+        <!-- 待审核题目 -->
+        <div class="apple-card p-6 hover:shadow-lg transition-all duration-300 group">
+            <div class="flex items-center justify-between mb-4">
+                <div class="w-12 h-12 rounded-xl bg-gradient-to-br from-orange-500 to-orange-600 flex items-center justify-center text-white text-xl shadow-lg group-hover:scale-110 transition-transform">
+                    <i class="ri-time-line"></i>
+                </div>
+                <div class="text-right">
+                    <div class="text-3xl font-bold text-gray-800">{{ pending_count }}</div>
+                    <div class="text-sm text-gray-500 mt-1">待审核</div>
+                </div>
+                            </div>
+            {% if pending_count > 0 %}
+            <a href="/audit_questions" class="block mt-4 text-sm text-orange-600 hover:text-orange-700 font-medium">
+                立即审核 →
+            </a>
+            {% else %}
+            <div class="text-xs text-gray-400 mt-2">全部已审核</div>
+            {% endif %}
+        </div>
+    </div>
+
+    <!-- 教材和知识点统计 -->
+    <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
+        <!-- 教材统计 -->
+        <div class="apple-card p-6">
+            <h2 class="text-xl font-bold text-gray-800 mb-6 flex items-center gap-2">
+                <i class="ri-book-open-line text-indigo-600"></i>
+                <span>教材统计</span>
+            </h2>
+            <div class="space-y-4">
+                <!-- 教材系列 -->
+                <div class="flex items-center justify-between p-4 rounded-lg bg-gradient-to-r from-indigo-50 to-purple-50">
+                    <div class="flex items-center gap-3">
+                        <div class="w-10 h-10 rounded-lg bg-gradient-to-r from-indigo-500 to-indigo-600 flex items-center justify-center text-white">
+                            <i class="ri-stack-line"></i>
+                        </div>
+                        <div>
+                            <div class="text-sm text-gray-600">教材系列</div>
+                            <div class="text-lg font-bold text-gray-800">{{ total_series }} 个系列</div>
+                        </div>
+                    </div>
+                    <div class="text-right">
+                        <div class="text-xs text-gray-500">激活</div>
+                        <div class="text-lg font-bold text-indigo-600">{{ active_series }}</div>
+                    </div>
+                </div>
+                <!-- 教材总数 -->
+                <div class="flex items-center justify-between p-4 rounded-lg bg-gradient-to-r from-blue-50 to-cyan-50">
+                    <div class="flex items-center gap-3">
+                        <div class="w-10 h-10 rounded-lg bg-gradient-to-r from-blue-500 to-blue-600 flex items-center justify-center text-white">
+                            <i class="ri-book-2-line"></i>
+                        </div>
+                        <div>
+                            <div class="text-sm text-gray-600">教材总数</div>
+                            <div class="text-lg font-bold text-gray-800">{{ total_textbooks }} 本</div>
+                        </div>
+                    </div>
+                    <a href="/textbook_management" class="text-blue-600 hover:text-blue-700">
+                        <i class="ri-arrow-right-s-line text-xl"></i>
+                    </a>
+                </div>
+            </div>
+        </div>
+        
+        <!-- 知识点统计 -->
+        <div class="apple-card p-6">
+            <h2 class="text-xl font-bold text-gray-800 mb-6 flex items-center gap-2">
+                <i class="ri-node-tree text-green-600"></i>
+                <span>知识点统计</span>
+            </h2>
+            <div class="space-y-4">
+                <!-- 知识点总数 -->
+                <div class="flex items-center justify-between p-4 rounded-lg bg-gradient-to-r from-green-50 to-emerald-50">
+                    <div class="flex items-center gap-3">
+                        <div class="w-10 h-10 rounded-lg bg-gradient-to-r from-green-500 to-green-600 flex items-center justify-center text-white">
+                            <i class="ri-file-list-3-line"></i>
+                        </div>
+                        <div>
+                            <div class="text-sm text-gray-600">知识点总数</div>
+                            <div class="text-lg font-bold text-gray-800">{{ total_kp }} 个</div>
+                        </div>
+                    </div>
+                    <a href="/kp_management" class="text-green-600 hover:text-green-700">
+                        <i class="ri-arrow-right-s-line text-xl"></i>
+                    </a>
+                </div>
+                <!-- 知识点层级分布 -->
+                <div class="space-y-2">
+                    <div class="flex items-center justify-between text-sm">
+                        <span class="text-gray-600">一级知识点</span>
+                        <span class="font-bold text-gray-800">{{ kp_level_0 }} 个</span>
+                    </div>
+                    <div class="flex items-center justify-between text-sm">
+                        <span class="text-gray-600">二级知识点</span>
+                        <span class="font-bold text-gray-800">{{ kp_level_1 }} 个</span>
+                    </div>
+                    <div class="flex items-center justify-between text-sm">
+                        <span class="text-gray-600">三级及以上</span>
+                        <span class="font-bold text-gray-800">{{ kp_level_2_plus }} 个</span>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+    
+    <!-- 审核状态分布 -->
+    <div class="apple-card p-6">
+        <h2 class="text-xl font-bold text-gray-800 mb-6 flex items-center gap-2">
+            <i class="ri-pie-chart-line text-blue-600"></i>
+            <span>审核状态分布</span>
+        </h2>
+        <div class="space-y-4">
+            <!-- 合格 -->
+            <div class="flex items-center justify-between">
+                <div class="flex items-center gap-3">
+                    <div class="w-4 h-4 rounded-full bg-gradient-to-r from-emerald-500 to-emerald-600"></div>
+                    <span class="text-gray-700 font-medium">合格题目</span>
+                </div>
+                <div class="flex items-center gap-4">
+                    <div class="w-32 h-2 bg-gray-200 rounded-full overflow-hidden">
+                        <div class="h-full bg-gradient-to-r from-emerald-500 to-emerald-600 rounded-full" style="width: {% if total > 0 %}{{ (pass_count / total * 100)|round(1) }}{% else %}0{% endif %}%"></div>
+                    </div>
+                    <span class="text-gray-800 font-bold w-16 text-right">{{ pass_count }}</span>
+                </div>
+            </div>
+            <!-- 不合格 -->
+            <div class="flex items-center justify-between">
+                <div class="flex items-center gap-3">
+                    <div class="w-4 h-4 rounded-full bg-gradient-to-r from-red-500 to-red-600"></div>
+                    <span class="text-gray-700 font-medium">不合格题目</span>
+                </div>
+                <div class="flex items-center gap-4">
+                    <div class="w-32 h-2 bg-gray-200 rounded-full overflow-hidden">
+                        <div class="h-full bg-gradient-to-r from-red-500 to-red-600 rounded-full" style="width: {% if total > 0 %}{{ (fail_count / total * 100)|round(1) }}{% else %}0{% endif %}%"></div>
+                    </div>
+                    <span class="text-gray-800 font-bold w-16 text-right">{{ fail_count }}</span>
+                </div>
+            </div>
+            <!-- 待审核 -->
+            <div class="flex items-center justify-between">
+                <div class="flex items-center gap-3">
+                    <div class="w-4 h-4 rounded-full bg-gradient-to-r from-orange-500 to-orange-600"></div>
+                    <span class="text-gray-700 font-medium">待审核题目</span>
+                </div>
+                <div class="flex items-center gap-4">
+                    <div class="w-32 h-2 bg-gray-200 rounded-full overflow-hidden">
+                        <div class="h-full bg-gradient-to-r from-orange-500 to-orange-600 rounded-full" style="width: {% if total > 0 %}{{ (pending_count / total * 100)|round(1) }}{% else %}0{% endif %}%"></div>
+                    </div>
+                    <span class="text-gray-800 font-bold w-16 text-right">{{ pending_count }}</span>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <!-- 最近添加的题目 -->
+    {% if recent_questions %}
+    <div class="apple-card p-6">
+        <h2 class="text-xl font-bold text-gray-800 mb-6 flex items-center gap-2">
+            <i class="ri-history-line text-indigo-600"></i>
+            <span>最近添加的题目</span>
+        </h2>
+        <div class="space-y-3">
+            {% for question in recent_questions %}
+            <a href="/detail/{{ question.question_code }}" class="block p-4 rounded-lg hover:bg-gray-50 transition-colors border-l-4 border-transparent hover:border-blue-500">
+                <div class="flex items-start justify-between gap-4">
+                    <div class="flex-1 min-w-0">
+                        <div class="flex items-center gap-2 mb-2">
+                            <span class="text-xs font-mono text-gray-500 bg-gray-100 px-2 py-1 rounded">ID: {{ question.question_code }}</span>
+                            {% if question.audit_reason %}
+                                {% if question.audit_reason == '合格' %}
+                                <span class="text-xs px-2 py-1 bg-emerald-100 text-emerald-700 rounded font-semibold">合格</span>
+                                {% else %}
+                                <span class="text-xs px-2 py-1 bg-red-100 text-red-700 rounded font-semibold">不合格</span>
+                                {% endif %}
+                            {% else %}
+                                <span class="text-xs px-2 py-1 bg-orange-100 text-orange-700 rounded font-semibold">待审核</span>
+                    {% endif %}
+                        </div>
+                        <div class="text-sm text-gray-700 line-clamp-2">
+                            {{ question.stem[:100]|safe }}{% if question.stem|length > 100 %}...{% endif %}
+                        </div>
+                    </div>
+                    <i class="ri-arrow-right-s-line text-gray-400 text-xl"></i>
+                </div>
+            </a>
+                {% endfor %}
+        </div>
+    </div>
+    {% endif %}
+</div>
+
+<style>
+    .line-clamp-2 {
+        display: -webkit-box;
+        -webkit-line-clamp: 2;
+        -webkit-box-orient: vertical;
+        overflow: hidden;
+    }
+</style>
+{% endblock %}

+ 806 - 0
templates/kp_management.html

@@ -0,0 +1,806 @@
+{% extends "layout.html" %}
+
+{% block page_title %}知识点管理{% endblock %}
+
+{% block content %}
+<!-- 树状节点宏定义 -->
+{% macro render_tree_node(kp, level) %}
+<div 
+    class="kp-node relative"
+    data-kp-id="{{ kp.id }}"
+    data-kp-code="{{ kp.kp_code }}"
+    data-parent="{{ kp.parent_kp_code or '' }}"
+    data-level="{{ level }}"
+    data-grade="{{ kp.grade or '' }}">
+    
+    <!-- 节点卡片 -->
+    <div class="flex items-start gap-4 group">
+        <!-- 展开/折叠按钮区域 -->
+        <div class="flex items-center gap-2 pt-4 flex-shrink-0">
+            {% if kp.children|length > 0 %}
+            <button 
+                onclick="toggleTreeNode('{{ kp.kp_code }}'); event.stopPropagation();"
+                class="w-8 h-8 rounded-full bg-white border-2 border-gray-300 hover:border-blue-500 flex items-center justify-center text-gray-600 hover:text-blue-600 transition-all expand-btn z-10 shadow-sm hover:shadow-md"
+                data-expanded="{% if level == 0 %}true{% else %}false{% endif %}">
+                {% if level == 0 %}
+                <i class="ri-subtract-line text-sm"></i>
+                {% else %}
+                <i class="ri-add-line text-sm"></i>
+                {% endif %}
+            </button>
+            {% else %}
+            <div class="w-8 h-8 flex items-center justify-center">
+                <div class="w-2 h-2 rounded-full bg-gray-400"></div>
+            </div>
+            {% endif %}
+        </div>
+        
+        <!-- 节点内容卡片 -->
+        <div class="flex-1 apple-card p-5 hover:shadow-xl transition-all duration-300 group-hover:border-blue-300 border-2 border-transparent rounded-xl {% if kp.children|length > 0 %}cursor-pointer{% endif %}" {% if kp.children|length > 0 %}onclick="handleCardClick(event, '{{ kp.kp_code }}')"{% endif %}>
+            <div class="flex items-start justify-between">
+                <div class="flex-1 min-w-0">
+                    <div class="flex items-center gap-3 mb-3 flex-wrap">
+                        <span class="px-3 py-1.5 rounded-lg text-xs font-bold font-mono bg-gradient-to-r {% if level == 0 %}from-blue-500 to-indigo-600{% elif level == 1 %}from-green-500 to-emerald-600{% else %}from-orange-500 to-amber-600{% endif %} text-white shadow-md">
+                            {{ kp.kp_code }}
+                        </span>
+                        <h3 class="text-lg font-bold text-gray-800 group-hover:text-blue-600 transition-colors">
+                            {{ kp.name }}
+                        </h3>
+                    </div>
+                    
+                    <div class="flex items-center gap-4 text-sm text-gray-500 mt-2 flex-wrap">
+                        {% if kp.grade %}
+                        <span class="flex items-center gap-1.5 px-2 py-1 bg-gray-50 rounded-md">
+                            <i class="ri-graduation-cap-line text-green-500"></i>
+                            <span>{{ kp.grade }}</span>
+                        </span>
+                        {% endif %}
+                        {% if kp.question_count is defined and kp.question_count > 0 %}
+                        <span class="flex items-center gap-1.5 px-2 py-1 bg-gradient-to-r from-red-50 to-orange-50 border border-red-200 rounded-md text-red-600 shadow-sm">
+                            <i class="ri-file-list-line text-red-500"></i>
+                            <span class="font-bold">{{ kp.question_count }} 道题目</span>
+                        </span>
+                        {% endif %}
+                        {% if kp.has_children %}
+                        <span class="flex items-center gap-1.5 px-2 py-1 bg-blue-50 rounded-md text-blue-600">
+                            <i class="ri-node-tree"></i>
+                            <span class="font-semibold">{{ kp.children|length }} 个子节点</span>
+                        </span>
+                        {% endif %}
+                    </div>
+                </div>
+                
+                <!-- 操作按钮 -->
+                <div class="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity ml-4 flex-shrink-0" onclick="event.stopPropagation()">
+                    <button 
+                        onclick="showAddChildModal('{{ kp.kp_code }}', '{{ kp.name }}')"
+                        class="px-3 py-1.5 text-xs font-semibold text-white bg-gradient-to-r from-green-500 to-emerald-600 hover:from-green-600 hover:to-emerald-700 rounded-lg transition-all shadow-sm hover:shadow-md flex items-center gap-1.5"
+                        title="添加子知识点">
+                        <i class="ri-add-line"></i>
+                        <span>添加子节点</span>
+                    </button>
+                    <button 
+                        onclick="showEditModal({{ kp.id }})"
+                        class="w-9 h-9 rounded-lg text-blue-600 bg-blue-50 hover:bg-blue-100 flex items-center justify-center transition-colors shadow-sm hover:shadow-md"
+                        title="编辑">
+                        <i class="ri-edit-line"></i>
+                    </button>
+                    <button 
+                        onclick="showDeleteConfirm({{ kp.id }}, '{{ kp.kp_code }}', '{{ kp.name }}')"
+                        class="w-9 h-9 rounded-lg text-red-600 bg-red-50 hover:bg-red-100 flex items-center justify-center transition-colors shadow-sm hover:shadow-md"
+                        title="删除">
+                        <i class="ri-delete-bin-line"></i>
+                    </button>
+                </div>
+            </div>
+        </div>
+    </div>
+    
+    <!-- 子节点容器 -->
+    {% if kp.children|length > 0 %}
+    <div class="children-container mt-3 ml-12 {% if level >= 1 %}hidden{% endif %}" id="children-{{ kp.kp_code }}" data-level="{{ level }}" data-parent-code="{{ kp.kp_code }}">
+        {% for child in kp.children %}
+            {{ render_tree_node(child, level + 1) }}
+        {% endfor %}
+    </div>
+    {% endif %}
+</div>
+{% endmacro %}
+
+<div class="space-y-6">
+    <!-- 学段统计卡片(只显示初中) -->
+    <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
+        {% for grade_name, info in grade_info.items() %}
+        {# 只显示初中,小学和高中已注释 #}
+        <div 
+            onclick="filterByGrade('{{ grade_name }}')"
+            data-grade="{{ grade_name }}"
+            class="grade-card apple-card p-6 cursor-pointer transform transition-all duration-300 hover:scale-[1.02] hover:shadow-xl group {{ info.bg }} border-2 border-transparent hover:border-gray-300">
+            <div class="flex items-center justify-between mb-4">
+                <div class="w-16 h-16 rounded-2xl bg-gradient-to-br {{ info.color }} flex items-center justify-center text-white text-2xl shadow-lg group-hover:scale-110 transition-transform group-hover:shadow-xl">
+                    <i class="{{ info.icon }}"></i>
+                </div>
+                <div class="text-right">
+                    <div class="text-3xl font-bold text-gray-800 group-hover:text-gray-900 transition-colors">{{ info.count }}</div>
+                    <div class="text-sm text-gray-500 mt-1 font-medium">知识点</div>
+                </div>
+            </div>
+            <div class="flex items-center justify-between mb-3">
+                <h3 class="text-xl font-bold text-gray-800 group-hover:text-gray-900 transition-colors">{{ grade_name }}</h3>
+                <div class="w-8 h-8 rounded-full bg-white/60 flex items-center justify-center group-hover:bg-white/80 transition-colors">
+                    <i class="ri-arrow-right-line text-gray-600 group-hover:text-gray-800 transition-colors"></i>
+                </div>
+            </div>
+            <div class="mt-4 h-1.5 bg-white/60 rounded-full overflow-hidden shadow-inner">
+                {% set total_count = grade_info.values()|sum(attribute='count') %}
+                {% if total_count > 0 %}
+                    {% set percentage = (info.count / total_count * 100)|round(1) %}
+                {% else %}
+                    {% set percentage = 0 %}
+                {% endif %}
+                <div class="h-full bg-gradient-to-r {{ info.color }} rounded-full transition-all duration-500 shadow-sm" 
+                     style="width: {{ percentage }}%"></div>
+            </div>
+        </div>
+        {% endfor %}
+    </div>
+
+    <!-- 页面标题和操作按钮 -->
+    <div class="flex items-center justify-between">
+        <!-- 搜索框 -->
+        <div class="flex-1 max-w-md">
+            <div class="relative">
+                <input 
+                    type="text" 
+                    id="kpSearchInput"
+                    placeholder="搜索知识点名称或代码..."
+                    class="w-full px-4 py-2 pl-10 pr-10 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
+                    onkeydown="handleSearchKeydown(event)"
+                    oninput="handleSearchInput(event)">
+                <i class="ri-search-line absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"></i>
+                <button 
+                    id="clearSearchBtn"
+                    onclick="clearSearch()"
+                    class="hidden absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors">
+                    <i class="ri-close-line"></i>
+                </button>
+            </div>
+        </div>
+        <div class="flex items-center gap-3">
+            <button 
+                onclick="clearFilter()"
+                id="clearFilterBtn"
+                class="hidden px-4 py-2 text-gray-600 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors flex items-center gap-2">
+                <i class="ri-close-line"></i>
+                <span>清除筛选</span>
+            </button>
+            <button 
+                onclick="showAddModal()"
+                class="btn-apple bg-gradient-to-r from-blue-600 to-indigo-600 text-white hover:from-blue-700 hover:to-indigo-700 px-4 py-2 rounded-lg shadow-sm flex items-center gap-2">
+                <i class="ri-add-circle-line"></i>
+                <span>添加知识点</span>
+            </button>
+        </div>
+    </div>
+
+    <!-- 知识点树状结构 -->
+    <div class="space-y-3" id="kpTreeContainer">
+        {% for root_kp in kp_tree %}
+            {{ render_tree_node(root_kp, 0) }}
+        {% endfor %}
+    </div>
+</div>
+
+<!-- 添加/编辑知识点模态框 -->
+<div id="kpModal" class="hidden fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center">
+    <div class="bg-white rounded-2xl shadow-2xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
+        <div class="p-6 border-b border-gray-200">
+            <h2 id="modalTitle" class="text-xl font-bold text-gray-800">添加知识点</h2>
+        </div>
+        <form id="kpForm" class="p-6 space-y-4">
+            <input type="hidden" id="kpId" name="id">
+            
+            <div>
+                <label class="block text-sm font-semibold text-gray-700 mb-2">知识点代码 *</label>
+                <input 
+                    type="text" 
+                    id="kpCode" 
+                    name="kp_code"
+                    required
+                    class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
+                    placeholder="例如:M01">
+            </div>
+            
+            <div>
+                <label class="block text-sm font-semibold text-gray-700 mb-2">名称 *</label>
+                <input 
+                    type="text" 
+                    id="kpName" 
+                    name="name"
+                    required
+                    class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
+                    placeholder="请输入知识点名称">
+            </div>
+            
+            <div>
+                <label class="block text-sm font-semibold text-gray-700 mb-2">年级</label>
+                <input 
+                    type="text" 
+                    id="kpGrade" 
+                    name="grade"
+                    class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
+                    placeholder="例如:初中">
+            </div>
+            
+            <!-- 科目字段隐藏,默认传"数学" -->
+            <input type="hidden" id="kpSubject" name="subject" value="数学">
+            
+            <div>
+                <label class="block text-sm font-semibold text-gray-700 mb-2">父知识点</label>
+                <select 
+                    id="kpParent" 
+                    name="parent_kp_code"
+                    class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
+                    <option value="">无(顶级知识点)</option>
+                    {% for option in kp_options %}
+                    <option value="{{ option.kp_code }}">{{ option.kp_code }} - {{ option.name }}</option>
+                    {% endfor %}
+                </select>
+            </div>
+            
+            <div class="flex justify-end gap-3 pt-4 border-t border-gray-200">
+                <button 
+                    type="button"
+                    onclick="closeModal()"
+                    class="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors">
+                    取消
+                </button>
+                <button 
+                    type="submit"
+                    class="px-4 py-2 bg-blue-600 text-white hover:bg-blue-700 rounded-lg transition-colors">
+                    保存
+                </button>
+            </div>
+        </form>
+    </div>
+</div>
+
+<script>
+// 知识点选项数据
+const kpOptions = {{ kp_options|tojson|safe }};
+
+// 显示添加模态框
+function showAddModal() {
+    document.getElementById('modalTitle').textContent = '添加知识点';
+    document.getElementById('kpForm').reset();
+    document.getElementById('kpId').value = '';
+    document.getElementById('kpCode').disabled = false;
+    document.getElementById('kpSubject').value = '数学'; // 默认科目为数学
+    document.getElementById('kpModal').classList.remove('hidden');
+}
+
+// 显示编辑模态框
+async function showEditModal(kpId) {
+    try {
+        const response = await fetch(`/api/kp/get/${kpId}`);
+        const result = await response.json();
+        
+        if (result.success) {
+            const kp = result.data;
+            document.getElementById('modalTitle').textContent = '编辑知识点';
+            document.getElementById('kpId').value = kp.id;
+            document.getElementById('kpCode').value = kp.kp_code;
+            document.getElementById('kpCode').disabled = true; // 编辑时不允许修改代码
+            document.getElementById('kpName').value = kp.name || '';
+            document.getElementById('kpSubject').value = kp.subject || '数学'; // 如果为空则默认为数学
+            document.getElementById('kpGrade').value = kp.grade || '';
+            document.getElementById('kpParent').value = kp.parent_kp_code || '';
+            document.getElementById('kpModal').classList.remove('hidden');
+        } else {
+            if (window.customAlert) {
+                window.customAlert('获取知识点信息失败: ' + result.error);
+            } else {
+                alert('获取知识点信息失败: ' + result.error);
+            }
+        }
+    } catch (error) {
+        console.error('Error:', error);
+        if (window.customAlert) {
+            window.customAlert('获取知识点信息失败: ' + error.message);
+        } else {
+            alert('获取知识点信息失败: ' + error.message);
+        }
+    }
+}
+
+// 关闭模态框
+function closeModal() {
+    document.getElementById('kpModal').classList.add('hidden');
+}
+
+// 表单提交
+document.getElementById('kpForm').addEventListener('submit', async function(e) {
+    e.preventDefault();
+    
+    const kpId = document.getElementById('kpId').value;
+    const formData = {
+        kp_code: (document.getElementById('kpCode').value || '').trim(),
+        name: (document.getElementById('kpName').value || '').trim(),
+        subject: (document.getElementById('kpSubject').value || '').trim() || '数学', // 默认科目为数学
+        grade: (document.getElementById('kpGrade').value || '').trim() || null,
+        parent_kp_code: (document.getElementById('kpParent').value || '').trim() || null
+    };
+    
+    try {
+        let response;
+        if (kpId) {
+            // 更新
+            response = await fetch(`/api/kp/update/${kpId}`, {
+                method: 'POST',
+                headers: {'Content-Type': 'application/json'},
+                body: JSON.stringify(formData)
+            });
+        } else {
+            // 创建
+            response = await fetch('/api/kp/create', {
+                method: 'POST',
+                headers: {'Content-Type': 'application/json'},
+                body: JSON.stringify(formData)
+            });
+        }
+        
+        const result = await response.json();
+        
+        if (result.success) {
+            if (window.customAlert) {
+                window.customAlert(result.message || '操作成功', () => {
+                    window.location.reload();
+                });
+            } else {
+                alert(result.message || '操作成功');
+                window.location.reload();
+            }
+        } else {
+            if (window.customAlert) {
+                window.customAlert('操作失败: ' + result.error);
+            } else {
+                alert('操作失败: ' + result.error);
+            }
+        }
+    } catch (error) {
+        console.error('Error:', error);
+        if (window.customAlert) {
+            window.customAlert('操作失败: ' + error.message);
+        } else {
+            alert('操作失败: ' + error.message);
+        }
+    }
+});
+
+// 切换树节点展开/折叠
+function toggleTreeNode(kpCode) {
+    const childrenContainer = document.getElementById(`children-${kpCode}`);
+    if (!childrenContainer) return;
+    
+    // 找到对应的展开按钮(通过查找包含该按钮的节点)
+    const kpNode = childrenContainer.closest('.kp-node');
+    if (!kpNode) return;
+    
+    const expandBtn = kpNode.querySelector('.expand-btn');
+    if (!expandBtn) return;
+    
+    const isExpanded = expandBtn.getAttribute('data-expanded') === 'true';
+    
+    if (isExpanded) {
+        // 折叠:隐藏所有子节点
+        childrenContainer.classList.add('hidden');
+        expandBtn.setAttribute('data-expanded', 'false');
+        const icon = expandBtn.querySelector('i');
+        if (icon) {
+            icon.className = 'ri-add-line text-sm';
+        }
+    } else {
+        // 展开:显示直接子节点
+        childrenContainer.classList.remove('hidden');
+        expandBtn.setAttribute('data-expanded', 'true');
+        const icon = expandBtn.querySelector('i');
+        if (icon) {
+            icon.className = 'ri-subtract-line text-sm';
+        }
+    }
+}
+
+// 强制展开节点(用于搜索时展开父节点)
+function expandTreeNode(kpCode) {
+    const childrenContainer = document.getElementById(`children-${kpCode}`);
+    if (!childrenContainer) return;
+    
+    const kpNode = childrenContainer.closest('.kp-node');
+    if (!kpNode) return;
+    
+    const expandBtn = kpNode.querySelector('.expand-btn');
+    if (!expandBtn) return;
+    
+    // 展开节点
+    childrenContainer.classList.remove('hidden');
+    expandBtn.setAttribute('data-expanded', 'true');
+    const icon = expandBtn.querySelector('i');
+    if (icon) {
+        icon.className = 'ri-subtract-line text-sm';
+    }
+}
+
+// 展开所有父节点直到根节点
+function expandAllParents(kpCode) {
+    const kpNode = document.querySelector(`.kp-node[data-kp-code="${kpCode}"]`);
+    if (!kpNode) return;
+    
+    let currentParentCode = kpNode.getAttribute('data-parent');
+    
+    // 递归展开所有父节点
+    while (currentParentCode && currentParentCode !== '') {
+        expandTreeNode(currentParentCode);
+        
+        // 获取父节点的父节点
+        const parentNode = document.querySelector(`.kp-node[data-kp-code="${currentParentCode}"]`);
+        if (parentNode) {
+            currentParentCode = parentNode.getAttribute('data-parent');
+        } else {
+            break;
+        }
+    }
+}
+
+// 搜索知识点
+function searchKnowledgePoint(searchText) {
+    if (!searchText || searchText.trim() === '') {
+        clearSearch();
+        return;
+    }
+    
+    const searchLower = searchText.toLowerCase().trim();
+    const allKpNodes = document.querySelectorAll('.kp-node');
+    let foundNodes = [];
+    
+    // 清除之前的高亮
+    allKpNodes.forEach(node => {
+        const card = node.querySelector('.apple-card');
+        if (card) {
+            card.classList.remove('ring-4', 'ring-blue-500', 'ring-offset-2', 'bg-blue-50');
+        }
+    });
+    
+    // 搜索匹配的知识点(收集所有匹配的节点)
+    for (const node of allKpNodes) {
+        const kpCode = node.getAttribute('data-kp-code') || '';
+        const kpName = node.querySelector('h3')?.textContent || '';
+        
+        // 模糊匹配:代码或名称包含搜索文本
+        if (kpCode.toLowerCase().includes(searchLower) || kpName.toLowerCase().includes(searchLower)) {
+            foundNodes.push({ node, kpCode, kpName });
+        }
+    }
+    
+    if (foundNodes.length === 0) {
+        // 如果没有找到,显示提示
+        if (window.customAlert) {
+            window.customAlert('未找到匹配的知识点');
+        } else {
+            alert('未找到匹配的知识点');
+        }
+        return;
+    }
+    
+    // 优先匹配代码,其次匹配名称
+    foundNodes.sort((a, b) => {
+        const aCodeMatch = a.kpCode.toLowerCase().includes(searchLower);
+        const bCodeMatch = b.kpCode.toLowerCase().includes(searchLower);
+        if (aCodeMatch && !bCodeMatch) return -1;
+        if (!aCodeMatch && bCodeMatch) return 1;
+        return 0;
+    });
+    
+    // 跳转到第一个匹配的知识点
+    const firstMatch = foundNodes[0];
+    const targetNode = firstMatch.node;
+    const targetKpCode = firstMatch.kpCode;
+    
+    // 展开所有父节点
+    expandAllParents(targetKpCode);
+    
+    // 高亮当前节点
+    const card = targetNode.querySelector('.apple-card');
+    if (card) {
+        card.classList.add('ring-4', 'ring-blue-500', 'ring-offset-2', 'bg-blue-50');
+    }
+    
+    // 滚动到该节点(等待展开动画完成)
+    setTimeout(() => {
+        targetNode.scrollIntoView({ behavior: 'smooth', block: 'center' });
+        
+        // 3秒后移除高亮
+        setTimeout(() => {
+            if (card) {
+                card.classList.remove('ring-4', 'ring-blue-500', 'ring-offset-2', 'bg-blue-50');
+            }
+        }, 3000);
+    }, 200);
+    
+    // 如果有多个匹配,在控制台提示
+    if (foundNodes.length > 1) {
+        console.log(`找到 ${foundNodes.length} 个匹配的知识点,已跳转到第一个`);
+    }
+}
+
+// 处理搜索输入(实时搜索)
+let searchTimeout = null;
+function handleSearchInput(event) {
+    const searchText = event.target.value;
+    const clearBtn = document.getElementById('clearSearchBtn');
+    
+    if (searchText.trim() !== '') {
+        clearBtn.classList.remove('hidden');
+        // 防抖:延迟500ms后执行搜索
+        clearTimeout(searchTimeout);
+        searchTimeout = setTimeout(() => {
+            searchKnowledgePoint(searchText);
+        }, 500);
+    } else {
+        clearBtn.classList.add('hidden');
+        clearSearch();
+    }
+}
+
+// 处理搜索框回车键(立即搜索)
+function handleSearchKeydown(event) {
+    if (event.key === 'Enter') {
+        event.preventDefault();
+        clearTimeout(searchTimeout);
+        const searchText = event.target.value;
+        searchKnowledgePoint(searchText);
+    }
+}
+
+// 清除搜索
+function clearSearch() {
+    const searchInput = document.getElementById('kpSearchInput');
+    const clearBtn = document.getElementById('clearSearchBtn');
+    
+    searchInput.value = '';
+    clearBtn.classList.add('hidden');
+    
+    // 清除高亮
+    const allKpNodes = document.querySelectorAll('.kp-node');
+    allKpNodes.forEach(node => {
+        const card = node.querySelector('.apple-card');
+        if (card) {
+            card.classList.remove('ring-4', 'ring-blue-500', 'ring-offset-2');
+        }
+    });
+}
+
+// 处理卡片点击事件(点击空白区域展开/折叠)
+function handleCardClick(event, kpCode) {
+    // 如果点击的是按钮或链接,不触发展开/折叠
+    if (event.target.closest('button') || event.target.closest('a')) {
+        return;
+    }
+    // 触发展开/折叠
+    toggleTreeNode(kpCode);
+}
+
+// 显示添加子节点模态框
+function showAddChildModal(parentKpCode, parentKpName) {
+    document.getElementById('modalTitle').textContent = `添加子知识点(父节点:${parentKpCode} - ${parentKpName})`;
+    document.getElementById('kpId').value = '';
+    document.getElementById('kpCode').value = '';
+    document.getElementById('kpName').value = '';
+    document.getElementById('kpSubject').value = '数学'; // 默认科目为数学
+    document.getElementById('kpGrade').value = '';
+    document.getElementById('kpParent').value = parentKpCode;
+    document.getElementById('kpCode').disabled = false;
+    document.getElementById('kpModal').classList.remove('hidden');
+}
+
+// 删除确认
+function showDeleteConfirm(kpId, kpCode, kpName) {
+    if (confirm(`确定要删除知识点 "${kpCode} - ${kpName}" 吗?\n\n注意:如果该知识点下有子知识点,将无法删除。`)) {
+        deleteKp(kpId);
+    }
+}
+
+// 删除知识点
+async function deleteKp(kpId) {
+    try {
+        const response = await fetch(`/api/kp/delete/${kpId}`, {
+            method: 'POST',
+            headers: {'Content-Type': 'application/json'}
+        });
+        
+        const result = await response.json();
+        
+        if (result.success) {
+            if (window.customAlert) {
+                window.customAlert(result.message || '删除成功', () => {
+                    window.location.reload();
+                });
+            } else {
+                alert(result.message || '删除成功');
+                window.location.reload();
+            }
+        } else {
+            if (window.customAlert) {
+                window.customAlert('删除失败: ' + result.error);
+            } else {
+                alert('删除失败: ' + result.error);
+            }
+        }
+    } catch (error) {
+        console.error('Error:', error);
+        if (window.customAlert) {
+            window.customAlert('删除失败: ' + error.message);
+        } else {
+            alert('删除失败: ' + error.message);
+        }
+    }
+}
+
+// 点击模态框外部关闭
+document.getElementById('kpModal').addEventListener('click', function(e) {
+    if (e.target === this) {
+        closeModal();
+    }
+});
+
+// 当前筛选的学段
+let currentFilterGrade = null;
+
+// 按学段筛选
+function filterByGrade(grade) {
+    currentFilterGrade = grade;
+    
+    // 显示清除筛选按钮
+    document.getElementById('clearFilterBtn').classList.remove('hidden');
+    
+    // 获取所有行
+    const allRows = Array.from(document.querySelectorAll('.kp-row'));
+    
+    // 先隐藏所有行
+    allRows.forEach(row => {
+        row.classList.add('hidden');
+    });
+    
+    // 显示匹配学段的行及其父节点
+    allRows.forEach(row => {
+        const rowGrade = row.getAttribute('data-grade');
+        if (rowGrade === grade) {
+            // 显示匹配的行
+            row.classList.remove('hidden');
+            // 确保父节点也显示
+            showParentRows(row);
+        }
+    });
+    
+    // 重新计算可见行数
+    const visibleCount = Array.from(document.querySelectorAll('.kp-row:not(.hidden)')).length;
+    // 筛选信息已移除
+    
+    // 高亮选中的学段卡片
+    document.querySelectorAll('.grade-card').forEach(card => {
+        card.classList.remove('ring-2', 'ring-blue-500', 'ring-offset-2', 'border-blue-400');
+    });
+    // 找到被点击的卡片并高亮
+    const clickedCard = document.querySelector(`.grade-card[data-grade="${grade}"]`);
+    if (clickedCard) {
+        clickedCard.classList.add('ring-2', 'ring-blue-500', 'ring-offset-2', 'border-blue-400');
+    }
+}
+
+// 显示父节点
+function showParentRows(row) {
+    const parentCode = row.getAttribute('data-parent');
+    if (parentCode) {
+        const parentRow = document.querySelector(`tr[data-kp-code="${parentCode}"]`);
+        if (parentRow) {
+            parentRow.classList.remove('hidden');
+            showParentRows(parentRow);
+        }
+    }
+}
+
+// 清除筛选
+function clearFilter() {
+    currentFilterGrade = null;
+    document.getElementById('clearFilterBtn').classList.add('hidden');
+    
+    // 恢复默认显示状态(level 0 和 level 1)
+    const allRows = Array.from(document.querySelectorAll('.kp-row'));
+    allRows.forEach(row => {
+        const level = parseInt(row.getAttribute('data-level'));
+        if (level <= 1) {
+            row.classList.remove('hidden');
+        } else {
+            row.classList.add('hidden');
+        }
+    });
+    
+    // 重置展开按钮状态
+    document.querySelectorAll('.expand-btn').forEach(btn => {
+        const row = btn.closest('tr');
+        const level = parseInt(row.getAttribute('data-level'));
+        if (level === 0) {
+            btn.setAttribute('data-expanded', 'true');
+            btn.innerHTML = '<i class="ri-arrow-down-s-line"></i>';
+        } else {
+            btn.setAttribute('data-expanded', 'false');
+            btn.innerHTML = '<i class="ri-arrow-right-s-line"></i>';
+        }
+    });
+    
+    const totalCount = {{ knowledge_points|length }};
+    // 筛选信息已移除
+    
+    // 移除卡片高亮
+    document.querySelectorAll('.grade-card').forEach(card => {
+        card.classList.remove('ring-2', 'ring-blue-500', 'ring-offset-2', 'border-blue-400');
+    });
+}
+
+// 展开/折叠子知识点
+function toggleChildren(kpCode) {
+    const row = document.querySelector(`tr[data-kp-code="${kpCode}"]`);
+    if (!row) return;
+    
+    const btn = row.querySelector('.expand-btn');
+    const isExpanded = btn.getAttribute('data-expanded') === 'true';
+    
+    // 递归函数:切换所有子节点的显示状态
+    function toggleChildrenRecursive(parentCode, shouldShow) {
+        const allRows = Array.from(document.querySelectorAll('.kp-row'));
+        const parentRow = allRows.find(r => r.getAttribute('data-kp-code') === parentCode);
+        if (!parentRow) return;
+        
+        const parentLevel = parseInt(parentRow.getAttribute('data-level'));
+        const parentIndex = allRows.indexOf(parentRow);
+        
+        // 找到所有直接子节点
+        for (let i = parentIndex + 1; i < allRows.length; i++) {
+            const childRow = allRows[i];
+            const childLevel = parseInt(childRow.getAttribute('data-level'));
+            const childParent = childRow.getAttribute('data-parent');
+            
+            // 如果是直接子节点
+            if (childLevel === parentLevel + 1 && childParent === parentCode) {
+                if (shouldShow) {
+                    childRow.classList.remove('hidden');
+                    // 如果子节点是展开的,继续展开它的子节点
+                    const childBtn = childRow.querySelector('.expand-btn');
+                    if (childBtn && childBtn.getAttribute('data-expanded') === 'true') {
+                        toggleChildrenRecursive(childRow.getAttribute('data-kp-code'), true);
+                    }
+                } else {
+                    childRow.classList.add('hidden');
+                    // 递归隐藏所有子节点
+                    toggleChildrenRecursive(childRow.getAttribute('data-kp-code'), false);
+                }
+            } else if (childLevel <= parentLevel) {
+                // 遇到同级或上级节点,停止
+                break;
+            }
+        }
+    }
+    
+    // 切换显示状态
+    toggleChildrenRecursive(kpCode, !isExpanded);
+    
+    // 更新按钮状态
+    if (isExpanded) {
+        btn.setAttribute('data-expanded', 'false');
+        btn.innerHTML = '<i class="ri-arrow-right-s-line"></i>';
+    } else {
+        btn.setAttribute('data-expanded', 'true');
+        btn.innerHTML = '<i class="ri-arrow-down-s-line"></i>';
+    }
+}
+</script>
+{% endblock %}

+ 868 - 0
templates/layout.html

@@ -0,0 +1,868 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>{% block title %}知了数学题库系统{% endblock %}</title>
+    
+    <!-- Favicon -->
+    <link rel="icon" type="image/jpeg" href="{{ url_for('static', filename='小猫.jpg') }}">
+    
+    <!-- Tailwind CSS -->
+    <script src="https://cdn.tailwindcss.com"></script>
+    
+    <!-- KaTeX -->
+    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
+    <script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
+    <script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
+    <script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
+    
+    <!-- Remix Icon -->
+    <link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
+    
+    <style>
+        @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
+        
+        * {
+            margin: 0;
+            padding: 0;
+            box-sizing: border-box;
+        }
+        
+        body {
+            font-family: 'Inter', 'Microsoft YaHei', -apple-system, BlinkMacSystemFont, sans-serif;
+            background-color: #F8F9FA;
+            color: #1D1D1F;
+            overflow-x: hidden;
+        }
+        
+        /* 左侧导航栏 */
+        .sidebar {
+            position: fixed;
+            left: 0;
+            top: 0;
+            width: 260px;
+            height: 100vh;
+            background: linear-gradient(180deg, #FFFFFF 0%, #FAFBFC 100%);
+            border-right: 1px solid rgba(0, 0, 0, 0.06);
+            display: flex;
+            flex-direction: column;
+            z-index: 1000;
+            box-shadow: 2px 0 12px rgba(0, 0, 0, 0.04);
+            transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+            overflow: visible;
+        }
+        
+        .sidebar.collapsed {
+            width: 100px;
+            overflow: visible;
+        }
+        
+        .sidebar-header {
+            padding: 24px 20px;
+            border-bottom: 1px solid rgba(0, 0, 0, 0.06);
+            display: flex;
+            align-items: center;
+            gap: 12px;
+            position: relative;
+        }
+        
+        .sidebar.collapsed .sidebar-header {
+            padding: 24px 12px;
+            justify-content: center;
+        }
+        
+        .sidebar-toggle {
+            position: absolute;
+            right: -12px;
+            top: 50%;
+            transform: translateY(-50%);
+            width: 24px;
+            height: 24px;
+            border-radius: 50%;
+            background: #FFFFFF;
+            border: 1px solid rgba(0, 0, 0, 0.1);
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            cursor: pointer;
+            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+            transition: all 0.2s;
+            z-index: 10;
+        }
+        
+        .sidebar-toggle:hover {
+            background: #F8F9FA;
+            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+        }
+        
+        .sidebar-toggle i {
+            font-size: 14px;
+            color: #6366F1;
+            transition: transform 0.3s;
+        }
+        
+        .sidebar.collapsed .sidebar-toggle i {
+            transform: rotate(180deg);
+        }
+        
+        .sidebar-logo {
+            width: 40px;
+            height: 40px;
+            border-radius: 10px;
+            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            font-size: 20px;
+            color: white;
+            font-weight: 700;
+            flex-shrink: 0;
+            overflow: hidden;
+        }
+        
+        .sidebar-title {
+            flex: 1;
+            transition: opacity 0.3s;
+            overflow: hidden;
+        }
+        
+        .sidebar.collapsed .sidebar-title {
+            opacity: 0;
+            width: 0;
+            height: 0;
+            overflow: hidden;
+        }
+        
+        .sidebar-title-main {
+            font-size: 18px;
+            font-weight: 700;
+            color: #1D1D1F;
+            line-height: 1.2;
+            white-space: nowrap;
+        }
+        
+        .sidebar-title-sub {
+            font-size: 12px;
+            color: #86868B;
+            margin-top: 2px;
+            white-space: nowrap;
+        }
+        
+        .sidebar-nav {
+            flex: 1;
+            padding: 16px 12px;
+            overflow-y: auto;
+            overflow-x: visible;
+        }
+        
+        .sidebar.collapsed .sidebar-nav {
+            overflow-x: visible;
+        }
+        
+        .nav-section {
+            margin-bottom: 24px;
+        }
+        
+        .nav-section-title {
+            font-size: 11px;
+            font-weight: 600;
+            color: #86868B;
+            text-transform: uppercase;
+            letter-spacing: 0.5px;
+            padding: 0 12px;
+            margin-bottom: 8px;
+            transition: opacity 0.3s;
+            white-space: nowrap;
+            overflow: hidden;
+        }
+        
+        .sidebar.collapsed .nav-section-title {
+            opacity: 0;
+            height: 0;
+            padding: 0;
+            margin: 0;
+        }
+        
+        .nav-item {
+            display: flex;
+            align-items: center;
+            gap: 12px;
+            padding: 12px 16px;
+            margin-bottom: 4px;
+            border-radius: 10px;
+            color: #1D1D1F;
+            text-decoration: none;
+            font-size: 14px;
+            font-weight: 500;
+            transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+            cursor: pointer;
+            position: relative;
+        }
+        
+        .sidebar.collapsed .nav-item {
+            padding: 12px;
+            justify-content: center;
+        }
+        
+        .nav-item span {
+            transition: opacity 0.3s;
+            white-space: nowrap;
+        }
+        
+        .sidebar.collapsed .nav-item span {
+            opacity: 0;
+            width: 0;
+            overflow: hidden;
+        }
+        
+        .sidebar.collapsed .nav-item .ml-auto {
+            display: none;
+        }
+        
+        /* Tooltip样式 */
+        .nav-item-tooltip {
+            position: fixed;
+            padding: 6px 10px;
+            background: #1D1D1F;
+            color: #FFFFFF;
+            font-size: 12px;
+            font-weight: 500;
+            border-radius: 6px;
+            white-space: nowrap;
+            opacity: 0;
+            pointer-events: none;
+            transition: opacity 0.2s ease-in-out;
+            z-index: 99999;
+            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+            visibility: hidden;
+        }
+        
+        .nav-item-tooltip::before {
+            content: '';
+            position: absolute;
+            right: 100%;
+            top: 50%;
+            transform: translateY(-50%);
+            border: 5px solid transparent;
+            border-right-color: #1D1D1F;
+        }
+        
+        .sidebar:not(.collapsed) .nav-item-tooltip {
+            display: none !important;
+        }
+        
+        .nav-item i {
+            font-size: 20px;
+            width: 20px;
+            height: 20px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            color: #86868B;
+            transition: color 0.2s;
+        }
+        
+        .nav-item:hover {
+            background: rgba(99, 102, 241, 0.08);
+            color: #6366F1;
+        }
+        
+        .nav-item:hover i {
+            color: #6366F1;
+        }
+        
+        .nav-item.active {
+            background: linear-gradient(135deg, rgba(99, 102, 241, 0.12) 0%, rgba(139, 92, 246, 0.12) 100%);
+            color: #6366F1;
+            font-weight: 600;
+        }
+        
+        .nav-item.active i {
+            color: #6366F1;
+        }
+        
+        .nav-item.active::before {
+            content: '';
+            position: absolute;
+            left: 0;
+            top: 50%;
+            transform: translateY(-50%);
+            width: 3px;
+            height: 20px;
+            background: linear-gradient(135deg, #6366F1 0%, #8B5CF6 100%);
+            border-radius: 0 2px 2px 0;
+        }
+        
+        /* 主内容区 */
+        .main-content {
+            margin-left: 260px;
+            min-height: 100vh;
+            background: #F8F9FA;
+            transition: margin-left 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+            position: relative;
+            z-index: 1;
+        }
+        
+        .sidebar.collapsed ~ .main-content {
+            margin-left: 100px;
+        }
+        
+        .main-header {
+            background: #FFFFFF;
+            border-bottom: 1px solid rgba(0, 0, 0, 0.06);
+            padding: 16px 32px;
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            position: sticky;
+            top: 0;
+            z-index: 100;
+            backdrop-filter: blur(20px);
+            background: rgba(255, 255, 255, 0.8);
+        }
+        
+        .main-header-title {
+            font-size: 20px;
+            font-weight: 700;
+            color: #1D1D1F;
+        }
+        
+        .main-header-actions {
+            display: flex;
+            align-items: center;
+            gap: 12px;
+        }
+        
+        .main-body {
+            padding: 32px;
+            max-width: 100%;
+        }
+        
+        .apple-card {
+            background: rgba(255, 255, 255, 0.9);
+            backdrop-filter: blur(20px);
+            border-radius: 16px;
+            border: 1px solid rgba(0,0,0,0.06);
+            transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+            box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
+        }
+        
+        .apple-card:hover {
+            transform: translateY(-2px);
+            box-shadow: 0 8px 24px rgba(0,0,0,0.08);
+        }
+
+        .btn-apple {
+            border-radius: 10px;
+            padding: 10px 20px;
+            font-weight: 600;
+            font-size: 14px;
+            transition: all 0.2s;
+        }
+        
+        /* 滚动条样式 */
+        .sidebar-nav::-webkit-scrollbar {
+            width: 6px;
+        }
+        
+        .sidebar-nav::-webkit-scrollbar-track {
+            background: transparent;
+        }
+        
+        .sidebar-nav::-webkit-scrollbar-thumb {
+            background: rgba(0, 0, 0, 0.2);
+            border-radius: 3px;
+        }
+        
+        .sidebar-nav::-webkit-scrollbar-thumb:hover {
+            background: rgba(0, 0, 0, 0.3);
+        }
+
+        /* 修复 SVG 在网页中的显示 */
+        svg {
+            max-width: 100%;
+            height: auto;
+            margin: 1rem auto;
+            display: block;
+        }
+
+        /* 题目中的图片尺寸限制 */
+        .math-render img,
+        #stem-container img,
+        .apple-card img:not([class*="rounded-full"]):not([class*="object-cover"]) {
+            max-width: 400px !important;
+            max-height: 300px !important;
+            width: auto !important;
+            height: auto !important;
+            object-fit: contain;
+            display: block;
+            margin: 0.5rem auto;
+        }
+
+        /* 打印时隐藏 UI 元素 */
+        @media print {
+            .no-print { display: none !important; }
+            .apple-card { border: none; background: white; backdrop-filter: none; }
+            body { background: white; }
+        }
+
+        /* 数学公式容器样式 */
+        .math-render {
+            line-height: 1.8;
+            font-size: 1.1rem;
+        }
+
+        /* 合格率进度条渐变 */
+        .progress-gradient {
+            background: linear-gradient(to right, #ef4444 0%, #f59e0b 50%, #22c55e 100%);
+        }
+        
+        /* 响应式 */
+        @media (max-width: 1024px) {
+            .sidebar {
+                transform: translateX(-100%);
+                transition: transform 0.3s;
+            }
+            
+            .sidebar.collapsed {
+                transform: translateX(0);
+                width: 100px;
+            }
+            
+            .main-content {
+                margin-left: 0;
+            }
+            
+            .sidebar.collapsed ~ .main-content {
+                margin-left: 100px;
+            }
+        }
+        
+        /* 搜索框折叠样式 */
+        .sidebar.collapsed .nav-section form {
+            padding: 0 12px;
+        }
+        
+        .sidebar.collapsed .nav-section form input {
+            opacity: 0;
+            width: 0;
+            padding: 0;
+        }
+        
+        .sidebar.collapsed .nav-section form .flex {
+            justify-content: center;
+        }
+        
+        .sidebar.collapsed .nav-section form .flex:hover .nav-item-tooltip {
+            opacity: 1;
+        }
+        
+        .nav-item {
+            position: relative;
+        }
+        
+        /* 修复 SVG 在网页中的显示 */
+        svg {
+            max-width: 100%;
+            height: auto;
+            margin: 1rem auto;
+            display: block;
+        }
+
+        /* 题目中的图片尺寸限制 */
+        .math-render img,
+        #stem-container img,
+        .apple-card img:not([class*="rounded-full"]):not([class*="object-cover"]) {
+            max-width: 400px !important;
+            max-height: 300px !important;
+            width: auto !important;
+            height: auto !important;
+            object-fit: contain;
+            display: block;
+            margin: 0.5rem auto;
+        }
+
+        /* 打印时隐藏 UI 元素 */
+        @media print {
+            .no-print { display: none !important; }
+            .apple-card { border: none; background: white; backdrop-filter: none; }
+            body { background: white; }
+            .sidebar { display: none !important; }
+            .main-content { margin-left: 0 !important; }
+        }
+
+        /* 数学公式容器样式 */
+        .math-render {
+            line-height: 1.8;
+            font-size: 1.1rem;
+        }
+
+        /* 合格率进度条渐变 */
+        .progress-gradient {
+            background: linear-gradient(to right, #ef4444 0%, #f59e0b 50%, #22c55e 100%);
+        }
+    </style>
+</head>
+<body class="min-h-screen">
+
+    <!-- 左侧导航栏 -->
+    <aside class="sidebar no-print" id="sidebar">
+        <div class="sidebar-header">
+            <div class="sidebar-logo">
+                <img src="{{ url_for('static', filename='小猫.jpg') }}" alt="知了数学" class="w-full h-full object-cover rounded-lg" onerror="this.style.display='none'; this.parentElement.innerHTML='知';">
+            </div>
+            <div class="sidebar-title">
+                <div class="sidebar-title-main">知了数学</div>
+                <div class="sidebar-title-sub">题库管理系统</div>
+            </div>
+            <button class="sidebar-toggle" id="sidebar-toggle" onclick="toggleSidebar()" title="折叠/展开菜单">
+                <i class="ri-arrow-left-s-line"></i>
+            </button>
+        </div>
+        
+        <nav class="sidebar-nav">
+            <div class="nav-section">
+                <div class="nav-section-title">主要功能</div>
+                <a href="/" class="nav-item {% if request.path == '/' %}active{% endif %}">
+                    <i class="ri-home-4-line"></i>
+                    <span>首页</span>
+                    <div class="nav-item-tooltip">首页</div>
+                </a>
+                <a href="/question_management" class="nav-item {% if request.path == '/question_management' %}active{% endif %}">
+                    <i class="ri-file-list-line"></i>
+                    <span>题目管理</span>
+                    <div class="nav-item-tooltip">题目管理</div>
+                </a>
+                <a href="/kp_management" class="nav-item {% if request.path.startswith('/kp_management') %}active{% endif %}">
+                    <i class="ri-book-open-line"></i>
+                    <span>知识点管理</span>
+                    <div class="nav-item-tooltip">知识点管理</div>
+                </a>
+                <a href="/textbook_management" class="nav-item {% if request.path.startswith('/textbook_management') %}active{% endif %}">
+                    <i class="ri-book-2-line"></i>
+                    <span>教材管理</span>
+                    <div class="nav-item-tooltip">教材管理</div>
+                </a>
+                <a href="/material_management" class="nav-item {% if request.path.startswith('/material_management') %}active{% endif %}">
+                    <i class="ri-folder-line"></i>
+                    <span>资料管理</span>
+                    <div class="nav-item-tooltip">资料管理</div>
+                </a>
+            </div>
+            
+            <div class="nav-section">
+                <div class="nav-section-title">常用工具</div>
+                <a href="https://math-analysis.chunsunqiuzhu.com" target="_blank" rel="noopener noreferrer" class="nav-item">
+                    <i class="ri-file-text-line"></i>
+                    <span>试卷解析工具</span>
+                    <i class="ri-external-link-line ml-auto text-xs"></i>
+                    <div class="nav-item-tooltip">试卷解析工具</div>
+                </a>
+                <a href="http://192.168.124.8:9300/" target="_blank" rel="noopener noreferrer" class="nav-item">
+                    <i class="ri-links-line"></i>
+                    <span>题目匹配知识点</span>
+                    <i class="ri-external-link-line ml-auto text-xs"></i>
+                    <div class="nav-item-tooltip">题目匹配知识点</div>
+                </a>
+            </div>
+            
+            <div class="nav-section">
+                <div class="nav-section-title">快速操作</div>
+                <form action="/search" method="get" class="mb-2 nav-item-form">
+                    <div class="flex items-center gap-2 px-4 py-2 bg-gray-50 rounded-lg border border-gray-200 relative">
+                        <i class="ri-search-line text-gray-400"></i>
+                        <input
+                            name="q"
+                            type="text"
+                            placeholder="搜索题号"
+                            class="flex-1 bg-transparent border-none outline-none text-sm"
+                            onkeypress="if(event.key==='Enter') this.form.submit()"
+                        />
+                        <div class="nav-item-tooltip">搜索题号</div>
+                    </div>
+                </form>
+                <form action="/search_id" method="get" class="mb-2 nav-item-form">
+                    <div class="flex items-center gap-2 px-4 py-2 bg-gray-50 rounded-lg border border-gray-200 relative">
+                        <i class="ri-hashtag text-gray-400"></i>
+                        <input
+                            name="id"
+                            type="text"
+                            placeholder="搜索ID"
+                            class="flex-1 bg-transparent border-none outline-none text-sm"
+                            onkeypress="if(event.key==='Enter') this.form.submit()"
+                        />
+                        <div class="nav-item-tooltip">搜索ID</div>
+                    </div>
+                </form>
+            </div>
+            
+            {% block sidebar_extra %}{% endblock %}
+        </nav>
+    </aside>
+
+    <!-- 主内容区 -->
+    <div class="main-content">
+        <header class="main-header no-print">
+            <div>
+                <h1 class="main-header-title">{% block page_title %}知了数学题库管理系统{% endblock %}</h1>
+            </div>
+            <div class="main-header-actions">
+                {% block header_actions %}{% endblock %}
+            </div>
+        </header>
+        
+        <main class="main-body">
+            {% block content %}{% endblock %}
+        </main>
+    </div>
+
+    <script>
+        // 检查并获取用户姓名
+        function getUserName() {
+            return localStorage.getItem('user_name') || null;
+        }
+        
+        // 设置用户姓名
+        function setUserName(name) {
+            if (name && name.trim()) {
+                localStorage.setItem('user_name', name.trim());
+            }
+        }
+        
+        // 显示姓名输入弹窗
+        function showNameInputModal() {
+            const userName = getUserName();
+            if (userName) {
+                return; // 已有姓名,不显示弹窗
+            }
+            
+            const modal = document.createElement('div');
+            modal.id = 'name-input-modal';
+            modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:10000;display:flex;align-items:center;justify-content:center;';
+            
+            const dialog = document.createElement('div');
+            dialog.style.cssText = 'background:white;padding:2rem;border-radius:16px;box-shadow:0 20px 60px rgba(0,0,0,0.3);max-width:400px;width:90%;text-align:center;';
+            
+            const title = document.createElement('div');
+            title.style.cssText = 'font-size:1.25rem;font-weight:bold;color:#333;margin-bottom:1rem;';
+            title.textContent = '请输入您的姓名';
+            
+            const input = document.createElement('input');
+            input.type = 'text';
+            input.placeholder = '请输入姓名';
+            input.style.cssText = 'width:100%;padding:0.75rem;border:2px solid #e5e7eb;border-radius:8px;font-size:1rem;margin-bottom:1.5rem;outline:none;transition:border-color 0.2s;';
+            input.addEventListener('focus', function() {
+                this.style.borderColor = '#3b82f6';
+            });
+            input.addEventListener('blur', function() {
+                this.style.borderColor = '#e5e7eb';
+            });
+            
+            const btn = document.createElement('button');
+            btn.style.cssText = 'background:#3b82f6;color:white;border:none;padding:0.75rem 2rem;border-radius:8px;font-size:1rem;cursor:pointer;font-weight:600;width:100%;transition:background 0.2s;';
+            btn.textContent = '确定';
+            btn.addEventListener('mouseenter', function() {
+                this.style.background = '#2563eb';
+            });
+            btn.addEventListener('mouseleave', function() {
+                this.style.background = '#3b82f6';
+            });
+            
+            const handleConfirm = () => {
+                const name = input.value.trim();
+                if (!name) {
+                    input.style.borderColor = '#ef4444';
+                    input.focus();
+                    return;
+                }
+                setUserName(name);
+                document.body.removeChild(modal);
+            };
+            
+            btn.onclick = handleConfirm;
+            input.onkeypress = function(e) {
+                if (e.key === 'Enter') {
+                    handleConfirm();
+                }
+            };
+            
+            dialog.appendChild(title);
+            dialog.appendChild(input);
+            dialog.appendChild(btn);
+            modal.appendChild(dialog);
+            document.body.appendChild(modal);
+            
+            // 聚焦输入框
+            setTimeout(() => input.focus(), 100);
+        }
+        
+        // 侧边栏折叠/展开功能
+        function toggleSidebar() {
+            const sidebar = document.getElementById('sidebar');
+            if (sidebar) {
+                sidebar.classList.toggle('collapsed');
+                // 保存状态到localStorage
+                const isCollapsed = sidebar.classList.contains('collapsed');
+                localStorage.setItem('sidebarCollapsed', isCollapsed ? 'true' : 'false');
+            }
+        }
+        
+        // 恢复侧边栏状态
+        function restoreSidebarState() {
+            const sidebar = document.getElementById('sidebar');
+            const savedState = localStorage.getItem('sidebarCollapsed');
+            if (sidebar && savedState === 'true') {
+                sidebar.classList.add('collapsed');
+            }
+        }
+        
+        // Tooltip位置计算和显示
+        function setupTooltips() {
+            const sidebar = document.getElementById('sidebar');
+            if (!sidebar || !sidebar.classList.contains('collapsed')) return;
+            
+            const navItems = document.querySelectorAll('.sidebar.collapsed .nav-item, .sidebar.collapsed .nav-section form .flex');
+            
+            navItems.forEach(item => {
+                const tooltip = item.querySelector('.nav-item-tooltip');
+                if (!tooltip) return;
+                
+                item.addEventListener('mouseenter', function(e) {
+                    const rect = item.getBoundingClientRect();
+                    tooltip.style.left = (rect.right + 8) + 'px';
+                    tooltip.style.top = (rect.top + rect.height / 2) + 'px';
+                    tooltip.style.transform = 'translateY(-50%)';
+                    tooltip.style.opacity = '1';
+                    tooltip.style.visibility = 'visible';
+                });
+                
+                item.addEventListener('mouseleave', function() {
+                    tooltip.style.opacity = '0';
+                    tooltip.style.visibility = 'hidden';
+                });
+            });
+        }
+        
+        // 监听侧边栏折叠状态变化
+        function observeSidebarCollapse() {
+            const sidebar = document.getElementById('sidebar');
+            if (!sidebar) return;
+            
+            const observer = new MutationObserver(function(mutations) {
+                mutations.forEach(function(mutation) {
+                    if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
+                        setTimeout(setupTooltips, 100);
+                    }
+                });
+            });
+            
+            observer.observe(sidebar, {
+                attributes: true,
+                attributeFilter: ['class']
+            });
+        }
+        
+        document.addEventListener("DOMContentLoaded", function() {
+            // 恢复侧边栏状态
+            restoreSidebarState();
+            
+            // 设置tooltip
+            setTimeout(setupTooltips, 100);
+            observeSidebarCollapse();
+            
+            // 检查是否需要显示姓名输入弹窗
+            showNameInputModal();
+            
+            renderMathInElement(document.body, {
+                delimiters: [
+                    {left: "$$", right: "$$", display: true},
+                    {left: "$", right: "$", display: false},
+                    {left: "\\(", right: "\\)", display: false},
+                    {left: "\\[", right: "\\]", display: true}
+                ],
+                throwOnError : false
+            });
+        });
+
+        // 自定义弹窗函数(支持空格键确认)
+        window.customAlert = function(message, callback) {
+            const modal = document.createElement('div');
+            modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.4);z-index:9999;display:flex;align-items:center;justify-content:center;';
+            
+            const dialog = document.createElement('div');
+            dialog.style.cssText = 'background:white;padding:2rem;border-radius:12px;box-shadow:0 10px 40px rgba(0,0,0,0.2);max-width:400px;text-align:center;';
+            
+            const msg = document.createElement('div');
+            msg.style.cssText = 'margin-bottom:1.5rem;font-size:1rem;color:#333;';
+            msg.textContent = message;
+            
+            const btn = document.createElement('button');
+            btn.style.cssText = 'background:#007AFF;color:white;border:none;padding:0.75rem 2rem;border-radius:8px;font-size:1rem;cursor:pointer;font-weight:600;';
+            btn.textContent = '确定';
+            
+            const handleConfirm = () => {
+                document.body.removeChild(modal);
+                if (callback) callback();
+            };
+            
+            btn.onclick = handleConfirm;
+            
+            // 监听空格键
+            const handleKeyPress = (e) => {
+                if (e.code === 'Space' || e.key === ' ') {
+                    e.preventDefault();
+                    handleConfirm();
+                }
+            };
+            
+            document.addEventListener('keydown', handleKeyPress);
+            btn.addEventListener('click', () => {
+                document.removeEventListener('keydown', handleKeyPress);
+            });
+            
+            dialog.appendChild(msg);
+            dialog.appendChild(btn);
+            modal.appendChild(dialog);
+            document.body.appendChild(modal);
+            
+            // 点击背景关闭
+            modal.onclick = (e) => {
+                if (e.target === modal) {
+                    document.removeEventListener('keydown', handleKeyPress);
+                    handleConfirm();
+                }
+            };
+            
+            btn.focus();
+        }
+
+        async function postAudit(code, reason) {
+            const res = await fetch('/audit', {
+                method: 'POST',
+                headers: {'Content-Type': 'application/json'},
+                body: JSON.stringify({question_code: code, audit_reason: reason})
+            });
+            const result = await res.json();
+            if(result.success) {
+                // 自动跳转下一题
+                const nextBtn = document.getElementById('next-btn');
+                if(nextBtn) nextBtn.click();
+                else window.location.reload();
+            } else {
+                customAlert('审核失败: ' + result.error);
+            }
+        }
+
+        // 录入题目按钮处理函数
+    </script>
+</body>
+</html>
+
+
+
+
+
+
+
+

+ 14 - 0
templates/material_management.html

@@ -0,0 +1,14 @@
+{% extends "layout.html" %}
+
+{% block page_title %}资料管理{% endblock %}
+
+{% block content %}
+<div class="apple-card p-12 text-center">
+    <div class="max-w-md mx-auto">
+        <i class="ri-folder-line text-6xl text-gray-300 mb-4"></i>
+        <h2 class="text-2xl font-bold text-gray-800 mb-2">资料管理</h2>
+        <p class="text-gray-500">功能开发中...</p>
+    </div>
+</div>
+{% endblock %}
+

+ 89 - 0
templates/open_multiple_pdfs.html

@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>正在打开 PDF...</title>
+    <style>
+        body {
+            font-family: 'Microsoft YaHei', sans-serif;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            height: 100vh;
+            margin: 0;
+            background: #F5F5F7;
+        }
+        .container {
+            text-align: center;
+            padding: 2rem;
+        }
+        .spinner {
+            border: 4px solid #f3f3f3;
+            border-top: 4px solid #3498db;
+            border-radius: 50%;
+            width: 40px;
+            height: 40px;
+            animation: spin 1s linear infinite;
+            margin: 0 auto 1rem;
+        }
+        @keyframes spin {
+            0% { transform: rotate(0deg); }
+            100% { transform: rotate(360deg); }
+        }
+        .hidden {
+            display: none;
+        }
+    </style>
+</head>
+<body>
+    <div class="container">
+        <div class="spinner"></div>
+        <p>正在打开 PDF 文件...</p>
+    </div>
+    
+    <!-- 使用隐藏的 <a> 标签,通过自动点击来打开 PDF(避免浏览器拦截) -->
+    <div id="pdf-links" class="hidden">
+        {% for url in pdf_urls %}
+        <a href="{{ url }}" target="_blank" id="pdf-link-{{ loop.index0 }}"></a>
+        {% endfor %}
+    </div>
+    
+    <script>
+        // 使用 <a> 标签自动点击来打开 PDF(避免浏览器拦截)
+        const pdfUrls = {{ pdf_urls | tojson }};
+        console.log('PDF URLs:', pdfUrls);
+        
+        if (pdfUrls && pdfUrls.length > 0) {
+            // 立即打开第一个(用户点击触发的,不会被拦截)
+            const firstLink = document.getElementById('pdf-link-0');
+            if (firstLink) {
+                firstLink.click();
+            }
+            
+            // 延迟打开其他的(在用户交互的上下文中,通常不会被拦截)
+            pdfUrls.forEach((url, index) => {
+                if (index === 0) return; // 第一个已经打开了
+                
+                setTimeout(() => {
+                    const link = document.getElementById(`pdf-link-${index}`);
+                    if (link) {
+                        link.click();
+                    } else {
+                        // 如果找不到链接,尝试用 window.open(可能被拦截)
+                        window.open(url, '_blank');
+                    }
+                }, index * 200);
+            });
+            
+            // 2 秒后提示可以关闭
+            setTimeout(() => {
+                document.querySelector('.container p').textContent = 'PDF 已在新标签页打开,可以关闭此窗口';
+            }, 2000);
+        } else {
+            document.querySelector('.container p').textContent = '没有找到 PDF 文件';
+        }
+    </script>
+</body>
+</html>
+

+ 14 - 0
templates/paper_parser.html

@@ -0,0 +1,14 @@
+{% extends "layout.html" %}
+
+{% block page_title %}试卷解析工具{% endblock %}
+
+{% block content %}
+<div class="apple-card p-12 text-center">
+    <div class="max-w-md mx-auto">
+        <i class="ri-file-text-line text-6xl text-gray-300 mb-4"></i>
+        <h2 class="text-2xl font-bold text-gray-800 mb-2">试卷解析工具</h2>
+        <p class="text-gray-500">功能开发中...</p>
+    </div>
+</div>
+{% endblock %}
+

+ 3753 - 0
templates/question_management.html

@@ -0,0 +1,3753 @@
+{% extends "layout.html" %}
+
+{% block page_title %}题目管理{% endblock %}
+
+{% block content %}
+{% set is_audit_mode = is_audit_mode|default(false) %}
+<div class="flex gap-6">
+    <!-- 左侧目录索引 -->
+    <div class="w-96 flex-shrink-0">
+        <div class="apple-card p-6 sticky top-4 max-h-[calc(100vh-2rem)] overflow-y-auto">
+            <div class="flex items-center justify-between mb-6 pb-3 border-b border-gray-200">
+                <h2 class="text-lg font-bold text-gray-800">{% if is_audit_mode %}未审核知识点{% else %}知识点目录{% endif %}</h2>
+            </div>
+            <nav class="space-y-1">
+                {% macro render_kp_node(node, level) %}
+                <div class="kp-node-item mb-1" data-kp-code="{{ node.kp_code }}" data-level="{{ level }}">
+                    <div class="flex items-center gap-1 group">
+                        {% if node.children|length > 0 %}
+                        <button 
+                            onclick="toggleKpNode('{{ node.kp_code }}'); event.stopPropagation();"
+                            class="w-6 h-6 rounded flex items-center justify-center text-gray-500 hover:text-blue-600 hover:bg-blue-50 transition-all flex-shrink-0 kp-expand-btn"
+                            data-expanded="{% if level == 0 %}true{% else %}false{% endif %}"
+                            data-kp-code="{{ node.kp_code }}">
+                            {% if level == 0 %}
+                            <i class="ri-subtract-line text-sm"></i>
+                            {% else %}
+                            <i class="ri-add-line text-sm"></i>
+                            {% endif %}
+                        </button>
+                        {% else %}
+                        <div class="w-6 h-6 flex items-center justify-center flex-shrink-0">
+                            <div class="w-1.5 h-1.5 rounded-full bg-gray-400"></div>
+                    </div>
+                        {% endif %}
+                        <a href="javascript:void(0)" 
+                           onclick="loadQuestionsByKp('{{ node.kp_code }}', '{{ node.name }}', this); return false;"
+                           data-kp-code="{{ node.kp_code }}"
+                           data-kp-name="{{ node.name }}"
+                           class="flex-1 block px-3 py-2 rounded-lg hover:bg-gradient-to-r hover:from-blue-50 hover:to-indigo-50 transition-all border-l-2 border-transparent hover:border-blue-400 kp-link">
+                            <div class="flex items-center gap-2 flex-nowrap">
+                                {% if level == 0 %}
+                                <span class="bg-gradient-to-r from-slate-600 to-slate-700 text-white px-2 py-0.5 rounded text-xs font-bold shadow-sm flex-shrink-0">章</span>
+                                {% elif level == 1 %}
+                                <span class="bg-gradient-to-r from-blue-500 to-blue-600 text-white px-2 py-0.5 rounded text-xs font-bold shadow-sm flex-shrink-0">节</span>
+                                {% else %}
+                                <span class="bg-gray-200 text-gray-700 px-1.5 py-0.5 rounded text-xs font-medium flex-shrink-0">小节</span>
+                                {% endif %}
+                                <span class="text-sm {% if level == 0 %}font-bold text-gray-800{% elif level == 1 %}font-semibold text-gray-800{% else %}font-normal text-gray-600{% endif %} group-hover:text-blue-600 transition-colors flex-1 min-w-0 truncate">{{ node.name }}</span>
+                                {% set total_count = node.total_question_count|default(node.question_count|default(0)) %}
+                                {% if total_count > 0 %}
+                                <span class="bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full text-xs font-bold flex-shrink-0 ml-1">{{ total_count }}</span>
+                                {% endif %}
+                            </div>
+                        </a>
+                        </div>
+                    {% if node.children|length > 0 %}
+                    <div class="kp-children ml-8 mt-1 {% if level >= 1 %}hidden{% endif %}" id="kp-children-{{ node.kp_code }}" data-level="{{ level }}">
+                        {% for child in node.children %}
+                            {{ render_kp_node(child, level + 1) }}
+                        {% endfor %}
+                    </div>
+                    {% endif %}
+                </div>
+                {% endmacro %}
+                
+                <!-- 其他题目节点 -->
+                <div class="kp-node-item mb-2 pb-2 border-b border-gray-200" data-kp-code="null" data-level="-1">
+                    <a href="javascript:void(0)" 
+                       onclick="loadQuestionsByKp('null', '其他题目', this); return false;"
+                       data-kp-code="null"
+                       data-kp-name="其他题目"
+                       class="flex-1 block px-3 py-2 rounded-lg hover:bg-gradient-to-r hover:from-purple-50 hover:to-pink-50 transition-all border-l-2 border-transparent hover:border-purple-400 kp-link">
+                        <div class="flex items-center gap-2 flex-nowrap">
+                            <span class="bg-gradient-to-r from-purple-500 to-pink-500 text-white px-2 py-0.5 rounded text-xs font-bold shadow-sm flex-shrink-0">其他</span>
+                            <span class="text-sm font-bold text-gray-800 group-hover:text-purple-600 transition-colors flex-1 min-w-0 truncate">其他题目</span>
+                            {% if other_questions_count|default(0) > 0 %}
+                            <span class="bg-purple-100 text-purple-700 px-2 py-0.5 rounded-full text-xs font-bold flex-shrink-0 ml-1">{{ other_questions_count }}</span>
+                            {% endif %}
+                        </div>
+                    </a>
+                </div>
+                
+                {% if kp_tree %}
+                    {% for root_node in kp_tree %}
+                        {{ render_kp_node(root_node, 0) }}
+                {% endfor %}
+                {% else %}
+                    <div class="text-sm text-gray-500 text-center py-8">暂无知识点数据</div>
+                {% endif %}
+            </nav>
+        </div>
+    </div>
+    
+    <!-- 右侧题目列表区域 -->
+    <div class="flex-1">
+        <!-- 年级筛选栏 -->
+        <div class="mb-4 flex items-center gap-3">
+            <span class="text-sm font-semibold text-gray-700">年级筛选:</span>
+            <div class="flex items-center gap-2">
+                <button 
+                    id="grade-filter-all"
+                    onclick="setGradeFilter(null)"
+                    class="grade-filter-btn px-4 py-2 rounded-lg text-sm font-medium transition-all border-2 border-gray-300 bg-white text-gray-700 hover:bg-gray-50 active">
+                    全部
+                </button>
+                <button 
+                    id="grade-filter-1"
+                    onclick="setGradeFilter(1)"
+                    class="grade-filter-btn px-4 py-2 rounded-lg text-sm font-medium transition-all border-2 border-pink-300 bg-white text-pink-700 hover:bg-pink-50">
+                    小学
+                </button>
+                <button 
+                    id="grade-filter-2"
+                    onclick="setGradeFilter(2)"
+                    class="grade-filter-btn px-4 py-2 rounded-lg text-sm font-medium transition-all border-2 border-blue-300 bg-white text-blue-700 hover:bg-blue-50">
+                    初中
+                </button>
+                <button 
+                    id="grade-filter-3"
+                    onclick="setGradeFilter(3)"
+                    class="grade-filter-btn px-4 py-2 rounded-lg text-sm font-medium transition-all border-2 border-purple-300 bg-white text-purple-700 hover:bg-purple-50">
+                    高中
+                </button>
+            </div>
+            <span id="grade-filter-indicator" class="text-xs text-gray-500 ml-2 hidden">
+                <span id="grade-filter-text"></span>
+            </span>
+        </div>
+        
+        <div id="questions-container" class="space-y-4">
+            <div class="apple-card p-12 text-center">
+                <div class="text-gray-400 mb-4">
+                    <i class="ri-file-list-line text-6xl"></i>
+                </div>
+                <h3 class="text-lg font-bold text-gray-600 mb-2">请选择左侧知识点</h3>
+                <p class="text-sm text-gray-500 mb-6">点击知识点查看该知识点下的题目</p>
+                <div class="flex items-center justify-center gap-3">
+                    <button onclick="showAddQuestionModal(null, null)" 
+                            class="btn-apple bg-gradient-to-r from-blue-600 to-indigo-600 text-white hover:from-blue-700 hover:to-indigo-700 px-6 py-3 shadow-lg shadow-blue-200 flex items-center gap-2">
+                        <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
+                                </svg>
+                        录入题目
+                    </button>
+                    <button onclick="showBatchImportModal(null, null)" 
+                            class="btn-apple bg-gradient-to-r from-green-600 to-emerald-600 text-white hover:from-green-700 hover:to-emerald-700 px-6 py-3 shadow-lg shadow-green-200 flex items-center gap-2">
+                        <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/>
+                        </svg>
+                        批量导入
+                            </button>
+                        </div>
+            </div>
+        </div>
+    </div>
+                </div>
+
+<script>
+// 知识点树形数据(从服务器端传递)
+const kpTreeData = {{ kp_tree|tojson|safe }};
+
+// 当前选中的年级筛选(null表示全部,1=小学,2=初中,3=高中)
+let currentGradeFilter = null;
+
+// 设置年级筛选
+function setGradeFilter(grade) {
+    currentGradeFilter = grade;
+    
+    // 更新按钮样式
+    document.querySelectorAll('.grade-filter-btn').forEach(btn => {
+        btn.classList.remove('active', 'bg-pink-100', 'bg-blue-100', 'bg-purple-100', 'border-pink-500', 'border-blue-500', 'border-purple-500');
+        btn.classList.add('border-gray-300', 'bg-white');
+    });
+    
+    // 设置当前选中按钮的样式
+    const indicator = document.getElementById('grade-filter-indicator');
+    const indicatorText = document.getElementById('grade-filter-text');
+    
+    if (grade === null) {
+        document.getElementById('grade-filter-all').classList.add('active', 'bg-gray-100', 'border-gray-500');
+        indicator.classList.add('hidden');
+    } else {
+        const gradeNames = { 1: '小学', 2: '初中', 3: '高中' };
+        const gradeColors = {
+            1: { bg: 'bg-pink-100', border: 'border-pink-500' },
+            2: { bg: 'bg-blue-100', border: 'border-blue-500' },
+            3: { bg: 'bg-purple-100', border: 'border-purple-500' }
+        };
+        
+        const btn = document.getElementById(`grade-filter-${grade}`);
+        btn.classList.add('active', gradeColors[grade].bg, gradeColors[grade].border);
+        btn.classList.remove('border-gray-300', 'bg-white');
+        
+        indicator.classList.remove('hidden');
+        indicatorText.textContent = `当前筛选:${gradeNames[grade]}`;
+    }
+    
+    // 如果当前有加载的题目列表,重新加载(根据年级筛选)
+    if (currentKpCode) {
+        loadQuestionsByKp(currentKpCode, currentKpName);
+    }
+}
+
+// 页面加载时初始化年级筛选按钮样式
+document.addEventListener('DOMContentLoaded', function() {
+    // 默认选中"全部"
+    setGradeFilter(null);
+});
+
+// 知识点目录折叠/展开功能
+function toggleKpNode(kpCode) {
+    const childrenContainer = document.getElementById(`kp-children-${kpCode}`);
+    const expandBtn = document.querySelector(`.kp-expand-btn[data-kp-code="${kpCode}"]`);
+    
+    if (!childrenContainer || !expandBtn) return;
+    
+    const isExpanded = expandBtn.getAttribute('data-expanded') === 'true';
+    const icon = expandBtn.querySelector('i');
+    
+    if (isExpanded) {
+        // 折叠
+        childrenContainer.classList.add('hidden');
+        expandBtn.setAttribute('data-expanded', 'false');
+        if (icon) {
+            icon.className = 'ri-add-line text-sm';
+        }
+    } else {
+        // 展开
+        childrenContainer.classList.remove('hidden');
+        expandBtn.setAttribute('data-expanded', 'true');
+        if (icon) {
+            icon.className = 'ri-subtract-line text-sm';
+        }
+    }
+}
+
+// 存储当前知识点的代码和名称(用于构建返回链接)
+let currentKpCode = null;
+let currentKpName = null;
+
+// 加载指定知识点的题目列表(带页码)
+function loadQuestionsByKpPage(kpCode, page) {
+    // 更新URL参数,但不刷新页面
+    const url = new URL(window.location);
+    url.searchParams.set('kp_code', kpCode);
+    if (page > 1) {
+        url.searchParams.set('page', page);
+    } else {
+        url.searchParams.delete('page');
+    }
+    window.history.pushState({}, '', url);
+    
+    // 重新加载题目列表
+    const kpLink = document.querySelector(`.kp-link[data-kp-code="${kpCode}"]`);
+    const kpName = kpLink ? (kpLink.getAttribute('data-kp-name') || kpCode) : kpCode;
+    loadQuestionsByKp(kpCode, kpName, kpLink, page);
+}
+
+// 加载指定知识点的题目列表
+function loadQuestionsByKp(kpCode, kpName, linkElement, page = null) {
+    const container = document.getElementById('questions-container');
+    if (!container) return;
+    
+    // 保存当前知识点信息
+    currentKpCode = kpCode;
+    currentKpName = kpName;
+    
+    // 更新选中状态
+    document.querySelectorAll('.kp-link').forEach(link => {
+        link.classList.remove('bg-blue-50', 'border-blue-400');
+    });
+    if (linkElement) {
+        linkElement.classList.add('bg-blue-50', 'border-blue-400');
+    }
+    
+    // 显示加载状态
+    container.innerHTML = `
+        <div class="apple-card p-12 text-center">
+            <div class="text-blue-500 mb-4">
+                <i class="ri-loader-4-line text-6xl animate-spin"></i>
+            </div>
+            <p class="text-gray-600">正在加载题目...</p>
+        </div>
+    `;
+    
+    // 如果没有指定页码,从URL参数获取
+    if (page === null) {
+        const urlParams = new URLSearchParams(window.location.search);
+        const pageParam = urlParams.get('page');
+        page = pageParam ? parseInt(pageParam) : 1;
+    }
+    
+    // 构建请求URL(如果有年级筛选或页码,添加参数)
+    let apiUrl = `/api/questions_by_kp/${encodeURIComponent(kpCode)}`;
+    const params = [];
+    if (currentGradeFilter !== null) {
+        params.push(`grade=${currentGradeFilter}`);
+    }
+    if (page > 1) {
+        params.push(`page=${page}`);
+    }
+    if (params.length > 0) {
+        apiUrl += '?' + params.join('&');
+    }
+    
+    // 请求题目列表
+    fetch(apiUrl)
+        .then(response => response.json())
+        .then(data => {
+            if (!data.success) {
+                throw new Error(data.error || '加载失败');
+            }
+            
+            const questions = data.questions || [];
+            const kpName = data.kp_name || kpCode;
+            
+            // 获取统计数据(即使没有题目也要显示)
+            const stats = data.stats || {};
+            const auditStats = stats.audit || {pass: 0, fail: 0, pending: 0, pass_rate: 0, audit_rate: 0};
+            const difficultyStats = stats.difficulty || {jichu: 0, tifen: 0, peiyou: 0, unknown: 0};
+            
+            // 构建录入题目URL
+            const addQuestionUrl = `/add_question?kp_code=${encodeURIComponent(kpCode)}`;
+            
+            // 渲染题目列表
+            let html = '';
+            
+            // 如果是"其他题目",只显示按钮,不显示横幅和统计数据
+            if (kpCode === 'null' || kpCode === '') {
+                html = `
+                    <div class="mb-6 flex items-center gap-2 flex-wrap">
+                        <button onclick="showAddQuestionModal(null, null)" 
+                                class="bg-white text-blue-600 hover:bg-blue-50 px-4 py-2 rounded-lg text-sm font-semibold transition-all flex items-center justify-center gap-2 shadow-md whitespace-nowrap flex-shrink-0 border border-blue-200">
+                            <svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
+                            </svg>
+                            <span>录入题目</span>
+                        </button>
+                        <button onclick="showBatchImportModal(null, null)" 
+                                class="bg-white text-green-600 hover:bg-green-50 px-4 py-2 rounded-lg text-sm font-semibold transition-all flex items-center justify-center gap-2 shadow-md whitespace-nowrap flex-shrink-0 border border-green-200">
+                            <svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/>
+                            </svg>
+                            <span>批量导入</span>
+                        </button>
+                    </div>
+                `;
+            } else {
+                // 其他知识点显示完整的横幅和统计数据
+                html = `
+                <div class="mb-6">
+                    <div class="bg-gradient-to-r from-gray-800 via-slate-800 to-gray-900 text-white px-6 py-5 rounded-2xl shadow-lg">
+                        <div class="mb-4">
+                            <div class="flex items-center justify-between mb-3">
+                                <div class="flex-1 min-w-0">
+                                    <h2 class="text-xl font-bold mb-2 truncate">${kpName}</h2>
+                                    <p class="text-sm text-white/80">共 ${stats.total || questions.length} 道题目</p>
+                                </div>
+                            </div>
+                            <div class="flex items-center gap-2 flex-wrap">
+                                <button onclick="showAddQuestionModal('${kpCode}', '${kpName}')" 
+                                        class="bg-white text-blue-600 hover:bg-blue-50 px-4 py-2 rounded-lg text-sm font-semibold transition-all flex items-center justify-center gap-2 shadow-md whitespace-nowrap flex-shrink-0 border border-blue-200">
+                                    <svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
+                                    </svg>
+                                    <span>录入题目</span>
+                                </button>
+                                <button onclick="showBatchImportModal('${kpCode}', '${kpName}')" 
+                                        class="bg-white text-green-600 hover:bg-green-50 px-4 py-2 rounded-lg text-sm font-semibold transition-all flex items-center justify-center gap-2 shadow-md whitespace-nowrap flex-shrink-0 border border-green-200">
+                                    <svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/>
+                                    </svg>
+                                    <span>批量导入</span>
+                                </button>
+                            </div>
+                        </div>
+                        
+                        <!-- 统计数据 -->
+                        <div class="grid grid-cols-2 md:grid-cols-4 gap-4 mt-4 pt-4 border-t border-white/20">
+                            <!-- 审核统计 -->
+                            <div class="bg-white/10 backdrop-blur-sm rounded-lg p-3">
+                                <div class="text-xs text-white/70 mb-1">审核状态</div>
+                                <div class="space-y-1">
+                                    <div class="flex items-center justify-between text-xs">
+                                        <span class="text-white/90">合格</span>
+                                        <span class="font-bold text-green-200">${auditStats.pass || 0}</span>
+                    </div>
+                                    <div class="flex items-center justify-between text-xs">
+                                        <span class="text-white/90">不合格</span>
+                                        <span class="font-bold text-red-200">${auditStats.fail || 0}</span>
+                </div>
+                                    <div class="flex items-center justify-between text-xs">
+                                        <span class="text-white/90">待审核</span>
+                                        <span class="font-bold text-orange-200">${auditStats.pending || 0}</span>
+                                    </div>
+        </div>
+    </div>
+    
+                            <!-- 难度统计 -->
+                            <div class="bg-white/10 backdrop-blur-sm rounded-lg p-3">
+                                <div class="text-xs text-white/70 mb-1">难度分布</div>
+                                <div class="space-y-1">
+                                    <div class="flex items-center justify-between text-xs">
+                                        <span class="text-white/90">筑基</span>
+                                        <span class="font-bold text-green-200">${difficultyStats.jichu || 0}</span>
+                                    </div>
+                                    <div class="flex items-center justify-between text-xs">
+                                        <span class="text-white/90">提分</span>
+                                        <span class="font-bold text-yellow-200">${difficultyStats.tifen || 0}</span>
+                                    </div>
+                                    <div class="flex items-center justify-between text-xs">
+                                        <span class="text-white/90">培优</span>
+                                        <span class="font-bold text-orange-200">${difficultyStats.peiyou || 0}</span>
+                                    </div>
+                                    ${(difficultyStats.unknown || 0) > 0 ? `
+                                    <div class="flex items-center justify-between text-xs">
+                                        <span class="text-white/90">未设置</span>
+                                        <span class="font-bold text-gray-300">${difficultyStats.unknown || 0}</span>
+                                    </div>
+                                    ` : ''}
+                                </div>
+                            </div>
+            
+                            <!-- 审核率 -->
+                            <div class="bg-white/10 backdrop-blur-sm rounded-lg p-3">
+                                <div class="text-xs text-white/70 mb-1">审核进度</div>
+                                <div class="space-y-2">
+                                    <div>
+                                        <div class="flex items-center justify-between text-xs mb-1">
+                                            <span class="text-white/90">审核率</span>
+                                            <span class="font-bold text-white">${auditStats.audit_rate || 0}%</span>
+                        </div>
+                                        <div class="w-full h-1.5 bg-white/20 rounded-full overflow-hidden">
+                                            <div class="h-full bg-blue-300 rounded-full transition-all" style="width: ${auditStats.audit_rate || 0}%"></div>
+                    </div>
+                                    </div>
+                                    <div>
+                                        <div class="flex items-center justify-between text-xs mb-1">
+                                            <span class="text-white/90">通过率</span>
+                                            <span class="font-bold text-white">${auditStats.pass_rate || 0}%</span>
+                                        </div>
+                                        <div class="w-full h-1.5 bg-white/20 rounded-full overflow-hidden">
+                                            <div class="h-full bg-green-300 rounded-full transition-all" style="width: ${auditStats.pass_rate || 0}%"></div>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                            
+                            <!-- 题型统计 -->
+                            <div class="bg-white/10 backdrop-blur-sm rounded-lg p-3">
+                                <div class="text-xs text-white/70 mb-1">题型分布</div>
+                                <div class="space-y-1 max-h-20 overflow-y-auto">
+                                    ${Object.keys(stats.question_type || {}).length > 0 ? Object.entries(stats.question_type || {}).map(([type, count]) => {
+                                        // 题型映射:英文转中文
+                                        const typeMap = {
+                                            'choice': '选择题',
+                                            'fill': '填空题',
+                                            'answer': '解答题',
+                                            '选择题': '选择题',
+                                            '填空题': '填空题',
+                                            '解答题': '解答题'
+                                        };
+                                        const typeName = typeMap[type] || type;
+                                        return `
+                                            <div class="flex items-center justify-between text-xs">
+                                                <span class="text-white/90 truncate">${typeName}</span>
+                                                <span class="font-bold text-white ml-2">${count}</span>
+                        </div>
+                                        `;
+                                    }).join('') : '<div class="text-xs text-white/60">暂无数据</div>'}
+                            </div>
+                        </div>
+                    </div>
+                </div>
+                `;
+            }
+            
+            html += `<div class="space-y-3 ${kpCode === 'null' || kpCode === '' ? '' : 'mt-6'}">`;
+            
+            // 如果没有题目,显示提示信息
+            if (questions.length === 0) {
+                html += `
+                    <div class="apple-card p-12 text-center">
+                        <div class="text-gray-400 mb-4">
+                            <i class="ri-file-list-line text-6xl"></i>
+                        </div>
+                        <h3 class="text-lg font-bold text-gray-600 mb-2">该知识点下暂无题目</h3>
+                        <p class="text-sm text-gray-500 mb-6">点击上方"录入题目"或"批量导入"按钮开始添加题目</p>
+                    </div>
+                `;
+            } else {
+                // 获取分页信息(从API返回)
+                const pagination = data.pagination || {};
+                const currentPage = pagination.page || 1;
+                const totalPages = pagination.total_pages || 1;
+                const totalCount = pagination.total_count || questions.length;
+                const pageSize = pagination.page_size || 20;
+                const startIndex = (currentPage - 1) * pageSize;
+                const endIndex = Math.min(startIndex + pageSize, totalCount);
+                
+                // 有题目时,渲染题目列表
+                questions.forEach(q => {
+                    // 审核状态
+                    let auditBadge = '';
+                    if (q.audit_reason === '合格') {
+                        auditBadge = '<span class="bg-green-100 text-green-700 px-2 py-0.5 rounded text-xs font-bold whitespace-nowrap">合格</span>';
+                    } else if (q.audit_reason === '不合格') {
+                        auditBadge = '<span class="bg-red-100 text-red-700 px-2 py-0.5 rounded text-xs font-bold whitespace-nowrap">不合格</span>';
+                    } else {
+                        auditBadge = '<span class="bg-orange-100 text-orange-700 px-2 py-0.5 rounded text-xs font-bold whitespace-nowrap">待审核</span>';
+                    }
+                    
+                    // 难度标签
+                    let difficultyBadge = '';
+                    if (q.difficulty !== null && q.difficulty !== undefined) {
+                        const diff = parseFloat(q.difficulty);
+                        if (diff === 0.2 || Math.abs(diff - 0.2) < 0.1) {
+                            difficultyBadge = '<span class="px-2 py-0.5 rounded-full text-xs font-bold bg-green-100 text-green-700 border border-green-200 whitespace-nowrap">筑基</span>';
+                        } else if (diff === 0.4 || Math.abs(diff - 0.4) < 0.1) {
+                            difficultyBadge = '<span class="px-2 py-0.5 rounded-full text-xs font-bold bg-yellow-100 text-yellow-700 border border-yellow-200 whitespace-nowrap">提分</span>';
+                        } else if (diff === 0.7 || Math.abs(diff - 0.7) < 0.1) {
+                            difficultyBadge = '<span class="px-2 py-0.5 rounded-full text-xs font-bold bg-orange-100 text-orange-700 border border-orange-200 whitespace-nowrap">培优</span>';
+                        }
+                    }
+                    
+                    // 题型标签
+                    const typeMap = {'choice': '选择题', 'fill': '填空题', 'answer': '解答题'};
+                    const questionTypeText = typeMap[q.question_type] || q.question_type || '未分类';
+                    
+                    // 年级标签
+                    let gradeBadge = '';
+                    if (q.grade !== null && q.grade !== undefined) {
+                        const grade = parseInt(q.grade);
+                        if (grade === 1) {
+                            gradeBadge = '<span class="px-2 py-0.5 rounded-full text-xs font-bold bg-pink-100 text-pink-700 border border-pink-200 whitespace-nowrap">小学</span>';
+                        } else if (grade === 2) {
+                            gradeBadge = '<span class="px-2 py-0.5 rounded-full text-xs font-bold bg-blue-100 text-blue-700 border border-blue-200 whitespace-nowrap">初中</span>';
+                        } else if (grade === 3) {
+                            gradeBadge = '<span class="px-2 py-0.5 rounded-full text-xs font-bold bg-purple-100 text-purple-700 border border-purple-200 whitespace-nowrap">高中</span>';
+                        }
+                    }
+                    
+                    // 题干预览(去除HTML标签,只显示文本)
+                    const stemText = (q.stem || '').replace(/<[^>]*>/g, '').substring(0, 150);
+                    
+                    html += `
+                        <a href="/detail/${q.question_code}${currentKpCode ? '?kp_code=' + encodeURIComponent(currentKpCode) : ''}" 
+                           class="apple-card p-4 block group hover:shadow-lg transition-all border-l-4 border-transparent hover:border-blue-500">
+                            <div class="flex items-center gap-4">
+                                <!-- 左侧:题号 -->
+                                <div class="flex-shrink-0">
+                                    <span class="text-xs font-mono text-gray-500 bg-gray-100 px-3 py-1.5 rounded font-semibold">${q.question_code}</span>
+                                </div>
+                                
+                                <!-- 中间:题干内容 -->
+                                <div class="flex-1 min-w-0">
+                                    <div class="text-sm text-gray-800 group-hover:text-blue-600 transition-colors line-clamp-1">
+                                        ${stemText}${stemText.length >= 150 ? '...' : ''}
+                                    </div>
+                                </div>
+
+                                <!-- 右侧:标签和操作 -->
+                                <div class="flex items-center gap-3 flex-shrink-0">
+                                    <span class="text-xs text-gray-500 bg-gray-50 px-2 py-1 rounded">${questionTypeText}</span>
+                                    ${gradeBadge}
+                                    ${difficultyBadge}
+                                    ${auditBadge}
+                                    <span class="text-xs text-blue-600 font-medium opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap">查看详情 →</span>
+                                </div>
+                            </div>
+                        </a>
+                    `;
+                });
+                
+                // 添加翻页控件
+                if (totalPages > 1) {
+                    html += `
+                        <div class="mt-6 flex items-center justify-between">
+                            <div class="text-sm text-gray-600">
+                                显示第 ${startIndex + 1}-${endIndex} 题,共 ${totalCount} 题
+                            </div>
+                            <div class="flex items-center gap-2">
+                                <button 
+                                    onclick="loadQuestionsByKpPage('${kpCode}', ${currentPage - 1})"
+                                    ${currentPage === 1 ? 'disabled' : ''}
+                                    class="px-4 py-2 rounded-lg text-sm font-medium transition-all border-2 ${
+                                        currentPage === 1 
+                                            ? 'border-gray-200 bg-gray-50 text-gray-400 cursor-not-allowed' 
+                                            : 'border-blue-300 bg-white text-blue-700 hover:bg-blue-50'
+                                    }">
+                                    <i class="ri-arrow-left-s-line"></i> 上一页
+                                </button>
+                                
+                                <div class="flex items-center gap-1">
+                                    ${Array.from({length: totalPages}, (_, i) => i + 1).map(page => {
+                                        if (page === 1 || page === totalPages || (page >= currentPage - 2 && page <= currentPage + 2)) {
+                                            return `
+                                                <button 
+                                                    onclick="loadQuestionsByKpPage('${kpCode}', ${page})"
+                                                    class="px-3 py-2 rounded-lg text-sm font-medium transition-all border-2 ${
+                                                        page === currentPage
+                                                            ? 'border-blue-500 bg-blue-500 text-white'
+                                                            : 'border-gray-300 bg-white text-gray-700 hover:bg-gray-50'
+                                                    }">
+                                                    ${page}
+                                                </button>
+                                            `;
+                                        } else if (page === currentPage - 3 || page === currentPage + 3) {
+                                            return '<span class="px-2 text-gray-400">...</span>';
+                                        }
+                                        return '';
+                                    }).join('')}
+                                </div>
+                                
+                                <button 
+                                    onclick="loadQuestionsByKpPage('${kpCode}', ${currentPage + 1})"
+                                    ${currentPage === totalPages ? 'disabled' : ''}
+                                    class="px-4 py-2 rounded-lg text-sm font-medium transition-all border-2 ${
+                                        currentPage === totalPages 
+                                            ? 'border-gray-200 bg-gray-50 text-gray-400 cursor-not-allowed' 
+                                            : 'border-blue-300 bg-white text-blue-700 hover:bg-blue-50'
+                                    }">
+                                    下一页 <i class="ri-arrow-right-s-line"></i>
+                                </button>
+                            </div>
+                        </div>
+                    `;
+                }
+            }
+            
+            html += '</div>';
+            container.innerHTML = html;
+            
+            // 渲染数学公式
+            if (window.renderMathInElement) {
+                container.querySelectorAll('.math-render').forEach(el => {
+                    try {
+                        window.renderMathInElement(el, {
+                            delimiters: [
+                                {left: "$$", right: "$$", display: true},
+                                {left: "$", right: "$", display: false},
+                                {left: "\\(", right: "\\)", display: false},
+                                {left: "\\[", right: "\\]", display: true}
+                            ],
+                            throwOnError: false
+                        });
+                    } catch (e) {
+                        console.warn('数学公式渲染失败:', e);
+                    }
+                });
+            }
+        })
+        .catch(error => {
+            container.innerHTML = `
+                <div class="apple-card p-12 text-center">
+                    <div class="text-red-400 mb-4">
+                        <i class="ri-error-warning-line text-6xl"></i>
+                    </div>
+                    <h3 class="text-lg font-bold text-gray-600 mb-2">加载失败</h3>
+                    <p class="text-sm text-red-500">${error.message || '未知错误'}</p>
+                </div>
+            `;
+        });
+}
+
+// 页面加载时,设置进度条宽度和初始化知识点目录
+document.addEventListener('DOMContentLoaded', function() {
+    // 设置进度条宽度
+    document.querySelectorAll('.progress-gradient[data-width]').forEach(function(el) {
+        const width = parseFloat(el.getAttribute('data-width')) || 0;
+        el.style.width = width + '%';
+    });
+    
+    // 初始化知识点目录:默认展开第一级节点
+    const firstLevelNodes = document.querySelectorAll('.kp-node-item[data-level="0"]');
+    firstLevelNodes.forEach(node => {
+        const kpCode = node.getAttribute('data-kp-code');
+        const expandBtn = node.querySelector('.kp-expand-btn');
+        if (expandBtn && expandBtn.getAttribute('data-expanded') === 'true') {
+            const childrenContainer = document.getElementById(`kp-children-${kpCode}`);
+            if (childrenContainer) {
+                childrenContainer.classList.remove('hidden');
+            }
+        }
+    });
+    
+    // 如果 URL 中有锚点(hash),自动滚动到对应知识点
+    const hash = window.location.hash;
+    if (hash && hash.startsWith('#kp-')) {
+        const targetId = hash.substring(1); // 去掉 # 号
+        const targetElement = document.getElementById(targetId);
+        if (targetElement) {
+            // 延迟一下,确保页面完全渲染
+            setTimeout(() => {
+                targetElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
+                // 高亮一下,让用户知道定位到了
+                targetElement.style.transition = 'box-shadow 0.3s';
+                targetElement.style.boxShadow = '0 0 20px rgba(59, 130, 246, 0.5)';
+                setTimeout(() => {
+                    targetElement.style.boxShadow = '';
+                }, 2000);
+            }, 100);
+        }
+    }
+    
+    // 如果 URL 中有 kp_code 参数,自动加载对应知识点的题目列表
+    const urlParams = new URLSearchParams(window.location.search);
+    const kpCode = urlParams.get('kp_code');
+    if (kpCode) {
+        // 延迟一下,确保页面完全渲染
+        setTimeout(() => {
+            // 查找对应的知识点链接
+            const kpLink = document.querySelector(`.kp-link[data-kp-code="${kpCode}"]`);
+            if (kpLink) {
+                // 获取知识点名称(从链接文本或data属性)
+                const kpName = kpLink.getAttribute('data-kp-name') || kpLink.textContent.trim() || kpCode;
+                
+                // 展开父节点(如果需要)
+                let parent = kpLink.closest('.kp-node-item');
+                while (parent) {
+                    const parentKpCode = parent.getAttribute('data-kp-code');
+                    if (parentKpCode) {
+                        const expandBtn = parent.querySelector('.kp-expand-btn');
+                        if (expandBtn && expandBtn.getAttribute('data-expanded') === 'false') {
+                            toggleKpNode(parentKpCode);
+                        }
+                    }
+                    parent = parent.parentElement?.closest('.kp-node-item');
+                }
+                
+                // 滚动到知识点链接
+                setTimeout(() => {
+                    kpLink.scrollIntoView({ behavior: 'smooth', block: 'center' });
+                    // 加载题目列表
+                    loadQuestionsByKp(kpCode, kpName, kpLink);
+                }, 200);
+            } else {
+                // 如果找不到链接,直接加载题目(可能是通过API获取知识点名称)
+                fetch(`/api/questions_by_kp/${encodeURIComponent(kpCode)}`)
+                    .then(response => response.json())
+                    .then(data => {
+                        if (data.success) {
+                            const kpName = data.kp_name || kpCode;
+                            loadQuestionsByKp(kpCode, kpName, null);
+                        }
+                    })
+                    .catch(error => {
+                        console.error('加载知识点信息失败:', error);
+                        // 即使失败也尝试加载题目列表
+                        loadQuestionsByKp(kpCode, kpCode, null);
+                    });
+            }
+        }, 300);
+    }
+});
+
+// 显示添加知识点弹窗
+function showAddKpModal() {
+    const modal = document.createElement('div');
+    modal.id = 'add-kp-modal';
+    modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:10000;display:flex;align-items:center;justify-content:center;';
+    
+    const dialog = document.createElement('div');
+    dialog.style.cssText = 'background:white;padding:2rem;border-radius:16px;box-shadow:0 20px 60px rgba(0,0,0,0.3);max-width:500px;width:90%;';
+    
+    const title = document.createElement('div');
+    title.style.cssText = 'font-size:1.25rem;font-weight:bold;color:#333;margin-bottom:1.5rem;';
+    title.textContent = '添加知识点';
+    
+    // Chapter 选择
+    const chapterLabel = document.createElement('label');
+    chapterLabel.style.cssText = 'display:block;font-size:0.875rem;font-weight:600;color:#4b5563;margin-bottom:0.5rem;';
+    chapterLabel.textContent = '选择章节 *';
+    
+    const chapterSelect = document.createElement('select');
+    chapterSelect.id = 'add-kp-chapter-select';
+    chapterSelect.style.cssText = 'width:100%;padding:0.75rem;border:2px solid #e5e7eb;border-radius:8px;font-size:1rem;margin-bottom:1.5rem;outline:none;transition:border-color 0.2s;';
+    chapterSelect.innerHTML = '<option value="">请选择章节</option>';
+    
+    // 填充章节选项(从知识点树形数据中提取)
+    function extractChaptersFromTree(tree) {
+        const chapters = [];
+        tree.forEach(function(node) {
+            chapters.push({
+                id: node.kp_code,
+                label: node.name
+            });
+        });
+        return chapters;
+    }
+    
+    const chapters = extractChaptersFromTree(kpTreeData || []);
+    chapters.forEach(function(chapter) {
+        const option = document.createElement('option');
+        option.value = chapter.id;
+        option.textContent = chapter.label;
+        chapterSelect.appendChild(option);
+    });
+    
+    // 类型选择
+    const typeLabel = document.createElement('label');
+    typeLabel.style.cssText = 'display:block;font-size:0.875rem;font-weight:600;color:#4b5563;margin-bottom:0.5rem;';
+    typeLabel.textContent = '添加类型 *';
+    
+    const typeSelect = document.createElement('select');
+    typeSelect.id = 'add-kp-type-select';
+    typeSelect.style.cssText = 'width:100%;padding:0.75rem;border:2px solid #e5e7eb;border-radius:8px;font-size:1rem;margin-bottom:1.5rem;outline:none;transition:border-color 0.2s;';
+    typeSelect.innerHTML = '<option value="">请选择类型</option><option value="section">节 (Section)</option><option value="subsection">小节 (Subsection)</option>';
+    
+    // Section 选择(仅当添加 subsection 时显示)
+    const sectionLabel = document.createElement('label');
+    sectionLabel.id = 'add-kp-section-label';
+    sectionLabel.style.cssText = 'display:none;font-size:0.875rem;font-weight:600;color:#4b5563;margin-bottom:0.5rem;';
+    sectionLabel.textContent = '选择节 *';
+    
+    const sectionSelect = document.createElement('select');
+    sectionSelect.id = 'add-kp-section-select';
+    sectionSelect.style.cssText = 'display:none;width:100%;padding:0.75rem;border:2px solid #e5e7eb;border-radius:8px;font-size:1rem;margin-bottom:1.5rem;outline:none;transition:border-color 0.2s;';
+    sectionSelect.innerHTML = '<option value="">请选择节</option>';
+    
+    // 监听类型变化,更新 section 选择框
+    typeSelect.addEventListener('change', function() {
+        if (this.value === 'subsection') {
+            sectionLabel.style.display = 'block';
+            sectionSelect.style.display = 'block';
+            sectionSelect.required = true;
+            updateSectionOptions(chapterSelect.value);
+        } else {
+            sectionLabel.style.display = 'none';
+            sectionSelect.style.display = 'none';
+            sectionSelect.required = false;
+        }
+    });
+    
+    // 监听章节变化,更新 section 选项
+    chapterSelect.addEventListener('change', function() {
+        if (typeSelect.value === 'subsection') {
+            updateSectionOptions(this.value);
+        }
+    });
+    
+    function updateSectionOptions(chapterId) {
+        sectionSelect.innerHTML = '<option value="">请选择节</option>';
+        if (!chapterId) return;
+        
+        // 从 kpTreeData 中查找对应章节的子节点(sections)
+        function findNodeByCode(tree, code) {
+            for (let node of tree) {
+                if (node.kp_code === code) {
+                    return node;
+                }
+                if (node.children && node.children.length > 0) {
+                    const found = findNodeByCode(node.children, code);
+                    if (found) return found;
+                }
+            }
+            return null;
+        }
+        
+        const chapter = findNodeByCode(kpTreeData || [], chapterId);
+        if (chapter && chapter.children) {
+            chapter.children.forEach(function(section) {
+                const option = document.createElement('option');
+                option.value = section.kp_code;
+                option.textContent = section.name;
+                sectionSelect.appendChild(option);
+            });
+        }
+    }
+    
+    // 名称输入
+    const nameLabel = document.createElement('label');
+    nameLabel.style.cssText = 'display:block;font-size:0.875rem;font-weight:600;color:#4b5563;margin-bottom:0.5rem;';
+    nameLabel.textContent = '名称 *';
+    
+    const nameInput = document.createElement('input');
+    nameInput.type = 'text';
+    nameInput.id = 'add-kp-name-input';
+    nameInput.placeholder = '请输入名称';
+    nameInput.required = true;
+    nameInput.style.cssText = 'width:100%;padding:0.75rem;border:2px solid #e5e7eb;border-radius:8px;font-size:1rem;margin-bottom:1.5rem;outline:none;transition:border-color 0.2s;';
+    nameInput.addEventListener('focus', function() {
+        this.style.borderColor = '#3b82f6';
+    });
+    nameInput.addEventListener('blur', function() {
+        this.style.borderColor = '#e5e7eb';
+    });
+    
+    // 按钮容器
+    const btnContainer = document.createElement('div');
+    btnContainer.style.cssText = 'display:flex;gap:1rem;justify-content:flex-end;';
+    
+    const cancelBtn = document.createElement('button');
+    cancelBtn.style.cssText = 'background:#f3f4f6;color:#374151;border:none;padding:0.75rem 1.5rem;border-radius:8px;font-size:1rem;cursor:pointer;font-weight:600;transition:background 0.2s;';
+    cancelBtn.textContent = '取消';
+    cancelBtn.addEventListener('mouseenter', function() {
+        this.style.background = '#e5e7eb';
+    });
+    cancelBtn.addEventListener('mouseleave', function() {
+        this.style.background = '#f3f4f6';
+    });
+    
+    const confirmBtn = document.createElement('button');
+    confirmBtn.style.cssText = 'background:#3b82f6;color:white;border:none;padding:0.75rem 1.5rem;border-radius:8px;font-size:1rem;cursor:pointer;font-weight:600;transition:background 0.2s;';
+    confirmBtn.textContent = '确定';
+    confirmBtn.addEventListener('mouseenter', function() {
+        this.style.background = '#2563eb';
+    });
+    confirmBtn.addEventListener('mouseleave', function() {
+        this.style.background = '#3b82f6';
+    });
+    
+    const handleCancel = () => {
+        document.body.removeChild(modal);
+    };
+    
+    const handleConfirm = async () => {
+        const chapterId = chapterSelect.value;
+        const type = typeSelect.value;
+        const sectionId = sectionSelect.value;
+        const name = nameInput.value.trim();
+        
+        if (!chapterId || !type || !name) {
+            if (window.customAlert) {
+                window.customAlert('请填写所有必填项!');
+            } else {
+                alert('请填写所有必填项!');
+            }
+            return;
+        }
+        
+        if (type === 'subsection' && !sectionId) {
+            if (window.customAlert) {
+                window.customAlert('添加小节时必须选择节!');
+            } else {
+                alert('添加小节时必须选择节!');
+            }
+            return;
+        }
+        
+        // 禁用按钮
+        confirmBtn.disabled = true;
+        confirmBtn.textContent = '添加中...';
+        
+        try {
+            const response = await fetch('/add_kp_node', {
+                method: 'POST',
+                headers: {'Content-Type': 'application/json'},
+                body: JSON.stringify({
+                    chapter_id: parseInt(chapterId),
+                    type: type,
+                    section_id: sectionId ? parseInt(sectionId) : null,
+                    name: name
+                })
+            });
+            
+            const result = await response.json();
+            
+            if (result.success) {
+                if (window.customAlert) {
+                    window.customAlert('添加成功!页面即将刷新。', () => {
+                        window.location.reload();
+                    });
+                } else {
+                    alert('添加成功!');
+                    window.location.reload();
+                }
+            } else {
+                if (window.customAlert) {
+                    window.customAlert('添加失败: ' + result.error);
+                } else {
+                    alert('添加失败: ' + result.error);
+                }
+                confirmBtn.disabled = false;
+                confirmBtn.textContent = '确定';
+            }
+        } catch (error) {
+            console.error('添加失败:', error);
+            if (window.customAlert) {
+                window.customAlert('添加失败: ' + error.message);
+            } else {
+                alert('添加失败: ' + error.message);
+            }
+            confirmBtn.disabled = false;
+            confirmBtn.textContent = '确定';
+        }
+    };
+    
+    cancelBtn.onclick = handleCancel;
+    confirmBtn.onclick = handleConfirm;
+    
+    // 点击背景关闭
+    modal.onclick = (e) => {
+        if (e.target === modal) {
+            handleCancel();
+        }
+    };
+    
+    dialog.appendChild(title);
+    dialog.appendChild(chapterLabel);
+    dialog.appendChild(chapterSelect);
+    dialog.appendChild(typeLabel);
+    dialog.appendChild(typeSelect);
+    dialog.appendChild(sectionLabel);
+    dialog.appendChild(sectionSelect);
+    dialog.appendChild(nameLabel);
+    dialog.appendChild(nameInput);
+    btnContainer.appendChild(cancelBtn);
+    btnContainer.appendChild(confirmBtn);
+    dialog.appendChild(btnContainer);
+    modal.appendChild(dialog);
+    document.body.appendChild(modal);
+    
+    // 聚焦第一个输入框
+    setTimeout(() => chapterSelect.focus(), 100);
+}
+
+// 显示编辑知识点名称弹窗
+function showEditKpModal(nodeId, nodeType, currentName) {
+    const modal = document.createElement('div');
+    modal.id = 'edit-kp-modal';
+    modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:10000;display:flex;align-items:center;justify-content:center;';
+    
+    const dialog = document.createElement('div');
+    dialog.style.cssText = 'background:white;padding:2rem;border-radius:16px;box-shadow:0 20px 60px rgba(0,0,0,0.3);max-width:500px;width:90%;';
+    
+    const title = document.createElement('div');
+    title.style.cssText = 'font-size:1.25rem;font-weight:bold;color:#333;margin-bottom:1.5rem;';
+    const typeName = nodeType === 'chapter' ? '章节' : nodeType === 'section' ? '节' : '小节';
+    title.textContent = '编辑' + typeName + '名称';
+    
+    // 名称输入
+    const nameLabel = document.createElement('label');
+    nameLabel.style.cssText = 'display:block;font-size:0.875rem;font-weight:600;color:#4b5563;margin-bottom:0.5rem;';
+    nameLabel.textContent = '名称 *';
+    
+    const nameInput = document.createElement('input');
+    nameInput.type = 'text';
+    nameInput.id = 'edit-kp-name-input';
+    nameInput.value = currentName;
+    nameInput.required = true;
+    nameInput.style.cssText = 'width:100%;padding:0.75rem;border:2px solid #e5e7eb;border-radius:8px;font-size:1rem;margin-bottom:1.5rem;outline:none;transition:border-color 0.2s;';
+    nameInput.addEventListener('focus', function() {
+        this.style.borderColor = '#3b82f6';
+    });
+    nameInput.addEventListener('blur', function() {
+        this.style.borderColor = '#e5e7eb';
+    });
+    
+    // 按钮容器
+    const btnContainer = document.createElement('div');
+    btnContainer.style.cssText = 'display:flex;gap:1rem;justify-content:flex-end;';
+    
+    const cancelBtn = document.createElement('button');
+    cancelBtn.style.cssText = 'background:#f3f4f6;color:#374151;border:none;padding:0.75rem 1.5rem;border-radius:8px;font-size:1rem;cursor:pointer;font-weight:600;transition:background 0.2s;';
+    cancelBtn.textContent = '取消';
+    cancelBtn.addEventListener('mouseenter', function() {
+        this.style.background = '#e5e7eb';
+    });
+    cancelBtn.addEventListener('mouseleave', function() {
+        this.style.background = '#f3f4f6';
+    });
+    
+    const confirmBtn = document.createElement('button');
+    confirmBtn.style.cssText = 'background:#3b82f6;color:white;border:none;padding:0.75rem 1.5rem;border-radius:8px;font-size:1rem;cursor:pointer;font-weight:600;transition:background 0.2s;';
+    confirmBtn.textContent = '确定';
+    confirmBtn.addEventListener('mouseenter', function() {
+        this.style.background = '#2563eb';
+    });
+    confirmBtn.addEventListener('mouseleave', function() {
+        this.style.background = '#3b82f6';
+    });
+    
+    const handleCancel = () => {
+        document.body.removeChild(modal);
+    };
+    
+    const handleConfirm = async () => {
+        const name = nameInput.value.trim();
+        
+        if (!name) {
+            if (window.customAlert) {
+                window.customAlert('请输入名称!');
+            } else {
+                alert('请输入名称!');
+            }
+            return;
+        }
+        
+        // 禁用按钮
+        confirmBtn.disabled = true;
+        confirmBtn.textContent = '保存中...';
+        
+        try {
+            const response = await fetch('/update_kp_node', {
+                method: 'POST',
+                headers: {'Content-Type': 'application/json'},
+                body: JSON.stringify({
+                    node_id: nodeId,
+                    node_type: nodeType,
+                    name: name
+                })
+            });
+            
+            const result = await response.json();
+            
+            if (result.success) {
+                // 更新页面上的显示
+                const labelElement = document.getElementById(nodeType + '-label-' + nodeId);
+                if (labelElement) {
+                    labelElement.textContent = name;
+                }
+                
+                if (window.customAlert) {
+                    window.customAlert('修改成功!', () => {
+                        document.body.removeChild(modal);
+                    });
+                } else {
+                    alert('修改成功!');
+                    document.body.removeChild(modal);
+                }
+            } else {
+                if (window.customAlert) {
+                    window.customAlert('修改失败: ' + result.error);
+                } else {
+                    alert('修改失败: ' + result.error);
+                }
+                confirmBtn.disabled = false;
+                confirmBtn.textContent = '确定';
+            }
+        } catch (error) {
+            console.error('修改失败:', error);
+            if (window.customAlert) {
+                window.customAlert('修改失败: ' + error.message);
+            } else {
+                alert('修改失败: ' + error.message);
+            }
+            confirmBtn.disabled = false;
+            confirmBtn.textContent = '确定';
+        }
+    };
+    
+    cancelBtn.onclick = handleCancel;
+    confirmBtn.onclick = handleConfirm;
+    
+    // 点击背景关闭
+    modal.onclick = (e) => {
+        if (e.target === modal) {
+            handleCancel();
+        }
+    };
+    
+    // Enter 键确认
+    nameInput.onkeypress = function(e) {
+        if (e.key === 'Enter') {
+            handleConfirm();
+        }
+    };
+    
+    dialog.appendChild(title);
+    dialog.appendChild(nameLabel);
+    dialog.appendChild(nameInput);
+    btnContainer.appendChild(cancelBtn);
+    btnContainer.appendChild(confirmBtn);
+    dialog.appendChild(btnContainer);
+    modal.appendChild(dialog);
+    document.body.appendChild(modal);
+    
+    // 聚焦输入框并选中所有文本
+    setTimeout(() => {
+        nameInput.focus();
+        nameInput.select();
+    }, 100);
+}
+</script>
+
+<!-- 批量导入模态框 -->
+<div id="batch-import-modal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center" style="z-index: 10002;">
+    <div class="bg-white rounded-2xl shadow-2xl w-full max-w-6xl max-h-[95vh] overflow-hidden flex flex-col">
+        <div class="bg-gradient-to-r from-green-600 to-emerald-600 text-white px-6 py-4 flex items-center justify-between">
+            <h2 class="text-xl font-bold">批量导入题目</h2>
+            <button onclick="hideBatchImportModal()" class="text-white hover:text-gray-200 text-2xl leading-none w-8 h-8 flex items-center justify-center rounded-full hover:bg-white/20 transition-colors">×</button>
+        </div>
+        <div class="flex-1 overflow-hidden flex flex-col">
+            <!-- JSON输入区域 -->
+            <div class="p-6 border-b border-gray-200">
+                <label class="block text-sm font-bold text-gray-700 mb-2">粘贴JSON数据(每道题以number字段切分)</label>
+                <textarea id="batch-json-input" 
+                          class="w-full h-32 p-3 border border-gray-300 rounded-lg font-mono text-xs focus:border-green-500 focus:ring-2 focus:ring-green-500/10 outline-none"
+                          placeholder='[{"number": "1", "stem": "...", "options": {"A": "...", "B": "..."}, "answer": "A", "question_type": "选择题", "solution": "..."}, ...]'
+                          oninput="handleBatchJsonInputChange()"></textarea>
+                <p class="text-xs text-gray-500 mt-2">支持数组格式或单个对象格式,每道题必须包含number字段。粘贴后自动填充到下方表单</p>
+            </div>
+            
+            <!-- 题目卡片区域 -->
+            <div id="batch-preview" class="hidden flex-1 overflow-hidden flex flex-col">
+                <!-- 题目导航 -->
+                <div class="px-6 py-3 bg-gray-50 border-b border-gray-200 flex items-center justify-between">
+                    <div class="flex items-center gap-3">
+                        <button id="batch-prev-btn" onclick="switchBatchQuestion(-1)" class="btn-apple bg-gray-100 text-gray-700 hover:bg-gray-200 px-3 py-1.5 disabled:opacity-50 disabled:cursor-not-allowed" disabled>
+                            <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
+                            </svg>
+                        </button>
+                        <span class="text-sm font-medium text-gray-700">
+                            第 <span id="current-question-index">1</span> / <span id="total-question-count">0</span> 题
+                        </span>
+                        <button id="batch-next-btn" onclick="switchBatchQuestion(1)" class="btn-apple bg-gray-100 text-gray-700 hover:bg-gray-200 px-3 py-1.5 disabled:opacity-50 disabled:cursor-not-allowed">
+                            <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
+                            </svg>
+                        </button>
+                    </div>
+                    <div class="text-xs text-gray-500">题号: <span id="current-question-number">-</span></div>
+                </div>
+                
+                <!-- 题目表单卡片容器 -->
+                <div class="flex-1 overflow-y-auto p-6 relative">
+                    <div id="questions-list" class="relative"></div>
+                </div>
+            </div>
+        </div>
+        
+        <!-- LaTeX 实时预览气泡 -->
+        <div id="batch-latex-preview-bubble" class="fixed right-8 top-24 w-[420px] max-h-[75vh] bg-white rounded-2xl shadow-2xl border border-gray-200 z-[60] overflow-hidden flex flex-col hidden" style="box-shadow: 0 20px 60px rgba(0,0,0,0.15);">
+            <div class="bg-gradient-to-r from-blue-500 to-indigo-600 text-white px-4 py-3 flex items-center justify-between">
+                <span class="text-sm font-bold">题目预览</span>
+                <button type="button" onclick="hideBatchPreviewBubble()" class="text-white hover:text-gray-200 text-xl leading-none w-6 h-6 flex items-center justify-center rounded-full hover:bg-white/20 transition-colors">×</button>
+            </div>
+            <div id="batch-latex-preview-content" class="flex-1 p-6 overflow-y-auto bg-gray-50 text-base leading-relaxed" style="min-height: 200px;">
+                <p class="text-sm text-gray-400 text-center">题目预览</p>
+            </div>
+        </div>
+        <div class="border-t border-gray-200 px-6 py-4 flex items-center justify-end gap-3">
+            <button onclick="hideBatchImportModal()" class="btn-apple bg-gray-100 text-gray-700 hover:bg-gray-200 px-4 py-2">取消</button>
+            <button id="batch-submit-btn" onclick="submitBatchQuestions()" class="btn-apple bg-gradient-to-r from-green-600 to-emerald-600 text-white hover:from-green-700 hover:to-emerald-700 px-4 py-2 hidden">批量提交</button>
+        </div>
+    </div>
+</div>
+
+<!-- 录入题目模态框 -->
+<div id="add-question-modal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center" style="z-index: 10002;">
+    <div class="bg-white rounded-2xl shadow-2xl w-full max-w-7xl max-h-[95vh] overflow-hidden flex flex-col">
+        <div class="bg-gradient-to-r from-blue-600 to-indigo-600 text-white px-6 py-4 flex items-center justify-between">
+            <div>
+                <h2 class="text-xl font-bold">录入新题目</h2>
+                <div class="mt-1 flex items-center gap-2 text-sm text-white/90">
+                    <span id="add-question-kp-name" class="font-semibold"></span>
+                    <span class="text-white/70">|</span>
+                    <span class="font-mono text-xs bg-white/20 px-2 py-0.5 rounded" id="add-question-kp-code"></span>
+                </div>
+            </div>
+            <button onclick="hideAddQuestionModal()" class="text-white hover:text-gray-200 text-2xl leading-none w-8 h-8 flex items-center justify-center rounded-full hover:bg-white/20 transition-colors">×</button>
+        </div>
+        <div class="flex-1 overflow-hidden flex">
+            <!-- 左侧表单区域 -->
+            <div class="flex-1 overflow-y-auto p-6 border-r border-gray-200">
+                <form id="add-question-form" class="space-y-4">
+                    <p class="text-gray-400 text-xs mb-4">填写题目信息,保存后自动生成题号并创建新题目</p>
+                    
+                    <!-- 题型和难度选择 -->
+                    <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
+                        <div class="space-y-1">
+                            <label class="text-xs font-bold text-gray-400 uppercase">题型</label>
+                            <select name="question_type" id="add-question-type" class="w-full p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10 outline-none transition-all text-sm">
+                                <option value="choice" selected>选择题</option>
+                                <option value="fill">填空题</option>
+                                <option value="answer">解答题</option>
+                            </select>
+                        </div>
+                        <div class="space-y-1">
+                            <label class="text-xs font-bold text-gray-400 uppercase">难度</label>
+                            <div class="flex items-center gap-2">
+                                <select name="difficulty" id="add-question-difficulty" class="flex-1 p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10 outline-none transition-all text-sm">
+                                    <option value="">请选择难度</option>
+                                    <option value="0.2">筑基</option>
+                                    <option value="0.4">提分</option>
+                                    <option value="0.7">培优</option>
+                                </select>
+                                <button type="button" id="add-evaluate-difficulty-btn" onclick="evaluateAddQuestionDifficulty()" class="btn-apple bg-gradient-to-r from-purple-500 to-indigo-600 text-white hover:from-purple-600 hover:to-indigo-700 text-xs py-2 px-3 shadow-md whitespace-nowrap">
+                                    <svg class="w-3 h-3 inline-block mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
+                                    </svg>
+                                    难度评价
+                                </button>
+                            </div>
+                        </div>
+                    </div>
+                    
+                    <!-- 年级选择(所有题目必填) -->
+                    <div id="add-question-grade-section" class="space-y-1">
+                        <label class="text-xs font-bold text-gray-400 uppercase">年级 <span class="text-red-500">*</span></label>
+                        <select name="grade" id="add-question-grade" required class="w-full p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10 outline-none transition-all text-sm">
+                            <option value="">请选择年级</option>
+                            <option value="1">小学</option>
+                            <option value="2">初中</option>
+                            <option value="3">高中</option>
+                        </select>
+                    </div>
+                
+                <!-- 题干编辑 -->
+                <div class="space-y-2">
+                    <div class="flex items-center justify-between">
+                        <label class="text-xs font-bold text-gray-400 uppercase">题干 <span class="text-red-500">*</span> (支持 LaTeX 和 HTML/SVG)</label>
+                        <button type="button" id="add-upload-stem-btn" class="btn-apple bg-blue-600 text-white hover:bg-blue-700 text-xs py-1.5 px-3 h-fit">上传图片</button>
+                    </div>
+                    <textarea name="stem" id="add-question-stem" required
+                              class="w-full h-32 p-3 rounded-xl bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10 outline-none transition-all font-mono text-sm" 
+                              placeholder="请输入题干内容或拖拽图片..."></textarea>
+                </div>
+                
+                <!-- 选项编辑(仅选择题显示) -->
+                <div id="add-options-section" class="space-y-2">
+                    <label class="text-xs font-bold text-gray-400 uppercase">选项</label>
+                    <div class="grid grid-cols-2 gap-3">
+                        <div class="space-y-1">
+                            <label class="text-xs font-medium text-gray-600">选项 A</label>
+                            <div class="flex gap-1.5">
+                                <textarea id="add-option-A" class="flex-1 h-12 p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-1 focus:ring-blue-500/10 outline-none transition-all text-xs" placeholder="输入选项A或拖拽图片..."></textarea>
+                                <button type="button" onclick="uploadAddQuestionOptionImage('A')" class="btn-apple bg-blue-600 text-white hover:bg-blue-700 text-xs py-1.5 px-2 h-fit self-start">上传</button>
+                            </div>
+                        </div>
+                        <div class="space-y-1">
+                            <label class="text-xs font-medium text-gray-600">选项 B</label>
+                            <div class="flex gap-1.5">
+                                <textarea id="add-option-B" class="flex-1 h-12 p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-1 focus:ring-blue-500/10 outline-none transition-all text-xs" placeholder="输入选项B或拖拽图片..."></textarea>
+                                <button type="button" onclick="uploadAddQuestionOptionImage('B')" class="btn-apple bg-blue-600 text-white hover:bg-blue-700 text-xs py-1.5 px-2 h-fit self-start">上传</button>
+                            </div>
+                        </div>
+                        <div class="space-y-1">
+                            <label class="text-xs font-medium text-gray-600">选项 C</label>
+                            <div class="flex gap-1.5">
+                                <textarea id="add-option-C" class="flex-1 h-12 p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-1 focus:ring-blue-500/10 outline-none transition-all text-xs" placeholder="输入选项C或拖拽图片..."></textarea>
+                                <button type="button" onclick="uploadAddQuestionOptionImage('C')" class="btn-apple bg-blue-600 text-white hover:bg-blue-700 text-xs py-1.5 px-2 h-fit self-start">上传</button>
+                            </div>
+                        </div>
+                        <div class="space-y-1">
+                            <label class="text-xs font-medium text-gray-600">选项 D</label>
+                            <div class="flex gap-1.5">
+                                <textarea id="add-option-D" class="flex-1 h-12 p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-1 focus:ring-blue-500/10 outline-none transition-all text-xs" placeholder="输入选项D或拖拽图片..."></textarea>
+                                <button type="button" onclick="uploadAddQuestionOptionImage('D')" class="btn-apple bg-blue-600 text-white hover:bg-blue-700 text-xs py-1.5 px-2 h-fit self-start">上传</button>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="mt-2 p-2 rounded-lg bg-gray-50 border border-gray-200">
+                        <label class="text-xs font-bold text-gray-600 mb-1 block">选项预览 (JSON格式,可直接编辑)</label>
+                        <textarea id="add-options-preview" class="w-full text-xs text-gray-700 font-mono bg-white p-2 rounded border border-gray-100 min-h-[80px] focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10 outline-none transition-all resize-y"
+                            placeholder='{"A": "选项A内容", "B": "选项B内容", ...}'></textarea>
+                    </div>
+                </div>
+                
+                <!-- 答案 -->
+                <div class="space-y-1">
+                    <label class="text-xs font-bold text-gray-400 uppercase">正确答案</label>
+                    <input type="text" name="answer" id="add-question-answer" class="w-full p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10 outline-none transition-all text-sm" placeholder="例如: A">
+                </div>
+                
+                <!-- 解析 -->
+                <div class="space-y-2">
+                    <div class="flex items-center justify-between">
+                        <label class="text-xs font-bold text-gray-400 uppercase">解析</label>
+                        <button type="button" onclick="uploadAddQuestionSolutionImage()" class="btn-apple bg-blue-600 text-white hover:bg-blue-700 text-xs py-1.5 px-3 h-fit">上传图片</button>
+                    </div>
+                    <textarea name="solution" id="add-question-solution" class="w-full h-32 p-3 rounded-xl bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10 outline-none transition-all font-mono text-sm" placeholder="请输入解析内容或拖拽图片..."></textarea>
+                </div>
+            </form>
+            </div>
+            
+            <!-- 右侧预览区域 -->
+            <div class="w-[420px] bg-gray-50 border-l border-gray-200 flex flex-col">
+                <div id="add-question-preview-content" class="flex-1 p-6 overflow-y-auto bg-gray-50 text-base leading-relaxed">
+                    <p class="text-sm text-gray-400 text-center">题目预览</p>
+                </div>
+            </div>
+        </div>
+        <div class="border-t border-gray-200 px-6 py-4 flex items-center justify-between">
+            <button onclick="checkDuplicateForAddQuestion()" class="btn-apple bg-gradient-to-r from-orange-500 to-red-500 text-white hover:from-orange-600 hover:to-red-600 px-4 py-2 flex items-center justify-center gap-2 whitespace-nowrap">
+                <svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
+                </svg>
+                <span>查重检测</span>
+            </button>
+            <div class="flex items-center gap-3">
+                <button onclick="hideAddQuestionModal()" class="btn-apple bg-gray-100 text-gray-700 hover:bg-gray-200 px-4 py-2">取消</button>
+                <button onclick="submitAddQuestion()" class="btn-apple bg-gradient-to-r from-blue-600 to-indigo-600 text-white hover:from-blue-700 hover:to-indigo-700 px-4 py-2">保存并创建</button>
+            </div>
+        </div>
+    </div>
+</div>
+
+<!-- 题目比对弹窗 -->
+<div id="duplicate-comparison-modal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center" style="z-index: 10003;">
+    <div class="bg-white rounded-2xl shadow-2xl w-full max-w-7xl max-h-[95vh] overflow-hidden flex flex-col">
+        <div class="bg-gradient-to-r from-orange-600 to-red-600 text-white px-6 py-4 flex items-center justify-between">
+            <div>
+                <h2 class="text-xl font-bold">题目查重比对</h2>
+                <p id="duplicate-similarity-info" class="text-sm text-white/90 mt-1"></p>
+            </div>
+            <button onclick="hideDuplicateComparisonModal()" class="text-white hover:text-gray-200 text-2xl leading-none w-8 h-8 flex items-center justify-center rounded-full hover:bg-white/20 transition-colors">×</button>
+        </div>
+        <div class="flex-1 overflow-hidden flex">
+            <!-- 左侧:当前录入的题目 -->
+            <div class="flex-1 overflow-y-auto p-6 border-r border-gray-200">
+                <div class="mb-4">
+                    <h3 class="text-lg font-bold text-gray-800 mb-2">当前录入的题目</h3>
+                </div>
+                <div id="duplicate-current-question" class="space-y-4">
+                    <!-- 动态填充 -->
+                </div>
+            </div>
+            <!-- 右侧:库中重复的题目 -->
+            <div class="flex-1 overflow-y-auto p-6 bg-gray-50">
+                <div class="mb-4">
+                    <h3 class="text-lg font-bold text-gray-800 mb-2">库中重复的题目</h3>
+                </div>
+                <div id="duplicate-existing-question" class="space-y-4">
+                    <!-- 动态填充 -->
+                </div>
+            </div>
+        </div>
+        <div class="border-t border-gray-200 px-6 py-4 flex items-center justify-end">
+            <button onclick="hideDuplicateComparisonModal()" class="btn-apple bg-gray-100 text-gray-700 hover:bg-gray-200 px-6 py-2">关闭</button>
+        </div>
+    </div>
+</div>
+
+<script>
+// 批量导入相关变量
+let parsedQuestions = [];
+let currentBatchQuestionIndex = 0;
+let batchJsonSyncTimer = null;
+let isSyncingBatchFromJson = false;
+let currentBatchKpCode = null;
+let currentBatchKpName = null;
+
+function showBatchImportModal(kpCode, kpName) {
+    // 处理 'null' 字符串,转换为 null
+    if (kpCode === 'null' || kpCode === null) {
+        currentBatchKpCode = null;
+        currentBatchKpName = '其他题目(未关联知识点)';
+    } else {
+        currentBatchKpCode = kpCode;
+        currentBatchKpName = kpName || '未指定知识点';
+    }
+    // 保存是否是"其他题目"的状态,用于后续渲染时显示年级选择
+    window.isBatchOtherQuestions = !kpCode || kpCode === 'null' || kpCode === '';
+    document.getElementById('batch-import-modal').classList.remove('hidden');
+    document.getElementById('batch-json-input').focus();
+}
+
+function hideBatchImportModal() {
+    document.getElementById('batch-import-modal').classList.add('hidden');
+    document.getElementById('batch-json-input').value = '';
+    document.getElementById('batch-preview').classList.add('hidden');
+    document.getElementById('batch-submit-btn').classList.add('hidden');
+    parsedQuestions = [];
+    currentBatchQuestionIndex = 0;
+}
+
+function handleBatchJsonInputChange() {
+    const jsonInput = document.getElementById('batch-json-input');
+    const jsonText = jsonInput.value.trim();
+    
+    if (jsonText) {
+        try {
+            JSON.parse(jsonText);
+            jsonInput.classList.remove('border-red-500');
+            jsonInput.classList.add('border-gray-300');
+            
+            clearTimeout(batchJsonSyncTimer);
+            batchJsonSyncTimer = setTimeout(() => {
+                if (!isSyncingBatchFromJson) {
+                    parseBatchJson();
+                }
+            }, 800);
+        } catch (e) {
+            jsonInput.classList.remove('border-gray-300');
+            jsonInput.classList.add('border-red-500');
+        }
+    }
+}
+
+function parseBatchJson() {
+    const jsonInput = document.getElementById('batch-json-input');
+    const jsonText = jsonInput.value.trim();
+    
+    if (!jsonText) return;
+    
+    try {
+        isSyncingBatchFromJson = true;
+        let data = JSON.parse(jsonText);
+        if (!Array.isArray(data)) {
+            data = [data];
+        }
+        
+        parsedQuestions = data.filter(q => q && q.number);
+        if (parsedQuestions.length === 0) return;
+        
+        currentBatchQuestionIndex = 0;
+        displayBatchPreview();
+    } catch (error) {
+        console.warn('JSON格式错误:', error);
+    } finally {
+        isSyncingBatchFromJson = false;
+    }
+}
+
+function switchBatchQuestion(direction) {
+    const newIndex = currentBatchQuestionIndex + direction;
+    if (newIndex >= 0 && newIndex < parsedQuestions.length) {
+        currentBatchQuestionIndex = newIndex;
+        showBatchQuestion(currentBatchQuestionIndex);
+        updateBatchNavigation();
+    }
+}
+
+function updateBatchNavigation() {
+    const prevBtn = document.getElementById('batch-prev-btn');
+    const nextBtn = document.getElementById('batch-next-btn');
+    const currentIndexSpan = document.getElementById('current-question-index');
+    const totalCountSpan = document.getElementById('total-question-count');
+    const questionNumberSpan = document.getElementById('current-question-number');
+    
+    if (prevBtn && nextBtn && currentIndexSpan && totalCountSpan) {
+        currentIndexSpan.textContent = currentBatchQuestionIndex + 1;
+        totalCountSpan.textContent = parsedQuestions.length;
+        prevBtn.disabled = currentBatchQuestionIndex === 0;
+        nextBtn.disabled = currentBatchQuestionIndex === parsedQuestions.length - 1;
+        if (questionNumberSpan && parsedQuestions[currentBatchQuestionIndex]) {
+            questionNumberSpan.textContent = parsedQuestions[currentBatchQuestionIndex].number || currentBatchQuestionIndex + 1;
+        }
+    }
+}
+
+function showBatchQuestion(index) {
+    const questionsList = document.getElementById('questions-list');
+    if (!questionsList) return;
+    
+    questionsList.querySelectorAll('.batch-question-card').forEach(card => card.classList.add('hidden'));
+    const currentCard = document.getElementById(`batch-q-card-${index}`);
+    if (currentCard) currentCard.classList.remove('hidden');
+    updateBatchFullPreview();
+}
+
+function hideBatchPreviewBubble() {
+    const bubble = document.getElementById('batch-latex-preview-bubble');
+    if (bubble) bubble.classList.add('hidden');
+}
+
+function updateBatchFullPreview() {
+    const previewContent = document.getElementById('batch-latex-preview-content');
+    const bubble = document.getElementById('batch-latex-preview-bubble');
+    if (!previewContent || !bubble) return;
+    
+    // 获取当前显示的题目卡片
+    const currentCard = document.getElementById(`batch-q-card-${currentBatchQuestionIndex}`);
+    if (!currentCard) return;
+    
+    // 收集当前题目的所有数据
+    const stemTextarea = currentCard.querySelector('.batch-stem');
+    const answerInput = currentCard.querySelector('.batch-answer');
+    const solutionTextarea = currentCard.querySelector('.batch-solution');
+    const optionsPreview = currentCard.querySelector('.batch-options-preview');
+    const questionTypeSelect = currentCard.querySelector('.batch-question-type');
+    const difficultySelect = currentCard.querySelector('.batch-difficulty');
+    
+    let html = '<div class="space-y-6">';
+    
+    // 题型和难度信息
+    html += '<div class="border-b border-gray-200 pb-3 mb-3">';
+    html += '<div class="flex items-center gap-3 text-sm">';
+    const typeMap = {'choice': '选择题', 'fill': '填空题', 'answer': '解答题'};
+    const questionType = questionTypeSelect ? questionTypeSelect.value : 'choice';
+    html += `<span class="bg-blue-100 text-blue-700 px-2 py-0.5 rounded text-xs font-bold">${typeMap[questionType] || questionType}</span>`;
+    if (difficultySelect && difficultySelect.value) {
+        const diffMap = {'0.2': '筑基', '0.4': '提分', '0.7': '培优'};
+        html += `<span class="bg-purple-100 text-purple-700 px-2 py-0.5 rounded text-xs font-bold">${diffMap[difficultySelect.value] || difficultySelect.value}</span>`;
+    }
+    html += '</div>';
+    html += '</div>';
+    
+    // 题干预览
+    html += '<div class="border-b border-gray-200 pb-4">';
+    html += '<div class="text-xs font-bold text-gray-500 mb-2">题干</div>';
+    const stem = stemTextarea ? stemTextarea.value : '';
+    if (stem) {
+        // 先提取图片标签,替换为占位符
+        const imagePlaceholders = [];
+        let stemProcessed = stem.replace(/<image\s+src="([^"]+)"\s*\/?>/gi, (match, url) => {
+            const placeholder = `__IMAGE_PLACEHOLDER_${imagePlaceholders.length}__`;
+            imagePlaceholders.push(url);
+            return placeholder;
+        });
+        
+        // 转义HTML
+        let stemHtml = stemProcessed
+            .replace(/&/g, '&amp;')
+            .replace(/</g, '&lt;')
+            .replace(/>/g, '&gt;')
+            .replace(/\n/g, '<br>');
+        
+        // 将占位符替换为img标签
+        imagePlaceholders.forEach((url, index) => {
+            const placeholder = `__IMAGE_PLACEHOLDER_${index}__`;
+            const imgTag = `<img src="${url}" class="max-w-[400px] max-h-[300px] w-auto h-auto my-2 rounded-lg mx-auto block" alt="题目图片" style="object-fit: contain;">`;
+            stemHtml = stemHtml.replace(placeholder, imgTag);
+        });
+        
+        html += `<div class="text-sm leading-relaxed">${stemHtml}</div>`;
+    } else {
+        html += '<p class="text-xs text-gray-400">暂无内容</p>';
+    }
+    html += '</div>';
+    
+    // 选项预览(仅选择题)
+    if (questionTypeSelect && questionTypeSelect.value === 'choice') {
+        html += '<div class="border-b border-gray-200 pb-4">';
+        html += '<div class="text-xs font-bold text-gray-500 mb-2">选项</div>';
+        
+        // 直接从选项输入框读取内容,确保图片标签能正确显示
+        let hasOptions = false;
+        ['A', 'B', 'C', 'D'].forEach(key => {
+            const optionInput = currentCard.querySelector(`.batch-option-${key}`);
+            const optionText = optionInput ? optionInput.value : '';
+            
+            if (optionText && optionText.trim()) {
+                hasOptions = true;
+                html += `<div class="mb-3">`;
+                html += `<div class="text-xs font-bold text-gray-500 mb-1">选项 ${key}</div>`;
+                
+                // 处理选项文本,支持图片标签
+                // 先提取图片标签,替换为占位符
+                const optionImagePlaceholders = [];
+                let optionProcessed = optionText.replace(/<image\s+src="([^"]+)"\s*\/?>/gi, (match, url) => {
+                    const placeholder = `__OPTION_IMAGE_PLACEHOLDER_${optionImagePlaceholders.length}__`;
+                    optionImagePlaceholders.push(url);
+                    return placeholder;
+                });
+                
+                // 转义HTML
+                let optionHtml = optionProcessed
+                    .replace(/&/g, '&amp;')
+                    .replace(/</g, '&lt;')
+                    .replace(/>/g, '&gt;')
+                    .replace(/\n/g, '<br>');
+                
+                // 将占位符替换为img标签
+                optionImagePlaceholders.forEach((url, index) => {
+                    const placeholder = `__OPTION_IMAGE_PLACEHOLDER_${index}__`;
+                    const imgTag = `<img src="${url}" class="max-w-[400px] max-h-[300px] w-auto h-auto my-2 rounded-lg mx-auto block" alt="题目图片" style="object-fit: contain;">`;
+                    optionHtml = optionHtml.replace(placeholder, imgTag);
+                });
+                
+                html += `<div class="text-sm leading-relaxed">${optionHtml}</div>`;
+                html += `</div>`;
+            }
+        });
+        
+        if (!hasOptions) {
+            html += '<p class="text-xs text-gray-400">暂无选项</p>';
+        }
+        html += '</div>';
+    }
+    
+    // 答案预览
+    html += '<div class="border-b border-gray-200 pb-4">';
+    html += '<div class="text-xs font-bold text-gray-500 mb-2">正确答案</div>';
+    const answer = answerInput ? answerInput.value : '';
+    if (answer) {
+        html += `<div class="text-sm font-bold text-blue-600">${escapeHtml(answer)}</div>`;
+    } else {
+        html += '<p class="text-xs text-gray-400">暂无答案</p>';
+    }
+    html += '</div>';
+    
+    // 解析预览
+    html += '<div>';
+    html += '<div class="text-xs font-bold text-gray-500 mb-2">解析</div>';
+    const solution = solutionTextarea ? solutionTextarea.value : '';
+    if (solution) {
+        // 先提取图片标签,替换为占位符
+        const solutionImagePlaceholders = [];
+        let solutionProcessed = solution.replace(/<image\s+src="([^"]+)"\s*\/?>/gi, (match, url) => {
+            const placeholder = `__SOLUTION_IMAGE_PLACEHOLDER_${solutionImagePlaceholders.length}__`;
+            solutionImagePlaceholders.push(url);
+            return placeholder;
+        });
+        
+        // 转义HTML
+        let solutionHtml = solutionProcessed
+            .replace(/&/g, '&amp;')
+            .replace(/</g, '&lt;')
+            .replace(/>/g, '&gt;')
+            .replace(/\n/g, '<br>');
+        
+        // 将占位符替换为img标签
+        solutionImagePlaceholders.forEach((url, index) => {
+            const placeholder = `__SOLUTION_IMAGE_PLACEHOLDER_${index}__`;
+            const imgTag = `<img src="${url}" class="max-w-[400px] max-h-[300px] w-auto h-auto my-2 rounded-lg mx-auto block" alt="题目图片" style="object-fit: contain;">`;
+            solutionHtml = solutionHtml.replace(placeholder, imgTag);
+        });
+        
+        html += `<div class="text-sm leading-relaxed">${solutionHtml}</div>`;
+    } else {
+        html += '<p class="text-xs text-gray-400">暂无解析</p>';
+    }
+    html += '</div>';
+    
+    html += '</div>';
+    
+    previewContent.innerHTML = html;
+    
+    // 渲染LaTeX
+    if (window.renderMathInElement) {
+        try {
+            window.renderMathInElement(previewContent, {
+                delimiters: [
+                    {left: "$$", right: "$$", display: true},
+                    {left: "$", right: "$", display: false},
+                    {left: "\\(", right: "\\)", display: false},
+                    {left: "\\[", right: "\\]", display: true}
+                ],
+                throwOnError: false
+            });
+        } catch (e) {
+            console.warn('LaTeX 渲染失败:', e);
+        }
+    }
+    
+    // 显示预览气泡
+    bubble.classList.remove('hidden');
+}
+
+function displayBatchPreview() {
+    const previewDiv = document.getElementById('batch-preview');
+    const questionsList = document.getElementById('questions-list');
+    
+    previewDiv.classList.remove('hidden');
+    document.getElementById('batch-submit-btn').classList.remove('hidden');
+    questionsList.innerHTML = '';
+    
+    // 创建所有题目卡片(默认隐藏)
+    parsedQuestions.forEach((q, index) => {
+        const questionId = `batch-q-card-${index}`;
+        const questionDiv = document.createElement('div');
+        questionDiv.className = `batch-question-card apple-card p-6 space-y-4 ${index === 0 ? '' : 'hidden'}`;
+        questionDiv.id = questionId;
+        questionDiv.setAttribute('data-index', index);
+        
+        // 解析选项
+        let optionsJson = '{}';
+        let optionA = '', optionB = '', optionC = '', optionD = '';
+        if (q.options && typeof q.options === 'object') {
+            optionsJson = JSON.stringify(q.options, null, 2);
+            optionA = q.options.A || '';
+            optionB = q.options.B || '';
+            optionC = q.options.C || '';
+            optionD = q.options.D || '';
+        }
+        
+        // 映射题型
+        const questionType = mapQuestionType(q.question_type);
+        
+        questionDiv.innerHTML = `
+            <div class="border-b border-gray-100 pb-3 mb-3">
+                <div class="flex items-center gap-2">
+                    <span class="bg-blue-100 text-blue-700 px-2 py-0.5 rounded text-xs font-bold">题号 ${q.number || index + 1}</span>
+                </div>
+            </div>
+            
+            <!-- 题型和难度选择 -->
+            <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
+                <div class="space-y-1">
+                    <label class="text-xs font-bold text-gray-400 uppercase">题型</label>
+                    <select name="question_type" class="batch-question-type w-full p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10 outline-none transition-all text-sm">
+                        <option value="choice" ${questionType === 'choice' ? 'selected' : ''}>选择题</option>
+                        <option value="fill" ${questionType === 'fill' ? 'selected' : ''}>填空题</option>
+                        <option value="answer" ${questionType === 'answer' ? 'selected' : ''}>解答题</option>
+                    </select>
+                </div>
+                <div class="space-y-1">
+                    <label class="text-xs font-bold text-gray-400 uppercase">难度</label>
+                    <div class="flex items-center gap-2">
+                        <select name="difficulty" class="batch-difficulty flex-1 p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10 outline-none transition-all text-sm">
+                            <option value="">请选择难度</option>
+                            <option value="0.2" ${q.difficulty == 0.2 || q.difficulty == '0.2' ? 'selected' : ''}>筑基</option>
+                            <option value="0.4" ${q.difficulty == 0.4 || q.difficulty == '0.4' ? 'selected' : ''}>提分</option>
+                            <option value="0.7" ${q.difficulty == 0.7 || q.difficulty == '0.7' ? 'selected' : ''}>培优</option>
+                        </select>
+                        <button type="button" class="batch-evaluate-difficulty-btn btn-apple bg-gradient-to-r from-purple-500 to-indigo-600 text-white hover:from-purple-600 hover:to-indigo-700 text-xs py-2 px-3 shadow-md whitespace-nowrap" data-index="${index}" onclick="evaluateBatchDifficulty(${index})">
+                            <svg class="w-3 h-3 inline-block mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
+                            </svg>
+                            难度评价
+                        </button>
+                    </div>
+                </div>
+            </div>
+            
+            <!-- 年级选择(所有题目必填) -->
+            <div class="batch-grade-section space-y-1">
+                <label class="text-xs font-bold text-gray-400 uppercase">年级 <span class="text-red-500">*</span></label>
+                <select name="grade" class="batch-grade w-full p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10 outline-none transition-all text-sm" required>
+                    <option value="">请选择年级</option>
+                    <option value="1" ${q.grade == 1 || q.grade == '1' || (currentGradeFilter === 1 && !q.grade) ? 'selected' : ''}>小学</option>
+                    <option value="2" ${q.grade == 2 || q.grade == '2' || (currentGradeFilter === 2 && !q.grade) ? 'selected' : ''}>初中</option>
+                    <option value="3" ${q.grade == 3 || q.grade == '3' || (currentGradeFilter === 3 && !q.grade) ? 'selected' : ''}>高中</option>
+                </select>
+            </div>
+            
+            <!-- 题干编辑 -->
+            <div class="space-y-2">
+                <div class="flex items-center justify-between">
+                    <label class="text-xs font-bold text-gray-400 uppercase">题干 <span class="text-red-500">*</span> (支持 LaTeX 和 HTML/SVG)</label>
+                    <button type="button" class="batch-upload-stem btn-apple bg-blue-600 text-white hover:bg-blue-700 text-xs py-1.5 px-3 h-fit" data-index="${index}">上传图片</button>
+                </div>
+                <textarea name="stem" class="batch-stem w-full h-32 p-3 rounded-xl bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10 outline-none transition-all font-mono text-sm" 
+                    placeholder="请输入题干内容或拖拽图片..."
+                    data-index="${index}"
+                    data-type="stem"
+                    required>${escapeHtml(q.stem || '')}</textarea>
+            </div>
+            
+            <!-- 选项编辑 -->
+            <div class="batch-options-section space-y-2" style="display: ${questionType === 'choice' ? 'block' : 'none'}">
+                <label class="text-xs font-bold text-gray-400 uppercase">选项</label>
+                <div class="grid grid-cols-2 gap-3">
+                    <div class="option-item">
+                        <label class="text-xs font-medium text-gray-600 mb-1 block">选项 A</label>
+                        <div class="flex gap-1.5">
+                            <textarea class="batch-option-A flex-1 h-12 p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-1 focus:ring-blue-500/10 outline-none transition-all text-xs"
+                                placeholder="输入选项A的内容或拖拽图片..."
+                                data-index="${index}" data-option="A" data-type="option">${escapeHtml(optionA)}</textarea>
+                            <button type="button" class="batch-upload-option btn-apple bg-blue-600 text-white hover:bg-blue-700 text-xs py-1.5 px-2 h-fit self-start" data-index="${index}" data-option="A">上传图片</button>
+                        </div>
+                    </div>
+                    <div class="option-item">
+                        <label class="text-xs font-medium text-gray-600 mb-1 block">选项 B</label>
+                        <div class="flex gap-1.5">
+                            <textarea class="batch-option-B flex-1 h-12 p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-1 focus:ring-blue-500/10 outline-none transition-all text-xs"
+                                placeholder="输入选项B的内容或拖拽图片..."
+                                data-index="${index}" data-option="B" data-type="option">${escapeHtml(optionB)}</textarea>
+                            <button type="button" class="batch-upload-option btn-apple bg-blue-600 text-white hover:bg-blue-700 text-xs py-1.5 px-2 h-fit self-start" data-index="${index}" data-option="B">上传图片</button>
+                        </div>
+                    </div>
+                    <div class="option-item">
+                        <label class="text-xs font-medium text-gray-600 mb-1 block">选项 C</label>
+                        <div class="flex gap-1.5">
+                            <textarea class="batch-option-C flex-1 h-12 p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-1 focus:ring-blue-500/10 outline-none transition-all text-xs"
+                                placeholder="输入选项C的内容或拖拽图片..."
+                                data-index="${index}" data-option="C" data-type="option">${escapeHtml(optionC)}</textarea>
+                            <button type="button" class="batch-upload-option btn-apple bg-blue-600 text-white hover:bg-blue-700 text-xs py-1.5 px-2 h-fit self-start" data-index="${index}" data-option="C">上传图片</button>
+                        </div>
+                    </div>
+                    <div class="option-item">
+                        <label class="text-xs font-medium text-gray-600 mb-1 block">选项 D</label>
+                        <div class="flex gap-1.5">
+                            <textarea class="batch-option-D flex-1 h-12 p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-1 focus:ring-blue-500/10 outline-none transition-all text-xs"
+                                placeholder="输入选项D的内容或拖拽图片..."
+                                data-index="${index}" data-option="D" data-type="option">${escapeHtml(optionD)}</textarea>
+                            <button type="button" class="batch-upload-option btn-apple bg-blue-600 text-white hover:bg-blue-700 text-xs py-1.5 px-2 h-fit self-start" data-index="${index}" data-option="D">上传图片</button>
+                        </div>
+                    </div>
+                </div>
+                <div class="mt-2 p-2 rounded-lg bg-gray-50 border border-gray-200">
+                    <label class="text-xs font-bold text-gray-600 mb-1 block">选项预览 (JSON格式,可直接编辑)</label>
+                    <textarea class="batch-options-preview w-full text-xs text-gray-700 font-mono bg-white p-2 rounded border border-gray-100 min-h-[80px] focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10 outline-none transition-all resize-y"
+                        data-index="${index}"
+                        placeholder='{"A": "选项A内容", "B": "选项B内容", ...}'>${escapeHtml(optionsJson)}</textarea>
+                </div>
+            </div>
+            
+            <!-- 答案 -->
+            <div class="space-y-1">
+                <label class="text-xs font-bold text-gray-400 uppercase">正确答案</label>
+                <input type="text" name="answer" class="batch-answer w-full p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10 outline-none transition-all text-sm" 
+                    placeholder="例如: A" value="${escapeHtml(q.answer || '')}">
+            </div>
+            
+            <!-- 解析 -->
+            <div class="space-y-2">
+                <div class="flex items-center justify-between">
+                    <label class="text-xs font-bold text-gray-400 uppercase">解析</label>
+                    <button type="button" class="batch-upload-solution btn-apple bg-blue-600 text-white hover:bg-blue-700 text-xs py-1.5 px-3 h-fit" data-index="${index}">上传图片</button>
+                </div>
+                <textarea name="solution" class="batch-solution w-full h-32 p-3 rounded-xl bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10 outline-none transition-all font-mono text-sm" 
+                    placeholder="请输入解析内容或拖拽图片..."
+                    data-index="${index}"
+                    data-type="solution">${escapeHtml(q.solution || '')}</textarea>
+            </div>
+            
+            <!-- 查重检测按钮 -->
+            <div class="pt-4 border-t border-gray-200">
+                <button onclick="checkDuplicateForBatch(${index})" class="btn-apple bg-gradient-to-r from-orange-500 to-red-500 text-white hover:from-orange-600 hover:to-red-600 px-4 py-2 w-full flex items-center justify-center gap-2 whitespace-nowrap">
+                    <svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
+                    </svg>
+                    <span>查重检测</span>
+                </button>
+            </div>
+        `;
+        
+        questionsList.appendChild(questionDiv);
+        
+        // 设置选项同步逻辑
+        setupBatchQuestionEvents(index);
+    });
+    
+    // 显示当前题并更新导航(不重置索引)
+    showBatchQuestion(currentBatchQuestionIndex);
+    updateBatchNavigation();
+    
+    // 初始化预览
+    updateBatchFullPreview();
+}
+
+function escapeHtml(text) {
+    if (!text) return '';
+    const div = document.createElement('div');
+    div.textContent = text;
+    return div.innerHTML;
+}
+
+function mapQuestionType(type) {
+    if (!type) return 'choice';
+    const typeMap = {
+        '选择题': 'choice', '填空题': 'fill', '解答题': 'answer',
+        'choice': 'choice', 'fill': 'fill', 'answer': 'answer'
+    };
+    return typeMap[type] || 'choice';
+}
+
+function setupBatchQuestionEvents(index) {
+    // 题型变化时显示/隐藏选项
+    const questionDiv = document.getElementById(`batch-q-card-${index}`);
+    if (!questionDiv) return;
+    
+    const typeSelect = questionDiv.querySelector('.batch-question-type');
+    const optionsSection = questionDiv.querySelector('.batch-options-section');
+    
+    typeSelect.addEventListener('change', function() {
+        if (this.value === 'choice') {
+            optionsSection.style.display = 'block';
+        } else {
+            optionsSection.style.display = 'none';
+        }
+        updateBatchFullPreview();
+    });
+    
+    // 选项输入框同步到预览
+    ['A', 'B', 'C', 'D'].forEach(opt => {
+        const input = questionDiv.querySelector(`.batch-option-${opt}`);
+        if (input) {
+            input.addEventListener('input', function() {
+                updateBatchOptionsPreview(index);
+                updateBatchFullPreview();
+            });
+        }
+    });
+    
+    // 预览框同步到输入框
+    const previewTextarea = questionDiv.querySelector('.batch-options-preview');
+    if (previewTextarea) {
+        previewTextarea.addEventListener('input', function() {
+            syncBatchOptionsFromPreview(index);
+            updateBatchFullPreview();
+        });
+    }
+    
+    // 题干、答案、解析字段变化时更新预览和拖拽上传
+    const stemTextarea = questionDiv.querySelector('.batch-stem');
+    if (stemTextarea) {
+        stemTextarea.addEventListener('input', updateBatchFullPreview);
+        // 拖拽上传功能
+        stemTextarea.addEventListener('dragover', function(e) {
+            e.preventDefault();
+            e.stopPropagation();
+            this.classList.add('border-blue-500', 'bg-blue-50');
+        });
+        stemTextarea.addEventListener('dragleave', function(e) {
+            e.preventDefault();
+            e.stopPropagation();
+            this.classList.remove('border-blue-500', 'bg-blue-50');
+        });
+        stemTextarea.addEventListener('drop', async function(e) {
+            e.preventDefault();
+            e.stopPropagation();
+            this.classList.remove('border-blue-500', 'bg-blue-50');
+            const files = e.dataTransfer.files;
+            if (files && files.length > 0 && files[0].type.startsWith('image/')) {
+                await uploadBatchImageFile(files[0], index, 'stem');
+            }
+        });
+    }
+    
+    const answerInput = questionDiv.querySelector('.batch-answer');
+    if (answerInput) {
+        answerInput.addEventListener('input', updateBatchFullPreview);
+    }
+    
+    const solutionTextarea = questionDiv.querySelector('.batch-solution');
+    if (solutionTextarea) {
+        solutionTextarea.addEventListener('input', updateBatchFullPreview);
+        // 拖拽上传功能
+        solutionTextarea.addEventListener('dragover', function(e) {
+            e.preventDefault();
+            e.stopPropagation();
+            this.classList.add('border-blue-500', 'bg-blue-50');
+        });
+        solutionTextarea.addEventListener('dragleave', function(e) {
+            e.preventDefault();
+            e.stopPropagation();
+            this.classList.remove('border-blue-500', 'bg-blue-50');
+        });
+        solutionTextarea.addEventListener('drop', async function(e) {
+            e.preventDefault();
+            e.stopPropagation();
+            this.classList.remove('border-blue-500', 'bg-blue-50');
+            const files = e.dataTransfer.files;
+            if (files && files.length > 0 && files[0].type.startsWith('image/')) {
+                await uploadBatchImageFile(files[0], index, 'solution');
+            }
+        });
+    }
+    
+    const difficultySelect = questionDiv.querySelector('.batch-difficulty');
+    if (difficultySelect) {
+        difficultySelect.addEventListener('change', updateBatchFullPreview);
+    }
+    
+    // 图片上传按钮
+    const uploadStemBtn = questionDiv.querySelector('.batch-upload-stem');
+    if (uploadStemBtn) {
+        uploadStemBtn.addEventListener('click', function() {
+            uploadBatchImage(index, 'stem');
+        });
+    }
+    
+    ['A', 'B', 'C', 'D'].forEach(opt => {
+        const uploadBtn = questionDiv.querySelector(`.batch-upload-option[data-option="${opt}"]`);
+        if (uploadBtn) {
+            uploadBtn.addEventListener('click', function() {
+                uploadBatchImage(index, 'option', opt);
+            });
+        }
+    });
+    
+    const uploadSolutionBtn = questionDiv.querySelector('.batch-upload-solution');
+    if (uploadSolutionBtn) {
+        uploadSolutionBtn.addEventListener('click', function() {
+            uploadBatchImage(index, 'solution');
+        });
+    }
+    
+    // 选项拖拽上传功能
+    ['A', 'B', 'C', 'D'].forEach(opt => {
+        const optionInput = questionDiv.querySelector(`.batch-option-${opt}`);
+        if (optionInput) {
+            optionInput.addEventListener('dragover', function(e) {
+                e.preventDefault();
+                e.stopPropagation();
+                this.classList.add('border-blue-500', 'bg-blue-50');
+            });
+            optionInput.addEventListener('dragleave', function(e) {
+                e.preventDefault();
+                e.stopPropagation();
+                this.classList.remove('border-blue-500', 'bg-blue-50');
+            });
+            optionInput.addEventListener('drop', async function(e) {
+                e.preventDefault();
+                e.stopPropagation();
+                this.classList.remove('border-blue-500', 'bg-blue-50');
+                const files = e.dataTransfer.files;
+                if (files && files.length > 0 && files[0].type.startsWith('image/')) {
+                    await uploadBatchImageFile(files[0], index, 'option', opt);
+                }
+            });
+        }
+    });
+    
+    // 粘贴图片功能
+    const allInputs = questionDiv.querySelectorAll('textarea, input[type="text"]');
+    allInputs.forEach(input => {
+        input.addEventListener('paste', async function(e) {
+            const clipboardData = e.clipboardData || window.clipboardData;
+            if (!clipboardData) {
+                return;
+            }
+            
+            // 检查是否有图片数据
+            const items = clipboardData.items;
+            if (!items) {
+                return;
+            }
+            
+            for (let i = 0; i < items.length; i++) {
+                const item = items[i];
+                
+                // 如果是图片类型
+                if (item.type.indexOf('image') !== -1) {
+                    e.preventDefault(); // 阻止默认粘贴行为
+                    
+                    const file = item.getAsFile();
+                    if (file) {
+                        // 直接上传图片并插入到当前输入框
+                        await uploadBatchImageToInput(file, input, index);
+                    }
+                    break;
+                }
+            }
+        });
+    });
+}
+
+function updateBatchOptionsPreview(index) {
+    const questionDiv = document.getElementById(`batch-q-card-${index}`);
+    const previewTextarea = questionDiv.querySelector('.batch-options-preview');
+    const optionsObj = {};
+    
+    ['A', 'B', 'C', 'D'].forEach(opt => {
+        const input = questionDiv.querySelector(`.batch-option-${opt}`);
+        if (input && input.value.trim()) {
+            optionsObj[opt] = input.value.trim();
+        }
+    });
+    
+    previewTextarea.value = JSON.stringify(optionsObj, null, 2);
+}
+
+function syncBatchOptionsFromPreview(index) {
+    const questionDiv = document.getElementById(`batch-q-card-${index}`);
+    const previewTextarea = questionDiv.querySelector('.batch-options-preview');
+    
+    try {
+        const optionsObj = JSON.parse(previewTextarea.value.trim() || '{}');
+        ['A', 'B', 'C', 'D'].forEach(opt => {
+            const input = questionDiv.querySelector(`.batch-option-${opt}`);
+            if (input) {
+                input.value = optionsObj[opt] || '';
+            }
+        });
+    } catch (error) {
+        console.warn('选项预览 JSON 格式错误:', error);
+    }
+}
+
+function uploadBatchImage(index, type, option = null) {
+    const fileInput = document.createElement('input');
+    fileInput.type = 'file';
+    fileInput.accept = 'image/*';
+    fileInput.style.display = 'none';
+    
+    fileInput.onchange = async (e) => {
+        const file = e.target.files[0];
+        if (!file) return;
+        
+        if (!file.type.startsWith('image/')) {
+            if (window.customAlert) {
+                window.customAlert('请选择图片文件!');
+            } else {
+                alert('请选择图片文件!');
+            }
+            return;
+        }
+        
+        await uploadBatchImageFile(file, index, type, option);
+    };
+    
+    document.body.appendChild(fileInput);
+    fileInput.click();
+    document.body.removeChild(fileInput);
+}
+
+// 通用图片上传函数:上传图片并插入到指定输入框
+async function uploadBatchImageToInput(file, inputElement, index) {
+    if (!inputElement || !file) {
+        return;
+    }
+    
+    // 检查文件类型
+    if (!file.type.startsWith('image/')) {
+        if (window.customAlert) {
+            window.customAlert('请选择图片文件!');
+        } else {
+            alert('请选择图片文件!');
+        }
+        return;
+    }
+    
+    // 显示上传中状态
+    const originalPlaceholder = inputElement.placeholder || '';
+    const originalOpacity = inputElement.style.opacity || '1';
+    inputElement.placeholder = '正在上传图片...';
+    inputElement.style.opacity = '0.6';
+    inputElement.disabled = true;
+    
+    try {
+        const formData = new FormData();
+        formData.append('file', file);
+        
+        const response = await fetch('https://crmapi.dcjxb.yunzhixue.cn/file/upload', {
+            method: 'POST',
+            body: formData
+        });
+        
+        if (!response.ok) {
+            throw new Error(`上传失败: ${response.status} ${response.statusText}`);
+        }
+        
+        const result = await response.json();
+        
+        // 检查返回结果,提取URL
+        let imageUrl = null;
+        if (typeof result === 'string') {
+            imageUrl = result;
+        } else if (result.url) {
+            imageUrl = result.url;
+        } else if (result.data && result.data.url) {
+            imageUrl = result.data.url;
+        } else if (result.data && typeof result.data === 'string') {
+            imageUrl = result.data;
+        } else {
+            const resultStr = JSON.stringify(result);
+            const urlMatch = resultStr.match(/https?:\/\/[^\s"']+/);
+            if (urlMatch) {
+                imageUrl = urlMatch[0];
+            }
+        }
+        
+        if (!imageUrl) {
+            throw new Error('接口返回格式异常,未找到图片URL。返回结果:' + JSON.stringify(result));
+        }
+        
+        // 构建 image 标签
+        const imageTag = `<image src="${imageUrl}"/>`;
+        
+        // 获取当前光标位置
+        const cursorPos = inputElement.selectionStart || inputElement.value.length;
+        const textBefore = inputElement.value.substring(0, cursorPos);
+        const textAfter = inputElement.value.substring(cursorPos);
+        
+        // 插入 image 标签
+        inputElement.value = textBefore + imageTag + textAfter;
+        
+        // 设置光标位置到插入内容之后
+        const newCursorPos = cursorPos + imageTag.length;
+        if (inputElement.setSelectionRange) {
+            inputElement.setSelectionRange(newCursorPos, newCursorPos);
+        }
+        inputElement.focus();
+        
+        // 触发input事件,更新预览
+        inputElement.dispatchEvent(new Event('input', { bubbles: true }));
+        
+        // 如果是选项输入框,更新选项预览
+        if (inputElement.classList.contains('batch-option-A') || 
+            inputElement.classList.contains('batch-option-B') ||
+            inputElement.classList.contains('batch-option-C') ||
+            inputElement.classList.contains('batch-option-D')) {
+            updateBatchOptionsPreview(index);
+        } else if (inputElement.classList.contains('batch-options-preview')) {
+            // 如果是在选项预览框中粘贴,需要同步到各个选项输入框
+            syncBatchOptionsFromPreview(index);
+        }
+        
+        // 更新完整预览
+        updateBatchFullPreview();
+        
+    } catch (error) {
+        console.error('上传失败:', error);
+        if (window.customAlert) {
+            window.customAlert('图片上传失败: ' + error.message);
+        } else {
+            alert('图片上传失败: ' + error.message);
+        }
+    } finally {
+        inputElement.placeholder = originalPlaceholder;
+        inputElement.style.opacity = originalOpacity;
+        inputElement.disabled = false;
+    }
+}
+
+// 保留旧函数以兼容拖拽上传
+async function uploadBatchImageFile(file, index, type, option) {
+    const questionDiv = document.getElementById(`batch-q-card-${index}`);
+    if (!questionDiv) {
+        questionDiv = document.querySelector(`[data-index="${index}"]`);
+    }
+    if (!questionDiv) return;
+    
+    let targetInput;
+    
+    if (type === 'stem') {
+        targetInput = questionDiv.querySelector('.batch-stem');
+    } else if (type === 'solution') {
+        targetInput = questionDiv.querySelector('.batch-solution');
+    } else if (type === 'answer') {
+        targetInput = questionDiv.querySelector('.batch-answer');
+    } else if (type === 'options-preview') {
+        targetInput = questionDiv.querySelector('.batch-options-preview');
+    } else if (type === 'option' && option) {
+        targetInput = questionDiv.querySelector(`.batch-option-${option}`);
+    }
+    
+    if (!targetInput) return;
+    
+    // 使用新的通用函数
+    await uploadBatchImageToInput(file, targetInput, index);
+}
+
+// 批量导入的难度评价函数
+async function evaluateBatchDifficulty(index) {
+    const questionDiv = document.getElementById(`batch-q-card-${index}`);
+    if (!questionDiv) return;
+    
+    const btn = questionDiv.querySelector('.batch-evaluate-difficulty-btn');
+    const difficultySelect = questionDiv.querySelector('.batch-difficulty');
+    
+    if (!btn || !difficultySelect) return;
+    
+    // 收集题目信息
+    const stemTextarea = questionDiv.querySelector('.batch-stem');
+    const answerInput = questionDiv.querySelector('.batch-answer');
+    const solutionTextarea = questionDiv.querySelector('.batch-solution');
+    const optionsPreview = questionDiv.querySelector('.batch-options-preview');
+    const questionTypeSelect = questionDiv.querySelector('.batch-question-type');
+    
+    const stem = stemTextarea ? stemTextarea.value.trim() : '';
+    
+    if (!stem) {
+        if (window.customAlert) {
+            window.customAlert('请先填写题干内容');
+        } else {
+            alert('请先填写题干内容');
+        }
+        return;
+    }
+    
+    // 构建请求数据
+    const requestData = {
+        stem: stem,
+        answer: answerInput ? answerInput.value.trim() : '',
+        solution: solutionTextarea ? solutionTextarea.value.trim() : '',
+        question_type: questionTypeSelect ? questionTypeSelect.value : ''
+    };
+    
+    // 处理选项
+    if (optionsPreview && optionsPreview.value.trim() && optionsPreview.value.trim() !== '{}') {
+        try {
+            requestData.options = JSON.parse(optionsPreview.value.trim());
+        } catch (e) {
+            console.warn('选项JSON解析失败:', e);
+        }
+    }
+    
+    // 显示加载状态
+    const originalText = btn.innerHTML;
+    btn.disabled = true;
+    btn.innerHTML = '<svg class="w-3 h-3 inline-block mr-1 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>评价中...';
+    
+    try {
+        const response = await fetch('/api/score', {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json'
+            },
+            body: JSON.stringify(requestData)
+        });
+        
+        if (!response.ok) {
+            throw new Error(`请求失败: ${response.status} ${response.statusText}`);
+        }
+        
+        const result = await response.json();
+        
+        // 处理返回的 difficulty_level
+        let difficultyLevel = result.data?.difficulty_level || result.difficulty_level || result.difficulty || result.level;
+        
+        // 映射难度等级到枚举值
+        let difficultyValue = '';
+        
+        if (difficultyLevel !== undefined && difficultyLevel !== null) {
+            const levelStr = String(difficultyLevel).trim();
+            
+            if (levelStr === '筑基' || levelStr === '0.2' || levelStr === '0.20') {
+                difficultyValue = '0.2';
+            } else if (levelStr === '提分' || levelStr === '0.4' || levelStr === '0.40') {
+                difficultyValue = '0.4';
+            } else if (levelStr === '培优' || levelStr === '0.7' || levelStr === '0.70') {
+                difficultyValue = '0.7';
+            } else {
+                const levelNum = parseFloat(levelStr);
+                if (!isNaN(levelNum)) {
+                    if (Math.abs(levelNum - 0.2) < 0.1) {
+                        difficultyValue = '0.2';
+                    } else if (Math.abs(levelNum - 0.4) < 0.1) {
+                        difficultyValue = '0.4';
+                    } else if (Math.abs(levelNum - 0.7) < 0.1) {
+                        difficultyValue = '0.7';
+                    }
+                }
+            }
+        }
+        
+        if (difficultyValue) {
+            difficultySelect.value = difficultyValue;
+            difficultySelect.dispatchEvent(new Event('change', { bubbles: true }));
+            updateBatchFullPreview();
+        } else {
+            console.error('无法识别难度等级,完整返回结果:', result);
+            if (window.customAlert) {
+                window.customAlert('无法识别返回的难度等级。返回数据:' + JSON.stringify(result));
+            } else {
+                alert('无法识别返回的难度等级。返回数据:' + JSON.stringify(result));
+            }
+        }
+        
+    } catch (error) {
+        console.error('难度评价失败:', error);
+        if (window.customAlert) {
+            window.customAlert('难度评价失败: ' + error.message);
+        } else {
+            alert('难度评价失败: ' + error.message);
+        }
+    } finally {
+        btn.disabled = false;
+        btn.innerHTML = originalText;
+    }
+}
+
+async function submitBatchQuestions() {
+    const questionDivs = document.querySelectorAll('[data-index]');
+    if (questionDivs.length === 0) {
+        if (window.customAlert) {
+            window.customAlert('没有可提交的题目');
+        } else {
+            alert('没有可提交的题目');
+        }
+        return;
+    }
+    
+    const submitBtn = document.getElementById('batch-submit-btn');
+    submitBtn.disabled = true;
+    submitBtn.textContent = '提交中...';
+    
+    let successCount = 0;
+    const userName = localStorage.getItem('user_name') || '';
+    
+    for (let i = 0; i < questionDivs.length; i++) {
+        const questionDiv = questionDivs[i];
+        const index = parseInt(questionDiv.getAttribute('data-index'));
+        
+        try {
+            const stem = questionDiv.querySelector('.batch-stem').value.trim();
+            if (!stem) continue;
+            
+            const questionData = {
+                stem: stem,
+                answer: questionDiv.querySelector('.batch-answer').value.trim(),
+                solution: questionDiv.querySelector('.batch-solution').value.trim(),
+                question_type: questionDiv.querySelector('.batch-question-type').value
+            };
+            
+            // 只在有知识点代码时才添加 kp_code
+            if (currentBatchKpCode && currentBatchKpCode !== 'null' && currentBatchKpCode !== '') {
+                questionData.kp_code = currentBatchKpCode;
+            }
+            
+            // 处理年级(所有题目必填)
+            const gradeSelect = questionDiv.querySelector('.batch-grade');
+            if (gradeSelect) {
+                const grade = gradeSelect.value;
+                if (!grade) {
+                    if (window.customAlert) {
+                        window.customAlert(`第 ${index + 1} 道题目:请选择年级`);
+                    } else {
+                        alert(`第 ${index + 1} 道题目:请选择年级`);
+                    }
+                    gradeSelect.focus();
+                    submitBtn.disabled = false;
+                    submitBtn.textContent = '批量提交';
+                    return;
+                }
+                questionData.grade = parseInt(grade);
+            }
+            
+            const difficulty = questionDiv.querySelector('.batch-difficulty').value;
+            if (difficulty) questionData.difficulty = parseFloat(difficulty);
+            
+            const optionsPreview = questionDiv.querySelector('.batch-options-preview');
+            if (questionData.question_type === 'choice' && optionsPreview && optionsPreview.value.trim() && optionsPreview.value.trim() !== '{}') {
+                try {
+                    const optionsObj = JSON.parse(optionsPreview.value.trim());
+                    if (Object.keys(optionsObj).length > 0) {
+                        questionData.options = JSON.stringify(optionsObj);
+                    }
+                } catch (e) {
+                    console.warn('选项JSON解析失败:', e);
+                }
+            }
+            
+            if (userName) questionData.create_by = userName;
+            
+            const response = await fetch('/create_question', {
+                method: 'POST',
+                headers: {'Content-Type': 'application/json'},
+                body: JSON.stringify(questionData)
+            });
+            
+            const result = await response.json();
+            if (result.success) {
+                successCount++;
+                // 调用接口2确认不重复
+                if (result.question_id) {
+                    try {
+                        await fetch('/api/confirm_repeat', {
+                            method: 'POST',
+                            headers: {'Content-Type': 'application/json'},
+                            body: JSON.stringify({
+                                questionId: result.question_id,
+                                isRepeat: 0
+                            })
+                        });
+                    } catch (error) {
+                        console.warn('确认查重结果失败:', error);
+                    }
+                }
+            }
+        } catch (error) {
+            console.error('提交失败:', error);
+        }
+    }
+    
+    submitBtn.disabled = false;
+    submitBtn.textContent = '批量提交';
+    
+    const message = `成功导入 ${successCount} 道题`;
+    if (window.customAlert) {
+        window.customAlert(message, () => {
+            if (successCount > 0) {
+                // 只在有知识点代码时才刷新题目列表
+                if (currentBatchKpCode && currentBatchKpCode !== 'null' && currentBatchKpCode !== '') {
+                    loadQuestionsByKp(currentBatchKpCode, currentBatchKpName);
+                }
+                hideBatchImportModal();
+            }
+        });
+    } else {
+        alert(message);
+        if (successCount > 0) {
+            // 只在有知识点代码时才刷新题目列表
+            if (currentBatchKpCode && currentBatchKpCode !== 'null' && currentBatchKpCode !== '') {
+                loadQuestionsByKp(currentBatchKpCode, currentBatchKpName);
+            }
+            hideBatchImportModal();
+        }
+    }
+}
+
+// ==================== 录入题目弹窗相关函数 ====================
+let currentAddQuestionKpCode = null;
+let currentAddQuestionKpName = null;
+
+function showAddQuestionModal(kpCode, kpName) {
+    // 处理 'null' 字符串,转换为 null
+    if (kpCode === 'null' || kpCode === null) {
+        currentAddQuestionKpCode = null;
+        currentAddQuestionKpName = null;
+    } else {
+        currentAddQuestionKpCode = kpCode;
+        currentAddQuestionKpName = kpName;
+    }
+    
+    // 判断是否是"其他题目"
+    const isOtherQuestions = !kpCode || kpCode === 'null' || kpCode === '';
+    
+    // 设置知识点信息
+    if (kpCode && kpCode !== 'null' && kpName && kpName !== 'null') {
+        document.getElementById('add-question-kp-name').textContent = kpName;
+        document.getElementById('add-question-kp-code').textContent = kpCode;
+    } else {
+        document.getElementById('add-question-kp-name').textContent = '其他题目(未关联知识点)';
+        document.getElementById('add-question-kp-code').textContent = 'N/A';
+    }
+    
+    // 重置表单
+    document.getElementById('add-question-form').reset();
+    document.getElementById('add-question-type').value = 'choice';
+    document.getElementById('add-question-difficulty').value = '';
+    
+    // 如果有年级筛选,自动填充年级(但可以修改)
+    if (document.getElementById('add-question-grade')) {
+        if (currentGradeFilter !== null) {
+            document.getElementById('add-question-grade').value = currentGradeFilter.toString();
+        } else {
+            document.getElementById('add-question-grade').value = '';
+        }
+    }
+    ['A', 'B', 'C', 'D'].forEach(opt => {
+        document.getElementById(`add-option-${opt}`).value = '';
+    });
+    // 重置选项预览框
+    const optionsPreview = document.getElementById('add-options-preview');
+    if (optionsPreview) {
+        optionsPreview.value = '';
+    }
+    
+    // 显示选项区域(默认选择题)
+    document.getElementById('add-options-section').style.display = 'block';
+    
+    // 设置题型变化监听器
+    const typeSelect = document.getElementById('add-question-type');
+    const optionsSection = document.getElementById('add-options-section');
+    typeSelect.onchange = function() {
+        if (this.value === 'choice') {
+            optionsSection.style.display = 'block';
+        } else {
+            optionsSection.style.display = 'none';
+        }
+        updateAddQuestionPreview();
+    };
+    
+    // 设置预览更新监听器
+    setupAddQuestionPreviewListeners();
+    
+    // 设置图片上传和粘贴功能
+    setupAddQuestionImageUpload();
+    
+    // 显示弹窗
+    document.getElementById('add-question-modal').classList.remove('hidden');
+    
+    // 更新预览和选项预览
+    updateAddQuestionOptionsPreview();
+    updateAddQuestionPreview();
+    
+    // 聚焦到题干输入框
+    setTimeout(() => {
+        document.getElementById('add-question-stem').focus();
+    }, 100);
+}
+
+function hideAddQuestionPreview() {
+    // 预览区域始终显示,这个函数可以用于未来扩展
+}
+
+// ==================== 图片上传和粘贴功能 ====================
+function setupAddQuestionImageUpload() {
+    // 题干图片上传按钮
+    const uploadStemBtn = document.getElementById('add-upload-stem-btn');
+    if (uploadStemBtn) {
+        uploadStemBtn.onclick = function() {
+            const fileInput = document.createElement('input');
+            fileInput.type = 'file';
+            fileInput.accept = 'image/*';
+            fileInput.style.display = 'none';
+            fileInput.onchange = async (e) => {
+                const file = e.target.files[0];
+                if (file) {
+                    await uploadAddQuestionImageToInput(file, document.getElementById('add-question-stem'));
+                }
+            };
+            document.body.appendChild(fileInput);
+            fileInput.click();
+            document.body.removeChild(fileInput);
+        };
+    }
+    
+    // 题干拖拽上传
+    const stemTextarea = document.getElementById('add-question-stem');
+    if (stemTextarea) {
+        stemTextarea.addEventListener('dragover', function(e) {
+            e.preventDefault();
+            e.stopPropagation();
+            this.classList.add('border-blue-500', 'bg-blue-50');
+        });
+        stemTextarea.addEventListener('dragleave', function(e) {
+            e.preventDefault();
+            e.stopPropagation();
+            this.classList.remove('border-blue-500', 'bg-blue-50');
+        });
+        stemTextarea.addEventListener('drop', async function(e) {
+            e.preventDefault();
+            e.stopPropagation();
+            this.classList.remove('border-blue-500', 'bg-blue-50');
+            const files = e.dataTransfer.files;
+            if (files && files.length > 0 && files[0].type.startsWith('image/')) {
+                await uploadAddQuestionImageToInput(files[0], this);
+            }
+        });
+    }
+    
+    // 选项拖拽上传
+    ['A', 'B', 'C', 'D'].forEach(opt => {
+        const optionInput = document.getElementById(`add-option-${opt}`);
+        if (optionInput) {
+            optionInput.addEventListener('dragover', function(e) {
+                e.preventDefault();
+                e.stopPropagation();
+                this.classList.add('border-blue-500', 'bg-blue-50');
+            });
+            optionInput.addEventListener('dragleave', function(e) {
+                e.preventDefault();
+                e.stopPropagation();
+                this.classList.remove('border-blue-500', 'bg-blue-50');
+            });
+            optionInput.addEventListener('drop', async function(e) {
+                e.preventDefault();
+                e.stopPropagation();
+                this.classList.remove('border-blue-500', 'bg-blue-50');
+                const files = e.dataTransfer.files;
+                if (files && files.length > 0 && files[0].type.startsWith('image/')) {
+                    await uploadAddQuestionImageToInput(files[0], this);
+                }
+            });
+        }
+    });
+    
+    // 解析拖拽上传
+    const solutionTextarea = document.getElementById('add-question-solution');
+    if (solutionTextarea) {
+        solutionTextarea.addEventListener('dragover', function(e) {
+            e.preventDefault();
+            e.stopPropagation();
+            this.classList.add('border-blue-500', 'bg-blue-50');
+        });
+        solutionTextarea.addEventListener('dragleave', function(e) {
+            e.preventDefault();
+            e.stopPropagation();
+            this.classList.remove('border-blue-500', 'bg-blue-50');
+        });
+        solutionTextarea.addEventListener('drop', async function(e) {
+            e.preventDefault();
+            e.stopPropagation();
+            this.classList.remove('border-blue-500', 'bg-blue-50');
+            const files = e.dataTransfer.files;
+            if (files && files.length > 0 && files[0].type.startsWith('image/')) {
+                await uploadAddQuestionImageToInput(files[0], this);
+            }
+        });
+    }
+    
+    // 为所有输入框添加粘贴图片功能
+    const allInputs = document.querySelectorAll('#add-question-form textarea, #add-question-form input[type="text"]');
+    allInputs.forEach(input => {
+        input.addEventListener('paste', async function(e) {
+            const clipboardData = e.clipboardData || window.clipboardData;
+            if (!clipboardData) {
+                return;
+            }
+            
+            const items = clipboardData.items;
+            if (!items) {
+                return;
+            }
+            
+            for (let i = 0; i < items.length; i++) {
+                const item = items[i];
+                
+                // 如果是图片类型
+                if (item.type.indexOf('image') !== -1) {
+                    e.preventDefault(); // 阻止默认粘贴行为
+                    
+                    const file = item.getAsFile();
+                    if (file) {
+                        // 上传图片并插入到当前输入框
+                        await uploadAddQuestionImageToInput(file, this);
+                    }
+                    break;
+                }
+            }
+        });
+    });
+}
+
+// 通用图片上传函数:上传图片并插入到指定输入框
+async function uploadAddQuestionImageToInput(file, inputElement) {
+    if (!inputElement || !file) {
+        return;
+    }
+    
+    // 检查文件类型
+    if (!file.type.startsWith('image/')) {
+        if (window.customAlert) {
+            window.customAlert('请选择图片文件!');
+        } else {
+            alert('请选择图片文件!');
+        }
+        return;
+    }
+    
+    // 显示上传中状态
+    const originalPlaceholder = inputElement.placeholder || '';
+    const originalOpacity = inputElement.style.opacity || '1';
+    inputElement.placeholder = '正在上传图片...';
+    inputElement.style.opacity = '0.6';
+    inputElement.disabled = true;
+    
+    try {
+        const formData = new FormData();
+        formData.append('file', file);
+        
+        const response = await fetch('https://crmapi.dcjxb.yunzhixue.cn/file/upload', {
+            method: 'POST',
+            body: formData
+        });
+        
+        if (!response.ok) {
+            throw new Error(`上传失败: ${response.status} ${response.statusText}`);
+        }
+        
+        const result = await response.json();
+        
+        // 检查返回结果,提取URL
+        let imageUrl = null;
+        if (typeof result === 'string') {
+            imageUrl = result;
+        } else if (result.url) {
+            imageUrl = result.url;
+        } else if (result.data && result.data.url) {
+            imageUrl = result.data.url;
+        } else if (result.data && typeof result.data === 'string') {
+            imageUrl = result.data;
+        } else {
+            const resultStr = JSON.stringify(result);
+            const urlMatch = resultStr.match(/https?:\/\/[^\s"']+/);
+            if (urlMatch) {
+                imageUrl = urlMatch[0];
+            }
+        }
+        
+        if (!imageUrl) {
+            throw new Error('接口返回格式异常,未找到图片URL。返回结果:' + JSON.stringify(result));
+        }
+        
+        // 构建 image 标签
+        const imageTag = `<image src="${imageUrl}"/>`;
+        
+        // 获取当前光标位置
+        const cursorPos = inputElement.selectionStart || inputElement.value.length;
+        const textBefore = inputElement.value.substring(0, cursorPos);
+        const textAfter = inputElement.value.substring(cursorPos);
+        
+        // 插入 image 标签
+        inputElement.value = textBefore + imageTag + textAfter;
+        
+        // 设置光标位置到插入内容之后
+        const newCursorPos = cursorPos + imageTag.length;
+        if (inputElement.setSelectionRange) {
+            inputElement.setSelectionRange(newCursorPos, newCursorPos);
+        }
+        inputElement.focus();
+        
+        // 触发input事件,更新预览
+        inputElement.dispatchEvent(new Event('input', { bubbles: true }));
+        
+        // 更新预览和选项预览
+        updateAddQuestionOptionsPreview();
+        updateAddQuestionPreview();
+        
+    } catch (error) {
+        console.error('上传失败:', error);
+        if (window.customAlert) {
+            window.customAlert('图片上传失败: ' + error.message);
+        } else {
+            alert('图片上传失败: ' + error.message);
+        }
+    } finally {
+        inputElement.placeholder = originalPlaceholder;
+        inputElement.style.opacity = originalOpacity;
+        inputElement.disabled = false;
+    }
+}
+
+// 选项图片上传函数
+function uploadAddQuestionOptionImage(optionKey) {
+    const fileInput = document.createElement('input');
+    fileInput.type = 'file';
+    fileInput.accept = 'image/*';
+    fileInput.style.display = 'none';
+    fileInput.onchange = async (e) => {
+        const file = e.target.files[0];
+        if (file) {
+            await uploadAddQuestionImageToInput(file, document.getElementById(`add-option-${optionKey}`));
+        }
+    };
+    document.body.appendChild(fileInput);
+    fileInput.click();
+    document.body.removeChild(fileInput);
+}
+
+// 解析图片上传函数
+function uploadAddQuestionSolutionImage() {
+    const fileInput = document.createElement('input');
+    fileInput.type = 'file';
+    fileInput.accept = 'image/*';
+    fileInput.style.display = 'none';
+    fileInput.onchange = async (e) => {
+        const file = e.target.files[0];
+        if (file) {
+            await uploadAddQuestionImageToInput(file, document.getElementById('add-question-solution'));
+        }
+    };
+    document.body.appendChild(fileInput);
+    fileInput.click();
+    document.body.removeChild(fileInput);
+}
+
+function setupAddQuestionPreviewListeners() {
+    // 题干预览
+    const stemTextarea = document.getElementById('add-question-stem');
+    if (stemTextarea) {
+        stemTextarea.addEventListener('input', updateAddQuestionPreview);
+        stemTextarea.addEventListener('focus', updateAddQuestionPreview);
+    }
+    
+    // 选项预览 - 从各个选项输入框自动拼接成JSON
+    ['A', 'B', 'C', 'D'].forEach(opt => {
+        const optionInput = document.getElementById(`add-option-${opt}`);
+        if (optionInput) {
+            optionInput.addEventListener('input', function() {
+                updateAddQuestionOptionsPreview();
+                updateAddQuestionPreview();
+            });
+        }
+    });
+    
+    // 选项预览框 - 从JSON同步回各个选项输入框
+    const optionsPreview = document.getElementById('add-options-preview');
+    if (optionsPreview) {
+        optionsPreview.addEventListener('input', function() {
+            syncAddQuestionOptionsFromPreview();
+            updateAddQuestionPreview();
+        });
+    }
+    
+    // 答案预览
+    const answerInput = document.getElementById('add-question-answer');
+    if (answerInput) {
+        answerInput.addEventListener('input', updateAddQuestionPreview);
+    }
+    
+    // 解析预览
+    const solutionTextarea = document.getElementById('add-question-solution');
+    if (solutionTextarea) {
+        solutionTextarea.addEventListener('input', updateAddQuestionPreview);
+        solutionTextarea.addEventListener('focus', updateAddQuestionPreview);
+    }
+    
+    // 题型和难度变化
+    const typeSelect = document.getElementById('add-question-type');
+    if (typeSelect) {
+        typeSelect.addEventListener('change', updateAddQuestionPreview);
+    }
+    
+    const difficultySelect = document.getElementById('add-question-difficulty');
+    if (difficultySelect) {
+        difficultySelect.addEventListener('change', updateAddQuestionPreview);
+    }
+}
+
+function updateAddQuestionPreview() {
+    const previewContent = document.getElementById('add-question-preview-content');
+    if (!previewContent) return;
+    
+    const stem = document.getElementById('add-question-stem')?.value || '';
+    const answer = document.getElementById('add-question-answer')?.value || '';
+    const solution = document.getElementById('add-question-solution')?.value || '';
+    const questionType = document.getElementById('add-question-type')?.value || 'choice';
+    const difficulty = document.getElementById('add-question-difficulty')?.value || '';
+    
+    let html = '<div class="space-y-6">';
+    
+    // 题型和难度信息
+    html += '<div class="border-b border-gray-200 pb-3 mb-3">';
+    html += '<div class="flex items-center gap-3 text-sm">';
+    const typeMap = {'choice': '选择题', 'fill': '填空题', 'answer': '解答题'};
+    html += `<span class="bg-blue-100 text-blue-700 px-2 py-0.5 rounded text-xs font-bold">${typeMap[questionType] || questionType}</span>`;
+    if (difficulty) {
+        const diffMap = {'0.2': '筑基', '0.4': '提分', '0.7': '培优'};
+        html += `<span class="bg-purple-100 text-purple-700 px-2 py-0.5 rounded text-xs font-bold">${diffMap[difficulty] || difficulty}</span>`;
+    }
+    html += '</div>';
+    html += '</div>';
+    
+    // 题干预览
+    html += '<div class="border-b border-gray-200 pb-4">';
+    html += '<div class="text-xs font-bold text-gray-500 mb-2">题干</div>';
+    if (stem) {
+        // 处理图片标签
+        let stemHtml = stem.replace(/<image\s+src="([^"]+)"\s*\/?>/gi, (match, url) => {
+            return `<img src="${url}" class="max-w-full max-h-[300px] w-auto h-auto my-2 rounded-lg mx-auto block" alt="题目图片" style="object-fit: contain;">`;
+        });
+        
+        // 转义HTML(但保留图片标签)
+        stemHtml = stemHtml
+            .replace(/&/g, '&amp;')
+            .replace(/</g, '&lt;')
+            .replace(/>/g, '&gt;')
+            .replace(/\n/g, '<br>');
+        
+        // 恢复图片标签
+        stemHtml = stemHtml.replace(/&lt;img[^&]*&gt;/g, (match) => {
+            return match.replace(/&lt;/g, '<').replace(/&gt;/g, '>');
+        });
+        
+        html += `<div class="text-sm leading-relaxed">${stemHtml}</div>`;
+    } else {
+        html += '<p class="text-xs text-gray-400">暂无内容</p>';
+    }
+    html += '</div>';
+    
+    // 选项预览(仅选择题)
+    if (questionType === 'choice') {
+        html += '<div class="border-b border-gray-200 pb-4">';
+        html += '<div class="text-xs font-bold text-gray-500 mb-2">选项</div>';
+        let hasOptions = false;
+        ['A', 'B', 'C', 'D'].forEach(key => {
+            const optionText = document.getElementById(`add-option-${key}`)?.value || '';
+            if (optionText && optionText.trim()) {
+                hasOptions = true;
+                html += `<div class="mb-3">`;
+                html += `<div class="text-xs font-bold text-gray-500 mb-1">选项 ${key}</div>`;
+                
+                // 处理选项文本,支持图片标签
+                let optionHtml = optionText.replace(/<image\s+src="([^"]+)"\s*\/?>/gi, (match, url) => {
+                    return `<img src="${url}" class="max-w-full max-h-[200px] w-auto h-auto my-2 rounded-lg mx-auto block" alt="选项图片" style="object-fit: contain;">`;
+                });
+                
+                // 转义HTML
+                optionHtml = optionHtml
+                    .replace(/&/g, '&amp;')
+                    .replace(/</g, '&lt;')
+                    .replace(/>/g, '&gt;')
+                    .replace(/\n/g, '<br>');
+                
+                // 恢复图片标签
+                optionHtml = optionHtml.replace(/&lt;img[^&]*&gt;/g, (match) => {
+                    return match.replace(/&lt;/g, '<').replace(/&gt;/g, '>');
+                });
+                
+                html += `<div class="text-sm leading-relaxed">${optionHtml}</div>`;
+                html += `</div>`;
+            }
+        });
+        if (!hasOptions) {
+            html += '<p class="text-xs text-gray-400">暂无选项</p>';
+        }
+        html += '</div>';
+    }
+    
+    // 答案预览
+    html += '<div class="border-b border-gray-200 pb-4">';
+    html += '<div class="text-xs font-bold text-gray-500 mb-2">正确答案</div>';
+    if (answer) {
+        html += `<div class="text-sm font-bold text-blue-600">${escapeHtml(answer)}</div>`;
+    } else {
+        html += '<p class="text-xs text-gray-400">暂无答案</p>';
+    }
+    html += '</div>';
+    
+    // 解析预览
+    html += '<div>';
+    html += '<div class="text-xs font-bold text-gray-500 mb-2">解析</div>';
+    if (solution) {
+        // 处理图片标签
+        let solutionHtml = solution.replace(/<image\s+src="([^"]+)"\s*\/?>/gi, (match, url) => {
+            return `<img src="${url}" class="max-w-full max-h-[300px] w-auto h-auto my-2 rounded-lg mx-auto block" alt="解析图片" style="object-fit: contain;">`;
+        });
+        
+        // 转义HTML
+        solutionHtml = solutionHtml
+            .replace(/&/g, '&amp;')
+            .replace(/</g, '&lt;')
+            .replace(/>/g, '&gt;')
+            .replace(/\n/g, '<br>');
+        
+        // 恢复图片标签
+        solutionHtml = solutionHtml.replace(/&lt;img[^&]*&gt;/g, (match) => {
+            return match.replace(/&lt;/g, '<').replace(/&gt;/g, '>');
+        });
+        
+        html += `<div class="text-sm leading-relaxed">${solutionHtml}</div>`;
+    } else {
+        html += '<p class="text-xs text-gray-400">暂无解析</p>';
+    }
+    html += '</div>';
+    
+    html += '</div>';
+    
+    previewContent.innerHTML = html;
+    
+    // 渲染LaTeX
+    if (window.renderMathInElement) {
+        try {
+            window.renderMathInElement(previewContent, {
+                delimiters: [
+                    {left: "$$", right: "$$", display: true},
+                    {left: "$", right: "$", display: false},
+                    {left: "\\(", right: "\\)", display: false},
+                    {left: "\\[", right: "\\]", display: true}
+                ],
+                throwOnError: false
+            });
+        } catch (e) {
+            console.warn('LaTeX 渲染失败:', e);
+        }
+    }
+}
+
+// 更新单个录入题目的选项预览(从各个选项输入框自动拼接成JSON)
+function updateAddQuestionOptionsPreview() {
+    const previewTextarea = document.getElementById('add-options-preview');
+    if (!previewTextarea) return;
+    
+    const optionsObj = {};
+    ['A', 'B', 'C', 'D'].forEach(opt => {
+        const input = document.getElementById(`add-option-${opt}`);
+        if (input && input.value.trim()) {
+            optionsObj[opt] = input.value.trim();
+        }
+    });
+    
+    previewTextarea.value = JSON.stringify(optionsObj, null, 2);
+}
+
+// 从选项预览框同步回各个选项输入框(单个录入题目)
+function syncAddQuestionOptionsFromPreview() {
+    const previewTextarea = document.getElementById('add-options-preview');
+    if (!previewTextarea) return;
+    
+    try {
+        const optionsObj = JSON.parse(previewTextarea.value.trim() || '{}');
+        ['A', 'B', 'C', 'D'].forEach(opt => {
+            const input = document.getElementById(`add-option-${opt}`);
+            if (input) {
+                input.value = optionsObj[opt] || '';
+            }
+        });
+    } catch (error) {
+        console.warn('选项预览 JSON 格式错误:', error);
+    }
+}
+
+function hideAddQuestionModal() {
+    document.getElementById('add-question-modal').classList.add('hidden');
+    currentAddQuestionKpCode = null;
+    currentAddQuestionKpName = null;
+}
+
+// ==================== 查重检测相关函数 ====================
+
+// 单独录题的查重检测
+async function checkDuplicateForAddQuestion() {
+    const stem = document.getElementById('add-question-stem').value.trim();
+    
+    if (!stem) {
+        if (window.customAlert) {
+            window.customAlert('请先填写题干内容');
+        } else {
+            alert('请先填写题干内容');
+        }
+        return;
+    }
+    
+    // 构建查重请求数据
+    const checkData = {
+        stem: stem,
+        answer: document.getElementById('add-question-answer').value.trim(),
+        solution: document.getElementById('add-question-solution').value.trim()
+    };
+    
+    // 处理选项(仅选择题)
+    const questionType = document.getElementById('add-question-type').value;
+    if (questionType === 'choice') {
+        const optionsObj = {};
+        ['A', 'B', 'C', 'D'].forEach(opt => {
+            const value = document.getElementById(`add-option-${opt}`).value.trim();
+            if (value) {
+                optionsObj[opt] = value;
+            }
+        });
+        if (Object.keys(optionsObj).length > 0) {
+            checkData.options = JSON.stringify(optionsObj);
+        }
+    }
+    
+    await performDuplicateCheck(checkData, null);
+}
+
+// 批量导入的查重检测
+async function checkDuplicateForBatch(index) {
+    const questionDiv = document.getElementById(`batch-q-card-${index}`);
+    if (!questionDiv) return;
+    
+    const stem = questionDiv.querySelector('.batch-stem').value.trim();
+    if (!stem) {
+        if (window.customAlert) {
+            window.customAlert('请先填写题干内容');
+        } else {
+            alert('请先填写题干内容');
+        }
+        return;
+    }
+    
+    // 构建查重请求数据
+    const checkData = {
+        stem: stem,
+        answer: questionDiv.querySelector('.batch-answer').value.trim(),
+        solution: questionDiv.querySelector('.batch-solution').value.trim()
+    };
+    
+    // 处理选项(仅选择题)
+    const questionType = questionDiv.querySelector('.batch-question-type').value;
+    if (questionType === 'choice') {
+        const optionsPreview = questionDiv.querySelector('.batch-options-preview');
+        if (optionsPreview && optionsPreview.value.trim() && optionsPreview.value.trim() !== '{}') {
+            try {
+                checkData.options = optionsPreview.value.trim();
+            } catch (e) {
+                console.warn('选项JSON解析失败:', e);
+            }
+        }
+    }
+    
+    await performDuplicateCheck(checkData, questionDiv);
+}
+
+// 关闭所有 customAlert 弹窗
+function closeAllCustomAlerts() {
+    // 查找所有 customAlert 创建的模态框(通过样式特征识别)
+    const modals = document.querySelectorAll('div[style*="position:fixed"][style*="z-index:9999"]');
+    modals.forEach(modal => {
+        // 检查是否是 customAlert 创建的(包含白色背景的 dialog)
+        const dialog = modal.querySelector('div[style*="background:white"]');
+        if (dialog) {
+            document.body.removeChild(modal);
+        }
+    });
+}
+
+// 执行查重检测
+async function performDuplicateCheck(checkData, sourceElement) {
+    let loadingModal = null;
+    try {
+        // 显示加载状态
+        if (window.customAlert) {
+            // 创建 loading 模式的弹窗(不显示按钮)
+            loadingModal = document.createElement('div');
+            loadingModal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.4);z-index:9999;display:flex;align-items:center;justify-content:center;';
+            
+            const dialog = document.createElement('div');
+            dialog.style.cssText = 'background:white;padding:2rem;border-radius:12px;box-shadow:0 10px 40px rgba(0,0,0,0.2);max-width:400px;text-align:center;';
+            
+            const msg = document.createElement('div');
+            msg.style.cssText = 'margin-bottom:1.5rem;font-size:1rem;color:#333;';
+            msg.textContent = '正在检测重复题目...';
+            
+            dialog.appendChild(msg);
+            loadingModal.appendChild(dialog);
+            document.body.appendChild(loadingModal);
+        }
+        
+        const response = await fetch('/api/check_duplicate', {
+            method: 'POST',
+            headers: {'Content-Type': 'application/json'},
+            body: JSON.stringify(checkData)
+        });
+        
+        const result = await response.json();
+        
+        // 关闭加载弹窗
+        if (loadingModal && loadingModal.parentNode) {
+            document.body.removeChild(loadingModal);
+            loadingModal = null;
+        }
+        closeAllCustomAlerts(); // 确保所有弹窗都关闭
+        
+        if (result.code === -1 && result.result && result.result.repeatIdList && result.result.repeatIdList.length > 0) {
+            // 发现重复,显示比对弹窗
+            const repeatInfo = result.result.repeatIdList[0];
+            await showDuplicateComparison(checkData, repeatInfo, sourceElement);
+        } else if (result.code === 0) {
+            // 无重复
+            if (window.customAlert) {
+                window.customAlert('未发现重复题目,可以继续录入');
+            } else {
+                alert('未发现重复题目,可以继续录入');
+            }
+        } else {
+            throw new Error('查重接口返回异常');
+        }
+    } catch (error) {
+        // 关闭加载弹窗
+        if (loadingModal && loadingModal.parentNode) {
+            document.body.removeChild(loadingModal);
+            loadingModal = null;
+        }
+        closeAllCustomAlerts(); // 确保所有弹窗都关闭
+        
+        console.error('查重检测失败:', error);
+        if (window.customAlert) {
+            window.customAlert('查重检测失败: ' + error.message);
+        } else {
+            alert('查重检测失败: ' + error.message);
+        }
+    }
+}
+
+// 显示比对弹窗
+async function showDuplicateComparison(currentQuestionData, repeatInfo, sourceElement) {
+    const modal = document.getElementById('duplicate-comparison-modal');
+    const similarityInfo = document.getElementById('duplicate-similarity-info');
+    const currentQuestionDiv = document.getElementById('duplicate-current-question');
+    const existingQuestionDiv = document.getElementById('duplicate-existing-question');
+    
+    // 显示相似度信息
+    similarityInfo.textContent = repeatInfo.repeatMsg || '发现重复题目';
+    
+    // 渲染当前录入的题目
+    currentQuestionDiv.innerHTML = renderQuestionForComparison(currentQuestionData, sourceElement);
+    
+    // 查询库中重复的题目
+    try {
+        const response = await fetch(`/api/question_by_id/${repeatInfo.questionsId}`);
+        const result = await response.json();
+        
+        if (result.success && result.question) {
+            existingQuestionDiv.innerHTML = renderQuestionForComparison(result.question, null, true);
+        } else {
+            existingQuestionDiv.innerHTML = '<p class="text-gray-500">无法加载重复题目详情</p>';
+        }
+    } catch (error) {
+        console.error('加载重复题目失败:', error);
+        existingQuestionDiv.innerHTML = '<p class="text-red-500">加载重复题目失败: ' + error.message + '</p>';
+    }
+    
+    // 显示弹窗
+    modal.classList.remove('hidden');
+    
+    // 渲染LaTeX
+    setTimeout(() => {
+        [currentQuestionDiv, existingQuestionDiv].forEach(container => {
+            if (window.renderMathInElement) {
+                try {
+                    window.renderMathInElement(container, {
+                        delimiters: [
+                            {left: "$$", right: "$$", display: true},
+                            {left: "$", right: "$", display: false},
+                            {left: "\\(", right: "\\)", display: false},
+                            {left: "\\[", right: "\\]", display: true}
+                        ],
+                        throwOnError: false
+                    });
+                } catch (e) {
+                    console.warn('LaTeX 渲染失败:', e);
+                }
+            }
+        });
+    }, 100);
+}
+
+// 渲染题目用于比对
+function renderQuestionForComparison(questionData, sourceElement, isExisting = false) {
+    let html = '';
+    
+    // 题型
+    const typeMap = {'choice': '选择题', 'fill': '填空题', 'answer': '解答题'};
+    const questionType = questionData.question_type || '';
+    const typeText = typeMap[questionType] || questionType || '未分类';
+    
+    html += `<div class="mb-4">
+        <span class="bg-blue-100 text-blue-700 px-2 py-1 rounded text-xs font-bold">${typeText}</span>
+    </div>`;
+    
+    // 题干
+    html += '<div class="mb-4">';
+    html += '<div class="text-xs font-bold text-gray-500 mb-2">题干</div>';
+    let stem = '';
+    if (isExisting) {
+        stem = questionData.stem || '';
+    } else if (sourceElement) {
+        stem = sourceElement.querySelector('.batch-stem').value;
+    } else {
+        stem = document.getElementById('add-question-stem').value;
+    }
+    
+    // 处理图片标签
+    const imagePlaceholders = [];
+    let stemProcessed = stem.replace(/<image\s+src="([^"]+)"\s*\/?>/gi, (match, url) => {
+        const placeholder = `__IMAGE_PLACEHOLDER_${imagePlaceholders.length}__`;
+        imagePlaceholders.push(url);
+        return placeholder;
+    });
+    
+    // 转义HTML
+    let stemHtml = stemProcessed
+        .replace(/&/g, '&amp;')
+        .replace(/</g, '&lt;')
+        .replace(/>/g, '&gt;')
+        .replace(/\n/g, '<br>');
+    
+    // 替换图片占位符
+    imagePlaceholders.forEach((url, index) => {
+        const placeholder = `__IMAGE_PLACEHOLDER_${index}__`;
+        const imgTag = `<img src="${url}" class="max-w-full max-h-[300px] w-auto h-auto my-2 rounded-lg" alt="题目图片" style="object-fit: contain;">`;
+        stemHtml = stemHtml.replace(placeholder, imgTag);
+    });
+    
+    html += `<div class="text-sm leading-relaxed">${stemHtml}</div>`;
+    html += '</div>';
+    
+    // 选项(仅选择题)
+    if (questionType === 'choice') {
+        html += '<div class="mb-4">';
+        html += '<div class="text-xs font-bold text-gray-500 mb-2">选项</div>';
+        
+        let options = {};
+        if (isExisting) {
+            if (questionData.options) {
+                try {
+                    options = typeof questionData.options === 'string' ? JSON.parse(questionData.options) : questionData.options;
+                } catch (e) {
+                    options = {};
+                }
+            }
+        } else if (sourceElement) {
+            const optionsPreview = sourceElement.querySelector('.batch-options-preview');
+            if (optionsPreview && optionsPreview.value.trim() && optionsPreview.value.trim() !== '{}') {
+                try {
+                    options = JSON.parse(optionsPreview.value.trim());
+                } catch (e) {
+                    options = {};
+                }
+            }
+        } else {
+            ['A', 'B', 'C', 'D'].forEach(opt => {
+                const value = document.getElementById(`add-option-${opt}`).value.trim();
+                if (value) {
+                    options[opt] = value;
+                }
+            });
+        }
+        
+        Object.entries(options).forEach(([key, value]) => {
+            html += `<div class="mb-2">
+                <span class="font-semibold text-gray-700">${key}:</span>
+                <span class="text-sm text-gray-600 ml-2">${escapeHtml(value)}</span>
+            </div>`;
+        });
+        
+        html += '</div>';
+    }
+    
+    // 答案
+    html += '<div class="mb-4">';
+    html += '<div class="text-xs font-bold text-gray-500 mb-2">答案</div>';
+    let answer = '';
+    if (isExisting) {
+        answer = questionData.answer || '';
+    } else if (sourceElement) {
+        answer = sourceElement.querySelector('.batch-answer').value;
+    } else {
+        answer = document.getElementById('add-question-answer').value;
+    }
+    html += `<div class="text-sm font-bold text-blue-600">${escapeHtml(answer) || '暂无'}</div>`;
+    html += '</div>';
+    
+    // 解析
+    html += '<div class="mb-4">';
+    html += '<div class="text-xs font-bold text-gray-500 mb-2">解析</div>';
+    let solution = '';
+    if (isExisting) {
+        solution = questionData.solution || '';
+    } else if (sourceElement) {
+        solution = sourceElement.querySelector('.batch-solution').value;
+    } else {
+        solution = document.getElementById('add-question-solution').value;
+    }
+    
+    if (solution) {
+        // 处理图片标签
+        const solutionImagePlaceholders = [];
+        let solutionProcessed = solution.replace(/<image\s+src="([^"]+)"\s*\/?>/gi, (match, url) => {
+            const placeholder = `__SOLUTION_IMAGE_PLACEHOLDER_${solutionImagePlaceholders.length}__`;
+            solutionImagePlaceholders.push(url);
+            return placeholder;
+        });
+        
+        // 转义HTML
+        let solutionHtml = solutionProcessed
+            .replace(/&/g, '&amp;')
+            .replace(/</g, '&lt;')
+            .replace(/>/g, '&gt;')
+            .replace(/\n/g, '<br>');
+        
+        // 替换图片占位符
+        solutionImagePlaceholders.forEach((url, index) => {
+            const placeholder = `__SOLUTION_IMAGE_PLACEHOLDER_${index}__`;
+            const imgTag = `<img src="${url}" class="max-w-full max-h-[300px] w-auto h-auto my-2 rounded-lg" alt="解析图片" style="object-fit: contain;">`;
+            solutionHtml = solutionHtml.replace(placeholder, imgTag);
+        });
+        
+        html += `<div class="text-sm leading-relaxed">${solutionHtml}</div>`;
+    } else {
+        html += '<p class="text-sm text-gray-400">暂无解析</p>';
+    }
+    html += '</div>';
+    
+    return html;
+}
+
+// 关闭比对弹窗
+function hideDuplicateComparisonModal() {
+    document.getElementById('duplicate-comparison-modal').classList.add('hidden');
+}
+
+async function submitAddQuestion() {
+    const form = document.getElementById('add-question-form');
+    const stem = document.getElementById('add-question-stem').value.trim();
+    
+    // 验证必填项
+    if (!stem) {
+        if (window.customAlert) {
+            window.customAlert('请填写题干');
+        } else {
+            alert('请填写题干');
+        }
+        return;
+    }
+    
+    // 构建提交数据
+    const data = {
+        stem: stem,
+        answer: document.getElementById('add-question-answer').value.trim(),
+        solution: document.getElementById('add-question-solution').value.trim(),
+        question_type: document.getElementById('add-question-type').value
+    };
+    
+    // 只在有知识点代码时才添加 kp_code
+    if (currentAddQuestionKpCode && currentAddQuestionKpCode !== 'null' && currentAddQuestionKpCode !== '') {
+        data.kp_code = currentAddQuestionKpCode;
+    }
+    
+    // 处理年级(所有题目必填)
+    const gradeSelect = document.getElementById('add-question-grade');
+    if (gradeSelect) {
+        const grade = gradeSelect.value;
+        if (!grade) {
+            if (window.customAlert) {
+                window.customAlert('请选择年级');
+            } else {
+                alert('请选择年级');
+            }
+            gradeSelect.focus();
+            return;
+        }
+        data.grade = parseInt(grade);
+    }
+    
+    // 处理难度
+    const difficulty = document.getElementById('add-question-difficulty').value;
+    if (difficulty) {
+        data.difficulty = parseFloat(difficulty);
+    }
+    
+    // 处理选项(仅选择题)
+    if (data.question_type === 'choice') {
+        const optionsObj = {};
+        ['A', 'B', 'C', 'D'].forEach(opt => {
+            const value = document.getElementById(`add-option-${opt}`).value.trim();
+            if (value) {
+                optionsObj[opt] = value;
+            }
+        });
+        if (Object.keys(optionsObj).length > 0) {
+            data.options = JSON.stringify(optionsObj);
+        }
+    }
+    
+    // 添加创建者
+    const userName = localStorage.getItem('user_name');
+    if (userName && userName.trim()) {
+        data.create_by = userName.trim();
+    }
+    
+    // 提交按钮状态
+    const submitBtn = document.querySelector('#add-question-modal button[onclick="submitAddQuestion()"]');
+    const originalText = submitBtn.textContent;
+    submitBtn.disabled = true;
+    submitBtn.textContent = '提交中...';
+    
+    try {
+        const response = await fetch('/create_question', {
+            method: 'POST',
+            headers: {'Content-Type': 'application/json'},
+            body: JSON.stringify(data)
+        });
+        
+        const result = await response.json();
+        
+        if (result.success) {
+            // 调用接口2确认不重复
+            if (result.question_id) {
+                try {
+                    await fetch('/api/confirm_repeat', {
+                        method: 'POST',
+                        headers: {'Content-Type': 'application/json'},
+                        body: JSON.stringify({
+                            questionId: result.question_id,
+                            isRepeat: 0
+                        })
+                    });
+                } catch (error) {
+                    console.warn('确认查重结果失败:', error);
+                }
+            }
+            
+            if (window.customAlert) {
+                window.customAlert('题目创建成功!', () => {
+                    // 只在有知识点代码时才刷新题目列表
+                    if (currentAddQuestionKpCode && currentAddQuestionKpCode !== 'null' && currentAddQuestionKpCode !== '' && currentAddQuestionKpName) {
+                        loadQuestionsByKp(currentAddQuestionKpCode, currentAddQuestionKpName);
+                    }
+                    hideAddQuestionModal();
+                });
+            } else {
+                alert('题目创建成功!');
+                // 只在有知识点代码时才刷新题目列表
+                if (currentAddQuestionKpCode && currentAddQuestionKpCode !== 'null' && currentAddQuestionKpCode !== '' && currentAddQuestionKpName) {
+                    loadQuestionsByKp(currentAddQuestionKpCode, currentAddQuestionKpName);
+                }
+                hideAddQuestionModal();
+            }
+        } else {
+            if (window.customAlert) {
+                window.customAlert('创建失败: ' + (result.error || '未知错误'));
+            } else {
+                alert('创建失败: ' + (result.error || '未知错误'));
+            }
+        }
+    } catch (error) {
+        console.error('提交失败:', error);
+        if (window.customAlert) {
+            window.customAlert('提交失败: ' + error.message);
+        } else {
+            alert('提交失败: ' + error.message);
+        }
+    } finally {
+        submitBtn.disabled = false;
+        submitBtn.textContent = originalText;
+    }
+}
+
+// 难度评价函数
+async function evaluateAddQuestionDifficulty() {
+    const btn = document.getElementById('add-evaluate-difficulty-btn');
+    const difficultySelect = document.getElementById('add-question-difficulty');
+    const stem = document.getElementById('add-question-stem').value.trim();
+    
+    if (!stem) {
+        if (window.customAlert) {
+            window.customAlert('请先填写题干内容');
+        } else {
+            alert('请先填写题干内容');
+        }
+        return;
+    }
+    
+    const requestData = {
+        stem: stem,
+        answer: document.getElementById('add-question-answer').value.trim(),
+        solution: document.getElementById('add-question-solution').value.trim(),
+        question_type: document.getElementById('add-question-type').value
+    };
+    
+    // 处理选项
+    if (requestData.question_type === 'choice') {
+        const optionsObj = {};
+        ['A', 'B', 'C', 'D'].forEach(opt => {
+            const value = document.getElementById(`add-option-${opt}`).value.trim();
+            if (value) {
+                optionsObj[opt] = value;
+            }
+        });
+        if (Object.keys(optionsObj).length > 0) {
+            requestData.options = optionsObj;
+        }
+    }
+    
+    const originalText = btn.innerHTML;
+    btn.disabled = true;
+    btn.innerHTML = '<svg class="w-3 h-3 inline-block mr-1 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>评价中...';
+    
+    try {
+        const response = await fetch('/api/score', {
+            method: 'POST',
+            headers: {'Content-Type': 'application/json'},
+            body: JSON.stringify(requestData)
+        });
+        
+        if (!response.ok) {
+            throw new Error(`请求失败: ${response.status}`);
+        }
+        
+        const result = await response.json();
+        let difficultyLevel = result.data?.difficulty_level || result.difficulty_level || result.difficulty || result.level;
+        
+        let difficultyValue = '';
+        if (difficultyLevel !== undefined && difficultyLevel !== null) {
+            const levelStr = String(difficultyLevel).trim();
+            if (levelStr === '筑基' || levelStr === '0.2' || levelStr === '0.20') {
+                difficultyValue = '0.2';
+            } else if (levelStr === '提分' || levelStr === '0.4' || levelStr === '0.40') {
+                difficultyValue = '0.4';
+            } else if (levelStr === '培优' || levelStr === '0.7' || levelStr === '0.70') {
+                difficultyValue = '0.7';
+            } else {
+                const levelNum = parseFloat(levelStr);
+                if (!isNaN(levelNum)) {
+                    if (Math.abs(levelNum - 0.2) < 0.1) {
+                        difficultyValue = '0.2';
+                    } else if (Math.abs(levelNum - 0.4) < 0.1) {
+                        difficultyValue = '0.4';
+                    } else if (Math.abs(levelNum - 0.7) < 0.1) {
+                        difficultyValue = '0.7';
+                    }
+                }
+            }
+        }
+        
+        if (difficultyValue) {
+            difficultySelect.value = difficultyValue;
+        } else {
+            console.error('无法识别难度等级:', result);
+            if (window.customAlert) {
+                window.customAlert('无法识别返回的难度等级');
+            }
+        }
+    } catch (error) {
+        console.error('难度评价失败:', error);
+        if (window.customAlert) {
+            window.customAlert('难度评价失败: ' + error.message);
+        } else {
+            alert('难度评价失败: ' + error.message);
+        }
+    } finally {
+        btn.disabled = false;
+        btn.innerHTML = originalText;
+    }
+}
+</script>
+{% endblock %}
+

+ 1356 - 0
templates/questions.html

@@ -0,0 +1,1356 @@
+{% extends "layout.html" %}
+
+{% block page_title %}{{ kp_name }}{% if audit_only %} <span class="text-orange-500 text-lg font-normal">(未审核)</span>{% endif %} <span class="text-gray-400 text-lg font-normal">({{ questions|length }} 题)</span>{% endblock %}
+
+{% block content %}
+{% set audit_only = audit_only|default(false) %}
+<div class="mb-6 flex items-center gap-4">
+    {% if audit_only %}
+    <a href="/audit_questions" class="text-blue-600 font-medium hover:underline text-sm">← 返回审核题目</a>
+    {% elif kp_code %}
+    <a href="/question_management?kp_code={{ kp_code }}" class="text-blue-600 font-medium hover:underline text-sm">← 返回知识点列表</a>
+    {% elif node_id %}
+    <a href="/textbook/{{ node_id }}" class="text-blue-600 font-medium hover:underline text-sm">← 返回教材列表</a>
+    {% else %}
+    <a href="javascript:history.back()" class="text-blue-600 font-medium hover:underline text-sm">← 返回上一页</a>
+    {% endif %}
+    </div>
+    {% if kp_code %}
+    <div class="flex items-center gap-2">
+        <a href="{{ add_question_url }}" 
+           class="btn-apple bg-gradient-to-r from-blue-600 to-indigo-600 text-white hover:from-blue-700 hover:to-indigo-700 text-sm py-2 px-4">
+            录入题目
+        </a>
+        <button onclick="showBatchImportModal()" 
+                class="btn-apple bg-gradient-to-r from-green-600 to-emerald-600 text-white hover:from-green-700 hover:to-emerald-700 text-sm py-2 px-4">
+            批量导入
+        </button>
+    </div>
+    {% endif %}
+</div>
+
+<div class="space-y-4">
+    {% for q in questions %}
+    <div class="apple-card p-6 flex items-center justify-between">
+        <div class="flex-1 pr-8">
+            <div class="flex items-center space-x-3 mb-2">
+                <span class="text-xs font-bold uppercase tracking-wider text-gray-400">#{{ q.question_code }}</span>
+                {% set pk = (q.get('id') or q.get('question_id') or q.get('pk_id') or q.get('qid')) %}
+                {% if pk %}
+                <span class="text-xs text-gray-300 font-mono">ID: {{ pk }}</span>
+                {% endif %}
+                {% if q.audit_reason == '合格' %}
+                <span class="bg-green-100 text-green-700 px-2 py-0.5 rounded text-xs font-bold">合格</span>
+                {% elif q.audit_reason == '不合格' %}
+                <span class="bg-red-100 text-red-700 px-2 py-0.5 rounded text-xs font-bold">不合格</span>
+                {% endif %}
+            </div>
+            <p class="text-gray-700 line-clamp-2 math-render">{{ q.stem }}</p>
+        </div>
+        <a href="/detail/{{ q.question_code }}" class="btn-apple bg-blue-600 text-white hover:bg-blue-700 shadow-lg shadow-blue-200">去审核</a>
+    </div>
+    {% endfor %}
+</div>
+
+<!-- 批量导入模态框 -->
+<div id="batch-import-modal" class="hidden fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center">
+    <div class="bg-white rounded-2xl shadow-2xl w-full max-w-6xl max-h-[95vh] overflow-hidden flex flex-col">
+        <div class="bg-gradient-to-r from-green-600 to-emerald-600 text-white px-6 py-4 flex items-center justify-between">
+            <h2 class="text-xl font-bold">批量导入题目</h2>
+            <button onclick="hideBatchImportModal()" class="text-white hover:text-gray-200 text-2xl leading-none w-8 h-8 flex items-center justify-center rounded-full hover:bg-white/20 transition-colors">×</button>
+        </div>
+        <div class="flex-1 overflow-hidden flex flex-col">
+            <!-- JSON输入区域 -->
+            <div class="p-6 border-b border-gray-200">
+                <label class="block text-sm font-bold text-gray-700 mb-2">粘贴JSON数据(每道题以number字段切分)</label>
+                <textarea id="batch-json-input" 
+                          class="w-full h-32 p-3 border border-gray-300 rounded-lg font-mono text-xs focus:border-green-500 focus:ring-2 focus:ring-green-500/10 outline-none"
+                          placeholder='[{"number": "1", "stem": "...", "options": {"A": "...", "B": "..."}, "answer": "A", "question_type": "选择题", "solution": "..."}, ...]'
+                          oninput="handleBatchJsonInputChange()"></textarea>
+                <p class="text-xs text-gray-500 mt-2">支持数组格式或单个对象格式,每道题必须包含number字段。粘贴后自动填充到下方表单</p>
+            </div>
+            
+            <!-- 题目卡片区域 -->
+            <div id="batch-preview" class="hidden flex-1 overflow-hidden flex flex-col">
+                <!-- 题目导航 -->
+                <div class="px-6 py-3 bg-gray-50 border-b border-gray-200 flex items-center justify-between">
+                    <div class="flex items-center gap-3">
+                        <button id="batch-prev-btn" onclick="switchBatchQuestion(-1)" class="btn-apple bg-gray-100 text-gray-700 hover:bg-gray-200 px-3 py-1.5 disabled:opacity-50 disabled:cursor-not-allowed" disabled>
+                            <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
+                            </svg>
+                        </button>
+                        <span class="text-sm font-medium text-gray-700">
+                            第 <span id="current-question-index">1</span> / <span id="total-question-count">0</span> 题
+                        </span>
+                        <button id="batch-next-btn" onclick="switchBatchQuestion(1)" class="btn-apple bg-gray-100 text-gray-700 hover:bg-gray-200 px-3 py-1.5 disabled:opacity-50 disabled:cursor-not-allowed">
+                            <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
+                            </svg>
+                        </button>
+                    </div>
+                    <div class="text-xs text-gray-500">题号: <span id="current-question-number">-</span></div>
+                </div>
+                
+                <!-- 题目表单卡片容器 -->
+                <div class="flex-1 overflow-y-auto p-6 relative">
+                    <div id="questions-list" class="relative"></div>
+                </div>
+            </div>
+        </div>
+        
+        <!-- LaTeX 实时预览气泡 -->
+        <div id="batch-latex-preview-bubble" class="fixed right-8 top-24 w-[420px] max-h-[75vh] bg-white rounded-2xl shadow-2xl border border-gray-200 z-[60] overflow-hidden flex flex-col hidden" style="box-shadow: 0 20px 60px rgba(0,0,0,0.15);">
+            <div class="bg-gradient-to-r from-blue-500 to-indigo-600 text-white px-4 py-3 flex items-center justify-between">
+                <span class="text-sm font-bold">题目预览</span>
+                <button type="button" onclick="hideBatchPreviewBubble()" class="text-white hover:text-gray-200 text-xl leading-none w-6 h-6 flex items-center justify-center rounded-full hover:bg-white/20 transition-colors">×</button>
+            </div>
+            <div id="batch-latex-preview-content" class="flex-1 p-6 overflow-y-auto bg-gray-50 text-base leading-relaxed" style="min-height: 200px;">
+                <p class="text-sm text-gray-400 text-center">题目预览</p>
+            </div>
+        </div>
+        <div class="border-t border-gray-200 px-6 py-4 flex items-center justify-end gap-3">
+            <button onclick="hideBatchImportModal()" class="btn-apple bg-gray-100 text-gray-700 hover:bg-gray-200 px-4 py-2">取消</button>
+            <button id="batch-submit-btn" onclick="submitBatchQuestions()" class="btn-apple bg-gradient-to-r from-green-600 to-emerald-600 text-white hover:from-green-700 hover:to-emerald-700 px-4 py-2 hidden">批量提交</button>
+        </div>
+    </div>
+</div>
+
+<script>
+let parsedQuestions = [];
+let currentBatchQuestionIndex = 0;
+let batchJsonSyncTimer = null;
+let isSyncingBatchFromJson = false;
+let currentHierarchyInfo = {
+    kp_code: '{{ kp_code }}',
+    chapter: '{{ hierarchy_info.chapter.label if hierarchy_info.chapter else "" }}',
+    section: '{{ hierarchy_info.section.label if hierarchy_info.section else "" }}',
+    subsection: '{{ hierarchy_info.subsection.label if hierarchy_info.subsection else "" }}'
+};
+
+function showBatchImportModal() {
+    document.getElementById('batch-import-modal').classList.remove('hidden');
+    document.getElementById('batch-json-input').focus();
+}
+
+function hideBatchImportModal() {
+    document.getElementById('batch-import-modal').classList.add('hidden');
+    document.getElementById('batch-json-input').value = '';
+    document.getElementById('batch-preview').classList.add('hidden');
+    document.getElementById('batch-submit-btn').classList.add('hidden');
+    parsedQuestions = [];
+    currentBatchQuestionIndex = 0;
+}
+
+function handleBatchJsonInputChange() {
+    const jsonInput = document.getElementById('batch-json-input');
+    const jsonText = jsonInput.value.trim();
+    
+    if (jsonText) {
+        try {
+            JSON.parse(jsonText);
+            jsonInput.classList.remove('border-red-500');
+            jsonInput.classList.add('border-gray-300');
+            
+            // 防抖处理,自动解析并填充
+            clearTimeout(batchJsonSyncTimer);
+            batchJsonSyncTimer = setTimeout(() => {
+                if (!isSyncingBatchFromJson) {
+                    parseBatchJson();
+                }
+            }, 800);
+        } catch (e) {
+            jsonInput.classList.remove('border-gray-300');
+            jsonInput.classList.add('border-red-500');
+        }
+    }
+}
+
+function parseBatchJson() {
+    const jsonInput = document.getElementById('batch-json-input');
+    const jsonText = jsonInput.value.trim();
+    
+    if (!jsonText) {
+        return;
+    }
+    
+    try {
+        isSyncingBatchFromJson = true;
+        
+        let data = null;
+        
+        // 尝试1: 标准JSON数组格式 [ {...}, {...} ]
+        try {
+            data = JSON.parse(jsonText);
+            if (Array.isArray(data)) {
+                // 成功解析为数组
+            } else if (typeof data === 'object' && data !== null) {
+                // 单个对象,转换为数组
+                data = [data];
+            } else {
+                throw new Error('不是有效的JSON对象或数组');
+            }
+        } catch (e) {
+            // 尝试2: NDJSON格式(每行一个JSON对象)
+            const lines = jsonText.split('\n').filter(line => line.trim());
+            if (lines.length > 0) {
+                data = [];
+                for (let i = 0; i < lines.length; i++) {
+                    const line = lines[i].trim();
+                    if (!line) continue;
+                    try {
+                        const obj = JSON.parse(line);
+                        if (obj && typeof obj === 'object') {
+                            data.push(obj);
+                        }
+                    } catch (lineError) {
+                        console.warn(`第 ${i + 1} 行解析失败:`, lineError);
+                    }
+                }
+                if (data.length === 0) {
+                    throw new Error('无法解析任何有效的JSON对象');
+                }
+            } else {
+                throw e;
+            }
+        }
+        
+        // 按number字段切分
+        const newParsedQuestions = data.filter(q => q && q.number);
+        
+        if (newParsedQuestions.length === 0) {
+            return;
+        }
+        
+        // 保留当前题号索引(如果当前索引超出范围,则重置为0)
+        const oldIndex = currentBatchQuestionIndex;
+        parsedQuestions = newParsedQuestions;
+        
+        // 如果当前索引超出新数组范围,重置为0;否则保持当前索引
+        if (oldIndex >= parsedQuestions.length) {
+            currentBatchQuestionIndex = 0;
+        } else {
+            // 保持当前索引,但确保不超过新数组长度
+            currentBatchQuestionIndex = Math.min(oldIndex, parsedQuestions.length - 1);
+        }
+        
+        displayBatchPreview();
+        
+    } catch (error) {
+        console.warn('JSON格式错误:', error);
+    } finally {
+        isSyncingBatchFromJson = false;
+    }
+}
+
+function switchBatchQuestion(direction) {
+    const newIndex = currentBatchQuestionIndex + direction;
+    if (newIndex >= 0 && newIndex < parsedQuestions.length) {
+        currentBatchQuestionIndex = newIndex;
+        showBatchQuestion(currentBatchQuestionIndex);
+        updateBatchNavigation();
+    }
+}
+
+function updateBatchNavigation() {
+    const prevBtn = document.getElementById('batch-prev-btn');
+    const nextBtn = document.getElementById('batch-next-btn');
+    const currentIndexSpan = document.getElementById('current-question-index');
+    const totalCountSpan = document.getElementById('total-question-count');
+    const questionNumberSpan = document.getElementById('current-question-number');
+    
+    if (prevBtn && nextBtn && currentIndexSpan && totalCountSpan) {
+        currentIndexSpan.textContent = currentBatchQuestionIndex + 1;
+        totalCountSpan.textContent = parsedQuestions.length;
+        
+        prevBtn.disabled = currentBatchQuestionIndex === 0;
+        nextBtn.disabled = currentBatchQuestionIndex === parsedQuestions.length - 1;
+        
+        if (questionNumberSpan && parsedQuestions[currentBatchQuestionIndex]) {
+            questionNumberSpan.textContent = parsedQuestions[currentBatchQuestionIndex].number || currentBatchQuestionIndex + 1;
+        }
+    }
+}
+
+function showBatchQuestion(index) {
+    const questionsList = document.getElementById('questions-list');
+    if (!questionsList) return;
+    
+    // 隐藏所有题目卡片
+    const allCards = questionsList.querySelectorAll('.batch-question-card');
+    allCards.forEach(card => {
+        card.classList.add('hidden');
+    });
+    
+    // 显示当前题目卡片
+    const currentCard = document.getElementById(`batch-q-card-${index}`);
+    if (currentCard) {
+        currentCard.classList.remove('hidden');
+    }
+    
+    // 更新预览
+    updateBatchFullPreview();
+}
+
+function hideBatchPreviewBubble() {
+    const bubble = document.getElementById('batch-latex-preview-bubble');
+    if (bubble) {
+        bubble.classList.add('hidden');
+    }
+}
+
+function updateBatchFullPreview() {
+    const previewContent = document.getElementById('batch-latex-preview-content');
+    if (!previewContent) return;
+    
+    const bubble = document.getElementById('batch-latex-preview-bubble');
+    if (!bubble) return;
+    
+    // 获取当前显示的题目卡片
+    const currentCard = document.getElementById(`batch-q-card-${currentBatchQuestionIndex}`);
+    if (!currentCard) return;
+    
+    // 收集当前题目的所有数据
+    const stemTextarea = currentCard.querySelector('.batch-stem');
+    const answerInput = currentCard.querySelector('.batch-answer');
+    const solutionTextarea = currentCard.querySelector('.batch-solution');
+    const optionsPreview = currentCard.querySelector('.batch-options-preview');
+    const questionTypeSelect = currentCard.querySelector('.batch-question-type');
+    
+    let html = '<div class="space-y-6">';
+    
+    // 题干预览
+    html += '<div class="border-b border-gray-200 pb-4">';
+    html += '<div class="text-xs font-bold text-gray-500 mb-2">题干</div>';
+    const stem = stemTextarea ? stemTextarea.value : '';
+    if (stem) {
+        // 先提取图片标签,替换为占位符
+        const imagePlaceholders = [];
+        let stemProcessed = stem.replace(/<image\s+src="([^"]+)"\s*\/?>/gi, (match, url) => {
+            const placeholder = `__IMAGE_PLACEHOLDER_${imagePlaceholders.length}__`;
+            imagePlaceholders.push(url);
+            return placeholder;
+        });
+        
+        // 转义HTML
+        let stemHtml = stemProcessed
+            .replace(/&/g, '&amp;')
+            .replace(/</g, '&lt;')
+            .replace(/>/g, '&gt;')
+            .replace(/\n/g, '<br>');
+        
+        // 将占位符替换为img标签
+        imagePlaceholders.forEach((url, index) => {
+            const placeholder = `__IMAGE_PLACEHOLDER_${index}__`;
+            const imgTag = `<img src="${url}" class="max-w-[400px] max-h-[300px] w-auto h-auto my-2 rounded-lg mx-auto block" alt="题目图片" style="object-fit: contain;">`;
+            stemHtml = stemHtml.replace(placeholder, imgTag);
+        });
+        
+        html += `<div class="text-sm leading-relaxed">${stemHtml}</div>`;
+    } else {
+        html += '<p class="text-xs text-gray-400">暂无内容</p>';
+    }
+    html += '</div>';
+    
+    // 选项预览(仅选择题)
+    if (questionTypeSelect && questionTypeSelect.value === 'choice') {
+        html += '<div class="border-b border-gray-200 pb-4">';
+        html += '<div class="text-xs font-bold text-gray-500 mb-2">选项</div>';
+        
+        // 直接从选项输入框读取内容,确保图片标签能正确显示
+        let hasOptions = false;
+        ['A', 'B', 'C', 'D'].forEach(key => {
+            const optionInput = currentCard.querySelector(`.batch-option-${key}`);
+            const optionText = optionInput ? optionInput.value : '';
+            
+            if (optionText && optionText.trim()) {
+                hasOptions = true;
+                html += `<div class="mb-3">`;
+                html += `<div class="text-xs font-bold text-gray-500 mb-1">选项 ${key}</div>`;
+                
+                // 处理选项文本,支持图片标签
+                // 先提取图片标签,替换为占位符
+                const optionImagePlaceholders = [];
+                let optionProcessed = optionText.replace(/<image\s+src="([^"]+)"\s*\/?>/gi, (match, url) => {
+                    const placeholder = `__OPTION_IMAGE_PLACEHOLDER_${optionImagePlaceholders.length}__`;
+                    optionImagePlaceholders.push(url);
+                    return placeholder;
+                });
+                
+                // 转义HTML
+                let optionHtml = optionProcessed
+                    .replace(/&/g, '&amp;')
+                    .replace(/</g, '&lt;')
+                    .replace(/>/g, '&gt;')
+                    .replace(/\n/g, '<br>');
+                
+                // 将占位符替换为img标签
+                optionImagePlaceholders.forEach((url, index) => {
+                    const placeholder = `__OPTION_IMAGE_PLACEHOLDER_${index}__`;
+                    const imgTag = `<img src="${url}" class="max-w-[400px] max-h-[300px] w-auto h-auto my-2 rounded-lg mx-auto block" alt="题目图片" style="object-fit: contain;">`;
+                    optionHtml = optionHtml.replace(placeholder, imgTag);
+                });
+                
+                html += `<div class="text-sm leading-relaxed">${optionHtml}</div>`;
+                html += `</div>`;
+            }
+        });
+        
+        if (!hasOptions) {
+            html += '<p class="text-xs text-gray-400">暂无选项</p>';
+        }
+        html += '</div>';
+    }
+    
+    // 答案预览
+    html += '<div class="border-b border-gray-200 pb-4">';
+    html += '<div class="text-xs font-bold text-gray-500 mb-2">正确答案</div>';
+    const answer = answerInput ? answerInput.value : '';
+    if (answer) {
+        html += `<div class="text-sm font-bold text-blue-600">${answer}</div>`;
+    } else {
+        html += '<p class="text-xs text-gray-400">暂无答案</p>';
+    }
+    html += '</div>';
+    
+    // 解析预览
+    html += '<div>';
+    html += '<div class="text-xs font-bold text-gray-500 mb-2">解析</div>';
+    const solution = solutionTextarea ? solutionTextarea.value : '';
+    if (solution) {
+        // 先提取图片标签,替换为占位符
+        const solutionImagePlaceholders = [];
+        let solutionProcessed = solution.replace(/<image\s+src="([^"]+)"\s*\/?>/gi, (match, url) => {
+            const placeholder = `__SOLUTION_IMAGE_PLACEHOLDER_${solutionImagePlaceholders.length}__`;
+            solutionImagePlaceholders.push(url);
+            return placeholder;
+        });
+        
+        // 转义HTML
+        let solutionHtml = solutionProcessed
+            .replace(/&/g, '&amp;')
+            .replace(/</g, '&lt;')
+            .replace(/>/g, '&gt;')
+            .replace(/\n/g, '<br>');
+        
+        // 将占位符替换为img标签
+        solutionImagePlaceholders.forEach((url, index) => {
+            const placeholder = `__SOLUTION_IMAGE_PLACEHOLDER_${index}__`;
+            const imgTag = `<img src="${url}" class="max-w-[400px] max-h-[300px] w-auto h-auto my-2 rounded-lg mx-auto block" alt="题目图片" style="object-fit: contain;">`;
+            solutionHtml = solutionHtml.replace(placeholder, imgTag);
+        });
+        
+        html += `<div class="text-sm leading-relaxed">${solutionHtml}</div>`;
+    } else {
+        html += '<p class="text-xs text-gray-400">暂无解析</p>';
+    }
+    html += '</div>';
+    
+    html += '</div>';
+    
+    previewContent.innerHTML = html;
+    
+    // 渲染LaTeX
+    if (window.renderMathInElement) {
+        try {
+            window.renderMathInElement(previewContent, {
+                delimiters: [
+                    {left: "$$", right: "$$", display: true},
+                    {left: "$", right: "$", display: false},
+                    {left: "\\(", right: "\\)", display: false},
+                    {left: "\\[", right: "\\]", display: true}
+                ],
+                throwOnError: false
+            });
+        } catch (e) {
+            console.warn('LaTeX 渲染失败:', e);
+        }
+    }
+    
+    // 显示预览气泡
+    bubble.classList.remove('hidden');
+}
+
+function displayBatchPreview() {
+    const previewDiv = document.getElementById('batch-preview');
+    const questionsList = document.getElementById('questions-list');
+    
+    previewDiv.classList.remove('hidden');
+    document.getElementById('batch-submit-btn').classList.remove('hidden');
+    
+    questionsList.innerHTML = '';
+    
+    // 创建所有题目卡片(默认隐藏)
+    parsedQuestions.forEach((q, index) => {
+        const questionId = `batch-q-card-${index}`;
+        const questionDiv = document.createElement('div');
+        questionDiv.className = `batch-question-card apple-card p-6 space-y-4 ${index === 0 ? '' : 'hidden'}`;
+        questionDiv.id = questionId;
+        questionDiv.setAttribute('data-index', index);
+        
+        // 解析选项
+        let optionsJson = '{}';
+        let optionA = '', optionB = '', optionC = '', optionD = '';
+        if (q.options && typeof q.options === 'object') {
+            optionsJson = JSON.stringify(q.options, null, 2);
+            optionA = q.options.A || '';
+            optionB = q.options.B || '';
+            optionC = q.options.C || '';
+            optionD = q.options.D || '';
+        }
+        
+        // 映射题型
+        const questionType = mapQuestionType(q.question_type);
+        
+        questionDiv.innerHTML = `
+            <div class="border-b border-gray-100 pb-3 mb-3">
+                <div class="flex items-center gap-2">
+                    <span class="bg-blue-100 text-blue-700 px-2 py-0.5 rounded text-xs font-bold">题号 ${q.number || index + 1}</span>
+                </div>
+            </div>
+            
+            <!-- 题型和难度选择 -->
+            <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
+                <div class="space-y-1">
+                    <label class="text-xs font-bold text-gray-400 uppercase">题型</label>
+                    <select name="question_type" class="batch-question-type w-full p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10 outline-none transition-all text-sm">
+                        <option value="choice" ${questionType === 'choice' ? 'selected' : ''}>选择题</option>
+                        <option value="fill" ${questionType === 'fill' ? 'selected' : ''}>填空题</option>
+                        <option value="answer" ${questionType === 'answer' ? 'selected' : ''}>解答题</option>
+                    </select>
+                </div>
+                <div class="space-y-1">
+                    <div class="flex items-center gap-2">
+                        <label class="text-xs font-bold text-gray-400 uppercase">难度</label>
+                        <button type="button" class="batch-evaluate-difficulty-btn btn-apple bg-gradient-to-r from-purple-500 to-indigo-600 text-white hover:from-purple-600 hover:to-indigo-700 text-xs py-1 px-2 shadow-md whitespace-nowrap" data-index="${index}" onclick="evaluateBatchDifficulty(${index})">
+                            <svg class="w-3 h-3 inline-block mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
+                            </svg>
+                            难度评价
+                        </button>
+                    </div>
+                    <select name="difficulty" class="batch-difficulty w-full p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10 outline-none transition-all text-sm">
+                        <option value="">请选择难度</option>
+                        <option value="0.2" ${q.difficulty == 0.2 || q.difficulty == '0.2' ? 'selected' : ''}>筑基</option>
+                        <option value="0.4" ${q.difficulty == 0.4 || q.difficulty == '0.4' ? 'selected' : ''}>提分</option>
+                        <option value="0.7" ${q.difficulty == 0.7 || q.difficulty == '0.7' ? 'selected' : ''}>培优</option>
+                    </select>
+                </div>
+            </div>
+            
+            <!-- 题干编辑 -->
+            <div class="space-y-2">
+                <div class="flex items-center justify-between">
+                    <label class="text-xs font-bold text-gray-400 uppercase">题干 <span class="text-red-500">*</span> (支持 LaTeX 和 HTML/SVG)</label>
+                    <button type="button" class="batch-upload-stem btn-apple bg-blue-600 text-white hover:bg-blue-700 text-xs py-1.5 px-3 h-fit" data-index="${index}">上传图片</button>
+                </div>
+                <textarea name="stem" class="batch-stem w-full h-32 p-3 rounded-xl bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10 outline-none transition-all font-mono text-sm" 
+                    placeholder="请输入题干内容或拖拽图片..."
+                    data-index="${index}"
+                    data-type="stem"
+                    required>${escapeHtml(q.stem || '')}</textarea>
+            </div>
+            
+            <!-- 选项编辑 -->
+            <div class="batch-options-section space-y-2" style="display: ${questionType === 'choice' ? 'block' : 'none'}">
+                <label class="text-xs font-bold text-gray-400 uppercase">选项</label>
+                <div class="grid grid-cols-2 gap-3">
+                    <div class="option-item">
+                        <label class="text-xs font-medium text-gray-600 mb-1 block">选项 A</label>
+                        <div class="flex gap-1.5">
+                            <textarea class="batch-option-A flex-1 h-12 p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-1 focus:ring-blue-500/10 outline-none transition-all text-xs"
+                                placeholder="输入选项A的内容或拖拽图片..."
+                                data-index="${index}" data-option="A" data-type="option">${escapeHtml(optionA)}</textarea>
+                            <button type="button" class="batch-upload-option btn-apple bg-blue-600 text-white hover:bg-blue-700 text-xs py-1.5 px-2 h-fit self-start" data-index="${index}" data-option="A">上传图片</button>
+                        </div>
+                    </div>
+                    <div class="option-item">
+                        <label class="text-xs font-medium text-gray-600 mb-1 block">选项 B</label>
+                        <div class="flex gap-1.5">
+                            <textarea class="batch-option-B flex-1 h-12 p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-1 focus:ring-blue-500/10 outline-none transition-all text-xs"
+                                placeholder="输入选项B的内容或拖拽图片..."
+                                data-index="${index}" data-option="B" data-type="option">${escapeHtml(optionB)}</textarea>
+                            <button type="button" class="batch-upload-option btn-apple bg-blue-600 text-white hover:bg-blue-700 text-xs py-1.5 px-2 h-fit self-start" data-index="${index}" data-option="B">上传图片</button>
+                        </div>
+                    </div>
+                    <div class="option-item">
+                        <label class="text-xs font-medium text-gray-600 mb-1 block">选项 C</label>
+                        <div class="flex gap-1.5">
+                            <textarea class="batch-option-C flex-1 h-12 p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-1 focus:ring-blue-500/10 outline-none transition-all text-xs"
+                                placeholder="输入选项C的内容或拖拽图片..."
+                                data-index="${index}" data-option="C" data-type="option">${escapeHtml(optionC)}</textarea>
+                            <button type="button" class="batch-upload-option btn-apple bg-blue-600 text-white hover:bg-blue-700 text-xs py-1.5 px-2 h-fit self-start" data-index="${index}" data-option="C">上传图片</button>
+                        </div>
+                    </div>
+                    <div class="option-item">
+                        <label class="text-xs font-medium text-gray-600 mb-1 block">选项 D</label>
+                        <div class="flex gap-1.5">
+                            <textarea class="batch-option-D flex-1 h-12 p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-1 focus:ring-blue-500/10 outline-none transition-all text-xs"
+                                placeholder="输入选项D的内容或拖拽图片..."
+                                data-index="${index}" data-option="D" data-type="option">${escapeHtml(optionD)}</textarea>
+                            <button type="button" class="batch-upload-option btn-apple bg-blue-600 text-white hover:bg-blue-700 text-xs py-1.5 px-2 h-fit self-start" data-index="${index}" data-option="D">上传图片</button>
+                        </div>
+                    </div>
+                </div>
+                <div class="mt-2 p-2 rounded-lg bg-gray-50 border border-gray-200">
+                    <label class="text-xs font-bold text-gray-600 mb-1 block">选项预览 (JSON格式,可直接编辑)</label>
+                    <textarea class="batch-options-preview w-full text-xs text-gray-700 font-mono bg-white p-2 rounded border border-gray-100 min-h-[80px] focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10 outline-none transition-all resize-y"
+                        data-index="${index}"
+                        placeholder='{"A": "选项A内容", "B": "选项B内容", ...}'>${escapeHtml(optionsJson)}</textarea>
+                </div>
+            </div>
+            
+            <!-- 答案 -->
+            <div class="space-y-1">
+                <label class="text-xs font-bold text-gray-400 uppercase">正确答案</label>
+                <input type="text" name="answer" class="batch-answer w-full p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10 outline-none transition-all text-sm" 
+                    placeholder="例如: A" value="${escapeHtml(q.answer || '')}">
+            </div>
+            
+            <!-- 解析 -->
+            <div class="space-y-2">
+                <label class="text-xs font-bold text-gray-400 uppercase">解析</label>
+                <textarea name="solution" class="batch-solution w-full h-32 p-3 rounded-xl bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10 outline-none transition-all font-mono text-sm" 
+                    placeholder="请输入解析内容或拖拽图片..."
+                    data-index="${index}"
+                    data-type="solution">${escapeHtml(q.solution || '')}</textarea>
+            </div>
+        `;
+        
+        questionsList.appendChild(questionDiv);
+        
+        // 设置选项同步逻辑
+        setupBatchQuestionEvents(index);
+    });
+    
+    // 显示当前题并更新导航(不重置索引)
+    showBatchQuestion(currentBatchQuestionIndex);
+    updateBatchNavigation();
+    
+    // 初始化预览
+    updateBatchFullPreview();
+}
+
+function escapeHtml(text) {
+    if (!text) return '';
+    const div = document.createElement('div');
+    div.textContent = text;
+    return div.innerHTML;
+}
+
+function setupBatchQuestionEvents(index) {
+    // 题型变化时显示/隐藏选项
+    const questionDiv = document.getElementById(`batch-q-card-${index}`);
+    if (!questionDiv) return;
+    
+    const typeSelect = questionDiv.querySelector('.batch-question-type');
+    const optionsSection = questionDiv.querySelector('.batch-options-section');
+    
+    typeSelect.addEventListener('change', function() {
+        if (this.value === 'choice') {
+            optionsSection.style.display = 'block';
+        } else {
+            optionsSection.style.display = 'none';
+        }
+        updateBatchFullPreview();
+    });
+    
+    // 选项输入框同步到预览
+    ['A', 'B', 'C', 'D'].forEach(opt => {
+        const input = questionDiv.querySelector(`.batch-option-${opt}`);
+        if (input) {
+            input.addEventListener('input', function() {
+                updateBatchOptionsPreview(index);
+                updateBatchFullPreview();
+            });
+        }
+    });
+    
+    // 预览框同步到输入框
+    const previewTextarea = questionDiv.querySelector('.batch-options-preview');
+    if (previewTextarea) {
+        previewTextarea.addEventListener('input', function() {
+            syncBatchOptionsFromPreview(index);
+            updateBatchFullPreview();
+        });
+    }
+    
+    // 题干、答案、解析字段变化时更新预览和拖拽上传
+    const stemTextarea = questionDiv.querySelector('.batch-stem');
+    if (stemTextarea) {
+        stemTextarea.addEventListener('input', updateBatchFullPreview);
+        // 拖拽上传功能
+        stemTextarea.addEventListener('dragover', function(e) {
+            e.preventDefault();
+            e.stopPropagation();
+            this.classList.add('border-blue-500', 'bg-blue-50');
+        });
+        stemTextarea.addEventListener('dragleave', function(e) {
+            e.preventDefault();
+            e.stopPropagation();
+            this.classList.remove('border-blue-500', 'bg-blue-50');
+        });
+        stemTextarea.addEventListener('drop', async function(e) {
+            e.preventDefault();
+            e.stopPropagation();
+            this.classList.remove('border-blue-500', 'bg-blue-50');
+            const files = e.dataTransfer.files;
+            if (files && files.length > 0 && files[0].type.startsWith('image/')) {
+                await uploadBatchImageFile(files[0], index, 'stem');
+            }
+        });
+    }
+    
+    const answerInput = questionDiv.querySelector('.batch-answer');
+    if (answerInput) {
+        answerInput.addEventListener('input', updateBatchFullPreview);
+    }
+    
+    const solutionTextarea = questionDiv.querySelector('.batch-solution');
+    if (solutionTextarea) {
+        solutionTextarea.addEventListener('input', updateBatchFullPreview);
+        // 拖拽上传功能
+        solutionTextarea.addEventListener('dragover', function(e) {
+            e.preventDefault();
+            e.stopPropagation();
+            this.classList.add('border-blue-500', 'bg-blue-50');
+        });
+        solutionTextarea.addEventListener('dragleave', function(e) {
+            e.preventDefault();
+            e.stopPropagation();
+            this.classList.remove('border-blue-500', 'bg-blue-50');
+        });
+        solutionTextarea.addEventListener('drop', async function(e) {
+            e.preventDefault();
+            e.stopPropagation();
+            this.classList.remove('border-blue-500', 'bg-blue-50');
+            const files = e.dataTransfer.files;
+            if (files && files.length > 0 && files[0].type.startsWith('image/')) {
+                await uploadBatchImageFile(files[0], index, 'solution');
+            }
+        });
+    }
+    
+    const difficultySelect = questionDiv.querySelector('.batch-difficulty');
+    if (difficultySelect) {
+        difficultySelect.addEventListener('change', updateBatchFullPreview);
+    }
+    
+    // 图片上传按钮
+    const uploadStemBtn = questionDiv.querySelector('.batch-upload-stem');
+    if (uploadStemBtn) {
+        uploadStemBtn.addEventListener('click', function() {
+            uploadBatchImage(index, 'stem');
+        });
+    }
+    
+    ['A', 'B', 'C', 'D'].forEach(opt => {
+        const uploadBtn = questionDiv.querySelector(`.batch-upload-option[data-option="${opt}"]`);
+        if (uploadBtn) {
+            uploadBtn.addEventListener('click', function() {
+                uploadBatchImage(index, 'option', opt);
+            });
+        }
+    });
+    
+    // 选项拖拽上传功能
+    ['A', 'B', 'C', 'D'].forEach(opt => {
+        const optionInput = questionDiv.querySelector(`.batch-option-${opt}`);
+        if (optionInput) {
+            optionInput.addEventListener('dragover', function(e) {
+                e.preventDefault();
+                e.stopPropagation();
+                this.classList.add('border-blue-500', 'bg-blue-50');
+            });
+            optionInput.addEventListener('dragleave', function(e) {
+                e.preventDefault();
+                e.stopPropagation();
+                this.classList.remove('border-blue-500', 'bg-blue-50');
+            });
+            optionInput.addEventListener('drop', async function(e) {
+                e.preventDefault();
+                e.stopPropagation();
+                this.classList.remove('border-blue-500', 'bg-blue-50');
+                const files = e.dataTransfer.files;
+                if (files && files.length > 0 && files[0].type.startsWith('image/')) {
+                    await uploadBatchImageFile(files[0], index, 'option', opt);
+                }
+            });
+        }
+    });
+    
+    // 移除重复的拖拽代码
+    const solutionTextareaDuplicate = questionDiv.querySelector('.batch-solution');
+    if (solutionTextarea) {
+        solutionTextarea.addEventListener('dragover', function(e) {
+            e.preventDefault();
+            e.stopPropagation();
+            this.classList.add('border-blue-500', 'bg-blue-50');
+        });
+        solutionTextarea.addEventListener('dragleave', function(e) {
+            e.preventDefault();
+            e.stopPropagation();
+            this.classList.remove('border-blue-500', 'bg-blue-50');
+        });
+        solutionTextarea.addEventListener('drop', async function(e) {
+            e.preventDefault();
+            e.stopPropagation();
+            this.classList.remove('border-blue-500', 'bg-blue-50');
+            const files = e.dataTransfer.files;
+            if (files && files.length > 0 && files[0].type.startsWith('image/')) {
+                await uploadBatchImageFile(files[0], index, 'solution');
+            }
+        });
+    }
+    
+    ['A', 'B', 'C', 'D'].forEach(opt => {
+        const optionInput = questionDiv.querySelector(`.batch-option-${opt}`);
+        if (optionInput) {
+            optionInput.addEventListener('dragover', function(e) {
+                e.preventDefault();
+                e.stopPropagation();
+                this.classList.add('border-blue-500', 'bg-blue-50');
+            });
+            optionInput.addEventListener('dragleave', function(e) {
+                e.preventDefault();
+                e.stopPropagation();
+                this.classList.remove('border-blue-500', 'bg-blue-50');
+            });
+            optionInput.addEventListener('drop', async function(e) {
+                e.preventDefault();
+                e.stopPropagation();
+                this.classList.remove('border-blue-500', 'bg-blue-50');
+                const files = e.dataTransfer.files;
+                if (files && files.length > 0 && files[0].type.startsWith('image/')) {
+                    await uploadBatchImageFile(files[0], index, 'option', opt);
+                }
+            });
+        }
+    });
+    
+    // 粘贴图片功能 - 完全按照录入题目的逻辑
+    const allInputs = questionDiv.querySelectorAll('textarea, input[type="text"]');
+    allInputs.forEach(input => {
+        input.addEventListener('paste', async function(e) {
+            const clipboardData = e.clipboardData || window.clipboardData;
+            if (!clipboardData) {
+                return;
+            }
+            
+            // 检查是否有图片数据
+            const items = clipboardData.items;
+            if (!items) {
+                return;
+            }
+            
+            for (let i = 0; i < items.length; i++) {
+                const item = items[i];
+                
+                // 如果是图片类型
+                if (item.type.indexOf('image') !== -1) {
+                    e.preventDefault(); // 阻止默认粘贴行为
+                    
+                    const file = item.getAsFile();
+                    if (file) {
+                        // 直接上传图片并插入到当前输入框(完全按照录入题目的逻辑)
+                        await uploadBatchImageToInput(file, input, index);
+                    }
+                    break;
+                }
+            }
+        });
+    });
+}
+
+function updateBatchOptionsPreview(index) {
+    const questionDiv = document.querySelector(`[data-index="${index}"]`);
+    const previewTextarea = questionDiv.querySelector('.batch-options-preview');
+    const optionsObj = {};
+    
+    ['A', 'B', 'C', 'D'].forEach(opt => {
+        const input = questionDiv.querySelector(`.batch-option-${opt}`);
+        if (input && input.value.trim()) {
+            optionsObj[opt] = input.value.trim();
+        }
+    });
+    
+    previewTextarea.value = JSON.stringify(optionsObj, null, 2);
+}
+
+function syncBatchOptionsFromPreview(index) {
+    const questionDiv = document.querySelector(`[data-index="${index}"]`);
+    const previewTextarea = questionDiv.querySelector('.batch-options-preview');
+    
+    try {
+        const optionsObj = JSON.parse(previewTextarea.value.trim() || '{}');
+        ['A', 'B', 'C', 'D'].forEach(opt => {
+            const input = questionDiv.querySelector(`.batch-option-${opt}`);
+            if (input) {
+                input.value = optionsObj[opt] || '';
+            }
+        });
+    } catch (error) {
+        console.warn('选项预览 JSON 格式错误:', error);
+    }
+}
+
+function uploadBatchImage(index, type, option = null) {
+    const fileInput = document.createElement('input');
+    fileInput.type = 'file';
+    fileInput.accept = 'image/*';
+    fileInput.style.display = 'none';
+    
+    fileInput.onchange = async (e) => {
+        const file = e.target.files[0];
+        if (!file) return;
+        
+        if (!file.type.startsWith('image/')) {
+            if (window.customAlert) {
+                window.customAlert('请选择图片文件!');
+            } else {
+                alert('请选择图片文件!');
+            }
+            return;
+        }
+        
+        await uploadBatchImageFile(file, index, type, option);
+    };
+    
+    document.body.appendChild(fileInput);
+    fileInput.click();
+    document.body.removeChild(fileInput);
+}
+
+// 通用图片上传函数:上传图片并插入到指定输入框(完全按照录入题目的逻辑)
+async function uploadBatchImageToInput(file, inputElement, index) {
+    if (!inputElement || !file) {
+        return;
+    }
+    
+    // 检查文件类型
+    if (!file.type.startsWith('image/')) {
+        if (window.customAlert) {
+            window.customAlert('请选择图片文件!');
+        } else {
+            alert('请选择图片文件!');
+        }
+        return;
+    }
+    
+    // 显示上传中状态
+    const originalPlaceholder = inputElement.placeholder || '';
+    const originalOpacity = inputElement.style.opacity || '1';
+    inputElement.placeholder = '正在上传图片...';
+    inputElement.style.opacity = '0.6';
+    inputElement.disabled = true;
+    
+    try {
+        const formData = new FormData();
+        formData.append('file', file);
+        
+        const response = await fetch('https://crmapi.dcjxb.yunzhixue.cn/file/upload', {
+            method: 'POST',
+            body: formData
+        });
+        
+        if (!response.ok) {
+            throw new Error(`上传失败: ${response.status} ${response.statusText}`);
+        }
+        
+        const result = await response.json();
+        
+        // 检查返回结果,提取URL
+        let imageUrl = null;
+        if (typeof result === 'string') {
+            imageUrl = result;
+        } else if (result.url) {
+            imageUrl = result.url;
+        } else if (result.data && result.data.url) {
+            imageUrl = result.data.url;
+        } else if (result.data && typeof result.data === 'string') {
+            imageUrl = result.data;
+        } else {
+            const resultStr = JSON.stringify(result);
+            const urlMatch = resultStr.match(/https?:\/\/[^\s"']+/);
+            if (urlMatch) {
+                imageUrl = urlMatch[0];
+            }
+        }
+        
+        if (!imageUrl) {
+            throw new Error('接口返回格式异常,未找到图片URL。返回结果:' + JSON.stringify(result));
+        }
+        
+        // 构建 image 标签
+        const imageTag = `<image src="${imageUrl}"/>`;
+        
+        // 获取当前光标位置
+        const cursorPos = inputElement.selectionStart || inputElement.value.length;
+        const textBefore = inputElement.value.substring(0, cursorPos);
+        const textAfter = inputElement.value.substring(cursorPos);
+        
+        // 插入 image 标签
+        inputElement.value = textBefore + imageTag + textAfter;
+        
+        // 设置光标位置到插入内容之后
+        const newCursorPos = cursorPos + imageTag.length;
+        if (inputElement.setSelectionRange) {
+            inputElement.setSelectionRange(newCursorPos, newCursorPos);
+        }
+        inputElement.focus();
+        
+        // 触发input事件,更新预览
+        inputElement.dispatchEvent(new Event('input', { bubbles: true }));
+        
+        // 如果是选项输入框,更新选项预览
+        if (inputElement.classList.contains('batch-option-A') || 
+            inputElement.classList.contains('batch-option-B') ||
+            inputElement.classList.contains('batch-option-C') ||
+            inputElement.classList.contains('batch-option-D')) {
+            updateBatchOptionsPreview(index);
+        } else if (inputElement.classList.contains('batch-options-preview')) {
+            // 如果是在选项预览框中粘贴,需要同步到各个选项输入框
+            syncBatchOptionsFromPreview(index);
+        }
+        
+        // 更新完整预览
+        updateBatchFullPreview();
+        
+    } catch (error) {
+        console.error('上传失败:', error);
+        if (window.customAlert) {
+            window.customAlert('图片上传失败: ' + error.message);
+        } else {
+            alert('图片上传失败: ' + error.message);
+        }
+    } finally {
+        inputElement.placeholder = originalPlaceholder;
+        inputElement.style.opacity = originalOpacity;
+        inputElement.disabled = false;
+    }
+}
+
+// 保留旧函数以兼容拖拽上传
+async function uploadBatchImageFile(file, index, type, option) {
+    const questionDiv = document.getElementById(`batch-q-card-${index}`);
+    if (!questionDiv) {
+        questionDiv = document.querySelector(`[data-index="${index}"]`);
+    }
+    if (!questionDiv) return;
+    
+    let targetInput;
+    
+    if (type === 'stem') {
+        targetInput = questionDiv.querySelector('.batch-stem');
+    } else if (type === 'solution') {
+        targetInput = questionDiv.querySelector('.batch-solution');
+    } else if (type === 'answer') {
+        targetInput = questionDiv.querySelector('.batch-answer');
+    } else if (type === 'options-preview') {
+        targetInput = questionDiv.querySelector('.batch-options-preview');
+    } else if (type === 'option' && option) {
+        targetInput = questionDiv.querySelector(`.batch-option-${option}`);
+    }
+    
+    if (!targetInput) return;
+    
+    // 使用新的通用函数
+    await uploadBatchImageToInput(file, targetInput, index);
+}
+
+async function submitBatchQuestions() {
+    const questionDivs = document.querySelectorAll('[data-index]');
+    if (questionDivs.length === 0) {
+        if (window.customAlert) {
+            window.customAlert('没有可提交的题目');
+        } else {
+            alert('没有可提交的题目');
+        }
+        return;
+    }
+    
+    const submitBtn = document.getElementById('batch-submit-btn');
+    submitBtn.disabled = true;
+    submitBtn.textContent = '提交中...';
+    
+    let successCount = 0;
+    let failCount = 0;
+    const errors = [];
+    
+    // 获取用户姓名
+    const userName = localStorage.getItem('user_name') || '';
+    
+    // 逐个提交题目(从表单中读取数据)
+    for (let i = 0; i < questionDivs.length; i++) {
+        const questionDiv = questionDivs[i];
+        const index = parseInt(questionDiv.getAttribute('data-index'));
+        
+        try {
+            // 从表单中读取数据
+            const stem = questionDiv.querySelector('.batch-stem').value.trim();
+            const answer = questionDiv.querySelector('.batch-answer').value.trim();
+            const solution = questionDiv.querySelector('.batch-solution').value.trim();
+            const questionType = questionDiv.querySelector('.batch-question-type').value;
+            const difficulty = questionDiv.querySelector('.batch-difficulty').value;
+            
+            // 验证必填项(仅题干)
+            if (!stem) {
+                failCount++;
+                errors.push(`题号 ${parsedQuestions[index]?.number || i + 1}: 缺少必填项(题干)`);
+                continue;
+            }
+            
+            // 构建题目数据
+            const questionData = {
+                stem: stem,
+                answer: answer,
+                solution: solution,
+                question_type: questionType,
+                kp_code: currentHierarchyInfo.kp_code,
+                chapter: currentHierarchyInfo.chapter,
+                section: currentHierarchyInfo.section,
+                subsection: currentHierarchyInfo.subsection
+            };
+            
+            // 处理难度(可选)
+            if (difficulty) {
+                questionData.difficulty = parseFloat(difficulty);
+            }
+            
+            // 处理选项(仅选择题)
+            if (questionType === 'choice') {
+                const previewTextarea = questionDiv.querySelector('.batch-options-preview');
+                if (previewTextarea && previewTextarea.value.trim() && previewTextarea.value.trim() !== '{}') {
+                    try {
+                        const optionsObj = JSON.parse(previewTextarea.value.trim());
+                        if (Object.keys(optionsObj).length > 0) {
+                            questionData.options = JSON.stringify(optionsObj);
+                        }
+                    } catch (e) {
+                        // JSON解析失败,从输入框收集
+                        const optionsObj = {};
+                        ['A', 'B', 'C', 'D'].forEach(opt => {
+                            const input = questionDiv.querySelector(`.batch-option-${opt}`);
+                            if (input && input.value.trim()) {
+                                optionsObj[opt] = input.value.trim();
+                            }
+                        });
+                        if (Object.keys(optionsObj).length > 0) {
+                            questionData.options = JSON.stringify(optionsObj);
+                        }
+                    }
+                } else {
+                    // 预览为空,从输入框收集
+                    const optionsObj = {};
+                    ['A', 'B', 'C', 'D'].forEach(opt => {
+                        const input = questionDiv.querySelector(`.batch-option-${opt}`);
+                        if (input && input.value.trim()) {
+                            optionsObj[opt] = input.value.trim();
+                        }
+                    });
+                    if (Object.keys(optionsObj).length > 0) {
+                        questionData.options = JSON.stringify(optionsObj);
+                    }
+                }
+            }
+            
+            // 添加创建者
+            if (userName) {
+                questionData.create_by = userName;
+            }
+            
+            // 提交到后端
+            const response = await fetch('/create_question', {
+                method: 'POST',
+                headers: {'Content-Type': 'application/json'},
+                body: JSON.stringify(questionData)
+            });
+            
+            const result = await response.json();
+            
+            if (result.success) {
+                successCount++;
+            } else {
+                failCount++;
+                errors.push(`题号 ${parsedQuestions[index]?.number || i + 1}: ${result.error || '未知错误'}`);
+            }
+            
+        } catch (error) {
+            failCount++;
+            errors.push(`题号 ${parsedQuestions[index]?.number || i + 1}: ${error.message}`);
+        }
+    }
+    
+    submitBtn.disabled = false;
+    submitBtn.textContent = '批量提交';
+    
+    // 显示结果(仅显示成功数量)
+    const message = `成功导入 ${successCount} 道题`;
+    
+    if (window.customAlert) {
+        window.customAlert(message, () => {
+            if (successCount > 0) {
+                // 刷新页面
+                window.location.reload();
+            }
+        });
+    } else {
+        alert(message);
+        if (successCount > 0) {
+            window.location.reload();
+        }
+    }
+}
+
+// 批量导入的难度评价函数
+async function evaluateBatchDifficulty(index) {
+    const questionDiv = document.getElementById(`batch-q-card-${index}`);
+    if (!questionDiv) return;
+    
+    const btn = questionDiv.querySelector('.batch-evaluate-difficulty-btn');
+    const difficultySelect = questionDiv.querySelector('.batch-difficulty');
+    
+    if (!btn || !difficultySelect) return;
+    
+    // 收集题目信息
+    const stemTextarea = questionDiv.querySelector('.batch-stem');
+    const answerInput = questionDiv.querySelector('.batch-answer');
+    const solutionTextarea = questionDiv.querySelector('.batch-solution');
+    const optionsPreview = questionDiv.querySelector('.batch-options-preview');
+    const questionTypeSelect = questionDiv.querySelector('.batch-question-type');
+    
+    const stem = stemTextarea ? stemTextarea.value.trim() : '';
+    
+    if (!stem) {
+        if (window.customAlert) {
+            window.customAlert('请先填写题干内容');
+        } else {
+            alert('请先填写题干内容');
+        }
+        return;
+    }
+    
+    // 构建请求数据
+    const requestData = {
+        stem: stem,
+        answer: answerInput ? answerInput.value.trim() : '',
+        solution: solutionTextarea ? solutionTextarea.value.trim() : '',
+        question_type: questionTypeSelect ? questionTypeSelect.value : ''
+    };
+    
+    // 处理选项
+    if (optionsPreview && optionsPreview.value.trim() && optionsPreview.value.trim() !== '{}') {
+        try {
+            requestData.options = JSON.parse(optionsPreview.value.trim());
+        } catch (e) {
+            console.warn('选项JSON解析失败:', e);
+        }
+    }
+    
+    // 显示加载状态
+    const originalText = btn.innerHTML;
+    btn.disabled = true;
+    btn.innerHTML = '<svg class="w-3 h-3 inline-block mr-1 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>评价中...';
+    
+    try {
+        const response = await fetch('/api/score', {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json'
+            },
+            body: JSON.stringify(requestData)
+        });
+        
+        if (!response.ok) {
+            throw new Error(`请求失败: ${response.status} ${response.statusText}`);
+        }
+        
+        const result = await response.json();
+        
+        // 调试:打印完整返回结果
+        console.log('难度评价接口返回:', result);
+        
+        // 处理返回的 difficulty_level(优先使用 data.difficulty_level,兼容旧格式)
+        let difficultyLevel = result.data?.difficulty_level || result.difficulty_level || result.difficulty || result.level;
+        
+        // 映射难度等级到枚举值
+        let difficultyValue = '';
+        
+        if (difficultyLevel !== undefined && difficultyLevel !== null) {
+            const levelStr = String(difficultyLevel).trim();
+            
+            // 字符串匹配
+            if (levelStr === '筑基' || levelStr === '0.2' || levelStr === '0.20') {
+                difficultyValue = '0.2';
+            } else if (levelStr === '提分' || levelStr === '0.4' || levelStr === '0.40') {
+                difficultyValue = '0.4';
+            } else if (levelStr === '培优' || levelStr === '0.7' || levelStr === '0.70') {
+                difficultyValue = '0.7';
+            } else {
+                // 尝试转换为数字
+                const levelNum = parseFloat(levelStr);
+                if (!isNaN(levelNum)) {
+                    if (Math.abs(levelNum - 0.2) < 0.1) {
+                        difficultyValue = '0.2';
+                    } else if (Math.abs(levelNum - 0.4) < 0.1) {
+                        difficultyValue = '0.4';
+                    } else if (Math.abs(levelNum - 0.7) < 0.1) {
+                        difficultyValue = '0.7';
+                    }
+                }
+            }
+        }
+        
+               if (difficultyValue) {
+                   difficultySelect.value = difficultyValue;
+                   // 触发change事件以更新预览
+                   difficultySelect.dispatchEvent(new Event('change', { bubbles: true }));
+                   updateBatchFullPreview();
+                   // 不显示弹窗,直接完成
+               } else {
+            // 如果无法识别,打印完整返回结果以便调试
+            console.error('无法识别难度等级,完整返回结果:', result);
+            if (window.customAlert) {
+                window.customAlert('无法识别返回的难度等级。返回数据:' + JSON.stringify(result));
+            } else {
+                alert('无法识别返回的难度等级。返回数据:' + JSON.stringify(result));
+            }
+        }
+        
+    } catch (error) {
+        console.error('难度评价失败:', error);
+        if (window.customAlert) {
+            window.customAlert('难度评价失败: ' + error.message);
+        } else {
+            alert('难度评价失败: ' + error.message);
+        }
+    } finally {
+        // 恢复按钮
+        btn.disabled = false;
+        btn.innerHTML = originalText;
+    }
+}
+
+// 映射题型:将中文转换为英文
+function mapQuestionType(type) {
+    if (!type) return 'choice';
+    const typeMap = {
+        '选择题': 'choice',
+        '填空题': 'fill',
+        '解答题': 'answer',
+        'choice': 'choice',
+        'fill': 'fill',
+        'answer': 'answer'
+    };
+    return typeMap[type] || 'choice';
+}
+
+// 移除点击背景关闭功能,只能通过X按钮关闭
+// document.getElementById('batch-import-modal').addEventListener('click', function(e) {
+//     if (e.target === this) {
+//         hideBatchImportModal();
+//     }
+// });
+</script>
+{% endblock %}
+

+ 28 - 0
templates/search_id_not_found.html

@@ -0,0 +1,28 @@
+{% extends "layout.html" %}
+
+{% block title %}未找到题目ID - 知了数学题库系统{% endblock %}
+
+{% block content %}
+<div class="apple-card p-10">
+  <div class="flex items-start justify-between gap-6">
+    <div>
+      <h1 class="text-2xl font-bold mb-2">没找到这个主键ID</h1>
+      <p class="text-gray-500">你输入的 <span class="font-mono text-gray-800">{{ q }}</span> 在数据库里没有匹配到题目。</p>
+      <div class="text-xs text-gray-400 mt-2">
+        当前支持的主键列:<span class="font-mono">{{ pk_cols|join(', ') }}</span>
+      </div>
+    </div>
+    <a href="javascript:history.back()" class="btn-apple bg-blue-600 text-white hover:bg-blue-700 no-print">返回上一页</a>
+  </div>
+
+  <div class="mt-8 text-sm text-gray-600">
+    <div class="font-bold mb-2">提示:</div>
+    <ul class="list-disc pl-6 space-y-1">
+      <li>确认你输入的是纯数字/完整ID(不要带空格)</li>
+      <li>确认你连的是正确数据库(可检查 DB_DATABASE)</li>
+    </ul>
+  </div>
+</div>
+{% endblock %}
+
+

+ 30 - 0
templates/search_id_not_supported.html

@@ -0,0 +1,30 @@
+{% extends "layout.html" %}
+
+{% block title %}主键ID搜索不可用 - 知了数学题库系统{% endblock %}
+
+{% block content %}
+<div class="apple-card p-10">
+  <div class="flex items-start justify-between gap-6">
+    <div>
+      <h1 class="text-2xl font-bold mb-2">这个数据库暂不支持按主键ID搜索</h1>
+      <p class="text-gray-500">
+        我在 <span class="font-mono">questions</span> 表里没找到常见的主键列名(例如:id / question_id)。
+      </p>
+      {% if q %}
+      <div class="text-xs text-gray-400 mt-2">你刚输入的:<span class="font-mono">{{ q }}</span></div>
+      {% endif %}
+    </div>
+    <a href="javascript:history.back()" class="btn-apple bg-blue-600 text-white hover:bg-blue-700 no-print">返回上一页</a>
+  </div>
+
+  <div class="mt-8 text-sm text-gray-600">
+    <div class="font-bold mb-2">你可以这样解决:</div>
+    <ul class="list-disc pl-6 space-y-1">
+      <li>告诉我你们表的真实主键字段名(例如:<span class="font-mono">qid</span> 或 <span class="font-mono">question_pk</span>)</li>
+      <li>或者直接继续用题号(question_code)搜索</li>
+    </ul>
+  </div>
+</div>
+{% endblock %}
+
+

+ 26 - 0
templates/search_not_found.html

@@ -0,0 +1,26 @@
+{% extends "layout.html" %}
+
+{% block title %}未找到题目 - 知了数学题库系统{% endblock %}
+
+{% block content %}
+<div class="apple-card p-10">
+  <div class="flex items-start justify-between gap-6">
+    <div>
+      <h1 class="text-2xl font-bold mb-2">没找到这道题</h1>
+      <p class="text-gray-500">你输入的 <span class="font-mono text-gray-800">{{ q }}</span> 在数据库里不存在。</p>
+    </div>
+    <a href="javascript:history.back()" class="btn-apple bg-blue-600 text-white hover:bg-blue-700 no-print">返回上一页</a>
+  </div>
+
+  <div class="mt-8 text-sm text-gray-600">
+    <div class="font-bold mb-2">常见原因:</div>
+    <ul class="list-disc pl-6 space-y-1">
+      <li>题号多了空格或少了字符</li>
+      <li>题号里有大小写(请和数据库一致)</li>
+      <li>你连的不是你想要的数据库(可在 config.env / 环境变量里检查 DB_DATABASE)</li>
+    </ul>
+  </div>
+</div>
+{% endblock %}
+
+

+ 1539 - 0
templates/textbook_management.html

@@ -0,0 +1,1539 @@
+{% extends "layout.html" %}
+
+{% block page_title %}教材管理{% endblock %}
+
+{% block content %}
+<!-- 目录节点树状结构宏定义 -->
+{% macro render_catalog_node(node, level) %}
+<div 
+    class="catalog-node relative"
+    data-node-id="{{ node.id }}"
+    data-node-type="{{ node.node_type }}"
+    data-parent="{{ node.parent_id or '' }}"
+    data-level="{{ level }}"
+    data-textbook-id="{{ node.textbook_id }}">
+    
+    <!-- 节点卡片 -->
+    <div class="flex items-start gap-4 group">
+        <!-- 展开/折叠按钮区域 -->
+        <div class="flex items-center gap-2 pt-4 flex-shrink-0">
+            {% if node.children|length > 0 %}
+            <button 
+                onclick="toggleCatalogNode('{{ node.id }}'); event.stopPropagation();"
+                class="w-8 h-8 rounded-full bg-white border-2 border-gray-300 hover:border-blue-500 flex items-center justify-center text-gray-600 hover:text-blue-600 transition-all expand-btn z-10 shadow-sm hover:shadow-md"
+                data-expanded="{% if level == 0 %}true{% else %}false{% endif %}">
+                {% if level == 0 %}
+                <i class="ri-subtract-line text-sm"></i>
+                {% else %}
+                <i class="ri-add-line text-sm"></i>
+                {% endif %}
+            </button>
+            {% else %}
+            <div class="w-8 h-8 flex items-center justify-center">
+                <div class="w-2 h-2 rounded-full bg-gray-400"></div>
+            </div>
+            {% endif %}
+        </div>
+        
+        <!-- 节点内容卡片 -->
+        <div class="flex-1 apple-card p-5 hover:shadow-xl transition-all duration-300 group-hover:border-blue-300 border-2 border-transparent rounded-xl {% if node.children|length > 0 %}cursor-pointer{% endif %}" {% if node.children|length > 0 %}onclick="handleCatalogCardClick(event, '{{ node.id }}')"{% endif %}>
+            <div class="flex items-start justify-between">
+                <div class="flex-1 min-w-0">
+                    <div class="flex items-center gap-3 mb-3 flex-wrap">
+                        <span class="px-3 py-1.5 rounded-lg text-xs font-bold font-mono bg-gradient-to-r {% if node.node_type == 'chapter' %}from-blue-500 to-indigo-600{% elif node.node_type == 'section' %}from-green-500 to-emerald-600{% else %}from-orange-500 to-amber-600{% endif %} text-white shadow-md">
+                            {{ node.display_no or node.node_type }}
+                        </span>
+                        <h3 class="text-lg font-bold text-gray-800 group-hover:text-blue-600 transition-colors">
+                            {{ node.title }}
+                        </h3>
+                    </div>
+                    
+                    <div class="flex items-center gap-4 text-sm text-gray-500 mt-2 flex-wrap">
+                        <span class="flex items-center gap-1.5 px-2 py-1 bg-gray-50 rounded-md">
+                            <i class="ri-file-list-line text-blue-500"></i>
+                            <span>{{ node.node_type }}</span>
+                        </span>
+                        {% if node.depth %}
+                        <span class="flex items-center gap-1.5 px-2 py-1 bg-gray-50 rounded-md">
+                            <i class="ri-stack-line text-green-500"></i>
+                            <span>层级: {{ node.depth }}</span>
+                        </span>
+                        {% endif %}
+                        {% if node.children|length > 0 %}
+                        <span class="flex items-center gap-1.5 px-2 py-1 bg-blue-50 rounded-md text-blue-600">
+                            <i class="ri-node-tree"></i>
+                            <span class="font-semibold">{{ node.children|length }} 个子节点</span>
+                        </span>
+                        {% endif %}
+                    </div>
+                </div>
+                
+                <!-- 操作按钮 -->
+                <div class="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity ml-4 flex-shrink-0" onclick="event.stopPropagation()">
+                    {% if node.node_type == 'section' %}
+                    <button 
+                        onclick="showAddKpRelationModal('{{ node.id }}', '{{ node.title }}')"
+                        class="px-3 py-1.5 text-xs font-semibold text-white bg-gradient-to-r from-purple-500 to-indigo-600 hover:from-purple-600 hover:to-indigo-700 rounded-lg transition-all shadow-sm hover:shadow-md flex items-center gap-1.5"
+                        title="关联知识点">
+                        <i class="ri-link"></i>
+                        <span>关联知识点</span>
+                    </button>
+                    {% endif %}
+                    <button 
+                        onclick="showAddChildCatalogModal('{{ node.id }}', '{{ node.title }}', '{{ node.node_type }}')"
+                        class="px-3 py-1.5 text-xs font-semibold text-white bg-gradient-to-r from-green-500 to-emerald-600 hover:from-green-600 hover:to-emerald-700 rounded-lg transition-all shadow-sm hover:shadow-md flex items-center gap-1.5"
+                        title="添加子节点">
+                        <i class="ri-add-line"></i>
+                        <span>添加子节点</span>
+                    </button>
+                    <button 
+                        onclick="showEditCatalogModal({{ node.id }})"
+                        class="w-9 h-9 rounded-lg text-blue-600 bg-blue-50 hover:bg-blue-100 flex items-center justify-center transition-colors shadow-sm hover:shadow-md"
+                        title="编辑">
+                        <i class="ri-edit-line"></i>
+                    </button>
+                    <button 
+                        onclick="showDeleteCatalogConfirm({{ node.id }}, '{{ node.title }}')"
+                        class="w-9 h-9 rounded-lg text-red-600 bg-red-50 hover:bg-red-100 flex items-center justify-center transition-colors shadow-sm hover:shadow-md"
+                        title="删除">
+                        <i class="ri-delete-bin-line"></i>
+                    </button>
+                </div>
+            </div>
+        </div>
+    </div>
+    
+    <!-- 子节点容器 -->
+    {% if node.children|length > 0 %}
+    <div class="children-container mt-3 ml-12 {% if level >= 1 %}hidden{% endif %}" id="children-{{ node.id }}" data-level="{{ level }}">
+        {% for child in node.children %}
+            {{ render_catalog_node(child, level + 1) }}
+        {% endfor %}
+    </div>
+    {% endif %}
+</div>
+{% endmacro %}
+
+<div class="flex gap-6">
+    <!-- 左侧系列菜单 -->
+    <div class="w-80 flex-shrink-0">
+        <div class="apple-card p-6 sticky top-4 max-h-[calc(100vh-2rem)] overflow-y-auto">
+            <div class="flex items-center justify-between mb-6 pb-3 border-b border-gray-200">
+                <h2 class="text-lg font-bold text-gray-800">教材系列</h2>
+                <button 
+                    onclick="showAddSeriesModal()"
+                    class="btn-apple bg-gradient-to-r from-blue-600 to-indigo-600 text-white hover:from-blue-700 hover:to-indigo-700 text-xs py-1.5 px-3 rounded-lg shadow-sm flex items-center gap-1">
+                    <i class="ri-add-circle-line"></i>
+                    <span>添加</span>
+                </button>
+            </div>
+            
+            <!-- 搜索框 -->
+            <div class="mb-4">
+                <div class="relative">
+                    <i class="ri-search-line absolute left-2.5 top-1/2 transform -translate-y-1/2 text-gray-400 text-xs"></i>
+                    <input 
+                        type="text" 
+                        id="seriesSearchInput"
+                        placeholder="搜索系列..."
+                        class="w-full pl-7 pr-3 py-2 text-xs border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
+                        oninput="filterSeriesList()">
+                </div>
+            </div>
+            
+            <!-- 系列列表 -->
+            <nav class="space-y-1" id="seriesListNav">
+                {% for series in series_list %}
+                <div class="series-item mb-2" data-series-id="{{ series.id }}" data-series-name="{{ series.name }}" data-series-active="{{ series.is_active }}">
+                    <div class="flex items-center gap-2 group">
+                        <div class="flex-1 flex items-center gap-2">
+                            <a href="javascript:void(0)" 
+                               onclick="switchSeries({{ series.id }})"
+                               class="flex-1 block px-4 py-2.5 rounded-xl hover:bg-gradient-to-r hover:from-slate-50 hover:to-blue-50 transition-all border-l-4 border-transparent hover:border-slate-500 series-link {% if loop.first %}bg-gradient-to-r from-slate-50 to-blue-50 border-slate-500{% endif %}">
+                                <span class="text-sm font-semibold text-gray-800 group-hover:text-slate-600 transition-colors">{{ series.name }}</span>
+                            </a>
+                            <!-- 激活状态开关 -->
+                            <label class="relative inline-flex items-center cursor-pointer flex-shrink-0" onclick="event.stopPropagation(); event.preventDefault(); toggleSeriesActiveDirect({{ series.id }}, this);">
+                                <input type="checkbox" class="sr-only peer" {% if series.is_active == 1 %}checked{% endif %} onclick="event.stopPropagation();">
+                                <div class="w-9 h-5 bg-gray-300 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-blue-500 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-blue-600"></div>
+                            </label>
+                        </div>
+                        <button 
+                            onclick="event.stopPropagation(); event.preventDefault(); showEditSeriesModal({{ series.id }});"
+                            class="opacity-0 group-hover:opacity-100 transition-opacity p-1.5 hover:bg-gray-100 rounded text-gray-500 hover:text-blue-600"
+                            title="编辑">
+                            <i class="ri-edit-line text-sm"></i>
+                        </button>
+                    </div>
+                </div>
+                {% endfor %}
+            </nav>
+        </div>
+    </div>
+    
+    <!-- 右侧内容区域 -->
+    <div class="flex-1 space-y-6">
+        <!-- 教材列表(当前系列下的教材) -->
+        <div id="textbookListContainer" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4">
+            <!-- 教材卡片将通过JavaScript动态加载 -->
+        </div>
+
+        <!-- 目录树结构(选中教材后显示) -->
+        <div id="catalogTreeContainer" class="hidden">
+            <div class="flex items-center justify-between mb-4">
+                <h2 class="text-xl font-bold text-gray-800" id="currentTextbookTitle"></h2>
+                <button 
+                    onclick="showAddCatalogModal()"
+                    class="btn-apple bg-gradient-to-r from-blue-600 to-indigo-600 text-white hover:from-blue-700 hover:to-indigo-700 px-4 py-2 rounded-lg shadow-sm flex items-center gap-2">
+                    <i class="ri-add-circle-line"></i>
+                    <span>添加目录节点</span>
+                </button>
+            </div>
+            
+            <div class="space-y-3" id="catalogTree">
+                <!-- 目录树将通过JavaScript动态加载 -->
+            </div>
+        </div>
+    </div>
+</div>
+
+<!-- 添加/编辑教材系列模态框 -->
+<div id="seriesModal" class="hidden fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center">
+    <div class="bg-white rounded-2xl shadow-2xl max-w-md w-full mx-4">
+        <div class="p-6 border-b border-gray-200">
+            <h2 id="seriesModalTitle" class="text-xl font-bold text-gray-800">添加教材系列</h2>
+        </div>
+        <form id="seriesForm" class="p-6 space-y-4">
+            <input type="hidden" id="seriesId" name="id">
+            <div>
+                <label class="block text-sm font-semibold text-gray-700 mb-2">系列名称 *</label>
+                <input type="text" id="seriesName" name="name" required
+                    class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
+            </div>
+            <div>
+                <label class="block text-sm font-semibold text-gray-700 mb-2">标识符</label>
+                <input type="text" id="seriesSlug" name="slug"
+                    class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
+            </div>
+            <div>
+                <label class="flex items-center justify-between cursor-pointer">
+                    <span class="text-sm font-semibold text-gray-700">激活状态</span>
+                    <label class="relative inline-flex items-center cursor-pointer">
+                        <input type="checkbox" id="seriesIsActive" class="sr-only peer" checked>
+                        <div class="w-11 h-6 bg-gray-300 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-blue-500 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
+                    </label>
+                </label>
+                <p class="text-xs text-gray-500 mt-1">激活的系列将在系统中可用</p>
+            </div>
+            <div class="flex gap-3 pt-4">
+                <button type="button" onclick="closeSeriesModal()" class="flex-1 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50">取消</button>
+                <button type="submit" class="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">保存</button>
+            </div>
+        </form>
+    </div>
+</div>
+
+<!-- 添加/编辑教材模态框 -->
+<div id="textbookModal" class="hidden fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center">
+    <div class="bg-white rounded-2xl shadow-2xl max-w-md w-full mx-4">
+        <div class="p-6 border-b border-gray-200">
+            <h2 id="textbookModalTitle" class="text-xl font-bold text-gray-800">添加教材</h2>
+        </div>
+        <form id="textbookForm" class="p-6 space-y-4">
+            <input type="hidden" id="textbookId" name="id">
+            <div>
+                <label class="block text-sm font-semibold text-gray-700 mb-2">教材名称 *</label>
+                <input type="text" id="textbookTitle" name="official_title" required
+                    class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
+            </div>
+            <div class="grid grid-cols-2 gap-4">
+                <div>
+                    <label class="block text-sm font-semibold text-gray-700 mb-2">学段</label>
+                    <select id="textbookStage" name="stage" class="w-full px-4 py-2 border border-gray-300 rounded-lg">
+                        <option value="">请选择</option>
+                        <option value="primary">小学</option>
+                        <option value="junior">初中</option>
+                        <option value="senior">高中</option>
+                    </select>
+                </div>
+                <div>
+                    <label class="block text-sm font-semibold text-gray-700 mb-2">年级</label>
+                    <input type="text" id="textbookGrade" name="grade"
+                        class="w-full px-4 py-2 border border-gray-300 rounded-lg">
+                </div>
+            </div>
+            <div>
+                <label class="block text-sm font-semibold text-gray-700 mb-2">学期</label>
+                <select id="textbookSemester" name="semester" class="w-full px-4 py-2 border border-gray-300 rounded-lg">
+                    <option value="">请选择</option>
+                    <option value="1">上学期</option>
+                    <option value="2">下学期</option>
+                </select>
+            </div>
+            <div class="flex gap-3 pt-4">
+                <button type="button" onclick="closeTextbookModal()" class="flex-1 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50">取消</button>
+                <button type="submit" class="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">保存</button>
+            </div>
+        </form>
+    </div>
+</div>
+
+<!-- 添加/编辑目录节点模态框 -->
+<div id="catalogModal" class="hidden fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center">
+    <div class="bg-white rounded-2xl shadow-2xl max-w-md w-full mx-4">
+        <div class="p-6 border-b border-gray-200">
+            <h2 id="catalogModalTitle" class="text-xl font-bold text-gray-800">添加目录节点</h2>
+        </div>
+        <form id="catalogForm" class="p-6 space-y-4">
+            <input type="hidden" id="catalogId" name="id">
+            <input type="hidden" id="catalogTextbookId" name="textbook_id">
+            <input type="hidden" id="catalogParentId" name="parent_id">
+            <div>
+                <label class="block text-sm font-semibold text-gray-700 mb-2">节点类型 *</label>
+                <select id="catalogNodeType" name="node_type" required class="w-full px-4 py-2 border border-gray-300 rounded-lg">
+                    <option value="chapter">章节</option>
+                    <option value="section">小节</option>
+                    <option value="subsection">子小节</option>
+                </select>
+            </div>
+            <div>
+                <label class="block text-sm font-semibold text-gray-700 mb-2">标题 *</label>
+                <input type="text" id="catalogTitle" name="title" required
+                    class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
+            </div>
+            <div>
+                <label class="block text-sm font-semibold text-gray-700 mb-2">编号</label>
+                <input type="text" id="catalogDisplayNo" name="display_no"
+                    class="w-full px-4 py-2 border border-gray-300 rounded-lg">
+            </div>
+            <div class="flex gap-3 pt-4">
+                <button type="button" onclick="closeCatalogModal()" class="flex-1 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50">取消</button>
+                <button type="submit" class="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">保存</button>
+            </div>
+        </form>
+    </div>
+</div>
+
+<!-- 关联知识点模态框 -->
+<div id="kpRelationModal" class="hidden fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center">
+    <div class="bg-white rounded-2xl shadow-2xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
+        <div class="p-6 border-b border-gray-200">
+            <h2 id="kpRelationModalTitle" class="text-xl font-bold text-gray-800">关联知识点</h2>
+        </div>
+        <div class="p-6">
+            <div class="mb-4">
+                <label class="block text-sm font-semibold text-gray-700 mb-2">选择知识点</label>
+                <select id="kpCodeSelect" class="w-full px-4 py-2 border border-gray-300 rounded-lg">
+                    <option value="">请选择知识点</option>
+                    {% for kp in kp_options %}
+                    <option value="{{ kp.kp_code }}">{{ kp.kp_code }} - {{ kp.name }}</option>
+                    {% endfor %}
+                </select>
+            </div>
+            <button onclick="addKpRelation()" class="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 mb-4">
+                添加关联
+            </button>
+            <div id="kpRelationList" class="space-y-2">
+                <!-- 关联列表将通过JavaScript动态加载 -->
+            </div>
+        </div>
+        <div class="p-6 border-t border-gray-200">
+            <button onclick="closeKpRelationModal()" class="w-full px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50">关闭</button>
+        </div>
+    </div>
+</div>
+
+<script>
+// 当前选中的系列ID和教材ID
+let currentSeriesId = null;
+let currentTextbookId = null;
+let currentCatalogChapterId = null;
+
+// 所有系列数据
+const allSeries = {{ series_list|tojson|safe }};
+
+// 初始化:加载第一个系列
+document.addEventListener('DOMContentLoaded', function() {
+    const firstSeriesItem = document.querySelector('.series-item');
+    if (firstSeriesItem) {
+        const firstSeriesId = parseInt(firstSeriesItem.getAttribute('data-series-id'));
+        switchSeries(firstSeriesId);
+    }
+});
+
+// 筛选系列列表
+function filterSeriesList() {
+    const searchInput = document.getElementById('seriesSearchInput');
+    const searchTerm = (searchInput.value || '').toLowerCase().trim();
+    const seriesItems = document.querySelectorAll('.series-item');
+    
+    let visibleCount = 0;
+    seriesItems.forEach(item => {
+        const seriesName = item.getAttribute('data-series-name').toLowerCase();
+        const matchesSearch = !searchTerm || seriesName.includes(searchTerm);
+        
+        if (matchesSearch) {
+            item.style.display = '';
+            visibleCount++;
+        } else {
+            item.style.display = 'none';
+        }
+    });
+}
+
+// 切换教材系列
+async function switchSeries(seriesId) {
+    // 确保seriesId是数字类型
+    seriesId = parseInt(seriesId);
+    if (!seriesId || isNaN(seriesId)) {
+        console.error('Invalid seriesId:', seriesId);
+        return;
+    }
+    
+    currentSeriesId = seriesId;
+    
+    // 更新左侧菜单选中状态
+    document.querySelectorAll('.series-link').forEach(link => {
+        link.classList.remove('bg-gradient-to-r', 'from-slate-50', 'to-blue-50', 'border-slate-500');
+    });
+    
+    const selectedLink = document.querySelector(`.series-item[data-series-id="${seriesId}"] .series-link`);
+    if (selectedLink) {
+        selectedLink.classList.add('bg-gradient-to-r', 'from-slate-50', 'to-blue-50', 'border-slate-500');
+    }
+    
+    // 获取容器
+    const container = document.getElementById('textbookListContainer');
+    if (!container) {
+        console.error('textbookListContainer not found');
+        return;
+    }
+    
+    // 显示教材列表容器,隐藏目录树
+    container.classList.remove('hidden');
+    document.getElementById('catalogTreeContainer').classList.add('hidden');
+    
+    // 显示加载状态
+    container.innerHTML = '<div class="col-span-full text-center py-12"><i class="ri-loader-4-line animate-spin text-3xl text-blue-500"></i><p class="mt-3 text-gray-500">加载中...</p></div>';
+    
+    // 加载该系列下的教材
+    try {
+        const response = await fetch(`/api/textbook/list/${seriesId}`);
+        
+        if (!response.ok) {
+            throw new Error(`HTTP error! status: ${response.status}`);
+        }
+        
+        const result = await response.json();
+        
+        if (result.success) {
+            // 确保data是数组
+            const textbooks = Array.isArray(result.data) ? result.data : [];
+            
+            if (textbooks.length === 0) {
+                container.innerHTML = '<div class="col-span-full text-center py-12"><i class="ri-book-open-line text-4xl text-gray-300 mb-3"></i><p class="text-gray-500">暂无教材</p><p class="text-xs text-gray-400 mt-2">该系列下还没有添加教材</p></div>';
+            } else {
+                renderTextbookList(textbooks);
+            }
+        } else {
+            console.error('API error:', result.error);
+            container.innerHTML = '<div class="col-span-full text-center py-12"><i class="ri-error-warning-line text-4xl text-red-300 mb-3"></i><p class="text-gray-500">加载失败:' + (result.error || '未知错误') + '</p><button onclick="switchSeries(' + seriesId + ')" class="mt-4 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600">重试</button></div>';
+        }
+    } catch (error) {
+        console.error('Error loading textbooks:', error);
+        container.innerHTML = '<div class="col-span-full text-center py-12"><i class="ri-error-warning-line text-4xl text-red-300 mb-3"></i><p class="text-gray-500">加载教材列表失败</p><p class="text-xs text-gray-400 mt-2">' + error.message + '</p><button onclick="switchSeries(' + seriesId + ')" class="mt-4 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600">重试</button></div>';
+    }
+}
+
+// 直接在列表中切换系列激活状态
+async function toggleSeriesActiveDirect(seriesId, labelElement) {
+    const checkbox = labelElement.querySelector('input[type="checkbox"]');
+    if (!checkbox) return;
+    
+    // 立即切换开关状态(乐观更新)
+    const newState = !checkbox.checked;
+    checkbox.checked = newState;
+    
+    // 禁用开关,防止重复点击
+    checkbox.disabled = true;
+    
+    try {
+        const response = await fetch(`/api/textbook/series/toggle_active/${seriesId}`, {
+            method: 'POST',
+            headers: {'Content-Type': 'application/json'}
+        });
+        
+        const result = await response.json();
+        
+        if (result.success) {
+            // 更新系列项的data-active属性
+            const seriesItem = document.querySelector(`.series-item[data-series-id="${seriesId}"]`);
+            if (seriesItem) {
+                seriesItem.setAttribute('data-series-active', result.is_active);
+            }
+            
+            // 更新allSeries数据
+            const series = allSeries.find(s => s.id === seriesId);
+            if (series) {
+                series.is_active = result.is_active;
+            }
+            
+            // 确保checkbox状态与服务器一致
+            checkbox.checked = result.is_active === 1;
+            
+            // 如果当前选中的系列,刷新教材列表
+            if (currentSeriesId === seriesId) {
+                switchSeries(seriesId);
+            }
+        } else {
+            // 恢复开关状态
+            checkbox.checked = !newState;
+            if (window.customAlert) {
+                window.customAlert('操作失败: ' + result.error);
+            } else {
+                alert('操作失败: ' + result.error);
+            }
+        }
+    } catch (error) {
+        console.error('Error:', error);
+        // 恢复开关状态
+        checkbox.checked = !newState;
+        if (window.customAlert) {
+            window.customAlert('操作失败: ' + error.message);
+        } else {
+            alert('操作失败: ' + error.message);
+        }
+    } finally {
+        checkbox.disabled = false;
+    }
+}
+
+
+// 渲染教材列表
+function renderTextbookList(textbooks) {
+    const container = document.getElementById('textbookListContainer');
+    container.innerHTML = '';
+    
+    textbooks.forEach(textbook => {
+        const card = document.createElement('div');
+        card.className = 'group relative apple-card overflow-hidden cursor-pointer hover:shadow-2xl transition-all duration-300 hover:-translate-y-1';
+        card.onclick = () => loadTextbookCatalog(textbook.id, textbook.official_title);
+        
+        // 生成封面占位图的渐变背景色(根据教材ID生成不同颜色)
+        const colors = [
+            'from-blue-500 to-indigo-600',
+            'from-green-500 to-emerald-600',
+            'from-purple-500 to-violet-600',
+            'from-pink-500 to-rose-600',
+            'from-orange-500 to-amber-600',
+            'from-cyan-500 to-blue-600'
+        ];
+        const colorIndex = (textbook.id || 0) % colors.length;
+        const gradientColor = colors[colorIndex];
+        
+        card.innerHTML = `
+            <!-- 封面占位图 -->
+            <div class="relative w-full aspect-[3/4] bg-gradient-to-br ${gradientColor} overflow-hidden">
+                <div class="absolute inset-0 flex items-center justify-center">
+                    <div class="text-center text-white/90">
+                        <i class="ri-book-open-line text-6xl mb-2 opacity-80"></i>
+                        <div class="text-xs font-semibold opacity-70">教材封面</div>
+                    </div>
+                </div>
+                <!-- 封面装饰线条 -->
+                <div class="absolute top-0 left-0 right-0 h-1 bg-white/20"></div>
+                <div class="absolute bottom-0 left-0 right-0 h-1 bg-black/10"></div>
+                <!-- 操作按钮(悬停显示) -->
+                <div class="absolute top-2 right-2 flex gap-2 opacity-0 group-hover:opacity-100 transition-opacity" onclick="event.stopPropagation()">
+                    <button onclick="showEditTextbookModal(${textbook.id})" class="w-8 h-8 rounded-full bg-white/90 backdrop-blur-sm text-blue-600 hover:bg-white hover:scale-110 transition-all shadow-lg flex items-center justify-center">
+                        <i class="ri-edit-line text-sm"></i>
+                    </button>
+                    <button onclick="showDeleteTextbookConfirm(${textbook.id}, '${textbook.official_title}')" class="w-8 h-8 rounded-full bg-white/90 backdrop-blur-sm text-red-600 hover:bg-white hover:scale-110 transition-all shadow-lg flex items-center justify-center">
+                        <i class="ri-delete-bin-line text-sm"></i>
+                    </button>
+                </div>
+            </div>
+            
+            <!-- 教材信息 -->
+            <div class="p-4 bg-white">
+                <h3 class="text-base font-bold text-gray-800 mb-2 line-clamp-2 leading-tight">${textbook.official_title || '未命名教材'}</h3>
+                <div class="flex items-center gap-2 text-xs text-gray-500">
+                    ${textbook.grade ? `<span class="px-2 py-0.5 bg-gray-100 rounded">${textbook.grade}年级</span>` : ''}
+                    ${textbook.semester ? `<span class="px-2 py-0.5 bg-gray-100 rounded">${textbook.semester === 1 ? '上学期' : '下学期'}</span>` : ''}
+                </div>
+            </div>
+        `;
+        container.appendChild(card);
+    });
+    
+    // 添加"添加教材"卡片
+    const addCard = document.createElement('div');
+    addCard.className = 'apple-card overflow-hidden cursor-pointer hover:shadow-xl transition-all border-2 border-dashed border-gray-300 hover:border-blue-400';
+    addCard.onclick = () => showAddTextbookModal();
+    addCard.innerHTML = `
+        <div class="w-full aspect-[3/4] flex flex-col items-center justify-center text-gray-400 hover:text-blue-500 transition-colors">
+            <div class="mb-3">
+                <i class="ri-add-circle-line text-5xl"></i>
+            </div>
+            <div class="text-sm font-medium">添加教材</div>
+        </div>
+    `;
+    container.appendChild(addCard);
+}
+
+// 加载教材目录树
+async function loadTextbookCatalog(textbookId, textbookTitle) {
+    currentTextbookId = textbookId;
+    const titleElement = document.getElementById('currentTextbookTitle');
+    const catalogContainer = document.getElementById('catalogTreeContainer');
+    const textbookContainer = document.getElementById('textbookListContainer');
+    
+    if (titleElement) {
+        titleElement.textContent = textbookTitle;
+    }
+    
+    // 显示目录树,隐藏教材列表
+    if (catalogContainer) {
+        catalogContainer.classList.remove('hidden');
+    }
+    if (textbookContainer) {
+        textbookContainer.classList.add('hidden');
+    }
+    
+    try {
+        const response = await fetch(`/api/textbook/catalog/tree/${textbookId}`);
+        const result = await response.json();
+        
+        if (result.success) {
+            renderCatalogTree(result.data);
+        } else {
+            console.error('Failed to load catalog tree:', result.error);
+            if (window.customAlert) {
+                window.customAlert('加载目录树失败: ' + (result.error || '未知错误'));
+            } else {
+                alert('加载目录树失败: ' + (result.error || '未知错误'));
+            }
+        }
+    } catch (error) {
+        console.error('Error loading catalog tree:', error);
+        if (window.customAlert) {
+            window.customAlert('加载目录树失败: ' + error.message);
+        } else {
+            alert('加载目录树失败: ' + error.message);
+        }
+    }
+}
+
+// 渲染目录树
+function renderCatalogTree(nodes) {
+    const container = document.getElementById('catalogTree');
+    container.innerHTML = '';
+    
+    // 这里需要使用Jinja2宏来渲染,但由于是动态加载,我们需要用JavaScript递归渲染
+    // 为了简化,我们先用简单的HTML结构
+    nodes.forEach(node => {
+        container.appendChild(createCatalogNodeElement(node, 0));
+    });
+}
+
+// 创建目录节点元素(简化版,实际应该使用服务端渲染)
+function createCatalogNodeElement(node, level) {
+    const div = document.createElement('div');
+    div.className = `catalog-node relative mb-3`;
+    div.setAttribute('data-node-id', node.id);
+    div.setAttribute('data-level', level);
+    
+    const hasChildren = node.children && node.children.length > 0;
+    const colorClass = node.node_type === 'chapter' ? 'from-blue-500 to-indigo-600' : 
+                      node.node_type === 'section' ? 'from-green-500 to-emerald-600' : 
+                      'from-orange-500 to-amber-600';
+    
+    div.innerHTML = `
+        <div class="flex items-start gap-4 group">
+            <div class="flex items-center gap-2 pt-4 flex-shrink-0">
+                ${hasChildren ? `
+                <button onclick="toggleCatalogNode('${node.id}'); event.stopPropagation();" 
+                    class="w-8 h-8 rounded-full bg-white border-2 border-gray-300 hover:border-blue-500 flex items-center justify-center text-gray-600 hover:text-blue-600 transition-all expand-btn z-10 shadow-sm hover:shadow-md"
+                    data-expanded="${level === 0 ? 'true' : 'false'}">
+                    <i class="ri-${level === 0 ? 'subtract' : 'add'}-line text-sm"></i>
+                </button>
+                ` : `
+                <div class="w-8 h-8 flex items-center justify-center">
+                    <div class="w-2 h-2 rounded-full bg-gray-400"></div>
+                </div>
+                `}
+            </div>
+            <div class="flex-1 apple-card p-5 hover:shadow-xl transition-all duration-300 group-hover:border-blue-300 border-2 border-transparent rounded-xl ${hasChildren ? 'cursor-pointer' : ''}" 
+                 ${hasChildren ? `onclick="handleCatalogCardClick(event, '${node.id}')"` : ''}>
+                <div class="flex items-start justify-between">
+                    <div class="flex-1 min-w-0">
+                        <div class="flex items-center gap-3 mb-3 flex-wrap">
+                            <span class="px-3 py-1.5 rounded-lg text-xs font-bold font-mono bg-gradient-to-r ${colorClass} text-white shadow-md">
+                                ${node.display_no || node.node_type}
+                            </span>
+                            <h3 class="text-lg font-bold text-gray-800 group-hover:text-blue-600 transition-colors">
+                                ${node.title}
+                            </h3>
+                        </div>
+                        <div class="flex items-center gap-4 text-sm text-gray-500 mt-2 flex-wrap">
+                            <span class="flex items-center gap-1.5 px-2 py-1 bg-gray-50 rounded-md">
+                                <i class="ri-file-list-line text-blue-500"></i>
+                                <span>${node.node_type}</span>
+                            </span>
+                            ${hasChildren ? `
+                            <span class="flex items-center gap-1.5 px-2 py-1 bg-blue-50 rounded-md text-blue-600">
+                                <i class="ri-node-tree"></i>
+                                <span class="font-semibold">${node.children.length} 个子节点</span>
+                            </span>
+                            ` : ''}
+                        </div>
+                    </div>
+                    <div class="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity ml-4 flex-shrink-0" onclick="event.stopPropagation()">
+                        ${node.node_type === 'section' ? `
+                        <button onclick="showAddKpRelationModal('${node.id}', '${node.title}')" 
+                            class="px-3 py-1.5 text-xs font-semibold text-white bg-gradient-to-r from-purple-500 to-indigo-600 hover:from-purple-600 hover:to-indigo-700 rounded-lg transition-all shadow-sm hover:shadow-md flex items-center gap-1.5">
+                            <i class="ri-link"></i>
+                            <span>关联知识点</span>
+                        </button>
+                        ` : ''}
+                        <button onclick="showAddChildCatalogModal('${node.id}', '${node.title}', '${node.node_type}')" 
+                            class="px-3 py-1.5 text-xs font-semibold text-white bg-gradient-to-r from-green-500 to-emerald-600 hover:from-green-600 hover:to-emerald-700 rounded-lg transition-all shadow-sm hover:shadow-md flex items-center gap-1.5">
+                            <i class="ri-add-line"></i>
+                            <span>添加子节点</span>
+                        </button>
+                        <button onclick="showEditCatalogModal(${node.id})" 
+                            class="w-9 h-9 rounded-lg text-blue-600 bg-blue-50 hover:bg-blue-100 flex items-center justify-center transition-colors shadow-sm hover:shadow-md">
+                            <i class="ri-edit-line"></i>
+                        </button>
+                        <button onclick="showDeleteCatalogConfirm(${node.id}, '${node.title}')" 
+                            class="w-9 h-9 rounded-lg text-red-600 bg-red-50 hover:bg-red-100 flex items-center justify-center transition-colors shadow-sm hover:shadow-md">
+                            <i class="ri-delete-bin-line"></i>
+                        </button>
+                    </div>
+                </div>
+            </div>
+        </div>
+        ${hasChildren ? `
+        <div class="children-container mt-3 ml-12 ${level >= 1 ? 'hidden' : ''}" id="children-${node.id}" data-level="${level}">
+            ${node.children.map(child => createCatalogNodeElement(child, level + 1).outerHTML).join('')}
+        </div>
+        ` : ''}
+        ${node.node_type === 'section' ? `
+        <div class="knowledge-points-container mt-3 ml-12" id="kp-${node.id}">
+            <div class="flex items-center gap-2 mb-2">
+                <i class="ri-loader-4-line animate-spin text-gray-400 text-sm"></i>
+                <span class="text-xs text-gray-500">加载知识点...</span>
+            </div>
+        </div>
+        ` : ''}
+    `;
+    
+    // 如果是section节点,异步加载关联的知识点
+    if (node.node_type === 'section') {
+        setTimeout(() => loadKnowledgePointsForSection(node.id), 100);
+    }
+    
+    return div;
+}
+
+// 切换目录节点展开/折叠
+function toggleCatalogNode(nodeId) {
+    const childrenContainer = document.getElementById(`children-${nodeId}`);
+    if (!childrenContainer) return;
+    
+    const kpNode = childrenContainer.closest('.catalog-node');
+    if (!kpNode) return;
+    
+    const expandBtn = kpNode.querySelector('.expand-btn');
+    if (!expandBtn) return;
+    
+    const isExpanded = expandBtn.getAttribute('data-expanded') === 'true';
+    
+    if (isExpanded) {
+        childrenContainer.classList.add('hidden');
+        expandBtn.setAttribute('data-expanded', 'false');
+        const icon = expandBtn.querySelector('i');
+        if (icon) {
+            icon.className = 'ri-add-line text-sm';
+        }
+    } else {
+        childrenContainer.classList.remove('hidden');
+        expandBtn.setAttribute('data-expanded', 'true');
+        const icon = expandBtn.querySelector('i');
+        if (icon) {
+            icon.className = 'ri-subtract-line text-sm';
+        }
+    }
+}
+
+// 处理卡片点击
+function handleCatalogCardClick(event, nodeId) {
+    if (event.target.closest('button') || event.target.closest('a')) {
+        return;
+    }
+    toggleCatalogNode(nodeId);
+}
+
+// 加载section关联的知识点
+async function loadKnowledgePointsForSection(sectionId) {
+    const container = document.getElementById(`kp-${sectionId}`);
+    if (!container) return;
+    
+    try {
+        const response = await fetch(`/api/textbook/relation/list/${sectionId}`);
+        const result = await response.json();
+        
+        if (result.success && result.data && result.data.length > 0) {
+            // 去重:按kp_code去重,保留第一个
+            const uniqueRelations = [];
+            const seenKpCodes = new Set();
+            
+            result.data.forEach(relation => {
+                if (!seenKpCodes.has(relation.kp_code)) {
+                    seenKpCodes.add(relation.kp_code);
+                    uniqueRelations.push(relation);
+                }
+            });
+            
+            container.innerHTML = `
+                <div class="mb-2">
+                    <span class="text-xs font-semibold text-purple-600 flex items-center gap-1">
+                        <i class="ri-link"></i>
+                        <span>关联知识点 (${uniqueRelations.length})</span>
+                    </span>
+                </div>
+                <div class="flex flex-wrap gap-2">
+                    ${uniqueRelations.map(relation => `
+                        <div class="group relative px-3 py-2 bg-gradient-to-r from-purple-50 to-indigo-50 border border-purple-200 rounded-lg hover:shadow-md transition-all">
+                            <div class="flex items-center gap-2">
+                                <span class="text-xs font-mono font-bold text-purple-600">${relation.kp_code || ''}</span>
+                                <span class="text-sm text-gray-700">${relation.kp_name || '未知知识点'}</span>
+                                <button onclick="removeKpRelationByCode('${relation.kp_code}', ${sectionId})" 
+                                    class="opacity-0 group-hover:opacity-100 ml-1 w-5 h-5 rounded-full bg-red-100 hover:bg-red-200 text-red-600 flex items-center justify-center transition-all"
+                                    title="取消关联">
+                                    <i class="ri-close-line text-xs"></i>
+                                </button>
+                            </div>
+                        </div>
+                    `).join('')}
+                </div>
+            `;
+        } else {
+            container.innerHTML = `
+                <div class="text-xs text-gray-400 italic">
+                    <i class="ri-information-line"></i>
+                    <span>暂无关联知识点</span>
+                </div>
+            `;
+        }
+    } catch (error) {
+        console.error('Error loading knowledge points:', error);
+        container.innerHTML = `
+            <div class="text-xs text-red-400">
+                <i class="ri-error-warning-line"></i>
+                <span>加载知识点失败</span>
+            </div>
+        `;
+    }
+}
+
+// 移除知识点关联(通过relationId)
+async function removeKpRelation(relationId, sectionId) {
+    if (!confirm('确定要取消这个知识点关联吗?')) {
+        return;
+    }
+    
+    try {
+        const response = await fetch(`/api/textbook/relation/delete/${relationId}`, {
+            method: 'POST',
+            headers: {'Content-Type': 'application/json'}
+        });
+        
+        const result = await response.json();
+        
+        if (result.success) {
+            // 重新加载知识点列表
+            loadKnowledgePointsForSection(sectionId);
+            
+            if (window.customAlert) {
+                window.customAlert('已取消关联');
+            } else {
+                alert('已取消关联');
+            }
+        } else {
+            if (window.customAlert) {
+                window.customAlert('操作失败: ' + result.error);
+            } else {
+                alert('操作失败: ' + result.error);
+            }
+        }
+    } catch (error) {
+        console.error('Error:', error);
+        if (window.customAlert) {
+            window.customAlert('操作失败: ' + error.message);
+        } else {
+            alert('操作失败: ' + error.message);
+        }
+    }
+}
+
+// 移除知识点关联(通过kp_code,删除所有重复的关联)
+async function removeKpRelationByCode(kpCode, sectionId) {
+    if (!confirm('确定要取消这个知识点关联吗?')) {
+        return;
+    }
+    
+    try {
+        // 先获取所有该知识点的关联
+        const listResponse = await fetch(`/api/textbook/relation/list/${sectionId}`);
+        const listResult = await listResponse.json();
+        
+        if (!listResult.success || !listResult.data) {
+            throw new Error('获取关联列表失败');
+        }
+        
+        // 找到所有匹配的关联ID
+        const relationIds = listResult.data
+            .filter(rel => rel.kp_code === kpCode)
+            .map(rel => rel.id);
+        
+        if (relationIds.length === 0) {
+            if (window.customAlert) {
+                window.customAlert('未找到关联');
+            } else {
+                alert('未找到关联');
+            }
+            return;
+        }
+        
+        // 删除所有重复的关联
+        const deletePromises = relationIds.map(id => 
+            fetch(`/api/textbook/relation/delete/${id}`, {
+                method: 'POST',
+                headers: {'Content-Type': 'application/json'}
+            })
+        );
+        
+        await Promise.all(deletePromises);
+        
+        // 重新加载知识点列表
+        loadKnowledgePointsForSection(sectionId);
+        
+        // 如果模态框打开,也刷新模态框中的列表
+        if (currentKpRelationSectionId === sectionId) {
+            await loadKpRelations(sectionId);
+        }
+        
+        if (window.customAlert) {
+            window.customAlert('已取消关联');
+        } else {
+            alert('已取消关联');
+        }
+    } catch (error) {
+        console.error('Error:', error);
+        if (window.customAlert) {
+            window.customAlert('操作失败: ' + error.message);
+        } else {
+            alert('操作失败: ' + error.message);
+        }
+    }
+}
+
+// ==================== 教材系列 CRUD ====================
+function showAddSeriesModal() {
+    document.getElementById('seriesModalTitle').textContent = '添加教材系列';
+    document.getElementById('seriesForm').reset();
+    document.getElementById('seriesId').value = '';
+    document.getElementById('seriesIsActive').checked = true; // 默认激活
+    document.getElementById('seriesModal').classList.remove('hidden');
+}
+
+async function showEditSeriesModal(seriesId) {
+    try {
+        const response = await fetch(`/api/textbook/series/get/${seriesId}`);
+        const result = await response.json();
+        
+        if (result.success) {
+            const series = result.data;
+            document.getElementById('seriesModalTitle').textContent = '编辑教材系列';
+            document.getElementById('seriesId').value = series.id;
+            document.getElementById('seriesName').value = series.name || '';
+            document.getElementById('seriesSlug').value = series.slug || '';
+            document.getElementById('seriesIsActive').checked = series.is_active === 1;
+            document.getElementById('seriesModal').classList.remove('hidden');
+        }
+    } catch (error) {
+        console.error('Error:', error);
+        if (window.customAlert) {
+            window.customAlert('获取系列信息失败: ' + error.message);
+        } else {
+            alert('获取系列信息失败');
+        }
+    }
+}
+
+function closeSeriesModal() {
+    document.getElementById('seriesModal').classList.add('hidden');
+}
+
+document.getElementById('seriesForm').addEventListener('submit', async function(e) {
+    e.preventDefault();
+    
+    const seriesId = document.getElementById('seriesId').value;
+    const formData = {
+        name: document.getElementById('seriesName').value.trim(),
+        slug: document.getElementById('seriesSlug').value.trim() || null,
+        is_active: document.getElementById('seriesIsActive').checked
+    };
+    
+    try {
+        let response;
+        if (seriesId) {
+            response = await fetch(`/api/textbook/series/update/${seriesId}`, {
+                method: 'POST',
+                headers: {'Content-Type': 'application/json'},
+                body: JSON.stringify(formData)
+            });
+        } else {
+            response = await fetch('/api/textbook/series/create', {
+                method: 'POST',
+                headers: {'Content-Type': 'application/json'},
+                body: JSON.stringify(formData)
+            });
+        }
+        
+        const result = await response.json();
+        
+        if (result.success) {
+            alert(result.message || '操作成功');
+            window.location.reload();
+        } else {
+            alert('操作失败: ' + result.error);
+        }
+    } catch (error) {
+        console.error('Error:', error);
+        alert('操作失败: ' + error.message);
+    }
+});
+
+// ==================== 教材 CRUD ====================
+function showAddTextbookModal() {
+    document.getElementById('textbookModalTitle').textContent = '添加教材';
+    document.getElementById('textbookForm').reset();
+    document.getElementById('textbookId').value = '';
+    document.getElementById('textbookModal').classList.remove('hidden');
+}
+
+async function showEditTextbookModal(textbookId) {
+    try {
+        const response = await fetch(`/api/textbook/get/${textbookId}`);
+        const result = await response.json();
+        
+        if (result.success) {
+            const textbook = result.data;
+            document.getElementById('textbookModalTitle').textContent = '编辑教材';
+            document.getElementById('textbookId').value = textbook.id;
+            document.getElementById('textbookTitle').value = textbook.official_title || '';
+            document.getElementById('textbookStage').value = textbook.stage || '';
+            document.getElementById('textbookGrade').value = textbook.grade || '';
+            document.getElementById('textbookSemester').value = textbook.semester || '';
+            document.getElementById('textbookModal').classList.remove('hidden');
+        }
+    } catch (error) {
+        console.error('Error:', error);
+        alert('获取教材信息失败');
+    }
+}
+
+function closeTextbookModal() {
+    document.getElementById('textbookModal').classList.add('hidden');
+}
+
+document.getElementById('textbookForm').addEventListener('submit', async function(e) {
+    e.preventDefault();
+    
+    const textbookId = document.getElementById('textbookId').value;
+    const formData = {
+        series_id: currentSeriesId,
+        official_title: document.getElementById('textbookTitle').value.trim(),
+        stage: document.getElementById('textbookStage').value || null,
+        grade: document.getElementById('textbookGrade').value.trim() || null,
+        semester: document.getElementById('textbookSemester').value ? parseInt(document.getElementById('textbookSemester').value) : null
+    };
+    
+    try {
+        let response;
+        if (textbookId) {
+            response = await fetch(`/api/textbook/update/${textbookId}`, {
+                method: 'POST',
+                headers: {'Content-Type': 'application/json'},
+                body: JSON.stringify(formData)
+            });
+        } else {
+            response = await fetch('/api/textbook/create', {
+                method: 'POST',
+                headers: {'Content-Type': 'application/json'},
+                body: JSON.stringify(formData)
+            });
+        }
+        
+        const result = await response.json();
+        
+        if (result.success) {
+            if (window.customAlert) {
+                window.customAlert(result.message || '操作成功', () => {
+                    switchSeries(currentSeriesId);
+                    closeTextbookModal();
+                });
+            } else {
+                alert(result.message || '操作成功');
+                switchSeries(currentSeriesId);
+                closeTextbookModal();
+            }
+        } else {
+            if (window.customAlert) {
+                window.customAlert('操作失败: ' + result.error);
+            } else {
+                alert('操作失败: ' + result.error);
+            }
+        }
+    } catch (error) {
+        console.error('Error:', error);
+        alert('操作失败: ' + error.message);
+    }
+});
+
+async function showDeleteTextbookConfirm(textbookId, textbookTitle) {
+    if (confirm(`确定要删除教材 "${textbookTitle}" 吗?`)) {
+        try {
+            const response = await fetch(`/api/textbook/delete/${textbookId}`, {
+                method: 'POST',
+                headers: {'Content-Type': 'application/json'}
+            });
+            
+            const result = await response.json();
+            
+            if (result.success) {
+                if (window.customAlert) {
+                    window.customAlert(result.message || '删除成功', () => {
+                        switchSeries(currentSeriesId);
+                    });
+                } else {
+                    alert(result.message || '删除成功');
+                    switchSeries(currentSeriesId);
+                }
+            } else {
+                if (window.customAlert) {
+                    window.customAlert('删除失败: ' + result.error);
+                } else {
+                    alert('删除失败: ' + result.error);
+                }
+            }
+        } catch (error) {
+            console.error('Error:', error);
+            alert('删除失败: ' + error.message);
+        }
+    }
+}
+
+// ==================== 目录节点 CRUD ====================
+function showAddCatalogModal(parentId = null, parentNodeType = null) {
+    document.getElementById('catalogModalTitle').textContent = parentId ? '添加子节点' : '添加目录节点';
+    document.getElementById('catalogForm').reset();
+    document.getElementById('catalogId').value = '';
+    document.getElementById('catalogTextbookId').value = currentTextbookId;
+    document.getElementById('catalogParentId').value = parentId || '';
+    
+    // 根据父节点类型限制子节点类型
+    const nodeTypeSelect = document.getElementById('catalogNodeType');
+    const allOptions = nodeTypeSelect.querySelectorAll('option');
+    
+    // 先显示所有选项
+    allOptions.forEach(opt => opt.style.display = '');
+    
+    if (parentId && parentNodeType) {
+        // 有父节点,根据父节点类型限制
+        if (parentNodeType === 'chapter') {
+            // 章节下只能创建 section
+            allOptions.forEach(opt => {
+                if (opt.value !== 'section') {
+                    opt.style.display = 'none';
+                }
+            });
+            nodeTypeSelect.value = 'section';
+        } else if (parentNodeType === 'section') {
+            // section 下只能创建 subsection
+            allOptions.forEach(opt => {
+                if (opt.value !== 'subsection') {
+                    opt.style.display = 'none';
+                }
+            });
+            nodeTypeSelect.value = 'subsection';
+        } else {
+            // subsection 下不能再创建子节点(但这里不应该被调用)
+            allOptions.forEach(opt => {
+                if (opt.value !== 'subsection') {
+                    opt.style.display = 'none';
+                }
+            });
+        }
+    } else {
+        // 没有父节点,只能创建顶级节点(chapter)
+        allOptions.forEach(opt => {
+            if (opt.value !== 'chapter') {
+                opt.style.display = 'none';
+            }
+        });
+        nodeTypeSelect.value = 'chapter';
+    }
+    
+    document.getElementById('catalogModal').classList.remove('hidden');
+}
+
+async function showAddChildCatalogModal(parentId, parentTitle, parentNodeType = null) {
+    // 如果未传递父节点类型,则通过API获取
+    if (!parentNodeType) {
+        try {
+            const response = await fetch(`/api/textbook/catalog/get/${parentId}`);
+            const result = await response.json();
+            
+            if (result.success) {
+                parentNodeType = result.data.node_type;
+            }
+        } catch (error) {
+            console.error('获取父节点信息失败:', error);
+        }
+    }
+    
+    showAddCatalogModal(parentId, parentNodeType);
+}
+
+async function showEditCatalogModal(nodeId) {
+    try {
+        const response = await fetch(`/api/textbook/catalog/get/${nodeId}`);
+        const result = await response.json();
+        
+        if (result.success) {
+            const node = result.data;
+            document.getElementById('catalogModalTitle').textContent = '编辑目录节点';
+            document.getElementById('catalogId').value = node.id;
+            document.getElementById('catalogTextbookId').value = node.textbook_id;
+            document.getElementById('catalogParentId').value = node.parent_id || '';
+            document.getElementById('catalogTitle').value = node.title || '';
+            document.getElementById('catalogDisplayNo').value = node.display_no || '';
+            
+            // 根据父节点类型限制节点类型选择
+            const nodeTypeSelect = document.getElementById('catalogNodeType');
+            const allOptions = nodeTypeSelect.querySelectorAll('option');
+            
+            // 先显示所有选项
+            allOptions.forEach(opt => opt.style.display = '');
+            
+            if (node.parent_id) {
+                // 有父节点,需要获取父节点类型
+                try {
+                    const parentResponse = await fetch(`/api/textbook/catalog/get/${node.parent_id}`);
+                    const parentResult = await parentResponse.json();
+                    
+                    if (parentResult.success) {
+                        const parentNodeType = parentResult.data.node_type;
+                        if (parentNodeType === 'chapter') {
+                            // 章节下只能创建 section
+                            allOptions.forEach(opt => {
+                                if (opt.value !== 'section') {
+                                    opt.style.display = 'none';
+                                }
+                            });
+                        } else if (parentNodeType === 'section') {
+                            // section 下只能创建 subsection
+                            allOptions.forEach(opt => {
+                                if (opt.value !== 'subsection') {
+                                    opt.style.display = 'none';
+                                }
+                            });
+                        }
+                    }
+                } catch (error) {
+                    console.error('获取父节点信息失败:', error);
+                }
+            } else {
+                // 没有父节点,只能创建顶级节点(chapter)
+                allOptions.forEach(opt => {
+                    if (opt.value !== 'chapter') {
+                        opt.style.display = 'none';
+                    }
+                });
+            }
+            
+            document.getElementById('catalogNodeType').value = node.node_type || 'chapter';
+            document.getElementById('catalogModal').classList.remove('hidden');
+        }
+    } catch (error) {
+        console.error('Error:', error);
+        alert('获取节点信息失败');
+    }
+}
+
+function closeCatalogModal() {
+    document.getElementById('catalogModal').classList.add('hidden');
+}
+
+document.getElementById('catalogForm').addEventListener('submit', async function(e) {
+    e.preventDefault();
+    
+    const catalogId = document.getElementById('catalogId').value;
+    const formData = {
+        textbook_id: parseInt(document.getElementById('catalogTextbookId').value),
+        parent_id: document.getElementById('catalogParentId').value ? parseInt(document.getElementById('catalogParentId').value) : null,
+        node_type: document.getElementById('catalogNodeType').value,
+        title: document.getElementById('catalogTitle').value.trim(),
+        display_no: document.getElementById('catalogDisplayNo').value.trim() || null
+    };
+    
+    try {
+        let response;
+        if (catalogId) {
+            response = await fetch(`/api/textbook/catalog/update/${catalogId}`, {
+                method: 'POST',
+                headers: {'Content-Type': 'application/json'},
+                body: JSON.stringify(formData)
+            });
+        } else {
+            response = await fetch('/api/textbook/catalog/create', {
+                method: 'POST',
+                headers: {'Content-Type': 'application/json'},
+                body: JSON.stringify(formData)
+            });
+        }
+        
+        const result = await response.json();
+        
+        if (result.success) {
+            if (window.customAlert) {
+                window.customAlert(result.message || '操作成功', () => {
+                    loadTextbookCatalog(currentTextbookId, document.getElementById('currentTextbookTitle').textContent);
+                    closeCatalogModal();
+                });
+            } else {
+                alert(result.message || '操作成功');
+                loadTextbookCatalog(currentTextbookId, document.getElementById('currentTextbookTitle').textContent);
+                closeCatalogModal();
+            }
+        } else {
+            if (window.customAlert) {
+                window.customAlert('操作失败: ' + result.error);
+            } else {
+                alert('操作失败: ' + result.error);
+            }
+        }
+    } catch (error) {
+        console.error('Error:', error);
+        alert('操作失败: ' + error.message);
+    }
+});
+
+async function showDeleteCatalogConfirm(nodeId, nodeTitle) {
+    if (confirm(`确定要删除节点 "${nodeTitle}" 吗?`)) {
+        try {
+            const response = await fetch(`/api/textbook/catalog/delete/${nodeId}`, {
+                method: 'POST',
+                headers: {'Content-Type': 'application/json'}
+            });
+            
+            const result = await response.json();
+            
+            if (result.success) {
+                if (window.customAlert) {
+                    window.customAlert(result.message || '删除成功', () => {
+                        loadTextbookCatalog(currentTextbookId, document.getElementById('currentTextbookTitle').textContent);
+                    });
+                } else {
+                    alert(result.message || '删除成功');
+                    loadTextbookCatalog(currentTextbookId, document.getElementById('currentTextbookTitle').textContent);
+                }
+            } else {
+                if (window.customAlert) {
+                    window.customAlert('删除失败: ' + result.error);
+                } else {
+                    alert('删除失败: ' + result.error);
+                }
+            }
+        } catch (error) {
+            console.error('Error:', error);
+            alert('删除失败: ' + error.message);
+        }
+    }
+}
+
+// ==================== 知识点关联 CRUD ====================
+let currentKpRelationSectionId = null;
+
+async function showAddKpRelationModal(chapterId, chapterTitle) {
+    currentKpRelationSectionId = chapterId;
+    currentCatalogChapterId = chapterId;
+    document.getElementById('kpRelationModalTitle').textContent = `关联知识点 - ${chapterTitle}`;
+    document.getElementById('kpRelationModal').classList.remove('hidden');
+    
+    // 加载已有的关联
+    await loadKpRelations(chapterId);
+}
+
+function closeKpRelationModal() {
+    document.getElementById('kpRelationModal').classList.add('hidden');
+    currentCatalogChapterId = null;
+}
+
+async function loadKpRelations(chapterId) {
+    try {
+        const response = await fetch(`/api/textbook/relation/list/${chapterId}`);
+        const result = await response.json();
+        
+        if (result.success) {
+            const container = document.getElementById('kpRelationList');
+            container.innerHTML = '';
+            
+            if (result.data.length === 0) {
+                container.innerHTML = '<p class="text-gray-500 text-center py-4">暂无关联的知识点</p>';
+            } else {
+                // 去重:按kp_code去重,保留第一个
+                const uniqueRelations = [];
+                const seenKpCodes = new Set();
+                
+                result.data.forEach(relation => {
+                    if (!seenKpCodes.has(relation.kp_code)) {
+                        seenKpCodes.add(relation.kp_code);
+                        uniqueRelations.push(relation);
+                    }
+                });
+                
+                uniqueRelations.forEach(relation => {
+                    const div = document.createElement('div');
+                    div.className = 'flex items-center justify-between p-3 bg-gray-50 rounded-lg';
+                    div.innerHTML = `
+                        <div>
+                            <span class="font-semibold">${relation.kp_code}</span>
+                            ${relation.kp_name ? `<span class="text-gray-500 ml-2">${relation.kp_name}</span>` : ''}
+                        </div>
+                        <button onclick="deleteKpRelation(${relation.id})" class="text-red-600 hover:text-red-800">
+                            <i class="ri-delete-bin-line"></i>
+                        </button>
+                    `;
+                    container.appendChild(div);
+                });
+            }
+        }
+    } catch (error) {
+        console.error('Error:', error);
+    }
+}
+
+async function addKpRelation() {
+    const kpCode = document.getElementById('kpCodeSelect').value;
+    if (!kpCode) {
+        if (window.customAlert) {
+            window.customAlert('请选择知识点');
+        } else {
+            alert('请选择知识点');
+        }
+        return;
+    }
+    
+    // 检查是否已存在该关联
+    try {
+        const checkResponse = await fetch(`/api/textbook/relation/list/${currentCatalogChapterId}`);
+        const checkResult = await checkResponse.json();
+        
+        if (checkResult.success && checkResult.data) {
+            const exists = checkResult.data.some(rel => rel.kp_code === kpCode);
+            if (exists) {
+                if (window.customAlert) {
+                    window.customAlert('该知识点已关联,不能重复添加');
+                } else {
+                    alert('该知识点已关联,不能重复添加');
+                }
+                return;
+            }
+        }
+    } catch (error) {
+        console.error('Error checking existing relations:', error);
+    }
+    
+    try {
+        const response = await fetch('/api/textbook/relation/create', {
+            method: 'POST',
+            headers: {'Content-Type': 'application/json'},
+            body: JSON.stringify({
+                catalog_chapter_id: currentCatalogChapterId,
+                kp_code: kpCode
+            })
+        });
+        
+        const result = await response.json();
+        
+        if (result.success) {
+            document.getElementById('kpCodeSelect').value = '';
+            await loadKpRelations(currentCatalogChapterId);
+            // 刷新section下显示的知识点列表
+            if (currentKpRelationSectionId) {
+                loadKnowledgePointsForSection(currentKpRelationSectionId);
+            }
+        } else {
+            if (window.customAlert) {
+                window.customAlert('添加失败: ' + result.error);
+            } else {
+                alert('添加失败: ' + result.error);
+            }
+        }
+    } catch (error) {
+        console.error('Error:', error);
+        if (window.customAlert) {
+            window.customAlert('添加失败: ' + error.message);
+        } else {
+            alert('添加失败: ' + error.message);
+        }
+    }
+}
+
+async function deleteKpRelation(relationId) {
+    if (!confirm('确定要删除这个关联吗?')) {
+        return;
+    }
+    
+    try {
+        const response = await fetch(`/api/textbook/relation/delete/${relationId}`, {
+            method: 'POST',
+            headers: {'Content-Type': 'application/json'}
+        });
+        
+        const result = await response.json();
+        
+        if (result.success) {
+            await loadKpRelations(currentCatalogChapterId);
+            // 刷新section下显示的知识点列表
+            if (currentKpRelationSectionId) {
+                loadKnowledgePointsForSection(currentKpRelationSectionId);
+            }
+        } else {
+            alert('删除失败: ' + result.error);
+        }
+    } catch (error) {
+        console.error('Error:', error);
+        alert('删除失败: ' + error.message);
+    }
+}
+
+// 点击模态框外部关闭
+document.getElementById('seriesModal').addEventListener('click', function(e) {
+    if (e.target === this) closeSeriesModal();
+});
+document.getElementById('textbookModal').addEventListener('click', function(e) {
+    if (e.target === this) closeTextbookModal();
+});
+document.getElementById('catalogModal').addEventListener('click', function(e) {
+    if (e.target === this) closeCatalogModal();
+});
+document.getElementById('kpRelationModal').addEventListener('click', function(e) {
+    if (e.target === this) closeKpRelationModal();
+});
+</script>
+{% endblock %}

BIN=BIN
小猫.png


+ 1437 - 0
知了数学题库/tree(1)(1).json

@@ -0,0 +1,1437 @@
+{
+  "id": "M00",
+  "label": "初中数学知识体系",
+  "children": [
+    {
+      "id": "M01",
+      "label": "数与代数",
+      "children": [
+        {
+          "id": "S01",
+          "label": "数的认识与运算",
+          "children": [
+            {
+              "id": "R01",
+              "label": "整数与自然数",
+              "skills": ["整数概念", "自然数概念", "数轴位置"],
+              "direct_score": [1,2],
+              "related_score": [1,2],
+              "children": []
+            },
+            {
+              "id": "R02",
+              "label": "有理数分类",
+              "skills": ["正数负数", "零", "分数小数互化"],
+              "direct_score": [1,3],
+              "related_score": [2,4],
+              "children": []
+            },
+            {
+              "id": "R03",
+              "label": "有理数加减法",
+              "skills": ["同号相加", "异号相减", "数轴表示运算"],
+              "direct_score": [1,3],
+              "related_score": [2,4],
+              "children": []
+            },
+            {
+              "id": "R04",
+              "label": "有理数乘除法",
+              "skills": ["符号法则", "倒数", "乘方意义"],
+              "direct_score": [1,3],
+              "related_score": [2,4],
+              "children": []
+            },
+            {
+              "id": "R05",
+              "label": "幂与指数",
+              "skills": ["指数意义", "幂的运算性质"],
+              "direct_score": [1,3],
+              "related_score": [3,5],
+              "children": []
+            },
+            {
+              "id": "R06",
+              "label": "科学记数法",
+              "skills": ["有效数字", "数量级"],
+              "direct_score": [1,2],
+              "related_score": [2,4],
+              "children": []
+            },
+            {
+              "id": "R07",
+              "label": "平方与平方根",
+              "skills": ["平方意义", "算术平方根"],
+              "direct_score": [2,4],
+              "related_score": [3,5],
+              "children": []
+            },
+            {
+              "id": "R08",
+              "label": "立方与立方根",
+              "skills": ["立方运算", "立方根概念"],
+              "direct_score": [2,3],
+              "related_score": [3,5],
+              "children": []
+            },
+            {
+              "id": "R09",
+              "label": "实数",
+              "skills": ["无理数", "实数分类", "数轴连续性"],
+              "direct_score": [2,4],
+              "related_score": [3,6],
+              "children": []
+            }
+          ]
+        },
+
+        {
+          "id": "S02",
+          "label": "代数式与整式运算",
+          "children": [
+            {
+              "id": "A01",
+              "label": "代数式基本概念",
+              "skills": ["字母表示数", "代入求值", "代数式结构"],
+              "direct_score": [1,2],
+              "related_score": [2,4],
+              "children": []
+            },
+            {
+              "id": "A02",
+              "label": "整式的概念",
+              "skills": ["单项式", "多项式", "次数"],
+              "direct_score": [1,3],
+              "related_score": [2,4],
+              "children": []
+            },
+            {
+              "id": "A03",
+              "label": "同类项合并",
+              "skills": ["合并规则", "规范化表达"],
+              "direct_score": [1,3],
+              "related_score": [2,4],
+              "children": []
+            },
+            {
+              "id": "A04",
+              "label": "去括号与加减法",
+              "skills": ["符号处理", "括号运算"],
+              "direct_score": [2,4],
+              "related_score": [3,5],
+              "children": []
+            },
+            {
+              "id": "A05",
+              "label": "整式乘法",
+              "skills": ["分配律", "单项式乘多项式", "多项式乘多项式"],
+              "direct_score": [2,4],
+              "related_score": [3,6],
+              "children": []
+            },
+            {
+              "id": "A06",
+              "label": "特殊乘法公式",
+              "skills": ["平方差公式", "完全平方公式"],
+              "direct_score": [2,4],
+              "related_score": [3,6],
+              "children": []
+            },
+
+            {
+              "id": "A07",
+              "label": "因式分解(基础)",
+              "skills": ["提公因式法", "公式法"],
+              "direct_score": [3,5],
+              "related_score": [5,8],
+              "children": []
+            },
+            {
+              "id": "A08",
+              "label": "因式分解(进阶)",
+              "skills": [
+                "分组分解",
+                "十字相乘",
+                "AC分解法",
+                "拆项构造分解"
+              ],
+              "direct_score": [4,6],
+              "related_score": [6,10],
+              "children": []
+            },
+
+            {
+              "id": "A09",
+              "label": "因式分解综合应用",
+              "skills": [
+                "识别分解形式",
+                "构造分解",
+                "复杂代数式化简"
+              ],
+              "direct_score": [5,7],
+              "related_score": [7,12],
+              "children": []
+            }
+          ]
+        },
+
+        {
+          "id": "S03",
+          "label": "分式与分式运算",
+          "children": [
+            {
+              "id": "F01",
+              "label": "分式概念",
+              "skills": ["整式做分子分母", "分式值域"],
+              "direct_score": [1,2],
+              "related_score": [2,4],
+              "children": []
+            },
+            {
+              "id": "F02",
+              "label": "分式约分",
+              "skills": ["因式约分", "整体法"],
+              "direct_score": [2,4],
+              "related_score": [4,6],
+              "children": []
+            },
+            {
+              "id": "F03",
+              "label": "分式通分",
+              "skills": ["最小公倍式", "整体通分"],
+              "direct_score": [2,4],
+              "related_score": [4,6],
+              "children": []
+            },
+            {
+              "id": "F04",
+              "label": "分式加减法",
+              "skills": ["通分", "同分母加减"],
+              "direct_score": [2,4],
+              "related_score": [4,6],
+              "children": []
+            },
+            {
+              "id": "F05",
+              "label": "分式乘除法",
+              "skills": ["倒数乘法", "因式消去"],
+              "direct_score": [2,4],
+              "related_score": [4,6],
+              "children": []
+            },
+            {
+              "id": "F06",
+              "label": "分式综合化简",
+              "skills": ["整体代数化简", "复杂表达式处理"],
+              "direct_score": [3,6],
+              "related_score": [5,8],
+              "children": []
+            }
+          ]
+        },
+
+        {
+          "id": "S04",
+          "label": "实数与根式运算",
+          "children": [
+            {
+              "id": "RS01",
+              "label": "平方根",
+              "skills": ["平方根意义", "正负根"],
+              "direct_score": [1,3],
+              "related_score": [3,5],
+              "children": []
+            },
+            {
+              "id": "RS02",
+              "label": "立方根",
+              "skills": ["立方根意义", "实数范围讨论"],
+              "direct_score": [1,3],
+              "related_score": [3,5],
+              "children": []
+            },
+            {
+              "id": "RS03",
+              "label": "根式的基本性质",
+              "skills": ["根式拆分", "根式乘除"],
+              "direct_score": [2,4],
+              "related_score": [3,6],
+              "children": []
+            },
+            {
+              "id": "RS04",
+              "label": "根式化简",
+              "skills": ["二次根式化简", "最简根式"],
+              "direct_score": [2,4],
+              "related_score": [4,7],
+              "children": []
+            },
+            {
+              "id": "RS05",
+              "label": "根式混合运算",
+              "skills": ["同类项合并", "整体化简"],
+              "direct_score": [3,5],
+              "related_score": [5,8],
+              "children": []
+            }
+          ]
+        },
+
+        {
+          "id": "S05",
+          "label": "代数建模基础",
+          "children": [
+            {
+              "id": "M01A",
+              "label": "代数式表示现实问题",
+              "skills": ["变量表示数量", "式子表示关系"],
+              "direct_score": [1,2],
+              "related_score": [2,4],
+              "children": []
+            },
+            {
+              "id": "M01B",
+              "label": "数量关系建模",
+              "skills": ["数形结合", "方程建模"],
+              "direct_score": [2,4],
+              "related_score": [4,6],
+              "children": []
+            },
+            {
+              "id": "M01C",
+              "label": "复杂代数式结构分析",
+              "skills": ["拆项分析", "整体处理"],
+              "direct_score": [3,5],
+              "related_score": [5,8],
+              "children": []
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "id": "M02",
+      "label": "方程与不等式",
+      "children": [
+        {
+          "id": "E01",
+          "label": "一元一次方程",
+          "children": [
+            {
+              "id": "E01A",
+              "label": "方程基本概念",
+              "skills": ["未知数", "等式性质", "等式变形"],
+              "direct_score": [1,2],
+              "related_score": [2,3],
+              "children": []
+            },
+            {
+              "id": "E01B",
+              "label": "移项与合并同类项",
+              "skills": ["移项规则", "符号处理"],
+              "direct_score": [2,3],
+              "related_score": [3,4],
+              "children": []
+            },
+            {
+              "id": "E01C",
+              "label": "方程解的检验",
+              "skills": ["代入检验", "增根分析"],
+              "direct_score": [1,2],
+              "related_score": [2,3],
+              "children": []
+            }
+          ]
+        },
+
+        {
+          "id": "E02",
+          "label": "一元一次方程组",
+          "children": [
+            {
+              "id": "E02A",
+              "label": "代入法",
+              "skills": ["代入变量", "化简求解"],
+              "direct_score": [2,4],
+              "related_score": [3,5],
+              "children": []
+            },
+            {
+              "id": "E02B",
+              "label": "加减法",
+              "skills": ["同系数构造", "消元"],
+              "direct_score": [2,4],
+              "related_score": [4,6],
+              "children": []
+            },
+            {
+              "id": "E02C",
+              "label": "方程组应用建模",
+              "skills": ["数量关系分析", "二元问题建模"],
+              "direct_score": [3,5],
+              "related_score": [5,7],
+              "children": []
+            }
+          ]
+        },
+
+        {
+          "id": "E03",
+          "label": "不等式与不等式组",
+          "children": [
+            {
+              "id": "E03A",
+              "label": "不等式性质",
+              "skills": ["基本性质", "不等式方向判断"],
+              "direct_score": [1,3],
+              "related_score": [2,4],
+              "children": []
+            },
+            {
+              "id": "E03B",
+              "label": "一元一次不等式",
+              "skills": ["移项变形", "解集表示"],
+              "direct_score": [2,4],
+              "related_score": [3,5],
+              "children": []
+            },
+            {
+              "id": "E03C",
+              "label": "数轴表示解集",
+              "skills": ["区间表示法", "开闭区间理解"],
+              "direct_score": [2,3],
+              "related_score": [3,5],
+              "children": []
+            },
+            {
+              "id": "E03D",
+              "label": "不等式组",
+              "skills": ["两个不等式共同解集", "区间交集"],
+              "direct_score": [3,4],
+              "related_score": [4,6],
+              "children": []
+            }
+          ]
+        },
+
+        {
+          "id": "E04",
+          "label": "分式方程",
+          "children": [
+            {
+              "id": "E04A",
+              "label": "分式方程概念",
+              "skills": ["整式作分母", "方程定义域"],
+              "direct_score": [2,3],
+              "related_score": [3,5],
+              "children": []
+            },
+            {
+              "id": "E04B",
+              "label": "方程去分母",
+              "skills": ["最小公倍式", "消分母技巧"],
+              "direct_score": [2,4],
+              "related_score": [4,6],
+              "children": []
+            },
+            {
+              "id": "E04C",
+              "label": "增根分析",
+              "skills": ["定义域限制", "代入检验"],
+              "direct_score": [2,3],
+              "related_score": [4,6],
+              "children": []
+            }
+          ]
+        },
+
+        {
+          "id": "E05",
+          "label": "一元二次方程",
+          "children": [
+            {
+              "id": "E05A",
+              "label": "配方法",
+              "skills": ["完全平方构造", "顶点式连接"],
+              "direct_score": [3,4],
+              "related_score": [5,7],
+              "children": []
+            },
+            {
+              "id": "E05B",
+              "label": "求根公式",
+              "skills": ["判别式", "根的表达"],
+              "direct_score": [3,5],
+              "related_score": [5,8],
+              "children": []
+            },
+            {
+              "id": "E05C",
+              "label": "因式分解解法",
+              "skills": ["二次三项式识别", "零乘积定律"],
+              "direct_score": [3,5],
+              "related_score": [5,8],
+              "children": []
+            },
+            {
+              "id": "E05D",
+              "label": "根与系数关系",
+              "skills": ["韦达定理", "求参数"],
+              "direct_score": [4,6],
+              "related_score": [6,10],
+              "children": []
+            },
+            {
+              "id": "E05E",
+              "label": "方程根的分布",
+              "skills": ["根的符号判断", "根的位置"],
+              "direct_score": [4,6],
+              "related_score": [7,10],
+              "children": []
+            }
+          ]
+        },
+
+        {
+          "id": "E06",
+          "label": "方程思想与实际应用",
+          "children": [
+            {
+              "id": "APP_E1",
+              "label": "行程问题方程建模",
+              "skills": ["路程速度时间关系", "方程建模"],
+              "direct_score": [3,5],
+              "related_score": [5,8],
+              "children": []
+            },
+            {
+              "id": "APP_E2",
+              "label": "工程问题方程建模",
+              "skills": ["工作效率", "整体工作量划分"],
+              "direct_score": [3,5],
+              "related_score": [5,8],
+              "children": []
+            },
+            {
+              "id": "APP_E3",
+              "label": "溶液浓度建模",
+              "skills": ["百分比", "方程求解"],
+              "direct_score": [3,4],
+              "related_score": [5,7],
+              "children": []
+            },
+            {
+              "id": "APP_E4",
+              "label": "几何方程建模",
+              "skills": ["几何量关系", "代数化处理"],
+              "direct_score": [4,6],
+              "related_score": [6,9],
+              "children": []
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "id": "M03",
+      "label": "几何图形与性质",
+      "children": [
+        {
+          "id": "G01",
+          "label": "基本几何知识",
+          "children": [
+            {
+              "id": "G01A",
+              "label": "点线面基本关系",
+              "skills": ["点线面概念", "线段长度", "射线"],
+              "direct_score": [1,2],
+              "related_score": [1,3],
+              "children": []
+            },
+            {
+              "id": "G01B",
+              "label": "角的概念与分类",
+              "skills": ["锐角钝角直角", "平角周角"],
+              "direct_score": [1,2],
+              "related_score": [2,3],
+              "children": []
+            },
+            {
+              "id": "G01C",
+              "label": "角的度量",
+              "skills": ["量角器使用", "角度换算"],
+              "direct_score": [1,2],
+              "related_score": [2,3],
+              "children": []
+            },
+            {
+              "id": "G01D",
+              "label": "对顶角与邻补角",
+              "skills": ["对顶角相等", "补角性质"],
+              "direct_score": [1,2],
+              "related_score": [2,4],
+              "children": []
+            }
+          ]
+        },
+
+        {
+          "id": "G02",
+          "label": "平行线与角",
+          "children": [
+            {
+              "id": "G02A",
+              "label": "平行线判定",
+              "skills": ["同位角相等", "内错角相等", "同旁内角互补"],
+              "direct_score": [2,3],
+              "related_score": [3,5],
+              "children": []
+            },
+            {
+              "id": "G02B",
+              "label": "平行线性质",
+              "skills": ["对应角相等", "内错角相等"],
+              "direct_score": [2,3],
+              "related_score": [3,6],
+              "children": []
+            },
+            {
+              "id": "G02C",
+              "label": "平移与图形性质",
+              "skills": ["平移定义", "图形保持性"],
+              "direct_score": [1,3],
+              "related_score": [3,5],
+              "children": []
+            }
+          ]
+        },
+
+        {
+          "id": "G03",
+          "label": "三角形",
+          "children": [
+            {
+              "id": "G03A",
+              "label": "三角形分类",
+              "skills": ["按角分类", "按边分类"],
+              "direct_score": [1,3],
+              "related_score": [2,4],
+              "children": []
+            },
+            {
+              "id": "G03B",
+              "label": "三角形基本性质",
+              "skills": ["三角形内角和", "外角定理"],
+              "direct_score": [2,4],
+              "related_score": [4,6],
+              "children": []
+            },
+            {
+              "id": "G03C",
+              "label": "三角形稳定性",
+              "skills": ["边构成条件", "三角形不等式"],
+              "direct_score": [2,4],
+              "related_score": [4,6],
+              "children": []
+            },
+            {
+              "id": "G03D",
+              "label": "角平分线性质",
+              "skills": ["等距性质", "分线比例"],
+              "direct_score": [2,4],
+              "related_score": [4,7],
+              "children": []
+            },
+            {
+              "id": "G03E",
+              "label": "中线与垂心",
+              "skills": ["中线定义", "三角形重心"],
+              "direct_score": [2,4],
+              "related_score": [4,6],
+              "children": []
+            },
+            {
+              "id": "G03F",
+              "label": "三角形全等(核心)",
+              "skills": ["SSS", "SAS", "AAS"],
+              "direct_score": [3,5],
+              "related_score": [5,8],
+              "children": []
+            },
+            {
+              "id": "G03G",
+              "label": "全等三角形判定综合应用",
+              "skills": ["构造辅助线", "转化为全等"],
+              "direct_score": [4,6],
+              "related_score": [6,9],
+              "children": []
+            }
+          ]
+        },
+
+        {
+          "id": "G04",
+          "label": "四边形与平行四边形",
+          "children": [
+            {
+              "id": "G04A",
+              "label": "四边形分类",
+              "skills": ["一般四边形性质", "特殊四边形识别"],
+              "direct_score": [1,3],
+              "related_score": [2,4],
+              "children": []
+            },
+            {
+              "id": "G04B",
+              "label": "平行四边形判定",
+              "skills": ["对边平行", "对角线平分"],
+              "direct_score": [2,4],
+              "related_score": [4,6],
+              "children": []
+            },
+            {
+              "id": "G04C",
+              "label": "平行四边形性质",
+              "skills": ["对边相等", "对角相等"],
+              "direct_score": [2,4],
+              "related_score": [4,7],
+              "children": []
+            },
+            {
+              "id": "G04D",
+              "label": "特殊四边形:矩形",
+              "skills": ["判定", "对角线相等"],
+              "direct_score": [2,4],
+              "related_score": [3,6],
+              "children": []
+            },
+            {
+              "id": "G04E",
+              "label": "特殊四边形:菱形",
+              "skills": ["对角线垂直", "对角线平分角"],
+              "direct_score": [2,4],
+              "related_score": [4,7],
+              "children": []
+            },
+            {
+              "id": "G04F",
+              "label": "特殊四边形:正方形",
+              "skills": ["综合性质", "全能图形应用"],
+              "direct_score": [1,3],
+              "related_score": [4,8],
+              "children": []
+            }
+          ]
+        },
+
+        {
+          "id": "G05",
+          "label": "圆与相关性质",
+          "children": [
+            {
+              "id": "G05A",
+              "label": "圆的基本性质",
+              "skills": ["半径直径", "同心圆"],
+              "direct_score": [1,2],
+              "related_score": [2,4],
+              "children": []
+            },
+            {
+              "id": "G05B",
+              "label": "弦与圆心距",
+              "skills": ["弦长变化", "圆心距关系"],
+              "direct_score": [2,4],
+              "related_score": [4,7],
+              "children": []
+            },
+            {
+              "id": "G05C",
+              "label": "切线性质",
+              "skills": ["切线垂直半径", "切割线定理"],
+              "direct_score": [3,5],
+              "related_score": [5,8],
+              "children": []
+            },
+            {
+              "id": "G05D",
+              "label": "圆周角定理(核心)",
+              "skills": ["圆周角等于同弧所对圆心角的一半"],
+              "direct_score": [3,5],
+              "related_score": [6,9],
+              "children": []
+            },
+            {
+              "id": "G05E",
+              "label": "弧长与扇形面积",
+              "skills": ["弧长公式", "扇形面积公式"],
+              "direct_score": [3,5],
+              "related_score": [5,8],
+              "children": []
+            }
+          ]
+        },
+
+        {
+          "id": "G06",
+          "label": "几何综合与辅助线",
+          "children": [
+            {
+              "id": "G06A",
+              "label": "常用辅助线方法",
+              "skills": ["延长线", "平移", "作垂线", "作角平分线"],
+              "direct_score": [3,5],
+              "related_score": [6,10],
+              "children": []
+            },
+            {
+              "id": "G06B",
+              "label": "数形结合思想",
+              "skills": ["代数化几何", "几何量化"],
+              "direct_score": [4,6],
+              "related_score": [6,10],
+              "children": []
+            },
+            {
+              "id": "G06C",
+              "label": "几何证明基础",
+              "skills": ["已知与求证", "推理链", "结构化证明"],
+              "direct_score": [4,6],
+              "related_score": [7,12],
+              "children": []
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "id": "M04",
+      "label": "图形度量",
+      "children": [
+        {
+          "id": "M04A",
+          "label": "基础度量概念",
+          "children": [
+            {
+              "id": "M04A1",
+              "label": "长度与单位换算",
+              "skills": ["毫米厘米米换算", "比例应用"],
+              "direct_score": [1,2],
+              "related_score": [2,3],
+              "children": []
+            },
+            {
+              "id": "M04A2",
+              "label": "面积单位与换算",
+              "skills": ["平方厘米", "平方米", "单位换算"],
+              "direct_score": [1,2],
+              "related_score": [2,3],
+              "children": []
+            },
+            {
+              "id": "M04A3",
+              "label": "体积单位与换算",
+              "skills": ["立方厘米", "立方分米", "升"],
+              "direct_score": [1,2],
+              "related_score": [2,4],
+              "children": []
+            }
+          ]
+        },
+
+        {
+          "id": "M04B",
+          "label": "周长计算",
+          "children": [
+            {
+              "id": "M04B1",
+              "label": "三角形周长",
+              "skills": ["三边相加", "条件关系转换"],
+              "direct_score": [1,3],
+              "related_score": [2,4],
+              "children": []
+            },
+            {
+              "id": "M04B2",
+              "label": "四边形周长",
+              "skills": ["平行四边形", "矩形", "菱形"],
+              "direct_score": [1,3],
+              "related_score": [2,5],
+              "children": []
+            },
+            {
+              "id": "M04B3",
+              "label": "圆的周长",
+              "skills": ["周长公式", "π的应用"],
+              "direct_score": [2,3],
+              "related_score": [3,5],
+              "children": []
+            }
+          ]
+        },
+
+        {
+          "id": "M04C",
+          "label": "面积计算",
+          "children": [
+            {
+              "id": "M04C1",
+              "label": "三角形面积",
+              "skills": ["底×高÷2", "高的作法"],
+              "direct_score": [2,4],
+              "related_score": [3,6],
+              "children": []
+            },
+            {
+              "id": "M04C2",
+              "label": "四边形面积",
+              "skills": ["平行四边形", "梯形面积"],
+              "direct_score": [2,4],
+              "related_score": [3,6],
+              "children": []
+            },
+            {
+              "id": "M04C3",
+              "label": "圆与扇形面积",
+              "skills": ["扇形面积公式", "弧长关系"],
+              "direct_score": [3,5],
+              "related_score": [5,8],
+              "children": []
+            },
+            {
+              "id": "M04C4",
+              "label": "组合图形面积",
+              "skills": ["拆分法", "补形法"],
+              "direct_score": [3,5],
+              "related_score": [5,9],
+              "children": []
+            }
+          ]
+        },
+
+        {
+          "id": "M04D",
+          "label": "立体几何度量(表面积与体积)",
+          "children": [
+            {
+              "id": "M04D1",
+              "label": "长方体与正方体",
+              "skills": ["表面积公式", "体积公式", "几何特征"],
+              "direct_score": [2,4],
+              "related_score": [4,6],
+              "children": []
+            },
+            {
+              "id": "M04D2",
+              "label": "棱柱与棱锥初步",
+              "skills": ["底面积×高", "侧面积结构"],
+              "direct_score": [3,4],
+              "related_score": [4,7],
+              "children": []
+            },
+            {
+              "id": "M04D3",
+              "label": "圆柱体积与表面积",
+              "skills": ["圆柱体积公式", "侧面积公式"],
+              "direct_score": [3,5],
+              "related_score": [5,8],
+              "children": []
+            },
+            {
+              "id": "M04D4",
+              "label": "立体几何展开图",
+              "skills": ["展开图识别", "面与面的关系"],
+              "direct_score": [2,4],
+              "related_score": [4,6],
+              "children": []
+            }
+          ]
+        },
+
+        {
+          "id": "M04E",
+          "label": "图形变换与度量保持",
+          "children": [
+            {
+              "id": "M04E1",
+              "label": "平移",
+              "skills": ["方向与距离", "度量保持"],
+              "direct_score": [2,3],
+              "related_score": [3,5],
+              "children": []
+            },
+            {
+              "id": "M04E2",
+              "label": "旋转",
+              "skills": ["旋转中心", "旋转角度"],
+              "direct_score": [2,4],
+              "related_score": [3,6],
+              "children": []
+            },
+            {
+              "id": "M04E3",
+              "label": "轴对称",
+              "skills": ["对称轴", "对应点性质"],
+              "direct_score": [2,4],
+              "related_score": [3,6],
+              "children": []
+            },
+            {
+              "id": "M04E4",
+              "label": "图形变换综合",
+              "skills": ["三大变换组合", "不变量分析"],
+              "direct_score": [3,5],
+              "related_score": [5,8],
+              "children": []
+            }
+          ]
+        },
+
+        {
+          "id": "M04F",
+          "label": "图形度量综合应用",
+          "children": [
+            {
+              "id": "M04F1",
+              "label": "几何量与代数结合",
+              "skills": ["代数化几何", "未知数建模"],
+              "direct_score": [4,6],
+              "related_score": [6,10],
+              "children": []
+            },
+            {
+              "id": "M04F2",
+              "label": "中考图形计算综合题",
+              "skills": ["面积体积混合", "多步骤推理"],
+              "direct_score": [4,7],
+              "related_score": [7,12],
+              "children": []
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "id": "M05",
+      "label": "相似与勾股",
+      "children": [
+        {
+          "id": "SIM01",
+          "label": "相似三角形判定",
+          "children": [
+            {
+              "id": "SIM01A",
+              "label": "相似三角形概念",
+              "skills": ["对应角相等", "对应边成比例"],
+              "direct_score": [2,3],
+              "related_score": [3,5],
+              "children": []
+            },
+            {
+              "id": "SIM01B",
+              "label": "AA 相似判定",
+              "skills": ["两角对应相等", "角角相似"],
+              "direct_score": [3,5],
+              "related_score": [5,7],
+              "children": []
+            },
+            {
+              "id": "SIM01C",
+              "label": "SAS 比例判定",
+              "skills": ["两边成比例", "夹角相等"],
+              "direct_score": [3,5],
+              "related_score": [5,7],
+              "children": []
+            },
+            {
+              "id": "SIM01D",
+              "label": "SS 比例判定",
+              "skills": ["三组成比例", "比例验证"],
+              "direct_score": [3,5],
+              "related_score": [5,7],
+              "children": []
+            },
+            {
+              "id": "SIM01E",
+              "label": "相似三角形判定综合应用",
+              "skills": ["构造相似", "比例线段构造"],
+              "direct_score": [4,6],
+              "related_score": [6,10],
+              "children": []
+            }
+          ]
+        },
+
+        {
+          "id": "SIM02",
+          "label": "相似三角形性质",
+          "children": [
+            {
+              "id": "SIM02A",
+              "label": "对应边成比例",
+              "skills": ["比例关系建立", "比例计算"],
+              "direct_score": [3,5],
+              "related_score": [5,8],
+              "children": []
+            },
+            {
+              "id": "SIM02B",
+              "label": "面积比",
+              "skills": ["面积比 = 边比平方", "相似比关系"],
+              "direct_score": [3,5],
+              "related_score": [5,8],
+              "children": []
+            },
+            {
+              "id": "SIM02C",
+              "label": "体积比基础",
+              "skills": ["三维图形相似的体积比", "边比立方"],
+              "direct_score": [2,4],
+              "related_score": [4,7],
+              "children": []
+            },
+            {
+              "id": "SIM02D",
+              "label": "线段比例分割(平行线)",
+              "skills": ["平行线分线段比例", "三线段比例定理"],
+              "direct_score": [3,5],
+              "related_score": [5,9],
+              "children": []
+            },
+            {
+              "id": "SIM02E",
+              "label": "相似性质综合应用",
+              "skills": ["构造辅助线", "代数几何结合"],
+              "direct_score": [4,7],
+              "related_score": [7,12],
+              "children": []
+            }
+          ]
+        },
+
+        {
+          "id": "PY01",
+          "label": "勾股定理与直角三角形",
+          "children": [
+            {
+              "id": "PY01A",
+              "label": "勾股定理",
+              "skills": ["a² + b² = c²", "几何意义"],
+              "direct_score": [3,5],
+              "related_score": [5,8],
+              "children": []
+            },
+            {
+              "id": "PY01B",
+              "label": "勾股逆定理",
+              "skills": ["判定是否为直角三角形"],
+              "direct_score": [2,4],
+              "related_score": [4,7],
+              "children": []
+            },
+            {
+              "id": "PY01C",
+              "label": "勾股数",
+              "skills": ["常见勾股数", "比例放缩"],
+              "direct_score": [2,3],
+              "related_score": [3,6],
+              "children": []
+            },
+            {
+              "id": "PY01D",
+              "label": "直角三角形性质",
+              "skills": ["锐角三角函数雏形", "特殊直角三角形"],
+              "direct_score": [3,5],
+              "related_score": [5,8],
+              "children": []
+            }
+          ]
+        },
+
+        {
+          "id": "PY02",
+          "label": "勾股定理应用",
+          "children": [
+            {
+              "id": "PY02A",
+              "label": "平面距离计算",
+              "skills": ["坐标系距离", "几何点距"],
+              "direct_score": [3,5],
+              "related_score": [5,8],
+              "children": []
+            },
+            {
+              "id": "PY02B",
+              "label": "最短路径问题",
+              "skills": ["反射法", "路径构造"],
+              "direct_score": [4,6],
+              "related_score": [6,10],
+              "children": []
+            },
+            {
+              "id": "PY02C",
+              "label": "复杂图形中的直角关系",
+              "skills": ["几何结构分析", "辅助线构造"],
+              "direct_score": [4,7],
+              "related_score": [7,12],
+              "children": []
+            },
+            {
+              "id": "PY02D",
+              "label": "勾股+相似综合(中考核心)",
+              "skills": ["相似构造", "比例 + 勾股联合"],
+              "direct_score": [4,7],
+              "related_score": [8,14],
+              "children": []
+            }
+          ]
+        },
+
+        {
+          "id": "SIM03",
+          "label": "相似与勾股在压轴题中的整合",
+          "children": [
+            {
+              "id": "SIM03A",
+              "label": "辅助线构造策略",
+              "skills": ["平移法", "延长线法", "旋转法"],
+              "direct_score": [5,7],
+              "related_score": [7,12],
+              "children": []
+            },
+            {
+              "id": "SIM03B",
+              "label": "比例关系链",
+              "skills": ["多组比例建立", "代数化比例"],
+              "direct_score": [5,7],
+              "related_score": [8,14],
+              "children": []
+            },
+            {
+              "id": "SIM03C",
+              "label": "直角结构构造",
+              "skills": ["构造垂线", "反射构造直角"],
+              "direct_score": [4,6],
+              "related_score": [6,10],
+              "children": []
+            },
+            {
+              "id": "SIM03D",
+              "label": "几何综合(压轴结构)",
+              "skills": ["相似 + 勾股 + 面积 + 角度", "多步骤推理"],
+              "direct_score": [6,9],
+              "related_score": [10,18],
+              "children": []
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "id": "M06",
+      "label": "统计与概率",
+      "children": [
+        {
+          "id": "ST01",
+          "label": "数据收集与整理",
+          "children": [
+            {
+              "id": "ST01A",
+              "label": "数据的获取与抽样",
+              "skills": ["总体与样本", "简单随机抽样"],
+              "direct_score": [1,2],
+              "related_score": [2,4],
+              "children": []
+            },
+            {
+              "id": "ST01B",
+              "label": "数据分类整理",
+              "skills": ["分组整理", "列表与频数"],
+              "direct_score": [1,3],
+              "related_score": [2,4],
+              "children": []
+            },
+            {
+              "id": "ST01C",
+              "label": "频数与频率",
+              "skills": ["频数", "频率", "累计频率"],
+              "direct_score": [2,3],
+              "related_score": [3,5],
+              "children": []
+            }
+          ]
+        },
+
+        {
+          "id": "ST02",
+          "label": "数据的统计量(集中趋势)",
+          "children": [
+            {
+              "id": "ST02A",
+              "label": "平均数(均值)",
+              "skills": ["计算平均数", "加权平均"],
+              "direct_score": [2,3],
+              "related_score": [3,6],
+              "children": []
+            },
+            {
+              "id": "ST02B",
+              "label": "中位数",
+              "skills": ["排序", "奇偶位置处理"],
+              "direct_score": [1,3],
+              "related_score": [2,5],
+              "children": []
+            },
+            {
+              "id": "ST02C",
+              "label": "众数",
+              "skills": ["数据出现频率最高", "分类众数"],
+              "direct_score": [1,2],
+              "related_score": [2,4],
+              "children": []
+            }
+          ]
+        },
+
+        {
+          "id": "ST03",
+          "label": "数据的离散程度(新课标 2022)",
+          "children": [
+            {
+              "id": "ST03A",
+              "label": "极差",
+              "skills": ["最大-最小", "数据波动"],
+              "direct_score": [1,2],
+              "related_score": [2,4],
+              "children": []
+            },
+            {
+              "id": "ST03B",
+              "label": "方差概念(初步)",
+              "skills": ["偏差平方和", "离散程度理解"],
+              "direct_score": [2,3],
+              "related_score": [3,6],
+              "children": []
+            },
+            {
+              "id": "ST03C",
+              "label": "标准差(认识)",
+              "skills": ["方差开根号", "稳定性判断"],
+              "direct_score": [2,3],
+              "related_score": [4,7],
+              "children": []
+            }
+          ]
+        },
+
+        {
+          "id": "ST04",
+          "label": "统计图表示与分析",
+          "children": [
+            {
+              "id": "ST04A",
+              "label": "条形图",
+              "skills": ["分类比较", "趋势识别"],
+              "direct_score": [1,2],
+              "related_score": [2,4],
+              "children": []
+            },
+            {
+              "id": "ST04B",
+              "label": "折线图",
+              "skills": ["变化趋势", "最大最小值判断"],
+              "direct_score": [1,3],
+              "related_score": [3,5],
+              "children": []
+            },
+            {
+              "id": "ST04C",
+              "label": "扇形图",
+              "skills": ["角度表示频率", "组成比例分析"],
+              "direct_score": [2,3],
+              "related_score": [3,6],
+              "children": []
+            },
+            {
+              "id": "ST04D",
+              "label": "统计图综合解读",
+              "skills": ["关系推理", "数据变化分析"],
+              "direct_score": [3,5],
+              "related_score": [5,8],
+              "children": []
+            }
+          ]
+        },
+
+        {
+          "id": "ST05",
+          "label": "概率初步",
+          "children": [
+            {
+              "id": "ST05A",
+              "label": "随机事件",
+              "skills": ["必然事件", "不可能事件", "随机事件理解"],
+              "direct_score": [1,2],
+              "related_score": [2,4],
+              "children": []
+            },
+            {
+              "id": "ST05B",
+              "label": "古典概率(等可能模型)",
+              "skills": ["概率 =  favorable / total", "等可能事件"],
+              "direct_score": [2,3],
+              "related_score": [3,5],
+              "children": []
+            },
+            {
+              "id": "ST05C",
+              "label": "实验概率",
+              "skills": ["频率逼近概率", "大量试验规律"],
+              "direct_score": [2,3],
+              "related_score": [3,6],
+              "children": []
+            },
+            {
+              "id": "ST05D",
+              "label": "简单树状图与列表法",
+              "skills": ["列举所有可能", "概率空间构建"],
+              "direct_score": [2,4],
+              "related_score": [4,7],
+              "children": []
+            }
+          ]
+        },
+
+        {
+          "id": "ST06",
+          "label": "统计与概率综合应用(中考核心)",
+          "children": [
+            {
+              "id": "ST06A",
+              "label": "统计综合题",
+              "skills": ["多图表综合分析", "数据趋势推断"],
+              "direct_score": [3,5],
+              "related_score": [5,9],
+              "children": []
+            },
+            {
+              "id": "ST06B",
+              "label": "概率综合题",
+              "skills": ["树状图+表格法", "多阶段概率"],
+              "direct_score": [3,5],
+              "related_score": [6,10],
+              "children": []
+            },
+            {
+              "id": "ST06C",
+              "label": "统计与概率融合应用",
+              "skills": ["现实问题建模", "基于数据的概率推断"],
+              "direct_score": [4,6],
+              "related_score": [7,12],
+              "children": []
+            }
+          ]
+        }
+      ]
+    }
+  ]
+}

+ 3405 - 0
知了数学题库/tree_new.json

@@ -0,0 +1,3405 @@
+{
+  "id": 1,
+  "label": "初中数学知识体系",
+  "parent_id": null,
+  "kp_level": "all",
+  "children": [
+    {
+      "id": 2,
+      "label": "第一章 有理数",
+      "parent_id": 1,
+      "kp_level": "chapter",
+      "children": [
+        {
+          "id": 3,
+          "label": "1.1 正数和负数",
+          "parent_id": 2,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": []
+        },
+        {
+          "id": 4,
+          "label": "1.2 有理数及其大小比较",
+          "parent_id": 2,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 5,
+              "label": "1.2.1 有理数的概念",
+              "parent_id": 4,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 6,
+              "label": "1.2.2 数轴",
+              "parent_id": 4,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 7,
+              "label": "1.2.3 相反数",
+              "parent_id": 4,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 8,
+              "label": "1.2.4 绝对值",
+              "parent_id": 4,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 9,
+              "label": "1.2.5 有理数的大小比较",
+              "parent_id": 4,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        },
+        {
+          "id": 335,
+          "label": "1",
+          "parent_id": 2,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": []
+        }
+      ]
+    },
+    {
+      "id": 10,
+      "label": "第二章 有理数的运算",
+      "parent_id": 1,
+      "kp_level": "chapter",
+      "children": [
+        {
+          "id": 11,
+          "label": "2.1 有理数的加法与减法",
+          "parent_id": 10,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 12,
+              "label": "2.1.1 有理数的加法",
+              "parent_id": 11,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": [
+                {
+                  "id": 13,
+                  "label": "有理数的加法法则",
+                  "parent_id": 12,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                },
+                {
+                  "id": 14,
+                  "label": "有理数加法的运算律",
+                  "parent_id": 12,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                }
+              ]
+            },
+            {
+              "id": 15,
+              "label": "2.1.2 有理数的减法",
+              "parent_id": 11,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": [
+                {
+                  "id": 16,
+                  "label": "有理数的减法法则",
+                  "parent_id": 15,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                },
+                {
+                  "id": 17,
+                  "label": "有理数的加减混合运算",
+                  "parent_id": 15,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                }
+              ]
+            }
+          ]
+        },
+        {
+          "id": 18,
+          "label": "2.2 有理数的乘法与除法",
+          "parent_id": 10,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 19,
+              "label": "2.2.1 有理数的乘法",
+              "parent_id": 18,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": [
+                {
+                  "id": 20,
+                  "label": "有理数的乘法法则",
+                  "parent_id": 19,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                },
+                {
+                  "id": 21,
+                  "label": "有理数乘法的运算律",
+                  "parent_id": 19,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                }
+              ]
+            },
+            {
+              "id": 22,
+              "label": "2.2.2 有理数的除法",
+              "parent_id": 18,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": [
+                {
+                  "id": 23,
+                  "label": "有理数的除法法则",
+                  "parent_id": 22,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                },
+                {
+                  "id": 24,
+                  "label": "有理数的加减乘除混合运算",
+                  "parent_id": 22,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                }
+              ]
+            }
+          ]
+        },
+        {
+          "id": 25,
+          "label": "2.3 有理数的乘方",
+          "parent_id": 10,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 26,
+              "label": "2.3.1 乘方",
+              "parent_id": 25,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": [
+                {
+                  "id": 27,
+                  "label": "乘方的性质",
+                  "parent_id": 26,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                },
+                {
+                  "id": 28,
+                  "label": "有理数的混合运算",
+                  "parent_id": 26,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                }
+              ]
+            },
+            {
+              "id": 29,
+              "label": "2.3.2 科学记数法",
+              "parent_id": 25,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 30,
+              "label": "2.3.3 近似数",
+              "parent_id": 25,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "id": 31,
+      "label": "第三章 代数式",
+      "parent_id": 1,
+      "kp_level": "chapter",
+      "children": [
+        {
+          "id": 32,
+          "label": "3.1 列代数式表示数量关系",
+          "parent_id": 31,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 33,
+              "label": "代数式",
+              "parent_id": 32,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 34,
+              "label": "列代数式",
+              "parent_id": 32,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 35,
+              "label": "反比例关系",
+              "parent_id": 32,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        },
+        {
+          "id": 36,
+          "label": "3.2 代数式的值",
+          "parent_id": 31,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": []
+        }
+      ]
+    },
+    {
+      "id": 37,
+      "label": "第四章 整式的加减",
+      "parent_id": 1,
+      "kp_level": "chapter",
+      "children": [
+        {
+          "id": 38,
+          "label": "4.1 整式",
+          "parent_id": 37,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 39,
+              "label": "单项式",
+              "parent_id": 38,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 40,
+              "label": "多项式",
+              "parent_id": 38,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        },
+        {
+          "id": 41,
+          "label": "4.2 整式的加法与减法",
+          "parent_id": 37,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 42,
+              "label": "合并同类项",
+              "parent_id": 41,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 43,
+              "label": "去括号",
+              "parent_id": 41,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 44,
+              "label": "整式的加减",
+              "parent_id": 41,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "id": 45,
+      "label": "第五章 一元一次方程",
+      "parent_id": 1,
+      "kp_level": "chapter",
+      "children": [
+        {
+          "id": 46,
+          "label": "5.1 方程",
+          "parent_id": 45,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 47,
+              "label": "5.1.1 从算式到方程",
+              "parent_id": 46,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 48,
+              "label": "5.1.2 等式的性质",
+              "parent_id": 46,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        },
+        {
+          "id": 49,
+          "label": "5.2 解一元一次方程",
+          "parent_id": 45,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 50,
+              "label": "合并同类项解一元一次方程",
+              "parent_id": 49,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 51,
+              "label": "移项解一元一次方程",
+              "parent_id": 49,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 52,
+              "label": "去括号解一元一次方程",
+              "parent_id": 49,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 53,
+              "label": "去分母解一元一次方程",
+              "parent_id": 49,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        },
+        {
+          "id": 54,
+          "label": "5.3 实际问题与一元一次方程",
+          "parent_id": 45,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 55,
+              "label": "配套问题与工程问题",
+              "parent_id": 54,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 56,
+              "label": "商品销售问题与比赛积分问题",
+              "parent_id": 54,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 57,
+              "label": "方案决策问题与分段计费问题",
+              "parent_id": 54,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 58,
+              "label": "其他问题",
+              "parent_id": 54,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "id": 59,
+      "label": "第六章 几何图形初步",
+      "parent_id": 1,
+      "kp_level": "chapter",
+      "children": [
+        {
+          "id": 60,
+          "label": "6.1 几何图形",
+          "parent_id": 59,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 61,
+              "label": "6.1.1 立体图形与平面图形",
+              "parent_id": 60,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": [
+                {
+                  "id": 62,
+                  "label": "立体图形与平面图形的概念",
+                  "parent_id": 61,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                },
+                {
+                  "id": 63,
+                  "label": "从不同方向看物体",
+                  "parent_id": 61,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                },
+                {
+                  "id": 64,
+                  "label": "立体图形的展开与折叠",
+                  "parent_id": 61,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                }
+              ]
+            },
+            {
+              "id": 65,
+              "label": "6.1.2 点、线、面、体",
+              "parent_id": 60,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        },
+        {
+          "id": 66,
+          "label": "6.2 直线、射线、线段",
+          "parent_id": 59,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 67,
+              "label": "6.2.1 直线、射线、线段的概念",
+              "parent_id": 66,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 68,
+              "label": "6.2.2 线段的比较与运算",
+              "parent_id": 66,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        },
+        {
+          "id": 69,
+          "label": "6.3 角",
+          "parent_id": 59,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 70,
+              "label": "6.3.1 角的概念",
+              "parent_id": 69,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 71,
+              "label": "6.3.2 角的比较与运算",
+              "parent_id": 69,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 72,
+              "label": "6.3.3 余角和补角",
+              "parent_id": 69,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "id": 73,
+      "label": "第七章 相交线与平行线",
+      "parent_id": 1,
+      "kp_level": "chapter",
+      "children": [
+        {
+          "id": 74,
+          "label": "7.1 相交线",
+          "parent_id": 73,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 75,
+              "label": "7.1.1 两条直线相交",
+              "parent_id": 74,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 76,
+              "label": "7.1.2 两条直线垂直",
+              "parent_id": 74,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": [
+                {
+                  "id": 77,
+                  "label": "垂线及其画法",
+                  "parent_id": 76,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                },
+                {
+                  "id": 78,
+                  "label": "垂线段最短",
+                  "parent_id": 76,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                }
+              ]
+            },
+            {
+              "id": 79,
+              "label": "7.1.3 两条直线被第三条直线所截",
+              "parent_id": 74,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        },
+        {
+          "id": 80,
+          "label": "7.2 平行线",
+          "parent_id": 73,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 81,
+              "label": "7.2.1 平行线的概念",
+              "parent_id": 80,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 82,
+              "label": "7.2.2 平行线的判定",
+              "parent_id": 80,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 83,
+              "label": "7.2.3 平行线的性质",
+              "parent_id": 80,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": [
+                {
+                  "id": 84,
+                  "label": "平行线的性质",
+                  "parent_id": 83,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                },
+                {
+                  "id": 85,
+                  "label": "平行线的性质与判定",
+                  "parent_id": 83,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                }
+              ]
+            }
+          ]
+        },
+        {
+          "id": 86,
+          "label": "7.3 定义、命题、定理",
+          "parent_id": 73,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": []
+        },
+        {
+          "id": 87,
+          "label": "7.4 平移",
+          "parent_id": 73,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": []
+        }
+      ]
+    },
+    {
+      "id": 88,
+      "label": "第八章 实数",
+      "parent_id": 1,
+      "kp_level": "chapter",
+      "children": [
+        {
+          "id": 89,
+          "label": "8.1 平方根",
+          "parent_id": 88,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 90,
+              "label": "平方根的概念",
+              "parent_id": 89,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 91,
+              "label": "算术平方根",
+              "parent_id": 89,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 92,
+              "label": "算术平方根的估算",
+              "parent_id": 89,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        },
+        {
+          "id": 93,
+          "label": "8.2 立方根",
+          "parent_id": 88,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": []
+        },
+        {
+          "id": 94,
+          "label": "8.3 实数及其简单运算",
+          "parent_id": 88,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": []
+        }
+      ]
+    },
+    {
+      "id": 95,
+      "label": "第九章 平面直角坐标系",
+      "parent_id": 1,
+      "kp_level": "chapter",
+      "children": [
+        {
+          "id": 96,
+          "label": "9.1 用坐标描述平面内点的位置",
+          "parent_id": 95,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 97,
+              "label": "9.1.1 平面直角坐标系的概念",
+              "parent_id": 96,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 98,
+              "label": "9.1.2 用坐标描述简单几何图形",
+              "parent_id": 96,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        },
+        {
+          "id": 99,
+          "label": "9.2 坐标方法的简单应用",
+          "parent_id": 95,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 100,
+              "label": "9.2.1 用坐标表示地理位置",
+              "parent_id": 99,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 101,
+              "label": "9.2.2 用坐标表示平移",
+              "parent_id": 99,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "id": 102,
+      "label": "第十章 二元一次方程组",
+      "parent_id": 1,
+      "kp_level": "chapter",
+      "children": [
+        {
+          "id": 103,
+          "label": "10.1 二元一次方程组的概念",
+          "parent_id": 102,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": []
+        },
+        {
+          "id": 104,
+          "label": "10.2 消元——解二元一次方程组",
+          "parent_id": 102,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 105,
+              "label": "10.2.1 代入消元法",
+              "parent_id": 104,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 106,
+              "label": "10.2.2 加减消元法",
+              "parent_id": 104,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        },
+        {
+          "id": 107,
+          "label": "10.3 实际问题与二元一次方程组",
+          "parent_id": 102,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": []
+        },
+        {
+          "id": 108,
+          "label": "10.4 三元一次方程组的解法",
+          "parent_id": 102,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": []
+        }
+      ]
+    },
+    {
+      "id": 109,
+      "label": "第十一章 不等式与不等式组",
+      "parent_id": 1,
+      "kp_level": "chapter",
+      "children": [
+        {
+          "id": 110,
+          "label": "11.1 不等式",
+          "parent_id": 109,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 111,
+              "label": "11.1.1 不等式及其解集",
+              "parent_id": 110,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 112,
+              "label": "11.1.2 不等式的性质",
+              "parent_id": 110,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        },
+        {
+          "id": 113,
+          "label": "11.2 一元一次不等式",
+          "parent_id": 109,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 114,
+              "label": "解一元一次不等式",
+              "parent_id": 113,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 115,
+              "label": "一元一次不等式的应用",
+              "parent_id": 113,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        },
+        {
+          "id": 116,
+          "label": "11.3 一元一次不等式组",
+          "parent_id": 109,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": []
+        }
+      ]
+    },
+    {
+      "id": 117,
+      "label": "第十二章 数据的收集、整理与描述",
+      "parent_id": 1,
+      "kp_level": "chapter",
+      "children": [
+        {
+          "id": 118,
+          "label": "12.1 统计调查",
+          "parent_id": 117,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 119,
+              "label": "12.1.1 全面调查",
+              "parent_id": 118,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 120,
+              "label": "12.1.2 抽样调查",
+              "parent_id": 118,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        },
+        {
+          "id": 121,
+          "label": "12.2 用统计图描述数据",
+          "parent_id": 117,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 122,
+              "label": "12.2.1 扇形图、条形图和折线图",
+              "parent_id": 121,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 123,
+              "label": "12.2.2 直方图",
+              "parent_id": 121,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 124,
+              "label": "12.2.3 趋势图",
+              "parent_id": 121,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "id": 125,
+      "label": "第十三章 三角形",
+      "parent_id": 1,
+      "kp_level": "chapter",
+      "children": [
+        {
+          "id": 126,
+          "label": "13.1 与三角形有关的线段",
+          "parent_id": 125,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 127,
+              "label": "13.1.1 三角形的边",
+              "parent_id": 126,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 128,
+              "label": "13.1.2 三角形的高、中线与角平分线",
+              "parent_id": 126,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 129,
+              "label": "13.1.3 三角形的稳定性",
+              "parent_id": 126,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        },
+        {
+          "id": 130,
+          "label": "13.2 与三角形有关的角",
+          "parent_id": 125,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 131,
+              "label": "13.2.1 三角形的内角",
+              "parent_id": 130,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": [
+                {
+                  "id": 132,
+                  "label": "三角形的内角的概念",
+                  "parent_id": 131,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                },
+                {
+                  "id": 133,
+                  "label": "直角三角形的两个锐角互余",
+                  "parent_id": 131,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                }
+              ]
+            },
+            {
+              "id": 134,
+              "label": "13.2.2 三角形的外角",
+              "parent_id": 130,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        },
+        {
+          "id": 135,
+          "label": "13.3 多边形及其内角和",
+          "parent_id": 125,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": []
+        }
+      ]
+    },
+    {
+      "id": 136,
+      "label": "第十四章 全等三角形",
+      "parent_id": 1,
+      "kp_level": "chapter",
+      "children": [
+        {
+          "id": 137,
+          "label": "14.1 全等三角形",
+          "parent_id": 136,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": []
+        },
+        {
+          "id": 138,
+          "label": "14.2 三角形全等的判定",
+          "parent_id": 136,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 139,
+              "label": "三边证全等(SSS)",
+              "parent_id": 138,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 140,
+              "label": "两边及夹角证全等(SAS)",
+              "parent_id": 138,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 141,
+              "label": "两角及一边证全等(ASA、AAS)",
+              "parent_id": 138,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 142,
+              "label": "斜边及一直角边证全等(HL)",
+              "parent_id": 138,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        },
+        {
+          "id": 143,
+          "label": "14.3 角的平分线的性质",
+          "parent_id": 136,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 144,
+              "label": "角的平分线的性质的概念",
+              "parent_id": 143,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 145,
+              "label": "角的平分线的判定",
+              "parent_id": 143,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "id": 146,
+      "label": "第十五章 轴对称",
+      "parent_id": 1,
+      "kp_level": "chapter",
+      "children": [
+        {
+          "id": 147,
+          "label": "15.1 轴对称",
+          "parent_id": 146,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 148,
+              "label": "轴对称的概念",
+              "parent_id": 147,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 149,
+              "label": "线段的垂直平分线的性质",
+              "parent_id": 147,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        },
+        {
+          "id": 150,
+          "label": "15.2 画轴对称图形",
+          "parent_id": 146,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 151,
+              "label": "画轴对称图形的概念",
+              "parent_id": 150,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 152,
+              "label": "用坐标表示轴对称",
+              "parent_id": 150,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        },
+        {
+          "id": 153,
+          "label": "15.3 等腰三角形",
+          "parent_id": 146,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 154,
+              "label": "15.3.1 等腰三角形的概念",
+              "parent_id": 153,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": [
+                {
+                  "id": 155,
+                  "label": "等腰三角形的性质",
+                  "parent_id": 154,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                },
+                {
+                  "id": 156,
+                  "label": "等腰三角形的判定",
+                  "parent_id": 154,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                }
+              ]
+            },
+            {
+              "id": 157,
+              "label": "15.3.2 等边三角形",
+              "parent_id": 153,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": [
+                {
+                  "id": 158,
+                  "label": "等边三角形的性质与判定",
+                  "parent_id": 157,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                },
+                {
+                  "id": 159,
+                  "label": "含30°角的直角三角形的性质",
+                  "parent_id": 157,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                }
+              ]
+            }
+          ]
+        },
+        {
+          "id": 160,
+          "label": "15.4 课题学习 最短路径问题",
+          "parent_id": 146,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": []
+        }
+      ]
+    },
+    {
+      "id": 161,
+      "label": "第十六章 整式的乘法与因式分解",
+      "parent_id": 1,
+      "kp_level": "chapter",
+      "children": [
+        {
+          "id": 162,
+          "label": "16.1 整式的乘法",
+          "parent_id": 161,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 163,
+              "label": "16.1.1 同底数幂的乘法",
+              "parent_id": 162,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 164,
+              "label": "16.1.2 幂的乘方",
+              "parent_id": 162,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 165,
+              "label": "16.1.3 积的乘方",
+              "parent_id": 162,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 166,
+              "label": "16.1.4 整式的乘法的概念",
+              "parent_id": 162,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": [
+                {
+                  "id": 167,
+                  "label": "单项式乘单项式",
+                  "parent_id": 166,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                },
+                {
+                  "id": 168,
+                  "label": "单项式乘多项式",
+                  "parent_id": 166,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                },
+                {
+                  "id": 169,
+                  "label": "多项式乘多项式",
+                  "parent_id": 166,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                },
+                {
+                  "id": 170,
+                  "label": "同底数幂的除法",
+                  "parent_id": 166,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                },
+                {
+                  "id": 171,
+                  "label": "单(多)项式除以单项式",
+                  "parent_id": 166,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                }
+              ]
+            }
+          ]
+        },
+        {
+          "id": 172,
+          "label": "16.2 乘法公式",
+          "parent_id": 161,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 173,
+              "label": "16.2.1 平方差公式",
+              "parent_id": 172,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 174,
+              "label": "16.2.2 完全平方公式",
+              "parent_id": 172,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        },
+        {
+          "id": 175,
+          "label": "16.3 因式分解",
+          "parent_id": 161,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 176,
+              "label": "16.3.1 提公因式法",
+              "parent_id": 175,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 177,
+              "label": "16.3.2 公式法",
+              "parent_id": 175,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "id": 178,
+      "label": "第十七章 分式",
+      "parent_id": 1,
+      "kp_level": "chapter",
+      "children": [
+        {
+          "id": 179,
+          "label": "17.1 分式",
+          "parent_id": 178,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": []
+        },
+        {
+          "id": 180,
+          "label": "17.2 分式的运算",
+          "parent_id": 178,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 181,
+              "label": "17.2.1 分式的乘除",
+              "parent_id": 180,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 182,
+              "label": "17.2.2 分式的加减",
+              "parent_id": 180,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 183,
+              "label": "17.2.3 整数指数幂",
+              "parent_id": 180,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        },
+        {
+          "id": 184,
+          "label": "17.3 分式方程",
+          "parent_id": 178,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": []
+        }
+      ]
+    },
+    {
+      "id": 185,
+      "label": "第十八章 二次根式",
+      "parent_id": 1,
+      "kp_level": "chapter",
+      "children": [
+        {
+          "id": 186,
+          "label": "18.1 二次根式",
+          "parent_id": 185,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 187,
+              "label": "18.1.1 二次根式的概念",
+              "parent_id": 186,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 188,
+              "label": "18.1.2 二次根式的性质",
+              "parent_id": 186,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        },
+        {
+          "id": 189,
+          "label": "18.2 二次根式的乘除",
+          "parent_id": 185,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 190,
+              "label": "18.2.1 二次根式的乘法",
+              "parent_id": 189,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 191,
+              "label": "18.2.2 二次根式的除法",
+              "parent_id": 189,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        },
+        {
+          "id": 192,
+          "label": "18.3 二次根式的加减",
+          "parent_id": 185,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 193,
+              "label": "二次根式的加减的概念",
+              "parent_id": 192,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 194,
+              "label": "二次根式的混合运算",
+              "parent_id": 192,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "id": 195,
+      "label": "第十九章 勾股定理",
+      "parent_id": 1,
+      "kp_level": "chapter",
+      "children": [
+        {
+          "id": 196,
+          "label": "19.1 勾股定理",
+          "parent_id": 195,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 197,
+              "label": "19.1.1 勾股定理的概念",
+              "parent_id": 196,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 198,
+              "label": "19.1.2 勾股定理的应用",
+              "parent_id": 196,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        },
+        {
+          "id": 199,
+          "label": "19.2 勾股定理的逆定理",
+          "parent_id": 195,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 200,
+              "label": "勾股定理的逆定理的概念",
+              "parent_id": 199,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 201,
+              "label": "勾股定理的逆定理的应用",
+              "parent_id": 199,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "id": 202,
+      "label": "第二十章 平行四边形",
+      "parent_id": 1,
+      "kp_level": "chapter",
+      "children": [
+        {
+          "id": 203,
+          "label": "20.1 平行四边形",
+          "parent_id": 202,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 204,
+              "label": "20.1.1 平行四边形的性质",
+              "parent_id": 203,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": [
+                {
+                  "id": 205,
+                  "label": "平行四边形的边、角性质",
+                  "parent_id": 204,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                },
+                {
+                  "id": 206,
+                  "label": "平行四边形对角线的性质",
+                  "parent_id": 204,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                }
+              ]
+            },
+            {
+              "id": 207,
+              "label": "20.1.2 平行四边形的判定",
+              "parent_id": 203,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": [
+                {
+                  "id": 208,
+                  "label": "平行四边形的判定基本概念",
+                  "parent_id": 207,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                },
+                {
+                  "id": 209,
+                  "label": "三角形的中位线",
+                  "parent_id": 207,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                }
+              ]
+            }
+          ]
+        },
+        {
+          "id": 210,
+          "label": "20.2 特殊的平行四边形",
+          "parent_id": 202,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 211,
+              "label": "20.2.1 矩形",
+              "parent_id": 210,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": [
+                {
+                  "id": 212,
+                  "label": "矩形的性质",
+                  "parent_id": 211,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                },
+                {
+                  "id": 213,
+                  "label": "矩形的判定",
+                  "parent_id": 211,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                }
+              ]
+            },
+            {
+              "id": 214,
+              "label": "20.2.2 菱形",
+              "parent_id": 210,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": [
+                {
+                  "id": 215,
+                  "label": "菱形的性质",
+                  "parent_id": 214,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                },
+                {
+                  "id": 216,
+                  "label": "菱形的判定",
+                  "parent_id": 214,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                }
+              ]
+            },
+            {
+              "id": 217,
+              "label": "20.2.3 正方形",
+              "parent_id": 210,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "id": 218,
+      "label": "第二十一章 一次函数",
+      "parent_id": 1,
+      "kp_level": "chapter",
+      "children": [
+        {
+          "id": 219,
+          "label": "21.1 函数",
+          "parent_id": 218,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 220,
+              "label": "21.1.1 变量与函数",
+              "parent_id": 219,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 221,
+              "label": "21.1.2 函数的图象",
+              "parent_id": 219,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": [
+                {
+                  "id": 222,
+                  "label": "函数的图象及其画法",
+                  "parent_id": 221,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                },
+                {
+                  "id": 223,
+                  "label": "函数的表示方法",
+                  "parent_id": 221,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                }
+              ]
+            }
+          ]
+        },
+        {
+          "id": 224,
+          "label": "21.2 一次函数",
+          "parent_id": 218,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 225,
+              "label": "21.2.1 正比例函数",
+              "parent_id": 224,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 226,
+              "label": "21.2.2 一次函数(1)",
+              "parent_id": 224,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 227,
+              "label": "21.2.3 一次函数与方程、不等式",
+              "parent_id": 224,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        },
+        {
+          "id": 228,
+          "label": "21.3 课题学习 选择方案",
+          "parent_id": 218,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": []
+        }
+      ]
+    },
+    {
+      "id": 229,
+      "label": "第二十二章 数据的分析",
+      "parent_id": 1,
+      "kp_level": "chapter",
+      "children": [
+        {
+          "id": 230,
+          "label": "22.1 数据的集中趋势",
+          "parent_id": 229,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 231,
+              "label": "22.1.1 平均数",
+              "parent_id": 230,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 232,
+              "label": "22.1.2 中位数和众数",
+              "parent_id": 230,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        },
+        {
+          "id": 233,
+          "label": "22.2 数据的波动程度",
+          "parent_id": 229,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": []
+        },
+        {
+          "id": 234,
+          "label": "22.3 课题学习 体质健康测试中的数据分析",
+          "parent_id": 229,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": []
+        }
+      ]
+    },
+    {
+      "id": 235,
+      "label": "第二十三章 一元二次方程",
+      "parent_id": 1,
+      "kp_level": "chapter",
+      "children": [
+        {
+          "id": 236,
+          "label": "23.1 一元二次方程",
+          "parent_id": 235,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 332,
+              "label": "一元二次方程的定义及一般形式",
+              "parent_id": 236,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 333,
+              "label": "一元二次方程的根",
+              "parent_id": 236,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 334,
+              "label": "根据实际问题列出一元二次方程",
+              "parent_id": 236,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        },
+        {
+          "id": 237,
+          "label": "23.2 解一元二次方程",
+          "parent_id": 235,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 238,
+              "label": "23.2.1 配方法",
+              "parent_id": 237,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": [
+                {
+                  "id": 239,
+                  "label": "直接开平方法",
+                  "parent_id": 238,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                },
+                {
+                  "id": 240,
+                  "label": "配方法的概念",
+                  "parent_id": 238,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                }
+              ]
+            },
+            {
+              "id": 241,
+              "label": "23.2.2 公式法",
+              "parent_id": 237,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 242,
+              "label": "23.2.3 因式分解法",
+              "parent_id": 237,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 243,
+              "label": "23.2.4 一元二次方程的根与系数的关系",
+              "parent_id": 237,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        },
+        {
+          "id": 244,
+          "label": "23.3 实际问题与一元二次方程",
+          "parent_id": 235,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 245,
+              "label": "列一元二次方程解决传播问题和数字问题",
+              "parent_id": 244,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 246,
+              "label": "列一元二次方程解决增长率问题",
+              "parent_id": 244,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 247,
+              "label": "列一元二次方程解决几何图形问题",
+              "parent_id": 244,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "id": 248,
+      "label": "第二十四章 二次函数",
+      "parent_id": 1,
+      "kp_level": "chapter",
+      "children": [
+        {
+          "id": 249,
+          "label": "24.1 二次函数的图象和性质",
+          "parent_id": 248,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 250,
+              "label": "24.1.1 二次函数",
+              "parent_id": 249,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 251,
+              "label": "24.1.2 二次函数y=ax²的图象和性质",
+              "parent_id": 249,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 252,
+              "label": "24.1.3 二次函数y=a(x-h)²+k的图象和性质",
+              "parent_id": 249,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": [
+                {
+                  "id": 253,
+                  "label": "二次函数y=ax²+k的图象和性质",
+                  "parent_id": 252,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                },
+                {
+                  "id": 254,
+                  "label": "二次函数y=a(x-h)²的图象和性质",
+                  "parent_id": 252,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                },
+                {
+                  "id": 255,
+                  "label": "二次函数y=a(x-h)²+k的图象和性质的概念",
+                  "parent_id": 252,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                }
+              ]
+            },
+            {
+              "id": 256,
+              "label": "24.1.4 二次函数y=ax²+bx+c的图象和性质",
+              "parent_id": 249,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        },
+        {
+          "id": 257,
+          "label": "24.2 二次函数与一元二次方程",
+          "parent_id": 248,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": []
+        },
+        {
+          "id": 258,
+          "label": "24.3 实际问题与二次函数",
+          "parent_id": 248,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 259,
+              "label": "二次函数与图形面积问题",
+              "parent_id": 258,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 260,
+              "label": "二次函数与商品利润问题",
+              "parent_id": 258,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 261,
+              "label": "抛物线形问题",
+              "parent_id": 258,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "id": 262,
+      "label": "第二十五章 旋转",
+      "parent_id": 1,
+      "kp_level": "chapter",
+      "children": [
+        {
+          "id": 263,
+          "label": "25.1 图形的旋转",
+          "parent_id": 262,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": []
+        },
+        {
+          "id": 264,
+          "label": "25.2 中心对称",
+          "parent_id": 262,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 265,
+              "label": "25.2.1 中心对称的概念",
+              "parent_id": 264,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 266,
+              "label": "25.2.2 中心对称图形",
+              "parent_id": 264,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 267,
+              "label": "25.2.3 关于原点对称的点的坐标",
+              "parent_id": 264,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "id": 268,
+      "label": "第二十六章 圆",
+      "parent_id": 1,
+      "kp_level": "chapter",
+      "children": [
+        {
+          "id": 269,
+          "label": "26.1 圆的有关性质",
+          "parent_id": 268,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 270,
+              "label": "26.1.1 圆",
+              "parent_id": 269,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 271,
+              "label": "26.1.2 垂直于弦的直径",
+              "parent_id": 269,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 272,
+              "label": "26.1.3 弧、弦、圆心角",
+              "parent_id": 269,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 273,
+              "label": "26.1.4 圆周角",
+              "parent_id": 269,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": [
+                {
+                  "id": 274,
+                  "label": "圆周角定理及其推论",
+                  "parent_id": 273,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                },
+                {
+                  "id": 275,
+                  "label": "圆内接四边形",
+                  "parent_id": 273,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                }
+              ]
+            }
+          ]
+        },
+        {
+          "id": 276,
+          "label": "26.2 点和圆、直线和圆的位置关系",
+          "parent_id": 268,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 277,
+              "label": "26.2.1 点和圆的位置关系",
+              "parent_id": 276,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 278,
+              "label": "26.2.2 直线和圆的位置关系",
+              "parent_id": 276,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": [
+                {
+                  "id": 279,
+                  "label": "直线和圆的位置关系的概念",
+                  "parent_id": 278,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                },
+                {
+                  "id": 280,
+                  "label": "切线的判定与性质",
+                  "parent_id": 278,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                },
+                {
+                  "id": 281,
+                  "label": "切线长定理和三角形的内切圆",
+                  "parent_id": 278,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                }
+              ]
+            }
+          ]
+        },
+        {
+          "id": 282,
+          "label": "26.3 正多边形和圆",
+          "parent_id": 268,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": []
+        },
+        {
+          "id": 283,
+          "label": "26.4 弧长和扇形面积",
+          "parent_id": 268,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 284,
+              "label": "弧长",
+              "parent_id": 283,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 285,
+              "label": "扇形面积",
+              "parent_id": 283,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "id": 286,
+      "label": "第二十七章 概率初步",
+      "parent_id": 1,
+      "kp_level": "chapter",
+      "children": [
+        {
+          "id": 287,
+          "label": "27.1 随机事件与概率",
+          "parent_id": 286,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 288,
+              "label": "27.1.1 随机事件",
+              "parent_id": 287,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 289,
+              "label": "27.1.2 概率",
+              "parent_id": 287,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        },
+        {
+          "id": 290,
+          "label": "27.2 用列举法求概率",
+          "parent_id": 286,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 291,
+              "label": "用列表法求概率",
+              "parent_id": 290,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 292,
+              "label": "用画树状图法求概率",
+              "parent_id": 290,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        },
+        {
+          "id": 293,
+          "label": "27.3 用频率估计概率",
+          "parent_id": 286,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": []
+        }
+      ]
+    },
+    {
+      "id": 294,
+      "label": "第二十八章 反比例函数",
+      "parent_id": 1,
+      "kp_level": "chapter",
+      "children": [
+        {
+          "id": 295,
+          "label": "28.1 反比例函数",
+          "parent_id": 294,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 296,
+              "label": "28.1.1 反比例函数的概念",
+              "parent_id": 295,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 297,
+              "label": "28.1.2 反比例函数的图象和性质",
+              "parent_id": 295,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": [
+                {
+                  "id": 298,
+                  "label": "反比例函数的图象和性质的概念",
+                  "parent_id": 297,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                },
+                {
+                  "id": 299,
+                  "label": "反比例函数的图象和性质的综合应用",
+                  "parent_id": 297,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                }
+              ]
+            }
+          ]
+        },
+        {
+          "id": 300,
+          "label": "28.2 实际问题与反比例函数",
+          "parent_id": 294,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 301,
+              "label": "实际问题中的反比例函数",
+              "parent_id": 300,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 302,
+              "label": "物理学中的反比例函数",
+              "parent_id": 300,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "id": 303,
+      "label": "第二十九章 相似",
+      "parent_id": 1,
+      "kp_level": "chapter",
+      "children": [
+        {
+          "id": 304,
+          "label": "29.1 图形的相似",
+          "parent_id": 303,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": []
+        },
+        {
+          "id": 305,
+          "label": "29.2 相似三角形",
+          "parent_id": 303,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 306,
+              "label": "29.2.1 相似三角形的判定",
+              "parent_id": 305,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": [
+                {
+                  "id": 307,
+                  "label": "相似三角形及平行线分线段成比例",
+                  "parent_id": 306,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                },
+                {
+                  "id": 308,
+                  "label": "相似三角形的判定",
+                  "parent_id": 306,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                }
+              ]
+            },
+            {
+              "id": 309,
+              "label": "29.2.2 相似三角形的性质",
+              "parent_id": 305,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 310,
+              "label": "29.2.3 相似三角形应用举例",
+              "parent_id": 305,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        },
+        {
+          "id": 311,
+          "label": "29.3 位似",
+          "parent_id": 303,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 312,
+              "label": "位似图形的概念与画法",
+              "parent_id": 311,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 313,
+              "label": "坐标平面内的位似",
+              "parent_id": 311,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "id": 314,
+      "label": "第三十章 锐角三角函数",
+      "parent_id": 1,
+      "kp_level": "chapter",
+      "children": [
+        {
+          "id": 315,
+          "label": "30.1 锐角三角函数",
+          "parent_id": 314,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 316,
+              "label": "正弦",
+              "parent_id": 315,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 317,
+              "label": "余弦和正切",
+              "parent_id": 315,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 318,
+              "label": "特殊角的三角函数值",
+              "parent_id": 315,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        },
+        {
+          "id": 319,
+          "label": "30.2 解直角三角形及其应用",
+          "parent_id": 314,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 320,
+              "label": "30.2.1 解直角三角形",
+              "parent_id": 319,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 321,
+              "label": "30.2.2 应用举例",
+              "parent_id": 319,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": [
+                {
+                  "id": 322,
+                  "label": "实际生活问题,仰角、俯角问题",
+                  "parent_id": 321,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                },
+                {
+                  "id": 323,
+                  "label": "方向角问题,坡度、坡角问题",
+                  "parent_id": 321,
+                  "kp_level": "lesson",
+                  "skills": [],
+                  "direct_score": [],
+                  "related_score": [],
+                  "children": []
+                }
+              ]
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "id": 324,
+      "label": "第三十一章 投影与视图",
+      "parent_id": 1,
+      "kp_level": "chapter",
+      "children": [
+        {
+          "id": 325,
+          "label": "31.1 投影",
+          "parent_id": 324,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 326,
+              "label": "平行投影与中心投影",
+              "parent_id": 325,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 327,
+              "label": "正投影",
+              "parent_id": 325,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        },
+        {
+          "id": 328,
+          "label": "31.2 三视图",
+          "parent_id": 324,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 329,
+              "label": "几何体的三视图",
+              "parent_id": 328,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 330,
+              "label": "根据三视图确定几何体",
+              "parent_id": 328,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            },
+            {
+              "id": 331,
+              "label": "与三视图有关的计算",
+              "parent_id": 328,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "id": 336,
+      "label": "平行四边形",
+      "parent_id": 1,
+      "kp_level": "chapter",
+      "skills": [],
+      "direct_score": [],
+      "related_score": [],
+      "children": [
+        {
+          "id": 337,
+          "label": "平行四边形的性质与判定",
+          "parent_id": 336,
+          "kp_level": "section",
+          "skills": [],
+          "direct_score": [],
+          "related_score": [],
+          "children": [
+            {
+              "id": 338,
+              "label": "平行四边形",
+              "parent_id": 337,
+              "kp_level": "subsection",
+              "skills": [],
+              "direct_score": [],
+              "related_score": [],
+              "children": []
+            }
+          ]
+        }
+      ]
+    }
+  ]
+}