| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207 |
- """docx 合并 + 导出排版模块"""
- import re
- import shutil
- import tempfile
- from datetime import datetime
- from pathlib import Path
- from docx import Document
- from docx.enum.text import WD_ALIGN_PARAGRAPH
- from docx.oxml import OxmlElement
- from docx.oxml.ns import qn
- from docx.shared import Pt, Cm
- TYPE_ORDER = ["单选题", "多选题", "填空题", "解答题"]
- TYPE_HEADERS = {
- "单选题": "一、单选题",
- "多选题": "二、多选题",
- "填空题": "三、填空题",
- "解答题": "四、解答题",
- }
- def build_export_docx(
- problem_ids: list[str],
- problems_dir: str | Path,
- title: str = "试卷",
- include_solution: bool = True,
- ) -> Path:
- """从题目 ID 列表合并生成导出 docx
- Args:
- problem_ids: 题目 ID 列表(已按题型分组、按难度排序)
- problems_dir: 单题 docx 存储目录
- title: 试卷标题
- include_solution: True = 教师版, False = 学生版
- Returns: 生成的 docx 文件路径
- """
- problems_dir = Path(problems_dir)
- doc = Document()
- # 设置默认样式
- style = doc.styles["Normal"]
- font = style.font
- font.name = "宋体"
- font.size = Pt(10.5)
- style.element.rPr.rFonts.set(qn('w:eastAsia'), '宋体')
- # 标题
- title_para = doc.add_paragraph()
- title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
- title_run = title_para.add_run(title)
- title_run.bold = True
- title_run.font.size = Pt(14)
- title_run.font.name = "宋体"
- title_run._element.rPr.rFonts.set(qn('w:eastAsia'), '宋体')
- subtitle = "教师版" if include_solution else "学生版"
- sub_para = doc.add_paragraph()
- sub_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
- sub_run = sub_para.add_run(subtitle)
- sub_run.font.size = Pt(10.5)
- doc.add_paragraph() # 空行
- # 从数据库获取题目并按题型分组
- from app.database import get_connection
- conn = get_connection(problems_dir.parent / "bank.db")
- try:
- problems = []
- for pid in problem_ids:
- row = conn.execute("SELECT * FROM problems WHERE id = ?", (pid,)).fetchone()
- if row:
- problems.append(dict(row))
- # 按题型分组
- groups: dict[str, list] = {}
- for p in problems:
- qt = p.get("question_type", "")
- if qt not in groups:
- groups[qt] = []
- groups[qt].append(p)
- # 按题型排序
- sorted_groups = [(qt, groups[qt]) for qt in TYPE_ORDER if qt in groups]
- # 加上未知题型
- for qt, items in groups.items():
- if qt not in TYPE_ORDER:
- sorted_groups.append((qt, items))
- question_num = 1
- for qt, items in sorted_groups:
- # 题型标题
- header_text = TYPE_HEADERS.get(qt, qt)
- header_para = doc.add_paragraph()
- header_run = header_para.add_run(header_text)
- header_run.bold = True
- header_run.font.size = Pt(12)
- header_run.font.name = "宋体"
- header_run._element.rPr.rFonts.set(qn('w:eastAsia'), '宋体')
- for item in items:
- pid = item["id"]
- docx_path = problems_dir / f"{pid}.docx"
- if not docx_path.exists():
- continue
- # 复制该题的内容
- _copy_question(doc, docx_path, question_num, include_solution)
- question_num += 1
- finally:
- conn.close()
- # 添加 Word 自动编号
- _apply_auto_numbering(doc)
- # 保存
- papers_dir = problems_dir.parent / "papers"
- papers_dir.mkdir(parents=True, exist_ok=True)
- ts = datetime.now().strftime("%Y%m%d_%H%M%S")
- suffix = "教师版" if include_solution else "学生版"
- safe_title = title.replace("/", "_").replace(" ", "_")
- output_path = papers_dir / f"{safe_title}_{ts}_{suffix}.docx"
- doc.save(str(output_path))
- return output_path
- def _copy_question(doc: Document, source_path: Path, num: int, include_solution: bool):
- """复制题目的段落到目标 docx"""
- source = Document(str(source_path))
- in_answer = False
- for para in source.paragraphs:
- text = para.text.strip()
- if not text:
- # 复制空段落(保留格式间距)
- p = doc.add_paragraph()
- continue
- # 检测答案区
- if text.startswith("【答案】"):
- in_answer = True
- if not include_solution:
- break
- if in_answer and not include_solution:
- break
- # 复制段落
- p = doc.add_paragraph()
- for run in para.runs:
- r = p.add_run(run.text)
- r.bold = run.bold
- r.italic = run.italic
- r.font.name = "宋体"
- r._element.rPr.rFonts.set(qn('w:eastAsia'), '宋体')
- r.font.size = Pt(10.5)
- # 保留段落对齐
- if para.alignment:
- p.alignment = para.alignment
- def _apply_auto_numbering(doc: Document):
- """对题号段落应用 Word 自动编号"""
- # 识别题型标题后的题号段落
- numbering_id = 1
- in_section = False
- for para in doc.paragraphs:
- text = para.text.strip()
- # 检测题型标题
- is_header = False
- for qt, header in TYPE_HEADERS.items():
- if header in text:
- is_header = True
- in_section = True
- numbering_id += 1
- break
- if is_header:
- continue
- # 对题号段落加编号
- if in_section and re.match(r'\d+[..]', text):
- _add_numbering(para, numbering_id)
- def _add_numbering(para, num_id: int):
- """给段落添加 Word 自动编号"""
- pPr = para._element.get_or_add_pPr()
- numPr = OxmlElement('w:numPr')
- ilvl = OxmlElement('w:ilvl')
- ilvl.set(qn('w:val'), '0')
- numPr.append(ilvl)
- numId = OxmlElement('w:numId')
- numId.set(qn('w:val'), str(num_id))
- numPr.append(numId)
- pPr.insert(0, numPr)
|