From: root Date: Sat, 12 Sep 2026 03:36:27 +0000 (+0800) Subject: 重构为 Web 版:FastAPI + Vue3 前后端分离,部署于 https://acesimba.cloud/StudyBase。后端逐端口移植原桌面版全部功能逻辑... X-Git-Url: http://acesimba.cloud/gitweb/?a=commitdiff_plain;h=cf25c65e68130f39c97ca3cc3dc7adc0cc5ee7ab;p=studybase.git 重构为 Web 版:FastAPI + Vue3 前后端分离,部署于 https://acesimba.cloud/StudyBase。后端逐端口移植原桌面版全部功能逻辑(含智学网/组卷网/自组WORD三种 docx 解析器与试卷 Word 生成),前端素净风格 UI 与原版交互一致;移除旧 Flask 版 app.py/templates/static --- diff --git a/.gitignore b/.gitignore index fe66e6c..3501a03 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,7 @@ ENV/ Media/ *.db .DS_Store +node_modules/ +nohup.out +temp_preview.docx +temp_review_preview.docx diff --git a/app.log b/app.log deleted file mode 100644 index 7daa52b..0000000 --- a/app.log +++ /dev/null @@ -1,41 +0,0 @@ -nohup: ignoring input - * Serving Flask app 'app' - * Debug mode: off -WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. - * Running on all addresses (0.0.0.0) - * Running on http://127.0.0.1:5000 - * Running on http://10.0.0.16:5000 -Press CTRL+C to quit -219.144.254.168 - - [18/May/2026 07:11:39] "GET / HTTP/1.1" 200 - -219.144.254.168 - - [18/May/2026 07:11:39] "GET /static/css/style.css HTTP/1.1" 304 - -219.144.254.168 - - [18/May/2026 07:11:39] "GET /favicon.ico HTTP/1.1" 404 - -219.144.254.168 - - [18/May/2026 07:11:47] "GET /subject/ HTTP/1.1" 200 - -219.144.254.168 - - [18/May/2026 07:11:47] "GET /static/css/style.css HTTP/1.1" 304 - -219.144.254.168 - - [18/May/2026 07:11:50] "GET / HTTP/1.1" 200 - -219.144.254.168 - - [18/May/2026 07:11:50] "GET /static/css/style.css HTTP/1.1" 304 - -219.144.254.168 - - [18/May/2026 07:11:51] "GET /questions/ HTTP/1.1" 200 - -219.144.254.168 - - [18/May/2026 07:11:51] "GET /static/css/style.css HTTP/1.1" 304 - -219.144.254.168 - - [18/May/2026 07:11:52] "GET / HTTP/1.1" 200 - -219.144.254.168 - - [18/May/2026 07:11:52] "GET /static/css/style.css HTTP/1.1" 304 - -219.144.254.168 - - [18/May/2026 07:16:45] "GET / HTTP/1.1" 200 - -219.144.254.168 - - [18/May/2026 07:16:45] "GET /static/css/style.css HTTP/1.1" 304 - -219.144.254.168 - - [18/May/2026 07:17:23] "GET /settings/ HTTP/1.1" 200 - -219.144.254.168 - - [18/May/2026 07:17:23] "GET /static/css/style.css HTTP/1.1" 304 - -219.144.254.168 - - [18/May/2026 07:17:25] "GET / HTTP/1.1" 200 - -219.144.254.168 - - [18/May/2026 07:17:25] "GET /static/css/style.css HTTP/1.1" 304 - -219.144.254.168 - - [18/May/2026 07:17:26] "GET /papers/ HTTP/1.1" 200 - -219.144.254.168 - - [18/May/2026 07:17:26] "GET /static/css/style.css HTTP/1.1" 304 - -219.144.254.168 - - [18/May/2026 07:18:24] "GET / HTTP/1.1" 200 - -219.144.254.168 - - [18/May/2026 07:18:24] "GET /static/css/style.css HTTP/1.1" 304 - -219.144.254.168 - - [18/May/2026 07:18:25] "GET /papers/ HTTP/1.1" 200 - -219.144.254.168 - - [18/May/2026 07:18:25] "GET /static/css/style.css HTTP/1.1" 304 - -220.196.160.101 - - [18/May/2026 07:18:29] "GET / HTTP/1.1" 200 - -220.196.160.101 - - [18/May/2026 07:18:29] "GET /static/css/style.css HTTP/1.1" 200 - -219.144.254.168 - - [18/May/2026 07:19:45] "GET /papers/ HTTP/1.1" 200 - -219.144.254.168 - - [18/May/2026 07:19:45] "GET /static/css/style.css HTTP/1.1" 304 - -219.144.254.168 - - [18/May/2026 07:19:46] "GET /papers/ HTTP/1.1" 200 - -219.144.254.168 - - [18/May/2026 07:19:46] "GET /static/css/style.css HTTP/1.1" 304 - -219.144.254.168 - - [18/May/2026 07:19:47] "GET /papers/ HTTP/1.1" 200 - -219.144.254.168 - - [18/May/2026 07:19:47] "GET /static/css/style.css HTTP/1.1" 304 - -219.144.254.168 - - [18/May/2026 07:21:04] "GET / HTTP/1.1" 200 - -219.144.254.168 - - [18/May/2026 07:21:04] "GET /static/css/style.css HTTP/1.1" 304 - diff --git a/app.py b/app.py deleted file mode 100644 index 959a0ff..0000000 --- a/app.py +++ /dev/null @@ -1,134 +0,0 @@ -from flask import Flask, render_template, request, jsonify -from database import query_db - -app = Flask(__name__) - -# ==================== 1. 核心页面路由 ==================== - -@app.route('/') -def index(): - return render_template('index.html') - -# --- 题目列表模块 --- -@app.route('/questions/') -def questions_list(): - return render_template('questions/index.html') - -# --- 试卷列表模块 --- -@app.route('/papers/') -def papers_list(): - return render_template('papers/index.html') - -# --- 系统设定模块 --- -@app.route('/settings/') -def settings_page(): - return render_template('settings/index.html') - -# --- 错题分析模块 (刚才修复的部分) --- -@app.route('/subject/') -def subject_portal(): - subjects = query_db("SELECT ID, Subject as name FROM Subject WHERE enabled = 1") - stages = query_db("SELECT ID, stage_name FROM stage WHERE enabled = 1 ORDER BY ID") - default_id = subjects[0]['ID'] if subjects else None - return render_template('subject/index.html', subjects=subjects, stages=stages, default_id=default_id) - -@app.route('/subject/chapters') -def subject_chapters(): - subjects = query_db("SELECT DISTINCT Subject FROM Subject ORDER BY Subject") - return render_template('subject/chapters.html', subjects=subjects) - -@app.route('/subject/knowledge') -def subject_knowledge(): - subjects = query_db("SELECT DISTINCT Subject FROM Subject ORDER BY Subject") - chapter_id = request.args.get('chapter_id', '') - return render_template('subject/knowledge.html', subjects=subjects, chapter_id=chapter_id) - - -# ==================== 2. API 数据接口 ==================== - -# 科目概览统计 -@app.route('/api/subject_stats') -def get_subject_stats(): - sub_id = request.args.get('subject_id') - stage_ids_raw = request.args.get('stage_ids', '') - stage_ids = [sid for sid in stage_ids_raw.split(',') if sid] - - if not sub_id or not stage_ids: - return jsonify({"wrong": {"total": 0, "fixed": 0, "ratio": "0%", "remained": 0}, - "push": {"total": 0, "fixed": 0, "ratio": "0%", "remained": 0}}) - - placeholders = ','.join(['?'] * len(stage_ids)) - def get_count(q_type, extra_sql=""): - sql = f"SELECT COUNT(*) as c FROM questions WHERE subject_id = ? AND stage_id IN ({placeholders}) AND question_type = ? {extra_sql}" - res = query_db(sql, [sub_id] + stage_ids + [q_type], one=True) - return res['c'] if res else 0 - - try: - w_total = get_count('错题'); w_fixed = get_count('错题', 'AND right_times > 0') - p_total = get_count('推题'); p_fixed = get_count('推题', 'AND right_times > 0') - return jsonify({ - "wrong": {"total": w_total, "fixed": w_fixed, "ratio": f"{(w_fixed/w_total*100):.1f}%" if w_total>0 else "0%", "remained": w_total-w_fixed}, - "push": {"total": p_total, "fixed": p_fixed, "ratio": f"{(p_fixed/p_total*100):.1f}%" if p_total>0 else "0%", "remained": p_total-p_fixed} - }) - except Exception as e: - return jsonify({"error": str(e)}), 500 - -# 章节数据接口 (已适配 chapter_desc) -@app.route('/api/chapters') -def get_chapters(): - subject_name = request.args.get('subject', '全部') - keyword = request.args.get('keyword', '') - params = [] - where = " WHERE 1=1" - if subject_name != '全部': - where += " AND s.Subject = ?"; params.append(subject_name) - if keyword: - where += " AND c.chapter_desc LIKE ?"; params.append(f"%{keyword}%") - - sql = f""" - SELECT c.id AS chapter_id, s.Subject AS SubjectName, c.chapter_desc AS ChapterDesc, - (SELECT COUNT(DISTINCT r.question_id) FROM question_kp_relation r JOIN knowledge_point kp ON r.kp_id = kp.id WHERE kp.chapter_id = c.id) AS total, - (SELECT COUNT(DISTINCT r.question_id) FROM question_kp_relation r JOIN knowledge_point kp ON r.kp_id = kp.id JOIN questions q ON r.question_id = q.id WHERE kp.chapter_id = c.id AND q.right_times > 0) AS correct - FROM chapters c JOIN Subject s ON c.subject_id = s.ID {where} - """ - try: - rows = query_db(sql, params) - result = [dict(row) for row in rows] - for item in result: - item['not_correct'] = (item['total'] or 0) - (item['correct'] or 0) - return jsonify([i for i in result if i['total'] > 0]) - except Exception as e: - return jsonify({"error": str(e)}), 500 - -# 知识点数据接口 -@app.route('/api/knowledge') -def get_knowledge(): - subject_name = request.args.get('subject', '全部') - keyword = request.args.get('keyword', '') - chapter_id = request.args.get('chapter_id', '') - params = [] - where = " WHERE kp.is_enabled = 1" - if chapter_id: - where += " AND kp.chapter_id = ?"; params.append(chapter_id) - if subject_name != '全部': - where += " AND s.Subject = ?"; params.append(subject_name) - if keyword: - where += " AND kp.content LIKE ?"; params.append(f"%{keyword}%") - - sql = f""" - SELECT kp.id, s.Subject as subject_name, kp.content, - (SELECT COUNT(*) FROM question_kp_relation r WHERE r.kp_id = kp.id) as total, - (SELECT COUNT(DISTINCT r.question_id) FROM question_kp_relation r JOIN questions q ON r.question_id = q.id WHERE r.kp_id = kp.id AND q.right_times >= 1) as correct - FROM knowledge_point kp JOIN Subject s ON kp.subject_id = s.ID {where} - """ - try: - rows = query_db(sql, params) - result = [dict(row) for row in rows] - for item in result: - item['not_correct'] = (item['total'] or 0) - (item['correct'] or 0) - return jsonify(result) - except Exception as e: - return jsonify({"error": str(e)}), 500 - -if __name__ == '__main__': - app.run(host='0.0.0.0', port=5000, debug=True) diff --git a/backend/create_word.py b/backend/create_word.py new file mode 100644 index 0000000..41ce016 --- /dev/null +++ b/backend/create_word.py @@ -0,0 +1,590 @@ +# create_word.py +import os +import datetime +import sqlite3 +import re +import traceback + +from docx import Document +from docx.shared import Inches, Pt, RGBColor, Emu, Cm +from docx.enum.text import WD_PARAGRAPH_ALIGNMENT, WD_LINE_SPACING +from docx.oxml.ns import qn +from docx.oxml import OxmlElement + +# 页面内容区域最大宽度(保守值) +MAX_DOCX_CONTENT_WIDTH_EMU = Inches(7.36) +BLUE = RGBColor(0, 102, 204) + + +def _get_val(row, key_name, default=""): + keys = row.keys() + for k in keys: + if k.lower() == key_name.lower(): + val = row[k] + return str(val) if val is not None and str(val).strip() != "" else default + return default + + +def _get_int(row, key_name, default=0): + try: + keys = row.keys() + for k in keys: + if k.lower() == key_name.lower(): + val = row[k] + if val is None or str(val).strip() == "": + return default + return int(val) + except Exception: + return default + return default + + +def _set_font(run, name, size, color=None): + run.font.name = name + run._element.rPr.rFonts.set(qn('w:eastAsia'), name) + run.font.size = Pt(size) + if color: + run.font.color.rgb = color + + +def _insert_field(run, field_type): + fld = OxmlElement('w:fldChar') + fld.set(qn('w:fldCharType'), 'begin') + run._element.append(fld) + + instr = OxmlElement('w:instrText') + instr.set(qn('xml:space'), 'preserve') + instr.text = field_type + run._element.append(instr) + + fld = OxmlElement('w:fldChar') + fld.set(qn('w:fldCharType'), 'end') + run._element.append(fld) + + +def _insert_mixed_content(paragraph, full_text, base_dir, font_name='SimSun', font_size=12, debug_print=False): + if not full_text: + return + full_text = str(full_text).replace('\xa0', ' ') + img_pattern = r'\s*(!\[\]\s*\(\s*.*?\s*\))\s*' + parts = re.split(img_pattern, full_text) + for part in parts: + if not part: + continue + match = re.match(r'!\[\]\s*\(\s*(.*?)\s*\)', part) + if match: + content_inner = match.group(1).strip() + rel_path = content_inner + width_emu = None + height_emu = None + if '?' in content_inner: + try: + rel_path, params = content_inner.split('?', 1) + w_match = re.search(r'w=(\d+)', params) + h_match = re.search(r'h=(\d+)', params) + if w_match: + width_emu = int(w_match.group(1)) + if h_match: + height_emu = int(h_match.group(1)) + except Exception: + pass + + rel_path = rel_path.strip().replace('\\', '/') + full_img_path = os.path.normpath(os.path.join(base_dir, rel_path)) + if debug_print: + print("DEBUG image candidate:", full_img_path) + + if not os.path.exists(full_img_path) and "Media" in rel_path: + no_space_path = rel_path.replace(" ", "") + full_img_path_b = os.path.normpath(os.path.join(base_dir, no_space_path)) + if os.path.exists(full_img_path_b): + full_img_path = full_img_path_b + + if os.path.exists(full_img_path): + try: + run = paragraph.add_run() + target_width = None + target_height = None + if width_emu and height_emu: + target_width = Emu(width_emu) + target_height = Emu(height_emu) + elif width_emu: + target_width = Emu(width_emu) + elif height_emu: + target_height = Emu(height_emu) + + if target_width: + if target_width > MAX_DOCX_CONTENT_WIDTH_EMU: + run.add_picture(full_img_path, width=MAX_DOCX_CONTENT_WIDTH_EMU) + else: + if target_height: + run.add_picture(full_img_path, width=target_width, height=target_height) + else: + run.add_picture(full_img_path, width=target_width) + elif target_height: + run.add_picture(full_img_path, height=target_height) + else: + run.add_picture(full_img_path, width=MAX_DOCX_CONTENT_WIDTH_EMU) + except Exception as e: + print(f"Insert image error: {e}, Path: {full_img_path}") + _set_font(paragraph.add_run("[图片错误]"), font_name, font_size, RGBColor(255, 0, 0)) + else: + _set_font(paragraph.add_run(part), font_name, font_size) + else: + _set_font(paragraph.add_run(part), font_name, font_size) + + +def _set_table_borders(table, color="000000", size=4): + def _make_border_element(name, color, size): + el = OxmlElement(f"w:{name}") + el.set(qn("w:val"), "single") + el.set(qn("w:sz"), str(size)) + el.set(qn("w:color"), color) + el.set(qn("w:space"), "0") + return el + + tbl = table._element + tblPr = getattr(tbl, 'tblPr', None) + if tblPr is None: + tblPr = OxmlElement("w:tblPr") + tbl.insert(0, tblPr) + + tblBorders = getattr(tblPr, 'tblBorders', None) + if tblBorders is None: + tblBorders = OxmlElement("w:tblBorders") + tblPr.append(tblBorders) + else: + for child in list(tblBorders): + tblBorders.remove(child) + + for border_name in ("top", "left", "bottom", "right", "insideH", "insideV"): + tblBorders.append(_make_border_element(border_name, color, size)) + + for row in table.rows: + for cell in row.cells: + tc = cell._element + tcPr = getattr(tc, 'tcPr', None) + if tcPr is None: + tcPr = OxmlElement("w:tcPr") + tc.insert(0, tcPr) + + tcBorders = getattr(tcPr, 'tcBorders', None) + if tcBorders is None: + tcBorders = OxmlElement("w:tcBorders") + tcPr.append(tcBorders) + else: + for child in list(tcBorders): + tcBorders.remove(child) + + for bname in ("top", "left", "bottom", "right"): + tcBorders.append(_make_border_element(bname, color, size)) + + +def _starts_with_image_markdown(text): + img_pattern = r'^\s*!\[\]\s*\(\s*.*?\s*\)' + return re.match(img_pattern, text.strip()) is not None + + +def _add_header_footer(section, left_header_text): + # 页眉 + header = section.header + if header.paragraphs: + header.paragraphs[0].text = "" + htable = header.add_table(1, 2, width=Inches(6.7)) + htable.autofit = False + + cell_left = htable.cell(0, 0) + try: + cell_left.paragraphs[0].clear() + except Exception: + cell_left.paragraphs[0].text = "" + _set_font(cell_left.paragraphs[0].add_run(left_header_text), 'Microsoft YaHei', 10.5, BLUE) + + cell_right = htable.cell(0, 1) + cell_right.paragraphs[0].alignment = WD_PARAGRAPH_ALIGNMENT.RIGHT + _set_font(cell_right.paragraphs[0].add_run("试卷生成日期:" + datetime.datetime.now().strftime("%Y-%m-%d")), '黑体', 9) + + # 页脚 + footer_p = section.footer.paragraphs[0] + footer_p.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER + parts = [("第 ", None), (" ", "PAGE"), (" 页 共 ", None), (" ", "NUMPAGES"), (" 页", None)] + for text, field in parts: + run = footer_p.add_run(text) + _set_font(run, 'SimSun', 9) + if field: + _insert_field(run, field) + + +def _load_questions(selected_ids, get_db_connection): + conn = get_db_connection() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + placeholders = ','.join(['?'] * len(selected_ids)) + query = f""" + SELECT + q.*, + s.Subject, + qs.source_description, + q.latest_status, + q.half_right_description + FROM questions q + LEFT JOIN Subject s ON q.subject_id = s.ID + LEFT JOIN question_source qs ON q.source_id = qs.id + WHERE q.id IN ({placeholders}) + ORDER BY q.subject_id ASC, q.id ASC + """ + cursor.execute(query, tuple(sorted(selected_ids))) + rows = cursor.fetchall() + conn.close() + return rows + + +def _resolve_real_paper_id(selected_ids, questions_data, paper_id_for_header): + real_paper_id = None + if paper_id_for_header is not None: + real_paper_id = str(paper_id_for_header) + else: + if questions_data: + first_row = questions_data[0] + candidate_keys = ['paper_id', 'paperid', 'paper', 'exam_id', 'examid', 'test_id', 'testid', 'paperID', 'PaperID', 'paperId'] + for k in first_row.keys(): + if k.lower() in [c.lower() for c in candidate_keys]: + val = first_row[k] + if val is not None and str(val).strip() != "": + real_paper_id = str(val) + break + if real_paper_id is None: + for k in first_row.keys(): + if 'paper' in k.lower() or 'exam' in k.lower() or 'test' in k.lower(): + val = first_row[k] + if val is not None and str(val).strip() != "": + real_paper_id = str(val) + break + + if real_paper_id is None and hasattr(selected_ids, '__iter__'): + try: + real_paper_id = str(next(iter(selected_ids))) + except StopIteration: + real_paper_id = "未知" + return real_paper_id + + +def _render_question_stem(doc, q, idx, total_questions, base_dir, default_font_size_pt=12, debug_print=False): + # 第一行:题号 + 基础信息 + 统计 + info_p = doc.add_paragraph() + + num_run = info_p.add_run(f"{idx+1}. ") + _set_font(num_run, 'SimSun', 9, RGBColor(0, 0, 0)) + num_run.bold = True + + base_text = ( + f"ID:{_get_val(q, 'id')} | " + f"{_get_val(q,'question_type','题目')} | " + f"{_get_val(q,'Subject')} | " + f"{_get_val(q,'source_description')}" + ) + _set_font(info_p.add_run(base_text), 'SimSun', 9, RGBColor(0, 0, 0)) + + paper_count = _get_int(q, 'paper_count', 0) + right_times = _get_int(q, 'right_times', 0) + wrong_times = _get_int(q, 'wrong_times', 0) + half_right_times = _get_int(q, 'half_right_times', 0) + + _set_font(info_p.add_run(" | 组卷次数:"), 'SimSun', 9, RGBColor(0, 0, 0)) + cnt_run = info_p.add_run(str(paper_count)) + _set_font(cnt_run, 'SimSun', 9, RGBColor(255, 0, 0) if paper_count > 2 else RGBColor(0, 0, 0)) + _set_font( + info_p.add_run(f" | 正确次数:{right_times} | 错误次数:{wrong_times} | 部分正确次数:{half_right_times}"), + 'SimSun', 9, RGBColor(0, 0, 0) + ) + + # 上次部分错误说明 + latest_status = _get_val(q, 'latest_status') + half_right_description = _get_val(q, 'half_right_description') + if latest_status == 'half_right' and half_right_description: + half_right_p = doc.add_paragraph() + run_half_right_prefix = half_right_p.add_run("上次部分错误:") + _set_font(run_half_right_prefix, 'SimSun', 9, RGBColor(0, 0, 0)) + run_half_right_prefix.bold = True + run_half_right_desc = half_right_p.add_run(half_right_description) + _set_font(run_half_right_desc, 'SimSun', 9, RGBColor(0, 0, 0)) + + # 题干渲染 + content_text = _get_val(q, 'content', '') + content_lines = content_text.split('\n') + + first_line = True + i = 0 + while i < len(content_lines): + line_content = content_lines[i] + if not line_content.strip() and len(content_lines) > 1: + i += 1 + continue + + stripped_line = line_content.strip() + + # 表格 + if " | " in stripped_line: + table_rows = [] + while i < len(content_lines): + row_candidate = content_lines[i].strip() + if not row_candidate or " | " not in row_candidate: + break + table_rows.append(row_candidate) + i += 1 + + if table_rows: + num_cols = 0 + for r in table_rows: + cols = r.split(" | ") + if len(cols) > num_cols: + num_cols = len(cols) + if num_cols < 1: + num_cols = 1 + + tbl = doc.add_table(rows=len(table_rows), cols=num_cols) + tbl.autofit = True + _set_table_borders(tbl, color="000000", size=4) + + for r_idx, row_text in enumerate(table_rows): + cells_text = row_text.split(" | ") + for c_idx in range(num_cols): + cell_para = tbl.cell(r_idx, c_idx).paragraphs[0] + cell_para.paragraph_format.line_spacing_rule = WD_LINE_SPACING.AT_LEAST + cell_para.paragraph_format.line_spacing = Pt(default_font_size_pt * 1.5) + this_cell_text = cells_text[c_idx] if c_idx < len(cells_text) else "" + _insert_mixed_content( + cell_para, + this_cell_text.strip(), + base_dir, + font_size=default_font_size_pt, + debug_print=debug_print + ) + continue + + # 普通行 + p = doc.add_paragraph() + p.paragraph_format.line_spacing_rule = WD_LINE_SPACING.AT_LEAST + p.paragraph_format.line_spacing = Pt(default_font_size_pt * 4) + + prefix = "" if first_line else " " + _insert_mixed_content( + p, + f"{prefix}{stripped_line}", + base_dir, + font_size=default_font_size_pt, + debug_print=debug_print + ) + first_line = False + i += 1 + + # 分页/空行规则(原逻辑) + is_sub_str = _get_val(q, 'is_subjective', '0') + if debug_print: + print(f"DEBUG: 题目ID={_get_val(q, 'id')}, is_subjective={is_sub_str}, Current Index={idx}, Total Questions={total_questions}") + + return str(is_sub_str).lower() in ['1', 'true', 'yes', 'on'] + + +def _render_answer_block(doc, q, idx, base_dir, default_font_size_pt=12, debug_print=False): + analysis_text = _get_val(q, 'analysis', '') or _get_val(q, 'answer', '略') + lines = analysis_text.split('\n') + + # 标题行 + title_p = doc.add_paragraph() + title_p.paragraph_format.line_spacing_rule = WD_LINE_SPACING.AT_LEAST + title_p.paragraph_format.line_spacing = Pt(default_font_size_pt * 4) + + run_prefix = title_p.add_run(f"ID:{_get_val(q, 'id')} | {idx+1}. 【参考答案】") + _set_font(run_prefix, 'SimSun', 9, RGBColor(0, 0, 0)) + run_prefix.bold = True + + # 内容行 + for line_content in lines: + if not line_content.strip(): + continue + ans_p = doc.add_paragraph() + ans_p.paragraph_format.line_spacing_rule = WD_LINE_SPACING.AT_LEAST + ans_p.paragraph_format.line_spacing = Pt(default_font_size_pt * 4) + _insert_mixed_content(ans_p, line_content.strip(), base_dir, font_size=default_font_size_pt, debug_print=debug_print) + + +def generate_docx(selected_ids, base_dir, get_db_connection, title=None, paper_id_for_header=None, debug_print=False): + """ + 原功能:题目页 + 末尾集中“答案与解析”页 + """ + if not selected_ids: + raise ValueError("selected_ids 为空,无法生成试卷") + + doc = Document() + section = doc.sections[0] + section.page_height = Cm(29.7) + section.page_width = Cm(21) + section.left_margin = Cm(1.3) + section.right_margin = Cm(1.0) + section.top_margin = Cm(2.0) + section.bottom_margin = Cm(1.0) + + default_font_size_pt = 12 + + try: + questions_data = _load_questions(selected_ids, get_db_connection) + total_questions = len(questions_data) + + real_paper_id = _resolve_real_paper_id(selected_ids, questions_data, paper_id_for_header) + _add_header_footer(section, f"试卷{real_paper_id}") + + # 标题 + title_text = title if title else f"试卷 {datetime.datetime.now().strftime('%Y%m%d')}" + title_p = doc.add_paragraph() + title_p.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER + title_run = title_p.add_run(title_text) + _set_font(title_run, '黑体', 16) + title_run.bold = True + + # 题目页 + for idx, q in enumerate(questions_data): + is_subjective = _render_question_stem( + doc, q, idx, total_questions, base_dir, + default_font_size_pt=default_font_size_pt, + debug_print=debug_print + ) + + if is_subjective and idx < total_questions - 1: + doc.add_page_break() + else: + if idx < total_questions - 1: + doc.add_paragraph() + + # 答案页 + if questions_data: + doc.add_page_break() + ans_title_p = doc.add_paragraph() + ans_title_p.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER + ans_run = ans_title_p.add_run("答案与解析") + _set_font(ans_run, '黑体', 16) + ans_run.bold = True + + for idx, q in enumerate(questions_data): + analysis_text = _get_val(q, 'analysis', '') or _get_val(q, 'answer', '略') + lines = analysis_text.split('\n') + + for line_idx, line_content in enumerate(lines): + if not line_content.strip(): + continue + + ans_p = doc.add_paragraph() + ans_p.paragraph_format.line_spacing_rule = WD_LINE_SPACING.AT_LEAST + ans_p.paragraph_format.line_spacing = Pt(default_font_size_pt * 4) + + prefix = "" + if line_idx == 0: + prefix_content = f"ID:{_get_val(q, 'id')} | {idx+1}. 【参考答案】" + if _starts_with_image_markdown(line_content.strip()): + _insert_mixed_content( + ans_p, + prefix_content, + base_dir, + font_size=default_font_size_pt, + debug_print=debug_print + ) + ans_p = doc.add_paragraph() + ans_p.paragraph_format.line_spacing_rule = WD_LINE_SPACING.AT_LEAST + ans_p.paragraph_format.line_spacing = Pt(default_font_size_pt * 4) + else: + prefix = prefix_content + else: + prefix = " " + + _insert_mixed_content(ans_p, f"{prefix}{line_content.strip()}", base_dir, font_size=default_font_size_pt, debug_print=debug_print) + + except Exception as e: + print(f"ERROR in generate_docx: {e}") + traceback.print_exc() + raise + + out_path = os.path.join(base_dir, "temp_preview.docx") + try: + doc.save(out_path) + except Exception as e: + print(f"Save docx error: {e}") + traceback.print_exc() + raise + +# ========================= +# 新增:复习卷生成(答案紧跟题目后) +# ========================= + +def generate_review_docx(selected_ids, base_dir, get_db_connection, title=None, paper_id_for_header=None, debug_print=False): + """ + 复习卷: + - 每道题的答案与解析紧跟该题题干之后 + - 页眉试卷ID后追加“-复习卷” + - 其他生成规则保持与 generate_docx 一致 + """ + if not selected_ids: + raise ValueError("selected_ids 为空,无法生成试卷") + + doc = Document() + section = doc.sections[0] + section.page_height = Cm(29.7) + section.page_width = Cm(21) + section.left_margin = Cm(1.3) + section.right_margin = Cm(1.0) + section.top_margin = Cm(2.0) + section.bottom_margin = Cm(1.0) + + default_font_size_pt = 12 + + try: + questions_data = _load_questions(selected_ids, get_db_connection) + total_questions = len(questions_data) + + real_paper_id = _resolve_real_paper_id(selected_ids, questions_data, paper_id_for_header) + _add_header_footer(section, f"试卷{real_paper_id}-复习卷") + + # 标题(与原规则一致) + title_text = title if title else f"试卷 {datetime.datetime.now().strftime('%Y%m%d')}" + title_p = doc.add_paragraph() + title_p.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER + title_run = title_p.add_run(title_text) + _set_font(title_run, '黑体', 16) + title_run.bold = True + + for idx, q in enumerate(questions_data): + # 题干(复用原渲染逻辑) + is_subjective = _render_question_stem( + doc, q, idx, total_questions, base_dir, + default_font_size_pt=default_font_size_pt, + debug_print=debug_print + ) + + # 关键:答案紧跟在本题后 + _render_answer_block( + doc, q, idx, base_dir, + default_font_size_pt=default_font_size_pt, + debug_print=debug_print + ) + + # 分页规则保持与原逻辑一致(主观题分页) + if is_subjective and idx < total_questions - 1: + doc.add_page_break() + else: + if idx < total_questions - 1: + doc.add_paragraph() + + except Exception as e: + print(f"ERROR in generate_review_docx: {e}") + traceback.print_exc() + raise + + out_path = os.path.join(base_dir, "temp_review_preview.docx") + try: + doc.save(out_path) + except Exception as e: + print(f"Save review docx error: {e}") + traceback.print_exc() + raise + + return out_path diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..475f010 --- /dev/null +++ b/backend/main.py @@ -0,0 +1,1964 @@ +# ===================== StudyBase Web backend (FastAPI) ===================== +# Every endpoint ports the logic of the corresponding Tkinter View from the +# original desktop app 1:1 (SQL, validations, side-effects included). +import os +import io +import re +import sqlite3 +import datetime +import shutil +import tempfile +import traceback + +from fastapi import FastAPI, Request, UploadFile, File, Form, HTTPException +from fastapi.responses import FileResponse, StreamingResponse, JSONResponse +from pydantic import BaseModel +from typing import Optional, List, Dict, Any + +import create_word +import media_utils +from parsers.zhixuewang_file_parsing import ZhixuewangParser +from parsers.zujuanwang_file_parsing import ZujuanwangParser +from parsers.zizuword_file_parsing import ZizuWordParser + +BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +DB_PATH = os.path.join(BASE_DIR, "s", "studybase.db") +MEDIA_BASE_DIR = os.path.join(BASE_DIR, "Media") + +app = FastAPI(title="StudyBase Web") + +# per-request image counters (question_detail uses question ids as keys, +# manuallyadd uses question numbers as keys) +_image_counters: Dict[Any, int] = {} + + +def get_db(): + conn = sqlite3.connect(DB_PATH, timeout=10) + conn.row_factory = sqlite3.Row + return conn + + +def row_to_dict(row): + return {k: row[k] for k in row.keys()} if row is not None else None + + +def now_str(): + return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + +# ============================================================ +# 科目面板 (SubjectPortalView) +# ============================================================ + +@app.get("/api/portal/options") +def portal_options(): + conn = get_db() + cur = conn.cursor() + cur.execute("SELECT Subject FROM Subject WHERE enabled = 1") + subjects = [r[0] for r in cur.fetchall()] + if not subjects: + subjects = ["数学"] # original fallback + cur.execute("SELECT ID, stage_name FROM stage WHERE enabled = 1 ORDER BY ID") + stages = [{"id": r[0], "name": r[1]} for r in cur.fetchall()] + conn.close() + return {"subjects": subjects, "stages": stages} + + +class PortalStatsReq(BaseModel): + subject: str + stage_ids: List[int] + + +@app.post("/api/portal/stats") +def portal_stats(req: PortalStatsReq): + stats = { + "wrong_total": 0, "wrong_fixed": 0, "wrong_ratio": "0%", "wrong_remained": 0, + "push_total": 0, "push_fixed": 0, "push_ratio": "0%", "push_remained": 0, + } + if not req.stage_ids: + return stats + + conn = get_db() + cur = conn.cursor() + cur.execute("SELECT ID FROM Subject WHERE Subject = ?", (req.subject,)) + res = cur.fetchone() + if not res: + conn.close() + return stats + sub_id = res[0] + + stage_filter = f"AND stage_id IN ({','.join(map(str, req.stage_ids))})" + + cur.execute(f"SELECT COUNT(*) FROM questions WHERE question_type = '错题' AND subject_id = ? {stage_filter}", (sub_id,)) + w_total = cur.fetchone()[0] + cur.execute(f"SELECT COUNT(*) FROM questions WHERE question_type = '错题' AND subject_id = ? AND right_times > 0 {stage_filter}", (sub_id,)) + w_fixed = cur.fetchone()[0] + cur.execute(f"SELECT COUNT(*) FROM questions WHERE question_type = '推题' AND subject_id = ? {stage_filter}", (sub_id,)) + p_total = cur.fetchone()[0] + cur.execute(f"SELECT COUNT(*) FROM questions WHERE question_type = '推题' AND subject_id = ? AND right_times > 0 {stage_filter}", (sub_id,)) + p_fixed = cur.fetchone()[0] + conn.close() + + stats.update({ + "wrong_total": w_total, + "wrong_fixed": w_fixed, + "wrong_ratio": f"{(w_fixed / w_total * 100):.1f}%" if w_total > 0 else "0%", + "wrong_remained": w_total - w_fixed, + "push_total": p_total, + "push_fixed": p_fixed, + "push_ratio": f"{(p_fixed / p_total * 100):.1f}%" if p_total > 0 else "0%", + "push_remained": p_total - p_fixed, + }) + return stats + + +# ============================================================ +# 题目列表 (QuestionListView) +# ============================================================ + +@app.get("/api/questions/filter-options") +def question_filter_options(): + conn = get_db() + cur = conn.cursor() + + def opts(table, col, enabled_only=False): + if enabled_only: + cur.execute(f"SELECT DISTINCT {col} FROM {table} WHERE Enabled = 1 ORDER BY {col}") + else: + cur.execute(f"SELECT DISTINCT {col} FROM {table} WHERE {col} IS NOT NULL ORDER BY {col}") + return [r[0] for r in cur.fetchall()] + + data = { + "subject": opts("Subject", "Subject"), + "cause": opts("Error_Cause", "Cause"), + "source": opts("question_source", "source_description"), + "archive_status": opts("Question_Status", "StatusName", enabled_only=True), + "term": opts("term", "term_name"), + "stage": opts("stage", "stage_name"), + } + conn.close() + return data + + +class QuestionSearchReq(BaseModel): + filters: Dict[str, Any] = {} + id_input: str = "" + kp_input: str = "" + chapter_id_filter: Optional[int] = None + kp_id_filter: Optional[int] = None + right_times_filter: Optional[str] = None + mode: str = "table" # table | preview + sort_col: str = "q.id" + sort_desc: bool = False + + +# map of table-column -> db column (question_list.sort_by) +SORT_COL_WHITELIST = { + "subject_id", "q.id", "latest_status", "question_type", "knowledge_points_desc", + "answer_time", "input_time", "paper_count", "right_times", "wrong_times", + "half_right_times", "last_right_time", +} + + +def build_question_search_sql(req: QuestionSearchReq): + """Exact port of QuestionListView.load_filtered_data SQL construction.""" + conn = get_db() + cur = conn.cursor() + + sql = ("SELECT q.*, s.Subject, qs.source_description, ec.Cause, t.term_name, st.stage_name, qst.StatusName, " + "(SELECT GROUP_CONCAT(kp.content, '、') FROM question_kp_relation qkr JOIN knowledge_point kp ON qkr.kp_id = kp.id WHERE qkr.question_id = q.id) AS knowledge_points_desc " + "FROM questions q " + "LEFT JOIN Subject s ON q.subject_id = s.ID " + "LEFT JOIN question_source qs ON q.source_id = qs.id " + "LEFT JOIN Error_Cause ec ON q.error_cause_id = ec.ID " + "LEFT JOIN term t ON q.term_id = t.ID " + "LEFT JOIN stage st ON q.stage_id = st.ID " + "LEFT JOIN Question_Status qst ON q.status_id = qst.ID " + "WHERE 1=1") + + params = [] + f = req.filters or {} + + if req.chapter_id_filter is not None: + sql += " AND EXISTS (SELECT 1 FROM question_kp_relation qkr JOIN knowledge_point kp ON qkr.kp_id = kp.id WHERE qkr.question_id = q.id AND kp.chapter_id = ?)" + params.append(req.chapter_id_filter) + if req.kp_id_filter is not None: + sql += " AND EXISTS (SELECT 1 FROM question_kp_relation qkr WHERE qkr.kp_id = ? AND qkr.question_id = q.id)" + params.append(req.kp_id_filter) + if req.right_times_filter == '>=1': + sql += " AND q.latest_status = 'right'" + elif req.right_times_filter == '=0': + sql += " AND (q.latest_status != 'right' OR q.latest_status IS NULL)" + + v_sub = f.get("subject") + if v_sub and v_sub != "全部": + sql += " AND s.Subject = ?"; params.append(v_sub) + v_nature = f.get("question_nature") + if v_nature and v_nature != "全部": + sql += " AND q.question_type = ?"; params.append(v_nature) + v_latest_status = f.get("status") + if v_latest_status and v_latest_status != "全部": + if v_latest_status == "未完成": + sql += " AND (q.latest_status IS NULL OR q.latest_status = '')" + elif v_latest_status == "正确": + sql += " AND q.latest_status = 'right'" + elif v_latest_status == "错误": + sql += " AND q.latest_status = 'wrong'" + elif v_latest_status == "部分正确": + sql += " AND q.latest_status = 'half_right'" + v_archive_status = f.get("archive_status") + if v_archive_status and v_archive_status != "全部": + if v_archive_status == "未归档": + cur.execute("SELECT ID FROM Question_Status WHERE StatusName = '未归档' AND Enabled = 1") + not_archive_id_row = cur.fetchone() + if not_archive_id_row: + sql += " AND (q.status_id = ? OR q.status_id IS NULL)" + params.append(not_archive_id_row[0]) + else: + sql += " AND q.status_id IS NULL" + else: + cur.execute("SELECT ID FROM Question_Status WHERE StatusName = ? AND Enabled = 1", (v_archive_status,)) + archive_id_row = cur.fetchone() + if archive_id_row: + sql += " AND q.status_id = ?" + params.append(archive_id_row[0]) + else: + sql += " AND 1=0" + + v_cause = f.get("cause") + if v_cause and v_cause != "全部": + sql += " AND ec.Cause = ?"; params.append(v_cause) + v_src = f.get("source") + if v_src and v_src != "全部": + sql += " AND qs.source_description = ?"; params.append(v_src) + v_term = f.get("term") + if v_term and v_term != "全部": + sql += " AND t.term_name = ?"; params.append(v_term) + v_stage = f.get("stage") + if v_stage and v_stage != "全部": + sql += " AND st.stage_name = ?"; params.append(v_stage) + + cnt_map = {"paper_count": "q.paper_count", "right_times": "q.right_times", + "wrong_times": "q.wrong_times", "half_right_times": "q.half_right_times"} + for k, col in cnt_map.items(): + if k == "right_times" and req.right_times_filter is not None: + continue + val = f.get(k) + if val and val != "全部": + if val == "4次及以上": + sql += f" AND {col} >= 4" + else: + try: + sql += f" AND {col} = ?" + params.append(int(val)) + except ValueError: + pass + + id_in = (req.id_input or "").strip() + if id_in: + ids = [] + for s_id in id_in.split(): + try: + ids.append(int(s_id)) + except ValueError: + pass + if ids: + sql += f" AND q.id IN ({','.join(['?'] * len(ids))})" + params.extend(ids) + kp_in = (req.kp_input or "").strip() + if kp_in: + for w in kp_in.split(): + sql += " AND EXISTS (SELECT 1 FROM question_kp_relation qkr_c JOIN knowledge_point kp_c ON qkr_c.kp_id = kp_c.id WHERE qkr_c.question_id = q.id AND kp_c.content LIKE ?)" + params.append(f"%{w}%") + + if req.mode == "table": + sort_col = req.sort_col if req.sort_col in SORT_COL_WHITELIST else "q.id" + sql += f" ORDER BY {sort_col} {'DESC' if req.sort_desc else 'ASC'}" + else: + sql += " ORDER BY q.id ASC" + + conn.close() + return sql, params + + +@app.post("/api/questions/search") +def question_search(req: QuestionSearchReq): + try: + sql, params = build_question_search_sql(req) + conn = get_db() + cur = conn.cursor() + cur.execute(sql, params) + rows = [row_to_dict(r) for r in cur.fetchall()] + + # 所属试卷ID (render_as_table) + for r in rows: + cur.execute("SELECT paper_id FROM paper_questions WHERE question_id = ?", (r["id"],)) + r["paper_ids"] = ",".join(str(x[0]) for x in cur.fetchall()) + conn.close() + return {"count": len(rows), "rows": rows} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +class IdsReq(BaseModel): + ids: List[int] + + +@app.post("/api/questions/delete") +def question_delete(req: IdsReq): + """QuestionListView.perform_deletion""" + qids = req.ids + if not qids: + raise HTTPException(status_code=400, detail="请勾选题目") + try: + conn = get_db() + cur = conn.cursor() + for qid in qids: + cur.execute("SELECT media_path FROM questions WHERE id = ?", (qid,)) + res = cur.fetchone() + if res and res[0]: + p = os.path.normpath(os.path.join(BASE_DIR, res[0])) + if os.path.exists(p): + shutil.rmtree(p) + ps = ','.join(['?'] * len(qids)) + cur.execute(f"DELETE FROM paper_questions WHERE question_id IN ({ps})", qids) + cur.execute(f"DELETE FROM questions WHERE id IN ({ps})", qids) + cur.execute(f"DELETE FROM question_kp_relation WHERE question_id IN ({ps})", qids) + conn.commit() + conn.close() + return {"deleted": len(qids)} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.post("/api/questions/bulk-archive") +def question_bulk_archive(req: IdsReq): + """QuestionListView.bulk_archive_selected""" + selected_ids = req.ids + if not selected_ids: + raise HTTPException(status_code=400, detail="请选择要归档的题目") + try: + conn = get_db() + cur = conn.cursor() + cur.execute("SELECT ID FROM Question_Status WHERE StatusName = '已归档' AND Enabled = 1") + archive_status_id_row = cur.fetchone() + if not archive_status_id_row: + conn.close() + raise HTTPException(status_code=400, detail="未找到“已归档”状态配置") + archive_status_id = archive_status_id_row[0] + placeholders = ','.join(['?'] * len(selected_ids)) + cur.execute(f"UPDATE questions SET status_id = ? WHERE id IN ({placeholders})", + tuple([archive_status_id] + selected_ids)) + conn.commit() + conn.close() + return {"archived": len(selected_ids)} + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +def _xlsx_bytes(dataframe, sheet_name, kp_col_name=None): + """Original used pandas + xlsxwriter (with a wrap format on the KP + column); falls back to openpyxl when xlsxwriter is unavailable.""" + buf = io.BytesIO() + try: + import xlsxwriter + with __import__("pandas").ExcelWriter(buf, engine='xlsxwriter') as writer: + dataframe.to_excel(writer, index=False, sheet_name=sheet_name) + workbook, worksheet = writer.book, writer.sheets[sheet_name] + wrap_format = workbook.add_format({'text_wrap': True, 'valign': 'top'}) + if kp_col_name and kp_col_name in dataframe.columns: + try: + kp_idx = list(dataframe.columns).index(kp_col_name) + worksheet.set_column(kp_idx, kp_idx, 40, wrap_format) + except ValueError: + pass + except ImportError: + with __import__("pandas").ExcelWriter(buf, engine='openpyxl') as writer: + dataframe.to_excel(writer, index=False, sheet_name=sheet_name) + buf.seek(0) + return buf + + +QUESTION_TABLE_COLUMNS = ["科目", "试题ID", "掌握状态", "题目类型", "题目知识点", "答题时间", + "录入时间", "组卷次数", "正确次数", "错误次数", "部分正确次数", "最晚正确时间", "所属试卷ID"] + + +def _question_table_rows(req: QuestionSearchReq): + req.mode = "table" + sql, params = build_question_search_sql(req) + conn = get_db() + cur = conn.cursor() + cur.execute(sql, params) + rows = cur.fetchall() + conn.close() + st_map = {'right': '正确', 'wrong': '错误', 'half_right': '部分正确'} + data = [] + for row in rows: + qid = row["id"] + conn = get_db() + cur = conn.cursor() + cur.execute("SELECT paper_id FROM paper_questions WHERE question_id = ?", (qid,)) + p_ids = [str(r[0]) for r in cur.fetchall()] + conn.close() + data.append([ + row["Subject"], qid, st_map.get(row["latest_status"], '未完成'), + row["question_type"], row["knowledge_points_desc"], row["answer_time"], + row["input_time"], row["paper_count"], row["right_times"], + row["wrong_times"], row["half_right_times"], row["last_right_time"], + ",".join(p_ids) + ]) + return data + + +@app.post("/api/questions/export-excel") +def question_export_excel(req: QuestionSearchReq): + import pandas as pd + data = _question_table_rows(req) + if not data: + raise HTTPException(status_code=400, detail="当前列表没有数据可以导出") + df = pd.DataFrame(data, columns=QUESTION_TABLE_COLUMNS) + buf = _xlsx_bytes(df, '筛选结果', kp_col_name="题目知识点") + filename = f"题目筛选结果_{datetime.datetime.now().strftime('%Y%m%d_%H%M')}.xlsx" + return StreamingResponse( + buf, + media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + headers={"Content-Disposition": f"attachment; filename*=UTF-8''{__import__('urllib.parse', fromlist=['quote']).quote(filename)}"}) + + +@app.get("/api/questions/validate-ids") +def questions_validate_ids(ids: str): + """PapersCreateByIdView.generate_paper — ID validation part""" + id_strings = re.split(r'[,,\s\n]+', ids.strip()) + selected_ids = [] + for id_str in id_strings: + if id_str: + try: + selected_ids.append(int(id_str)) + except ValueError: + raise HTTPException(status_code=400, detail=f"ID '{id_str}' 无效,请确保所有ID都是数字。") + if not selected_ids: + raise HTTPException(status_code=400, detail="未检测到有效试题ID,请重新输入。") + + conn = get_db() + cur = conn.cursor() + placeholders = ','.join(['?'] * len(selected_ids)) + cur.execute(f"SELECT id FROM questions WHERE id IN ({placeholders})", tuple(selected_ids)) + valid_ids = [row[0] for row in cur.fetchall()] + conn.close() + + if not valid_ids: + raise HTTPException(status_code=400, detail="输入的试题ID均不存在于数据库中,请检查。") + + missing_ids = sorted(set(selected_ids) - set(valid_ids)) + return {"valid_ids": valid_ids, "missing_ids": missing_ids} + + +# ============================================================ +# 题目详情 (QuestionDetailView) +# ============================================================ + +@app.get("/api/questions/{question_id}") +def question_detail(question_id: int): + conn = get_db() + cur = conn.cursor() + sql = """ + SELECT q.*, s.Subject, qs.source_description, qst.StatusName, ec.Cause, t.term_name, st.stage_name, media_path, + (SELECT GROUP_CONCAT(kp.content, '、') FROM question_kp_relation qkr + JOIN knowledge_point kp ON qkr.kp_id = kp.id WHERE qkr.question_id = q.id) AS knowledge_points_desc + FROM questions q + LEFT JOIN Subject s ON q.subject_id = s.ID + LEFT JOIN question_source qs ON q.source_id = qs.id + LEFT JOIN Question_Status qst ON q.status_id = qst.ID + LEFT JOIN Error_Cause ec ON q.error_cause_id = ec.ID + LEFT JOIN term t ON q.term_id = t.ID + LEFT JOIN stage st ON q.stage_id = st.ID + WHERE q.id = ? + """ + cur.execute(sql, (question_id,)) + row = cur.fetchone() + conn.close() + if row is None: + raise HTTPException(status_code=404, detail="题目不存在") + data = row_to_dict(row) + # 编辑模式下科目下拉选项 + conn = get_db() + cur = conn.cursor() + cur.execute("SELECT Subject FROM Subject WHERE enabled = 1") + data["subject_options"] = [r[0] for r in cur.fetchall()] + conn.close() + return data + + +class QuestionUpdateReq(BaseModel): + subject: str + question_type: str + answer_time: str + is_subjective: bool + content_raw: str + analysis_raw: str + + +@app.put("/api/questions/{question_id}") +def question_update(question_id: int, req: QuestionUpdateReq): + """QuestionDetailView.save_changes""" + try: + conn = get_db() + cur = conn.cursor() + cur.execute("SELECT ID FROM Subject WHERE Subject = ?", (req.subject,)) + res = cur.fetchone() + if not res: + conn.close() + raise HTTPException(status_code=400, detail=f"科目 {req.subject} 不存在") + sid = res[0] + + counters = {} + stem, s_path = media_utils.process_content_for_save(conn, MEDIA_BASE_DIR, req.content_raw, question_id, counters) + ana, a_path = media_utils.process_content_for_save(conn, MEDIA_BASE_DIR, req.analysis_raw, question_id, counters) + + cur.execute(""" + UPDATE questions SET subject_id=?, question_type=?, answer_time=?, is_subjective=?, content=?, analysis=?, media_path=? + WHERE id=? + """, (sid, req.question_type, req.answer_time, 1 if req.is_subjective else 0, + stem, ana, s_path or a_path, question_id)) + conn.commit() + conn.close() + return {"ok": True} + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +# ============================================================ +# 文件导入 (FileImportView + JsonAddToQuestionView + manuallyadd) +# ============================================================ + +@app.get("/api/import/options") +def import_options(): + """FileImportView.load_all_db_options — defaults included.""" + conn = get_db() + cur = conn.cursor() + + def load(table, name_col, default_col, enabled_col): + if table in ("term", "stage", "question_origin"): + cur.execute(f"SELECT {name_col}, {default_col} FROM {table} WHERE enabled = 1 ORDER BY ID") + rows = cur.fetchall() + names = [r[0] for r in rows] + default = next((r[0] for r in rows if r[1] == 1), None) + return names, (default or (names[0] if names else "")) + else: # Subject / question_source + cur.execute(f"SELECT {name_col} FROM {table} WHERE {enabled_col} = 1 ORDER BY ID") + names = [r[0] for r in cur.fetchall()] + return names, (names[0] if names else "") + + subject, subject_d = load("Subject", "Subject", None, "enabled") + source, source_d = load("question_source", "source_description", None, "is_enabled") + term, term_d = load("term", "term_name", "is_default", None) + stage, stage_d = load("stage", "stage_name", "is_default", None) + origin, origin_d = load("question_origin", "origin_name", "is_default", None) + conn.close() + + return { + "subject": {"options": subject, "default": subject_d}, + "source": {"options": source, "default": source_d}, + "term": {"options": term, "default": term_d}, + "stage": {"options": stage, "default": stage_d}, + "origin": {"options": origin, "default": origin_d}, + "type": {"options": ["错题", "推题", "补充提高"], "default": "错题"}, + } + + +@app.post("/api/import/parse") +async def import_parse( + file: UploadFile = File(...), + subject: str = Form(...), + source: str = Form(...), + q_type: str = Form(...), + term: str = Form(...), + stage: str = Form(...), + origin: str = Form(...), +): + """FileImportView.run_parsing""" + if not all([subject, source, q_type, term, stage, origin]): + raise HTTPException(status_code=400, detail="请确保所有导入参数都已选择!") + + meta_data = { + "subject": subject, + "source": source, + "type": q_type, + "import_time": now_str(), + "term": term, + "stage": stage, + "origin": origin, + } + + # save upload to temp file + suffix = os.path.splitext(file.filename or "")[1] or ".docx" + with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp: + content = await file.read() + tmp.write(content) + tmp_path = tmp.name + + log_lines = [] + + def log_cb(msg): + log_lines.append(f"[{datetime.datetime.now().strftime('%H:%M:%S')}] {msg}") + + try: + src = meta_data.get("source", "") + parser = None + if src == "智学网": + parser = ZhixuewangParser(tmp_path, meta_data, log_callback=log_cb, media_base_dir=MEDIA_BASE_DIR) + elif src == "组卷网": + parser = ZujuanwangParser(tmp_path, meta_data, log_callback=log_cb, media_base_dir=MEDIA_BASE_DIR) + elif src.lower() == "自组word": + parser = ZizuWordParser(tmp_path, meta_data, log_callback=log_cb, media_base_dir=MEDIA_BASE_DIR) + else: + log_cb(f"未知的题目来源:{src}") + return {"log": log_lines, "questions": None, "error": f"未知的题目来源:{src}"} + + questions_list, error = parser.parse() + + if error: + log_cb(f"解析失败: {error}") + return {"log": log_lines, "questions": None, "error": error} + if not questions_list: + log_cb("解析完成,但未发现题目数据。") + return {"log": log_lines, "questions": None, "error": None} + + log_cb(f"解析成功!共提取 {len(questions_list)} 道题目。") + log_cb("\n--- JSON 预览数据 ---") + import json as _json + log_cb(_json.dumps(questions_list, indent=4, ensure_ascii=False)) + log_cb("--- 预览结束 ---\n") + return {"log": log_lines, "questions": questions_list, "error": None} + except Exception as e: + log_cb(f"解析过程中发生未预期错误: {e}") + return {"log": log_lines, "questions": None, "error": str(e)} + finally: + try: + os.unlink(tmp_path) + except Exception: + pass + + +@app.post("/api/import/upload-image") +async def import_upload_image(file: UploadFile = File(...)): + """Web equivalent of clipboard paste / local image pick: the uploaded + image is stored in Media/_clipboard_temp and its path is returned to be + inserted into the text as a [[IMG:...]] token.""" + clipboard_temp_dir = media_utils.ensure_media_base(MEDIA_BASE_DIR) + data = await file.read() + if not data: + raise HTTPException(status_code=400, detail="空的图片文件") + path = media_utils.save_clipboard_upload(clipboard_temp_dir, file.filename or "image.png", data) + return {"path": path} + + +class ManualAddReq(BaseModel): + """manuallyadd._validate_and_build_question_json""" + category: str + q_type: str + kp_text: str + answer_time: str + stem_raw: str + analysis_raw: str + question_no: int + session_timestamp: str + meta: Dict[str, Any] = {} + force: bool = False # 用户确认按原样保存非常规日期格式 + + +@app.post("/api/import/manual-add") +def import_manual_add(req: ManualAddReq): + # 校验(与原版一致) + if not req.category: + raise HTTPException(status_code=400, detail="请先选择题目分类(客观题或主观题)。") + if not req.kp_text: + raise HTTPException(status_code=400, detail="题目知识点为必填项。") + if not req.stem_raw: + raise HTTPException(status_code=400, detail="题干内容不能为空。") + if not req.analysis_raw: + raise HTTPException(status_code=400, detail="答案解析不能为空。") + + answer_time_str = req.answer_time.strip() + if not req.force and answer_time_str and len(answer_time_str) == 10: + try: + datetime.datetime.strptime(answer_time_str, "%Y-%m-%d") + except Exception: + raise HTTPException(status_code=422, detail="答题时间格式不是标准日期(YYYY-MM-DD),仍要按原样保存吗?") + + counters = {} + stem_processed, stem_dir = media_utils.collect_images_for_json( + MEDIA_BASE_DIR, req.stem_raw, req.session_timestamp, req.question_no, counters) + analysis_processed, analysis_dir = media_utils.collect_images_for_json( + MEDIA_BASE_DIR, req.analysis_raw, req.session_timestamp, req.question_no, counters) + + image_dir = stem_dir or analysis_dir + + meta = req.meta or {} + q_json = { + "题号": str(req.question_no), + "科目": meta.get("subject", ""), + "题目来源": meta.get("source", ""), + "题目类型": req.q_type or "错题", + "输入时间": now_str(), + "答题时间": answer_time_str, + "题目知识点": req.kp_text, + "题目分类": req.category, + "题干内容": stem_processed, + "答案解析": analysis_processed, + "学期": meta.get("term", ""), + "阶段": meta.get("stage", ""), + "题目出处": "手动创建", + "图片目录": image_dir or "" + } + return q_json + + +class ImportSaveReq(BaseModel): + questions: List[Dict[str, Any]] + meta: Dict[str, Any] = {} + + +@app.post("/api/import/save") +def import_save(req: ImportSaveReq): + """JsonAddToQuestionView.save_to_db""" + if not req.questions: + raise HTTPException(status_code=400, detail="没有题目可以入库。") + + # 知识点缺失检查 + missing_kp = [] + for item in req.questions: + if not (item.get("题目知识点") or "").strip(): + missing_kp.append(str(item.get("题号", ""))) + if missing_kp: + raise HTTPException(status_code=400, detail=( + f"以下题目缺少知识点信息,无法保存整组题目:\n题号: {', '.join(missing_kp)}\n请补充完整后再保存。")) + + conn = None + try: + conn = get_db() + cur = conn.cursor() + cur.execute("BEGIN") + + def get_id(table, col, val, en_col="enabled"): + if not val: + return None + cur.execute(f"SELECT ID FROM {table} WHERE {col} = ?", (val,)) + res = cur.fetchone() + if res: + return res[0] + if table == "question_source": + cur.execute(f"INSERT INTO {table} ({col}, is_enabled) VALUES (?, 1)", (val,)) + else: + cur.execute(f"INSERT INTO {table} ({col}, {en_col}) VALUES (?, 1)", (val,)) + return cur.lastrowid + + for item in req.questions: + meta = req.meta + effective_subject = meta.get("subject") if meta else item.get("科目") + effective_source = meta.get("source") if meta else item.get("题目来源") + effective_type = meta.get("type") if meta else item.get("题目类型", "错题") + effective_term = meta.get("term") if meta else item.get("学期") + effective_stage = meta.get("stage") if meta else item.get("阶段") + effective_origin = meta.get("origin") if meta else item.get("题目出处") + effective_import_time = meta.get("import_time") if meta else item.get( + "输入时间", datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + + sid = get_id("Subject", "Subject", effective_subject) + src_id = get_id("question_source", "source_description", effective_source) + tid = get_id("term", "term_name", effective_term) + stgid = get_id("stage", "stage_name", effective_stage) + orig_id = get_id("question_origin", "origin_name", effective_origin) + + sql = ''' + INSERT INTO questions ( + subject_id, status_id, error_cause_id, source_id, question_type, + content, analysis, score, input_time, answer_time, + has_subquestions, subquestion_count, is_subjective, + term_id, stage_id, origin_id, media_path + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''' + cur.execute(sql, ( + sid, None, None, src_id, effective_type, + item.get("题干内容"), item.get("答案解析"), item.get("分值", 0), + effective_import_time, + item.get("答题时间"), + 1 if item.get("是否有小题") else 0, item.get("小题个数", 0), + 1 if item.get("题目分类") == "主观题" else 0, + tid, stgid, orig_id, item.get("图片目录") + )) + + new_qid = cur.lastrowid + + kp_text = (item.get("题目知识点") or "").strip() + if kp_text: + kp_names = [n.strip() for n in re.split(r'[,,、\s]+', str(kp_text)) if n.strip()] + for name in kp_names: + cur.execute("SELECT id FROM knowledge_point WHERE content = ?", (name,)) + kp_res = cur.fetchone() + if kp_res: + kp_id = kp_res[0] + else: + cur.execute("INSERT INTO knowledge_point (content, subject_id, is_enabled) VALUES (?, ?, 1)", (name, sid)) + kp_id = cur.lastrowid + cur.execute("INSERT OR IGNORE INTO question_kp_relation (question_id, kp_id) VALUES (?, ?)", (new_qid, kp_id)) + + conn.commit() + conn.close() + return {"inserted": len(req.questions)} + except Exception as e: + if conn: + conn.rollback() + raise HTTPException(status_code=500, detail=f"入库失败 错误详情: {e}") + + +# ============================================================ +# 试卷管理 (PapersListView / PapersPreviewView / PapersResultView) +# ============================================================ + +PAPER_SORT_WHITELIST = {"p.id", "p.paper_name", "p.subject", "question_count", "p.create_time", "ps.status_name"} + + +@app.get("/api/papers/list") +def papers_list(status: str = "未完成", paper_id: str = "", sort_col: str = "p.create_time", sort_dir: str = "DESC"): + """PapersListView.load_papers_data""" + conn = get_db() + cur = conn.cursor() + + sql = """ + SELECT p.id, p.paper_name, p.subject, ps.status_name, + (SELECT COUNT(*) FROM paper_questions WHERE paper_id = p.id) AS question_count, + p.create_time + FROM papers p + LEFT JOIN paper_status ps ON p.status_id = ps.id + WHERE p.enabled = 1 + """ + params = [] + + paper_id_input = (paper_id or "").strip() + if paper_id_input: + ids = [] + for s_id in paper_id_input.replace(',', ',').split(','): + s_id = s_id.strip() + if s_id.isdigit(): + ids.append(int(s_id)) + if ids: + placeholders = ','.join(['?'] * len(ids)) + sql += f" AND p.id IN ({placeholders})" + params.extend(ids) + + status_text_to_id_map = {"未完成": 1, "已完成": 2, "部分完成": 3, "归档": 4} + if not paper_id_input and status != "全部": + target_status_id = status_text_to_id_map.get(status) + if target_status_id is not None: + sql += " AND p.status_id = ?" + params.append(target_status_id) + + if sort_col in PAPER_SORT_WHITELIST and sort_dir in ("ASC", "DESC"): + sql += f" ORDER BY {sort_col} {sort_dir}" + + cur.execute(sql, params) + rows = cur.fetchall() + conn.close() + + return {"rows": [{ + "ID": r["id"], + "试卷名称": r["paper_name"], + "科目": r["subject"] or "-", + "题数": r["question_count"], + "创建时间": (r["create_time"][:19] if r["create_time"] else ""), + "状态": r["status_name"] or "-" + } for r in rows]} + + +class PaperActionReq(BaseModel): + status: str = "未完成" + paper_id: str = "" + sort_col: str = "p.create_time" + sort_dir: str = "DESC" + + +@app.post("/api/papers/list/export-excel") +def papers_export_excel(req: PaperActionReq): + import pandas as pd + data = papers_list(status=req.status, paper_id=req.paper_id, sort_col=req.sort_col, sort_dir=req.sort_dir) + rows = data["rows"] + if not rows: + raise HTTPException(status_code=400, detail="当前列表没有数据可以导出") + df = pd.DataFrame(rows, columns=["ID", "试卷名称", "科目", "题数", "创建时间", "状态"]) + buf = _xlsx_bytes(df, '试卷列表') + filename = f"试卷列表_{datetime.datetime.now().strftime('%Y%m%d_%H%M')}.xlsx" + from urllib.parse import quote + return StreamingResponse( + buf, + media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + headers={"Content-Disposition": f"attachment; filename*=UTF-8''{quote(filename)}"}) + + +@app.get("/api/papers/{paper_id}/basic") +def paper_basic(paper_id: int): + conn = get_db() + cur = conn.cursor() + cur.execute("SELECT paper_name FROM papers WHERE id = ?", (paper_id,)) + row = cur.fetchone() + paper_name = row[0] if row else f"试卷{paper_id}" + cur.execute("SELECT question_id FROM paper_questions WHERE paper_id = ? ORDER BY question_id", (paper_id,)) + qids = [r[0] for r in cur.fetchall()] + conn.close() + return {"paper_name": paper_name, "question_ids": qids} + + +@app.delete("/api/papers/{paper_id}") +def paper_delete(paper_id: int): + """PapersListView.delete_paper""" + conn = get_db() + cur = conn.cursor() + cur.execute(""" + SELECT p.paper_name, (SELECT COUNT(*) FROM paper_questions WHERE paper_id = p.id) as q_count + FROM papers p WHERE p.id = ? + """, (paper_id,)) + res = cur.fetchone() + paper_name = res[0] if res else "未知" + q_count = res[1] if res else 0 + + cur.execute("SELECT question_id, result_type FROM paper_questions WHERE paper_id = ?", (paper_id,)) + results = cur.fetchall() + for q_id, res_type in results: + cur.execute("UPDATE questions SET paper_count = MAX(0, paper_count - 1) WHERE id = ?", (q_id,)) + if res_type is not None: + if res_type == 1: + cur.execute("UPDATE questions SET right_times = MAX(0, right_times - 1) WHERE id = ?", (q_id,)) + elif res_type == 0: + cur.execute("UPDATE questions SET wrong_times = MAX(0, wrong_times - 1) WHERE id = ?", (q_id,)) + elif res_type == 2: + cur.execute("UPDATE questions SET half_right_times = MAX(0, half_right_times - 1) WHERE id = ?", (q_id,)) + cur.execute("UPDATE questions SET review_times = MAX(0, review_times - 1) WHERE id = ?", (q_id,)) + + cur.execute("DELETE FROM paper_questions WHERE paper_id = ?", (paper_id,)) + cur.execute("DELETE FROM papers WHERE id = ?", (paper_id,)) + conn.commit() + conn.close() + return {"deleted": paper_id, "paper_name": paper_name, "question_count": q_count} + + +@app.get("/api/papers/{paper_id}/download") +def paper_download(paper_id: int): + """PapersListView.download_paper""" + conn = get_db() + cur = conn.cursor() + cur.execute("SELECT question_id FROM paper_questions WHERE paper_id = ? ORDER BY question_id", (paper_id,)) + qids = [r[0] for r in cur.fetchall()] + cur.execute("SELECT paper_name FROM papers WHERE id = ?", (paper_id,)) + row = cur.fetchone() + paper_name = row[0] if row else f"试卷{paper_id}" + conn.close() + + if not qids: + raise HTTPException(status_code=400, detail=f"试卷 {paper_id} 暂无题目,无法下载。") + + try: + # generate_docx 与原版一致:写入 BASE_DIR/temp_preview.docx(原函数无返回值) + create_word.generate_docx( + qids, BASE_DIR, get_db, + title=paper_name, + paper_id_for_header=paper_id + ) + out_path = os.path.join(BASE_DIR, "temp_preview.docx") + except Exception as e: + raise HTTPException(status_code=500, detail=f"下载失败: {e}") + + default_filename = f"{paper_name}_{paper_id}.docx".replace("/", "_").replace("\\", "_") + from urllib.parse import quote + return FileResponse( + out_path, + filename=default_filename, + headers={"Content-Disposition": f"attachment; filename*=UTF-8''{quote(default_filename)}"}) + + +# ---- 试卷预览 (PapersPreviewView) ---- + +class PaperPreviewInfoReq(BaseModel): + selected_ids: Optional[List[int]] = None + paper_id: Optional[int] = None + + +@app.post("/api/papers/preview-info") +def papers_preview_info(req: PaperPreviewInfoReq): + """init_data_and_render: title + selected ids""" + if req.paper_id is not None: + basic = paper_basic(req.paper_id) + return {"paper_id": req.paper_id, "title": basic["paper_name"], + "selected_ids": basic["question_ids"]} + return {"paper_id": None, "title": _generate_default_title(req.selected_ids or []), + "selected_ids": req.selected_ids or []} + + +def _generate_default_title(selected_ids): + """PapersPreviewView.generate_default_title""" + subjects, answer_times = set(), [] + try: + conn = get_db() + cur = conn.cursor() + + if not selected_ids: + conn.close() + return f"综合练习 {datetime.datetime.now().strftime('%Y%m%d')}" + + placeholders = ','.join(['?'] * len(selected_ids)) + query = f""" + SELECT s.Subject, q.answer_time + FROM questions q + LEFT JOIN Subject s ON q.subject_id = s.ID + WHERE q.id IN ({placeholders}) + """ + cur.execute(query, tuple(sorted(selected_ids))) + for row in cur.fetchall(): + if row["Subject"]: + subjects.add(row["Subject"]) + if row["answer_time"]: + try: + dt_obj = datetime.datetime.strptime(row["answer_time"], "%Y-%m-%d %H:%M:%S") + except ValueError: + try: + dt_obj = datetime.datetime.strptime(row["answer_time"], "%Y-%m-%d") + except ValueError: + dt_obj = None + if dt_obj: + answer_times.append(dt_obj) + conn.close() + + if not subjects: + return f"综合练习 {datetime.datetime.now().strftime('%Y%m%d')}" + + if answer_times: + min_date = min(answer_times).strftime("%Y-%m-%d") + max_date = max(answer_times).strftime("%Y-%m-%d") + date_range = f"{min_date}至{max_date}" if min_date != max_date else min_date + else: + date_range = datetime.datetime.now().strftime("%Y-%m-%d") + + subject_str = ' + '.join(sorted(subjects)) + return f"{subject_str} {date_range}" + except Exception: + return "新试卷" + + +class PaperDocxReq(BaseModel): + selected_ids: List[int] + title: Optional[str] = None + paper_id: Optional[int] = None + + +@app.post("/api/papers/preview-docx") +def papers_preview_docx(req: PaperDocxReq): + """PapersPreviewView.render_docx_preview_placeholder""" + if not req.selected_ids: + raise HTTPException(status_code=400, detail="未选择题目,无法预览。") + try: + out_path = os.path.join(BASE_DIR, "temp_preview.docx") + create_word.generate_docx( + req.selected_ids, BASE_DIR, get_db, + title=req.title, + paper_id_for_header=req.paper_id + ) + return {"ok": True, "path": out_path, "download_url": "/StudyBase/api/papers/temp-file?name=preview"} + except Exception as e: + raise HTTPException(status_code=500, detail=f"生成预览失败: {e}") + + +@app.post("/api/papers/review-docx") +def papers_review_docx(req: PaperDocxReq): + """PapersPreviewView.generate_review_paper""" + if not req.selected_ids: + raise HTTPException(status_code=400, detail="未选择题目,无法生成复习卷。") + try: + out_path = create_word.generate_review_docx( + req.selected_ids, BASE_DIR, get_db, + title=req.title, + paper_id_for_header=req.paper_id + ) + return {"ok": True, "path": out_path, "download_url": "/StudyBase/api/papers/temp-file?name=review"} + except Exception as e: + raise HTTPException(status_code=500, detail=f"生成复习卷失败: {e}") + + +@app.get("/api/papers/temp-file") +def papers_temp_file(name: str): + """打开文档 / 打开复习卷""" + if name == "preview": + path = os.path.join(BASE_DIR, "temp_preview.docx") + elif name == "review": + path = os.path.join(BASE_DIR, "temp_review_preview.docx") + else: + raise HTTPException(status_code=400, detail="未知文件") + if not os.path.exists(path): + raise HTTPException(status_code=404, detail="文档未找到,请确认已生成预览。") + fname = "temp_preview.docx" if name == "preview" else "temp_review_preview.docx" + return FileResponse(path, filename=fname) + + +class PaperSaveReq(BaseModel): + selected_ids: List[int] + title: str + + +@app.post("/api/papers/save") +def papers_save(req: PaperSaveReq): + """PapersPreviewView.on_save_and_generate""" + if not req.selected_ids: + raise HTTPException(status_code=400, detail="未选择题目,无法生成试卷。") + try: + conn = get_db() + cur = conn.cursor() + paper_name = req.title + create_time = now_str() + + subjects = set() + for qid in req.selected_ids: + cur.execute(""" + SELECT s.Subject FROM questions q + JOIN Subject s ON q.subject_id = s.ID + WHERE q.id = ? + """, (qid,)) + res = cur.fetchone() + if res: + subjects.add(res[0]) + subjects_str = ', '.join(sorted(list(subjects))) if subjects else '未指定' + + cur.execute(""" + INSERT INTO papers (paper_name, create_time, status_id, enabled, subject) + VALUES (?, ?, ?, ?, ?) + """, (paper_name, create_time, 1, 1, subjects_str)) + + paper_id = cur.lastrowid + + for qid in req.selected_ids: + cur.execute("SELECT subject_id FROM questions WHERE id = ?", (qid,)) + subject_id_row = cur.fetchone() + subject_id = subject_id_row[0] if subject_id_row else None + + cur.execute(""" + INSERT INTO paper_questions (paper_id, question_id, subject_id) + VALUES (?, ?, ?) + """, (paper_id, qid, subject_id)) + + cur.execute("UPDATE questions SET paper_count = paper_count + 1 WHERE id = ?", (qid,)) + + conn.commit() + conn.close() + return {"paper_id": paper_id} + except Exception as e: + raise HTTPException(status_code=500, detail=f"数据库操作失败: {e}") + + +# ---- 试卷结果反馈 (PapersResultView) ---- + +@app.get("/api/papers/{paper_id}/result") +def papers_result(paper_id: int): + conn = get_db() + cur = conn.cursor() + cur.execute(""" + SELECT q.id, s.Subject, pq.result_type, pq.error_cause_id + FROM paper_questions pq + JOIN questions q ON pq.question_id = q.id + LEFT JOIN Subject s ON q.subject_id = s.ID + WHERE pq.paper_id = ? + """, (paper_id,)) + questions = [] + for question_id, subject, result_type, error_cause_id in cur.fetchall(): + questions.append({ + "question_id": question_id, "subject": subject, + "result_type": result_type, "error_cause_id": error_cause_id}) + + cur.execute("SELECT Cause FROM Error_Cause WHERE Enabled = 1") + causes = [r[0] for r in cur.fetchall()] + + # 已判定题目的错误原因文本 + for q in questions: + if q["error_cause_id"]: + cur.execute("SELECT Cause FROM Error_Cause WHERE ID = ?", (q["error_cause_id"],)) + r = cur.fetchone() + q["error_cause_text"] = r[0] if r else "无" + else: + q["error_cause_text"] = "无" + conn.close() + + has_pending = any(q["result_type"] == 0 for q in questions) + return {"questions": questions, "causes": causes, "has_pending": has_pending} + + +class ResultConfirmReq(BaseModel): + results: List[Dict[str, Any]] # [{question_id, result, error_cause, partial_desc}] + + +@app.post("/api/papers/{paper_id}/result/confirm") +def papers_result_confirm(paper_id: int, req: ResultConfirmReq): + """PapersResultView.confirm_results""" + try: + conn = get_db() + cur = conn.cursor() + + for q_state in req.results: + question_id = q_state["question_id"] + current_val = q_state.get("result", "无") + error_var = q_state.get("error_cause", "无") + error_sub_description = q_state.get("partial_desc", "") if current_val == "部分错误" else "" + + updated_pq_result_type = 0 + + if current_val == "正确": + updated_pq_result_type = 1 + cur.execute(""" + UPDATE questions + SET review_times = COALESCE(review_times, 0) + 1, + right_times = COALESCE(right_times, 0) + 1, + last_right_time = ?, + latest_status = 'right', + half_right_description = NULL + WHERE id = ? + """, (now_str(), question_id)) + elif current_val == "错误": + updated_pq_result_type = 3 + cur.execute(""" + UPDATE questions + SET review_times = COALESCE(review_times, 0) + 1, + wrong_times = COALESCE(wrong_times, 0) + 1, + last_wrong_time = ?, + latest_status = 'wrong', + half_right_description = NULL + WHERE id = ? + """, (now_str(), question_id)) + elif current_val == "部分错误": + updated_pq_result_type = 2 + cur.execute(""" + UPDATE questions + SET review_times = COALESCE(review_times, 0) + 1, + half_right_times = COALESCE(half_right_times, 0) + 1, + last_half_right_time = ?, + latest_status = 'half_right', + half_right_description = ? + WHERE id = ? + """, (now_str(), error_sub_description, question_id)) + elif current_val == "未完成": + updated_pq_result_type = 0 + + error_cause_id = None + if error_var != "无": + cur.execute("SELECT ID FROM Error_Cause WHERE Cause = ?", (error_var,)) + row = cur.fetchone() + if row is not None: + error_cause_id = row[0] + + cur.execute(""" + UPDATE paper_questions + SET error_cause_id = ?, result_type = ?, error_sub_description = ?, record_time = ? + WHERE paper_id = ? AND question_id = ? + """, (error_cause_id, updated_pq_result_type, error_sub_description, now_str(), paper_id, question_id)) + + # 重新评估试卷状态 + cur.execute("SELECT result_type FROM paper_questions WHERE paper_id = ?", (paper_id,)) + all_question_results = cur.fetchall() + + total_questions_in_paper = len(all_question_results) + num_pending_questions = sum(1 for row in all_question_results if row[0] == 0) + + new_paper_status_id = 1 + if num_pending_questions == 0: + new_paper_status_id = 2 + elif num_pending_questions < total_questions_in_paper: + new_paper_status_id = 3 + + cur.execute("UPDATE papers SET status_id = ? WHERE id = ?", (new_paper_status_id, paper_id)) + + conn.commit() + conn.close() + return {"ok": True} + except Exception as e: + raise HTTPException(status_code=500, detail=f"结果保存失败: {e}") + + +@app.post("/api/papers/{paper_id}/archive") +def papers_archive(paper_id: int): + """PapersResultView.archive_paper""" + try: + conn = get_db() + cur = conn.cursor() + cur.execute("UPDATE papers SET status_id = 4 WHERE id = ?", (paper_id,)) + conn.commit() + conn.close() + return {"ok": True} + except Exception as e: + raise HTTPException(status_code=500, detail=f"操作失败: {e}") + + +# ============================================================ +# 分析 (章节分析 / 知识点分析 / 遗留分析) +# ============================================================ + +class ChapterReq(BaseModel): + subject: str = "全部" + correct_count: str = "" + not_correct_count: str = "" + chapter_desc: str = "" + sort_column: str = "not_correct_questions" + sort_direction: str = "DESC" + + +@app.post("/api/analysis/chapters") +def analysis_chapters(req: ChapterReq): + """SubjectChapterListView.load_chapter_data""" + conn = get_db() + cur = conn.cursor() + inner_where = " WHERE 1=1" + params = [] + if req.subject and req.subject != "全部": + inner_where += " AND s.Subject = ?" + params.append(req.subject) + chapter_desc_filter = (req.chapter_desc or "").strip() + if chapter_desc_filter: + for keyword in chapter_desc_filter.split(): + inner_where += " AND c.chapter_desc LIKE ?" + params.append(f"%{keyword}%") + + sql_query = f""" + WITH ChapterCounts AS ( + SELECT + c.id AS chapter_id, + s.Subject AS SubjectName, + c.volume_name AS VolumeName, + c.chapter_desc AS ChapterDesc, + ( + SELECT COUNT(DISTINCT q.id) + FROM knowledge_point kp + JOIN question_kp_relation qkr ON kp.id = qkr.kp_id + JOIN questions q ON qkr.question_id = q.id + WHERE kp.chapter_id = c.id AND q.question_type = '错题' + ) AS total_wrong_questions, + ( + SELECT COUNT(DISTINCT q.id) + FROM knowledge_point kp + JOIN question_kp_relation qkr ON kp.id = qkr.kp_id + JOIN questions q ON qkr.question_id = q.id + WHERE kp.chapter_id = c.id AND q.question_type = '错题' AND q.latest_status = 'right' + ) AS correct_questions, + ( + SELECT COUNT(DISTINCT q.id) + FROM knowledge_point kp + JOIN question_kp_relation qkr ON kp.id = qkr.kp_id + JOIN questions q ON qkr.question_id = q.id + WHERE kp.chapter_id = c.id AND q.question_type = '错题' AND (q.latest_status != 'right' OR q.latest_status IS NULL) + ) AS not_correct_questions + FROM + chapters c + LEFT JOIN + Subject s ON c.subject_id = s.ID + {inner_where} + ) + SELECT * FROM ChapterCounts + WHERE (total_wrong_questions != 0 OR correct_questions != 0 OR not_correct_questions != 0) + """ + if (req.correct_count or "").strip().isdigit(): + sql_query += " AND correct_questions = ?" + params.append(int(req.correct_count.strip())) + if (req.not_correct_count or "").strip().isdigit(): + sql_query += " AND not_correct_questions = ?" + params.append(int(req.not_correct_count.strip())) + + sort_column_map = { + "id": "chapter_id", "subject": "SubjectName", "volume_name": "VolumeName", + "chapter_desc": "ChapterDesc", "total_wrong_questions": "total_wrong_questions", + "correct_questions": "correct_questions", "not_correct_questions": "not_correct_questions" + } + db_sort_column = sort_column_map.get(req.sort_column, "chapter_id") + dir_ = "ASC" if req.sort_direction == "ASC" else "DESC" + sql_query += f" ORDER BY {db_sort_column} {dir_}, chapter_id ASC" + + cur.execute(sql_query, params) + rows = [{ + "chapter_id": r["chapter_id"], + "subject": r["SubjectName"] if r["SubjectName"] else "-", + "volume_name": r["VolumeName"] if r["VolumeName"] else "-", + "chapter_desc": r["ChapterDesc"] if r["ChapterDesc"] else "-", + "total_wrong_questions": r["total_wrong_questions"], + "correct_questions": r["correct_questions"], + "not_correct_questions": r["not_correct_questions"], + } for r in cur.fetchall()] + conn.close() + return {"rows": rows} + + +@app.post("/api/analysis/chapters/export-excel") +def analysis_chapters_export(req: ChapterReq): + import pandas as pd + data = analysis_chapters(req) + rows = data["rows"] + if not rows: + raise HTTPException(status_code=400, detail="当前列表没有数据可以导出") + df = pd.DataFrame([[r["chapter_id"], r["subject"], r["volume_name"], r["chapter_desc"], + r["total_wrong_questions"], r["correct_questions"], r["not_correct_questions"]] + for r in rows], + columns=["ID", "科目", "册", "章节", "错题数量", "已正确题目数", "尚未正确题目数"]) + buf = _xlsx_bytes(df, '章节分析') + filename = f"章节分析-{datetime.datetime.now().strftime('%Y%m%d')}.xlsx" + from urllib.parse import quote + return StreamingResponse( + buf, + media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + headers={"Content-Disposition": f"attachment; filename*=UTF-8''{quote(filename)}"}) + + +class KnowledgeReq(BaseModel): + subject: str = "全部" + correct_count: str = "" + not_correct_count: str = "" + kp_content: str = "" + chapter_id_filter: Optional[int] = None + sort_column: str = "not_correct_questions" + sort_direction: str = "DESC" + + +@app.post("/api/analysis/knowledge") +def analysis_knowledge(req: KnowledgeReq): + """SubjectKnowledgeListView.load_knowledge_data""" + conn = get_db() + cur = conn.cursor() + params = [] + inner_where = " WHERE 1=1" + + if req.chapter_id_filter: + inner_where += " AND kp.chapter_id = ?" + params.append(req.chapter_id_filter) + + if req.subject and req.subject != "全部": + inner_where += " AND s.Subject = ?" + params.append(req.subject) + + kp_content_filter = (req.kp_content or "").strip() + if kp_content_filter: + for keyword in kp_content_filter.split(): + inner_where += " AND kp.content LIKE ?" + params.append(f"%{keyword}%") + + sql_query = f""" + WITH KnowledgeCounts AS ( + SELECT + kp.id AS kp_id, + s.Subject AS SubjectName, + COALESCE(c.volume_name, '') || ':' || + CASE WHEN p.chapter_desc IS NOT NULL THEN p.chapter_desc || ':' ELSE '' END || + COALESCE(c.chapter_desc, '') AS ChapterFullInfo, + kp.content AS KnowledgeContent, + (SELECT COUNT(qkr.question_id) FROM question_kp_relation qkr WHERE qkr.kp_id = kp.id) AS total_questions, + (SELECT COUNT(DISTINCT qkr_right.question_id) + FROM question_kp_relation qkr_right + JOIN questions q_right ON qkr_right.question_id = q_right.id + WHERE qkr_right.kp_id = kp.id AND q_right.right_times >= 1) AS correct_questions, + (SELECT COUNT(DISTINCT qkr_not_correct.question_id) + FROM question_kp_relation qkr_not_correct + JOIN questions q_not_correct ON qkr_not_correct.question_id = q_not_correct.id + WHERE qkr_not_correct.kp_id = kp.id AND q_not_correct.right_times = 0) AS not_correct_questions + FROM + knowledge_point kp + JOIN + Subject s ON kp.subject_id = s.ID + LEFT JOIN + chapters c ON kp.chapter_id = c.id + LEFT JOIN + chapters p ON c.parent_id = p.id + {inner_where} + ) + SELECT * FROM KnowledgeCounts WHERE 1=1 + """ + if (req.correct_count or "").strip().isdigit(): + sql_query += " AND correct_questions = ?" + params.append(int(req.correct_count.strip())) + if (req.not_correct_count or "").strip().isdigit(): + sql_query += " AND not_correct_questions = ?" + params.append(int(req.not_correct_count.strip())) + + sort_column_map = { + "id": "kp_id", "subject": "SubjectName", "knowledge_content": "KnowledgeContent", + "chapter_info": "ChapterFullInfo", + "total_questions": "total_questions", "correct_questions": "correct_questions", + "not_correct_questions": "not_correct_questions" + } + db_sort_column = sort_column_map.get(req.sort_column, "not_correct_questions") + dir_ = "ASC" if req.sort_direction == "ASC" else "DESC" + sql_query += f" ORDER BY {db_sort_column} {dir_}, kp_id ASC" + + cur.execute(sql_query, params) + rows = [{ + "kp_id": r["kp_id"], + "subject": r["SubjectName"], + "chapter_info": r["ChapterFullInfo"] if r["ChapterFullInfo"] else "-", + "knowledge_content": r["KnowledgeContent"], + "total_questions": r["total_questions"], + "correct_questions": r["correct_questions"], + "not_correct_questions": r["not_correct_questions"], + } for r in cur.fetchall()] + conn.close() + return {"rows": rows} + + +@app.post("/api/analysis/knowledge/export-excel") +def analysis_knowledge_export(req: KnowledgeReq): + import pandas as pd + data = analysis_knowledge(req) + rows = data["rows"] + if not rows: + raise HTTPException(status_code=400, detail="当前列表没有数据可以导出") + df = pd.DataFrame([[r["kp_id"], r["subject"], r["chapter_info"], r["knowledge_content"], + r["total_questions"], r["correct_questions"], r["not_correct_questions"]] + for r in rows], + columns=["ID", "科目", "章节", "知识点", "试题数量", "已正确题目数", "尚未正确题目数"]) + buf = _xlsx_bytes(df, '知识点分析') + filename = f"知识点分析-{datetime.datetime.now().strftime('%Y%m%d')}.xlsx" + from urllib.parse import quote + return StreamingResponse( + buf, + media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + headers={"Content-Disposition": f"attachment; filename*=UTF-8''{quote(filename)}"}) + + +@app.get("/api/analysis/pending") +def analysis_pending(term: str = "", mode: str = "legacy"): + """PendingAnalysisView: term options + stats matrix""" + conn = get_db() + cur = conn.cursor() + cur.execute("SELECT ID, Subject FROM Subject WHERE enabled = 1 ORDER BY ID") + subjects = cur.fetchall() + cur.execute("SELECT ID, stage_name FROM stage WHERE enabled = 1 ORDER BY ID") + stages = cur.fetchall() + cur.execute("SELECT ID, term_name, is_default FROM term WHERE enabled = 1") + terms = cur.fetchall() + term_names = [t[1] for t in terms] + default_term = next((t[1] for t in terms if t[2] == 1), None) + if not term and default_term: + term = default_term + elif not term and term_names: + term = term_names[0] + + cur.execute("SELECT ID FROM term WHERE term_name = ?", (term,)) + res = cur.fetchone() + curr_term_id = res[0] if res else None + + status_condition_sql = "AND (right_times = 0 OR right_times IS NULL)" if mode == "legacy" else "" + + def count_q(sub_id=None, st_id=None, extra=""): + sql = f""" + SELECT COUNT(*) FROM questions + WHERE question_type = '错题' + {status_condition_sql} + {extra} + """ + params = [] + if sub_id is not None: + sql += " AND subject_id = ?" + params.append(sub_id) + if st_id is not None: + sql += " AND stage_id = ?" + params.append(st_id) + if curr_term_id is not None: + sql += " AND term_id = ?" + params.append(curr_term_id) + cur.execute(sql, params) + return cur.fetchone()[0] + + stage_rows = [] + subject_totals = {s[0]: 0 for s in subjects} + for st_id, st_name in stages: + row = {"label": st_name, "cells": [], "sum": 0} + for sub_id, _ in subjects: + c = count_q(sub_id=sub_id, st_id=st_id) + row["cells"].append({"sub_id": sub_id, "st_id": st_id, "count": c}) + row["sum"] += c + subject_totals[sub_id] += c + stage_rows.append(row) + + total_label = "总计遗留" if mode == "legacy" else "总计错题" + total_row = {"label": total_label, "cells": [], "sum": 0} + for sub_id, _ in subjects: + total_row["cells"].append({"sub_id": sub_id, "st_id": None, "count": subject_totals[sub_id]}) + total_row["sum"] += subject_totals[sub_id] + + paper_rows = [] + paper_counts = [ + ("组卷次数0次", "AND paper_count = 0", "0"), + ("组卷次数1次", "AND paper_count = 1", "1"), + ("组卷次数2次", "AND paper_count = 2", "2"), + ("组卷次数3次", "AND paper_count = 3", "3"), + ("组卷次数4次及以上", "AND paper_count >= 4", "4次及以上") + ] + for label, sql_cond, p_val in paper_counts: + row = {"label": label, "cells": [], "sum": 0, "paper_count": p_val} + for sub_id, _ in subjects: + c = count_q(sub_id=sub_id, extra=sql_cond) + row["cells"].append({"sub_id": sub_id, "st_id": None, "count": c, "paper_count": p_val}) + row["sum"] += c + paper_rows.append(row) + + conn.close() + + return { + "terms": term_names, + "current_term": term, + "mode": mode, + "subjects": [{"id": s[0], "name": s[1]} for s in subjects], + "stages": [{"id": s[0], "name": s[1]} for s in stages], + "stage_rows": stage_rows, + "total_row": total_row, + "paper_rows": paper_rows, + } + + +@app.get("/api/analysis/pending/export-excel") +def analysis_pending_export(term: str = "", mode: str = "legacy"): + import pandas as pd + data = analysis_pending(term=term, mode=mode) + headers = ["阶段"] + [s["name"] for s in data["subjects"]] + ["总数"] + rows = [] + for r in data["stage_rows"] + [data["total_row"]] + data["paper_rows"]: + rows.append([r["label"]] + [c["count"] for c in r["cells"]] + [r["sum"]]) + if not rows: + raise HTTPException(status_code=400, detail="当前没有数据可供导出") + df = pd.DataFrame(rows, columns=headers) + mode_name = "遗留错题" if mode == "legacy" else "全部错题" + filename = f"遗留分析汇总_{mode_name}_{term}_{datetime.datetime.now().strftime('%Y%m%d')}.xlsx" + buf = _xlsx_bytes(df, '遗留分析') + from urllib.parse import quote + return StreamingResponse( + buf, + media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + headers={"Content-Disposition": f"attachment; filename*=UTF-8''{quote(filename)}"}) + + +# ============================================================ +# 系统设定 (setting views) +# ============================================================ + +@app.get("/api/settings/subjects") +def settings_subjects(): + conn = get_db() + cur = conn.cursor() + cur.execute("SELECT ID, Subject, enabled, CreateTime FROM Subject ORDER BY ID DESC") + rows = [{"id": r[0], "subject": r[1], "enabled": bool(r[2] in (1, True, '1', 'true')), "create_time": r[3]} + for r in cur.fetchall()] + conn.close() + return {"rows": rows} + + +class NameReq(BaseModel): + name: str + + +@app.post("/api/settings/subjects") +def settings_subjects_add(req: NameReq): + """SubjectManagerView.add_subject""" + name = req.name + if len(name) > 10: + raise HTTPException(status_code=400, detail="学科名称不能超过10个字符") + try: + conn = get_db() + cur = conn.cursor() + now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + cur.execute("INSERT INTO Subject (Subject, CreateTime, enabled) VALUES (?, ?, ?)", (name, now, 1)) + conn.commit() + conn.close() + return {"ok": True} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.put("/api/settings/subjects/{record_id}") +def settings_subjects_edit(record_id: int, req: NameReq): + if len(req.name) > 10: + raise HTTPException(status_code=400, detail="不能超过10个字符") + try: + conn = get_db() + cur = conn.cursor() + cur.execute("UPDATE Subject SET Subject = ? WHERE ID = ?", (req.name, record_id)) + conn.commit() + conn.close() + return {"ok": True} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.post("/api/settings/subjects/{record_id}/toggle") +def settings_subjects_toggle(record_id: int): + try: + conn = get_db() + cur = conn.cursor() + cur.execute("SELECT enabled FROM Subject WHERE ID = ?", (record_id,)) + r = cur.fetchone() + new_status = 0 if (r and r[0] in (1, True, '1', 'true')) else 1 + cur.execute("UPDATE Subject SET enabled = ? WHERE ID = ?", (new_status, record_id)) + conn.commit() + conn.close() + return {"ok": True, "enabled": bool(new_status)} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +# ---- 错误原因管理 (ErrorCauseManagerView) ---- + +@app.get("/api/settings/error-causes") +def settings_error_causes(): + conn = get_db() + cur = conn.cursor() + cur.execute("SELECT ID, Cause, Enabled, ParentID FROM Error_Cause ORDER BY ID") + rows = [{"id": r[0], "cause": r[1], "enabled": bool(r[2] in (1, True, '1', 'true')), "parent_id": r[3]} + for r in cur.fetchall()] + conn.close() + return {"rows": rows} + + +class CauseReq(BaseModel): + name: str + parent_id: Optional[int] = None + + +@app.post("/api/settings/error-causes") +def settings_error_causes_add(req: CauseReq): + """ErrorCauseManagerView._add_cause_logic (5个中文字符限制)""" + name = req.name.strip() + if not name: + raise HTTPException(status_code=400, detail="名称不能为空") + if len(re.findall(r'[\u4e00-\u9fff]', name)) > 5: + raise HTTPException(status_code=400, detail="名称不能超过5个中文字符。") + try: + conn = get_db() + cur = conn.cursor() + cur.execute("INSERT INTO Error_Cause (Cause, Enabled, ParentID) VALUES (?, ?, ?)", + (name, 1, req.parent_id)) + conn.commit() + conn.close() + return {"ok": True} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.put("/api/settings/error-causes/{record_id}") +def settings_error_causes_edit(record_id: int, req: NameReq): + new_name = req.name.strip() + if len(re.findall(r'[\u4e00-\u9fff]', new_name)) > 5: + raise HTTPException(status_code=400, detail="名称不能超过5个中文字符。") + try: + conn = get_db() + cur = conn.cursor() + cur.execute("UPDATE Error_Cause SET Cause = ? WHERE ID = ?", (new_name, record_id)) + conn.commit() + conn.close() + return {"ok": True} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.post("/api/settings/error-causes/{record_id}/toggle") +def settings_error_causes_toggle(record_id: int): + """ErrorCauseManagerView.toggle_status — 含父级限制校验""" + conn = get_db() + cur = conn.cursor() + cur.execute("SELECT ID, Cause, Enabled, ParentID FROM Error_Cause WHERE ID = ?", (record_id,)) + row = cur.fetchone() + if not row: + conn.close() + raise HTTPException(status_code=404, detail="记录不存在") + r_id, r_cause, r_enabled, r_parent = row + current_status = 1 if r_enabled in (1, True, '1', 'true') else 0 + new_status = 0 if current_status == 1 else 1 + + if new_status == 1 and r_parent is not None: + cur.execute("SELECT Enabled FROM Error_Cause WHERE ID = ?", (r_parent,)) + p = cur.fetchone() + if p and not (p[0] in (1, True, '1', 'true')): + conn.close() + raise HTTPException(status_code=400, detail="父级原因已停用,无法单独启用此子原因。") + + if new_status == 0: + cur.execute("SELECT COUNT(*) FROM Error_Cause WHERE ParentID = ?", (record_id,)) + if cur.fetchone()[0] > 0: + conn.close() + # 原版此处有 askyesno 确认,由前端确认后带 force=1 重发 + raise HTTPException(status_code=422, detail=( + f"【{r_cause}】包含子原因。\n停用父级在逻辑上通常意味着子级也不可用。\n确定要继续吗?")) + + try: + cur.execute("UPDATE Error_Cause SET Enabled = ? WHERE ID = ?", (new_status, record_id)) + conn.commit() + conn.close() + return {"ok": True, "enabled": bool(new_status)} + except Exception as e: + conn.close() + raise HTTPException(status_code=500, detail=str(e)) + + +@app.post("/api/settings/error-causes/{record_id}/toggle-force") +def settings_error_causes_toggle_force(record_id: int): + """用户确认停用含子原因的父级后调用""" + conn = get_db() + cur = conn.cursor() + cur.execute("SELECT Enabled FROM Error_Cause WHERE ID = ?", (record_id,)) + row = cur.fetchone() + if not row: + conn.close() + raise HTTPException(status_code=404, detail="记录不存在") + current_status = 1 if row[0] in (1, True, '1', 'true') else 0 + new_status = 0 if current_status == 1 else 1 + cur.execute("UPDATE Error_Cause SET Enabled = ? WHERE ID = ?", (new_status, record_id)) + conn.commit() + conn.close() + return {"ok": True, "enabled": bool(new_status)} + + +# ---- 题目状态管理 (QuestionStatusManagerView) ---- + +@app.get("/api/settings/question-statuses") +def settings_question_statuses(): + conn = get_db() + cur = conn.cursor() + cur.execute("SELECT ID, StatusName, Description, Enabled FROM Question_Status ORDER BY ID") + rows = [{"id": r[0], "name": r[1], "description": r[2], "enabled": bool(r[3] in (1, True, '1', 'true'))} + for r in cur.fetchall()] + conn.close() + return {"rows": rows} + + +class StatusReq(BaseModel): + name: str + description: str = "" + + +@app.post("/api/settings/question-statuses") +def settings_question_statuses_add(req: StatusReq): + name = req.name.strip() + desc = req.description.strip() + if not name: + raise HTTPException(status_code=400, detail="状态名称不能为空") + if len(name) > 20: + raise HTTPException(status_code=400, detail="状态名称过长(建议20字以内)") + try: + conn = get_db() + cur = conn.cursor() + cur.execute("INSERT INTO Question_Status (StatusName, Description, Enabled) VALUES (?, ?, ?)", + (name, desc, 1)) + conn.commit() + conn.close() + return {"ok": True} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.put("/api/settings/question-statuses/{record_id}") +def settings_question_statuses_edit(record_id: int, req: StatusReq): + name = req.name.strip() + desc = req.description.strip() + if not name: + raise HTTPException(status_code=400, detail="状态名称不能为空") + try: + conn = get_db() + cur = conn.cursor() + cur.execute("UPDATE Question_Status SET StatusName = ?, Description = ? WHERE ID = ?", + (name, desc, record_id)) + conn.commit() + conn.close() + return {"ok": True} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.post("/api/settings/question-statuses/{record_id}/toggle") +def settings_question_statuses_toggle(record_id: int): + conn = get_db() + cur = conn.cursor() + cur.execute("SELECT Enabled FROM Question_Status WHERE ID = ?", (record_id,)) + row = cur.fetchone() + if not row: + conn.close() + raise HTTPException(status_code=404, detail="记录不存在") + current = 1 if row[0] in (1, True, '1', 'true') else 0 + new_val = 0 if current else 1 + cur.execute("UPDATE Question_Status SET Enabled = ? WHERE ID = ?", (new_val, record_id)) + conn.commit() + conn.close() + return {"ok": True, "enabled": bool(new_val)} + + +# ---- 默认参数设定 (DefaultParamsSettingView) ---- + +PARAM_TABLES = { + "term": "term_name", + "stage": "stage_name", + "question_origin": "origin_name", +} + + +@app.get("/api/settings/default-params") +def settings_default_params(): + conn = get_db() + cur = conn.cursor() + result = {} + for table, name_col in PARAM_TABLES.items(): + cur.execute(f"SELECT ID, {name_col}, is_default FROM {table} WHERE enabled = 1 ORDER BY ID") + rows = cur.fetchall() + names = [r[1] for r in rows] + default_name = next((r[1] for r in rows if r[2] == 1), None) + result[table] = {"options": names, "default": default_name, + "default_display": default_name if default_name + else (f"{names[0]} (未设置默认值)" if names else "无 (无可用选项)")} + conn.close() + return result + + +@app.post("/api/settings/default-params/{table}") +def settings_default_params_set(table: str, req: NameReq): + """DefaultParamsSettingView._save_default_param""" + if table not in PARAM_TABLES: + raise HTTPException(status_code=404, detail="未知参数表") + name_col = PARAM_TABLES[table] + selected_name = req.name + if not selected_name: + raise HTTPException(status_code=400, detail="请选择一个选项!") + + conn = get_db() + cur = conn.cursor() + cur.execute(f"SELECT ID FROM {table} WHERE {name_col} = ?", (selected_name,)) + row = cur.fetchone() + if not row: + conn.close() + raise HTTPException(status_code=400, detail=f"未找到 '{selected_name}' 对应的ID,请刷新页面。") + selected_id = row[0] + + try: + cur.execute(f"UPDATE {table} SET is_default = 0 WHERE enabled = 1") + cur.execute(f"UPDATE {table} SET is_default = 1 WHERE ID = ?", (selected_id,)) + conn.commit() + conn.close() + return {"ok": True} + except Exception as e: + conn.rollback() + conn.close() + raise HTTPException(status_code=500, detail=f"保存默认{table}失败: {e}") + + +# ============================================================ +# 科目/章节下拉辅助 +# ============================================================ + +@app.get("/api/meta/subjects") +def meta_subjects(): + conn = get_db() + cur = conn.cursor() + cur.execute("SELECT DISTINCT Subject FROM Subject WHERE Subject IS NOT NULL ORDER BY Subject") + subjects = [r[0] for r in cur.fetchall()] + conn.close() + return {"subjects": subjects} + + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="127.0.0.1", port=1065) diff --git a/backend/media_utils.py b/backend/media_utils.py new file mode 100644 index 0000000..89132e2 --- /dev/null +++ b/backend/media_utils.py @@ -0,0 +1,164 @@ +# ===================== media_utils.py ===================== +# Image-token handling ported from the original app: +# - manuallyadd.py: _save_image_for_question / _collect_images_for_json +# - question_detail.py: _ensure_question_folder_for_save / +# _save_image_for_new_or_edited_content / _process_content_for_save +# The [[IMG:...]] pattern regex is taken from the intact version used by +# question_list.py / manuallyadd.py (in question_detail.py of the zip the +# same regex was corrupted by a find-replace accident: "(" had become "$"). +import os +import re +import shutil +import datetime +from PIL import Image + + +def ensure_media_base(media_base_dir): + os.makedirs(media_base_dir, exist_ok=True) + clipboard_temp = os.path.join(media_base_dir, "_clipboard_temp") + os.makedirs(clipboard_temp, exist_ok=True) + return clipboard_temp + + +def save_clipboard_upload(clipboard_temp_dir, filename, data: bytes): + """Save an uploaded (pasted / picked) image into the clipboard temp dir + and return its absolute path (to be embedded as [[IMG:]]).""" + ts = datetime.datetime.now().strftime("%Y%m%d%H%M%S%f") + ext = os.path.splitext(filename)[1] or ".png" + temp_path = os.path.join(clipboard_temp_dir, f"clip_{ts}{ext}") + with open(temp_path, "wb") as f: + f.write(data) + return temp_path + + +# ---------------- manuallyadd.py logic ---------------- + +def ensure_question_folder_manually(media_base_dir, session_timestamp, question_no): + """Media/{timestamp}-{question_no} (manuallyadd._ensure_question_folder)""" + folder_name = f"{session_timestamp}-{question_no}" + folder_path = os.path.join(media_base_dir, folder_name) + os.makedirs(folder_path, exist_ok=True) + return folder_name, folder_path + + +def collect_images_for_json(media_base_dir, raw_text, session_timestamp, question_no, image_counters): + """manuallyadd._collect_images_for_json + _save_image_for_question. + Replaces [[IMG:...]] tokens with markdown image marks.""" + text = raw_text or "" + pattern = re.compile(r"\[\[IMG:(.*?)\]\]") + matches = list(pattern.finditer(text)) + + if not matches: + return text, None + + md_tags = [] + image_dir = None + + for m in matches: + src_path = m.group(1).strip() + if not src_path: + continue + md_tag, rel_dir = _save_image_for_question_impl( + media_base_dir, src_path, session_timestamp, question_no, image_counters) + if md_tag: + md_tags.append(md_tag) + image_dir = rel_dir + + def repl(_match): + return md_tags.pop(0) if md_tags else "" + + processed = pattern.sub(repl, text) + return processed, image_dir + + +def _save_image_for_question_impl(media_base_dir, src_path, session_timestamp, question_no, image_counters): + if not os.path.exists(src_path): + return None, None + + folder_name, folder_path = ensure_question_folder_manually( + media_base_dir, session_timestamp, question_no) + + current_count = image_counters.get(question_no, 0) + 1 + image_counters[question_no] = current_count + + ext = os.path.splitext(src_path)[1] + if not ext: + ext = ".png" + dest_filename = f"image{current_count}{ext}" + dest_full_path = os.path.join(folder_path, dest_filename) + + try: + shutil.copy2(src_path, dest_full_path) + except Exception: + return None, None + + try: + img = Image.open(dest_full_path) + width_px, height_px = img.size + except Exception: + return None, None + + # 估算 EMU:1px ≈ 9525 EMU + w_emu = int(width_px * 9525) + h_emu = int(height_px * 9525) + + max_w_emu = 6_000_000 + max_h_emu = 7_000_000 + + if w_emu > max_w_emu or h_emu > max_h_emu: + scale = min(max_w_emu / float(w_emu), max_h_emu / float(h_emu)) + w_emu = int(w_emu * scale) + h_emu = int(h_emu * scale) + + rel_dir = os.path.join("Media", folder_name).replace(os.sep, "/") + rel_path = f"{rel_dir}/{dest_filename}" + + md_tag = f" ![]( {rel_path}?w={w_emu}&h={h_emu} ) " + return md_tag, rel_dir + + +# ---------------- question_detail.py logic ---------------- + +def ensure_question_folder_for_save(db_conn, media_base_dir, question_id): + """question_detail._ensure_question_folder_for_save""" + cursor = db_conn.cursor() + cursor.execute("SELECT media_path FROM questions WHERE id = ?", (question_id,)) + res = cursor.fetchone() + folder_name = os.path.basename(res[0]) if res and res[0] else \ + f"{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}-edit-{question_id}" + folder_path = os.path.join(media_base_dir, folder_name) + os.makedirs(folder_path, exist_ok=True) + return folder_name, folder_path + + +def save_image_for_new_or_edited_content(media_base_dir, src_path, question_id, folder_name, image_counters): + """question_detail._save_image_for_new_or_edited_content""" + dest_dir = os.path.join(media_base_dir, folder_name) + if question_id not in image_counters: + image_counters[question_id] = len(os.listdir(dest_dir)) + image_counters[question_id] += 1 + dest_filename = f"image{image_counters[question_id]}{os.path.splitext(src_path)[1] or '.png'}" + dest_path = os.path.join(dest_dir, dest_filename) + shutil.copy2(src_path, dest_path) + img = Image.open(dest_path) + w_emu, h_emu = int(img.width * 9525), int(img.height * 9525) + rel_path = f"Media/{folder_name}/{dest_filename}" + return f" ![]( {rel_path}?w={w_emu}&h={h_emu} ) " + + +def process_content_for_save(db_conn, media_base_dir, raw_text, question_id, image_counters): + """question_detail._process_content_for_save (regex fixed to the intact + version: (\\[\\[IMG:(.+?)\\]\\])|(\\!\\[\\]\\s*\\(\\s*(.+?)\\s*\\)))""" + text = raw_text or "" + folder_name, _ = ensure_question_folder_for_save(db_conn, media_base_dir, question_id) + pattern = re.compile(r'(\[\[IMG:(.+?)\]\])|(\!\[\]\s*\(\s*(.+?)\s*\))') + + def repl(m): + if m.group(1): + src = m.group(2).strip() + if os.path.isabs(src) and os.path.exists(src): + return save_image_for_new_or_edited_content( + media_base_dir, src, question_id, folder_name, image_counters) + return m.group(0) + + return pattern.sub(repl, text), f"Media/{folder_name}" diff --git a/backend/parsers/__init__.py b/backend/parsers/__init__.py new file mode 100644 index 0000000..f6a49b3 --- /dev/null +++ b/backend/parsers/__init__.py @@ -0,0 +1,3 @@ +# Core parsers ported 1:1 from the original desktop app +# (py/question/file_parsing/*.py in the original zip). +# The only changes: no tkinter; logs collected into a list. diff --git a/backend/parsers/zhixuewang_file_parsing.py b/backend/parsers/zhixuewang_file_parsing.py new file mode 100644 index 0000000..ee68ca8 --- /dev/null +++ b/backend/parsers/zhixuewang_file_parsing.py @@ -0,0 +1,402 @@ +# ===================== zhixuewang_file_parsing.py ===================== +# Ported 1:1 from original py/question/file_parsing/zhixuewang_file_parsing.py +# Changes: media_base_dir points to BASE_DIR (project root); no other logic changes. +import os +import re +import datetime +from docx import Document +from docx.table import Table +from docx.text.paragraph import Paragraph + + +class ZhixuewangParser: + def __init__(self, file_path, meta_data, log_callback=None, media_base_dir=None): + super().__init__() + self.file_path = file_path + self.meta_data = meta_data + self.log_callback = log_callback + + self.media_base_dir = media_base_dir or os.path.abspath( + os.path.join(os.path.dirname(__file__), "..", "..", "Media") + ) + + if not os.path.exists(self.media_base_dir): + os.makedirs(self.media_base_dir) + + def log(self, msg): + if self.log_callback: + self.log_callback(msg) + + def get_para_mixed_content(self, para, image_index): + text_content = "" + for run in para.runs: + run_text = run.text + + # --- 增强的下划线处理逻辑 --- + has_underline = False + if run.underline is True: + has_underline = True + elif run.underline is not None and run.underline != False and run.underline != 0: + has_underline = True + + if has_underline and run_text: + processed_text = "" + for char in run_text: + if char == ' ': + processed_text += '_' + elif char == '\t': + processed_text += '____' + elif char == '\u00A0': # 不间断空格 + processed_text += '_' + else: + processed_text += char + run_text = processed_text + + # --- 处理下标格式 --- + if run.font.subscript: + subscript_map = { + '0': '₀', '1': '₁', '2': '₂', '3': '₃', '4': '₄', + '5': '₅', '6': '₆', '7': '₇', '8': '₈', '9': '₉', + '+': '₊', '-': '₋', '=': '₌', '(': '₍', ')': '₎', + 'a': 'ₐ', 'e': 'ₑ', 'o': 'ₒ', 'x': 'ₓ' + } + processed_sub = "" + for char in run_text: + processed_sub += subscript_map.get(char, char) + run_text = processed_sub + + # --- 处理上标格式 --- + if run.font.superscript: + if run_text == '2': + run_text = '²' + elif run_text == '3': + run_text = '³' + # 可以根据实际情况添加更多上标字符的映射 + + if run_text: + text_content += run_text + + xml_str = run._element.xml + rids = re.findall(r'embed="(rId\d+)"', xml_str) + re.findall(r'id="(rId\d+)"', xml_str) + size_match = re.search(r'extent cx="(\d+)" cy="(\d+)"', xml_str) + + width_emu = size_match.group(1) if size_match else 0 + height_emu = size_match.group(2) if size_match else 0 + + for rid in rids: + if rid in image_index: + path = image_index[rid] + if width_emu and height_emu: + text_content += f" ![]( {path}?w={width_emu}&h={height_emu} ) " + else: + text_content += f" ![]( {path} ) " + return text_content + + def parse(self): + if not os.path.exists(self.file_path): + return None, "文件不存在" + + try: + doc = Document(self.file_path) + except Exception as e: + return None, f"Word打开失败: {e}" + + all_image_blobs = {} + for rel in doc.part.rels.values(): + if "image" in rel.target_ref: + img_name = os.path.basename(rel.target_ref) + all_image_blobs[rel.rId] = (img_name, rel.target_part.blob) + + self.log("正在预解析答案区域...") + answers_info = {} + is_in_answer_zone = False + current_ans_id = None + ans_temp_paras = [] + + answer_anchor_match_regex = re.compile(r'^([\d\.]+)\s*【(?:原错题|变式题)参考答案\s*】') + + for para in doc.paragraphs: + p_text = para.text.strip() + answer_anchor_match = answer_anchor_match_regex.match(p_text) + + if answer_anchor_match: + if current_ans_id: # 保存上一段收集到的答案 + answers_info[current_ans_id] = { + "content_paras": ans_temp_paras, + } + current_ans_id = answer_anchor_match.group(1) # 获取题号 (如 "1" 或 "1.1") + is_in_answer_zone = True + ans_temp_paras = [para] # 重置答案段落缓存,并将当前锚点段落加入 + elif is_in_answer_zone: + ans_temp_paras.append(para) + + if current_ans_id: # 循环结束后,保存最后一个答案 + answers_info[current_ans_id] = { + "content_paras": ans_temp_paras, + } + + self.log("开始拆解题目主体...") + questions_list = [] + now_ts = datetime.datetime.now().strftime("%Y%m%d%H%M%S") + + current_qid = None + collecting_q = False + q_temp_paras = [] + this_q_image_index = {} + + # 题目信息变量,用于存储当前题目的元数据 + ans_time = "" + kn_info = "" + current_json_type = self.meta_data["type"] + q_category = "客观题" # Default category + current_image_folder = "" + + last_primary_kn_info = "" + + question_start_pattern = re.compile(r'^(?:变式题\s*)?([\d\.]+)\s*[\((]\s*(主观题|客观题|巩固题|0)\s*[\))]') + + for child in doc.element.body: + element_text = "" + element_is_table = False + table_col_count = 0 + cells = None + current_paragraph_obj = None + + if child.tag.endswith('tbl'): + table = Table(child, doc) + cells = table.rows[0].cells + table_col_count = len(cells) + if table_col_count < 2: + self.log(f"DEBUG: 跳过列数不足2的表格 (len={table_col_count}).") + continue + element_text = cells[1].text.strip().replace('\xa0', ' ') + element_is_table = True + elif child.tag.endswith('p'): + para = Paragraph(child, doc) + element_text = para.text.strip() + current_paragraph_obj = para + else: + continue + + question_match = question_start_pattern.match(element_text) + + if question_match: + self.log(f"--- Detected NEW QUESTION START: '{element_text}' ---") + + if current_qid and q_temp_paras: + self.process_and_append( + questions_list, + current_qid, + q_temp_paras, + answers_info, + this_q_image_index, + kn_info, + now_ts, + q_category, + ans_time, + current_image_folder, + current_json_type + ) + + current_qid = question_match.group(1) + question_type_label_raw = question_match.group(2) + + if question_type_label_raw == "0": + actual_question_type_label = "巩固题" + else: + actual_question_type_label = question_type_label_raw + + current_json_type = "推题" if actual_question_type_label == "巩固题" else self.meta_data["type"] + q_category = "主观题" if actual_question_type_label == "主观题" else "客观题" + + ans_time = "" + kn_info = "" + + if element_is_table and table_col_count >= 3: + ans_time = cells[2].text.replace("答题时间:", "").strip() + + full_kn_text = element_text + + # 1. 提取类型括号后的主体部分 + after_bracket_match = re.search(r'[))](.*)', full_kn_text) + if after_bracket_match: + kn = after_bracket_match.group(1).strip() + else: + kn = full_kn_text + + # 2. 移除开头的 "作业 n" + kn = re.sub(r'^(作业|练习)\s*\d*\s*', '', kn).strip() + + # 3. 移除知识点第一个文字前的数字(如 8.4, 8.2 等) + kn = re.sub(r'^[\d\.\s]+', '', kn).strip() + + # 4. 移除绝对位于末尾的括号内容 (xxx) + kn = re.sub(r'[((][^(())]+[))]$', '', kn).strip() + + # 5. 移除班级信息 + kn_info = re.split(r'\d+-\d+班', kn)[0].strip() + + last_primary_kn_info = kn_info + + elif actual_question_type_label == "巩固题": + kn_info = last_primary_kn_info + self.log(f"DEBUG: 巩固题 '{current_qid}' 继承知识点: '{kn_info}'") + + folder_name = f"{now_ts}-{current_qid}" + current_image_folder = folder_name + target_media_dir = os.path.join(self.media_base_dir, folder_name) + os.makedirs(target_media_dir, exist_ok=True) + + this_q_image_index = {} + for rid, (name, blob) in all_image_blobs.items(): + img_save_path = os.path.join(target_media_dir, name) + with open(img_save_path, "wb") as f: + f.write(blob) + this_q_image_index[rid] = os.path.join("Media", folder_name, name) + + q_temp_paras = [] + collecting_q = True + self.log(f"解析到题目: {current_qid} (类型: {current_json_type}, 分类: {q_category}) - 源: {'表格' if element_is_table else '段落'}") + if actual_question_type_label == "巩固题": + self.log(f"---识别到巩固题: {current_qid} (成功匹配到正则) ---") + + continue + + elif collecting_q and element_is_table: + q_temp_paras.append(table) + + elif collecting_q and current_paragraph_obj: + if ("参考答案" in element_text and len(element_text) < 15) or \ + answer_anchor_match_regex.match(element_text): + self.log(f"--- 停止收集 {current_qid} 的题干,检测到答案标记: '{element_text}' ---") + collecting_q = False + continue + q_temp_paras.append(current_paragraph_obj) + + if current_qid and q_temp_paras: + self.process_and_append( + questions_list, + current_qid, + q_temp_paras, + answers_info, + this_q_image_index, + kn_info, + now_ts, + q_category, + ans_time, + current_image_folder, + current_json_type + ) + + return questions_list, None + + def process_and_append(self, q_list, qid, q_paras, answers_info_map, img_idx, kn, ts, qt, at, img_folder_name, custom_question_type): + """ + 处理单道题目的数据并添加到列表。 + 新增逻辑:根据答案内容判断客观题是单选还是多选,并修改 q_content。 + """ + # 1. 构建题目内容 (q_content) + content_parts = [] + for p in q_paras: + if isinstance(p, Table): + for row in p.rows: + row_text_parts = [] + for cell in row.cells: + for para in cell.paragraphs: + cell_text = self.get_para_mixed_content(para, img_idx).strip() + if cell_text: + row_text_parts.append(cell_text) + if row_text_parts: + content_parts.append(" | ".join(row_text_parts)) + else: + content_parts.append(self.get_para_mixed_content(p, img_idx)) + q_content = "\n".join(content_parts).strip() + + # 2. 构建答案内容 (ans_content) + ans_content = "" + answer_data = answers_info_map.get(qid) + + # 定义一个变量来存储答案选项行,用于后续判断单选/多选 + answer_option_line_text = "" + + if answer_data and answer_data["content_paras"]: + ans_content_paras = answer_data["content_paras"] + + # 从原始段落列表中构建 ans_content + ans_content = "\n".join([self.get_para_mixed_content(p, img_idx) for p in ans_content_paras]).strip() + + # --- 修正逻辑:查找实际的答案选项行 --- + answer_anchor_match_regex = re.compile(r'^[\d\.]+\s*【(?:原错题|变式题)参考答案\s*】') + + first_effective_para_index = -1 + for i, para in enumerate(ans_content_paras): + para_text_raw = para.text.strip() + if not para_text_raw: # 跳过空段落 + continue + if answer_anchor_match_regex.match(para_text_raw): # 跳过锚点段落 + self.log(f"DEBUG: QID {qid}: 跳过答案锚点行: '{para_text_raw}'") + continue + + first_effective_para_index = i + answer_option_line_text = self.get_para_mixed_content(para, img_idx).strip() + self.log(f"DEBUG: QID {qid}: 找到答案选项行内容: '{answer_option_line_text}' (来自段落索引 {i})") + break + + if first_effective_para_index == -1 and ans_content_paras: + self.log(f"DEBUG: QID {qid}: 未找到明确的答案选项段落,回退到使用ans_content第一行尝试匹配。") + first_significant_ans_line_from_content = "" + for line in ans_content.split('\n'): + stripped_line = line.strip() + if stripped_line: + first_significant_ans_line_from_content = stripped_line + break + answer_option_line_text = first_significant_ans_line_from_content + + else: + self.log(f"DEBUG: 题号 '{qid}' 未在答案字典中找到对应答案或答案内容为空。") + + # 3. 客观题的单选/多选判断 + objective_type_prefix = "" + if qt == "客观题": # 如果是客观题 + self.log(f"DEBUG: QID {qid} 是客观题,尝试判断单选/多选。") + if answer_option_line_text: + options_match = re.match(r'^([A-D]+)(?:\s*\.|\s|$)', answer_option_line_text, re.IGNORECASE) + + if options_match: + options_string = options_match.group(1).upper() + if len(options_string) == 1: + objective_type_prefix = "【单选题】" + self.log(f"DEBUG: QID {qid}: 识别为单选题 ({options_string}).") + elif len(options_string) > 1: + objective_type_prefix = "【多选题】" + self.log(f"DEBUG: QID {qid}: 识别为多选题 ({options_string}).") + else: + self.log(f"DEBUG: QID {qid}: 选项字符串长度为0,未识别。") + else: + self.log(f"DEBUG: QID {qid}: 答案选项行 '{answer_option_line_text}' 未以有效选项 (A-D) 开头,未识别单选/多选。") + else: + self.log(f"DEBUG: QID {qid}: 答案选项行内容为空,无法识别单选/多选。") + else: + self.log(f"DEBUG: QID {qid} 为 '{qt}',跳过单选/多选判断。") + + # 4. 将确定的客观题类型前缀加入题干内容 + q_content = objective_type_prefix + q_content + self.log(f"DEBUG: QID {qid} 最终题干前缀: '{objective_type_prefix}'.") + + # 5. 追加到 questions_list + q_list.append({ + "题号": qid, + "科目": self.meta_data["subject"], + "题目来源": self.meta_data["source"], + "题目类型": custom_question_type, + "输入时间": self.meta_data["import_time"], + "答题时间": at, + "题目知识点": kn, + "题目分类": qt, # qt 仍然是 "客观题" 或 "主观题" + "题干内容": q_content, # 已经加入了单选/多选前缀 + "答案解析": ans_content, + "学期": self.meta_data.get("term"), + "阶段": self.meta_data.get("stage"), + "题目出处": self.meta_data.get("origin"), + "图片目录": os.path.join("Media", img_folder_name) + }) diff --git a/backend/parsers/zizuword_file_parsing.py b/backend/parsers/zizuword_file_parsing.py new file mode 100644 index 0000000..befb14b --- /dev/null +++ b/backend/parsers/zizuword_file_parsing.py @@ -0,0 +1,329 @@ +# ===================== zizuword_file_parsing.py ===================== +# Ported 1:1 from original py/question/file_parsing/zizuword_file_parsing.py +# Changes: media_base_dir points to BASE_DIR (project root); no other logic changes. +import os +import re +import datetime +from docx import Document +from docx.text.paragraph import Paragraph + + +class ZizuWordParser: + """ + 自组WORD解析器 + + 设计目标: + - 初始化、日志、打开 Word、Media 目录处理、JSON 结构字段命名等, + 与 ZhixuewangParser 保持一致风格; + - 内容解析规则按题述的自组 Word 规范实现。 + """ + + def __init__(self, file_path, meta_data, log_callback=None, media_base_dir=None): + super().__init__() + self.file_path = file_path + self.meta_data = meta_data + self.log_callback = log_callback + + self.media_base_dir = media_base_dir or os.path.abspath( + os.path.join(os.path.dirname(__file__), "..", "..", "Media") + ) + + if not os.path.exists(self.media_base_dir): + os.makedirs(self.media_base_dir) + + # -------------------- 日志 -------------------- + + def log(self, msg): + if self.log_callback: + self.log_callback(msg) + + # -------------------- 段落文本 + 图片混合内容提取 -------------------- + + def get_para_mixed_content(self, para, image_index): + """ + 参考 zhixuewang_file_parsing.py 的实现: + - 处理下划线(填空题); + - 将段落中的内嵌图片转为 Markdown 标记:![]( path?w=...&h=... )。 + """ + text_content = "" + for run in para.runs: + run_text = run.text + + # --- 下划线处理逻辑(与 ZhixuewangParser 一致) --- + has_underline = False + if run.underline is True: + has_underline = True + elif run.underline is not None and run.underline != False and run.underline != 0: + has_underline = True + + if has_underline and run_text: + processed_text = "" + for char in run_text: + if char == ' ': + processed_text += '_' + elif char == '\t': + processed_text += '____' + elif char == '\u00A0': # 不间断空格 + processed_text += '_' + else: + processed_text += char + run_text = processed_text + + if run_text: + text_content += run_text + + # --- 图片信息提取 --- + xml_str = run._element.xml + rids = re.findall(r'embed="(rId\d+)"', xml_str) + re.findall(r'id="(rId\d+)"', xml_str) + size_match = re.search(r'extent cx="(\d+)" cy="(\d+)"', xml_str) + + width_emu = size_match.group(1) if size_match else 0 + height_emu = size_match.group(2) if size_match else 0 + + for rid in rids: + if rid in image_index: + path = image_index[rid] + if width_emu and height_emu: + text_content += f" ![]( {path}?w={width_emu}&h={height_emu} ) " + else: + text_content += f" ![]( {path} ) " + return text_content + + # -------------------- 图片目录处理 -------------------- + + def _build_image_index_for_question(self, all_image_blobs, now_ts, qid_str): + """ + 为每道题目复制当前文档所有图片到 Media/{now_ts}-{qid}/ 目录, + 并返回 image_index 映射和 folder_name。 + """ + folder_name = f"{now_ts}-{qid_str}" + target_media_dir = os.path.join(self.media_base_dir, folder_name) + os.makedirs(target_media_dir, exist_ok=True) + + image_index = {} + for rid, (img_name, blob) in all_image_blobs.items(): + img_save_path = os.path.join(target_media_dir, img_name) + with open(img_save_path, "wb") as f: + f.write(blob) + image_index[rid] = os.path.join("Media", folder_name, img_name) + + return image_index, folder_name + + def _append_question_json( + self, + questions_list, + qid_str, + q_type, + q_category, + kp_text, + stem_lines, + ans_lines, + folder_name + ): + """ + 将一题的解析结果按照统一 JSON 结构追加到 questions_list 中。 + """ + q_content = "\n".join(stem_lines).strip() + ans_content = "\n".join(ans_lines).strip() + + now_input_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + questions_list.append({ + "题号": qid_str, + "科目": self.meta_data.get("subject", ""), + "题目来源": "自组WORD", + "题目类型": q_type, + "输入时间": now_input_time, + "答题时间": "", + "题目知识点": kp_text, + "题目分类": q_category, + "题干内容": q_content, + "答案解析": ans_content, + "学期": self.meta_data.get("term"), + "阶段": self.meta_data.get("stage"), + "题目出处": self.meta_data.get("origin"), + "图片目录": os.path.join("Media", folder_name) + }) + + # -------------------- 主解析逻辑 -------------------- + + def parse(self): + """ + 解析自组 Word 文档: + - 当段落中出现“【错题】”或“【推题】”时识别为一道新题目; + - 结构分块: + 【错题】/【推题】 [【主观题】/【客观题】] # 题目头行 + 【知识点】... # 知识点行 + 【题干】... # 题干起始行 + ... # 题干后续行 + 【答案】... # 答案起始行 + ... # 答案后续行,直到下一个题目头或文档结束 + """ + if not os.path.exists(self.file_path): + return None, "文件不存在" + + try: + doc = Document(self.file_path) + except Exception as e: + return None, f"Word打开失败: {e}" + + # 收集文档中所有图片的 blob,与 rId 对应(与 zhixuewang 相同) + all_image_blobs = {} + for rel in doc.part.rels.values(): + if "image" in rel.target_ref: + img_name = os.path.basename(rel.target_ref) + all_image_blobs[rel.rId] = (img_name, rel.target_part.blob) + + self.log("开始拆解自组WORD题目...") + questions_list = [] + now_ts = datetime.datetime.now().strftime("%Y%m%d%H%M%S") + + # 状态变量 + current_qid_int = 0 # 题号(从1开始自增) + current_q_type = None # "错题" / "推题" + current_category = None # "主观题" / "客观题" + current_kp_text = "" # 知识点 + current_stem_lines = [] # 题干内容行(已含图片标记) + current_ans_lines = [] # 答案内容行(已含图片标记) + current_image_index = {} # 当前题目的 rId->路径 映射 + current_folder_name = "" # Media 子目录名 + + in_stem = False # 当前是否在题干区域 + in_answer = False # 当前是否在答案区域 + + def flush_current_question(): + """结束当前题目时,将其追加到 questions_list。""" + nonlocal current_qid_int, current_q_type, current_category + nonlocal current_kp_text, current_stem_lines, current_ans_lines + nonlocal current_folder_name + + if current_qid_int == 0: + return + qid_str = str(current_qid_int) + if not current_q_type: + # 未找到【错题】或【推题】类型,则跳过 + return + if not current_category: + # 未找到主观/客观分类,则默认为客观题 + current_category_local = "客观题" + else: + current_category_local = current_category + + self._append_question_json( + questions_list, + qid_str=qid_str, + q_type=current_q_type, + q_category=current_category_local, + kp_text=current_kp_text, + stem_lines=current_stem_lines, + ans_lines=current_ans_lines, + folder_name=current_folder_name + ) + + # 正则:题目头:“【错题】”或“【推题】”,可选地跟一个“【主观题】/【客观题】” + header_pattern = re.compile(r'^(.*?)(【错题】|【推题】)(.*)$') + category_pattern = re.compile(r'【(主观题|客观题)】') + + for para in doc.paragraphs: + if not isinstance(para, Paragraph): + continue + + raw_text = para.text or "" + p_text = raw_text.strip() + + # 是否为题目头? + header_match = header_pattern.search(p_text) + if header_match: + # 刷新上一题 + flush_current_question() + + # 新题号 + current_qid_int += 1 + qid_str = str(current_qid_int) + + # 为本题建立图片目录和 image_index + current_image_index, current_folder_name = self._build_image_index_for_question( + all_image_blobs, now_ts, qid_str + ) + + # 重置题内容 + current_stem_lines = [] + current_ans_lines = [] + current_kp_text = "" + in_stem = False + in_answer = False + + # 题目类型:错题/推题 + type_token = header_match.group(2) + if "错题" in type_token: + current_q_type = "错题" + else: + current_q_type = "推题" + + # 题目分类:主观题/客观题(若存在) + cat_match = category_pattern.search(p_text) + if cat_match: + current_category = cat_match.group(1) + else: + current_category = None # 若未写,则后续 flush 时默认客观题 + + self.log(f"检测到新题目: 题号={qid_str}, 类型={current_q_type}, 分类={current_category or '未显式标注'}") + # 题目头本身不计入题干,继续下一段 + continue + + # 若尚未进入任何题目(还没遇到第一个【错题】/【推题】),则跳过 + if current_qid_int == 0: + continue + + # 处理知识点、题干、答案区 + # 优先检测【知识点】 + if p_text.startswith("【知识点】"): + kp_raw = p_text.replace("【知识点】", "", 1).strip() + if kp_raw: + current_kp_text = kp_raw + else: + current_kp_text = "" + continue + + # 再检测【题干】 + if p_text.startswith("【题干】"): + in_stem = True + in_answer = False + after = p_text.replace("【题干】", "", 1).strip() + if after: + mixed = self.get_para_mixed_content(para, current_image_index) + if mixed.startswith("【题干】"): + mixed = mixed.replace("【题干】", "", 1).strip() + current_stem_lines.append(mixed) + continue + + # 再检测【答案】 + if p_text.startswith("【答案】"): + in_stem = False + in_answer = True + after = p_text.replace("【答案】", "", 1).strip() + if after: + mixed = self.get_para_mixed_content(para, current_image_index) + if mixed.startswith("【答案】"): + mixed = mixed.replace("【答案】", "", 1).strip() + current_ans_lines.append(mixed) + continue + + # 其他普通段落,根据当前状态追加到题干或答案 + mixed = self.get_para_mixed_content(para, current_image_index) + mixed = mixed.strip() + if not mixed: + continue + + if in_answer: + current_ans_lines.append(mixed) + elif in_stem: + current_stem_lines.append(mixed) + else: + # 若尚未进入题干/答案区,但已经在题目下,可以选择默认进入题干 + current_stem_lines.append(mixed) + + # 文档结束后,刷新最后一题 + flush_current_question() + + return questions_list, None diff --git a/backend/parsers/zujuanwang_file_parsing.py b/backend/parsers/zujuanwang_file_parsing.py new file mode 100644 index 0000000..ba80ad3 --- /dev/null +++ b/backend/parsers/zujuanwang_file_parsing.py @@ -0,0 +1,439 @@ +# ===================== zujuanwang_file_parsing.py ===================== +# Ported 1:1 from original py/question/file_parsing/zujuanwang_file_parsing.py +# Changes: +# - no tkinter / messagebox (the format notice is shown by the web frontend); +# - WMF->PNG conversion: original used macOS `sips`; on Linux we try +# `soffice`/`convert` if available, otherwise keep the .wmf exactly like +# the original's fallback branch. +import os +import re +import datetime +import shutil +import subprocess +from docx import Document +from docx.text.paragraph import Paragraph + + +def _try_convert_wmf_to_png(wmf_path, png_path): + """Linux replacement for macOS `sips -s format png` (best effort).""" + try: + if shutil.which("soffice"): + subprocess.run( + ["soffice", "--headless", "--convert-to", "png", "--outdir", + os.path.dirname(png_path), wmf_path], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=30) + converted = os.path.splitext(wmf_path)[0] + ".png" + if os.path.exists(converted) and os.path.abspath(converted) != os.path.abspath(png_path): + shutil.move(converted, png_path) + if os.path.exists(png_path): + return True + if shutil.which("convert"): + subprocess.run(["convert", wmf_path, png_path], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=30) + if os.path.exists(png_path): + return True + except Exception: + pass + return False + + +class ZujuanwangParser: + def __init__(self, file_path, meta_data, log_callback=None, media_base_dir=None): + super().__init__() + self.file_path = file_path + self.meta_data = meta_data + self.log_callback = log_callback + + self.media_base_dir = media_base_dir or os.path.abspath( + os.path.join(os.path.dirname(__file__), "..", "..", "Media") + ) + + if not os.path.exists(self.media_base_dir): + os.makedirs(self.media_base_dir) + + def log(self, msg): + if self.log_callback: + self.log_callback(msg) + + def get_para_mixed_content(self, para, image_index, trim_leading_text_length=0): + """ + 从段落中提取文本内容,包括图片占位符和下划线处理,并支持裁剪开头的纯文本。 + trim_leading_text_length: 从段落开头裁剪掉的纯文本字符数。 + """ + content_parts = [] + current_pure_text_pos = 0 # Tracks the length of pure text encountered so far (original text) + para_images_added = set() # Record rIds of images already added for this paragraph to prevent duplicates + + W_NAMESPACE = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" + A_NAMESPACE = "http://schemas.openxmlformats.org/drawingml/2006/main" + WP_NAMESPACE = "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" + R_NAMESPACE_ATTR = "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + V_NAMESPACE = "urn:schemas-microsoft-com:vml" + + for run in para.runs: + original_run_text = run.text + + # --- 1. Process Text Content with Trimming and Underline --- + processed_text_for_run = "" + if current_pure_text_pos < trim_leading_text_length: + remaining_trim_for_this_run = trim_leading_text_length - current_pure_text_pos + + if len(original_run_text) <= remaining_trim_for_this_run: + processed_text_for_run = "" + else: + processed_text_for_run = original_run_text[remaining_trim_for_this_run:] + else: + processed_text_for_run = original_run_text + + current_pure_text_pos += len(original_run_text) + + # Apply underline logic to the processed text (after trimming) + has_underline = False + if run.underline is True: + has_underline = True + elif run.underline is not None and run.underline != False and run.underline != 0: + has_underline = True + + if has_underline and processed_text_for_run: + temp_text_with_underline = "" + for char in processed_text_for_run: + if char == ' ': + temp_text_with_underline += '_' + elif char == '\t': + temp_text_with_underline += '____' + elif char == '\u00A0': # Non-breaking space + temp_text_with_underline += '_' + else: + temp_text_with_underline += char + processed_text_for_run = temp_text_with_underline + + if processed_text_for_run: + content_parts.append(processed_text_for_run) + + # --- 2. Process Image Content (Independent of text trimming) --- + + # A. Find elements + for drawing in run._element.findall(f'.//{{{W_NAMESPACE}}}drawing'): + for blip in drawing.findall(f'.//{{{A_NAMESPACE}}}blip'): + r_id = blip.get(f'{{{R_NAMESPACE_ATTR}}}embed') or blip.get(f'{{{R_NAMESPACE_ATTR}}}id') + if r_id and r_id in image_index and r_id not in para_images_added: + path = image_index[r_id] + extent_element = drawing.find(f'.//{{{WP_NAMESPACE}}}extent') + + width_emu = 0 + height_emu = 0 + if extent_element is not None: + width_emu = int(extent_element.get('cx', '0')) + height_emu = int(extent_element.get('cy', '0')) + + if width_emu and height_emu: + content_parts.append(f" ![]( {path}?w={width_emu}&h={height_emu} ) ") + else: + content_parts.append(f" ![]( {path} ) ") + para_images_added.add(r_id) + + # B. Find or elements (Critical for X, Y, Z images) + for imagedata in run._element.findall(f'.//{{{V_NAMESPACE}}}imagedata'): + r_id = imagedata.get(f'{{{R_NAMESPACE_ATTR}}}id') or imagedata.get(f'{{{R_NAMESPACE_ATTR}}}relid') + if r_id and r_id in image_index and r_id not in para_images_added: + path = image_index[r_id] + content_parts.append(f" ![]( {path} ) ") + para_images_added.add(r_id) + + return "".join(content_parts).strip() + + def parse(self): + if not os.path.exists(self.file_path): + return None, "文件不存在" + + try: + doc = Document(self.file_path) + except Exception as e: + return None, f"Word打开失败: {e}" + + all_image_blobs = {} + for rel in doc.part.rels.values(): + if "image" in rel.target_ref: + img_name = os.path.basename(rel.target_ref) + all_image_blobs[rel.rId] = (img_name, rel.target_part.blob) + + # ------------------------------------------------------- + # 第一阶段:预解析所有答案区域,收集答案内容和知识点 + # ------------------------------------------------------- + self.log("正在预解析答案区域,收集答案内容和知识点...") + answers_info = {} # {qid: {"content_paras": [para_objects], "knowledge_points": "..."}} + + ans_start_pattern = re.compile(r'^【答案】(\d+(?:\.\d+)*)\s*(.*)') + + current_ans_qid = None + current_ans_content_paras = [] + current_knowledge_points = [] + + end_of_doc_summary_pattern = re.compile(r'^《.*》参考答案$') + + for para in doc.paragraphs: + p_text = para.text.strip() + + if end_of_doc_summary_pattern.match(p_text): + if current_ans_qid: + answers_info[current_ans_qid] = { + "content_paras": list(current_ans_content_paras), + "knowledge_points": ", ".join(current_knowledge_points).strip() + } + current_ans_qid = None + current_ans_content_paras = [] + current_knowledge_points = [] + continue + + ans_match = ans_start_pattern.match(p_text) + + if ans_match: + if current_ans_qid: + answers_info[current_ans_qid] = { + "content_paras": list(current_ans_content_paras), + "knowledge_points": ", ".join(current_knowledge_points).strip() + } + + current_ans_qid = ans_match.group(1) + + current_ans_content_paras = [para] + current_knowledge_points = [] + + kn_match = re.search(r'【知识点】\s*(.*)', p_text) + if kn_match: + kps = [kp.strip() for kp in kn_match.group(1).split(',') if kp.strip()] + current_knowledge_points.extend(kps) + + elif current_ans_qid: + if re.match(r'^(【错题】|【推题】)\s*(?:【(主观题|客观题)】\s*)?\d+(?:\.\d+)*', p_text): + if current_ans_qid: + answers_info[current_ans_qid] = { + "content_paras": list(current_ans_content_paras), + "knowledge_points": ", ".join(current_knowledge_points).strip() + } + current_ans_qid = None + current_ans_content_paras = [] + current_knowledge_points = [] + continue + + current_ans_content_paras.append(para) + + kn_match = re.search(r'【知识点】\s*(.*)', p_text) + if kn_match: + kps = [kp.strip() for kp in kn_match.group(1).split(',') if kp.strip()] + current_knowledge_points.extend(kps) + + if current_ans_qid: + answers_info[current_ans_qid] = { + "content_paras": list(current_ans_content_paras), + "knowledge_points": ", ".join(current_knowledge_points).strip() + } + + self.log(f"答案预解析完成。找到 {len(answers_info)} 个答案块。") + + # ------------------------------------------------------- + # 第二阶段:遍历文档,提取题目信息 + # ------------------------------------------------------- + self.log("开始拆解题目主体...") + questions_list = [] + now_ts = datetime.datetime.now().strftime("%Y%m%d%H%M%S") + + current_qid = None + current_q_type = None # "错题" or "推题" + current_q_category = None # "主观题" or "客观题" + collecting_q_body = False + q_temp_paras = [] + current_image_folder = "" + this_q_image_index = {} + + question_start_pattern = re.compile(r'^(【错题】|【推题】)\s*(?:【(主观题|客观题)】\s*)?(\d+(?:\.\d+)*)\s*(.*)') + + for para in doc.paragraphs: + p_text = para.text.strip() + + if end_of_doc_summary_pattern.match(p_text): + if collecting_q_body and current_qid and q_temp_paras: + self._process_and_append_question( + questions_list, + current_qid, + q_temp_paras, + answers_info.get(current_qid, {"content_paras": [], "knowledge_points": ""}), + this_q_image_index, + now_ts, + current_q_type, + current_q_category, + current_image_folder + ) + current_qid = None + current_q_type = None + current_q_category = None + collecting_q_body = False + q_temp_paras = [] + continue + + q_start_match = question_start_pattern.match(p_text) + + if q_start_match: + self.log(f"--- Detected NEW QUESTION START: '{p_text}' ---") + + if collecting_q_body and current_qid and q_temp_paras: + self._process_and_append_question( + questions_list, + current_qid, + q_temp_paras, + answers_info.get(current_qid, {"content_paras": [], "knowledge_points": ""}), + this_q_image_index, + now_ts, + current_q_type, + current_q_category, + current_image_folder + ) + + current_q_type_tag = q_start_match.group(1) + current_q_category_tag = q_start_match.group(2) + current_qid = q_start_match.group(3) + + current_q_type = current_q_type_tag.strip('【】') + current_q_category = current_q_category_tag.strip('【】') if current_q_category_tag else "" + + folder_name = f"{now_ts}-{current_qid}" + current_image_folder = folder_name + target_media_dir = os.path.join(self.media_base_dir, folder_name) + os.makedirs(target_media_dir, exist_ok=True) + + this_q_image_index = {} + for rid, (name, blob) in all_image_blobs.items(): + base_name, ext = os.path.splitext(name) + img_save_path = os.path.join(target_media_dir, name) + + if ext.lower() == '.wmf': + # 1. 写原始 wmf + with open(img_save_path, "wb") as f: + f.write(blob) + + png_name = base_name + ".png" + png_save_path = os.path.join(target_media_dir, png_name) + + # 2. 尝试转换(Linux 下尝试 soffice/convert,失败回退 wmf) + converted = _try_convert_wmf_to_png(img_save_path, png_save_path) + + # 3. 如果 png 生成成功,索引必须指向 png + if converted and os.path.exists(png_save_path): + this_q_image_index[rid] = os.path.join("Media", folder_name, png_name).replace("\\", "/") + else: + this_q_image_index[rid] = os.path.join("Media", folder_name, name).replace("\\", "/") + else: + with open(img_save_path, "wb") as f: + f.write(blob) + this_q_image_index[rid] = os.path.join("Media", folder_name, name).replace("\\", "/") + + q_temp_paras = [para] + collecting_q_body = True + + self.log(f"解析到题目: {current_qid} (类型: {current_q_type}, 分类: {current_q_category})") + + elif collecting_q_body: + ans_match = re.match(r'^【答案】(\d+(?:\.\d+)*)', p_text) + if ans_match: + self.log(f"--- Stopping collection for QID {current_qid}, detected answer tag: '{p_text}' ---") + if current_qid and q_temp_paras: + self._process_and_append_question( + questions_list, + current_qid, + q_temp_paras, + answers_info.get(current_qid, {"content_paras": [], "knowledge_points": ""}), + this_q_image_index, + now_ts, + current_q_type, + current_q_category, + current_image_folder + ) + current_qid = None + current_q_type = None + current_q_category = None + collecting_q_body = False + q_temp_paras = [] + continue + + q_temp_paras.append(para) + + if collecting_q_body and current_qid and q_temp_paras: + self._process_and_append_question( + questions_list, + current_qid, + q_temp_paras, + answers_info.get(current_qid, {"content_paras": [], "knowledge_points": ""}), + this_q_image_index, + now_ts, + current_q_type, + current_q_category, + current_image_folder + ) + + return questions_list, None + + def _process_and_append_question(self, q_list, qid, q_paras_raw, ans_data, img_idx, ts, q_type, q_category, img_folder_name): + question_content_parts = [] + + if q_paras_raw: + first_para = q_paras_raw[0] + p_text = first_para.text + prefix_match = re.match(r'^(【错题】|【推题】)\s*(?:【(主观题|客观题)】\s*)?(\d+(?:\.\d+)*)\s*', p_text) + trim_length = 0 + if prefix_match: + trim_length = len(prefix_match.group(0)) + + content_after_prefix = self.get_para_mixed_content(first_para, img_idx, trim_leading_text_length=trim_length) + if content_after_prefix: + question_content_parts.append(content_after_prefix) + + for p_idx, p in enumerate(q_paras_raw): + if p_idx == 0: + continue + content = self.get_para_mixed_content(p, img_idx) + if content: + question_content_parts.append(content) + + question_content = "\n".join(filter(None, question_content_parts)).strip() + + answer_content_parts = [] + answer_paras = ans_data.get("content_paras", []) + knowledge_points = ans_data.get("knowledge_points", "") + + if answer_paras: + first_ans_para = answer_paras[0] + ans_text = first_ans_para.text + ans_prefix_match = re.match(r'^【答案】\d+(?:\.\d+)*\s*', ans_text) + ans_trim_length = 0 + if ans_prefix_match: + ans_trim_length = len(ans_prefix_match.group(0)) + + content_after_ans_prefix = self.get_para_mixed_content(first_ans_para, img_idx, trim_leading_text_length=ans_trim_length) + if content_after_ans_prefix: + answer_content_parts.append(content_after_ans_prefix) + + for p_idx, p in enumerate(answer_paras): + if p_idx == 0: + continue + content = self.get_para_mixed_content(p, img_idx) + if content: + answer_content_parts.append(content) + + answer_content = "\n".join(filter(None, answer_content_parts)).strip() + + q_list.append({ + "题号": qid, + "科目": self.meta_data["subject"], + "题目来源": "组卷网", # Fixed as "组卷网" + "题目类型": q_type, # "错题" or "推题" + "输入时间": self.meta_data["import_time"], + "答题时间": "", # Empty + "题目知识点": knowledge_points, + "题目分类": q_category, # "主观题" or "客观题" + "题干内容": question_content, + "答案解析": answer_content, + "学期": self.meta_data.get("term"), + "阶段": self.meta_data.get("stage"), + "题目出处": self.meta_data.get("origin"), + "图片目录": os.path.join("Media", img_folder_name).replace("\\", "/") + }) diff --git a/custom/conf/app.ini b/custom/conf/app.ini deleted file mode 100644 index b3e5be5..0000000 --- a/custom/conf/app.ini +++ /dev/null @@ -1,2 +0,0 @@ -[server] -LOCAL_ROOT_URL = http://localhost:4999/ diff --git a/database.py b/database.py deleted file mode 100644 index ec5e7c7..0000000 --- a/database.py +++ /dev/null @@ -1,28 +0,0 @@ -import sqlite3 -import os - -# 数据库文件的绝对路径 -DB_PATH = os.path.join(os.path.dirname(__file__), 's', 'studybase.db') - -def get_db_connection(): - """获取数据库连接,并将结果集设置为字典模式(方便前端调用)""" - conn = sqlite3.connect(DB_PATH) - # 这行代码能让你通过 row['column_name'] 访问数据,而不是 row[0] - conn.row_factory = sqlite3.Row - return conn - -def query_db(query, args=(), one=False): - """通用的查询函数""" - conn = get_db_connection() - cur = conn.execute(query, args) - rv = cur.fetchall() - conn.close() - return (rv[0] if rv else None) if one else rv - -def execute_db(query, args=()): - """通用的增删改函数""" - conn = get_db_connection() - cur = conn.execute(query, args) - conn.commit() - conn.close() - return cur diff --git a/frontend/dist/assets/AnalysisChapters-DTn9LWoa.css b/frontend/dist/assets/AnalysisChapters-DTn9LWoa.css new file mode 100644 index 0000000..31284c5 --- /dev/null +++ b/frontend/dist/assets/AnalysisChapters-DTn9LWoa.css @@ -0,0 +1 @@ +.center[data-v-ecc11515]{text-align:center}.link[data-v-ecc11515]{color:#1890ff;cursor:pointer} diff --git a/frontend/dist/assets/AnalysisChapters-rXnOtC4r.js b/frontend/dist/assets/AnalysisChapters-rXnOtC4r.js new file mode 100644 index 0000000..a4b2eb3 --- /dev/null +++ b/frontend/dist/assets/AnalysisChapters-rXnOtC4r.js @@ -0,0 +1 @@ +import{_ as $,b as A,c as a,a as e,w as p,v as E,F as m,r as h,g as f,d as o,l as B,o as u,t as l}from"./index-Dc_QwQl6.js";import{a as U,b as I,d as M}from"./api-gDOaCrl1.js";const F={class:"page-header"},K={class:"filter-panel"},G={class:"row"},L=["value"],N={class:"row"},P={class:"content-area"},Q={class:"card",style:{padding:"0",overflow:"auto"}},R={class:"data-table"},z=["onClick"],H={class:"center"},J={class:"center"},O={class:"center"},T=["onClick"],W=["onClick"],X=["onClick"],Y=["onClick"],Z={__name:"AnalysisChapters",setup(tt){const g=B(),x=o([]),_=o("全部"),v=o(""),y=o(""),k=o(""),b=o([]),r=o("not_correct_questions"),i=o("DESC"),D=[{key:"id",label:"ID"},{key:"subject",label:"科目"},{key:"volume_name",label:"册"},{key:"chapter_desc",label:"章节"},{key:"total_wrong_questions",label:"错题数量"},{key:"correct_questions",label:"已正确题目数"},{key:"not_correct_questions",label:"尚未正确题目数"}];function V(n){return n!==r.value?"":i.value==="ASC"?" ▲":" ▼"}function j(n){r.value===n?i.value=i.value==="ASC"?"DESC":"ASC":(r.value=n,i.value="ASC"),c()}function q(){return{subject:_.value,correct_count:v.value,not_correct_count:y.value,chapter_desc:k.value,sort_column:r.value,sort_direction:i.value}}async function c(){const n=await I("/analysis/chapters",q());b.value=n.rows}function w(n){g.push({path:"/analysis/knowledge",query:{chapter_id:n}})}function C(n,s){const t={chapter_id:n};s&&(t.right_times=s),g.push({path:"/questions",query:t})}async function S(){b.value.length&&await M("/analysis/chapters/export-excel",q())}return A(async()=>{const n=await U("/meta/subjects");x.value=n.subjects,await c()}),(n,s)=>(u(),a("div",null,[e("div",F,[s[5]||(s[5]=e("span",{class:"title"},"章节分析",-1)),e("button",{class:"btn",style:{"margin-left":"16px"},onClick:s[0]||(s[0]=t=>n.$router.push("/subject-portal"))},"返回科目首页"),e("button",{class:"btn orange",onClick:S},"📥 导出Excel")]),e("div",K,[e("div",G,[s[7]||(s[7]=e("label",null,"科目:",-1)),p(e("select",{"onUpdate:modelValue":s[1]||(s[1]=t=>_.value=t),onChange:c},[s[6]||(s[6]=e("option",null,"全部",-1)),(u(!0),a(m,null,h(x.value,t=>(u(),a("option",{key:t,value:t},l(t),9,L))),128))],544),[[E,_.value]]),s[8]||(s[8]=e("label",{style:{"margin-left":"10px"}},"已正确题目数:",-1)),p(e("input",{type:"text","onUpdate:modelValue":s[2]||(s[2]=t=>v.value=t),style:{width:"70px"}},null,512),[[f,v.value]]),s[9]||(s[9]=e("label",{style:{"margin-left":"10px"}},"尚未正确题目数:",-1)),p(e("input",{type:"text","onUpdate:modelValue":s[3]||(s[3]=t=>y.value=t),style:{width:"70px"}},null,512),[[f,y.value]])]),e("div",N,[s[10]||(s[10]=e("label",null,"章节名称:",-1)),p(e("input",{type:"text","onUpdate:modelValue":s[4]||(s[4]=t=>k.value=t),style:{width:"340px"}},null,512),[[f,k.value]]),e("button",{class:"btn primary",style:{"margin-left":"auto"},onClick:c},"🔍 开始搜索")])]),e("div",P,[e("div",Q,[e("table",R,[e("thead",null,[e("tr",null,[(u(),a(m,null,h(D,t=>e("th",{key:t.key,onClick:d=>j(t.key)},l(t.label)+l(V(t.key)),9,z)),64))])]),e("tbody",null,[(u(!0),a(m,null,h(b.value,t=>(u(),a("tr",{key:t.chapter_id},[e("td",H,l(t.chapter_id),1),e("td",J,l(t.subject),1),e("td",O,l(t.volume_name),1),e("td",{class:"link",onClick:d=>w(t.chapter_id)},l(t.chapter_desc),9,T),e("td",{class:"center link",onClick:d=>C(t.chapter_id,null)},l(t.total_wrong_questions),9,W),e("td",{class:"center link",onClick:d=>C(t.chapter_id,">=1")},l(t.correct_questions),9,X),e("td",{class:"center link",onClick:d=>C(t.chapter_id,"=0")},l(t.not_correct_questions),9,Y)]))),128))])])])])]))}},nt=$(Z,[["__scopeId","data-v-ecc11515"]]);export{nt as default}; diff --git a/frontend/dist/assets/AnalysisKnowledge-5QPt6Ak_.css b/frontend/dist/assets/AnalysisKnowledge-5QPt6Ak_.css new file mode 100644 index 0000000..90146ae --- /dev/null +++ b/frontend/dist/assets/AnalysisKnowledge-5QPt6Ak_.css @@ -0,0 +1 @@ +.center[data-v-2b42a9e8]{text-align:center}.link[data-v-2b42a9e8]{color:#1890ff;cursor:pointer} diff --git a/frontend/dist/assets/AnalysisKnowledge-D1yJ4mIN.js b/frontend/dist/assets/AnalysisKnowledge-D1yJ4mIN.js new file mode 100644 index 0000000..a20079e --- /dev/null +++ b/frontend/dist/assets/AnalysisKnowledge-D1yJ4mIN.js @@ -0,0 +1 @@ +import{_ as I,b as $,c as a,a as e,w as p,v as E,F as m,r as g,g as x,f as B,t as s,i as U,j as K,d as o,l as M,o as u}from"./index-Dc_QwQl6.js";import{a as N,b as R,d as z}from"./api-gDOaCrl1.js";const G={class:"page-header"},L={class:"filter-panel"},P={class:"row"},Q=["value"],T={class:"row"},H={key:0,class:"row",style:{color:"#1890FF","font-size":"13px"}},J={class:"content-area"},O={class:"card",style:{padding:"0",overflow:"auto"}},W={class:"data-table"},X=["onClick"],Y={class:"center"},Z={class:"center"},tt=["onClick"],et=["onClick"],lt=["onClick"],nt={__name:"AnalysisKnowledge",setup(st){const h=K(),V=M(),q=o([]),v=o("全部"),_=o(""),k=o(""),y=o(""),b=o([]),i=o(null),d=o("not_correct_questions"),r=o("DESC"),j=[{key:"id",label:"ID"},{key:"subject",label:"科目"},{key:"chapter_info",label:"章节"},{key:"knowledge_content",label:"知识点"},{key:"total_questions",label:"试题数量"},{key:"correct_questions",label:"已正确题目数"},{key:"not_correct_questions",label:"尚未正确题目数"}];function D(n){return n!==d.value?"":r.value==="ASC"?" ▲":" ▼"}function S(n){d.value===n?r.value=r.value==="ASC"?"DESC":"ASC":(d.value=n,r.value="ASC"),c()}function w(){return{subject:v.value,correct_count:_.value,not_correct_count:k.value,kp_content:y.value,chapter_id_filter:i.value,sort_column:d.value,sort_direction:r.value}}async function c(){const n=await R("/analysis/knowledge",w());b.value=n.rows}function A(){i.value=null,c()}function f(n,l){const t={kp_id:n};l&&(t.right_times=l),V.push({path:"/questions",query:t})}async function F(){b.value.length&&await z("/analysis/knowledge/export-excel",w())}return $(async()=>{h.query.chapter_id&&(i.value=parseInt(h.query.chapter_id));const n=await N("/meta/subjects");q.value=n.subjects,await c()}),(n,l)=>(u(),a("div",null,[e("div",G,[l[5]||(l[5]=e("span",{class:"title"},"科目知识列表",-1)),e("button",{class:"btn",style:{"margin-left":"16px"},onClick:l[0]||(l[0]=t=>n.$router.push("/subject-portal"))},"返回科目首页"),e("button",{class:"btn orange",onClick:F},"📥 导出Excel")]),e("div",L,[e("div",P,[l[7]||(l[7]=e("label",null,"科目:",-1)),p(e("select",{"onUpdate:modelValue":l[1]||(l[1]=t=>v.value=t),onChange:c},[l[6]||(l[6]=e("option",null,"全部",-1)),(u(!0),a(m,null,g(q.value,t=>(u(),a("option",{key:t,value:t},s(t),9,Q))),128))],544),[[E,v.value]]),l[8]||(l[8]=e("label",{style:{"margin-left":"10px"}},"已正确题目数:",-1)),p(e("input",{type:"text","onUpdate:modelValue":l[2]||(l[2]=t=>_.value=t),style:{width:"70px"}},null,512),[[x,_.value]]),l[9]||(l[9]=e("label",{style:{"margin-left":"10px"}},"尚未正确题目数:",-1)),p(e("input",{type:"text","onUpdate:modelValue":l[3]||(l[3]=t=>k.value=t),style:{width:"70px"}},null,512),[[x,k.value]])]),e("div",T,[l[10]||(l[10]=e("label",null,"知识点:",-1)),p(e("input",{type:"text","onUpdate:modelValue":l[4]||(l[4]=t=>y.value=t),style:{width:"340px"}},null,512),[[x,y.value]]),e("button",{class:"btn primary",style:{"margin-left":"auto"},onClick:c},"🔍 开始搜索")]),i.value?(u(),a("div",H,[B(" 当前按章节过滤:章节ID "+s(i.value)+" ",1),e("button",{class:"btn small",onClick:A},"清除章节过滤")])):U("",!0)]),e("div",J,[e("div",O,[e("table",W,[e("thead",null,[e("tr",null,[(u(),a(m,null,g(j,t=>e("th",{key:t.key,onClick:C=>S(t.key)},s(t.label)+s(D(t.key)),9,X)),64))])]),e("tbody",null,[(u(!0),a(m,null,g(b.value,t=>(u(),a("tr",{key:t.kp_id},[e("td",Y,s(t.kp_id),1),e("td",Z,s(t.subject),1),e("td",null,s(t.chapter_info),1),e("td",null,s(t.knowledge_content),1),e("td",{class:"center link",onClick:C=>f(t.kp_id,null)},s(t.total_questions),9,tt),e("td",{class:"center link",onClick:C=>f(t.kp_id,">=1")},s(t.correct_questions),9,et),e("td",{class:"center link",onClick:C=>f(t.kp_id,"=0")},s(t.not_correct_questions),9,lt)]))),128))])])])])]))}},ut=I(nt,[["__scopeId","data-v-2b42a9e8"]]);export{ut as default}; diff --git a/frontend/dist/assets/AnalysisPending-BfBXB5e_.js b/frontend/dist/assets/AnalysisPending-BfBXB5e_.js new file mode 100644 index 0000000..7c51a5e --- /dev/null +++ b/frontend/dist/assets/AnalysisPending-BfBXB5e_.js @@ -0,0 +1 @@ +import{_ as V,b as j,c as s,a as t,w as f,v as U,F as c,r as v,h as C,f as h,t as i,i as B,d as y,x as E,l as P,o as a}from"./index-Dc_QwQl6.js";import{a as w}from"./api-gDOaCrl1.js";const q={class:"page-header"},L={class:"filter-panel"},M=["value"],N={class:"content-area"},T={key:0,class:"card",style:{overflow:"auto",padding:"0"}},A={class:"data-table",style:{"min-width":"760px"}},D={class:"stage-cell"},F=["onClick"],R=["onClick"],G={class:"total-cell"},I=["onClick"],z={class:"paper-cell"},H=["onClick"],J=["onClick"],K={__name:"AnalysisPending",setup(O){const x=P(),g=y([]),p=y(""),r=y("legacy"),o=y(null),S=E(()=>o.value?["阶段",...o.value.subjects.map(u=>u.name),"总数"]:[]);async function b(){const u=new URLSearchParams({term:p.value,mode:r.value});o.value=await w("/analysis/pending?"+u.toString())}function m(u,l,e){const n={question_nature:"错题",term:p.value,status:"全部",archive_status:"全部"},d=o.value.subjects.find(k=>k.id===u);d&&(n.subject=d.name);const _=o.value.stages.find(k=>k.id===l);_&&(n.stage=_.name),e!=null&&(n.paper_count=e),r.value==="legacy"&&(n.right_times="=0"),x.push({path:"/questions",query:n})}async function $(){if(!o.value)return;const l="/StudyBase/api/analysis/pending/export-excel?"+new URLSearchParams({term:p.value,mode:r.value}).toString(),e=document.createElement("a");e.href=l,e.download="",document.body.appendChild(e),e.click(),e.remove()}return j(async()=>{const u=await w("/analysis/pending?mode=legacy");g.value=u.terms,p.value=u.current_term,o.value=u}),(u,l)=>(a(),s("div",null,[t("div",q,[l[5]||(l[5]=t("span",{class:"title"},"遗留分析汇总",-1)),t("button",{class:"btn",style:{"margin-left":"16px"},onClick:l[0]||(l[0]=e=>u.$router.push("/subject-portal"))},"返回科目首页"),t("button",{class:"btn orange",onClick:$},"📥 导出Excel")]),t("div",L,[l[8]||(l[8]=t("label",null,"选择学期:",-1)),f(t("select",{"onUpdate:modelValue":l[1]||(l[1]=e=>p.value=e),onChange:b},[(a(!0),s(c,null,v(g.value,e=>(a(),s("option",{key:e,value:e},i(e),9,M))),128))],544),[[U,p.value]]),l[9]||(l[9]=t("label",{style:{"margin-left":"25px"}},"统计范围:",-1)),t("label",null,[f(t("input",{type:"radio",value:"legacy","onUpdate:modelValue":l[2]||(l[2]=e=>r.value=e),onChange:b},null,544),[[C,r.value]]),l[6]||(l[6]=h(" 遗留错题",-1))]),t("label",null,[f(t("input",{type:"radio",value:"all","onUpdate:modelValue":l[3]||(l[3]=e=>r.value=e),onChange:b},null,544),[[C,r.value]]),l[7]||(l[7]=h(" 全部错题",-1))])]),t("div",N,[o.value?(a(),s("div",T,[t("table",A,[t("thead",null,[t("tr",null,[(a(!0),s(c,null,v(S.value,e=>(a(),s("th",{key:e},i(e),1))),128))])]),t("tbody",null,[(a(!0),s(c,null,v(o.value.stage_rows,e=>(a(),s("tr",{key:e.label},[t("td",D,i(e.label),1),(a(!0),s(c,null,v(e.cells,(n,d)=>(a(),s("td",{key:d,class:"link",onClick:_=>m(n.sub_id,n.st_id,null)},i(n.count),9,F))),128)),t("td",{class:"link bold",onClick:n=>m(null,e.cells[0]?e.cells[0].st_id:null,null)},i(e.sum),9,R)]))),128)),t("tr",null,[t("td",G,i(o.value.total_row.label),1),(a(!0),s(c,null,v(o.value.total_row.cells,(e,n)=>(a(),s("td",{key:n,class:"link total-cell bold",onClick:d=>m(e.sub_id,null,null)},i(e.count),9,I))),128)),t("td",{class:"link total-cell bold",onClick:l[4]||(l[4]=e=>m(null,null,null))},i(o.value.total_row.sum),1)]),(a(!0),s(c,null,v(o.value.paper_rows,e=>(a(),s("tr",{key:e.label},[t("td",z,i(e.label),1),(a(!0),s(c,null,v(e.cells,(n,d)=>(a(),s("td",{key:d,class:"link",onClick:_=>m(n.sub_id,null,e.paper_count)},i(n.count),9,H))),128)),t("td",{class:"link bold",onClick:n=>m(null,null,e.paper_count)},i(e.sum),9,J)]))),128))])])])):B("",!0)])]))}},X=V(K,[["__scopeId","data-v-9df751f3"]]);export{X as default}; diff --git a/frontend/dist/assets/AnalysisPending-C5SzviRH.css b/frontend/dist/assets/AnalysisPending-C5SzviRH.css new file mode 100644 index 0000000..c59c5ec --- /dev/null +++ b/frontend/dist/assets/AnalysisPending-C5SzviRH.css @@ -0,0 +1 @@ +.link[data-v-9df751f3]{color:#1890ff;cursor:pointer;text-align:center}.bold[data-v-9df751f3]{font-weight:700}.stage-cell[data-v-9df751f3]{background:#f9f0ff}.total-cell[data-v-9df751f3]{background:#fff7e6;font-weight:700}.paper-cell[data-v-9df751f3]{background:#f6ffed} diff --git a/frontend/dist/assets/FileImport-B8PhhnGN.css b/frontend/dist/assets/FileImport-B8PhhnGN.css new file mode 100644 index 0000000..ea01dbb --- /dev/null +++ b/frontend/dist/assets/FileImport-B8PhhnGN.css @@ -0,0 +1 @@ +.modal-mask[data-v-a85a16b8]{position:fixed;top:0;right:0;bottom:0;left:0;background:#00000059;display:flex;align-items:center;justify-content:center;z-index:100}.modal[data-v-a85a16b8]{background:#f0f2f5;width:760px;max-height:90vh;overflow:auto;border-radius:6px;box-shadow:0 4px 20px #00000040}.modal-header[data-v-a85a16b8]{background:#4a90e2;color:#000;font-size:17px;font-weight:700;padding:10px 16px}.modal-body[data-v-a85a16b8]{padding:14px 18px}.modal-footer[data-v-a85a16b8]{display:flex;justify-content:space-between;padding:12px 40px 18px}.form-row[data-v-a85a16b8]{display:flex;align-items:center;gap:8px;margin:8px 0}.form-row label[data-v-a85a16b8]{white-space:nowrap;font-size:13px}.section-label[data-v-a85a16b8]{font-weight:700;font-size:13px;margin:12px 0 4px}.toolbar[data-v-a85a16b8]{margin-bottom:4px} diff --git a/frontend/dist/assets/FileImport-BF8-5MOQ.js b/frontend/dist/assets/FileImport-BF8-5MOQ.js new file mode 100644 index 0000000..0472a9c --- /dev/null +++ b/frontend/dist/assets/FileImport-BF8-5MOQ.js @@ -0,0 +1,21 @@ +import{_ as ae,b as se,c as i,a as l,w as p,v as x,F as $,r as T,t as g,p as ne,g as B,i as oe,d as s,q as ue,l as ie,o as r}from"./index-Dc_QwQl6.js";import{a as re,t as f,c as Z,b as H}from"./api-gDOaCrl1.js";const ve={class:"content-area"},de={class:"card"},pe={style:{display:"grid","grid-template-columns":"110px 240px 110px 240px",gap:"10px 14px","align-items":"center"}},me=["value"],fe=["value"],ce=["value"],ge=["value"],ye=["value"],be=["value"],we={class:"card",style:{display:"flex",gap:"18px","align-items":"center","flex-wrap":"wrap"}},ke=["disabled"],xe=["disabled"],Se=["disabled"],Ce={key:0,class:"modal-mask"},$e={class:"modal"},Te={class:"modal-body"},Ve={class:"form-row"},De={class:"form-row"},Ue={class:"form-row"},qe={class:"form-row"},Ie={class:"toolbar"},je={class:"toolbar"},Fe=`智学网文件解析请注意以下格式: +!!请确定组卷网下载文件包括了【知识点】信息!! +1. 题目开始格式: + 【错题】/【推题】【客观题】/【主观题】[题号] + + 例如: + 【错题】【客观题】1.豌豆(雌雄同花)和玉米(雌雄同株异花)都是良好的遗传学材料。利用这两种材料进行遗传学实验时,操作正确的是(  ) + +2. 答案开始格式: + 【答案】[题号] + + 例如: + 【答案】2.B + 【知识点】孟德尔一对相对性状的杂交实验 + 【分析】孟德尔一对相对性状的杂交试验中,实现3:1的分离比必须同时满足的条件是:F1形成的配子数目相等且生活力相同,雌、雄配子结合的机会相等;F2不同的基因型的个体的存活率相等;等位基因间的显隐性关系是完全的;观察的子代样本数目足够多。 + +请点击“确定”开始解析。 +`,Me={__name:"FileImport",setup(Oe){const K=ie(),v=s({subject:{options:[],default:""},source:{options:[],default:""},term:{options:[],default:""},stage:{options:[],default:""},origin:{options:[],default:""},type:{options:["错题","推题","补充提高"],default:"错题"}}),q=s(""),V=s(""),I=s("错题"),j=s(""),F=s(""),M=s(""),O=s(""),E=s(null),D=s(!1),m=s(null),N=s(!1),S=s(""),U=s("错题"),y=s(""),b=s(""),w=s(""),k=s(""),P=s(null),_=s(null),C=s(0),R=s("");function u(a){O.value+=`[${new Date().toTimeString().slice(0,8)}] ${a} +`,ue(()=>{E.value&&(E.value.scrollTop=E.value.scrollHeight)})}function A(){return{subject:q.value,source:V.value,type:I.value,term:j.value,stage:F.value,origin:M.value}}function Q(){const a=A();if(!Object.values(a).every(Boolean)){f("请确保所有导入参数都已选择!");return}if(V.value==="组卷网"&&!Z(Fe))return;const e=document.createElement("input");e.type="file",e.accept=".docx",e.onchange=async()=>{const t=e.files[0];if(!t)return;O.value="",D.value=!0,m.value=null;const n=new FormData;n.append("file",t);for(const[d,o]of Object.entries(a))n.append(d,o);try{const o=await(await fetch("/StudyBase/api/import/parse",{method:"POST",body:n})).json();for(const J of o.log||[])u(J.replace(/^\[\d{2}:\d{2}:\d{2}\]\s?/,""));o.error?m.value=null:o.questions?m.value=o.questions:m.value=null}catch(d){u(`解析过程中发生未预期错误: ${d.message}`),m.value=null}finally{D.value=!1}},e.click()}function X(){O.value="",u("正在手动添加试题..."),N.value=!0,S.value="",U.value="错题",y.value="",b.value="",w.value="",k.value="",C.value=0,R.value=new Date().toISOString().replace(/[-:TZ.]/g,"").slice(0,14)}function h(){const a=new Date,e=t=>String(t).padStart(2,"0");b.value=`${a.getFullYear()}-${e(a.getMonth()+1)}-${e(a.getDate())}`}async function ee(){if(!S.value){f("请先选择题目分类(客观题或主观题)。");return}if(!y.value.trim()){f("题目知识点为必填项。");return}if(!w.value.trim()){f("题干内容不能为空。");return}if(!k.value.trim()){f("答案解析不能为空。");return}const a=C.value+1;try{const e=await H("/import/manual-add",{category:S.value,q_type:U.value,kp_text:y.value.trim(),answer_time:b.value,stem_raw:w.value,analysis_raw:k.value,question_no:a,session_timestamp:R.value,meta:A()});c||(c=[]),c.push(e),C.value=a,u(`添加一条试题。当前已添加 ${C.value} 条。`),f("当前试题已保存,可以继续添加下一题。"),y.value="",b.value="",w.value="",k.value=""}catch(e){if(e.needConfirm){if(Z("答题时间格式不是标准日期(YYYY-MM-DD),仍要按原样保存吗?"))try{const t=await H("/import/manual-add",{category:S.value,q_type:U.value,kp_text:y.value.trim(),answer_time:b.value,stem_raw:w.value,analysis_raw:k.value,question_no:a,session_timestamp:R.value,meta:A(),force:!0});c.push(t),C.value=a,u(`添加一条试题。当前已添加 ${C.value} 条。`),f("当前试题已保存,可以继续添加下一题。"),y.value="",b.value="",w.value="",k.value=""}catch(t){f(t.message)}}else f(e.message)}}let c=[];function te(){u("手动添加试题窗口已关闭。"),N.value=!1,c&&c.length?(m.value=c,u(`手动添加已完成。共添加 ${c.length} 道题目。`),u(` +--- 手动添加题目预览 ---`),u(JSON.stringify(c,null,4)),u(`--- 预览结束 --- +`)):(m.value=null,u("手动添加已取消或未添加任何题目。")),c=[]}async function G(a){const e=new FormData;e.append("file",a,a.name||"pasted.png");const t=await fetch("/StudyBase/api/import/upload-image",{method:"POST",body:e});if(!t.ok){let d="上传失败";try{d=(await t.json()).detail||d}catch{}throw new Error(d)}return(await t.json()).path}function z(a,e){if(a&&a.selectionStart!=null){const t=a.selectionStart,n=a.value;return a.value=n.slice(0,t)+e+n.slice(a.selectionEnd),a.value}return null}function L(a){const e=document.createElement("input");e.type="file",e.accept="image/png,image/jpeg,image/bmp,image/gif",e.onchange=async()=>{const t=e.files[0];if(t)try{const n=await G(t),d=`[[IMG:${n}]]`,o=a;o&&(z(o,d),o.dispatchEvent(new Event("input"))),u(`已选取图片: ${n}`)}catch(n){f(n.message)}},e.click()}async function W(a,e){const t=a.clipboardData&&a.clipboardData.items;if(t){for(const n of t)if(n.type&&n.type.startsWith("image/")){a.preventDefault();const d=n.getAsFile();try{const o=await G(d),J=`[[IMG:${o}]]`,Y=e==="stem"?P.value:_.value;Y&&(z(Y,J),Y.dispatchEvent(new Event("input"))),u(`从剪贴板插入图片: ${o}`)}catch(o){u(`保存剪贴板图片失败: ${o.message}`)}return}}}function le(){m.value?(sessionStorage.setItem("sb_import_data",JSON.stringify(m.value)),sessionStorage.setItem("sb_import_meta",JSON.stringify({...A(),import_time:new Date().toLocaleString("sv-PR")})),K.push("/questions/import/preview")):f("请先解析文件或选择手动添加,没有可预览的数据。")}return se(async()=>{v.value=await re("/import/options"),q.value=v.value.subject.default,V.value=v.value.source.default,j.value=v.value.term.default,F.value=v.value.stage.default,M.value=v.value.origin.default,I.value=v.value.type.default}),(a,e)=>(r(),i("div",null,[e[34]||(e[34]=l("div",{class:"page-header"},[l("span",{class:"title"},"文件导入题目")],-1)),l("div",ve,[l("div",de,[e[23]||(e[23]=l("div",{style:{"font-weight":"bold","margin-bottom":"10px"}},"导入参数设置",-1)),l("div",pe,[e[17]||(e[17]=l("label",null,"所属科目:",-1)),p(l("select",{"onUpdate:modelValue":e[0]||(e[0]=t=>q.value=t)},[(r(!0),i($,null,T(v.value.subject.options,t=>(r(),i("option",{key:t,value:t},g(t),9,me))),128))],512),[[x,q.value]]),e[18]||(e[18]=l("label",null,"学期:",-1)),p(l("select",{"onUpdate:modelValue":e[1]||(e[1]=t=>j.value=t)},[(r(!0),i($,null,T(v.value.term.options,t=>(r(),i("option",{key:t,value:t},g(t),9,fe))),128))],512),[[x,j.value]]),e[19]||(e[19]=l("label",null,"题目来源:",-1)),p(l("select",{"onUpdate:modelValue":e[2]||(e[2]=t=>V.value=t)},[(r(!0),i($,null,T(v.value.source.options,t=>(r(),i("option",{key:t,value:t},g(t),9,ce))),128))],512),[[x,V.value]]),e[20]||(e[20]=l("label",null,"阶段:",-1)),p(l("select",{"onUpdate:modelValue":e[3]||(e[3]=t=>F.value=t)},[(r(!0),i($,null,T(v.value.stage.options,t=>(r(),i("option",{key:t,value:t},g(t),9,ge))),128))],512),[[x,F.value]]),e[21]||(e[21]=l("label",null,"题目类型:",-1)),p(l("select",{"onUpdate:modelValue":e[4]||(e[4]=t=>I.value=t)},[(r(!0),i($,null,T(v.value.type.options,t=>(r(),i("option",{key:t,value:t},g(t),9,ye))),128))],512),[[x,I.value]]),e[22]||(e[22]=l("label",null,"题目出处:",-1)),p(l("select",{"onUpdate:modelValue":e[5]||(e[5]=t=>M.value=t)},[(r(!0),i($,null,T(v.value.origin.options,t=>(r(),i("option",{key:t,value:t},g(t),9,be))),128))],512),[[x,M.value]])])]),l("div",we,[l("button",{class:"btn",disabled:D.value,style:{"font-weight":"bold"},onClick:Q},g(D.value?"解析中...":"第一步:选择文件并解析"),9,ke),l("button",{class:"btn orange",disabled:D.value,style:{"font-weight":"bold"},onClick:X},g(N.value?"手动添加中...":"第一步:手动添加试题"),9,xe),l("button",{class:"btn",disabled:!m.value,style:ne([{"font-weight":"bold"},m.value?"":"color:#999;"]),onClick:le}," 第二步:预览并确定导入 ",12,Se),l("button",{class:"btn",style:{"margin-left":"auto"},onClick:e[6]||(e[6]=t=>a.$router.push("/questions/add"))},"返回")]),e[24]||(e[24]=l("div",{style:{"font-size":"13px",margin:"6px 2px"}},"解析进度与JSON预览:",-1)),l("div",{class:"log-box",ref_key:"logBox",ref:E},g(O.value),513)]),N.value?(r(),i("div",Ce,[l("div",$e,[e[33]||(e[33]=l("div",{class:"modal-header"},"手动添加试题",-1)),l("div",Te,[l("div",Ve,[e[26]||(e[26]=l("label",null,"题目分类(必选):",-1)),p(l("select",{"onUpdate:modelValue":e[7]||(e[7]=t=>S.value=t)},[...e[25]||(e[25]=[l("option",{value:"",disabled:""},"请选择",-1),l("option",null,"客观题",-1),l("option",null,"主观题",-1)])],512),[[x,S.value]])]),l("div",De,[e[28]||(e[28]=l("label",null,"题目类型(必选):",-1)),p(l("select",{"onUpdate:modelValue":e[8]||(e[8]=t=>U.value=t)},[...e[27]||(e[27]=[l("option",null,"错题",-1),l("option",null,"推题",-1)])],512),[[x,U.value]])]),l("div",Ue,[e[29]||(e[29]=l("label",null,"题目知识点(必填):",-1)),p(l("input",{type:"text","onUpdate:modelValue":e[9]||(e[9]=t=>y.value=t),style:{width:"320px"}},null,512),[[B,y.value]])]),l("div",qe,[e[30]||(e[30]=l("label",null,"答题时间(选填,例:2026-03-16):",-1)),p(l("input",{type:"text","onUpdate:modelValue":e[10]||(e[10]=t=>b.value=t),style:{width:"150px"}},null,512),[[B,b.value]]),l("button",{class:"btn small",onClick:h},"填入今天")]),e[31]||(e[31]=l("div",{class:"section-label"},"题干内容(必填,支持插入图片;可使用 Ctrl+V 粘贴)",-1)),l("div",Ie,[l("button",{class:"btn small",onClick:e[11]||(e[11]=t=>L(P.value))},"从本地选择图片")]),p(l("textarea",{ref_key:"mStemRef",ref:P,"onUpdate:modelValue":e[12]||(e[12]=t=>w.value=t),rows:"6",style:{width:"100%"},onPaste:e[13]||(e[13]=t=>W(t,"stem"))},null,544),[[B,w.value]]),e[32]||(e[32]=l("div",{class:"section-label"},"答案解析(必填,支持插入图片;可使用 Ctrl+V 粘贴)",-1)),l("div",je,[l("button",{class:"btn small",onClick:e[14]||(e[14]=t=>L(_.value))},"从本地选择图片")]),p(l("textarea",{ref_key:"mAnalysisRef",ref:_,"onUpdate:modelValue":e[15]||(e[15]=t=>k.value=t),rows:"5",style:{width:"100%"},onPaste:e[16]||(e[16]=t=>W(t,"analysis"))},null,544),[[B,k.value]])]),l("div",{class:"modal-footer"},[l("button",{class:"btn",onClick:ee},"继续添加"),l("button",{class:"btn primary",onClick:te},"确定返回")])])])):oe("",!0)]))}},Ae=ae(Me,[["__scopeId","data-v-a85a16b8"]]);export{Ae as default}; diff --git a/frontend/dist/assets/Home-BS2dZmih.css b/frontend/dist/assets/Home-BS2dZmih.css new file mode 100644 index 0000000..f7525f8 --- /dev/null +++ b/frontend/dist/assets/Home-BS2dZmih.css @@ -0,0 +1 @@ +.home[data-v-0c1f45b2]{height:100%;display:flex;flex-direction:column;background:#f0f0f0}.header[data-v-0c1f45b2]{background:#4a90e2;height:100px;display:flex;align-items:center;justify-content:center}.header .title[data-v-0c1f45b2]{color:#fff;font-size:28px;font-weight:700}.btn-frame[data-v-0c1f45b2]{flex:1;display:flex;align-items:center;justify-content:center;gap:30px;flex-wrap:wrap}.main-btn[data-v-0c1f45b2]{width:220px;height:62px;font-size:16px;font-weight:700;background:#fff;border:none;cursor:pointer;border-radius:4px;box-shadow:0 1px 3px #00000014;font-family:inherit}.main-btn[data-v-0c1f45b2]:hover{background:#f5f8fd} diff --git a/frontend/dist/assets/Home-DWL4BmBg.js b/frontend/dist/assets/Home-DWL4BmBg.js new file mode 100644 index 0000000..4feb6ea --- /dev/null +++ b/frontend/dist/assets/Home-DWL4BmBg.js @@ -0,0 +1 @@ +import{_ as n,c as r,a as t,o as l}from"./index-Dc_QwQl6.js";const a={},i={class:"home"},u={class:"btn-frame"};function p(o,s){return l(),r("div",i,[s[4]||(s[4]=t("div",{class:"header"},[t("span",{class:"title"},"错题智能整理系统")],-1)),t("div",u,[t("button",{class:"main-btn",style:{color:"#1A73E8"},onClick:s[0]||(s[0]=e=>o.$router.push("/subject-portal"))},"① 科目面板"),t("button",{class:"main-btn",style:{color:"#28A745"},onClick:s[1]||(s[1]=e=>o.$router.push("/questions"))},"② 题目管理"),t("button",{class:"main-btn",style:{color:"#E67E22"},onClick:s[2]||(s[2]=e=>o.$router.push("/papers"))},"③ 试卷管理"),t("button",{class:"main-btn",style:{color:"#6C757D"},onClick:s[3]||(s[3]=e=>o.$router.push("/settings"))},"④ 系统设定")])])}const b=n(a,[["render",p],["__scopeId","data-v-0c1f45b2"]]);export{b as default}; diff --git a/frontend/dist/assets/ImportPreview-CFWTYbgQ.js b/frontend/dist/assets/ImportPreview-CFWTYbgQ.js new file mode 100644 index 0000000..1dbeb71 --- /dev/null +++ b/frontend/dist/assets/ImportPreview-CFWTYbgQ.js @@ -0,0 +1 @@ +import{_ as A,b as C,c as l,a as t,i as c,F as I,r as N,d as m,l as S,o as r,t as n,w as g,g as b,m as f}from"./index-Dc_QwQl6.js";import{c as x,t as u,b as V}from"./api-gDOaCrl1.js";import{R as y}from"./RichContent-vEKXmEUi.js";const z={class:"content-area",style:{"padding-bottom":"90px"}},D={key:0,class:"card",style:{"text-align":"center",padding:"50px"}},E={style:{"font-weight":"bold",color:"#1976D2","font-size":"13px","border-bottom":"1px solid #E8E8E8","padding-bottom":"6px","margin-bottom":"8px"}},B={class:"meta-grid"},U={style:{display:"flex",gap:"8px","align-items":"flex-start",margin:"10px 0"}},q=["onUpdate:modelValue"],P={style:{background:"#FAFAFA",padding:"10px","border-radius":"3px",margin:"4px 0"}},R={key:0,style:{color:"#999"}},$={style:{background:"#FAFAFA",padding:"10px","border-radius":"3px",margin:"4px 0"}},J={key:0,style:{color:"#999"}},M={style:{display:"flex","align-items":"center",gap:"8px","margin-top":"10px"}},O=["onUpdate:modelValue"],T=["onClick"],L=["onClick"],Q={__name:"ImportPreview",setup(j){const v=S(),a=m([]),_=m({}),p=m({});C(()=>{try{a.value=JSON.parse(sessionStorage.getItem("sb_import_data")||"[]"),_.value=JSON.parse(sessionStorage.getItem("sb_import_meta")||"{}")}catch{a.value=[]}});function h(o){const s=(p.value[o]||"").trim();if(!s)return;const e=parseInt(s);if(isNaN(e)){u("请输入整数");return}a.value[o].是否有小题=e>0,a.value[o].小题个数=e,p.value[o]=""}function k(o){x("确定删除此题吗?")&&a.value.splice(o,1)}function w(){x("确定返回吗?所有未入库的修改将丢失!")&&v.push("/questions/import")}async function F(){if(!a.value.length){u("没有题目可以入库。");return}try{const o=await V("/import/save",{questions:a.value,meta:_.value});u(`成功入库 ${o.inserted} 道题目并建立知识点关联`),sessionStorage.removeItem("sb_import_data"),sessionStorage.removeItem("sb_import_meta"),v.push("/questions")}catch(o){u(o.message)}}return(o,s)=>(r(),l("div",null,[s[4]||(s[4]=t("div",{class:"page-header"},[t("span",{class:"title"},"导入题目预览与确认")],-1)),t("div",z,[a.value.length?c("",!0):(r(),l("div",D,"暂无题目数据")),(r(!0),l(I,null,N(a.value,(e,i)=>(r(),l("div",{key:i,class:"card"},[t("div",E," 题号: "+n(e.题号),1),t("div",B,[t("span",null,"科目: "+n(e.科目),1),t("span",null,"类型: "+n(e.题目类型),1),t("span",null,"来源: "+n(e.题目来源),1),t("span",null,"答题时间: "+n(e.答题时间),1),t("span",null,"科目分类: "+n(e.题目分类),1),t("span",null,"输入时间: "+n(e.输入时间),1),t("span",null,"有无小题: "+n(e.是否有小题?"是":"否"),1),t("span",null,"小题个数: "+n(e.小题个数||0),1),t("span",null,"题目分值: "+n(e.分值!==void 0?e.分值:"未设置"),1)]),t("div",U,[s[0]||(s[0]=t("label",{style:{"font-weight":"bold","font-size":"13px","white-space":"nowrap","margin-top":"4px"}},"知识点:",-1)),g(t("textarea",{"onUpdate:modelValue":d=>e.题目知识点=d,rows:"2",style:{flex:"1",background:"#FAFAFA",border:"1px solid #eee"}},null,8,q),[[b,e.题目知识点]])]),s[2]||(s[2]=t("div",{style:{"font-weight":"bold","font-size":"13px"}},"【题干内容】",-1)),t("div",P,[f(y,{content:e.题干内容||"","max-width":750},null,8,["content"]),e.题干内容?c("",!0):(r(),l("div",R,"(无内容)"))]),s[3]||(s[3]=t("div",{style:{"font-weight":"bold","font-size":"13px",color:"#2E7D32"}},"【答案解析】",-1)),t("div",$,[f(y,{content:e.答案解析||"","max-width":750},null,8,["content"]),e.答案解析?c("",!0):(r(),l("div",J,"(无内容)"))]),t("div",M,[s[1]||(s[1]=t("label",{style:{"font-size":"13px"}},"小题个数:",-1)),g(t("input",{type:"text","onUpdate:modelValue":d=>p.value[i]=d,style:{width:"60px"}},null,8,O),[[b,p.value[i]]]),t("button",{class:"btn small",onClick:d=>h(i)},"确认修改",8,T),t("button",{class:"btn small",style:{color:"red"},onClick:d=>k(i)},"删除此题",8,L)])]))),128))]),t("div",{class:"bottom-bar"},[t("button",{class:"btn",onClick:w},"返回修改"),t("button",{class:"btn primary",style:{"font-weight":"bold"},onClick:F},"确认全部入库")])]))}},W=A(Q,[["__scopeId","data-v-2c16a3ae"]]);export{W as default}; diff --git a/frontend/dist/assets/ImportPreview-Dx1LXj5L.css b/frontend/dist/assets/ImportPreview-Dx1LXj5L.css new file mode 100644 index 0000000..05d9593 --- /dev/null +++ b/frontend/dist/assets/ImportPreview-Dx1LXj5L.css @@ -0,0 +1 @@ +.meta-grid[data-v-2c16a3ae]{display:grid;grid-template-columns:repeat(5,1fr);gap:4px 10px;color:#555;font-size:12px;background:#f9f9f9;padding:8px;border-radius:3px}.bottom-bar[data-v-2c16a3ae]{position:fixed;bottom:0;left:0;right:0;height:64px;background:#fff;border-top:1px solid #d9d9d9;display:flex;align-items:center;justify-content:flex-end;gap:14px;padding:0 30px} diff --git a/frontend/dist/assets/PapersCreateById-CT-UILr2.js b/frontend/dist/assets/PapersCreateById-CT-UILr2.js new file mode 100644 index 0000000..bcd6f5f --- /dev/null +++ b/frontend/dist/assets/PapersCreateById-CT-UILr2.js @@ -0,0 +1 @@ +import{c as p,a as e,w as d,g as u,d as m,l as c,o as f}from"./index-Dc_QwQl6.js";import{t as o,a as g,c as v}from"./api-gDOaCrl1.js";const x={class:"page-header"},y={class:"content-area",style:{"padding-top":"16px"}},_={__name:"PapersCreateById",setup(I){const r=c(),i=m("");async function l(){const s=i.value.trim();if(!s){o("请输入至少一个试题ID。");return}let t;try{const n=new URLSearchParams({ids:s});t=await g("/questions/validate-ids?"+n.toString())}catch(n){o(n.message);return}const a=t.valid_ids;t.missing_ids.length&&!v(`部分试题ID (${t.missing_ids.join(", ")}) 不存在于数据库中,是否继续只使用有效ID生成试卷?`)||(sessionStorage.setItem("sb_selected_ids",JSON.stringify(a)),r.push("/papers/preview"))}return(s,t)=>(f(),p("div",null,[e("div",x,[t[2]||(t[2]=e("span",{class:"title"},"根据ID生成试卷",-1)),e("button",{class:"btn",style:{"margin-left":"16px"},onClick:t[0]||(t[0]=a=>s.$router.push("/papers"))},"返回")]),e("div",y,[t[3]||(t[3]=e("div",{style:{"margin-bottom":"10px","font-size":"14px"}},"请输入试题ID(多个ID请用逗号、空格或换行符分隔):",-1)),d(e("textarea",{"onUpdate:modelValue":t[1]||(t[1]=a=>i.value=a),style:{width:"100%",height:"320px","font-family":"Consolas,monospace","font-size":"13px"}},null,512),[[u,i.value]]),e("div",{style:{"margin-top":"16px"}},[e("button",{class:"btn green",style:{"font-weight":"bold","font-size":"14px",padding:"10px 22px"},onClick:l},"生成试卷")])])]))}};export{_ as default}; diff --git a/frontend/dist/assets/PapersDetail-CpYT1ySO.js b/frontend/dist/assets/PapersDetail-CpYT1ySO.js new file mode 100644 index 0000000..8e7c7cb --- /dev/null +++ b/frontend/dist/assets/PapersDetail-CpYT1ySO.js @@ -0,0 +1 @@ +import{b as r,c as i,a as s,t as l,u as d,j as u,d as c,o as m}from"./index-Dc_QwQl6.js";import{a as x}from"./api-gDOaCrl1.js";const g={class:"page-header"},b={class:"title"},f={class:"content-area",style:{"padding-top":"30px","text-align":"center"}},$={__name:"PapersDetail",setup(v){const p=u(),a=parseInt(p.params.id),n=c("未命名");return r(async()=>{const e=await x(`/papers/${a}/basic`);n.value=e.paper_name}),(e,t)=>(m(),i("div",null,[s("div",g,[s("span",b,"试卷详情: "+l(n.value),1)]),s("div",f,[t[2]||(t[2]=s("p",{style:{margin:"30px 0","font-size":"15px"}},"这里是试卷题目配置与执行区域",-1)),s("button",{class:"btn",style:{width:"200px",padding:"10px",margin:"10px"},onClick:t[0]||(t[0]=o=>e.$router.push(`/papers/${d(a)}/result`))},"生成并查看结果反馈"),s("button",{class:"btn",style:{width:"200px",padding:"10px",margin:"10px"},onClick:t[1]||(t[1]=o=>e.$router.push("/papers"))},"返回列表")])]))}};export{$ as default}; diff --git a/frontend/dist/assets/PapersList-CRTR-6gM.js b/frontend/dist/assets/PapersList-CRTR-6gM.js new file mode 100644 index 0000000..2b628e8 --- /dev/null +++ b/frontend/dist/assets/PapersList-CRTR-6gM.js @@ -0,0 +1,7 @@ +import{_ as q,b as A,c as d,a as t,w as k,v as V,g as B,s as F,F as f,r as y,d as c,l as L,o as b,t as l}from"./index-Dc_QwQl6.js";import{a as _,t as C,d as M,c as U,f as j}from"./api-gDOaCrl1.js";const K={class:"page-header"},R={class:"filter-panel"},G={class:"content-area"},N={class:"card",style:{padding:"0",overflow:"auto"}},T={class:"data-table"},z=["onClick"],H={class:"center"},J=["onDblclick"],O={class:"center"},Q={class:"center"},W={class:"center"},X={class:"center"},Y={class:"center nowrap"},Z=["onClick"],tt=["onClick"],et=["onClick"],st=["onClick"],nt={__name:"PapersList",setup(at){const m=L(),p=c("未完成"),i=c(""),v=c([]),u=c("p.create_time"),o=c("DESC"),$=[{key:"id",label:"ID",db:"p.id"},{key:"name",label:"试卷名称",db:"p.paper_name"},{key:"subject",label:"科目",db:"p.subject"},{key:"count",label:"题数",db:"question_count"},{key:"time",label:"创建时间",db:"p.create_time"},{key:"status",label:"状态",db:"ps.status_name"},{key:"actions",label:"操作",db:null}];function g(n){return!n||n!==u.value?"":o.value==="ASC"?" ▲":" ▼"}function h(n){u.value===n?o.value=o.value==="ASC"?"DESC":"ASC":(u.value=n,o.value="ASC"),r()}function D(){i.value="",r()}async function r(){const n=new URLSearchParams({status:p.value,paper_id:i.value,sort_col:u.value,sort_dir:o.value}),s=await _("/papers/list?"+n.toString());v.value=s.rows}function I(n){m.push("/papers/preview?paper_id="+n)}function x(n){m.push(`/papers/${n}/result`)}async function w(n){const s=await _(`/papers/${n}/basic`),e=s.question_ids,a=s.paper_name,E=`确定要永久删除该试卷吗? + +试卷ID:${n} +试卷名称:${a} +题目数量:${e.length} 道 + +注意:删除后将同步扣减相关题目的组卷及对错统计次数!`;U(E)&&(await j(`/papers/${n}`),await r())}async function S(n){const s=await _(`/papers/${n}/basic`);if(!s.question_ids.length){C(`试卷 ${n} 暂无题目,无法下载。`);return}const e=`/StudyBase/api/papers/${n}/download`,a=document.createElement("a");a.href=e,a.download=`${s.paper_name}_${n}.docx`.replace(/\//g,"_"),document.body.appendChild(a),a.click(),a.remove()}async function P(){if(!v.value.length){C("当前列表没有数据可以导出");return}await M("/papers/list/export-excel",{status:p.value,paper_id:i.value,sort_col:u.value,sort_dir:o.value})}return A(r),(n,s)=>(b(),d("div",null,[t("div",K,[s[5]||(s[5]=t("span",{class:"title"},"试卷管理列表",-1)),t("button",{class:"btn",onClick:s[0]||(s[0]=e=>n.$router.push("/"))},"返回主页"),t("button",{class:"btn orange",style:{"margin-left":"10px"},onClick:s[1]||(s[1]=e=>n.$router.push("/papers/create-by-id"))},"💡 根据ID生成试卷"),t("button",{class:"btn green",style:{"margin-left":"10px"},onClick:s[2]||(s[2]=e=>n.$router.push("/questions?from_paper_creation=1"))},"➕ 进入题目列表选题")]),t("div",R,[s[7]||(s[7]=t("label",null,"试卷状态:",-1)),k(t("select",{"onUpdate:modelValue":s[3]||(s[3]=e=>p.value=e),onChange:D},[...s[6]||(s[6]=[t("option",null,"全部",-1),t("option",null,"未完成",-1),t("option",null,"已完成",-1),t("option",null,"部分完成",-1),t("option",null,"归档",-1)])],544),[[V,p.value]]),s[8]||(s[8]=t("label",{style:{"margin-left":"20px"}},"试卷ID:",-1)),k(t("input",{type:"text","onUpdate:modelValue":s[4]||(s[4]=e=>i.value=e),style:{width:"150px"},onKeyup:F(r,["enter"])},null,544),[[B,i.value]]),t("button",{class:"btn primary",style:{"margin-left":"10px"},onClick:r},"🔍 搜索"),t("button",{class:"btn orange",style:{"margin-left":"6px"},onClick:P},"📥 导出Excel")]),t("div",G,[t("div",N,[t("table",T,[t("thead",null,[t("tr",null,[(b(),d(f,null,y($,e=>t("th",{key:e.key,onClick:a=>e.db&&h(e.db)},l(e.label)+l(g(e.db)),9,z)),64))])]),t("tbody",null,[(b(!0),d(f,null,y(v.value,e=>(b(),d("tr",{key:e.ID},[t("td",H,l(e.ID),1),t("td",{onDblclick:a=>n.$router.push(`/papers/${e.ID}/detail`),style:{cursor:"pointer"}},l(e.试卷名称),41,J),t("td",O,l(e.科目),1),t("td",Q,l(e.题数),1),t("td",W,l((e.创建时间||"").slice(0,10)||"-"),1),t("td",X,l(e.状态),1),t("td",Y,[t("button",{class:"btn small",onClick:a=>I(e.ID)},"预览",8,Z),t("button",{class:"btn small",onClick:a=>x(e.ID)},"反馈",8,tt),t("button",{class:"btn small",onClick:a=>w(e.ID)},"删除",8,et),t("button",{class:"btn small",onClick:a=>S(e.ID)},"下载",8,st)])]))),128))])])])])]))}},rt=q(nt,[["__scopeId","data-v-baad42d8"]]);export{rt as default}; diff --git a/frontend/dist/assets/PapersList-CnsFf_No.css b/frontend/dist/assets/PapersList-CnsFf_No.css new file mode 100644 index 0000000..9b7ae09 --- /dev/null +++ b/frontend/dist/assets/PapersList-CnsFf_No.css @@ -0,0 +1 @@ +.center[data-v-baad42d8]{text-align:center}.nowrap[data-v-baad42d8]{white-space:nowrap} diff --git a/frontend/dist/assets/PapersPreview-ffjysDLy.js b/frontend/dist/assets/PapersPreview-ffjysDLy.js new file mode 100644 index 0000000..78b8723 --- /dev/null +++ b/frontend/dist/assets/PapersPreview-ffjysDLy.js @@ -0,0 +1,5 @@ +import{b as D,c as r,a,w as R,g as B,t as o,F as I,r as P,i as V,j,d as u,l as N,o as d,m as T}from"./index-Dc_QwQl6.js";import{b as p,t as v}from"./api-gDOaCrl1.js";import{R as E}from"./RichContent-vEKXmEUi.js";const Q={class:"page-header"},z=["disabled"],A={class:"content-area",style:{"padding-top":"12px"}},F={class:"card",style:{display:"flex","align-items":"center",gap:"8px"}},L={class:"card"},M={style:{"font-weight":"bold","margin-bottom":"8px"}},G={style:{"font-size":"12px",color:"#555","margin-bottom":"4px"}},J={key:0,style:{color:"#999"}},O={class:"log-box",style:{height:"120px"}},U={style:{display:"flex","justify-content":"flex-end",gap:"12px",margin:"12px 0 30px"}},H=["disabled"],Z={__name:"PapersPreview",setup(K){const _=j(),w=N(),i=u(null),n=u([]),l=u(""),c=u(""),y=u(!1),m=u([]);function f(){return new Date().toTimeString().slice(0,8)}function h(){w.push("/questions?from_paper_creation=1")}async function b(){if(!n.value.length){m.value=[];return}const t=await p("/questions/search",{filters:{},mode:"preview",id_input:n.value.join(" ")}),e={};for(const s of t.rows)e[s.id]=s;m.value=n.value.map(s=>e[s]).filter(Boolean)}async function g(){if(!n.value.length){c.value+=`[${f()}] 未选择题目,无法预览。 +`;return}try{const t=await p("/papers/preview-docx",{selected_ids:n.value,title:l.value,paper_id:i.value});c.value+=`[${f()}] 预览已生成:${t.path} +`}catch(t){c.value+=`[${f()}] 生成预览失败: ${t.message} +`}}async function x(){if(!n.value.length){v("未选择题目,无法生成复习卷。");return}try{const t=await p("/papers/review-docx",{selected_ids:n.value,title:l.value,paper_id:i.value});y.value=!0,c.value+=`[${f()}] 复习卷已生成:${t.path} +`}catch(t){v("生成复习卷失败:"+t.message)}}function k(){const t="/StudyBase/api/papers/temp-file?name=preview",e=document.createElement("a");e.href=t,e.download="temp_preview.docx",document.body.appendChild(e),e.click(),e.remove()}function C(){const t="/StudyBase/api/papers/temp-file?name=review",e=document.createElement("a");e.href=t,e.download="temp_review_preview.docx",document.body.appendChild(e),e.click(),e.remove()}async function $(){if(!n.value.length){v("未选择题目,无法生成试卷。");return}if(i.value===null)try{const t=await p("/papers/save",{selected_ids:n.value,title:l.value});i.value=t.paper_id,v(`试卷已成功保存!ID: ${t.paper_id}`),await g()}catch(t){v("保存失败:"+t.message)}}return D(async()=>{if(_.query.paper_id){i.value=parseInt(_.query.paper_id);const t=await p("/papers/preview-info",{paper_id:i.value});l.value=t.title,n.value=t.selected_ids}else{try{n.value=JSON.parse(sessionStorage.getItem("sb_selected_ids")||"[]")}catch{n.value=[]}const t=await p("/papers/preview-info",{selected_ids:n.value});l.value=t.title}await b(),await g()}),(t,e)=>(d(),r("div",null,[a("div",Q,[e[2]||(e[2]=a("span",{class:"title"},"试卷预览",-1)),i.value!==null?(d(),r("button",{key:0,class:"btn",style:{"margin-left":"16px"},onClick:e[0]||(e[0]=s=>t.$router.push("/papers"))},"返回试卷列表")):(d(),r("button",{key:1,class:"btn",style:{"margin-left":"16px"},onClick:h},"返回选题列表")),a("button",{class:"btn",onClick:k},"打开文档"),a("button",{class:"btn green",style:{"font-weight":"bold"},disabled:i.value!==null,onClick:$},"💾 确认生成试卷",8,z)]),a("div",A,[a("div",F,[e[3]||(e[3]=a("label",{style:{"font-weight":"bold"}},"试卷标题:",-1)),R(a("input",{type:"text","onUpdate:modelValue":e[1]||(e[1]=s=>l.value=s),style:{width:"420px"}},null,512),[[B,l.value]]),e[4]||(e[4]=a("span",{style:{color:"gray","font-size":"12px"}},"(可手动修改标题)",-1))]),a("div",L,[a("div",M,"试卷内容预览(共 "+o(n.value.length)+" 道题)",1),(d(!0),r(I,null,P(m.value,(s,S)=>(d(),r("div",{key:s.id,style:{"border-bottom":"1px solid #eee",padding:"10px 0"}},[a("div",G,o(S+1)+". ID:"+o(s.id)+" | "+o(s.question_type||"题目")+" | "+o(s.Subject)+" | "+o(s.source_description)+" | 组卷次数:"+o(s.paper_count)+" | 正确次数:"+o(s.right_times)+" | 错误次数:"+o(s.wrong_times)+" | 部分正确次数:"+o(s.half_right_times),1),T(E,{content:s.content,"max-width":800},null,8,["content"])]))),128)),m.value.length?V("",!0):(d(),r("div",J,"未选择题目,无法预览。"))]),a("div",O,o(c.value),1),a("div",U,[a("button",{class:"btn",disabled:!y.value,onClick:C},"打开复习卷",8,H),a("button",{class:"btn green",style:{"font-weight":"bold"},onClick:x},"生成复习卷")])])]))}};export{Z as default}; diff --git a/frontend/dist/assets/PapersResult-BN_2zJ7H.js b/frontend/dist/assets/PapersResult-BN_2zJ7H.js new file mode 100644 index 0000000..0220ff4 --- /dev/null +++ b/frontend/dist/assets/PapersResult-BN_2zJ7H.js @@ -0,0 +1 @@ +import{b as R,c as o,a as t,F as u,r as b,j as C,d as c,l as $,o as l,t as r,w as i,h as d,f as p,g as P,i as j}from"./index-Dc_QwQl6.js";import{a as B,b as g,t as _,c as D}from"./api-gDOaCrl1.js";const M={class:"content-area",style:{"padding-top":"16px"}},N={style:{"font-weight":"bold",color:"blue","font-size":"13px"}},T={style:{display:"flex","align-items":"center",gap:"14px","flex-wrap":"wrap",margin:"8px 0"}},F=["name","onUpdate:modelValue","value"],I={style:{display:"flex","align-items":"center",gap:"14px","flex-wrap":"wrap"}},A={style:{"font-size":"13px"}},E=["name","onUpdate:modelValue"],G={style:{"font-size":"13px"}},L=["name","onUpdate:modelValue"],S={style:{"font-size":"13px"}},H=["name","onUpdate:modelValue"],J={style:{"font-size":"13px"}},K=["name","onUpdate:modelValue"],O=["onUpdate:modelValue"],Q={style:{display:"flex","align-items":"center",gap:"8px",margin:"8px 0"}},W={style:{"font-size":"13px"}},X={style:{display:"flex","align-items":"center",gap:"8px"}},Y={style:{"font-size":"13px"}},Z={style:{display:"flex",gap:"12px","justify-content":"center",margin:"20px 0 40px"}},q=["disabled"],ee=["disabled"],ne={__name:"PapersResult",setup(te){const h=C(),x=$(),m=parseInt(h.params.id),y=c([]),v=c([]),f=c(!1);function w(a){return a===1?"正确":a===0?"未判定/未选择":a===2?"部分错误":a===3?"错误":""}async function V(){const a=await B(`/papers/${m}/result`);v.value=a.causes,f.value=a.has_pending,y.value=a.questions.map(s=>({...s,_result:"无",_error:"无",_partial:""}))}async function k(){const s=y.value.filter(e=>e.result_type===0).map(e=>({question_id:e.question_id,result:e._result,error_cause:e._error,partial_desc:e._partial}));try{await g(`/papers/${m}/result/confirm`,{results:s}),_("结果已保存到数据库中!"),x.push("/papers")}catch(e){_("保存失败:"+e.message)}}async function z(){if(D("确定要归档此试卷吗?"))try{await g(`/papers/${m}/archive`),_("试卷已归档!"),x.push("/papers")}catch(a){_("归档失败:"+a.message)}}return R(V),(a,s)=>(l(),o("div",null,[s[9]||(s[9]=t("div",{class:"page-header"},[t("span",{class:"title"},"试卷结果反馈")],-1)),t("div",M,[(l(!0),o(u,null,b(y.value,e=>(l(),o("div",{key:e.question_id,class:"card"},[t("div",N," 试题ID: "+r(e.question_id)+", 科目: "+r(e.subject),1),e.result_type===0?(l(),o(u,{key:0},[t("div",T,[s[1]||(s[1]=t("span",{style:{"font-weight":"bold","font-size":"13px"}},"错误原因反馈:",-1)),(l(!0),o(u,null,b(v.value,n=>(l(),o("label",{key:n,style:{"font-size":"13px"}},[i(t("input",{type:"radio",name:"err_"+e.question_id,"onUpdate:modelValue":U=>e._error=U,value:n},null,8,F),[[d,e._error]]),p(" "+r(n),1)]))),128))]),t("div",I,[s[6]||(s[6]=t("span",{style:{"font-weight":"bold","font-size":"13px"}},"试题结果:",-1)),t("label",A,[i(t("input",{type:"radio",name:"res_"+e.question_id,"onUpdate:modelValue":n=>e._result=n,value:"正确"},null,8,E),[[d,e._result]]),s[2]||(s[2]=p(" 正确",-1))]),t("label",G,[i(t("input",{type:"radio",name:"res_"+e.question_id,"onUpdate:modelValue":n=>e._result=n,value:"错误"},null,8,L),[[d,e._result]]),s[3]||(s[3]=p(" 错误",-1))]),t("label",S,[i(t("input",{type:"radio",name:"res_"+e.question_id,"onUpdate:modelValue":n=>e._result=n,value:"未完成"},null,8,H),[[d,e._result]]),s[4]||(s[4]=p(" 未完成",-1))]),t("label",J,[i(t("input",{type:"radio",name:"res_"+e.question_id,"onUpdate:modelValue":n=>e._result=n,value:"部分错误"},null,8,K),[[d,e._result]]),s[5]||(s[5]=p(" 部分错误",-1))]),e._result==="部分错误"?i((l(),o("input",{key:0,type:"text","onUpdate:modelValue":n=>e._partial=n,placeholder:"错误小题描述",style:{width:"260px"}},null,8,O)),[[P,e._partial]]):j("",!0)])],64)):(l(),o(u,{key:1},[t("div",Q,[s[7]||(s[7]=t("span",{style:{"font-weight":"bold","font-size":"13px"}},"错误原因反馈:",-1)),t("span",W,r(e.error_cause_text),1)]),t("div",X,[s[8]||(s[8]=t("span",{style:{"font-weight":"bold","font-size":"13px"}},"试题结果:",-1)),t("span",Y,r(w(e.result_type)),1)])],64))]))),128)),t("div",Z,[t("button",{class:"btn",style:{width:"130px"},disabled:!f.value,onClick:k},"确定结果",8,q),t("button",{class:"btn",style:{width:"130px"},disabled:!f.value,onClick:z},"试卷归档",8,ee),t("button",{class:"btn",style:{width:"130px"},onClick:s[0]||(s[0]=e=>a.$router.push("/papers"))},"返回试卷列表")])])]))}};export{ne as default}; diff --git a/frontend/dist/assets/QuestionAdd-MfLONVTZ.js b/frontend/dist/assets/QuestionAdd-MfLONVTZ.js new file mode 100644 index 0000000..f765995 --- /dev/null +++ b/frontend/dist/assets/QuestionAdd-MfLONVTZ.js @@ -0,0 +1 @@ +import{_ as o,c as i,a as t,o as l}from"./index-Dc_QwQl6.js";const d={},p={class:"nav-bar"},a={style:{padding:"20px"}},r={class:"card",style:{"max-width":"560px"}},u={style:{display:"flex","flex-direction":"column",gap:"14px","margin-top":"24px"}};function x(n,s){return l(),i("div",null,[t("div",p,[t("button",{class:"btn",onClick:s[0]||(s[0]=e=>n.$router.push("/questions"))},"< 返回"),s[3]||(s[3]=t("span",{class:"nav-title"},"新增题目",-1))]),t("div",a,[t("div",r,[s[4]||(s[4]=t("div",{style:{display:"flex","align-items":"center",gap:"10px",margin:"10px 0"}},[t("label",null,"题目内容:"),t("input",{type:"text",style:{flex:"1"}})],-1)),t("div",u,[t("button",{class:"btn",style:{background:"#E1E1E1",padding:"8px",width:"180px"},onClick:s[1]||(s[1]=e=>n.$router.push("/questions/import"))},"文件导入"),t("button",{class:"btn",style:{padding:"8px",width:"180px"},onClick:s[2]||(s[2]=e=>n.$router.push("/questions"))},"取消返回")])])])])}const f=o(d,[["render",x]]);export{f as default}; diff --git a/frontend/dist/assets/QuestionDetail-iaLTMiSx.css b/frontend/dist/assets/QuestionDetail-iaLTMiSx.css new file mode 100644 index 0000000..56d79bc --- /dev/null +++ b/frontend/dist/assets/QuestionDetail-iaLTMiSx.css @@ -0,0 +1 @@ +.form-row[data-v-88827f0e]{display:flex;align-items:center;gap:10px;margin:8px 0}.form-row label[data-v-88827f0e]{white-space:nowrap;font-size:13px;min-width:190px;text-align:right} diff --git a/frontend/dist/assets/QuestionDetail-s29u4nvx.js b/frontend/dist/assets/QuestionDetail-s29u4nvx.js new file mode 100644 index 0000000..e6b501e --- /dev/null +++ b/frontend/dist/assets/QuestionDetail-s29u4nvx.js @@ -0,0 +1 @@ +import{_ as A,b as Q,c as u,a as e,t as j,u as L,w as i,v as E,F as C,r as O,g as y,m as I,i as W,j as Y,d as a,o as d}from"./index-Dc_QwQl6.js";import{a as z,t as b,c as H,e as J}from"./api-gDOaCrl1.js";import{R as U}from"./RichContent-vEKXmEUi.js";const K={class:"page-header"},X={class:"title"},Z={class:"content-area",style:{"padding-top":"14px","max-width":"980px"}},tt={class:"card"},et={class:"form-row"},st=["disabled"],nt=["value"],lt={class:"form-row"},ot=["disabled"],at={class:"form-row"},it=["disabled"],ut={class:"form-row"},dt=["disabled"],rt={class:"form-row"},vt=["disabled"],pt=["disabled"],mt={class:"card"},ct={class:"card"},ft={class:"card"},yt={style:{"margin-bottom":"6px"}},bt={class:"card"},gt={style:{"margin-bottom":"6px"}},wt={style:{display:"flex","justify-content":"flex-end",gap:"10px",margin:"14px 0 30px"}},xt={__name:"QuestionDetail",setup(_t){const F=Y(),g=parseInt(F.params.id),w=a({}),V=a([]),v=a(""),p=a("客观题"),m=a("错题"),x=a(""),r=a(""),c=a(""),f=a(""),o=a(!1),_=a(null),k=a(null);async function D(){const n=await z(`/questions/${g}`);w.value=n,V.value=n.subject_options||[],v.value=n.Subject||"",p.value=n.is_subjective===1?"主观题":"客观题",m.value=n.question_type||"错题",x.value=n.knowledge_points_desc||"",r.value=n.answer_time||"",c.value=n.content||"",f.value=n.analysis||""}function T(){o.value=!0}async function P(){H("确定取消编辑吗?")&&(await D(),o.value=!1)}function B(){const n=new Date,t=s=>String(s).padStart(2,"0");r.value=`${n.getFullYear()}-${t(n.getMonth()+1)}-${t(n.getDate())}`}async function G(){try{await J(`/questions/${g}`,{subject:v.value,question_type:m.value,answer_time:r.value,is_subjective:p.value==="主观题",content_raw:c.value.trim(),analysis_raw:f.value.trim()}),b("已更新!"),await D(),o.value=!1}catch(n){b("错误:"+n.message)}}async function R(n){const t=new FormData;t.append("file",n,n.name||"pasted.png");const s=await fetch("/StudyBase/api/import/upload-image",{method:"POST",body:t});if(!s.ok){let l="上传失败";try{l=(await s.json()).detail||l}catch{}throw new Error(l)}return(await s.json()).path}function S(n,t){if(n&&n.selectionStart!=null){const s=n.selectionStart,l=n.value;n.value=l.slice(0,s)+t+l.slice(n.selectionEnd),n.dispatchEvent(new Event("input"))}else n&&(n.value+=t,n.dispatchEvent(new Event("input")))}function M(n){const t=document.createElement("input");t.type="file",t.accept="image/png,image/jpeg,image/bmp,image/gif",t.onchange=async()=>{const s=t.files[0];if(s)try{const l=await R(s);S(n.value||n,`[[IMG:${l}]]`)}catch(l){b(l.message)}},t.click()}async function h(n,t){const s=n.clipboardData&&n.clipboardData.items;if(s){for(const l of s)if(l.type&&l.type.startsWith("image/")){n.preventDefault();const q=l.getAsFile();try{const $=await R(q),N=t==="stem"?_.value:k.value;S(N,`[[IMG:${$}]]`)}catch($){b($.message)}return}}}return Q(D),(n,t)=>(d(),u("div",null,[e("div",K,[e("span",X,"修改题目信息 (ID: "+j(L(g))+")",1)]),e("div",Z,[e("div",tt,[e("div",et,[t[13]||(t[13]=e("label",null,"所属科目:",-1)),i(e("select",{"onUpdate:modelValue":t[0]||(t[0]=s=>v.value=s),disabled:!o.value},[(d(!0),u(C,null,O(V.value,s=>(d(),u("option",{key:s,value:s},j(s),9,nt))),128))],8,st),[[E,v.value]])]),e("div",lt,[t[15]||(t[15]=e("label",null,"题目分类(必选):",-1)),i(e("select",{"onUpdate:modelValue":t[1]||(t[1]=s=>p.value=s),disabled:!o.value},[...t[14]||(t[14]=[e("option",null,"客观题",-1),e("option",null,"主观题",-1)])],8,ot),[[E,p.value]])]),e("div",at,[t[17]||(t[17]=e("label",null,"题目类型(必选):",-1)),i(e("select",{"onUpdate:modelValue":t[2]||(t[2]=s=>m.value=s),disabled:!o.value},[...t[16]||(t[16]=[e("option",null,"错题",-1),e("option",null,"推题",-1)])],8,it),[[E,m.value]])]),e("div",ut,[t[18]||(t[18]=e("label",null,"题目知识点(必填):",-1)),i(e("input",{type:"text","onUpdate:modelValue":t[3]||(t[3]=s=>x.value=s),style:{width:"420px"},disabled:!o.value},null,8,dt),[[y,x.value]])]),e("div",rt,[t[19]||(t[19]=e("label",null,"答题时间(选填,例:2026-03-16):",-1)),i(e("input",{type:"text","onUpdate:modelValue":t[4]||(t[4]=s=>r.value=s),style:{width:"150px"},disabled:!o.value},null,8,vt),[[y,r.value]]),e("button",{class:"btn small",disabled:!o.value,onClick:B},"填入今天",8,pt)])]),o.value?(d(),u(C,{key:1},[e("div",ft,[t[22]||(t[22]=e("div",{style:{"font-weight":"bold","margin-bottom":"6px"}},"题干内容",-1)),e("div",yt,[e("button",{class:"btn small",onClick:t[5]||(t[5]=s=>M(_.value))},"从本地选择图片")]),i(e("textarea",{ref_key:"stemRef",ref:_,"onUpdate:modelValue":t[6]||(t[6]=s=>c.value=s),rows:"8",style:{width:"100%"},onPaste:t[7]||(t[7]=s=>h(s,"stem"))},null,544),[[y,c.value]])]),e("div",bt,[t[23]||(t[23]=e("div",{style:{"font-weight":"bold","margin-bottom":"6px"}},"答案解析",-1)),e("div",gt,[e("button",{class:"btn small",onClick:t[8]||(t[8]=s=>M(k.value))},"从本地选择图片")]),i(e("textarea",{ref_key:"analysisRef",ref:k,"onUpdate:modelValue":t[9]||(t[9]=s=>f.value=s),rows:"6",style:{width:"100%"},onPaste:t[10]||(t[10]=s=>h(s,"analysis"))},null,544),[[y,f.value]])])],64)):(d(),u(C,{key:0},[e("div",mt,[t[20]||(t[20]=e("div",{style:{"font-weight":"bold","margin-bottom":"6px"}},"【题干】",-1)),I(U,{content:w.value.content,"max-width":850},null,8,["content"])]),e("div",ct,[t[21]||(t[21]=e("div",{style:{"font-weight":"bold","margin-bottom":"6px",color:"#2E7D32"}},"【答案解析】",-1)),I(U,{content:w.value.analysis,"max-width":850},null,8,["content"])])],64)),e("div",wt,[o.value?(d(),u("button",{key:0,class:"btn",onClick:P},"取消")):W("",!0),e("button",{class:"btn primary",onClick:t[11]||(t[11]=s=>o.value?G():T())},j(o.value?"确定保存":"编辑"),1),e("button",{class:"btn",onClick:t[12]||(t[12]=s=>n.$router.push("/questions"))},"返回列表")])])]))}},jt=A(xt,[["__scopeId","data-v-88827f0e"]]);export{jt as default}; diff --git a/frontend/dist/assets/QuestionList-Dv5J_SvP.css b/frontend/dist/assets/QuestionList-Dv5J_SvP.css new file mode 100644 index 0000000..0eab71e --- /dev/null +++ b/frontend/dist/assets/QuestionList-Dv5J_SvP.css @@ -0,0 +1 @@ +.center[data-v-fb1ccf35]{text-align:center} diff --git a/frontend/dist/assets/QuestionList-cbNQnKl-.js b/frontend/dist/assets/QuestionList-cbNQnKl-.js new file mode 100644 index 0000000..1b17265 --- /dev/null +++ b/frontend/dist/assets/QuestionList-cbNQnKl-.js @@ -0,0 +1 @@ +import{_ as ie,b as ae,c as a,a as s,t as n,u as M,F as v,r as x,w as k,g as N,h as R,f as E,i as A,j as oe,k as V,d as p,l as re,o,v as ue,n as de,e as ce,m as B}from"./index-Dc_QwQl6.js";import{a as pe,b as O,t as _,c as L,d as _e}from"./api-gDOaCrl1.js";import{R as T}from"./RichContent-vEKXmEUi.js";const be={class:"page-header"},fe={class:"title"},ye={class:"filter-panel"},ve={class:"row"},he=["onUpdate:modelValue"],ke=["value"],me={class:"row"},ge={class:"row",style:{"justify-content":"space-between"}},xe={style:{display:"flex","align-items":"center",gap:"14px"}},Ce={style:{display:"flex","align-items":"center",gap:"10px"}},we={style:{color:"#E64A19","font-weight":"bold","font-size":"13px"}},qe={class:"content-area"},Ie={key:0,class:"card",style:{padding:"0",overflow:"auto"}},De={class:"data-table"},je=["onClick"],Fe=["onClick","onDblclick"],Se={class:"center"},Ve={class:"center"},Oe={class:"center"},$e={class:"center"},Ue={class:"center"},ze={class:"center"},Me={class:"center"},Ne={class:"center"},Re={class:"center"},Ee={class:"center"},Ae={class:"center"},Be={class:"center"},Le={key:1},Te={style:{display:"flex"}},Ke={style:{display:"flex","flex-direction":"column","justify-content":"space-between","align-items":"center",padding:"0 10px"}},Pe=["onUpdate:modelValue"],Qe=["onClick"],Ge={style:{flex:"1","min-width":"0"}},Je={style:{color:"#1890FF","font-weight":"bold","font-size":"13px","margin-bottom":"6px","word-break":"break-all"}},He={__name:"QuestionList",setup(We){const b=oe(),K=re(),P=b.query.from_paper_creation==="1"||b.query.from_paper_creation==="true",C=["全部","0","1","2","3","4次及以上"],$=[{key:"subject",label:"科目",type:"db"},{key:"status",label:"状态",type:"fixed",options:["全部","未完成","正确","错误","部分正确"]},{key:"archive_status",label:"归档",type:"fixed",options:["全部","未归档","已归档"]},{key:"cause",label:"原因",type:"db"},{key:"source",label:"来源",type:"db"},{key:"term",label:"学期",type:"db"},{key:"stage",label:"阶段",type:"db"},{key:"question_nature",label:"试题类型",type:"fixed",options:["全部","错题","推题"]},{key:"paper_count",label:"组卷次数",type:"fixed",options:C},{key:"right_times",label:"正确次数",type:"fixed",options:C},{key:"wrong_times",label:"错误次数",type:"fixed",options:C},{key:"half_right_times",label:"部分正确次数",type:"fixed",options:C}],Q=$,r=V({});for(const e of $)r[e.key]="全部";r.archive_status="未归档";const U=p({}),w=p(""),q=p(""),d=p("table"),c=p([]),I=p(0),f=V({}),i=V(new Set),D=p(null),j=p(null),F=p(null),m=p("q.id"),h=p(!1),S=[{key:"subject",label:"科目",db:"subject_id"},{key:"id",label:"试题ID",db:"q.id"},{key:"latest_status",label:"掌握状态",db:"latest_status"},{key:"question_type",label:"题目类型",db:"question_type"},{key:"kp",label:"题目知识点",db:"knowledge_points_desc"},{key:"answer_time",label:"答题时间",db:"answer_time"},{key:"input_time",label:"录入时间",db:"input_time"},{key:"p_count",label:"组卷次数",db:"paper_count"},{key:"r_count",label:"正确次数",db:"right_times"},{key:"w_count",label:"错误次数",db:"wrong_times"},{key:"h_count",label:"部分正确次数",db:"half_right_times"},{key:"last_r_time",label:"最晚正确时间",db:"last_right_time"},{key:"paper_ids",label:"所属试卷ID",db:null}];function G(e){return e.type==="fixed"?e.options:["全部",...U.value[e.key]||[]]}function z(e){return{right:"正确",wrong:"错误",half_right:"部分正确"}[e]||"未完成"}function g(){return d.value==="table"?[...i]:Object.entries(f).filter(([,e])=>e).map(([e])=>parseInt(e))}function J(e,l){if(l&&(l.ctrlKey||l.metaKey||l.shiftKey))i.has(e)?i.delete(e):i.add(e);else{const t=i.size===1&&i.has(e);i.clear(),t||i.add(e)}}function H(e){const l=S.find(t=>t.key===e);!l||!l.db||(m.value===l.db?h.value=!h.value:(m.value=l.db,h.value=!1),y())}function W(e){const l=S.find(t=>t.key===e);return!l||l.db!==m.value?"":h.value?" ▼":" ▲"}async function y(){const e={filters:{...r},id_input:w.value,kp_input:q.value,chapter_id_filter:D.value,kp_id_filter:j.value,right_times_filter:F.value,mode:d.value,sort_col:m.value,sort_desc:h.value},l=await O("/questions/search",e);c.value=l.rows,I.value=l.count,i.clear();for(const t of Object.keys(f))delete f[t]}function X(){if(d.value==="table")for(const e of c.value)i.add(e.id);else for(const e of c.value)f[e.id]=!0}function Y(){if(d.value==="table")for(const e of c.value)i.has(e.id)?i.delete(e.id):i.add(e.id);else for(const e of c.value)f[e.id]=!f[e.id]}function Z(){const e=g();if(!e.length){_("请至少选择一道题目");return}sessionStorage.setItem("sb_selected_ids",JSON.stringify(e)),K.push("/papers/preview")}async function ee(){if(d.value==="table"){_("表格模式下不支持批量删除,请切换到预览模式通过复选框操作。");return}const e=g();if(!e.length){_("请勾选题目");return}L(`确定删除选中的 ${e.length} 道题吗?`)&&(await O("/questions/delete",{ids:e}),await y())}async function te(){const e=g();if(!e.length){_("请选择要归档的题目");return}L("确定要将选中的题目归档吗?")&&(await O("/questions/bulk-archive",{ids:e}),_(`成功归档 ${e.length} 道题目。`),await y())}async function se(){const e=g();if(!e.length){_("请选择题目");return}try{await navigator.clipboard.writeText(e.join(",")),_("ID已复制到剪贴板")}catch(l){_("复制失败:"+l.message)}}function le(){const e=new Set(g());if(!e.size){_("请先选择要从列表中移除的题目");return}c.value=c.value.filter(l=>!e.has(l.id)),I.value=c.value.length,i.clear();for(const l of e)delete f[l]}async function ne(){if(!c.value.length){_("当前列表没有数据可以导出");return}await _e("/questions/export-excel",{filters:{...r},id_input:w.value,kp_input:q.value,chapter_id_filter:D.value,kp_id_filter:j.value,right_times_filter:F.value,mode:"table",sort_col:m.value,sort_desc:h.value})}return ae(async()=>{U.value=await pe("/questions/filter-options"),b.query.chapter_id&&(D.value=parseInt(b.query.chapter_id)),b.query.kp_id&&(j.value=parseInt(b.query.kp_id)),b.query.right_times&&(F.value=b.query.right_times);const e=b.query;e.subject&&(r.subject=e.subject),e.stage&&(r.stage=e.stage),e.term&&(r.term=e.term),e.status&&(r.status=e.status),e.archive_status&&(r.archive_status=e.archive_status),e.question_nature&&(r.question_nature=e.question_nature),e.paper_count&&(r.paper_count=e.paper_count),await y()}),(e,l)=>(o(),a("div",null,[s("div",be,[s("span",fe,n(M(P)?"选择题目创建试卷":"题目列表管理"),1),s("button",{class:"btn",style:{"margin-left":"16px"},onClick:l[0]||(l[0]=t=>e.$router.push("/"))},"返回主页"),s("button",{class:"btn green",style:{"margin-left":"10px"},onClick:Z},"➕ 创建试卷"),s("button",{class:"btn green",style:{"margin-left":"10px"},onClick:l[1]||(l[1]=t=>e.$router.push("/questions/add"))},"➕ 新增题目")]),s("div",{style:{padding:"10px 20px 0",display:"flex",gap:"8px","align-items":"center","flex-wrap":"wrap"}},[s("button",{class:"btn small",onClick:X},"全选"),s("button",{class:"btn small",onClick:Y},"反选"),s("button",{class:"btn small",style:{color:"red","font-weight":"bold"},onClick:ee},"❗️ 从数据库中删除"),s("button",{class:"btn small",style:{color:"#F57C00","font-weight":"bold"},onClick:te},"📦 批量归档"),s("button",{class:"btn small",style:{color:"#1890FF","font-weight":"bold"},onClick:se},"📋 复制选中题目ID"),s("button",{class:"btn small",style:{color:"#1b5e20","font-weight":"bold"},onClick:le},"❎ 从列表中移除")]),s("div",ye,[s("div",ve,[(o(!0),a(v,null,x(M(Q),t=>(o(),a(v,{key:t.key},[s("label",null,n(t.label)+":",1),k(s("select",{"onUpdate:modelValue":u=>r[t.key]=u},[(o(!0),a(v,null,x(G(t),u=>(o(),a("option",{key:u,value:u},n(u),9,ke))),128))],8,he),[[ue,r[t.key]]])],64))),128))]),s("div",me,[l[6]||(l[6]=s("label",null,"ID:",-1)),k(s("input",{type:"text","onUpdate:modelValue":l[2]||(l[2]=t=>w.value=t),style:{width:"150px"}},null,512),[[N,w.value]]),l[7]||(l[7]=s("label",{style:{"margin-left":"10px"}},"知识点:",-1)),k(s("input",{type:"text","onUpdate:modelValue":l[3]||(l[3]=t=>q.value=t),style:{width:"200px"}},null,512),[[N,q.value]])]),s("div",ge,[s("div",xe,[s("label",null,[k(s("input",{type:"radio",value:"table","onUpdate:modelValue":l[4]||(l[4]=t=>d.value=t),onChange:y},null,544),[[R,d.value]]),l[8]||(l[8]=E(" 表格模式",-1))]),s("label",null,[k(s("input",{type:"radio",value:"preview","onUpdate:modelValue":l[5]||(l[5]=t=>d.value=t),onChange:y},null,544),[[R,d.value]]),l[9]||(l[9]=E(" 预览模式",-1))])]),s("div",Ce,[s("span",we,"共找到 "+n(I.value)+" 道题目",1),s("button",{class:"btn primary",onClick:y},"🔍 开始搜索"),d.value==="table"?(o(),a("button",{key:0,class:"btn orange",onClick:ne},"📥 导出Excel")):A("",!0)])])]),s("div",qe,[d.value==="table"?(o(),a("div",Ie,[s("table",De,[s("thead",null,[s("tr",null,[(o(),a(v,null,x(S,t=>s("th",{key:t.key,onClick:u=>H(t.key)},n(t.label)+n(W(t.key)),9,je)),64))])]),s("tbody",null,[(o(!0),a(v,null,x(c.value,t=>(o(),a("tr",{key:t.id,class:de(["clickable",{selected:i.has(t.id)}]),onClick:u=>J(t.id,u),onDblclick:u=>e.$router.push("/questions/"+t.id)},[s("td",Se,n(t.Subject),1),s("td",Ve,n(t.id),1),s("td",Oe,n(z(t.latest_status)),1),s("td",$e,n(t.question_type),1),s("td",null,n(t.knowledge_points_desc),1),s("td",Ue,n(t.answer_time),1),s("td",ze,n(t.input_time),1),s("td",Me,n(t.paper_count),1),s("td",Ne,n(t.right_times),1),s("td",Re,n(t.wrong_times),1),s("td",Ee,n(t.half_right_times),1),s("td",Ae,n(t.last_right_time),1),s("td",Be,n(t.paper_ids),1)],42,Fe))),128))])])])):(o(),a("div",Le,[(o(!0),a(v,null,x(c.value,t=>(o(),a("div",{class:"card",key:t.id},[s("div",Te,[s("div",Ke,[k(s("input",{type:"checkbox","onUpdate:modelValue":u=>f[t.id]=u},null,8,Pe),[[ce,f[t.id]]]),s("button",{class:"btn small",onClick:u=>e.$router.push("/questions/"+t.id)},"详情",8,Qe)]),s("div",Ge,[s("div",Je," ID: "+n(t.id)+" | 科目: "+n(t.Subject)+" | 状态: "+n(z(t.latest_status))+" | 归档: "+n(t.StatusName||"未归档")+" | 知识点: "+n(t.knowledge_points_desc),1),l[11]||(l[11]=s("div",{style:{"font-weight":"bold","font-size":"13px",margin:"4px 0 2px"}},"【题干】",-1)),B(T,{content:t.content,"max-width":800},null,8,["content"]),t.analysis?(o(),a(v,{key:0},[l[10]||(l[10]=s("div",{style:{"font-weight":"bold","font-size":"13px",margin:"6px 0 2px",color:"#2E7D32"}},"【解析】",-1)),B(T,{content:t.analysis,"max-width":800},null,8,["content"])],64)):A("",!0)])])]))),128))]))])]))}},lt=ie(He,[["__scopeId","data-v-fb1ccf35"]]);export{lt as default}; diff --git a/frontend/dist/assets/RichContent-i88-frEH.css b/frontend/dist/assets/RichContent-i88-frEH.css new file mode 100644 index 0000000..f18bce3 --- /dev/null +++ b/frontend/dist/assets/RichContent-i88-frEH.css @@ -0,0 +1 @@ +.img-error[data-v-d3039138]{color:#ff4d4f;font-size:13px;font-family:Microsoft YaHei,sans-serif} diff --git a/frontend/dist/assets/RichContent-vEKXmEUi.js b/frontend/dist/assets/RichContent-vEKXmEUi.js new file mode 100644 index 0000000..6a5749e --- /dev/null +++ b/frontend/dist/assets/RichContent-vEKXmEUi.js @@ -0,0 +1 @@ +import{_ as k,o as n,c as i,F as f,r as v,p as S,i as h,t as W,x as E,k as R}from"./index-Dc_QwQl6.js";const B={class:"rich-text"},C=["src","onError"],I={key:1,class:"img-error"},M={key:2},P={__name:"RichContent",props:{content:{type:String,default:""},maxWidth:{type:Number,default:800}},setup(m){const a=m;function _(p){const r=String(p||""),e=[],l=/!\[\]\s*\(\s*(.*?)\s*\)/g;let t=0,s;for(;(s=l.exec(r))!==null;){s.index>t&&e.push({type:"text",text:r.slice(t,s.index)});const d=s[1].trim();let c=d,u=null;if(d.includes("?")){const[g,w]=d.split("?",2);c=g;const x=w.match(/w=(\d+)/);x&&(u=parseInt(x[1]))}c=c.replace(/\\/g,"/").replace(/ /g,"");let o=null;u&&(o=Math.max(20,Math.round(u/9525)),o>a.maxWidth&&(o=a.maxWidth)),e.push(R({type:"img",src:"/StudyBase/"+encodeURI(c),width:o,failed:!1})),t=s.index+s[0].length}return t_(a.content));return(p,r)=>(n(),i("div",B,[(n(!0),i(f,null,v(y.value,(e,l)=>(n(),i(f,{key:l},[e.type==="img"&&!e.failed?(n(),i("img",{key:0,src:e.src,style:S({width:e.width?e.width+"px":void 0,maxWidth:m.maxWidth+"px"}),onError:t=>e.failed=!0},null,44,C)):h("",!0),e.type==="img"&&e.failed?(n(),i("span",I,"[图片加载错误]")):h("",!0),e.type==="text"?(n(),i("span",M,W(e.text),1)):h("",!0)],64))),128))]))}},N=k(P,[["__scopeId","data-v-d3039138"]]);export{N as R}; diff --git a/frontend/dist/assets/Settings-CzRRQaVw.js b/frontend/dist/assets/Settings-CzRRQaVw.js new file mode 100644 index 0000000..e045c7c --- /dev/null +++ b/frontend/dist/assets/Settings-CzRRQaVw.js @@ -0,0 +1 @@ +import{_ as e,c as r,a as s,o as i}from"./index-Dc_QwQl6.js";const l={},d={class:"nav-bar"},u={class:"content-area",style:{padding:"26px 50px"}};function b(n,t){return i(),r("div",null,[s("div",d,[s("button",{class:"btn",onClick:t[0]||(t[0]=o=>n.$router.push("/"))},"< 返回主页"),t[5]||(t[5]=s("span",{class:"nav-title"},"系统设定",-1))]),s("div",u,[t[6]||(t[6]=s("div",{style:{"font-weight":"bold","font-size":"14px","margin-bottom":"14px"}},"基础数据管理",-1)),s("button",{class:"btn setting-btn",onClick:t[1]||(t[1]=o=>n.$router.push("/settings/subjects"))},"科目管理"),s("button",{class:"btn setting-btn",onClick:t[2]||(t[2]=o=>n.$router.push("/settings/error-causes"))},"错误原因管理"),s("button",{class:"btn setting-btn",onClick:t[3]||(t[3]=o=>n.$router.push("/settings/question-statuses"))},"题目状态管理"),t[7]||(t[7]=s("hr",{style:{border:"none","border-top":"1px solid #ddd",margin:"22px 0"}},null,-1)),t[8]||(t[8]=s("div",{style:{"font-weight":"bold","font-size":"14px","margin-bottom":"14px"}},"应用设定",-1)),s("button",{class:"btn setting-btn",onClick:t[4]||(t[4]=o=>n.$router.push("/settings/default-params"))},"默认参数设定")])])}const a=e(l,[["render",b],["__scopeId","data-v-dc043075"]]);export{a as default}; diff --git a/frontend/dist/assets/Settings-pzGFQctT.css b/frontend/dist/assets/Settings-pzGFQctT.css new file mode 100644 index 0000000..b001fef --- /dev/null +++ b/frontend/dist/assets/Settings-pzGFQctT.css @@ -0,0 +1 @@ +.setting-btn[data-v-dc043075]{display:block;width:100%;text-align:left;padding:9px 12px;margin:6px 0;font-size:13px} diff --git a/frontend/dist/assets/SettingsDefaultParams-Bwa8C6ww.js b/frontend/dist/assets/SettingsDefaultParams-Bwa8C6ww.js new file mode 100644 index 0000000..bdb5465 --- /dev/null +++ b/frontend/dist/assets/SettingsDefaultParams-Bwa8C6ww.js @@ -0,0 +1 @@ +import{b as g,c as n,a as e,F as p,r as c,x as y,d as _,o,t as i,w as v,v as b,p as k}from"./index-Dc_QwQl6.js";import{a as x,t as d,b as h}from"./api-gDOaCrl1.js";const w={class:"nav-bar"},D={class:"content-area",style:{padding:"26px 40px","max-width":"760px"}},$={style:{"font-weight":"bold","margin-bottom":"10px"}},q={style:{display:"flex","align-items":"center",gap:"10px"}},S=["onUpdate:modelValue"],B=["value"],C=["onClick"],A={__name:"SettingsDefaultParams",setup(M){const r=_(null),m=y(()=>r.value?[{key:"term",table:"term",title:"默认学期设定",label:"默认学期"},{key:"stage",table:"stage",title:"默认阶段设定",label:"默认阶段"},{key:"question_origin",table:"question_origin",title:"默认题目出处设定",label:"默认出处"}].map(a=>{const t=r.value[a.key];return{...a,options:t.options,selected:t.default||t.options[0]||"",display:t.default_display,hasDefault:!!t.default}}):[]);async function u(){r.value=await x("/settings/default-params")}async function f(s){if(!s.selected){d("请选择一个选项!");return}const a={term:"默认term",stage:"默认stage",question_origin:"默认question_origin"};try{await h(`/settings/default-params/${s.table}`,{name:s.selected}),d(`成功:${a[s.table]}已设置为 '${s.selected}'`),await u()}catch(t){d(t.message)}}return g(u),(s,a)=>(o(),n("div",null,[e("div",w,[e("button",{class:"btn",onClick:a[0]||(a[0]=t=>s.$router.push("/settings"))},"< 返回系统设定"),a[1]||(a[1]=e("span",{class:"nav-title"},"默认参数设定",-1))]),e("div",D,[(o(!0),n(p,null,c(m.value,t=>(o(),n("div",{class:"card",key:t.key},[e("div",$,i(t.title),1),e("div",q,[e("label",null,"选择"+i(t.label)+":",1),v(e("select",{"onUpdate:modelValue":l=>t.selected=l,style:{flex:"1"}},[(o(!0),n(p,null,c(t.options,l=>(o(),n("option",{key:l,value:l},i(l),9,B))),128))],8,S),[[b,t.selected]]),e("button",{class:"btn",onClick:l=>f(t)},"确定",8,C)]),e("div",{style:k([{"margin-top":"10px","font-size":"13px"},{color:t.hasDefault?"green":"gray"}])}," 当前默认:"+i(t.display),5)]))),128))])]))}};export{A as default}; diff --git a/frontend/dist/assets/SettingsErrorCauses-2WPrQUsX.js b/frontend/dist/assets/SettingsErrorCauses-2WPrQUsX.js new file mode 100644 index 0000000..91d971f --- /dev/null +++ b/frontend/dist/assets/SettingsErrorCauses-2WPrQUsX.js @@ -0,0 +1,2 @@ +import{a as B,t as i,g as _,e as P,b as v,c as z}from"./api-gDOaCrl1.js";import{_ as D,y as F,b as V,c as o,a as n,F as C,r as y,t as r,x as k,d as m,o as u,n as w,i as A}from"./index-Dc_QwQl6.js";const G={class:"nav-bar"},I={class:"content-area",style:{padding:"20px",display:"flex",gap:"14px"}},L={class:"card",style:{flex:"1","min-height":"400px"}},M={class:"cause-tree"},R=["onClick"],T={key:0},j=["onClick"],q={style:{padding:"0 20px",color:"#555","font-size":"12px"}},H={__name:"SettingsErrorCauses",setup(J){const p=m([]),a=m(null),f=m("当前状态: -"),x=k(()=>{const t={},s=[];for(const e of p.value)e.parent_id===null?s.push(e):(t[e.parent_id]||(t[e.parent_id]=[]),t[e.parent_id].push(e));return s.map(e=>({...e,children:t[e.id]||[]}))}),d=k(()=>p.value.find(t=>t.id===a.value));function h(){const t=d.value;if(t){const s=t.parent_id===null?"顶级原因":"子原因";f.value=`选中: ${t.cause} | 类型: ${s} | 状态: ${t.enabled?"启用":"停用"}`}else f.value="当前状态: -"}async function c(){const t=await B("/settings/error-causes");p.value=t.rows}async function $(){await b(null)}async function S(){if(a.value===null){i("请先选择一个【顶级原因】作为父级。");return}const t=d.value;if(t.parent_id!==null){i(`最多只支持两级错误原因。 +您不能在子原因下继续添加。`);return}if(!t.enabled){i("该父级原因已停用,无法添加子原因。");return}await b(t.id)}async function b(t){const e=_(t===null?"新增顶级原因":"新增子原因","请输入原因名称 (最多5个中文字符):");if(e===null)return;const l=e.trim();if(l)try{await v("/settings/error-causes",{name:l,parent_id:t}),await c()}catch(g){i(g.message)}}async function E(){if(a.value===null)return;const t=d.value,s=_("编辑名称","修改名称:",t.cause);if(s===null)return;const e=s.trim();if(e)try{await P(`/settings/error-causes/${a.value}`,{name:e}),await c()}catch(l){i(l.message)}}async function N(){if(a.value===null)return;d.value.enabled;try{await v(`/settings/error-causes/${a.value}/toggle`),await c()}catch(s){s.needConfirm?z(s.message)&&(await v(`/settings/error-causes/${a.value}/toggle-force`),await c()):i(s.message)}}return F(a,h),V(c),(t,s)=>(u(),o("div",null,[n("div",G,[n("button",{class:"btn",onClick:s[0]||(s[0]=e=>t.$router.push("/settings"))},"< 返回设置"),s[1]||(s[1]=n("span",{class:"nav-title"},"错误原因管理",-1))]),n("div",I,[n("div",{style:{width:"180px","flex-shrink":"0"}},[s[2]||(s[2]=n("div",{style:{"font-weight":"bold","margin-bottom":"12px"}},"操作菜单",-1)),n("button",{class:"btn side-btn",onClick:$},"📂 新增顶级原因"),n("button",{class:"btn side-btn",onClick:S},"↳ 新增子原因"),s[3]||(s[3]=n("hr",{style:{border:"none","border-top":"1px solid #CCCCCC",margin:"14px 0"}},null,-1)),n("button",{class:"btn side-btn",onClick:E},"✎ 编辑名称"),n("button",{class:"btn side-btn",onClick:N},"✅ 启用/停用")]),n("div",L,[n("ul",M,[(u(!0),o(C,null,y(x.value,e=>(u(),o("li",{key:e.id},[n("div",{class:w(["node",{selected:a.value===e.id,disabled:!e.enabled}]),onClick:l=>a.value=e.id},r(e.parent_id===null?"📂":"📄")+" "+r(e.cause)+r(e.enabled?"":" (已停用)"),11,R),e.children&&e.children.length?(u(),o("ul",T,[(u(!0),o(C,null,y(e.children,l=>(u(),o("li",{key:l.id},[n("div",{class:w(["node",{selected:a.value===l.id,disabled:!l.enabled}]),onClick:g=>a.value=l.id}," 📄 "+r(l.cause)+r(l.enabled?"":" (已停用)"),11,j)]))),128))])):A("",!0)]))),128))])])]),n("div",q,r(f.value),1)]))}},Q=D(H,[["__scopeId","data-v-cd6f6757"]]);export{Q as default}; diff --git a/frontend/dist/assets/SettingsErrorCauses-BA8Zr_cv.css b/frontend/dist/assets/SettingsErrorCauses-BA8Zr_cv.css new file mode 100644 index 0000000..2570ae2 --- /dev/null +++ b/frontend/dist/assets/SettingsErrorCauses-BA8Zr_cv.css @@ -0,0 +1 @@ +.side-btn[data-v-cd6f6757]{display:block;width:100%;margin:5px 0;font-size:12px}.cause-tree[data-v-cd6f6757]{list-style:none;padding-left:10px}.cause-tree ul[data-v-cd6f6757]{list-style:none;padding-left:26px}.node[data-v-cd6f6757]{padding:5px 8px;cursor:pointer;border-radius:3px;font-size:13px}.node[data-v-cd6f6757]:hover{background:#f0f6ff}.node.selected[data-v-cd6f6757]{background:#b0d0f5}.node.disabled[data-v-cd6f6757]{color:gray} diff --git a/frontend/dist/assets/SettingsStatuses-C1SsLo53.js b/frontend/dist/assets/SettingsStatuses-C1SsLo53.js new file mode 100644 index 0000000..7b0d313 --- /dev/null +++ b/frontend/dist/assets/SettingsStatuses-C1SsLo53.js @@ -0,0 +1 @@ +import{_ as $,b as S,c as f,a as t,F as D,r as q,t as i,w as g,g as _,i as V,d,o as b,n as B}from"./index-Dc_QwQl6.js";import{a as I,t as o,c as M,b as x,e as N}from"./api-gDOaCrl1.js";const T={class:"nav-bar"},z={class:"content-area",style:{padding:"20px",display:"flex",gap:"14px"}},A={class:"card",style:{flex:"1"}},E={class:"data-table"},F=["onClick"],P={class:"center"},U={class:"center"},G={style:{padding:"0 20px",color:"#555","font-size":"12px"}},L={key:0,class:"modal-mask"},O={class:"modal"},j={class:"modal-header"},H={class:"modal-body"},J={class:"form-row"},K={class:"form-row"},Q={class:"modal-footer2"},R={__name:"SettingsStatuses",setup(W){const c=d([]),n=d(null),m=d("就绪"),r=d(!1),v=d("add"),l=d(""),u=d("");async function p(){const a=await I("/settings/question-statuses");c.value=a.rows}function w(){v.value="add",l.value="",u.value="",r.value=!0}async function k(){if(!l.value.trim()){o("状态名称不能为空");return}if(l.value.trim().length>20){o("状态名称过长(建议20字以内)");return}try{await x("/settings/question-statuses",{name:l.value.trim(),description:u.value.trim()}),r.value=!1,m.value=`成功添加状态: ${l.value.trim()}`,await p()}catch(a){o(a.message)}}function y(){if(n.value===null){o("请先选择一行数据");return}const a=c.value.find(e=>e.id===n.value);v.value="edit",l.value=a.name,u.value=a.description||"",r.value=!0}async function C(){if(!l.value.trim()){o("状态名称不能为空");return}try{await N(`/settings/question-statuses/${n.value}`,{name:l.value.trim(),description:u.value.trim()}),r.value=!1,m.value=`成功修改状态: ${l.value.trim()}`,await p()}catch(a){o(a.message)}}async function h(){if(n.value===null){o("请先选择一行数据");return}const a=c.value.find(s=>s.id===n.value),e=a.enabled?"停用":"启用";if(M(`确定要 ${e} 状态【${a.name}】吗?`))try{await x(`/settings/question-statuses/${n.value}/toggle`),m.value=`å·²${e}: ${a.name}`,await p()}catch(s){o(s.message)}}return S(p),(a,e)=>(b(),f("div",null,[t("div",T,[t("button",{class:"btn",onClick:e[0]||(e[0]=s=>a.$router.push("/settings"))},"< 返回设置"),e[4]||(e[4]=t("span",{class:"nav-title"},"题目状态管理",-1))]),t("div",z,[t("div",{style:{width:"180px","flex-shrink":"0"}},[e[5]||(e[5]=t("div",{style:{"font-weight":"bold","margin-bottom":"12px"}},"操作菜单",-1)),t("button",{class:"btn side-btn",onClick:w},"✚ 新增状态"),t("button",{class:"btn side-btn",onClick:y},"✎ 编辑选中"),e[6]||(e[6]=t("hr",{style:{border:"none","border-top":"1px solid #CCCCCC",margin:"14px 0"}},null,-1)),t("button",{class:"btn side-btn",onClick:h},"✅ 启用/停用")]),t("div",A,[t("table",E,[e[7]||(e[7]=t("thead",null,[t("tr",null,[t("th",{style:{cursor:"default"}},"ID"),t("th",{style:{cursor:"default"}},"状态名称"),t("th",{style:{cursor:"default"}},"状态描述"),t("th",{style:{cursor:"default"}},"当前状态")])],-1)),t("tbody",null,[(b(!0),f(D,null,q(c.value,s=>(b(),f("tr",{key:s.id,class:B(["clickable",{selected:n.value===s.id}]),onClick:X=>n.value=s.id,onDblclick:y},[t("td",P,i(s.id),1),t("td",null,i(s.name),1),t("td",null,i(s.description),1),t("td",U,i(s.enabled?"✅ 启用":"❌ 停用"),1)],42,F))),128))])])])]),t("div",G,i(m.value),1),r.value?(b(),f("div",L,[t("div",O,[t("div",j,i(v.value==="add"?"新增题目状态":"编辑题目状态"),1),t("div",H,[t("div",J,[e[8]||(e[8]=t("label",null,"状态名称:",-1)),g(t("input",{type:"text","onUpdate:modelValue":e[1]||(e[1]=s=>l.value=s),style:{width:"220px"}},null,512),[[_,l.value]])]),t("div",K,[e[9]||(e[9]=t("label",null,"状态描述:",-1)),g(t("input",{type:"text","onUpdate:modelValue":e[2]||(e[2]=s=>u.value=s),style:{width:"220px"}},null,512),[[_,u.value]])])]),t("div",Q,[t("button",{class:"btn primary",onClick:e[3]||(e[3]=s=>v.value==="add"?k():C())},i(v.value==="add"?"保存":"更新"),1)])])])):V("",!0)]))}},tt=$(R,[["__scopeId","data-v-f1401540"]]);export{tt as default}; diff --git a/frontend/dist/assets/SettingsStatuses-XHEFsW5N.css b/frontend/dist/assets/SettingsStatuses-XHEFsW5N.css new file mode 100644 index 0000000..6a115b0 --- /dev/null +++ b/frontend/dist/assets/SettingsStatuses-XHEFsW5N.css @@ -0,0 +1 @@ +.center[data-v-f1401540]{text-align:center}.side-btn[data-v-f1401540]{display:block;width:100%;margin:5px 0;font-size:12px}.modal-mask[data-v-f1401540]{position:fixed;top:0;right:0;bottom:0;left:0;background:#00000059;display:flex;align-items:center;justify-content:center;z-index:100}.modal[data-v-f1401540]{background:#fff;width:400px;border-radius:6px;box-shadow:0 4px 20px #00000040}.modal-header[data-v-f1401540]{padding:12px 16px;border-bottom:1px solid #eee;font-weight:700}.modal-body[data-v-f1401540]{padding:16px}.modal-footer2[data-v-f1401540]{padding:0 16px 16px;text-align:center}.form-row[data-v-f1401540]{display:flex;align-items:center;gap:10px;margin:10px 0} diff --git a/frontend/dist/assets/SettingsSubjects-BOWGx4MO.js b/frontend/dist/assets/SettingsSubjects-BOWGx4MO.js new file mode 100644 index 0000000..d029ca1 --- /dev/null +++ b/frontend/dist/assets/SettingsSubjects-BOWGx4MO.js @@ -0,0 +1 @@ +import{a as g,g as d,b,t as l,e as m,c as y}from"./api-gDOaCrl1.js";import{_ as k,b as h,c as u,a as t,F as w,r as j,d as f,o as r,n as C,t as i}from"./index-Dc_QwQl6.js";const S={class:"nav-bar"},x={class:"content-area",style:{padding:"20px"}},$={class:"card"},B={class:"data-table"},D=["onClick"],I={class:"center"},P={class:"center"},F={class:"center"},N={__name:"SettingsSubjects",setup(z){const c=f([]),n=f(null);async function o(){const a=await g("/settings/subjects");c.value=a.rows}async function _(){const a=d("新增学科","请输入学科名称 (最多10字):");if(!(a===null||a===""))try{await b("/settings/subjects",{name:a}),await o()}catch(e){l(e.message)}}async function p(){if(n.value===null){l("请先选择一行数据");return}const a=c.value.find(s=>s.id===n.value),e=d("编辑学科","修改学科名称:",a.subject);if(!(e===null||e===""))try{await m(`/settings/subjects/${n.value}`,{name:e}),await o()}catch(s){l(s.message)}}async function v(){if(n.value===null){l("请先选择一行数据");return}const a=c.value.find(s=>s.id===n.value),e=a.enabled?"停用":"启用";if(y(`确定要 ${e} 学科【${a.subject}】吗?`))try{await b(`/settings/subjects/${n.value}/toggle`),await o()}catch(s){l(s.message)}}return h(o),(a,e)=>(r(),u("div",null,[t("div",S,[t("button",{class:"btn",onClick:e[0]||(e[0]=s=>a.$router.push("/settings"))},"< 返回设置"),e[1]||(e[1]=t("span",{class:"nav-title"},"学科管理",-1))]),t("div",x,[t("div",$,[t("div",{style:{display:"flex",gap:"10px","margin-bottom":"12px"}},[t("button",{class:"btn",onClick:_},"+ 新增学科"),t("button",{class:"btn",onClick:p},"编辑名称"),t("button",{class:"btn",onClick:v},"启用/停用")]),t("table",B,[e[2]||(e[2]=t("thead",null,[t("tr",null,[t("th",{style:{cursor:"default"}},"ID"),t("th",{style:{cursor:"default"}},"学科名称"),t("th",{style:{cursor:"default"}},"状态"),t("th",{style:{cursor:"default"}},"创建时间")])],-1)),t("tbody",null,[(r(!0),u(w,null,j(c.value,s=>(r(),u("tr",{key:s.id,class:C(["clickable",{selected:n.value===s.id}]),onClick:A=>n.value=s.id},[t("td",I,i(s.id),1),t("td",null,i(s.subject),1),t("td",P,i(s.enabled?"✅ 启用":"❌ 停用"),1),t("td",F,i(s.create_time),1)],10,D))),128))])])])])]))}},L=k(N,[["__scopeId","data-v-f827c0e4"]]);export{L as default}; diff --git a/frontend/dist/assets/SettingsSubjects-bGGsFjfA.css b/frontend/dist/assets/SettingsSubjects-bGGsFjfA.css new file mode 100644 index 0000000..b846e4e --- /dev/null +++ b/frontend/dist/assets/SettingsSubjects-bGGsFjfA.css @@ -0,0 +1 @@ +.center[data-v-f827c0e4]{text-align:center} diff --git a/frontend/dist/assets/SubjectPortal-C7-krIg3.js b/frontend/dist/assets/SubjectPortal-C7-krIg3.js new file mode 100644 index 0000000..50a5abc --- /dev/null +++ b/frontend/dist/assets/SubjectPortal-C7-krIg3.js @@ -0,0 +1 @@ +import{_ as y,b as x,c as d,a as s,w as b,v as w,F as _,r as g,t as e,d as r,o as p,e as m,f as h}from"./index-Dc_QwQl6.js";import{a as k,b as F}from"./api-gDOaCrl1.js";const C={class:"page-header"},D={class:"content-area"},j={class:"card"},$={class:"row",style:{display:"flex","align-items":"center",gap:"10px","flex-wrap":"wrap"}},S=["value"],V=["onUpdate:modelValue"],z={style:{display:"flex",gap:"20px","flex-wrap":"wrap"}},B={class:"card stat-card",style:{flex:"1","min-width":"360px"}},E={class:"stat-line"},I={class:"stat-val",style:{color:"#333"}},M={class:"stat-line"},P={class:"stat-val",style:{color:"#2E7D32"}},U={class:"stat-line"},A={class:"stat-val big",style:{color:"#1976D2"}},N={class:"stat-line"},G={class:"stat-val",style:{color:"#D32F2F"}},L={class:"card stat-card",style:{flex:"1","min-width":"360px"}},O={class:"stat-line"},T={class:"stat-val",style:{color:"#333"}},q={class:"stat-line"},H={class:"stat-val",style:{color:"#2E7D32"}},J={class:"stat-line"},K={class:"stat-val big",style:{color:"#F57C00"}},Q={class:"stat-line"},R={class:"stat-val",style:{color:"#D32F2F"}},W={style:{display:"flex",gap:"15px","margin-top":"20px","justify-content":"center","flex-wrap":"wrap"}},X={__name:"SubjectPortal",setup(Y){const n=r(["数学"]),c=r([]),u=r({}),i=r("数学"),o=r({wrong_total:0,wrong_fixed:0,wrong_ratio:"0%",wrong_remained:0,push_total:0,push_fixed:0,push_ratio:"0%",push_remained:0});async function v(){const l=Object.entries(u.value).filter(([,t])=>t).map(([t])=>parseInt(t));o.value=await F("/portal/stats",{subject:i.value,stage_ids:l})}return x(async()=>{const l=await k("/portal/options");n.value=l.subjects,n.value.length&&!n.value.includes(i.value)&&(i.value=n.value[0]),c.value=l.stages;const t={};for(const a of l.stages)t[a.id]=!0;u.value=t,await v()}),(l,t)=>(p(),d("div",null,[s("div",C,[t[5]||(t[5]=s("span",{class:"title"},"科目概要面板",-1)),s("button",{class:"btn",onClick:t[0]||(t[0]=a=>l.$router.push("/"))},"返回首页")]),s("div",D,[s("div",j,[s("div",$,[t[6]||(t[6]=s("label",null,"当前科目:",-1)),b(s("select",{"onUpdate:modelValue":t[1]||(t[1]=a=>i.value=a),onChange:v},[(p(!0),d(_,null,g(n.value,a=>(p(),d("option",{key:a,value:a},e(a),9,S))),128))],544),[[w,i.value]]),t[7]||(t[7]=s("label",{style:{"margin-left":"20px"}},"统计阶段:",-1)),(p(!0),d(_,null,g(c.value,a=>(p(),d("label",{key:a.id,style:{"font-size":"13px"}},[b(s("input",{type:"checkbox","onUpdate:modelValue":f=>u.value[a.id]=f},null,8,V),[[m,u.value[a.id]]]),h(" "+e(a.name),1)]))),128)),s("button",{class:"btn",onClick:v},"确定统计")])]),s("div",z,[s("div",B,[t[12]||(t[12]=s("div",{class:"stat-title",style:{color:"#E64A19"}},"错题情况回顾",-1)),s("div",E,[t[8]||(t[8]=s("span",{class:"stat-label"},"错题总数:",-1)),s("span",I,e(o.value.wrong_total),1)]),s("div",M,[t[9]||(t[9]=s("span",{class:"stat-label"},"已解决数:",-1)),s("span",P,e(o.value.wrong_fixed),1)]),s("div",U,[t[10]||(t[10]=s("span",{class:"stat-label"},"解决比例:",-1)),s("span",A,e(o.value.wrong_ratio),1)]),s("div",N,[t[11]||(t[11]=s("span",{class:"stat-label"},"遗留题目数:",-1)),s("span",G,e(o.value.wrong_remained),1)])]),s("div",L,[t[17]||(t[17]=s("div",{class:"stat-title",style:{color:"#00796B"}},"强化推题追踪",-1)),s("div",O,[t[13]||(t[13]=s("span",{class:"stat-label"},"推题总数:",-1)),s("span",T,e(o.value.push_total),1)]),s("div",q,[t[14]||(t[14]=s("span",{class:"stat-label"},"已完成数:",-1)),s("span",H,e(o.value.push_fixed),1)]),s("div",J,[t[15]||(t[15]=s("span",{class:"stat-label"},"完成比例:",-1)),s("span",K,e(o.value.push_ratio),1)]),s("div",Q,[t[16]||(t[16]=s("span",{class:"stat-label"},"遗留题目数:",-1)),s("span",R,e(o.value.push_remained),1)])])]),s("div",W,[s("button",{class:"btn",style:{background:"#FF4D4F",color:"#000","font-weight":"bold",padding:"10px 30px","font-size":"14px"},onClick:t[2]||(t[2]=a=>l.$router.push("/analysis/pending"))},"遗留分析"),s("button",{class:"btn",style:{background:"#1890FF",color:"#000","font-weight":"bold",padding:"10px 30px","font-size":"14px"},onClick:t[3]||(t[3]=a=>l.$router.push("/analysis/chapters"))},"章节分析"),s("button",{class:"btn",style:{background:"#52C41A",color:"#000","font-weight":"bold",padding:"10px 30px","font-size":"14px"},onClick:t[4]||(t[4]=a=>l.$router.push("/analysis/knowledge"))},"知识点分析")])])]))}},ts=y(X,[["__scopeId","data-v-3b1b110d"]]);export{ts as default}; diff --git a/frontend/dist/assets/SubjectPortal-yfCLF9Wp.css b/frontend/dist/assets/SubjectPortal-yfCLF9Wp.css new file mode 100644 index 0000000..11c4ab4 --- /dev/null +++ b/frontend/dist/assets/SubjectPortal-yfCLF9Wp.css @@ -0,0 +1 @@ +.stat-title[data-v-3b1b110d]{font-size:14px;font-weight:700;margin-bottom:10px}.stat-line[data-v-3b1b110d]{display:flex;align-items:center;margin:8px 0}.stat-label[data-v-3b1b110d]{color:#666;font-size:13px}.stat-val[data-v-3b1b110d]{font-size:24px;font-weight:700;font-family:Helvetica,sans-serif;margin-left:10px}.stat-val.big[data-v-3b1b110d]{font-size:32px} diff --git a/frontend/dist/assets/api-gDOaCrl1.js b/frontend/dist/assets/api-gDOaCrl1.js new file mode 100644 index 0000000..253caa3 --- /dev/null +++ b/frontend/dist/assets/api-gDOaCrl1.js @@ -0,0 +1,2 @@ +const s="/StudyBase/api";async function c(t){if(t.status===422){const n=await t.json(),o=new Error(n.detail||"需要确认");throw o.needConfirm=!0,o}if(!t.ok){let n=`HTTP ${t.status}`;try{n=(await t.json()).detail||n}catch{}throw new Error(n)}return t}async function u(t){const n=await fetch(s+t);return c(n).then(o=>o.json())}async function h(t,n){const o=await fetch(s+t,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n||{})});return c(o).then(e=>e.json())}async function p(t,n){const o=await fetch(s+t,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(n||{})});return c(o).then(e=>e.json())}async function w(t){const n=await fetch(s+t,{method:"DELETE"});return c(n).then(o=>o.json())}async function m(t,n,o){const e=await fetch(s+t,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n||{})});await c(e);const f=await e.blob(),l=e.headers.get("Content-Disposition")||"";let i="download.xlsx";const r=l.match(/filename\*=UTF-8''([^;]+)/);r&&(i=decodeURIComponent(r[1]));const d=URL.createObjectURL(f),a=document.createElement("a");a.href=d,a.download=i,document.body.appendChild(a),a.click(),a.remove(),URL.revokeObjectURL(d)}function y(t){return window.confirm(t)}function b(t,n,o){const e=window.prompt((t?t+` +`:"")+n,o||"");return e===null?null:e}function j(t){window.alert(t)}export{u as a,h as b,y as c,m as d,p as e,w as f,b as g,j as t}; diff --git a/frontend/dist/assets/index-D5_k2e2e.css b/frontend/dist/assets/index-D5_k2e2e.css new file mode 100644 index 0000000..3306b88 --- /dev/null +++ b/frontend/dist/assets/index-D5_k2e2e.css @@ -0,0 +1 @@ +*{box-sizing:border-box;margin:0;padding:0}html,body,#app{height:100%}body{font-family:Microsoft YaHei,PingFang SC,Helvetica,sans-serif;background:#f0f2f5;color:#333;font-size:14px}a{color:#1890ff;text-decoration:none}.page-header{background:#4a90e2;height:56px;display:flex;align-items:center;padding:0 20px}.page-header .title{color:#000;font-size:20px;font-weight:700;margin-right:auto}.page-header .spacer{margin-left:auto}.btn{display:inline-block;border:1px solid #d9d9d9;background:#fff;color:#333;padding:5px 14px;font-size:13px;cursor:pointer;border-radius:3px;font-family:inherit}.btn:hover{background:#f5f5f5}.btn:disabled{color:#999;cursor:not-allowed;background:#f0f0f0}.btn.primary{background:#4a90e2;border-color:#4a90e2;color:#fff}.btn.primary:hover{background:#3a7ecb}.btn.green{background:#52c41a;border-color:#52c41a;color:#000}.btn.orange{background:#ffc107;border-color:#ffc107;color:#000}.btn.red{color:#fff;background:#ff4d4f;border-color:#ff4d4f}.btn.small{padding:2px 8px;font-size:12px}.card{background:#fff;border:1px solid #E8E8E8;border-radius:4px;padding:14px 18px;margin-bottom:12px}.filter-panel{background:#fff;border:1px solid #E8E8E8;border-radius:4px;padding:14px 18px;margin:10px 20px}.filter-panel .row{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin:6px 0}.filter-panel label{font-size:13px}select,input[type=text],textarea{border:1px solid #d9d9d9;border-radius:3px;padding:4px 8px;font-size:13px;font-family:inherit;background:#fff}select:focus,input:focus,textarea:focus{outline:1px solid #4A90E2}table.data-table{width:100%;border-collapse:collapse;background:#fff}table.data-table th{background:#4a90e2;color:#000;font-size:13px;font-weight:700;padding:7px 8px;border:1px solid #3a7ecb;cursor:pointer;white-space:nowrap}table.data-table td{padding:6px 8px;border:1px solid #E8E8E8;font-size:13px;vertical-align:top}table.data-table tr.clickable{cursor:pointer}table.data-table tr.selected td{background:#b0d0f5}.status-ok{color:#2e7d32}.status-off{color:#999}.content-area{padding:0 20px 20px}.nav-bar{background:#e0e0e0;height:46px;display:flex;align-items:center;padding:0 12px}.nav-bar .nav-title{font-size:16px;font-weight:700;margin-left:18px}.rich-text{line-height:1.7;font-family:SimSun,Songti SC,serif;font-size:15px;white-space:pre-wrap;word-break:break-word}.rich-text img{max-width:100%;vertical-align:middle;margin:2px 0}.log-box{background:#fff;border:1px solid #d9d9d9;font-family:Consolas,Monaco,monospace;font-size:12px;padding:8px;white-space:pre-wrap;overflow:auto;height:300px}::-webkit-scrollbar{width:10px;height:10px}::-webkit-scrollbar-thumb{background:#c4c4c4;border-radius:5px}::-webkit-scrollbar-track{background:#f0f0f0} diff --git a/frontend/dist/assets/index-Dc_QwQl6.js b/frontend/dist/assets/index-Dc_QwQl6.js new file mode 100644 index 0000000..63b34ac --- /dev/null +++ b/frontend/dist/assets/index-Dc_QwQl6.js @@ -0,0 +1,27 @@ +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/Home-DWL4BmBg.js","assets/Home-BS2dZmih.css","assets/SubjectPortal-C7-krIg3.js","assets/api-gDOaCrl1.js","assets/SubjectPortal-yfCLF9Wp.css","assets/QuestionList-cbNQnKl-.js","assets/RichContent-vEKXmEUi.js","assets/RichContent-i88-frEH.css","assets/QuestionList-Dv5J_SvP.css","assets/FileImport-BF8-5MOQ.js","assets/FileImport-B8PhhnGN.css","assets/ImportPreview-CFWTYbgQ.js","assets/ImportPreview-Dx1LXj5L.css","assets/QuestionDetail-s29u4nvx.js","assets/QuestionDetail-iaLTMiSx.css","assets/PapersList-CRTR-6gM.js","assets/PapersList-CnsFf_No.css","assets/PapersCreateById-CT-UILr2.js","assets/PapersPreview-ffjysDLy.js","assets/PapersDetail-CpYT1ySO.js","assets/PapersResult-BN_2zJ7H.js","assets/AnalysisChapters-rXnOtC4r.js","assets/AnalysisChapters-DTn9LWoa.css","assets/AnalysisKnowledge-D1yJ4mIN.js","assets/AnalysisKnowledge-5QPt6Ak_.css","assets/AnalysisPending-BfBXB5e_.js","assets/AnalysisPending-C5SzviRH.css","assets/Settings-CzRRQaVw.js","assets/Settings-pzGFQctT.css","assets/SettingsSubjects-BOWGx4MO.js","assets/SettingsSubjects-bGGsFjfA.css","assets/SettingsErrorCauses-2WPrQUsX.js","assets/SettingsErrorCauses-BA8Zr_cv.css","assets/SettingsStatuses-C1SsLo53.js","assets/SettingsStatuses-XHEFsW5N.css","assets/SettingsDefaultParams-Bwa8C6ww.js"])))=>i.map(i=>d[i]); +(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const r of document.querySelectorAll('link[rel="modulepreload"]'))s(r);new MutationObserver(r=>{for(const i of r)if(i.type==="childList")for(const o of i.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&s(o)}).observe(document,{childList:!0,subtree:!0});function n(r){const i={};return r.integrity&&(i.integrity=r.integrity),r.referrerPolicy&&(i.referrerPolicy=r.referrerPolicy),r.crossOrigin==="use-credentials"?i.credentials="include":r.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function s(r){if(r.ep)return;r.ep=!0;const i=n(r);fetch(r.href,i)}})();const ao="modulepreload",ho=function(e){return"/StudyBase/"+e},$s={},re=function(t,n,s){let r=Promise.resolve();if(n&&n.length>0){document.getElementsByTagName("link");const o=document.querySelector("meta[property=csp-nonce]"),l=(o==null?void 0:o.nonce)||(o==null?void 0:o.getAttribute("nonce"));r=Promise.allSettled(n.map(c=>{if(c=ho(c),c in $s)return;$s[c]=!0;const d=c.endsWith(".css"),f=d?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${c}"]${f}`))return;const h=document.createElement("link");if(h.rel=d?"stylesheet":ao,d||(h.as="script"),h.crossOrigin="",h.href=c,l&&h.setAttribute("nonce",l),document.head.appendChild(h),d)return new Promise((g,m)=>{h.addEventListener("load",g),h.addEventListener("error",()=>m(new Error(`Unable to preload CSS for ${c}`)))})}))}function i(o){const l=new Event("vite:preloadError",{cancelable:!0});if(l.payload=o,window.dispatchEvent(l),!l.defaultPrevented)throw o}return r.then(o=>{for(const l of o||[])l.status==="rejected"&&i(l.reason);return t().catch(i)})};/** +* @vue/shared v3.5.42 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/function Rs(e){const t=Object.create(null);for(const n of e.split(","))t[n]=1;return n=>n in t}const X={},Dt=[],qe=()=>{},Kr=()=>!1,In=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),Nn=e=>e.startsWith("onUpdate:"),fe=Object.assign,xs=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},po=Object.prototype.hasOwnProperty,q=(e,t)=>po.call(e,t),L=Array.isArray,gt=e=>fn(e)==="[object Map]",st=e=>fn(e)==="[object Set]",zs=e=>fn(e)==="[object Date]",B=e=>typeof e=="function",ne=e=>typeof e=="string",$e=e=>typeof e=="symbol",$=e=>e!==null&&typeof e=="object",Gr=e=>($(e)||B(e))&&B(e.then)&&B(e.catch),Wr=Object.prototype.toString,fn=e=>Wr.call(e),go=e=>fn(e).slice(8,-1),kr=e=>fn(e)==="[object Object]",Ss=e=>ne(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,Jt=Rs(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),Dn=e=>{const t=Object.create(null);return n=>t[n]||(t[n]=e(n))},mo=/-\w/g,_e=Dn(e=>e.replace(mo,t=>t.slice(1).toUpperCase())),_o=/\B([A-Z])/g,_t=Dn(e=>e.replace(_o,"-$1").toLowerCase()),Mn=Dn(e=>e.charAt(0).toUpperCase()+e.slice(1)),$n=Dn(e=>e?`on${Mn(e)}`:""),ke=(e,t)=>!Object.is(e,t),yn=(e,...t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:s,value:n})},Ln=e=>{const t=parseFloat(e);return isNaN(t)?e:t};let Js;const Vn=()=>Js||(Js=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function ws(e){if(L(e)){const t={};for(let n=0;n{if(n){const s=n.split(vo);s.length>1&&(t[s[0].trim()]=s[1].trim())}}),t}function Cs(e){let t="";if(ne(e))t=e;else if(L(e))for(let n=0;nTe(n,t))}const zr=e=>!!(e&&e.__v_isRef===!0),So=e=>ne(e)?e:e==null?"":L(e)||$(e)&&(e.toString===Wr||!B(e.toString))?zr(e)?So(e.value):JSON.stringify(e,Jr,2):String(e),Jr=(e,t)=>zr(t)?Jr(e,t.value):gt(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((n,[s,r],i)=>(n[zn(s,i)+" =>"]=r,n),{})}:st(t)?{[`Set(${t.size})`]:[...t.values()].map(n=>zn(n))}:$e(t)?zn(t):$(t)&&!L(t)&&!kr(t)?String(t):t,zn=(e,t="")=>{var n;return $e(e)?`Symbol(${(n=e.description)!=null?n:t})`:e};/** +* @vue/reactivity v3.5.42 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let ue;class wo{constructor(t=!1){this.detached=t,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this._warnOnRun=!0,this.__v_skip=!0,!t&&ue&&(ue.active?(this.parent=ue,this.index=(ue.scopes||(ue.scopes=[])).push(this)-1):(this._active=!1,this._warnOnRun=!1))}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let t,n;if(this.scopes){const s=this.scopes.slice();for(t=0,n=s.length;t0&&--this._on===0){if(ue===this)ue=this.prevScope;else{let t=ue;for(;t;){if(t.prevScope===this){t.prevScope=this.prevScope;break}t=t.prevScope}}this.prevScope=void 0}}stop(t){if(this._active){this._active=!1;let n,s;for(n=0,s=this.effects.length;n0)return;if(Yt){let t=Yt;for(Yt=void 0;t;){const n=t.next;t.next=void 0,t.flags&=-9,t=n}}let e;for(;Qt;){let t=Qt;for(Qt=void 0;t;){const n=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(s){e||(e=s)}t=n}}if(e)throw e}function Zr(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function ei(e){let t,n=e.depsTail,s=n;for(;s;){const r=s.prevDep;s.version===-1?(s===n&&(n=r),Is(s),Oo(s)):t=s,s.dep.activeLink=s.prevActiveLink,s.prevActiveLink=void 0,s=r}e.deps=t,e.depsTail=n}function us(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(ti(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function ti(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===nn)||(e.globalVersion=nn,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!us(e))))return;e.flags|=2;const t=e.dep,n=Z,s=Oe;Z=e,Oe=!0;try{Zr(e);const r=e.fn(e._value);(t.version===0||ke(r,e._value))&&(e.flags|=128,e._value=r,t.version++)}catch(r){throw t.version++,r}finally{Z=n,Oe=s,ei(e),e.flags&=-3}}function Is(e,t=!1){const{dep:n,prevSub:s,nextSub:r}=e;if(s&&(s.nextSub=r,e.prevSub=void 0),r&&(r.prevSub=s,e.nextSub=void 0),n.subs===e&&(n.subs=s,!s&&n.computed)){n.computed.flags&=-5;for(let i=n.computed.deps;i;i=i.nextDep)Is(i,!0)}!t&&!--n.sc&&n.map&&n.map.delete(n.key)}function Oo(e){const{prevDep:t,nextDep:n}=e;t&&(t.nextDep=n,e.prevDep=void 0),n&&(n.prevDep=t,e.nextDep=void 0)}let Oe=!0;const ni=[];function rt(){ni.push(Oe),Oe=!1}function it(){const e=ni.pop();Oe=e===void 0?!0:e}function Ys(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const n=Z;Z=void 0;try{t()}finally{Z=n}}}let nn=0;class Po{constructor(t,n){this.sub=t,this.dep=n,this.version=n.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}}class Ns{constructor(t){this.computed=t,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(t){if(!Z||!Oe||Z===this.computed)return;let n=this.activeLink;if(n===void 0||n.sub!==Z)n=this.activeLink=new Po(Z,this),Z.deps?(n.prevDep=Z.depsTail,Z.depsTail.nextDep=n,Z.depsTail=n):Z.deps=Z.depsTail=n,si(n);else if(n.version===-1&&(n.version=this.version,n.nextDep)){const s=n.nextDep;s.prevDep=n.prevDep,n.prevDep&&(n.prevDep.nextDep=s),n.prevDep=Z.depsTail,n.nextDep=void 0,Z.depsTail.nextDep=n,Z.depsTail=n,Z.deps===n&&(Z.deps=s)}return n}trigger(t){this.version++,nn++,this.notify(t)}notify(t){Ps();try{for(let n=this.subs;n;n=n.prevSub)n.sub.notify()&&n.sub.dep.notify()}finally{Ts()}}}function si(e){if(e.dep.sc++,e.sub.flags&4){const t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let s=t.deps;s;s=s.nextDep)si(s)}const n=e.dep.subs;n!==e&&(e.prevSub=n,n&&(n.nextSub=e)),e.dep.subs=e}}const fs=new WeakMap,xt=Symbol(""),as=Symbol(""),sn=Symbol("");function ae(e,t,n){if(Oe&&Z){let s=fs.get(e);s||fs.set(e,s=new Map);let r=s.get(n);r||(s.set(n,r=new Ns),r.map=s,r.key=n),r.track()}}function et(e,t,n,s,r,i){const o=fs.get(e);if(!o){nn++;return}const l=c=>{c&&c.trigger()};if(Ps(),t==="clear")o.forEach(l);else{const c=L(e),d=c&&Ss(n);if(c&&n==="length"){const f=Number(s);o.forEach((h,g)=>{(g==="length"||g===sn||!$e(g)&&g>=f)&&l(h)})}else switch((n!==void 0||o.has(void 0))&&l(o.get(n)),d&&l(o.get(sn)),t){case"add":c?d&&l(o.get("length")):(l(o.get(xt)),gt(e)&&l(o.get(as)));break;case"delete":c||(l(o.get(xt)),gt(e)&&l(o.get(as)));break;case"set":gt(e)&&l(o.get(xt));break}}Ts()}function Tt(e){const t=k(e);return t===e?t:(ae(t,"iterate",sn),we(e)?t:t.map(Ie))}function Fn(e){return ae(e=k(e),"iterate",sn),e}function Ge(e,t){return ot(e)?Ft(St(e)?Ie(t):t):Ie(t)}const To={__proto__:null,[Symbol.iterator](){return Qn(this,Symbol.iterator,e=>Ge(this,e))},concat(...e){return Tt(this).concat(...e.map(t=>L(t)?Tt(t):t))},entries(){return Qn(this,"entries",e=>(e[1]=Ge(this,e[1]),e))},every(e,t){return Je(this,"every",e,t,void 0,arguments)},filter(e,t){return Je(this,"filter",e,t,n=>n.map(s=>Ge(this,s)),arguments)},find(e,t){return Je(this,"find",e,t,n=>Ge(this,n),arguments)},findIndex(e,t){return Je(this,"findIndex",e,t,void 0,arguments)},findLast(e,t){return Je(this,"findLast",e,t,n=>Ge(this,n),arguments)},findLastIndex(e,t){return Je(this,"findLastIndex",e,t,void 0,arguments)},forEach(e,t){return Je(this,"forEach",e,t,void 0,arguments)},includes(...e){return Yn(this,"includes",e)},indexOf(...e){return Yn(this,"indexOf",e)},join(e){return Tt(this).join(e)},lastIndexOf(...e){return Yn(this,"lastIndexOf",e)},map(e,t){return Je(this,"map",e,t,void 0,arguments)},pop(){return Wt(this,"pop")},push(...e){return Wt(this,"push",e)},reduce(e,...t){return Xs(this,"reduce",e,t)},reduceRight(e,...t){return Xs(this,"reduceRight",e,t)},shift(){return Wt(this,"shift")},some(e,t){return Je(this,"some",e,t,void 0,arguments)},splice(...e){return Wt(this,"splice",e)},toReversed(){return Tt(this).toReversed()},toSorted(e){return Tt(this).toSorted(e)},toSpliced(...e){return Tt(this).toSpliced(...e)},unshift(...e){return Wt(this,"unshift",e)},values(){return Qn(this,"values",e=>Ge(this,e))}};function Qn(e,t,n){const s=Fn(e),r=s[t]();return s!==e&&!we(e)&&(r._next=r.next,r.next=()=>{const i=r._next();return i.done||(i.value=n(i.value)),i}),r}const Io=Array.prototype;function Je(e,t,n,s,r,i){const o=Fn(e),l=o!==e&&!we(e),c=o[t];if(c!==Io[t]){const h=c.apply(e,i);return l?Ie(h):h}let d=n;o!==e&&(l?d=function(h,g){return n.call(this,Ge(e,h),g,e)}:n.length>2&&(d=function(h,g){return n.call(this,h,g,e)}));const f=c.call(o,d,s);return l&&r?r(f):f}function Xs(e,t,n,s){const r=Fn(e),i=r!==e&&!we(e);let o=n,l=!1;r!==e&&(i?(l=s.length===0,o=function(d,f,h){return l&&(l=!1,d=Ge(e,d)),n.call(this,d,Ge(e,f),h,e)}):n.length>3&&(o=function(d,f,h){return n.call(this,d,f,h,e)}));const c=r[t](o,...s);return l?Ge(e,c):c}function Yn(e,t,n){const s=k(e);ae(s,"iterate",sn);const r=s[t](...n);return(r===-1||r===!1)&&Ls(n[0])?(n[0]=k(n[0]),s[t](...n)):r}function Wt(e,t,n=[]){rt(),Ps();const s=k(e)[t].apply(e,n);return Ts(),it(),s}const No=Rs("__proto__,__v_isRef,__isVue"),ri=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter($e));function Do(e){$e(e)||(e=String(e));const t=k(this);return ae(t,"has",e),t.hasOwnProperty(e)}class ii{constructor(t=!1,n=!1){this._isReadonly=t,this._isShallow=n}get(t,n,s){if(n==="__v_skip")return t.__v_skip;const r=this._isReadonly,i=this._isShallow;if(n==="__v_isReactive")return!r;if(n==="__v_isReadonly")return r;if(n==="__v_isShallow")return i;if(n==="__v_raw")return s===(r?i?Go:ui:i?ci:li).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(s)?t:void 0;const o=L(t);if(!r){let c;if(o&&(c=To[n]))return c;if(n==="hasOwnProperty")return Do}const l=Reflect.get(t,n,he(t)?t:s);if(($e(n)?ri.has(n):No(n))||(r||ae(t,"get",n),i))return l;if(he(l)){const c=o&&Ss(n)?l:l.value;return r&&$(c)?hs(c):c}return $(l)?r?hs(l):jn(l):l}}class oi extends ii{constructor(t=!1){super(!1,t)}set(t,n,s,r){let i=t[n];const o=L(t)&&Ss(n);if(!this._isShallow){const d=ot(i);if(!we(s)&&!ot(s)&&(i=k(i),s=k(s)),!o&&he(i)&&!he(s))return d||(i.value=s),!0}const l=o?Number(n)e,hn=e=>Reflect.getPrototypeOf(e);function jo(e,t,n){return function(...s){const r=this.__v_raw,i=k(r),o=gt(i),l=e==="entries"||e===Symbol.iterator&&o,c=e==="keys"&&o,d=r[e](...s),f=n?ds:t?Ft:Ie;return!t&&ae(i,"iterate",c?as:xt),fe(Object.create(d),{next(){const{value:h,done:g}=d.next();return g?{value:h,done:g}:{value:l?[f(h[0]),f(h[1])]:f(h),done:g}}})}}function pn(e){return function(...t){return e==="delete"?!1:e==="clear"?void 0:this}}function Ho(e,t){const n={get(r){const i=this.__v_raw,o=k(i),l=k(r);e||(ke(r,l)&&ae(o,"get",r),ae(o,"get",l));const{has:c}=hn(o),d=t?ds:e?Ft:Ie;if(c.call(o,r))return d(i.get(r));if(c.call(o,l))return d(i.get(l));i!==o&&i.get(r)},get size(){const r=this.__v_raw;return!e&&ae(k(r),"iterate",xt),r.size},has(r){const i=this.__v_raw,o=k(i),l=k(r);return e||(ke(r,l)&&ae(o,"has",r),ae(o,"has",l)),r===l?i.has(r):i.has(r)||i.has(l)},forEach(r,i){const o=this,l=o.__v_raw,c=k(l),d=t?ds:e?Ft:Ie;return!e&&ae(c,"iterate",xt),l.forEach((f,h)=>r.call(i,d(f),d(h),o))}};return fe(n,e?{add:pn("add"),set:pn("set"),delete:pn("delete"),clear:pn("clear")}:{add(r){const i=k(this),o=hn(i),l=k(r),c=!t&&!we(r)&&!ot(r)?l:r;return o.has.call(i,c)||ke(r,c)&&o.has.call(i,r)||ke(l,c)&&o.has.call(i,l)||(i.add(c),et(i,"add",c,c)),this},set(r,i){!t&&!we(i)&&!ot(i)&&(i=k(i));const o=k(this),{has:l,get:c}=hn(o);let d=l.call(o,r);d||(r=k(r),d=l.call(o,r));const f=c.call(o,r);return o.set(r,i),d?ke(i,f)&&et(o,"set",r,i):et(o,"add",r,i),this},delete(r){const i=k(this),{has:o,get:l}=hn(i);let c=o.call(i,r);c||(r=k(r),c=o.call(i,r)),l&&l.call(i,r);const d=i.delete(r);return c&&et(i,"delete",r,void 0),d},clear(){const r=k(this),i=r.size!==0,o=r.clear();return i&&et(r,"clear",void 0,void 0),o}}),["keys","values","entries",Symbol.iterator].forEach(r=>{n[r]=jo(r,e,t)}),n}function Ds(e,t){const n=Ho(e,t);return(s,r,i)=>r==="__v_isReactive"?!e:r==="__v_isReadonly"?e:r==="__v_raw"?s:Reflect.get(q(n,r)&&r in s?n:s,r,i)}const Bo={get:Ds(!1,!1)},Uo={get:Ds(!1,!0)},Ko={get:Ds(!0,!1)};const li=new WeakMap,ci=new WeakMap,ui=new WeakMap,Go=new WeakMap;function Wo(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function jn(e){return ot(e)?e:Ms(e,!1,Lo,Bo,li)}function fi(e){return Ms(e,!1,Fo,Uo,ci)}function hs(e){return Ms(e,!0,Vo,Ko,ui)}function Ms(e,t,n,s,r){if(!$(e)||e.__v_raw&&!(t&&e.__v_isReactive)||e.__v_skip||!Object.isExtensible(e))return e;const i=r.get(e);if(i)return i;const o=Wo(go(e));if(o===0)return e;const l=new Proxy(e,o===2?s:n);return r.set(e,l),l}function St(e){return ot(e)?St(e.__v_raw):!!(e&&e.__v_isReactive)}function ot(e){return!!(e&&e.__v_isReadonly)}function we(e){return!!(e&&e.__v_isShallow)}function Ls(e){return e?!!e.__v_raw:!1}function k(e){const t=e&&e.__v_raw;return t?k(t):e}function ko(e){return!q(e,"__v_skip")&&Object.isExtensible(e)&&qr(e,"__v_skip",!0),e}const Ie=e=>$(e)?jn(e):e,Ft=e=>$(e)?hs(e):e;function he(e){return e?e.__v_isRef===!0:!1}function qo(e){return ai(e,!1)}function $o(e){return ai(e,!0)}function ai(e,t){return he(e)?e:new zo(e,t)}class zo{constructor(t,n){this.dep=new Ns,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=n?t:k(t),this._value=n?t:Ie(t),this.__v_isShallow=n}get value(){return this.dep.track(),this._value}set value(t){const n=this._rawValue,s=this.__v_isShallow||we(t)||ot(t);t=s?t:k(t),ke(t,n)&&(this._rawValue=t,this._value=s?t:Ie(t),this.dep.trigger())}}function Mt(e){return he(e)?e.value:e}const Jo={get:(e,t,n)=>t==="__v_raw"?e:Mt(Reflect.get(e,t,n)),set:(e,t,n,s)=>{const r=e[t];return he(r)&&!he(n)?(r.value=n,!0):Reflect.set(e,t,n,s)}};function di(e){return St(e)?e:new Proxy(e,Jo)}class Qo{constructor(t,n,s){this.fn=t,this.setter=n,this._value=void 0,this.dep=new Ns(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=nn-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!n,this.isSSR=s}notify(){if(this.flags|=16,!(this.flags&8)&&Z!==this)return Xr(this,!0),!0}get value(){const t=this.dep.track();return ti(this),t&&(t.version=this.dep.version),this._value}set value(t){this.setter&&this.setter(t)}}function Yo(e,t,n=!1){let s,r;return B(e)?s=e:(s=e.get,r=e.set),new Qo(s,r,n)}const gn={},An=new WeakMap;let At;function Xo(e,t=!1,n=At){if(n){let s=An.get(n);s||An.set(n,s=[]),s.push(e)}}function Zo(e,t,n=X){const{immediate:s,deep:r,once:i,scheduler:o,augmentJob:l,call:c}=n,d=T=>r?T:we(T)||r===!1||r===0?tt(T,1):tt(T);let f,h,g,m,I=!1,S=!1;if(he(e)?(h=()=>e.value,I=we(e)):St(e)?(h=()=>d(e),I=!0):L(e)?(S=!0,I=e.some(T=>St(T)||we(T)),h=()=>e.map(T=>{if(he(T))return T.value;if(St(T))return d(T);if(B(T))return c?c(T,2):T()})):B(e)?t?h=c?()=>c(e,2):e:h=()=>{if(g){rt();try{g()}finally{it()}}const T=At;At=f;try{return c?c(e,3,[m]):e(m)}finally{At=T}}:h=qe,t&&r){const T=h,Q=r===!0?1/0:r;h=()=>tt(T(),Q)}const H=Co(),F=()=>{f.stop(),H&&H.active&&xs(H.effects,f)};if(i&&t){const T=t;t=(...Q)=>{const ie=T(...Q);return F(),ie}}let O=S?new Array(e.length).fill(gn):gn;const N=T=>{if(!(!(f.flags&1)||!f.dirty&&!T))if(t){const Q=f.run();if(T||r||I||(S?Q.some((ie,ee)=>ke(ie,O[ee])):ke(Q,O))){g&&g();const ie=At;At=f;try{const ee=[Q,O===gn?void 0:S&&O[0]===gn?[]:O,m];O=Q,c?c(t,3,ee):t(...ee)}finally{At=ie}}}else f.run()};return l&&l(N),f=new Qr(h),f.scheduler=o?()=>o(N,!1):N,m=T=>Xo(T,!1,f),g=f.onStop=()=>{const T=An.get(f);if(T){if(c)c(T,4);else for(const Q of T)Q();An.delete(f)}},t?s?N(!0):O=f.run():o?o(N.bind(null,!0),!0):f.run(),F.pause=f.pause.bind(f),F.resume=f.resume.bind(f),F.stop=F,F}function tt(e,t=1/0,n){if(t<=0||!$(e)||e.__v_skip||(n=n||new Map,(n.get(e)||0)>=t))return e;if(n.set(e,t),t--,he(e))tt(e.value,t,n);else if(L(e))for(let s=0;s{tt(s,t,n)});else if(kr(e)){for(const s in e)tt(e[s],t,n);for(const s of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,s)&&tt(e[s],t,n)}return e}/** +* @vue/runtime-core v3.5.42 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/function an(e,t,n,s){try{return s?e(...s):e()}catch(r){Hn(r,t,n)}}function Ne(e,t,n,s){if(B(e)){const r=an(e,t,n,s);return r&&Gr(r)&&r.catch(i=>{Hn(i,t,n)}),r}if(L(e)){const r=[];for(let i=0;i>>1,r=me[s],i=rn(r);i=rn(n)?me.push(e):me.splice(tl(t),0,e),e.flags|=1,pi()}}function pi(){Rn||(Rn=hi.then(mi))}function nl(e){if(!L(e))ht&&e.id===-1?ht.splice(It+1,0,e):e.flags&1||(Lt.push(e),e.flags|=1);else for(let t=0;trn(n)-rn(s));if(Lt.length=0,ht){for(let n=0;ne.id==null?e.flags&2?-1:1/0:e.id;function mi(e){try{for(Ke=0;Ke{s._d&&Cn(-1);const i=xn(t),o=wt.length;let l;try{l=e(...r)}finally{for(let c=wt.length;c>o;c--)Gi();xn(i),s._d&&Cn(1)}return l};return s._n=!0,s._c=!0,s._d=!0,s}function ef(e,t){if(Ee===null)return e;const n=Wn(Ee),s=e.dirs||(e.dirs=[]);for(let r=0;r1)return n&&B(t)?t.call(s&&s.proxy):t}}const rl=Symbol.for("v-scx"),il=()=>Pe(rl);function bn(e,t,n){return yi(e,t,n)}function yi(e,t,n=X){const{immediate:s,deep:r,flush:i,once:o}=n,l=fe({},n),c=t&&s||!t&&i!=="post";let d;if(cn){if(i==="sync"){const m=il();d=m.__watcherHandles||(m.__watcherHandles=[])}else if(!c){const m=()=>{};return m.stop=qe,m.resume=qe,m.pause=qe,m}}const f=de;l.call=(m,I,S)=>Ne(m,f,I,S);let h=!1;i==="post"?l.scheduler=m=>{ve(m,f&&f.suspense)}:i!=="sync"&&(h=!0,l.scheduler=(m,I)=>{I?m():Fs(m)}),l.augmentJob=m=>{t&&(m.flags|=4),h&&(m.flags|=2,f&&(m.id=f.uid,m.i=f))};const g=Zo(e,t,l);return cn&&(d?d.push(g):c&&g()),g}function ol(e,t,n){const s=this.proxy,r=ne(e)?e.includes(".")?vi(s,e):()=>s[e]:e.bind(s,s);let i;B(t)?i=t:(i=t.handler,n=t);const o=dn(this),l=yi(r,i.bind(s),n);return o(),l}function vi(e,t){const n=t.split(".");return()=>{let s=e;for(let r=0;re.__isTeleport,Xn=Symbol("_leaveCb");function cl(e){let t=e[0];if(e.length>1){for(const n of e)if(n.type!==lt){t=n;break}}return t}function bi(e){if(!Hs(e))return Bn(e.type)&&e.children?cl(e.children):e;if(e.component)return e.component.subTree;const{shapeFlag:t,children:n}=e;if(n){if(t&16)return n[0];if(t&32&&B(n.default))return n.default()}}function js(e,t){if(e.shapeFlag&6&&e.component){e.transition=t;const n=e.component.subTree;js(Bn(n.type)&&bi(n)||n,t)}else e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function Ei(e,t){return B(e)?fe({name:e.name},t,{setup:e}):e}function Ai(e){e.ids=[e.ids[0]+e.ids[2]+++"-",0,0]}function er(e,t){let n;return!!((n=Object.getOwnPropertyDescriptor(e,t))&&!n.configurable)}const Sn=new WeakMap;function Xt(e,t,n,s,r=!1){if(L(e)){e.forEach((S,H)=>Xt(S,t&&(L(t)?t[H]:t),n,s,r));return}if(Zt(s)&&!r){s.shapeFlag&512&&s.type.__asyncResolved&&s.component.subTree.component&&Xt(e,t,n,s.component.subTree);return}const i=s.shapeFlag&4?Wn(s.component):s.el,o=r?null:i,{i:l,r:c}=e,d=t&&t.r,f=l.refs===X?l.refs={}:l.refs,h=l.setupState,g=k(h),m=h===X?Kr:S=>er(f,S)?!1:q(g,S),I=(S,H)=>!(H&&er(f,H));if(d!=null&&d!==c){if(tr(t),ne(d))f[d]=null,m(d)&&(h[d]=null);else if(he(d)){const S=t;I(d,S.k)&&(d.value=null),S.k&&(f[S.k]=null)}}if(B(c))an(c,l,12,[o,f]);else{const S=ne(c),H=he(c);if(S||H){const F=()=>{if(e.f){const O=S?m(c)?h[c]:f[c]:I()||!e.k?c.value:f[e.k];if(r)L(O)&&xs(O,i);else if(L(O))O.includes(i)||O.push(i);else if(S)f[c]=[i],m(c)&&(h[c]=f[c]);else{const N=[i];I(c,e.k)&&(c.value=N),e.k&&(f[e.k]=N)}}else S?(f[c]=o,m(c)&&(h[c]=o)):H&&(I(c,e.k)&&(c.value=o),e.k&&(f[e.k]=o))};if(o){const O=()=>{F(),Sn.delete(e)};O.id=-1,Sn.set(e,O),ve(O,n)}else tr(e),F()}}}function tr(e){const t=Sn.get(e);t&&(t.flags|=8,Sn.delete(e))}Vn().requestIdleCallback;Vn().cancelIdleCallback;const Zt=e=>!!e.type.__asyncLoader,Hs=e=>e.type.__isKeepAlive;function ul(e,t){Ri(e,"a",t)}function fl(e,t){Ri(e,"da",t)}function Ri(e,t,n=de){const s=e.__wdc||(e.__wdc=()=>{let r=n;for(;r;){if(r.isDeactivated)return;r=r.parent}return e()});if(Un(t,s,n),n){let r=n.parent;for(;r&&r.parent;)Hs(r.parent.vnode)&&al(s,t,n,r),r=r.parent}}function al(e,t,n,s){const r=Un(t,e,s,!0);xi(()=>{xs(s[t],r)},n)}function Un(e,t,n=de,s=!1){if(n){const r=n[e]||(n[e]=[]),i=t.__weh||(t.__weh=(...o)=>{rt();const l=dn(n),c=Ne(t,n,e,o);return l(),it(),c});return s?r.unshift(i):r.push(i),i}}const ct=e=>(t,n=de)=>{(!cn||e==="sp")&&Un(e,(...s)=>t(...s),n)},dl=ct("bm"),hl=ct("m"),pl=ct("bu"),gl=ct("u"),ml=ct("bum"),xi=ct("um"),_l=ct("sp"),yl=ct("rtg"),vl=ct("rtc");function bl(e,t=de){Un("ec",e,t)}const El="components";function Al(e,t){return xl(El,e,!0,t)||e}const Rl=Symbol.for("v-ndc");function xl(e,t,n=!0,s=!1){const r=Ee||de;if(r){const i=r.type;{const l=uc(i,!1);if(l&&(l===t||l===_e(t)||l===Mn(_e(t))))return i}const o=nr(r[e]||i[e],t)||nr(r.appContext[e],t);return!o&&s?i:o}}function nr(e,t){return e&&(e[t]||e[_e(t)]||e[Mn(_e(t))])}function tf(e,t,n,s){let r;const i=n,o=L(e);if(o||ne(e)){const l=o&&St(e);let c=!1,d=!1;l&&(c=!we(e),d=ot(e),e=Fn(e)),r=new Array(e.length);for(let f=0,h=e.length;ft(l,c,void 0,i));else{const l=Object.keys(e);r=new Array(l.length);for(let c=0,d=l.length;ce?zi(e)?Wn(e):ps(e.parent):null,en=fe(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>ps(e.parent),$root:e=>ps(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>wi(e),$forceUpdate:e=>e.f||(e.f=()=>{Fs(e.update)}),$nextTick:e=>e.n||(e.n=Vs.bind(e.proxy)),$watch:e=>ol.bind(e)}),Zn=(e,t)=>e!==X&&!e.__isScriptSetup&&q(e,t),Sl={get({_:e},t){if(t==="__v_skip")return!0;const{ctx:n,setupState:s,data:r,props:i,accessCache:o,type:l,appContext:c}=e;if(t[0]!=="$"){const g=o[t];if(g!==void 0)switch(g){case 1:return s[t];case 2:return r[t];case 4:return n[t];case 3:return i[t]}else{if(Zn(s,t))return o[t]=1,s[t];if(r!==X&&q(r,t))return o[t]=2,r[t];if(q(i,t))return o[t]=3,i[t];if(n!==X&&q(n,t))return o[t]=4,n[t];gs&&(o[t]=0)}}const d=en[t];let f,h;if(d)return t==="$attrs"&&ae(e.attrs,"get",""),d(e);if((f=l.__cssModules)&&(f=f[t]))return f;if(n!==X&&q(n,t))return o[t]=4,n[t];if(h=c.config.globalProperties,q(h,t))return h[t]},set({_:e},t,n){const{data:s,setupState:r,ctx:i}=e;return Zn(r,t)?(r[t]=n,!0):s!==X&&q(s,t)?(s[t]=n,!0):q(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(i[t]=n,!0)},has({_:{data:e,setupState:t,accessCache:n,ctx:s,appContext:r,props:i,type:o}},l){let c;return!!(n[l]||e!==X&&l[0]!=="$"&&q(e,l)||Zn(t,l)||q(i,l)||q(s,l)||q(en,l)||q(r.config.globalProperties,l)||(c=o.__cssModules)&&c[l])},defineProperty(e,t,n){return n.get!=null?e._.accessCache[t]=0:q(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}};function sr(e){return L(e)?e.reduce((t,n)=>(t[n]=null,t),{}):e}let gs=!0;function wl(e){const t=wi(e),n=e.proxy,s=e.ctx;gs=!1,t.beforeCreate&&rr(t.beforeCreate,e,"bc");const{data:r,computed:i,methods:o,watch:l,provide:c,inject:d,created:f,beforeMount:h,mounted:g,beforeUpdate:m,updated:I,activated:S,deactivated:H,beforeDestroy:F,beforeUnmount:O,destroyed:N,unmounted:T,render:Q,renderTracked:ie,renderTriggered:ee,errorCaptured:Me,serverPrefetch:ut,expose:Le,inheritAttrs:ft,components:yt,directives:Ve,filters:Kt}=t;if(d&&Cl(d,s,null),o)for(const z in o){const G=o[z];B(G)&&(s[z]=G.bind(n))}if(r){const z=r.call(n,n);$(z)&&(e.data=jn(z))}if(gs=!0,i)for(const z in i){const G=i[z],ze=B(G)?G.bind(n,n):B(G.get)?G.get.bind(n,n):qe,at=!B(G)&&B(G.set)?G.set.bind(n):qe,Fe=Ce({get:ze,set:at});Object.defineProperty(s,z,{enumerable:!0,configurable:!0,get:()=>Fe.value,set:ye=>Fe.value=ye})}if(l)for(const z in l)Si(l[z],s,n,z);if(c){const z=B(c)?c.call(n):c;Reflect.ownKeys(z).forEach(G=>{vn(G,z[G])})}f&&rr(f,e,"c");function le(z,G){L(G)?G.forEach(ze=>z(ze.bind(n))):G&&z(G.bind(n))}if(le(dl,h),le(hl,g),le(pl,m),le(gl,I),le(ul,S),le(fl,H),le(bl,Me),le(vl,ie),le(yl,ee),le(ml,O),le(xi,T),le(_l,ut),L(Le))if(Le.length){const z=e.exposed||(e.exposed={});Le.forEach(G=>{Object.defineProperty(z,G,{get:()=>n[G],set:ze=>n[G]=ze,enumerable:!0})})}else e.exposed||(e.exposed={});Q&&e.render===qe&&(e.render=Q),ft!=null&&(e.inheritAttrs=ft),yt&&(e.components=yt),Ve&&(e.directives=Ve),ut&&Ai(e)}function Cl(e,t,n=qe){L(e)&&(e=ms(e));for(const s in e){const r=e[s];let i;$(r)?"default"in r?i=Pe(r.from||s,r.default,!0):i=Pe(r.from||s):i=Pe(r),he(i)?Object.defineProperty(t,s,{enumerable:!0,configurable:!0,get:()=>i.value,set:o=>i.value=o}):t[s]=i}}function rr(e,t,n){Ne(L(e)?e.map(s=>s.bind(t.proxy)):e.bind(t.proxy),t,n)}function Si(e,t,n,s){let r=s.includes(".")?vi(n,s):()=>n[s];if(ne(e)){const i=t[e];B(i)&&bn(r,i)}else if(B(e))bn(r,e.bind(n));else if($(e))if(L(e))e.forEach(i=>Si(i,t,n,s));else{const i=B(e.handler)?e.handler.bind(n):t[e.handler];B(i)&&bn(r,i,e)}}function wi(e){const t=e.type,{mixins:n,extends:s}=t,{mixins:r,optionsCache:i,config:{optionMergeStrategies:o}}=e.appContext,l=i.get(t);let c;return l?c=l:!r.length&&!n&&!s?c=t:(c={},r.length&&r.forEach(d=>wn(c,d,o,!0)),wn(c,t,o)),$(t)&&i.set(t,c),c}function wn(e,t,n,s=!1){const{mixins:r,extends:i}=t;i&&wn(e,i,n,!0),r&&r.forEach(o=>wn(e,o,n,!0));for(const o in t)if(!(s&&o==="expose")){const l=Ol[o]||n&&n[o];e[o]=l?l(e[o],t[o]):t[o]}return e}const Ol={data:ir,props:or,emits:or,methods:$t,computed:$t,beforeCreate:pe,created:pe,beforeMount:pe,mounted:pe,beforeUpdate:pe,updated:pe,beforeDestroy:pe,beforeUnmount:pe,destroyed:pe,unmounted:pe,activated:pe,deactivated:pe,errorCaptured:pe,serverPrefetch:pe,components:$t,directives:$t,watch:Tl,provide:ir,inject:Pl};function ir(e,t){return t?e?function(){return fe(B(e)?e.call(this,this):e,B(t)?t.call(this,this):t)}:t:e}function Pl(e,t){return $t(ms(e),ms(t))}function ms(e){if(L(e)){const t={};for(let n=0;nt==="modelValue"||t==="model-value"?e.modelModifiers:e[`${t}Modifiers`]||e[`${_e(t)}Modifiers`]||e[`${_t(t)}Modifiers`];function Ml(e,t,...n){if(e.isUnmounted)return;const s=e.vnode.props||X;let r=n;const i=t.startsWith("update:"),o=i&&Dl(s,t.slice(7));o&&(o.trim&&(r=n.map(f=>ne(f)?f.trim():f)),o.number&&(r=r.map(Ln)));let l,c=s[l=$n(t)]||s[l=$n(_e(t))];!c&&i&&(c=s[l=$n(_t(t))]),c&&Ne(c,e,6,r);const d=s[l+"Once"];if(d){if(!e.emitted)e.emitted={};else if(e.emitted[l])return;e.emitted[l]=!0,Ne(d,e,6,r)}}const Ll=new WeakMap;function Oi(e,t,n=!1){const s=n?Ll:t.emitsCache,r=s.get(e);if(r!==void 0)return r;const i=e.emits;let o={},l=!1;if(!B(e)){const c=d=>{const f=Oi(d,t,!0);f&&(l=!0,fe(o,f))};!n&&t.mixins.length&&t.mixins.forEach(c),e.extends&&c(e.extends),e.mixins&&e.mixins.forEach(c)}return!i&&!l?($(e)&&s.set(e,null),null):(L(i)?i.forEach(c=>o[c]=null):fe(o,i),$(e)&&s.set(e,o),o)}function Kn(e,t){return!e||!In(t)?!1:(t=t.slice(2),t=t==="Once"?t:t.replace(/Once$/,""),q(e,t[0].toLowerCase()+t.slice(1))||q(e,_t(t))||q(e,t))}function lr(e){const{type:t,vnode:n,proxy:s,withProxy:r,propsOptions:[i],slots:o,attrs:l,emit:c,render:d,renderCache:f,props:h,data:g,setupState:m,ctx:I,inheritAttrs:S}=e,H=xn(e);let F,O;try{if(n.shapeFlag&4){const T=r||s,Q=T;F=We(d.call(Q,T,f,h,m,g,I)),O=l}else{const T=t;F=We(T.length>1?T(h,{attrs:l,slots:o,emit:c}):T(h,null)),O=t.props?l:Vl(l)}}catch(T){wt.length=0,Hn(T,e,1),F=Re(lt)}let N=F;if(O&&S!==!1){const T=Object.keys(O),{shapeFlag:Q}=N;T.length&&Q&7&&(i&&T.some(Nn)&&(O=Fl(O,i)),N=jt(N,O,!1,!0))}if(n.dirs&&(N=jt(N,null,!1,!0),N.dirs=N.dirs?N.dirs.concat(n.dirs):n.dirs),n.transition){const T=Bn(N.type)&&bi(N)||N;js(T,n.transition)}return F=N,xn(H),F}const Vl=e=>{let t;for(const n in e)(n==="class"||n==="style"||In(n))&&((t||(t={}))[n]=e[n]);return t},Fl=(e,t)=>{const n={};for(const s in e)(!Nn(s)||!(s.slice(9)in t))&&(n[s]=e[s]);return n};function jl(e,t,n){const{props:s,children:r,component:i}=e,{props:o,children:l,patchFlag:c}=t,d=i.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&c>=0){if(c&1024)return!0;if(c&16)return s?cr(s,o,d):!!o;if(c&8){const f=t.dynamicProps;for(let h=0;hObject.create(Ti),Ni=e=>Object.getPrototypeOf(e)===Ti;function Bl(e,t,n,s=!1){const r={},i=Ii();e.propsDefaults=Object.create(null),Di(e,t,r,i);for(const o in e.propsOptions[0])o in r||(r[o]=void 0);n?e.props=s?r:fi(r):e.type.props?e.props=r:e.props=i,e.attrs=i}function Ul(e,t,n,s){const{props:r,attrs:i,vnode:{patchFlag:o}}=e,l=k(r),[c]=e.propsOptions;let d=!1;if((s||o>0)&&!(o&16)){if(o&8){const f=e.vnode.dynamicProps;for(let h=0;h{c=!0;const[g,m]=Mi(h,t,!0);fe(o,g),m&&l.push(...m)};!n&&t.mixins.length&&t.mixins.forEach(f),e.extends&&f(e.extends),e.mixins&&e.mixins.forEach(f)}if(!i&&!c)return $(e)&&s.set(e,Dt),Dt;if(L(i))for(let f=0;fe==="_"||e==="_ctx"||e==="$stable",Us=e=>L(e)?e.map(We):[We(e)],Gl=(e,t,n)=>{if(t._n)return t;const s=sl((...r)=>Us(t(...r)),n);return s._c=!1,s},Li=(e,t,n)=>{const s=e._ctx;for(const r in e){if(Bs(r))continue;const i=e[r];if(B(i))t[r]=Gl(r,i,s);else if(i!=null){const o=Us(i);t[r]=()=>o}}},Vi=(e,t)=>{const n=Us(t);e.slots.default=()=>n},Fi=(e,t,n)=>{for(const s in t)(n||!Bs(s))&&(e[s]=t[s])},Wl=(e,t,n)=>{const s=e.slots=Ii();if(e.vnode.shapeFlag&32){const r=t._;r?(Fi(s,t,n),n&&qr(s,"_",r,!0)):Li(t,s)}else t&&Vi(e,t)},kl=(e,t,n)=>{const{vnode:s,slots:r}=e;let i=!0,o=X;if(s.shapeFlag&32){const l=t._;l?n&&l===1?i=!1:Fi(r,t,n):(i=!t.$stable,Li(t,r)),o=t}else t&&(Vi(e,t),o={default:1});if(i)for(const l in r)!Bs(l)&&o[l]==null&&delete r[l]},ve=Ql;function ql(e){return $l(e)}function $l(e,t){const n=Vn();n.__VUE__=!0;const{insert:s,remove:r,patchProp:i,createElement:o,createText:l,createComment:c,setText:d,setElementText:f,parentNode:h,nextSibling:g,setScopeId:m=qe,insertStaticContent:I}=e,S=(u,a,p,y=null,b=null,_=null,x=void 0,R=null,A=!!a.dynamicChildren)=>{if(u===a)return;u&&!kt(u,a)&&(y=v(u),ye(u,b,_,!0),u=null),a.patchFlag===-2&&(A=!1,a.dynamicChildren=null);const{type:E,ref:V,shapeFlag:C}=a;switch(E){case Gn:H(u,a,p,y);break;case lt:F(u,a,p,y);break;case ts:u==null&&O(a,p,y,x);break;case Xe:yt(u,a,p,y,b,_,x,R,A);break;default:C&1?Q(u,a,p,y,b,_,x,R,A):C&6?Ve(u,a,p,y,b,_,x,R,A):(C&64||C&128)&&E.process(u,a,p,y,b,_,x,R,A,D)}V!=null&&b?Xt(V,u&&u.ref,_,a||u,!a):V==null&&u&&u.ref!=null&&Xt(u.ref,null,_,u,!0)},H=(u,a,p,y)=>{if(u==null)s(a.el=l(a.children),p,y);else{const b=a.el=u.el;a.children!==u.children&&d(b,a.children)}},F=(u,a,p,y)=>{u==null?s(a.el=c(a.children||""),p,y):a.el=u.el},O=(u,a,p,y)=>{[u.el,u.anchor]=I(u.children,a,p,y,u.el,u.anchor)},N=({el:u,anchor:a},p,y)=>{let b;for(;u&&u!==a;)b=g(u),s(u,p,y),u=b;s(a,p,y)},T=({el:u,anchor:a})=>{let p;for(;u&&u!==a;)p=g(u),r(u),u=p;r(a)},Q=(u,a,p,y,b,_,x,R,A)=>{if(a.type==="svg"?x="svg":a.type==="math"&&(x="mathml"),u==null)ie(a,p,y,b,_,x,R,A);else{const E=u.el&&u.el._isVueCE?u.el:null;try{E&&E._beginPatch(),ut(u,a,b,_,x,R,A)}finally{E&&E._endPatch()}}},ie=(u,a,p,y,b,_,x,R)=>{let A,E;const{props:V,shapeFlag:C,transition:M,dirs:j}=u;if(A=u.el=o(u.type,_,V&&V.is,V),C&8?f(A,u.children):C&16&&Me(u.children,A,null,y,b,es(u,_),x,R),j&&bt(u,null,y,"created"),ee(A,u,u.scopeId,x,y),V){for(const Y in V)Y!=="value"&&!Jt(Y)&&i(A,Y,null,V[Y],_,y);"value"in V&&i(A,"value",null,V.value,_),(E=V.onVnodeBeforeMount)&&Ue(E,y,u)}j&&bt(u,null,y,"beforeMount");const K=zl(b,M);K&&M.beforeEnter(A),s(A,a,p),((E=V&&V.onVnodeMounted)||K||j)&&ve(()=>{try{E&&Ue(E,y,u),K&&M.enter(A),j&&bt(u,null,y,"mounted")}finally{}},b)},ee=(u,a,p,y,b)=>{if(p&&m(u,p),y)for(let _=0;_{for(let E=A;E{const R=a.el=u.el;let{patchFlag:A,dynamicChildren:E,dirs:V}=a;A|=u.patchFlag&16;const C=u.props||X,M=a.props||X;let j;if(p&&Et(p,!1),(j=M.onVnodeBeforeUpdate)&&Ue(j,p,a,u),V&&bt(a,u,p,"beforeUpdate"),p&&Et(p,!0),E&&(!u.dynamicChildren||u.dynamicChildren.length!==E.length)&&(A=0,x=!1,E=null),(C.innerHTML&&M.innerHTML==null||C.textContent&&M.textContent==null)&&f(R,""),E?Le(u.dynamicChildren,E,R,p,y,es(a,b),_):x||G(u,a,R,null,p,y,es(a,b),_,!1),A>0){if(A&16)ft(R,C,M,p,b);else if(A&2&&C.class!==M.class&&i(R,"class",null,M.class,b),A&4&&i(R,"style",C.style,M.style,b),A&8){const K=a.dynamicProps;for(let Y=0;Y{j&&Ue(j,p,a,u),V&&bt(a,u,p,"updated")},y)},Le=(u,a,p,y,b,_,x)=>{for(let R=0;R{if(a!==p){if(a!==X)for(const _ in a)!Jt(_)&&!(_ in p)&&i(u,_,a[_],null,b,y);for(const _ in p){if(Jt(_))continue;const x=p[_],R=a[_];x!==R&&_!=="value"&&i(u,_,R,x,b,y)}"value"in p&&i(u,"value",a.value,p.value,b)}},yt=(u,a,p,y,b,_,x,R,A)=>{const E=a.el=u?u.el:l(""),V=a.anchor=u?u.anchor:l("");let{patchFlag:C,dynamicChildren:M,slotScopeIds:j}=a;j&&(R=R?R.concat(j):j),u==null?(s(E,p,y),s(V,p,y),Me(a.children||[],p,V,b,_,x,R,A)):C>0&&C&64&&M&&u.dynamicChildren&&u.dynamicChildren.length===M.length?(Le(u.dynamicChildren,M,p,b,_,x,R),(a.key!=null||b&&a===b.subTree)&&ji(u,a,!0)):G(u,a,p,V,b,_,x,R,A)},Ve=(u,a,p,y,b,_,x,R,A)=>{a.slotScopeIds=R,u==null?a.shapeFlag&512?b.ctx.activate(a,p,y,x,A):Kt(a,p,y,b,_,x,A):Ct(u,a,A)},Kt=(u,a,p,y,b,_,x)=>{const R=u.component=sc(u,y,b);if(Hs(u)&&(R.ctx.renderer=D),ic(R,!1,x),R.asyncDep){if(b&&b.registerDep(R,le,x),!u.el){const A=R.subTree=Re(lt);F(null,A,a,p),u.placeholder=A.el}}else le(R,u,a,p,b,_,x)},Ct=(u,a,p)=>{const y=a.component=u.component;if(jl(u,a,p))if(y.asyncDep&&!y.asyncResolved){z(y,a,p);return}else y.next=a,y.update();else a.el=u.el,y.vnode=a},le=(u,a,p,y,b,_,x)=>{const R=()=>{if(u.isMounted){let{next:C,bu:M,u:j,parent:K,vnode:Y}=u;{const He=Hi(u);if(He){C&&(C.el=Y.el,z(u,C,x)),He.asyncDep.then(()=>{ve(()=>{u.isUnmounted||E()},b)});return}}let J=C,se;Et(u,!1),C?(C.el=Y.el,z(u,C,x)):C=Y,M&&yn(M),(se=C.props&&C.props.onVnodeBeforeUpdate)&&Ue(se,K,C,Y),Et(u,!0);const ce=lr(u),je=u.subTree;u.subTree=ce,S(je,ce,h(je.el),v(je),u,b,_),C.el=ce.el,J===null&&Hl(u,ce.el),j&&ve(j,b),(se=C.props&&C.props.onVnodeUpdated)&&ve(()=>Ue(se,K,C,Y),b)}else{let C;const{el:M,props:j}=a,{bm:K,m:Y,parent:J,root:se,type:ce}=u,je=Zt(a);Et(u,!1),K&&yn(K),!je&&(C=j&&j.onVnodeBeforeMount)&&Ue(C,J,a),Et(u,!0);{se.ce&&se.ce._hasShadowRoot()&&se.ce._injectChildStyle(ce,u.parent?u.parent.type:void 0);const He=u.subTree=lr(u);S(null,He,p,y,u,b,_),a.el=He.el}if(Y&&ve(Y,b),!je&&(C=j&&j.onVnodeMounted)){const He=a;ve(()=>Ue(C,J,He),b)}(a.shapeFlag&256||J&&Zt(J.vnode)&&J.vnode.shapeFlag&256)&&u.a&&ve(u.a,b),u.isMounted=!0,a=p=y=null}};u.scope.on();const A=u.effect=new Qr(R);u.scope.off();const E=u.update=A.run.bind(A),V=u.job=A.runIfDirty.bind(A);V.i=u,V.id=u.uid,A.scheduler=()=>Fs(V),Et(u,!0),E()},z=(u,a,p)=>{a.component=u;const y=u.vnode.props;u.vnode=a,u.next=null,Ul(u,a.props,y,p),kl(u,a.children,p),rt(),Zs(u),it()},G=(u,a,p,y,b,_,x,R,A=!1)=>{const E=u&&u.children,V=u?u.shapeFlag:0,C=a.children,{patchFlag:M,shapeFlag:j}=a;if(M>0){if(M&128){at(E,C,p,y,b,_,x,R,A);return}else if(M&256){ze(E,C,p,y,b,_,x,R,A);return}}j&8?(V&16&&Se(E,b,_),C!==E&&f(p,C)):V&16?j&16?at(E,C,p,y,b,_,x,R,A):Se(E,b,_,!0):(V&8&&f(p,""),j&16&&Me(C,p,y,b,_,x,R,A))},ze=(u,a,p,y,b,_,x,R,A)=>{u=u||Dt,a=a||Dt;const E=u.length,V=a.length,C=Math.min(E,V);let M;for(M=0;MV?Se(u,b,_,!0,!1,C):Me(a,p,y,b,_,x,R,A,C)},at=(u,a,p,y,b,_,x,R,A)=>{let E=0;const V=a.length;let C=u.length-1,M=V-1;for(;E<=C&&E<=M;){const j=u[E],K=a[E]=A?Ze(a[E]):We(a[E]);if(kt(j,K))S(j,K,p,null,b,_,x,R,A);else break;E++}for(;E<=C&&E<=M;){const j=u[C],K=a[M]=A?Ze(a[M]):We(a[M]);if(kt(j,K))S(j,K,p,null,b,_,x,R,A);else break;C--,M--}if(E>C){if(E<=M){const j=M+1,K=jM)for(;E<=C;)ye(u[E],b,_,!0),E++;else{const j=E,K=E,Y=new Map;for(E=K;E<=M;E++){const be=a[E]=A?Ze(a[E]):We(a[E]);be.key!=null&&Y.set(be.key,E)}let J,se=0;const ce=M-K+1;let je=!1,He=0;const Gt=new Array(ce);for(E=0;E=ce){ye(be,b,_,!0);continue}let Be;if(be.key!=null)Be=Y.get(be.key);else for(J=K;J<=M;J++)if(Gt[J-K]===0&&kt(be,a[J])){Be=J;break}Be===void 0?ye(be,b,_,!0):(Gt[Be-K]=E+1,Be>=He?He=Be:je=!0,S(be,a[Be],p,null,b,_,x,R,A),se++)}const Ws=je?Jl(Gt):Dt;for(J=Ws.length-1,E=ce-1;E>=0;E--){const be=K+E,Be=a[be],ks=a[be+1],qs=be+1{const{el:_,type:x,transition:R,children:A,shapeFlag:E}=u;if(E&6){Fe(u.component.subTree,a,p,y);return}if(E&128){u.suspense.move(a,p,y);return}if(E&64){x.move(u,a,p,D);return}if(x===Xe){s(_,a,p);for(let C=0;CR.enter(_),b));else{const{leave:C,delayLeave:M,afterLeave:j}=R,K=()=>{u.ctx.isUnmounted?r(_):s(_,a,p)},Y=()=>{const J=_._isLeaving||!!_[Xn];_._isLeaving&&_[Xn](!0),R.persisted&&!J?K():C(_,()=>{K(),j&&j()})};M?M(_,K,Y):Y()}else s(_,a,p)},ye=(u,a,p,y=!1,b=!1)=>{const{type:_,props:x,ref:R,children:A,dynamicChildren:E,shapeFlag:V,patchFlag:C,dirs:M,cacheIndex:j,memo:K}=u;if(C===-2&&(b=!1),R!=null&&(rt(),Xt(R,null,p,u,!0),it()),j!=null&&(a.renderCache[j]=void 0),V&256){a.ctx.deactivate(u);return}const Y=V&1&&M,J=!Zt(u);let se;if(J&&(se=x&&x.onVnodeBeforeUnmount)&&Ue(se,a,u),V&6)vt(u.component,p,y);else{if(V&128){u.suspense.unmount(p,y);return}Y&&bt(u,null,a,"beforeUnmount"),V&64?u.type.remove(u,a,p,D,y):E&&!E.hasOnce&&(_!==Xe||C>0&&C&64)?Se(E,a,p,!1,!0):(_===Xe&&C&384||!b&&V&16)&&Se(A,a,p),y&&Ot(u)}const ce=K!=null&&j==null;(J&&(se=x&&x.onVnodeUnmounted)||Y||ce)&&ve(()=>{se&&Ue(se,a,u),Y&&bt(u,null,a,"unmounted"),ce&&(u.el=null)},p)},Ot=u=>{const{type:a,el:p,anchor:y,transition:b}=u;if(a===Xe){Pt(p,y);return}if(a===ts){T(u);return}const _=()=>{r(p),b&&!b.persisted&&b.afterLeave&&b.afterLeave()};if(u.shapeFlag&1&&b&&!b.persisted){const{leave:x,delayLeave:R}=b,A=()=>x(p,_);R?R(u.el,_,A):A()}else _()},Pt=(u,a)=>{let p;for(;u!==a;)p=g(u),r(u),u=p;r(a)},vt=(u,a,p)=>{const{bum:y,scope:b,job:_,subTree:x,um:R,m:A,a:E}=u;fr(A),fr(E),y&&yn(y),b.stop(),_&&(_.flags|=8,ye(x,u,a,p)),R&&ve(R,a),ve(()=>{u.isUnmounted=!0},a)},Se=(u,a,p,y=!1,b=!1,_=0)=>{for(let x=_;x{if(u.shapeFlag&6)return v(u.component.subTree);if(u.shapeFlag&128)return u.suspense.next();const a=g(u.anchor||u.el),p=a&&a[ll];return p?g(p):a};let P=!1;const w=(u,a,p)=>{let y;u==null?a._vnode&&(ye(a._vnode,null,null,!0),y=a._vnode.component):S(a._vnode||null,u,a,null,null,null,p),a._vnode=u,P||(P=!0,Zs(y),gi(),P=!1)},D={p:S,um:ye,m:Fe,r:Ot,mt:Kt,mc:Me,pc:G,pbc:Le,n:v,o:e};return{render:w,hydrate:void 0,createApp:Nl(w)}}function es({type:e,props:t},n){return n==="svg"&&e==="foreignObject"||n==="mathml"&&e==="annotation-xml"&&t&&t.encoding&&t.encoding.includes("html")?void 0:n}function Et({effect:e,job:t},n){n?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function zl(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function ji(e,t,n=!1){const s=e.children,r=t.children;if(L(s)&&L(r))for(let i=0;i>1,e[n[l]]0&&(t[s]=n[i-1]),n[i]=s)}}for(i=n.length,o=n[i-1];i-- >0;)n[i]=o,o=t[o];return n}function Hi(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:Hi(t)}function fr(e){if(e)for(let t=0;te.__isSuspense;function Ql(e,t){t&&t.pendingBranch?L(e)?t.effects.push(...e):t.effects.push(e):nl(e)}const Xe=Symbol.for("v-fgt"),Gn=Symbol.for("v-txt"),lt=Symbol.for("v-cmt"),ts=Symbol.for("v-stc"),wt=[];let Ae=null;function Ki(e=!1){wt.push(Ae=e?null:[])}function Gi(){wt.pop(),Ae=wt[wt.length-1]||null}let on=1;function Cn(e,t=!1){on+=e,e<0&&Ae&&t&&(Ae.hasOnce=!0)}function Wi(e){return e.dynamicChildren=on>0?Ae||Dt:null,Gi(),on>0&&Ae&&Ae.push(e),e}function nf(e,t,n,s,r,i){return Wi($i(e,t,n,s,r,i,!0))}function ki(e,t,n,s,r){return Wi(Re(e,t,n,s,r,!0))}function On(e){return e?e.__v_isVNode===!0:!1}function kt(e,t){return e.type===t.type&&e.key===t.key}const qi=({key:e})=>e??null,En=({ref:e,ref_key:t,ref_for:n})=>(typeof e=="number"&&(e=""+e),e!=null?ne(e)||he(e)||B(e)?{i:Ee,r:e,k:t,f:!!n}:e:null);function $i(e,t=null,n=null,s=0,r=null,i=e===Xe?0:1,o=!1,l=!1){const c={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&qi(t),ref:t&&En(t),scopeId:_i,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:i,patchFlag:s,dynamicProps:r,dynamicChildren:null,appContext:null,ctx:Ee};return l?(Pn(c,n),i&128&&e.normalize(c)):n&&(c.shapeFlag|=ne(n)?8:16),on>0&&!o&&Ae&&(c.patchFlag>0||i&6)&&c.patchFlag!==32&&Ae.push(c),c}const Re=Yl;function Yl(e,t=null,n=null,s=0,r=null,i=!1){if((!e||e===Rl)&&(e=lt),On(e)){const l=jt(e,t,!0);return n&&Pn(l,n),on>0&&!i&&Ae&&(l.shapeFlag&6?Ae[Ae.indexOf(e)]=l:Ae.push(l)),l.patchFlag=-2,l}if(fc(e)&&(e=e.__vccOpts),t){t=Xl(t);let{class:l,style:c}=t;l&&!ne(l)&&(t.class=Cs(l)),$(c)&&(Ls(c)&&!L(c)&&(c=fe({},c)),t.style=ws(c))}const o=ne(e)?1:Ui(e)?128:Bn(e)?64:$(e)?4:B(e)?2:0;return $i(e,t,n,s,r,o,i,!0)}function Xl(e){return e?Ls(e)||Ni(e)?fe({},e):e:null}function jt(e,t,n=!1,s=!1){const{props:r,ref:i,patchFlag:o,children:l,transition:c}=e,d=t?ec(r||{},t):r,f={__v_isVNode:!0,__v_skip:!0,type:e.type,props:d,key:d&&qi(d),ref:t&&t.ref?n&&i?L(i)?i.concat(En(t)):[i,En(t)]:En(t):i,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:l,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==Xe?o===-1?16:o|16:o,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:c,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&jt(e.ssContent),ssFallback:e.ssFallback&&jt(e.ssFallback),placeholder:e.placeholder,el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return c&&s&&js(f,c.clone(f)),f}function Zl(e=" ",t=0){return Re(Gn,null,e,t)}function sf(e="",t=!1){return t?(Ki(),ki(lt,null,e)):Re(lt,null,e)}function We(e){return e==null||typeof e=="boolean"?Re(lt):L(e)?Re(Xe,null,e.slice()):On(e)?Ze(e):Re(Gn,null,String(e))}function Ze(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:jt(e)}function Pn(e,t){let n=0;const{shapeFlag:s}=e;if(t==null)t=null;else if(L(t))n=16;else if(typeof t=="object")if(s&65){const r=t.default;r&&(r._c&&(r._d=!1),Pn(e,r()),r._c&&(r._d=!0));return}else{n=32;const r=t._;!r&&!Ni(t)?t._ctx=Ee:r===3&&Ee&&(Ee.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else if(B(t)){if(s&65){Pn(e,{default:t});return}t={default:t,_ctx:Ee},n=32}else t=String(t),s&64?(n=16,t=[Zl(t)]):n=8;e.children=t,e.shapeFlag|=n}function ec(...e){const t={};for(let n=0;nde||Ee;let Tn,ln;{const e=Vn(),t=(n,s)=>{let r;return(r=e[n])||(r=e[n]=[]),r.push(s),i=>{r.length>1?r.forEach(o=>o(i)):r[0](i)}};Tn=t("__VUE_INSTANCE_SETTERS__",n=>de=n),ln=t("__VUE_SSR_SETTERS__",n=>cn=n)}const dn=e=>{const t=de;return Tn(e),e.scope.on(),()=>{e.scope.off(),Tn(t)}},ar=()=>{de&&de.scope.off(),Tn(null)};function zi(e){return e.vnode.shapeFlag&4}let cn=!1;function ic(e,t=!1,n=!1){t&&ln(t);const{props:s,children:r}=e.vnode,i=zi(e);Bl(e,s,i,t),Wl(e,r,n||t);const o=i?oc(e,t):void 0;return t&&ln(!1),o}function oc(e,t){const n=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,Sl);const{setup:s}=n;if(s){rt();const r=e.setupContext=s.length>1?cc(e):null,i=dn(e),o=an(s,e,0,[e.props,r]),l=Gr(o);if(it(),i(),(l||e.sp)&&!Zt(e)&&Ai(e),l){if(o.then(ar,ar),t)return o.then(c=>{ln(!0);try{dr(e,c,t)}finally{ln(!1)}}).catch(c=>{Hn(c,e,0)});e.asyncDep=o}else dr(e,o)}else Ji(e)}function dr(e,t,n){B(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:$(t)&&(e.setupState=di(t)),Ji(e)}function Ji(e,t,n){const s=e.type;e.render||(e.render=s.render||qe);{const r=dn(e);rt();try{wl(e)}finally{it(),r()}}}const lc={get(e,t){return ae(e,"get",""),e[t]}};function cc(e){const t=n=>{e.exposed=n||{}};return{attrs:new Proxy(e.attrs,lc),slots:e.slots,emit:e.emit,expose:t}}function Wn(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(di(ko(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in en)return en[n](e)},has(t,n){return n in t||n in en}})):e.proxy}function uc(e,t=!0){return B(e)?e.displayName||e.name:e.name||t&&e.__name}function fc(e){return B(e)&&"__vccOpts"in e}const Ce=(e,t)=>Yo(e,t,cn);function Qi(e,t,n){try{Cn(-1);const s=arguments.length;return s===2?$(t)&&!L(t)?On(t)?Re(e,null,[t]):Re(e,t):Re(e,null,t):(s>3?n=Array.prototype.slice.call(arguments,2):s===3&&On(n)&&(n=[n]),Re(e,t,n))}finally{Cn(1)}}const ac="3.5.42";/** +* @vue/runtime-dom v3.5.42 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let ys;const hr=typeof window<"u"&&window.trustedTypes;if(hr)try{ys=hr.createPolicy("vue",{createHTML:e=>e})}catch{}const Yi=ys?e=>ys.createHTML(e):e=>e,dc="http://www.w3.org/2000/svg",hc="http://www.w3.org/1998/Math/MathML",Ye=typeof document<"u"?document:null,pr=Ye&&Ye.createElement("template"),pc={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,s)=>{const r=t==="svg"?Ye.createElementNS(dc,e):t==="mathml"?Ye.createElementNS(hc,e):n?Ye.createElement(e,{is:n}):Ye.createElement(e);return e==="select"&&s&&s.multiple!=null&&r.setAttribute("multiple",s.multiple),r},createText:e=>Ye.createTextNode(e),createComment:e=>Ye.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>Ye.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,s,r,i){const o=n?n.previousSibling:t.lastChild;if(r&&(r===i||r.nextSibling))for(;t.insertBefore(r.cloneNode(!0),n),!(r===i||!(r=r.nextSibling)););else{pr.innerHTML=Yi(s==="svg"?`${e}`:s==="mathml"?`${e}`:e);const l=pr.content;if(s==="svg"||s==="mathml"){const c=l.firstChild;for(;c.firstChild;)l.appendChild(c.firstChild);l.removeChild(c)}t.insertBefore(l,n)}return[o?o.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},gc=Symbol("_vtc");function mc(e,t,n){const s=e[gc];s&&(t=(t?[t,...s]:[...s]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}const gr=Symbol("_vod"),_c=Symbol("_vsh"),yc=Symbol(""),vc=/(?:^|;)\s*display\s*:/;function bc(e,t,n){const s=e.style,r=ne(n);let i=!1;if(n&&!r){if(t)if(ne(t))for(const o of t.split(";")){const l=o.slice(0,o.indexOf(":")).trim();n[l]==null&&zt(s,l,"")}else for(const o in t)n[o]==null&&zt(s,o,"");for(const o in n){o==="display"&&(i=!0);const l=n[o];l!=null?Ac(e,o,!ne(t)&&t?t[o]:void 0,l)||zt(s,o,l):zt(s,o,"")}}else if(r){if(t!==n){const o=s[yc];o&&(n+=";"+o),s.cssText=n,i=vc.test(n)}}else t&&e.removeAttribute("style");gr in e&&(e[gr]=i?s.display:"",e[_c]&&(s.display="none"))}const mn=/\s*!important$/;function zt(e,t,n){if(L(n))n.forEach(s=>zt(e,t,s));else if(n==null&&(n=""),t.startsWith("--"))mn.test(n)?e.setProperty(t,n.replace(mn,""),"important"):e.setProperty(t,n);else{const s=Ec(e,t);mn.test(n)?e.setProperty(_t(s),n.replace(mn,""),"important"):e[s]=n}}const mr=["Webkit","Moz","ms"],ns={};function Ec(e,t){const n=ns[t];if(n)return n;let s=_e(t);if(s!=="filter"&&s in e)return ns[t]=s;s=Mn(s);for(let r=0;rss||(Oc.then(()=>ss=0),ss=Date.now());function Tc(e,t){const n=s=>{if(!s._vts)s._vts=Date.now();else if(s._vts<=n.attached)return;const r=n.value;if(L(r)){const i=s.stopImmediatePropagation;s.stopImmediatePropagation=()=>{i.call(s),s._stopped=!0};const o=r.slice(),l=[s];for(let c=0;ce.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,Ic=(e,t,n,s,r,i)=>{const o=r==="svg";t==="class"?mc(e,s,o):t==="style"?bc(e,n,s):In(t)?Nn(t)||xc(e,t,n,s,i):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):Nc(e,t,s,o))?(vr(e,t,s),!e.tagName.includes("-")&&(t==="value"||t==="checked"||t==="selected")&&yr(e,t,s,o,i,t!=="value")):e._isVueCE&&(Dc(e,t)||e._def.__asyncLoader&&(/[A-Z]/.test(t)||!ne(s)))?vr(e,_e(t),s,i,t):(t==="true-value"?e._trueValue=s:t==="false-value"&&(e._falseValue=s),yr(e,t,s,o))};function Nc(e,t,n,s){if(s)return!!(t==="innerHTML"||t==="textContent"||t in e&&Er(t)&&B(n));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="autocorrect"||t==="sandbox"&&e.tagName==="IFRAME"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const r=e.tagName;if(r==="IMG"||r==="VIDEO"||r==="CANVAS"||r==="SOURCE")return!1}return Er(t)&&ne(n)?!1:t in e}function Dc(e,t){const n=e._def.props;if(!n)return!1;const s=_e(t);return Array.isArray(n)?n.some(r=>_e(r)===s):Object.keys(n).some(r=>_e(r)===s)}const mt=e=>{const t=e.props["onUpdate:modelValue"]||!1;return L(t)?n=>yn(t,n):t};function Mc(e){e.target.composing=!0}function Ar(e){const t=e.target;t.composing&&(t.composing=!1,t.dispatchEvent(new Event("input")))}const xe=Symbol("_assign"),_n=Symbol("_initialValue");function rs(e,t,n){return t&&(e=e.trim()),n&&(e=Ln(e)),e}const rf={created(e,{modifiers:{lazy:t,trim:n,number:s}},r){e.parentNode&&(e.type==="text"?e[_n]=e.defaultValue.replace(/[\r\n]/g,""):e.type==="textarea"&&(e[_n]=e.defaultValue.replace(/\r\n?/g,` +`))),e[xe]=mt(r);const i=s||r.props&&r.props.type==="number";nt(e,t?"change":"input",o=>{o.target.composing||e[xe](rs(e.value,n,i))}),(n||i)&&nt(e,"change",()=>{e.value=rs(e.value,n,i)}),t||(nt(e,"compositionstart",Mc),nt(e,"compositionend",Ar),nt(e,"change",Ar))},mounted(e,{value:t,modifiers:{trim:n,number:s}}){const r=t??"",i=e[_n];delete e[_n],i!==void 0&&(e.type==="text"||e.type==="textarea")&&e.value!==i?e[xe](rs(e.value,n,s)):e.value=r},beforeUpdate(e,{value:t,oldValue:n,modifiers:{lazy:s,trim:r,number:i}},o){if(e[xe]=mt(o),e.composing)return;const l=(i||e.type==="number")&&!/^0\d/.test(e.value)?Ln(e.value):e.value,c=t??"";if(l===c)return;const d=e.getRootNode();(d instanceof Document||d instanceof ShadowRoot)&&d.activeElement===e&&e.type!=="range"&&(s&&t===n||r&&e.value.trim()===c)||(e.value=c)}},of={deep:!0,created(e,t,n){e[xe]=mt(n),nt(e,"change",()=>{const s=e._modelValue,r=Ht(e),i=e.checked,o=e[xe];if(L(s)){const l=Os(s,r),c=l!==-1;if(i&&!c)o(s.concat(r));else if(!i&&c){const d=[...s];d.splice(l,1),o(d)}}else if(st(s)){const l=new Set(s);i?l.add(r):l.delete(r),o(l)}else o(Xi(e,i))})},mounted:Rr,beforeUpdate(e,t,n){e[xe]=mt(n),Rr(e,t,n)}};function Rr(e,{value:t,oldValue:n},s){e._modelValue=t;let r;if(L(t))r=Os(t,s.props.value)>-1;else if(st(t))r=t.has(s.props.value);else{if(t===n)return;r=Te(t,Xi(e,!0))}e.checked!==r&&(e.checked=r)}const lf={created(e,{value:t},n){e.checked=Te(t,n.props.value),e[xe]=mt(n),nt(e,"change",()=>{e[xe](Ht(e))})},beforeUpdate(e,{value:t,oldValue:n},s){e[xe]=mt(s),t!==n&&(e.checked=Te(t,s.props.value))}},cf={deep:!0,created(e,{value:t,modifiers:{number:n}},s){e._modelValue=t,nt(e,"change",()=>{const r=Array.prototype.filter.call(e.options,c=>c.selected).map(c=>n?Ln(Ht(c)):Ht(c)),i=e.multiple,o=i?st(e._modelValue)?new Set(r):r:r[0],l=e._pendingValue=[i,i?L(o)?r.slice():r:o];try{e[xe](o)}finally{Vs(()=>{e._pendingValue===l&&(e._pendingValue=void 0)})}}),e[xe]=mt(s)},mounted(e,{value:t}){xr(e,t)},beforeUpdate(e,{value:t},n){e._modelValue=t,e[xe]=mt(n)},updated(e,{value:t}){const n=e._pendingValue;e._pendingValue=void 0,(!n||n[0]!==e.multiple||!Lc(t,n[1],n[0]))&&xr(e,t)}};function Lc(e,t,n){if(!n||L(e))return Te(e,t);if(st(e)){if(e.size!==t.length)return!1;for(const s of t)if(!e.has(s))return!1;return!0}return!1}function xr(e,t){const n=e.multiple,s=L(t);if(!(n&&!s&&!st(t))){for(let r=0,i=e.options.length;rString(d)===String(l)):o.selected=Os(t,l)>-1}else o.selected=t.has(l);else if(Te(Ht(o),t)){e.selectedIndex!==r&&(e.selectedIndex=r);return}}!n&&e.selectedIndex!==-1&&(e.selectedIndex=-1)}}function Ht(e){return"_value"in e?e._value:e.value}function Xi(e,t){const n=t?"_trueValue":"_falseValue";return n in e?e[n]:t}const Vc={esc:"escape",space:" ",up:"arrow-up",left:"arrow-left",right:"arrow-right",down:"arrow-down",delete:"backspace"},uf=(e,t)=>{const n=e._withKeys||(e._withKeys={}),s=t.join(".");return n[s]||(n[s]=r=>{if(!("key"in r))return;const i=_t(r.key);if(t.some(o=>o===i||Vc[o]===i))return e(r)})},Fc=fe({patchProp:Ic},pc);let Sr;function jc(){return Sr||(Sr=ql(Fc))}const Hc=(...e)=>{const t=jc().createApp(...e),{mount:n}=t;return t.mount=s=>{const r=Uc(s);if(!r)return;const i=t._component;!B(i)&&!i.render&&!i.template&&(i.template=r.innerHTML),r.nodeType===1&&(r.textContent="");const o=n(r,!1,Bc(r));return r instanceof Element&&(r.removeAttribute("v-cloak"),r.setAttribute("data-v-app","")),o},t};function Bc(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function Uc(e){return ne(e)?document.querySelector(e):e}/*! + * vue-router v4.6.4 + * (c) 2025 Eduardo San Martin Morote + * @license MIT + */const Nt=typeof document<"u";function Zi(e){return typeof e=="object"||"displayName"in e||"props"in e||"__vccOpts"in e}function Kc(e){return e.__esModule||e[Symbol.toStringTag]==="Module"||e.default&&Zi(e.default)}const W=Object.assign;function is(e,t){const n={};for(const s in t){const r=t[s];n[s]=De(r)?r.map(e):e(r)}return n}const tn=()=>{},De=Array.isArray;function wr(e,t){const n={};for(const s in e)n[s]=s in t?t[s]:e[s];return n}const eo=/#/g,Gc=/&/g,Wc=/\//g,kc=/=/g,qc=/\?/g,to=/\+/g,$c=/%5B/g,zc=/%5D/g,no=/%5E/g,Jc=/%60/g,so=/%7B/g,Qc=/%7C/g,ro=/%7D/g,Yc=/%20/g;function Ks(e){return e==null?"":encodeURI(""+e).replace(Qc,"|").replace($c,"[").replace(zc,"]")}function Xc(e){return Ks(e).replace(so,"{").replace(ro,"}").replace(no,"^")}function vs(e){return Ks(e).replace(to,"%2B").replace(Yc,"+").replace(eo,"%23").replace(Gc,"%26").replace(Jc,"`").replace(so,"{").replace(ro,"}").replace(no,"^")}function Zc(e){return vs(e).replace(kc,"%3D")}function eu(e){return Ks(e).replace(eo,"%23").replace(qc,"%3F")}function tu(e){return eu(e).replace(Wc,"%2F")}function un(e){if(e==null)return null;try{return decodeURIComponent(""+e)}catch{}return""+e}const nu=/\/$/,su=e=>e.replace(nu,"");function os(e,t,n="/"){let s,r={},i="",o="";const l=t.indexOf("#");let c=t.indexOf("?");return c=l>=0&&c>l?-1:c,c>=0&&(s=t.slice(0,c),i=t.slice(c,l>0?l:t.length),r=e(i.slice(1))),l>=0&&(s=s||t.slice(0,l),o=t.slice(l,t.length)),s=lu(s??t,n),{fullPath:s+i+o,path:s,query:r,hash:un(o)}}function ru(e,t){const n=t.query?e(t.query):"";return t.path+(n&&"?")+n+(t.hash||"")}function Cr(e,t){return!t||!e.toLowerCase().startsWith(t.toLowerCase())?e:e.slice(t.length)||"/"}function iu(e,t,n){const s=t.matched.length-1,r=n.matched.length-1;return s>-1&&s===r&&Bt(t.matched[s],n.matched[r])&&io(t.params,n.params)&&e(t.query)===e(n.query)&&t.hash===n.hash}function Bt(e,t){return(e.aliasOf||e)===(t.aliasOf||t)}function io(e,t){if(Object.keys(e).length!==Object.keys(t).length)return!1;for(var n in e)if(!ou(e[n],t[n]))return!1;return!0}function ou(e,t){return De(e)?Or(e,t):De(t)?Or(t,e):(e==null?void 0:e.valueOf())===(t==null?void 0:t.valueOf())}function Or(e,t){return De(t)?e.length===t.length&&e.every((n,s)=>n===t[s]):e.length===1&&e[0]===t}function lu(e,t){if(e.startsWith("/"))return e;if(!e)return t;const n=t.split("/"),s=e.split("/"),r=s[s.length-1];(r===".."||r===".")&&s.push("");let i=n.length-1,o,l;for(o=0;o1&&i--;else break;return n.slice(0,i).join("/")+"/"+s.slice(o).join("/")}const dt={path:"/",name:void 0,params:{},query:{},hash:"",fullPath:"/",matched:[],meta:{},redirectedFrom:void 0};let bs=function(e){return e.pop="pop",e.push="push",e}({}),ls=function(e){return e.back="back",e.forward="forward",e.unknown="",e}({});function cu(e){if(!e)if(Nt){const t=document.querySelector("base");e=t&&t.getAttribute("href")||"/",e=e.replace(/^\w+:\/\/[^\/]+/,"")}else e="/";return e[0]!=="/"&&e[0]!=="#"&&(e="/"+e),su(e)}const uu=/^[^#]+#/;function fu(e,t){return e.replace(uu,"#")+t}function au(e,t){const n=document.documentElement.getBoundingClientRect(),s=e.getBoundingClientRect();return{behavior:t.behavior,left:s.left-n.left-(t.left||0),top:s.top-n.top-(t.top||0)}}const kn=()=>({left:window.scrollX,top:window.scrollY});function du(e){let t;if("el"in e){const n=e.el,s=typeof n=="string"&&n.startsWith("#"),r=typeof n=="string"?s?document.getElementById(n.slice(1)):document.querySelector(n):n;if(!r)return;t=au(r,e)}else t=e;"scrollBehavior"in document.documentElement.style?window.scrollTo(t):window.scrollTo(t.left!=null?t.left:window.scrollX,t.top!=null?t.top:window.scrollY)}function Pr(e,t){return(history.state?history.state.position-t:-1)+e}const Es=new Map;function hu(e,t){Es.set(e,t)}function pu(e){const t=Es.get(e);return Es.delete(e),t}function gu(e){return typeof e=="string"||e&&typeof e=="object"}function oo(e){return typeof e=="string"||typeof e=="symbol"}let te=function(e){return e[e.MATCHER_NOT_FOUND=1]="MATCHER_NOT_FOUND",e[e.NAVIGATION_GUARD_REDIRECT=2]="NAVIGATION_GUARD_REDIRECT",e[e.NAVIGATION_ABORTED=4]="NAVIGATION_ABORTED",e[e.NAVIGATION_CANCELLED=8]="NAVIGATION_CANCELLED",e[e.NAVIGATION_DUPLICATED=16]="NAVIGATION_DUPLICATED",e}({});const lo=Symbol("");te.MATCHER_NOT_FOUND+"",te.NAVIGATION_GUARD_REDIRECT+"",te.NAVIGATION_ABORTED+"",te.NAVIGATION_CANCELLED+"",te.NAVIGATION_DUPLICATED+"";function Ut(e,t){return W(new Error,{type:e,[lo]:!0},t)}function Qe(e,t){return e instanceof Error&&lo in e&&(t==null||!!(e.type&t))}const mu=["params","query","hash"];function _u(e){if(typeof e=="string")return e;if(e.path!=null)return e.path;const t={};for(const n of mu)n in e&&(t[n]=e[n]);return JSON.stringify(t,null,2)}function yu(e){const t={};if(e===""||e==="?")return t;const n=(e[0]==="?"?e.slice(1):e).split("&");for(let s=0;sr&&vs(r)):[s&&vs(s)]).forEach(r=>{r!==void 0&&(t+=(t.length?"&":"")+n,r!=null&&(t+="="+r))})}return t}function vu(e){const t={};for(const n in e){const s=e[n];s!==void 0&&(t[n]=De(s)?s.map(r=>r==null?null:""+r):s==null?s:""+s)}return t}const bu=Symbol(""),Ir=Symbol(""),qn=Symbol(""),Gs=Symbol(""),As=Symbol("");function qt(){let e=[];function t(s){return e.push(s),()=>{const r=e.indexOf(s);r>-1&&e.splice(r,1)}}function n(){e=[]}return{add:t,list:()=>e.slice(),reset:n}}function pt(e,t,n,s,r,i=o=>o()){const o=s&&(s.enterCallbacks[r]=s.enterCallbacks[r]||[]);return()=>new Promise((l,c)=>{const d=g=>{g===!1?c(Ut(te.NAVIGATION_ABORTED,{from:n,to:t})):g instanceof Error?c(g):gu(g)?c(Ut(te.NAVIGATION_GUARD_REDIRECT,{from:t,to:g})):(o&&s.enterCallbacks[r]===o&&typeof g=="function"&&o.push(g),l())},f=i(()=>e.call(s&&s.instances[r],t,n,d));let h=Promise.resolve(f);e.length<3&&(h=h.then(d)),h.catch(g=>c(g))})}function cs(e,t,n,s,r=i=>i()){const i=[];for(const o of e)for(const l in o.components){let c=o.components[l];if(!(t!=="beforeRouteEnter"&&!o.instances[l]))if(Zi(c)){const d=(c.__vccOpts||c)[t];d&&i.push(pt(d,n,s,o,l,r))}else{let d=c();i.push(()=>d.then(f=>{if(!f)throw new Error(`Couldn't resolve component "${l}" at "${o.path}"`);const h=Kc(f)?f.default:f;o.mods[l]=f,o.components[l]=h;const g=(h.__vccOpts||h)[t];return g&&pt(g,n,s,o,l,r)()}))}}return i}function Eu(e,t){const n=[],s=[],r=[],i=Math.max(t.matched.length,e.matched.length);for(let o=0;oBt(d,l))?s.push(l):n.push(l));const c=e.matched[o];c&&(t.matched.find(d=>Bt(d,c))||r.push(c))}return[n,s,r]}/*! + * vue-router v4.6.4 + * (c) 2025 Eduardo San Martin Morote + * @license MIT + */let Au=()=>location.protocol+"//"+location.host;function co(e,t){const{pathname:n,search:s,hash:r}=t,i=e.indexOf("#");if(i>-1){let o=r.includes(e.slice(i))?e.slice(i).length:1,l=r.slice(o);return l[0]!=="/"&&(l="/"+l),Cr(l,"")}return Cr(n,e)+s+r}function Ru(e,t,n,s){let r=[],i=[],o=null;const l=({state:g})=>{const m=co(e,location),I=n.value,S=t.value;let H=0;if(g){if(n.value=m,t.value=g,o&&o===I){o=null;return}H=S?g.position-S.position:0}else s(m);r.forEach(F=>{F(n.value,I,{delta:H,type:bs.pop,direction:H?H>0?ls.forward:ls.back:ls.unknown})})};function c(){o=n.value}function d(g){r.push(g);const m=()=>{const I=r.indexOf(g);I>-1&&r.splice(I,1)};return i.push(m),m}function f(){if(document.visibilityState==="hidden"){const{history:g}=window;if(!g.state)return;g.replaceState(W({},g.state,{scroll:kn()}),"")}}function h(){for(const g of i)g();i=[],window.removeEventListener("popstate",l),window.removeEventListener("pagehide",f),document.removeEventListener("visibilitychange",f)}return window.addEventListener("popstate",l),window.addEventListener("pagehide",f),document.addEventListener("visibilitychange",f),{pauseListeners:c,listen:d,destroy:h}}function Nr(e,t,n,s=!1,r=!1){return{back:e,current:t,forward:n,replaced:s,position:window.history.length,scroll:r?kn():null}}function xu(e){const{history:t,location:n}=window,s={value:co(e,n)},r={value:t.state};r.value||i(s.value,{back:null,current:s.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0);function i(c,d,f){const h=e.indexOf("#"),g=h>-1?(n.host&&document.querySelector("base")?e:e.slice(h))+c:Au()+e+c;try{t[f?"replaceState":"pushState"](d,"",g),r.value=d}catch(m){console.error(m),n[f?"replace":"assign"](g)}}function o(c,d){i(c,W({},t.state,Nr(r.value.back,c,r.value.forward,!0),d,{position:r.value.position}),!0),s.value=c}function l(c,d){const f=W({},r.value,t.state,{forward:c,scroll:kn()});i(f.current,f,!0),i(c,W({},Nr(s.value,c,null),{position:f.position+1},d),!1),s.value=c}return{location:s,state:r,push:l,replace:o}}function Su(e){e=cu(e);const t=xu(e),n=Ru(e,t.state,t.location,t.replace);function s(i,o=!0){o||n.pauseListeners(),history.go(i)}const r=W({location:"",base:e,go:s,createHref:fu.bind(null,e)},t,n);return Object.defineProperty(r,"location",{enumerable:!0,get:()=>t.location.value}),Object.defineProperty(r,"state",{enumerable:!0,get:()=>t.state.value}),r}let Rt=function(e){return e[e.Static=0]="Static",e[e.Param=1]="Param",e[e.Group=2]="Group",e}({});var oe=function(e){return e[e.Static=0]="Static",e[e.Param=1]="Param",e[e.ParamRegExp=2]="ParamRegExp",e[e.ParamRegExpEnd=3]="ParamRegExpEnd",e[e.EscapeNext=4]="EscapeNext",e}(oe||{});const wu={type:Rt.Static,value:""},Cu=/[a-zA-Z0-9_]/;function Ou(e){if(!e)return[[]];if(e==="/")return[[wu]];if(!e.startsWith("/"))throw new Error(`Invalid path "${e}"`);function t(m){throw new Error(`ERR (${n})/"${d}": ${m}`)}let n=oe.Static,s=n;const r=[];let i;function o(){i&&r.push(i),i=[]}let l=0,c,d="",f="";function h(){d&&(n===oe.Static?i.push({type:Rt.Static,value:d}):n===oe.Param||n===oe.ParamRegExp||n===oe.ParamRegExpEnd?(i.length>1&&(c==="*"||c==="+")&&t(`A repeatable param (${d}) must be alone in its segment. eg: '/:ids+.`),i.push({type:Rt.Param,value:d,regexp:f,repeatable:c==="*"||c==="+",optional:c==="*"||c==="?"})):t("Invalid state to consume buffer"),d="")}function g(){d+=c}for(;lt.length?t.length===1&&t[0]===ge.Static+ge.Segment?1:-1:0}function uo(e,t){let n=0;const s=e.score,r=t.score;for(;n0&&t[t.length-1]<0}const Du={strict:!1,end:!0,sensitive:!1};function Mu(e,t,n){const s=Iu(Ou(e.path),n),r=W(s,{record:e,parent:t,children:[],alias:[]});return t&&!r.record.aliasOf==!t.record.aliasOf&&t.children.push(r),r}function Lu(e,t){const n=[],s=new Map;t=wr(Du,t);function r(h){return s.get(h)}function i(h,g,m){const I=!m,S=Vr(h);S.aliasOf=m&&m.record;const H=wr(t,h),F=[S];if("alias"in h){const T=typeof h.alias=="string"?[h.alias]:h.alias;for(const Q of T)F.push(Vr(W({},S,{components:m?m.record.components:S.components,path:Q,aliasOf:m?m.record:S})))}let O,N;for(const T of F){const{path:Q}=T;if(g&&Q[0]!=="/"){const ie=g.record.path,ee=ie[ie.length-1]==="/"?"":"/";T.path=g.record.path+(Q&&ee+Q)}if(O=Mu(T,g,H),m?m.alias.push(O):(N=N||O,N!==O&&N.alias.push(O),I&&h.name&&!Fr(O)&&o(h.name)),fo(O)&&c(O),S.children){const ie=S.children;for(let ee=0;ee{o(N)}:tn}function o(h){if(oo(h)){const g=s.get(h);g&&(s.delete(h),n.splice(n.indexOf(g),1),g.children.forEach(o),g.alias.forEach(o))}else{const g=n.indexOf(h);g>-1&&(n.splice(g,1),h.record.name&&s.delete(h.record.name),h.children.forEach(o),h.alias.forEach(o))}}function l(){return n}function c(h){const g=ju(h,n);n.splice(g,0,h),h.record.name&&!Fr(h)&&s.set(h.record.name,h)}function d(h,g){let m,I={},S,H;if("name"in h&&h.name){if(m=s.get(h.name),!m)throw Ut(te.MATCHER_NOT_FOUND,{location:h});H=m.record.name,I=W(Lr(g.params,m.keys.filter(N=>!N.optional).concat(m.parent?m.parent.keys.filter(N=>N.optional):[]).map(N=>N.name)),h.params&&Lr(h.params,m.keys.map(N=>N.name))),S=m.stringify(I)}else if(h.path!=null)S=h.path,m=n.find(N=>N.re.test(S)),m&&(I=m.parse(S),H=m.record.name);else{if(m=g.name?s.get(g.name):n.find(N=>N.re.test(g.path)),!m)throw Ut(te.MATCHER_NOT_FOUND,{location:h,currentLocation:g});H=m.record.name,I=W({},g.params,h.params),S=m.stringify(I)}const F=[];let O=m;for(;O;)F.unshift(O.record),O=O.parent;return{name:H,path:S,params:I,matched:F,meta:Fu(F)}}e.forEach(h=>i(h));function f(){n.length=0,s.clear()}return{addRoute:i,resolve:d,removeRoute:o,clearRoutes:f,getRoutes:l,getRecordMatcher:r}}function Lr(e,t){const n={};for(const s of t)s in e&&(n[s]=e[s]);return n}function Vr(e){const t={path:e.path,redirect:e.redirect,name:e.name,meta:e.meta||{},aliasOf:e.aliasOf,beforeEnter:e.beforeEnter,props:Vu(e),children:e.children||[],instances:{},leaveGuards:new Set,updateGuards:new Set,enterCallbacks:{},components:"components"in e?e.components||null:e.component&&{default:e.component}};return Object.defineProperty(t,"mods",{value:{}}),t}function Vu(e){const t={},n=e.props||!1;if("component"in e)t.default=n;else for(const s in e.components)t[s]=typeof n=="object"?n[s]:n;return t}function Fr(e){for(;e;){if(e.record.aliasOf)return!0;e=e.parent}return!1}function Fu(e){return e.reduce((t,n)=>W(t,n.meta),{})}function ju(e,t){let n=0,s=t.length;for(;n!==s;){const i=n+s>>1;uo(e,t[i])<0?s=i:n=i+1}const r=Hu(e);return r&&(s=t.lastIndexOf(r,s-1)),s}function Hu(e){let t=e;for(;t=t.parent;)if(fo(t)&&uo(e,t)===0)return t}function fo({record:e}){return!!(e.name||e.components&&Object.keys(e.components).length||e.redirect)}function jr(e){const t=Pe(qn),n=Pe(Gs),s=Ce(()=>{const c=Mt(e.to);return t.resolve(c)}),r=Ce(()=>{const{matched:c}=s.value,{length:d}=c,f=c[d-1],h=n.matched;if(!f||!h.length)return-1;const g=h.findIndex(Bt.bind(null,f));if(g>-1)return g;const m=Hr(c[d-2]);return d>1&&Hr(f)===m&&h[h.length-1].path!==m?h.findIndex(Bt.bind(null,c[d-2])):g}),i=Ce(()=>r.value>-1&&Wu(n.params,s.value.params)),o=Ce(()=>r.value>-1&&r.value===n.matched.length-1&&io(n.params,s.value.params));function l(c={}){if(Gu(c)){const d=t[Mt(e.replace)?"replace":"push"](Mt(e.to)).catch(tn);return e.viewTransition&&typeof document<"u"&&"startViewTransition"in document&&document.startViewTransition(()=>d),d}return Promise.resolve()}return{route:s,href:Ce(()=>s.value.href),isActive:i,isExactActive:o,navigate:l}}function Bu(e){return e.length===1?e[0]:e}const Uu=Ei({name:"RouterLink",compatConfig:{MODE:3},props:{to:{type:[String,Object],required:!0},replace:Boolean,activeClass:String,exactActiveClass:String,custom:Boolean,ariaCurrentValue:{type:String,default:"page"},viewTransition:Boolean},useLink:jr,setup(e,{slots:t}){const n=jn(jr(e)),{options:s}=Pe(qn),r=Ce(()=>({[Br(e.activeClass,s.linkActiveClass,"router-link-active")]:n.isActive,[Br(e.exactActiveClass,s.linkExactActiveClass,"router-link-exact-active")]:n.isExactActive}));return()=>{const i=t.default&&Bu(t.default(n));return e.custom?i:Qi("a",{"aria-current":n.isExactActive?e.ariaCurrentValue:null,href:n.href,onClick:n.navigate,class:r.value},i)}}}),Ku=Uu;function Gu(e){if(!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)&&!e.defaultPrevented&&!(e.button!==void 0&&e.button!==0)){if(e.currentTarget&&e.currentTarget.getAttribute){const t=e.currentTarget.getAttribute("target");if(/\b_blank\b/i.test(t))return}return e.preventDefault&&e.preventDefault(),!0}}function Wu(e,t){for(const n in t){const s=t[n],r=e[n];if(typeof s=="string"){if(s!==r)return!1}else if(!De(r)||r.length!==s.length||s.some((i,o)=>i.valueOf()!==r[o].valueOf()))return!1}return!0}function Hr(e){return e?e.aliasOf?e.aliasOf.path:e.path:""}const Br=(e,t,n)=>e??t??n,ku=Ei({name:"RouterView",inheritAttrs:!1,props:{name:{type:String,default:"default"},route:Object},compatConfig:{MODE:3},setup(e,{attrs:t,slots:n}){const s=Pe(As),r=Ce(()=>e.route||s.value),i=Pe(Ir,0),o=Ce(()=>{let d=Mt(i);const{matched:f}=r.value;let h;for(;(h=f[d])&&!h.components;)d++;return d}),l=Ce(()=>r.value.matched[o.value]);vn(Ir,Ce(()=>o.value+1)),vn(bu,l),vn(As,r);const c=qo();return bn(()=>[c.value,l.value,e.name],([d,f,h],[g,m,I])=>{f&&(f.instances[h]=d,m&&m!==f&&d&&d===g&&(f.leaveGuards.size||(f.leaveGuards=m.leaveGuards),f.updateGuards.size||(f.updateGuards=m.updateGuards))),d&&f&&(!m||!Bt(f,m)||!g)&&(f.enterCallbacks[h]||[]).forEach(S=>S(d))},{flush:"post"}),()=>{const d=r.value,f=e.name,h=l.value,g=h&&h.components[f];if(!g)return Ur(n.default,{Component:g,route:d});const m=h.props[f],I=m?m===!0?d.params:typeof m=="function"?m(d):m:null,H=Qi(g,W({},I,t,{onVnodeUnmounted:F=>{F.component.isUnmounted&&(h.instances[f]=null)},ref:c}));return Ur(n.default,{Component:H,route:d})||H}}});function Ur(e,t){if(!e)return null;const n=e(t);return n.length===1?n[0]:n}const qu=ku;function $u(e){const t=Lu(e.routes,e),n=e.parseQuery||yu,s=e.stringifyQuery||Tr,r=e.history,i=qt(),o=qt(),l=qt(),c=$o(dt);let d=dt;Nt&&e.scrollBehavior&&"scrollRestoration"in history&&(history.scrollRestoration="manual");const f=is.bind(null,v=>""+v),h=is.bind(null,tu),g=is.bind(null,un);function m(v,P){let w,D;return oo(v)?(w=t.getRecordMatcher(v),D=P):D=v,t.addRoute(D,w)}function I(v){const P=t.getRecordMatcher(v);P&&t.removeRoute(P)}function S(){return t.getRoutes().map(v=>v.record)}function H(v){return!!t.getRecordMatcher(v)}function F(v,P){if(P=W({},P||c.value),typeof v=="string"){const p=os(n,v,P.path),y=t.resolve({path:p.path},P),b=r.createHref(p.fullPath);return W(p,y,{params:g(y.params),hash:un(p.hash),redirectedFrom:void 0,href:b})}let w;if(v.path!=null)w=W({},v,{path:os(n,v.path,P.path).path});else{const p=W({},v.params);for(const y in p)p[y]==null&&delete p[y];w=W({},v,{params:h(p)}),P.params=h(P.params)}const D=t.resolve(w,P),U=v.hash||"";D.params=f(g(D.params));const u=ru(s,W({},v,{hash:Xc(U),path:D.path})),a=r.createHref(u);return W({fullPath:u,hash:U,query:s===Tr?vu(v.query):v.query||{}},D,{redirectedFrom:void 0,href:a})}function O(v){return typeof v=="string"?os(n,v,c.value.path):W({},v)}function N(v,P){if(d!==v)return Ut(te.NAVIGATION_CANCELLED,{from:P,to:v})}function T(v){return ee(v)}function Q(v){return T(W(O(v),{replace:!0}))}function ie(v,P){const w=v.matched[v.matched.length-1];if(w&&w.redirect){const{redirect:D}=w;let U=typeof D=="function"?D(v,P):D;return typeof U=="string"&&(U=U.includes("?")||U.includes("#")?U=O(U):{path:U},U.params={}),W({query:v.query,hash:v.hash,params:U.path!=null?{}:v.params},U)}}function ee(v,P){const w=d=F(v),D=c.value,U=v.state,u=v.force,a=v.replace===!0,p=ie(w,D);if(p)return ee(W(O(p),{state:typeof p=="object"?W({},U,p.state):U,force:u,replace:a}),P||w);const y=w;y.redirectedFrom=P;let b;return!u&&iu(s,D,w)&&(b=Ut(te.NAVIGATION_DUPLICATED,{to:y,from:D}),Fe(D,D,!0,!1)),(b?Promise.resolve(b):Le(y,D)).catch(_=>Qe(_)?Qe(_,te.NAVIGATION_GUARD_REDIRECT)?_:at(_):G(_,y,D)).then(_=>{if(_){if(Qe(_,te.NAVIGATION_GUARD_REDIRECT))return ee(W({replace:a},O(_.to),{state:typeof _.to=="object"?W({},U,_.to.state):U,force:u}),P||y)}else _=yt(y,D,!0,a,U);return ft(y,D,_),_})}function Me(v,P){const w=N(v,P);return w?Promise.reject(w):Promise.resolve()}function ut(v){const P=Pt.values().next().value;return P&&typeof P.runWithContext=="function"?P.runWithContext(v):v()}function Le(v,P){let w;const[D,U,u]=Eu(v,P);w=cs(D.reverse(),"beforeRouteLeave",v,P);for(const p of D)p.leaveGuards.forEach(y=>{w.push(pt(y,v,P))});const a=Me.bind(null,v,P);return w.push(a),Se(w).then(()=>{w=[];for(const p of i.list())w.push(pt(p,v,P));return w.push(a),Se(w)}).then(()=>{w=cs(U,"beforeRouteUpdate",v,P);for(const p of U)p.updateGuards.forEach(y=>{w.push(pt(y,v,P))});return w.push(a),Se(w)}).then(()=>{w=[];for(const p of u)if(p.beforeEnter)if(De(p.beforeEnter))for(const y of p.beforeEnter)w.push(pt(y,v,P));else w.push(pt(p.beforeEnter,v,P));return w.push(a),Se(w)}).then(()=>(v.matched.forEach(p=>p.enterCallbacks={}),w=cs(u,"beforeRouteEnter",v,P,ut),w.push(a),Se(w))).then(()=>{w=[];for(const p of o.list())w.push(pt(p,v,P));return w.push(a),Se(w)}).catch(p=>Qe(p,te.NAVIGATION_CANCELLED)?p:Promise.reject(p))}function ft(v,P,w){l.list().forEach(D=>ut(()=>D(v,P,w)))}function yt(v,P,w,D,U){const u=N(v,P);if(u)return u;const a=P===dt,p=Nt?history.state:{};w&&(D||a?r.replace(v.fullPath,W({scroll:a&&p&&p.scroll},U)):r.push(v.fullPath,U)),c.value=v,Fe(v,P,w,a),at()}let Ve;function Kt(){Ve||(Ve=r.listen((v,P,w)=>{if(!vt.listening)return;const D=F(v),U=ie(D,vt.currentRoute.value);if(U){ee(W(U,{replace:!0,force:!0}),D).catch(tn);return}d=D;const u=c.value;Nt&&hu(Pr(u.fullPath,w.delta),kn()),Le(D,u).catch(a=>Qe(a,te.NAVIGATION_ABORTED|te.NAVIGATION_CANCELLED)?a:Qe(a,te.NAVIGATION_GUARD_REDIRECT)?(ee(W(O(a.to),{force:!0}),D).then(p=>{Qe(p,te.NAVIGATION_ABORTED|te.NAVIGATION_DUPLICATED)&&!w.delta&&w.type===bs.pop&&r.go(-1,!1)}).catch(tn),Promise.reject()):(w.delta&&r.go(-w.delta,!1),G(a,D,u))).then(a=>{a=a||yt(D,u,!1),a&&(w.delta&&!Qe(a,te.NAVIGATION_CANCELLED)?r.go(-w.delta,!1):w.type===bs.pop&&Qe(a,te.NAVIGATION_ABORTED|te.NAVIGATION_DUPLICATED)&&r.go(-1,!1)),ft(D,u,a)}).catch(tn)}))}let Ct=qt(),le=qt(),z;function G(v,P,w){at(v);const D=le.list();return D.length?D.forEach(U=>U(v,P,w)):console.error(v),Promise.reject(v)}function ze(){return z&&c.value!==dt?Promise.resolve():new Promise((v,P)=>{Ct.add([v,P])})}function at(v){return z||(z=!v,Kt(),Ct.list().forEach(([P,w])=>v?w(v):P()),Ct.reset()),v}function Fe(v,P,w,D){const{scrollBehavior:U}=e;if(!Nt||!U)return Promise.resolve();const u=!w&&pu(Pr(v.fullPath,0))||(D||!w)&&history.state&&history.state.scroll||null;return Vs().then(()=>U(v,P,u)).then(a=>a&&du(a)).catch(a=>G(a,v,P))}const ye=v=>r.go(v);let Ot;const Pt=new Set,vt={currentRoute:c,listening:!0,addRoute:m,removeRoute:I,clearRoutes:t.clearRoutes,hasRoute:H,getRoutes:S,resolve:F,options:e,push:T,replace:Q,go:ye,back:()=>ye(-1),forward:()=>ye(1),beforeEach:i.add,beforeResolve:o.add,afterEach:l.add,onError:le.add,isReady:ze,install(v){v.component("RouterLink",Ku),v.component("RouterView",qu),v.config.globalProperties.$router=vt,Object.defineProperty(v.config.globalProperties,"$route",{enumerable:!0,get:()=>Mt(c)}),Nt&&!Ot&&c.value===dt&&(Ot=!0,T(r.location).catch(D=>{}));const P={};for(const D in dt)Object.defineProperty(P,D,{get:()=>c.value[D],enumerable:!0});v.provide(qn,vt),v.provide(Gs,fi(P)),v.provide(As,c);const w=v.unmount;Pt.add(v),v.unmount=function(){Pt.delete(v),Pt.size<1&&(d=dt,Ve&&Ve(),Ve=null,c.value=dt,Ot=!1,z=!1),w()}}};function Se(v){return v.reduce((P,w)=>P.then(()=>ut(w)),Promise.resolve())}return vt}function ff(){return Pe(qn)}function af(e){return Pe(Gs)}const zu=(e,t)=>{const n=e.__vccOpts||e;for(const[s,r]of t)n[s]=r;return n},Ju={};function Qu(e,t){const n=Al("router-view");return Ki(),ki(n)}const Yu=zu(Ju,[["render",Qu]]),Xu=[{path:"/",name:"home",component:()=>re(()=>import("./Home-DWL4BmBg.js"),__vite__mapDeps([0,1]))},{path:"/subject-portal",name:"subjectPortal",component:()=>re(()=>import("./SubjectPortal-C7-krIg3.js"),__vite__mapDeps([2,3,4]))},{path:"/questions",name:"questionList",component:()=>re(()=>import("./QuestionList-cbNQnKl-.js"),__vite__mapDeps([5,3,6,7,8]))},{path:"/questions/add",name:"questionAdd",component:()=>re(()=>import("./QuestionAdd-MfLONVTZ.js"),[])},{path:"/questions/import",name:"fileImport",component:()=>re(()=>import("./FileImport-BF8-5MOQ.js"),__vite__mapDeps([9,3,10]))},{path:"/questions/import/preview",name:"importPreview",component:()=>re(()=>import("./ImportPreview-CFWTYbgQ.js"),__vite__mapDeps([11,3,6,7,12]))},{path:"/questions/:id(\\d+)",name:"questionDetail",component:()=>re(()=>import("./QuestionDetail-s29u4nvx.js"),__vite__mapDeps([13,3,6,7,14]))},{path:"/papers",name:"papersList",component:()=>re(()=>import("./PapersList-CRTR-6gM.js"),__vite__mapDeps([15,3,16]))},{path:"/papers/create-by-id",name:"papersCreateById",component:()=>re(()=>import("./PapersCreateById-CT-UILr2.js"),__vite__mapDeps([17,3]))},{path:"/papers/preview",name:"papersPreview",component:()=>re(()=>import("./PapersPreview-ffjysDLy.js"),__vite__mapDeps([18,3,6,7]))},{path:"/papers/:id(\\d+)/detail",name:"papersDetail",component:()=>re(()=>import("./PapersDetail-CpYT1ySO.js"),__vite__mapDeps([19,3]))},{path:"/papers/:id(\\d+)/result",name:"papersResult",component:()=>re(()=>import("./PapersResult-BN_2zJ7H.js"),__vite__mapDeps([20,3]))},{path:"/analysis/chapters",name:"chapters",component:()=>re(()=>import("./AnalysisChapters-rXnOtC4r.js"),__vite__mapDeps([21,3,22]))},{path:"/analysis/knowledge",name:"knowledge",component:()=>re(()=>import("./AnalysisKnowledge-D1yJ4mIN.js"),__vite__mapDeps([23,3,24]))},{path:"/analysis/pending",name:"pending",component:()=>re(()=>import("./AnalysisPending-BfBXB5e_.js"),__vite__mapDeps([25,3,26]))},{path:"/settings",name:"settings",component:()=>re(()=>import("./Settings-CzRRQaVw.js"),__vite__mapDeps([27,28]))},{path:"/settings/subjects",name:"settingsSubjects",component:()=>re(()=>import("./SettingsSubjects-BOWGx4MO.js"),__vite__mapDeps([29,3,30]))},{path:"/settings/error-causes",name:"settingsErrorCauses",component:()=>re(()=>import("./SettingsErrorCauses-2WPrQUsX.js"),__vite__mapDeps([31,3,32]))},{path:"/settings/question-statuses",name:"settingsStatuses",component:()=>re(()=>import("./SettingsStatuses-C1SsLo53.js"),__vite__mapDeps([33,3,34]))},{path:"/settings/default-params",name:"settingsDefaults",component:()=>re(()=>import("./SettingsDefaultParams-Bwa8C6ww.js"),__vite__mapDeps([35,3]))}],Zu=$u({history:Su("/StudyBase/"),routes:Xu});Hc(Yu).use(Zu).mount("#app");export{Xe as F,zu as _,$i as a,hl as b,nf as c,qo as d,of as e,Zl as f,rf as g,lf as h,sf as i,af as j,jn as k,ff as l,Re as m,Cs as n,Ki as o,ws as p,Vs as q,tf as r,uf as s,So as t,Mt as u,cf as v,ef as w,Ce as x,bn as y}; diff --git a/frontend/dist/index.html b/frontend/dist/index.html new file mode 100644 index 0000000..6f76fbd --- /dev/null +++ b/frontend/dist/index.html @@ -0,0 +1,14 @@ + + + + + + + Base + + + + +
+ + \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..24e9a88 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Base + + +
+ + + \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..064e891 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,1242 @@ +{ + "name": "studybase-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "studybase-frontend", + "version": "1.0.0", + "dependencies": { + "vue": "^3.4.0", + "vue-router": "^4.3.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.0", + "vite": "^5.2.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "http://mirrors.tencent.com/npm/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "http://mirrors.tencent.com/npm/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "http://mirrors.tencent.com/npm/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "http://mirrors.tencent.com/npm/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "http://mirrors.tencent.com/npm/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "http://mirrors.tencent.com/npm/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "http://mirrors.tencent.com/npm/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "http://mirrors.tencent.com/npm/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "http://mirrors.tencent.com/npm/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "http://mirrors.tencent.com/npm/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "http://mirrors.tencent.com/npm/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "http://mirrors.tencent.com/npm/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "http://mirrors.tencent.com/npm/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "http://mirrors.tencent.com/npm/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "http://mirrors.tencent.com/npm/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "http://mirrors.tencent.com/npm/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "http://mirrors.tencent.com/npm/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "http://mirrors.tencent.com/npm/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "http://mirrors.tencent.com/npm/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "http://mirrors.tencent.com/npm/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "http://mirrors.tencent.com/npm/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "http://mirrors.tencent.com/npm/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "http://mirrors.tencent.com/npm/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "http://mirrors.tencent.com/npm/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "http://mirrors.tencent.com/npm/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "http://mirrors.tencent.com/npm/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "http://mirrors.tencent.com/npm/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "http://mirrors.tencent.com/npm/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "license": "MIT" + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "http://mirrors.tencent.com/npm/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.1", + "resolved": "http://mirrors.tencent.com/npm/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz", + "integrity": "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.1", + "resolved": "http://mirrors.tencent.com/npm/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz", + "integrity": "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.1", + "resolved": "http://mirrors.tencent.com/npm/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz", + "integrity": "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.1", + "resolved": "http://mirrors.tencent.com/npm/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz", + "integrity": "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.1", + "resolved": "http://mirrors.tencent.com/npm/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz", + "integrity": "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.1", + "resolved": "http://mirrors.tencent.com/npm/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz", + "integrity": "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.1", + "resolved": "http://mirrors.tencent.com/npm/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz", + "integrity": "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.1", + "resolved": "http://mirrors.tencent.com/npm/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz", + "integrity": "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.1", + "resolved": "http://mirrors.tencent.com/npm/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz", + "integrity": "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.1", + "resolved": "http://mirrors.tencent.com/npm/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz", + "integrity": "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.1", + "resolved": "http://mirrors.tencent.com/npm/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz", + "integrity": "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.1", + "resolved": "http://mirrors.tencent.com/npm/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz", + "integrity": "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.1", + "resolved": "http://mirrors.tencent.com/npm/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz", + "integrity": "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.1", + "resolved": "http://mirrors.tencent.com/npm/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz", + "integrity": "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.1", + "resolved": "http://mirrors.tencent.com/npm/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz", + "integrity": "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.1", + "resolved": "http://mirrors.tencent.com/npm/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz", + "integrity": "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.1", + "resolved": "http://mirrors.tencent.com/npm/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz", + "integrity": "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.1", + "resolved": "http://mirrors.tencent.com/npm/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz", + "integrity": "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.1", + "resolved": "http://mirrors.tencent.com/npm/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz", + "integrity": "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.1", + "resolved": "http://mirrors.tencent.com/npm/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz", + "integrity": "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.1", + "resolved": "http://mirrors.tencent.com/npm/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz", + "integrity": "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.1", + "resolved": "http://mirrors.tencent.com/npm/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz", + "integrity": "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.1", + "resolved": "http://mirrors.tencent.com/npm/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz", + "integrity": "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.1", + "resolved": "http://mirrors.tencent.com/npm/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz", + "integrity": "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.1", + "resolved": "http://mirrors.tencent.com/npm/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz", + "integrity": "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "http://mirrors.tencent.com/npm/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "http://mirrors.tencent.com/npm/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.42", + "resolved": "http://mirrors.tencent.com/npm/@vue/compiler-core/-/compiler-core-3.5.42.tgz", + "integrity": "sha512-2Ye1ilMtKXxl8qZUrQ5j0CdgenFp/HFQmta6rfRyfEsTG69L6Wk+tWuNoHYHMx9E8tF2Slvdg1FuwDvAXdy1LQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/shared": "3.5.42", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.42", + "resolved": "http://mirrors.tencent.com/npm/@vue/compiler-dom/-/compiler-dom-3.5.42.tgz", + "integrity": "sha512-qbhQZEFmycr+ni/qyuccS4sucNN7VAbDfbkvNxWOX2VfgFm90MNs3/UhRNKoPMEIVn0F8gdlYjLPvqxHwHeQOA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.42", + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.42", + "resolved": "http://mirrors.tencent.com/npm/@vue/compiler-sfc/-/compiler-sfc-3.5.42.tgz", + "integrity": "sha512-fkCAFB4okcAANGMThboWnScp/gzWjU0ZSkVnjTIiplmMDq2uq0tIB3j+xVu4rhv5rvOgBySCysudmbMd6xRRqw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/compiler-core": "3.5.42", + "@vue/compiler-dom": "3.5.42", + "@vue/compiler-ssr": "3.5.42", + "@vue/shared": "3.5.42", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.19", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.42", + "resolved": "http://mirrors.tencent.com/npm/@vue/compiler-ssr/-/compiler-ssr-3.5.42.tgz", + "integrity": "sha512-xmLk3wLkbizPAiLyomjgFFosf2ys9b5Ghb+oh/k2tnvipNz8OFrQOiTcWCzyK7MpBp9KkyGtfvgfLUivbmuGYA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.42", + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "http://mirrors.tencent.com/npm/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.42", + "resolved": "http://mirrors.tencent.com/npm/@vue/reactivity/-/reactivity-3.5.42.tgz", + "integrity": "sha512-TzNNfKpb7hDxbQltwAut8VDQA5YP+BuRlxntHUuRjyKwlMvmAPbs3unhCvieijifY6vFfVBwsS7wG/C7uq+bEQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.42", + "resolved": "http://mirrors.tencent.com/npm/@vue/runtime-core/-/runtime-core-3.5.42.tgz", + "integrity": "sha512-9uACtuHs7vJGkm5Bp3xu4xRDLFTIYy5DgxpToVjqGIAhAEKwQfsaLvKINhM6nFVp6bZPRFGdDqd1g52MqKsotA==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.42", + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.42", + "resolved": "http://mirrors.tencent.com/npm/@vue/runtime-dom/-/runtime-dom-3.5.42.tgz", + "integrity": "sha512-rsCmhiWLaRxGltLwhlCWyYkFn7WAbKRh0q17eZ1A6Dq6eqc2ACQ61IIryxz0LrsvCzHSilLA9JHovVwM8CNE2g==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.42", + "@vue/runtime-core": "3.5.42", + "@vue/shared": "3.5.42", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.42", + "resolved": "http://mirrors.tencent.com/npm/@vue/server-renderer/-/server-renderer-3.5.42.tgz", + "integrity": "sha512-2++5dUyYS4gvo7xQXSECUDhB7TS0aOl5SeVfC5qSq1Jgfhjvegw1zqhwTIR3imZ+QYPJQw9gfcFvXGAjGZ7ajQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.42", + "@vue/runtime-dom": "3.5.42", + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.42", + "resolved": "http://mirrors.tencent.com/npm/@vue/shared/-/shared-3.5.42.tgz", + "integrity": "sha512-2rPxex1jQf4jvl9MOHl6YaXCPcrNqz/FstMOEh3QWY+/OME9nQTvl9WYeCwhW7AFjaR0SnngZGlp/wkR6rkI6g==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "http://mirrors.tencent.com/npm/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "http://mirrors.tencent.com/npm/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "http://mirrors.tencent.com/npm/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "http://mirrors.tencent.com/npm/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "http://mirrors.tencent.com/npm/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "http://mirrors.tencent.com/npm/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "http://mirrors.tencent.com/npm/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "http://mirrors.tencent.com/npm/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "http://mirrors.tencent.com/npm/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.63.1", + "resolved": "http://mirrors.tencent.com/npm/rollup/-/rollup-4.63.1.tgz", + "integrity": "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.1", + "@rollup/rollup-android-arm64": "4.63.1", + "@rollup/rollup-darwin-arm64": "4.63.1", + "@rollup/rollup-darwin-x64": "4.63.1", + "@rollup/rollup-freebsd-arm64": "4.63.1", + "@rollup/rollup-freebsd-x64": "4.63.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", + "@rollup/rollup-linux-arm-musleabihf": "4.63.1", + "@rollup/rollup-linux-arm64-gnu": "4.63.1", + "@rollup/rollup-linux-arm64-musl": "4.63.1", + "@rollup/rollup-linux-loong64-gnu": "4.63.1", + "@rollup/rollup-linux-loong64-musl": "4.63.1", + "@rollup/rollup-linux-ppc64-gnu": "4.63.1", + "@rollup/rollup-linux-ppc64-musl": "4.63.1", + "@rollup/rollup-linux-riscv64-gnu": "4.63.1", + "@rollup/rollup-linux-riscv64-musl": "4.63.1", + "@rollup/rollup-linux-s390x-gnu": "4.63.1", + "@rollup/rollup-linux-x64-gnu": "4.63.1", + "@rollup/rollup-linux-x64-musl": "4.63.1", + "@rollup/rollup-openbsd-x64": "4.63.1", + "@rollup/rollup-openharmony-arm64": "4.63.1", + "@rollup/rollup-win32-arm64-msvc": "4.63.1", + "@rollup/rollup-win32-ia32-msvc": "4.63.1", + "@rollup/rollup-win32-x64-gnu": "4.63.1", + "@rollup/rollup-win32-x64-msvc": "4.63.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "http://mirrors.tencent.com/npm/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "http://mirrors.tencent.com/npm/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.42", + "resolved": "http://mirrors.tencent.com/npm/vue/-/vue-3.5.42.tgz", + "integrity": "sha512-4RyHQTbQvOPs3MfvUO1Sg0YRrKNnA0mAVtvpd12Tg1fKDN7OHBUl1IqSn8zGJjK9nI3NkNp8cgTpVrSZC5TTcA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.42", + "@vue/compiler-sfc": "3.5.42", + "@vue/runtime-dom": "3.5.42", + "@vue/server-renderer": "3.5.42", + "@vue/shared": "3.5.42" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "http://mirrors.tencent.com/npm/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..8dea078 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,19 @@ +{ + "name": "studybase-frontend", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "vue": "^3.4.0", + "vue-router": "^4.3.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.0", + "vite": "^5.2.0" + } +} diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..0e92665 --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,138 @@ + + + + + diff --git a/frontend/src/api.js b/frontend/src/api.js new file mode 100644 index 0000000..a71e089 --- /dev/null +++ b/frontend/src/api.js @@ -0,0 +1,85 @@ +// API helpers — all calls go to /StudyBase/api/* (nginx proxies to FastAPI) +const BASE = '/StudyBase/api' + +async function handle(res) { + if (res.status === 422) { + const d = await res.json() + const err = new Error(d.detail || '需要确认') + err.needConfirm = true + throw err + } + if (!res.ok) { + let msg = `HTTP ${res.status}` + try { + const d = await res.json() + msg = d.detail || msg + } catch (e) { /* ignore */ } + throw new Error(msg) + } + return res +} + +export async function apiGet(path) { + const res = await fetch(BASE + path) + return handle(res).then(r => r.json()) +} + +export async function apiPost(path, body) { + const res = await fetch(BASE + path, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body || {}) + }) + return handle(res).then(r => r.json()) +} + +export async function apiPut(path, body) { + const res = await fetch(BASE + path, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body || {}) + }) + return handle(res).then(r => r.json()) +} + +export async function apiDelete(path) { + const res = await fetch(BASE + path, { method: 'DELETE' }) + return handle(res).then(r => r.json()) +} + +// POST returning a file (triggers browser download) +export async function apiDownload(path, body, fallbackName) { + const res = await fetch(BASE + path, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body || {}) + }) + await handle(res) + const blob = await res.blob() + const cd = res.headers.get('Content-Disposition') || '' + let name = fallbackName || 'download.xlsx' + const m = cd.match(/filename\*=UTF-8''([^;]+)/) + if (m) name = decodeURIComponent(m[1]) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = name + document.body.appendChild(a) + a.click() + a.remove() + URL.revokeObjectURL(url) +} + +// simple confirm/prompt/toast shims replacing tkinter messagebox/simpledialog +export function tkConfirm(message) { + return window.confirm(message) +} + +export function tkPrompt(title, prompt, initial) { + const v = window.prompt((title ? title + '\n' : '') + prompt, initial || '') + return v === null ? null : v +} + +export function tkAlert(message) { + window.alert(message) +} diff --git a/frontend/src/components/RichContent.vue b/frontend/src/components/RichContent.vue new file mode 100644 index 0000000..2c0187b --- /dev/null +++ b/frontend/src/components/RichContent.vue @@ -0,0 +1,63 @@ + + + + + diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..b4481bf --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,33 @@ +import { createApp } from 'vue' +import { createRouter, createWebHistory } from 'vue-router' +import App from './App.vue' + +const routes = [ + { path: '/', name: 'home', component: () => import('./views/Home.vue') }, + { path: '/subject-portal', name: 'subjectPortal', component: () => import('./views/SubjectPortal.vue') }, + { path: '/questions', name: 'questionList', component: () => import('./views/QuestionList.vue') }, + { path: '/questions/add', name: 'questionAdd', component: () => import('./views/QuestionAdd.vue') }, + { path: '/questions/import', name: 'fileImport', component: () => import('./views/FileImport.vue') }, + { path: '/questions/import/preview', name: 'importPreview', component: () => import('./views/ImportPreview.vue') }, + { path: '/questions/:id(\\d+)', name: 'questionDetail', component: () => import('./views/QuestionDetail.vue') }, + { path: '/papers', name: 'papersList', component: () => import('./views/PapersList.vue') }, + { path: '/papers/create-by-id', name: 'papersCreateById', component: () => import('./views/PapersCreateById.vue') }, + { path: '/papers/preview', name: 'papersPreview', component: () => import('./views/PapersPreview.vue') }, + { path: '/papers/:id(\\d+)/detail', name: 'papersDetail', component: () => import('./views/PapersDetail.vue') }, + { path: '/papers/:id(\\d+)/result', name: 'papersResult', component: () => import('./views/PapersResult.vue') }, + { path: '/analysis/chapters', name: 'chapters', component: () => import('./views/AnalysisChapters.vue') }, + { path: '/analysis/knowledge', name: 'knowledge', component: () => import('./views/AnalysisKnowledge.vue') }, + { path: '/analysis/pending', name: 'pending', component: () => import('./views/AnalysisPending.vue') }, + { path: '/settings', name: 'settings', component: () => import('./views/Settings.vue') }, + { path: '/settings/subjects', name: 'settingsSubjects', component: () => import('./views/SettingsSubjects.vue') }, + { path: '/settings/error-causes', name: 'settingsErrorCauses', component: () => import('./views/SettingsErrorCauses.vue') }, + { path: '/settings/question-statuses', name: 'settingsStatuses', component: () => import('./views/SettingsStatuses.vue') }, + { path: '/settings/default-params', name: 'settingsDefaults', component: () => import('./views/SettingsDefaultParams.vue') }, +] + +const router = createRouter({ + history: createWebHistory('/StudyBase/'), + routes +}) + +createApp(App).use(router).mount('#app') diff --git a/frontend/src/views/AnalysisChapters.vue b/frontend/src/views/AnalysisChapters.vue new file mode 100644 index 0000000..4dd60e7 --- /dev/null +++ b/frontend/src/views/AnalysisChapters.vue @@ -0,0 +1,132 @@ + + + + + diff --git a/frontend/src/views/AnalysisKnowledge.vue b/frontend/src/views/AnalysisKnowledge.vue new file mode 100644 index 0000000..3467539 --- /dev/null +++ b/frontend/src/views/AnalysisKnowledge.vue @@ -0,0 +1,141 @@ + + + + + diff --git a/frontend/src/views/AnalysisPending.vue b/frontend/src/views/AnalysisPending.vue new file mode 100644 index 0000000..69d098e --- /dev/null +++ b/frontend/src/views/AnalysisPending.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/frontend/src/views/FileImport.vue b/frontend/src/views/FileImport.vue new file mode 100644 index 0000000..2fcbd2d --- /dev/null +++ b/frontend/src/views/FileImport.vue @@ -0,0 +1,417 @@ + + + + + diff --git a/frontend/src/views/Home.vue b/frontend/src/views/Home.vue new file mode 100644 index 0000000..8313138 --- /dev/null +++ b/frontend/src/views/Home.vue @@ -0,0 +1,31 @@ + + + + + diff --git a/frontend/src/views/ImportPreview.vue b/frontend/src/views/ImportPreview.vue new file mode 100644 index 0000000..e6de5c7 --- /dev/null +++ b/frontend/src/views/ImportPreview.vue @@ -0,0 +1,123 @@ + + + + + diff --git a/frontend/src/views/PapersCreateById.vue b/frontend/src/views/PapersCreateById.vue new file mode 100644 index 0000000..89abb0b --- /dev/null +++ b/frontend/src/views/PapersCreateById.vue @@ -0,0 +1,48 @@ + + + diff --git a/frontend/src/views/PapersDetail.vue b/frontend/src/views/PapersDetail.vue new file mode 100644 index 0000000..fc66b40 --- /dev/null +++ b/frontend/src/views/PapersDetail.vue @@ -0,0 +1,27 @@ + + + diff --git a/frontend/src/views/PapersList.vue b/frontend/src/views/PapersList.vue new file mode 100644 index 0000000..a57b325 --- /dev/null +++ b/frontend/src/views/PapersList.vue @@ -0,0 +1,151 @@ + + + + + diff --git a/frontend/src/views/PapersPreview.vue b/frontend/src/views/PapersPreview.vue new file mode 100644 index 0000000..043f397 --- /dev/null +++ b/frontend/src/views/PapersPreview.vue @@ -0,0 +1,153 @@ + + + diff --git a/frontend/src/views/PapersResult.vue b/frontend/src/views/PapersResult.vue new file mode 100644 index 0000000..5396bca --- /dev/null +++ b/frontend/src/views/PapersResult.vue @@ -0,0 +1,113 @@ + + + diff --git a/frontend/src/views/QuestionAdd.vue b/frontend/src/views/QuestionAdd.vue new file mode 100644 index 0000000..32421ac --- /dev/null +++ b/frontend/src/views/QuestionAdd.vue @@ -0,0 +1,23 @@ + + + diff --git a/frontend/src/views/QuestionDetail.vue b/frontend/src/views/QuestionDetail.vue new file mode 100644 index 0000000..68e2573 --- /dev/null +++ b/frontend/src/views/QuestionDetail.vue @@ -0,0 +1,210 @@ + + + + + diff --git a/frontend/src/views/QuestionList.vue b/frontend/src/views/QuestionList.vue new file mode 100644 index 0000000..981c436 --- /dev/null +++ b/frontend/src/views/QuestionList.vue @@ -0,0 +1,340 @@ + + + + + diff --git a/frontend/src/views/Settings.vue b/frontend/src/views/Settings.vue new file mode 100644 index 0000000..96bca15 --- /dev/null +++ b/frontend/src/views/Settings.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/frontend/src/views/SettingsDefaultParams.vue b/frontend/src/views/SettingsDefaultParams.vue new file mode 100644 index 0000000..bcfe8fb --- /dev/null +++ b/frontend/src/views/SettingsDefaultParams.vue @@ -0,0 +1,68 @@ + + + diff --git a/frontend/src/views/SettingsErrorCauses.vue b/frontend/src/views/SettingsErrorCauses.vue new file mode 100644 index 0000000..8c057e6 --- /dev/null +++ b/frontend/src/views/SettingsErrorCauses.vue @@ -0,0 +1,153 @@ + + + + + diff --git a/frontend/src/views/SettingsStatuses.vue b/frontend/src/views/SettingsStatuses.vue new file mode 100644 index 0000000..978cba3 --- /dev/null +++ b/frontend/src/views/SettingsStatuses.vue @@ -0,0 +1,137 @@ + + + + + diff --git a/frontend/src/views/SettingsSubjects.vue b/frontend/src/views/SettingsSubjects.vue new file mode 100644 index 0000000..0300d58 --- /dev/null +++ b/frontend/src/views/SettingsSubjects.vue @@ -0,0 +1,86 @@ + + + + + diff --git a/frontend/src/views/SubjectPortal.vue b/frontend/src/views/SubjectPortal.vue new file mode 100644 index 0000000..586961f --- /dev/null +++ b/frontend/src/views/SubjectPortal.vue @@ -0,0 +1,91 @@ + + + + + diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..ddf2bf7 --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,20 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue()], + base: '/StudyBase/', + server: { + port: 5180, + proxy: { + '/StudyBase/api': { + target: 'http://127.0.0.1:1065', + changeOrigin: true + } + } + }, + build: { + outDir: 'dist', + chunkSizeWarningLimit: 1500 + } +}) diff --git a/generate_report.py b/generate_report.py deleted file mode 100644 index b21b5c4..0000000 --- a/generate_report.py +++ /dev/null @@ -1,141 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import os -import sys -import io - -# ================= 配置区域 ================= -# 输出文件名 -OUTPUT_FILENAME = "project_all_in_one.txt" - -# 需要排除的目录名(完全匹配),例如:['.git','gitea', 'node_modules', 'ZZ Old Version'] -EXCLUDE_DIRS = {".git", "__pycache__", "ZZ Old Version", "Media"} - -# 需要排除的文件后缀(不区分大小写),例如:['.exe', '.pyc', '.png', '.jpg'] -EXCLUDE_EXTS = {".exe", ".pyc", ".png", ".jpg", ".jpeg", ".gif", ".pdf", ".zip", ".tar", ".gz"} - -# 需要排除的具体文件名 -EXCLUDE_FILES = {OUTPUT_FILENAME, ".DS_Store", "gitea"} -# =========================================== - -ROOT_DIR = os.path.abspath(os.path.dirname(__file__)) - -def write_line(f, line=""): - f.write(line + "\n") - -def should_exclude(path): - """检查路径中是否包含任何被排除的目录""" - parts = path.split(os.sep) - for d in EXCLUDE_DIRS: - if d in parts: - return True - return False - -def collect_tree(root): - """生成目录结构树""" - lines = [] - for dirpath, dirnames, filenames in os.walk(root): - # 过滤掉排除目录,修改 dirnames 会影响 os.walk 的后续遍历 - dirnames[:] = [d for d in dirnames if d not in EXCLUDE_DIRS] - - rel = os.path.relpath(dirpath, root) - if rel == ".": - indent_level = 0 - display_path = "." - else: - indent_level = rel.count(os.sep) + 1 - display_path = rel.replace(os.sep, "/") - - indent = " " * indent_level - lines.append(f"{indent}{display_path}/") - - for fn in sorted(filenames): - if fn in EXCLUDE_FILES: - continue - _, ext = os.path.splitext(fn) - if ext.lower() in EXCLUDE_EXTS: - continue - lines.append(f"{indent} {fn}") - return lines - -def gather_all_files(root): - """收集所有符合条件的文件路径""" - matches = [] - for dirpath, dirnames, filenames in os.walk(root): - # 过滤排除目录 - dirnames[:] = [d for d in dirnames if d not in EXCLUDE_DIRS] - - for fn in filenames: - # 过滤排除的文件名和后缀 - if fn in EXCLUDE_FILES: - continue - _, ext = os.path.splitext(fn) - if ext.lower() in EXCLUDE_EXTS: - continue - - full_path = os.path.join(dirpath, fn) - rel_path = os.path.relpath(full_path, root).replace(os.sep, "/") - matches.append((rel_path, full_path)) - - matches.sort() - return matches - -def is_text_file(filepath): - """检查文件是否为文本文件""" - text_extensions = ['.txt', '.py', '.html', '.css', '.js', '.md'] # 可根据需要扩展 - _, ext = os.path.splitext(filepath) - return ext.lower() in text_extensions - -def read_file_content(path): - """尝试多种编码读取文件内容,非文本或无法读取则跳过""" - encodings = ["utf-8-sig", "utf-8", "gbk", "latin-1"] # 优先使用 utf-8-sig - for enc in encodings: - try: - with io.open(path, "r", encoding=enc, errors="strict") as f: - return f.read() - except (UnicodeDecodeError, Exception): - continue - return "[警告:该文件编码不兼容或为二进制格式,已跳过内容提取]" - -def generate_report(root, out_path): - with io.open(out_path, "w", encoding="utf-8-sig") as out: # 使用 utf-8-sig 输出 - # 第一部分:目录结构 - write_line(out, "==================================================") - write_line(out, "第一部分:项目目录结构") - write_line(out, "==================================================") - tree_lines = collect_tree(root) - for ln in tree_lines: - write_line(out, ln) - write_line(out, "\n") - - # 第二部分:文件内容整合 - write_line(out, "==================================================") - write_line(out, "第二部分:文件内容整合") - write_line(out, "==================================================") - - all_files = gather_all_files(root) - if not all_files: - write_line(out, "[未找到符合条件的可处理文件]") - - for rel_path, full_path in all_files: - if is_text_file(full_path): # 仅处理文本文件 - write_line(out, f"\n>>> FILE START: {rel_path} <<<") - write_line(out, "=" * 40) - - content = read_file_content(full_path) - out.write(content) - - if not content.endswith("\n"): - out.write("\n") - write_line(out, "=" * 40) - write_line(out, f">>> FILE END: {rel_path} <<<\n") - -if __name__ == "__main__": - try: - output_full_path = os.path.join(ROOT_DIR, OUTPUT_FILENAME) - generate_report(ROOT_DIR, output_full_path) - print(f"成功!整合后的文件已保存至:{OUTPUT_FILENAME}") - except Exception as e: - print(f"程序运行出错:{e}") - sys.exit(1) diff --git a/project_all_in_one.txt b/project_all_in_one.txt deleted file mode 100644 index 3045718..0000000 --- a/project_all_in_one.txt +++ /dev/null @@ -1,1181 +0,0 @@ -================================================== -第一部分:项目目录结构 -================================================== -./ - .gitignore - app.log - app.py - database.py - generate_report.py - custom/ - custom/conf/ - app.ini - templates/ - index.html - templates/settings/ - index.html - templates/questions/ - index.html - question_list.html - templates/papers/ - index.html - templates/subject/ - chapters.html - index.html - knowledge.html - _clipboard_temp/ - static/ - static/css/ - style.css - s/ - schema.sql - studybase.db - studybase.sqbpro - workmode-studybase.sql - - -================================================== -第二部分:文件内容整合 -================================================== - ->>> FILE START: app.py <<< -======================================== -from flask import Flask, render_template, request, jsonify -from database import query_db - -app = Flask(__name__) - -# ==================== 1. 核心页面路由 ==================== - -@app.route('/') -def index(): - return render_template('index.html') - -# --- 题目列表模块 --- -@app.route('/questions/') -def questions_list(): - return render_template('questions/index.html') - -# --- 试卷列表模块 --- -@app.route('/papers/') -def papers_list(): - return render_template('papers/index.html') - -# --- 系统设定模块 --- -@app.route('/settings/') -def settings_page(): - return render_template('settings/index.html') - -# --- 错题分析模块 (刚才修复的部分) --- -@app.route('/subject/') -def subject_portal(): - subjects = query_db("SELECT ID, Subject as name FROM Subject WHERE enabled = 1") - stages = query_db("SELECT ID, stage_name FROM stage WHERE enabled = 1 ORDER BY ID") - default_id = subjects[0]['ID'] if subjects else None - return render_template('subject/index.html', subjects=subjects, stages=stages, default_id=default_id) - -@app.route('/subject/chapters') -def subject_chapters(): - subjects = query_db("SELECT DISTINCT Subject FROM Subject ORDER BY Subject") - return render_template('subject/chapters.html', subjects=subjects) - -@app.route('/subject/knowledge') -def subject_knowledge(): - subjects = query_db("SELECT DISTINCT Subject FROM Subject ORDER BY Subject") - chapter_id = request.args.get('chapter_id', '') - return render_template('subject/knowledge.html', subjects=subjects, chapter_id=chapter_id) - - -# ==================== 2. API 数据接口 ==================== - -# 科目概览统计 -@app.route('/api/subject_stats') -def get_subject_stats(): - sub_id = request.args.get('subject_id') - stage_ids_raw = request.args.get('stage_ids', '') - stage_ids = [sid for sid in stage_ids_raw.split(',') if sid] - - if not sub_id or not stage_ids: - return jsonify({"wrong": {"total": 0, "fixed": 0, "ratio": "0%", "remained": 0}, - "push": {"total": 0, "fixed": 0, "ratio": "0%", "remained": 0}}) - - placeholders = ','.join(['?'] * len(stage_ids)) - def get_count(q_type, extra_sql=""): - sql = f"SELECT COUNT(*) as c FROM questions WHERE subject_id = ? AND stage_id IN ({placeholders}) AND question_type = ? {extra_sql}" - res = query_db(sql, [sub_id] + stage_ids + [q_type], one=True) - return res['c'] if res else 0 - - try: - w_total = get_count('错题'); w_fixed = get_count('错题', 'AND right_times > 0') - p_total = get_count('推题'); p_fixed = get_count('推题', 'AND right_times > 0') - return jsonify({ - "wrong": {"total": w_total, "fixed": w_fixed, "ratio": f"{(w_fixed/w_total*100):.1f}%" if w_total>0 else "0%", "remained": w_total-w_fixed}, - "push": {"total": p_total, "fixed": p_fixed, "ratio": f"{(p_fixed/p_total*100):.1f}%" if p_total>0 else "0%", "remained": p_total-p_fixed} - }) - except Exception as e: - return jsonify({"error": str(e)}), 500 - -# 章节数据接口 (已适配 chapter_desc) -@app.route('/api/chapters') -def get_chapters(): - subject_name = request.args.get('subject', '全部') - keyword = request.args.get('keyword', '') - params = [] - where = " WHERE 1=1" - if subject_name != '全部': - where += " AND s.Subject = ?"; params.append(subject_name) - if keyword: - where += " AND c.chapter_desc LIKE ?"; params.append(f"%{keyword}%") - - sql = f""" - SELECT c.id AS chapter_id, s.Subject AS SubjectName, c.chapter_desc AS ChapterDesc, - (SELECT COUNT(DISTINCT r.question_id) FROM question_kp_relation r JOIN knowledge_point kp ON r.kp_id = kp.id WHERE kp.chapter_id = c.id) AS total, - (SELECT COUNT(DISTINCT r.question_id) FROM question_kp_relation r JOIN knowledge_point kp ON r.kp_id = kp.id JOIN questions q ON r.question_id = q.id WHERE kp.chapter_id = c.id AND q.right_times > 0) AS correct - FROM chapters c JOIN Subject s ON c.subject_id = s.ID {where} - """ - try: - rows = query_db(sql, params) - result = [dict(row) for row in rows] - for item in result: - item['not_correct'] = (item['total'] or 0) - (item['correct'] or 0) - return jsonify([i for i in result if i['total'] > 0]) - except Exception as e: - return jsonify({"error": str(e)}), 500 - -# 知识点数据接口 -@app.route('/api/knowledge') -def get_knowledge(): - subject_name = request.args.get('subject', '全部') - keyword = request.args.get('keyword', '') - chapter_id = request.args.get('chapter_id', '') - params = [] - where = " WHERE kp.is_enabled = 1" - if chapter_id: - where += " AND kp.chapter_id = ?"; params.append(chapter_id) - if subject_name != '全部': - where += " AND s.Subject = ?"; params.append(subject_name) - if keyword: - where += " AND kp.content LIKE ?"; params.append(f"%{keyword}%") - - sql = f""" - SELECT kp.id, s.Subject as subject_name, kp.content, - (SELECT COUNT(*) FROM question_kp_relation r WHERE r.kp_id = kp.id) as total, - (SELECT COUNT(DISTINCT r.question_id) FROM question_kp_relation r JOIN questions q ON r.question_id = q.id WHERE r.kp_id = kp.id AND q.right_times >= 1) as correct - FROM knowledge_point kp JOIN Subject s ON kp.subject_id = s.ID {where} - """ - try: - rows = query_db(sql, params) - result = [dict(row) for row in rows] - for item in result: - item['not_correct'] = (item['total'] or 0) - (item['correct'] or 0) - return jsonify(result) - except Exception as e: - return jsonify({"error": str(e)}), 500 - -if __name__ == '__main__': - app.run(host='0.0.0.0', port=5000, debug=True) -======================================== ->>> FILE END: app.py <<< - - ->>> FILE START: database.py <<< -======================================== -import sqlite3 -import os - -# 数据库文件的绝对路径 -DB_PATH = os.path.join(os.path.dirname(__file__), 's', 'studybase.db') - -def get_db_connection(): - """获取数据库连接,并将结果集设置为字典模式(方便前端调用)""" - conn = sqlite3.connect(DB_PATH) - # 这行代码能让你通过 row['column_name'] 访问数据,而不是 row[0] - conn.row_factory = sqlite3.Row - return conn - -def query_db(query, args=(), one=False): - """通用的查询函数""" - conn = get_db_connection() - cur = conn.execute(query, args) - rv = cur.fetchall() - conn.close() - return (rv[0] if rv else None) if one else rv - -def execute_db(query, args=()): - """通用的增删改函数""" - conn = get_db_connection() - cur = conn.execute(query, args) - conn.commit() - conn.close() - return cur -======================================== ->>> FILE END: database.py <<< - - ->>> FILE START: generate_report.py <<< -======================================== -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import os -import sys -import io - -# ================= 配置区域 ================= -# 输出文件名 -OUTPUT_FILENAME = "project_all_in_one.txt" - -# 需要排除的目录名(完全匹配),例如:['.git','gitea', 'node_modules', 'ZZ Old Version'] -EXCLUDE_DIRS = {".git", "__pycache__", "ZZ Old Version", "Media"} - -# 需要排除的文件后缀(不区分大小写),例如:['.exe', '.pyc', '.png', '.jpg'] -EXCLUDE_EXTS = {".exe", ".pyc", ".png", ".jpg", ".jpeg", ".gif", ".pdf", ".zip", ".tar", ".gz"} - -# 需要排除的具体文件名 -EXCLUDE_FILES = {OUTPUT_FILENAME, ".DS_Store", "gitea"} -# =========================================== - -ROOT_DIR = os.path.abspath(os.path.dirname(__file__)) - -def write_line(f, line=""): - f.write(line + "\n") - -def should_exclude(path): - """检查路径中是否包含任何被排除的目录""" - parts = path.split(os.sep) - for d in EXCLUDE_DIRS: - if d in parts: - return True - return False - -def collect_tree(root): - """生成目录结构树""" - lines = [] - for dirpath, dirnames, filenames in os.walk(root): - # 过滤掉排除目录,修改 dirnames 会影响 os.walk 的后续遍历 - dirnames[:] = [d for d in dirnames if d not in EXCLUDE_DIRS] - - rel = os.path.relpath(dirpath, root) - if rel == ".": - indent_level = 0 - display_path = "." - else: - indent_level = rel.count(os.sep) + 1 - display_path = rel.replace(os.sep, "/") - - indent = " " * indent_level - lines.append(f"{indent}{display_path}/") - - for fn in sorted(filenames): - if fn in EXCLUDE_FILES: - continue - _, ext = os.path.splitext(fn) - if ext.lower() in EXCLUDE_EXTS: - continue - lines.append(f"{indent} {fn}") - return lines - -def gather_all_files(root): - """收集所有符合条件的文件路径""" - matches = [] - for dirpath, dirnames, filenames in os.walk(root): - # 过滤排除目录 - dirnames[:] = [d for d in dirnames if d not in EXCLUDE_DIRS] - - for fn in filenames: - # 过滤排除的文件名和后缀 - if fn in EXCLUDE_FILES: - continue - _, ext = os.path.splitext(fn) - if ext.lower() in EXCLUDE_EXTS: - continue - - full_path = os.path.join(dirpath, fn) - rel_path = os.path.relpath(full_path, root).replace(os.sep, "/") - matches.append((rel_path, full_path)) - - matches.sort() - return matches - -def is_text_file(filepath): - """检查文件是否为文本文件""" - text_extensions = ['.txt', '.py', '.html', '.css', '.js', '.md'] # 可根据需要扩展 - _, ext = os.path.splitext(filepath) - return ext.lower() in text_extensions - -def read_file_content(path): - """尝试多种编码读取文件内容,非文本或无法读取则跳过""" - encodings = ["utf-8-sig", "utf-8", "gbk", "latin-1"] # 优先使用 utf-8-sig - for enc in encodings: - try: - with io.open(path, "r", encoding=enc, errors="strict") as f: - return f.read() - except (UnicodeDecodeError, Exception): - continue - return "[警告:该文件编码不兼容或为二进制格式,已跳过内容提取]" - -def generate_report(root, out_path): - with io.open(out_path, "w", encoding="utf-8-sig") as out: # 使用 utf-8-sig 输出 - # 第一部分:目录结构 - write_line(out, "==================================================") - write_line(out, "第一部分:项目目录结构") - write_line(out, "==================================================") - tree_lines = collect_tree(root) - for ln in tree_lines: - write_line(out, ln) - write_line(out, "\n") - - # 第二部分:文件内容整合 - write_line(out, "==================================================") - write_line(out, "第二部分:文件内容整合") - write_line(out, "==================================================") - - all_files = gather_all_files(root) - if not all_files: - write_line(out, "[未找到符合条件的可处理文件]") - - for rel_path, full_path in all_files: - if is_text_file(full_path): # 仅处理文本文件 - write_line(out, f"\n>>> FILE START: {rel_path} <<<") - write_line(out, "=" * 40) - - content = read_file_content(full_path) - out.write(content) - - if not content.endswith("\n"): - out.write("\n") - write_line(out, "=" * 40) - write_line(out, f">>> FILE END: {rel_path} <<<\n") - -if __name__ == "__main__": - try: - output_full_path = os.path.join(ROOT_DIR, OUTPUT_FILENAME) - generate_report(ROOT_DIR, output_full_path) - print(f"成功!整合后的文件已保存至:{OUTPUT_FILENAME}") - except Exception as e: - print(f"程序运行出错:{e}") - sys.exit(1) -======================================== ->>> FILE END: generate_report.py <<< - - ->>> FILE START: static/css/style.css <<< -======================================== -/* 全局基础样式 */ -body { - margin: 0; - padding: 0; - background-color: #f0f2f5; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; - -webkit-font-smoothing: antialiased; -} - -/* 顶部导航栏基础样式 */ -.navbar { - height: 60px; - background: linear-gradient(135deg, #4088d8 0%, #2a69b5 100%); - display: flex; - justify-content: center; - align-items: center; - color: white; - font-size: 22px; - font-weight: bold; - box-shadow: 0 2px 10px rgba(0,0,0,0.1); - position: sticky; - top: 0; - z-index: 1000; -} - -/* 子页面特定的 Header 颜色 - 对应首页按钮色系 */ -.header-subject { background: linear-gradient(135deg, #0066cc 0%, #004499 100%) !important; } -.header-question { background: linear-gradient(135deg, #009944 0%, #007733 100%) !important; } -.header-paper { background: linear-gradient(135deg, #e67e22 0%, #d35400 100%) !important; } -.header-setting { background: linear-gradient(135deg, #8e44ad 0%, #732d91 100%) !important; } - -/* 主体容器 - 响应式 Flex 布局 */ -.container { - display: flex; - flex-wrap: wrap; /* 核心:允许在空间不足时换行 */ - flex-direction: row; - justify-content: center; - align-items: center; - gap: 20px; - padding: 40px 20px; - max-width: 1200px; - margin: 0 auto; -} - -/* 详情页容器 */ -.sub-container { - padding: 30px 20px; - text-align: center; - max-width: 800px; - margin: 0 auto; -} - -/* 卡片按钮基础样式 */ -.card { - flex: 1; /* 自动伸缩 */ - min-width: 240px; /* 保证电脑端不会太窄 */ - max-width: 280px; /* 保证电脑端不会太宽 */ - height: 85px; - background-color: white; - border-radius: 15px; - display: flex; - align-items: center; - justify-content: center; - font-size: 18px; - font-weight: 600; - cursor: pointer; - transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); - text-decoration: none; - box-shadow: 0 4px 12px rgba(0,0,0,0.08); - border: 1px solid rgba(0,0,0,0.05); -} - -.card:hover { - transform: translateY(-5px); - box-shadow: 0 12px 20px rgba(0,0,0,0.15); -} - -/* 四个模块的丰富配色方案 */ -.card-subject { color: #0066cc; border-left: 8px solid #0066cc; } -.card-subject:hover { background-color: #f0f7ff; } - -.card-question { color: #009944; border-left: 8px solid #009944; } -.card-question:hover { background-color: #f0fff4; } - -.card-paper { color: #e67e22; border-left: 8px solid #e67e22; } -.card-paper:hover { background-color: #fffaf5; } - -.card-setting { color: #8e44ad; border-left: 8px solid #8e44ad; } -.card-setting:hover { background-color: #f9f5fb; } - -/* 图标/数字样式 */ -.icon-num { - margin-right: 12px; - font-size: 22px; - font-weight: bold; - opacity: 0.7; -} - -/* 返回按钮样式 */ -.back-btn { - display: inline-block; - margin-top: 30px; - padding: 12px 30px; - background: #666; - color: white; - text-decoration: none; - border-radius: 25px; - font-weight: 500; - transition: background 0.3s; -} -.back-btn:hover { - background: #333; -} - -/* === 移动端适配 (iPhone 等) === */ -@media (max-width: 600px) { - .navbar { - font-size: 18px; - height: 50px; - } - - .container { - padding: 20px 15px; - gap: 15px; - } - - .card { - /* 在手机上,让按钮占据几乎全部宽度,防止并排重叠 */ - min-width: 100%; - max-width: 100%; - height: 75px; - font-size: 17px; - border-left-width: 10px; /* 手机上加粗色块更醒目 */ - } - - .sub-container h2 { - font-size: 20px; - } -} -======================================== ->>> FILE END: static/css/style.css <<< - - ->>> FILE START: templates/index.html <<< -======================================== - - - - - - 错题智能整理系统 - - - - - - - - -======================================== ->>> FILE END: templates/index.html <<< - - ->>> FILE START: templates/papers/index.html <<< -======================================== - - - - - 试卷管理 - 错题系统 - - - - - -
-

这里是试卷管理内容区域

-

正在从原 Python 程序迁移逻辑...

- 返回首页 -
- - -======================================== ->>> FILE END: templates/papers/index.html <<< - - ->>> FILE START: templates/questions/index.html <<< -======================================== - - - - - 科目面板 - 错题系统 - - - - - -
-

这里是科目面板内容区域

-

正在从原 Python 程序迁移逻辑...

- 返回首页 -
- - -======================================== ->>> FILE END: templates/questions/index.html <<< - - ->>> FILE START: templates/questions/question_list.html <<< -======================================== - - - - - - 试题列表 - - - - -
试题列表
-
- - - - - - - - - - -
- -
- - - - - - - - - - - - - - - - - - -
ID科目题目类型题干内容答案解析答题时间输入时间题目知识点
正在加载数据...
-
- - - - -======================================== ->>> FILE END: templates/questions/question_list.html <<< - - ->>> FILE START: templates/settings/index.html <<< -======================================== - - - - - 系统设定 - 错题系统 - - - - - -
-

这里是系统设定内容区域

-

正在从原 Python 程序迁移逻辑...

- 返回首页 -
- - -======================================== ->>> FILE END: templates/settings/index.html <<< - - ->>> FILE START: templates/subject/chapters.html <<< -======================================== - - - - - - 章节分析 - - - - - -
-
- - - - 返回 -
- -
- - - - - - - - - - -
科目章节总数待完成
-
-
- - - - -======================================== ->>> FILE END: templates/subject/chapters.html <<< - - ->>> FILE START: templates/subject/index.html <<< -======================================== - - - - - - 科目概要面板 - - - - - - - -
- -
-
- - -
- - -
- {% for stage in stages %} -
- {{ stage.stage_name }} -
- {% endfor %} -
-
- - -
- -
-

错题情况回顾

- 0% -
总数0
-
已解决0
-
遗留0
-
- - -
-

强化推题追踪

- 0% -
总数0
-
已完成0
-
遗留0
-
-
- - - - - - -
- - - - -======================================== ->>> FILE END: templates/subject/index.html <<< - - ->>> FILE START: templates/subject/knowledge.html <<< -======================================== - - - - - - 知识点分析 - - - - - -
-
- - - - 返回 -
- -
- - - - - - - - - - -
科目知识点内容总数待完成
-
-
- - - - -======================================== ->>> FILE END: templates/subject/knowledge.html <<< - diff --git a/s/schema.sql b/s/schema.sql deleted file mode 100755 index 825d267..0000000 --- a/s/schema.sql +++ /dev/null @@ -1,139 +0,0 @@ -CREATE TABLE Subject ( - ID INTEGER PRIMARY KEY, - Subject TEXT NOT NULL CHECK(length(Subject) <= 10), - CreateTime DATETIME DEFAULT (datetime('now','localtime')) -, enabled BOOLEAN DEFAULT 1); -CREATE TABLE Error_Cause ( - ID INTEGER PRIMARY KEY AUTOINCREMENT, - Cause TEXT NOT NULL, - Enabled BOOLEAN DEFAULT 1, -- 默认为启用状态 - ParentID INTEGER, - FOREIGN KEY (ParentID) REFERENCES Error_Cause(ID) ON DELETE CASCADE -); -CREATE TABLE sqlite_sequence(name,seq); -CREATE TABLE Question_Status ( - ID INTEGER PRIMARY KEY AUTOINCREMENT, - StatusName TEXT NOT NULL, - Description TEXT, - Enabled BOOLEAN DEFAULT 1 - ); -CREATE TABLE question_source ( - id INTEGER PRIMARY KEY AUTOINCREMENT, -- 来源ID,自动递增 - source_description TEXT NOT NULL, -- 来源描述 - is_enabled BOOLEAN DEFAULT 1 -- 是否启用,默认为 1 (true) -); -CREATE TABLE questions_new ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - subject_id INTEGER, - status_id INTEGER, - error_cause_id INTEGER, - source_id INTEGER, - question_type TEXT, - content TEXT NOT NULL, - analysis TEXT, - score REAL, -- 修改点:允许为空,无默认值 - is_subjective INTEGER DEFAULT 0, - has_subquestions BOOLEAN DEFAULT 0, - subquestion_count INTEGER DEFAULT 0, - subquestion_description TEXT DEFAULT '', - subquestion_correct_count INTEGER DEFAULT 0, - input_time DATETIME DEFAULT CURRENT_TIMESTAMP, - answer_time DATETIME, - FOREIGN KEY (subject_id) REFERENCES subject(id), - FOREIGN KEY (status_id) REFERENCES Question_status(id), - FOREIGN KEY (error_cause_id) REFERENCES Error_cause(id), - FOREIGN KEY (source_id) REFERENCES question_source(id) -); -CREATE TABLE IF NOT EXISTS "questions" ( - "id" INTEGER, - "subject_id" INTEGER, - "status_id" INTEGER, - "error_cause_id" INTEGER, - "source_id" INTEGER, - "question_type" TEXT, - "content" TEXT NOT NULL, - "analysis" TEXT, - "score" REAL, - "input_time" DATETIME DEFAULT CURRENT_TIMESTAMP, - "answer_time" DATETIME, - "has_subquestions" BOOLEAN DEFAULT 0, - "subquestion_count" INTEGER DEFAULT 0, - "subquestion_description" TEXT DEFAULT '', - "subquestion_correct_count" INTEGER DEFAULT 0, - "is_subjective" INTEGER DEFAULT 0, term_id INTEGER, stage_id INTEGER, origin_id INTEGER, media_path TEXT, paper_count INTEGER DEFAULT 0, review_times INTEGER DEFAULT 0, right_times INTEGER DEFAULT 0, last_right_time DATETIME DEFAULT NULL, wrong_times INTEGER DEFAULT 0, last_wrong_time DATETIME DEFAULT NULL, half_right_times INTEGER DEFAULT 0, last_half_right_time DATETIME DEFAULT NULL, - PRIMARY KEY("id" AUTOINCREMENT), - FOREIGN KEY("error_cause_id") REFERENCES "Error_cause"("id"), - FOREIGN KEY("source_id") REFERENCES "question_source"("id"), - FOREIGN KEY("status_id") REFERENCES "Question_status"("id"), - FOREIGN KEY("subject_id") REFERENCES "subject"("id") -); -CREATE TABLE IF NOT EXISTS "question_kp_relation" ( - "question_id" INTEGER, - "kp_id" INTEGER, - PRIMARY KEY("question_id","kp_id"), - FOREIGN KEY("kp_id") REFERENCES "knowledge_point"("id") ON DELETE CASCADE, - FOREIGN KEY("question_id") REFERENCES "questions"("id") ON DELETE CASCADE -); -CREATE TABLE IF NOT EXISTS "knowledge_point" ( - "id" INTEGER, - "content" TEXT NOT NULL, - "parent_id" INTEGER, - "is_enabled" BOOLEAN DEFAULT 1, - "subject_id" INTEGER, - PRIMARY KEY("id" AUTOINCREMENT), - FOREIGN KEY("parent_id") REFERENCES "knowledge_point"("id") -); -CREATE TABLE schema_comments ( - object_type TEXT, -- 'table' 或 'column' - table_name TEXT NOT NULL, - column_name TEXT, -- table 类型时可为 NULL - description TEXT, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY(object_type, table_name, column_name) -); -CREATE TABLE term ( - ID INTEGER PRIMARY KEY AUTOINCREMENT, - term_name TEXT NOT NULL, - enabled INTEGER NOT NULL DEFAULT 1 -, is_default INTEGER NOT NULL DEFAULT 0); -CREATE TABLE stage ( - ID INTEGER PRIMARY KEY AUTOINCREMENT, - stage_name TEXT NOT NULL, - enabled INTEGER NOT NULL DEFAULT 1 -, is_default INTEGER NOT NULL DEFAULT 0); -CREATE TABLE question_origin ( - ID INTEGER PRIMARY KEY AUTOINCREMENT, - origin_name TEXT NOT NULL, - enabled INTEGER NOT NULL DEFAULT 1 -, is_default INTEGER NOT NULL DEFAULT 0); -CREATE TABLE paper_status ( - ID INTEGER PRIMARY KEY AUTOINCREMENT, - status_name TEXT NOT NULL, - description TEXT, - enabled INTEGER NOT NULL DEFAULT 1 -); -CREATE TABLE paper_questions ( - paper_id INTEGER NOT NULL, -- 试卷ID - question_id INTEGER NOT NULL, -- 题目ID - error_cause_id INTEGER, -- 错误原因ID (来自 Error_Cause) - result_type INTEGER DEFAULT 0, -- 结果判定: 0=错误; 1=正确; 2=部分正确 - correct_sub_count INTEGER DEFAULT 0, -- 正确小题个数 - accuracy REAL DEFAULT 0.0, -- 正确率 (数值) - error_sub_description TEXT, record_time TEXT, -- 错误小题描述 - PRIMARY KEY (paper_id, question_id) -); -CREATE TABLE papers_new ( - id INTEGER PRIMARY KEY, - title TEXT, - author TEXT, - subject TEXT -); -CREATE TABLE IF NOT EXISTS "papers" ( - "ID" INTEGER, - "paper_name" TEXT NOT NULL, - "create_time" DATETIME DEFAULT CURRENT_TIMESTAMP, - "status_id" INTEGER, - "enabled" INTEGER NOT NULL DEFAULT 1, - "subject" TEXT, - PRIMARY KEY("ID" AUTOINCREMENT) -); diff --git a/s/studybase.sqbpro b/s/studybase.sqbpro deleted file mode 100755 index c18e5f7..0000000 --- a/s/studybase.sqbpro +++ /dev/null @@ -1,4 +0,0 @@ -
UPDATE questions -SET stage_id = 2 -WHERE subject_id = 2 - AND answer_time= '2026-04-01'
diff --git a/s/workmode-studybase.sql b/s/workmode-studybase.sql deleted file mode 100755 index 60c28b7..0000000 --- a/s/workmode-studybase.sql +++ /dev/null @@ -1,139 +0,0 @@ -CREATE TABLE Subject ( - ID INTEGER PRIMARY KEY, - Subject TEXT NOT NULL CHECK(length(Subject) <= 10), - CreateTime DATETIME DEFAULT (datetime('now','localtime')) -, enabled BOOLEAN DEFAULT 1); -CREATE TABLE Error_Cause ( - ID INTEGER PRIMARY KEY AUTOINCREMENT, - Cause TEXT NOT NULL, - Enabled BOOLEAN DEFAULT 1, -- 默认为启用状态 - ParentID INTEGER, - FOREIGN KEY (ParentID) REFERENCES Error_Cause(ID) ON DELETE CASCADE -); -CREATE TABLE sqlite_sequence(name,seq); -CREATE TABLE Question_Status ( - ID INTEGER PRIMARY KEY AUTOINCREMENT, - StatusName TEXT NOT NULL, - Description TEXT, - Enabled BOOLEAN DEFAULT 1 - ); -CREATE TABLE question_source ( - id INTEGER PRIMARY KEY AUTOINCREMENT, -- 来源ID,自动递增 - source_description TEXT NOT NULL, -- 来源描述 - is_enabled BOOLEAN DEFAULT 1 -- 是否启用,默认为 1 (true) -); -CREATE TABLE questions_new ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - subject_id INTEGER, - status_id INTEGER, - error_cause_id INTEGER, - source_id INTEGER, - question_type TEXT, - content TEXT NOT NULL, - analysis TEXT, - score REAL, -- 修改点:允许为空,无默认值 - is_subjective INTEGER DEFAULT 0, - has_subquestions BOOLEAN DEFAULT 0, - subquestion_count INTEGER DEFAULT 0, - subquestion_description TEXT DEFAULT '', - subquestion_correct_count INTEGER DEFAULT 0, - input_time DATETIME DEFAULT CURRENT_TIMESTAMP, - answer_time DATETIME, - FOREIGN KEY (subject_id) REFERENCES subject(id), - FOREIGN KEY (status_id) REFERENCES Question_status(id), - FOREIGN KEY (error_cause_id) REFERENCES Error_cause(id), - FOREIGN KEY (source_id) REFERENCES question_source(id) -); -CREATE TABLE IF NOT EXISTS "questions" ( - "id" INTEGER, - "subject_id" INTEGER, - "status_id" INTEGER, - "error_cause_id" INTEGER, - "source_id" INTEGER, - "question_type" TEXT, - "content" TEXT NOT NULL, - "analysis" TEXT, - "score" REAL, - "input_time" DATETIME DEFAULT CURRENT_TIMESTAMP, - "answer_time" DATETIME, - "has_subquestions" BOOLEAN DEFAULT 0, - "subquestion_count" INTEGER DEFAULT 0, - "subquestion_description" TEXT DEFAULT '', - "subquestion_correct_count" INTEGER DEFAULT 0, - "is_subjective" INTEGER DEFAULT 0, term_id INTEGER, stage_id INTEGER, origin_id INTEGER, media_path TEXT, paper_count INTEGER DEFAULT 0, review_times INTEGER DEFAULT 0, right_times INTEGER DEFAULT 0, last_right_time DATETIME DEFAULT NULL, wrong_times INTEGER DEFAULT 0, last_wrong_time DATETIME DEFAULT NULL, half_right_times INTEGER DEFAULT 0, last_half_right_time DATETIME DEFAULT NULL, latest_status TEXT DEFAULT NULL, half_right_description TEXT DEFAULT NULL, - PRIMARY KEY("id" AUTOINCREMENT), - FOREIGN KEY("error_cause_id") REFERENCES "Error_cause"("id"), - FOREIGN KEY("source_id") REFERENCES "question_source"("id"), - FOREIGN KEY("status_id") REFERENCES "Question_status"("id"), - FOREIGN KEY("subject_id") REFERENCES "subject"("id") -); -CREATE TABLE IF NOT EXISTS "question_kp_relation" ( - "question_id" INTEGER, - "kp_id" INTEGER, - PRIMARY KEY("question_id","kp_id"), - FOREIGN KEY("kp_id") REFERENCES "knowledge_point"("id") ON DELETE CASCADE, - FOREIGN KEY("question_id") REFERENCES "questions"("id") ON DELETE CASCADE -); -CREATE TABLE IF NOT EXISTS "knowledge_point" ( - "id" INTEGER, - "content" TEXT NOT NULL, - "parent_id" INTEGER, - "is_enabled" BOOLEAN DEFAULT 1, - "subject_id" INTEGER, - PRIMARY KEY("id" AUTOINCREMENT), - FOREIGN KEY("parent_id") REFERENCES "knowledge_point"("id") -); -CREATE TABLE schema_comments ( - object_type TEXT, -- 'table' 或 'column' - table_name TEXT NOT NULL, - column_name TEXT, -- table 类型时可为 NULL - description TEXT, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY(object_type, table_name, column_name) -); -CREATE TABLE term ( - ID INTEGER PRIMARY KEY AUTOINCREMENT, - term_name TEXT NOT NULL, - enabled INTEGER NOT NULL DEFAULT 1 -, is_default INTEGER NOT NULL DEFAULT 0); -CREATE TABLE stage ( - ID INTEGER PRIMARY KEY AUTOINCREMENT, - stage_name TEXT NOT NULL, - enabled INTEGER NOT NULL DEFAULT 1 -, is_default INTEGER NOT NULL DEFAULT 0); -CREATE TABLE question_origin ( - ID INTEGER PRIMARY KEY AUTOINCREMENT, - origin_name TEXT NOT NULL, - enabled INTEGER NOT NULL DEFAULT 1 -, is_default INTEGER NOT NULL DEFAULT 0); -CREATE TABLE paper_status ( - ID INTEGER PRIMARY KEY AUTOINCREMENT, - status_name TEXT NOT NULL, - description TEXT, - enabled INTEGER NOT NULL DEFAULT 1 -); -CREATE TABLE paper_questions ( - paper_id INTEGER NOT NULL, -- 试卷ID - question_id INTEGER NOT NULL, -- 题目ID - error_cause_id INTEGER, -- 错误原因ID (来自 Error_Cause) - result_type INTEGER DEFAULT 0, -- 结果判定: 0=错误; 1=正确; 2=部分正确 - correct_sub_count INTEGER DEFAULT 0, -- 正确小题个数 - accuracy REAL DEFAULT 0.0, -- 正确率 (数值) - error_sub_description TEXT, record_time TEXT, subject_id INTEGER, -- 错误小题描述 - PRIMARY KEY (paper_id, question_id) -); -CREATE TABLE papers_new ( - id INTEGER PRIMARY KEY, - title TEXT, - author TEXT, - subject TEXT -); -CREATE TABLE IF NOT EXISTS "papers" ( - "ID" INTEGER, - "paper_name" TEXT NOT NULL, - "create_time" DATETIME DEFAULT CURRENT_TIMESTAMP, - "status_id" INTEGER, - "enabled" INTEGER NOT NULL DEFAULT 1, - "subject" TEXT, - PRIMARY KEY("ID" AUTOINCREMENT) -); diff --git a/static/css/style.css b/static/css/style.css deleted file mode 100644 index 144ce60..0000000 --- a/static/css/style.css +++ /dev/null @@ -1,139 +0,0 @@ -/* 全局基础样式 */ -body { - margin: 0; - padding: 0; - background-color: #f0f2f5; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; - -webkit-font-smoothing: antialiased; -} - -/* 顶部导航栏基础样式 */ -.navbar { - height: 60px; - background: linear-gradient(135deg, #4088d8 0%, #2a69b5 100%); - display: flex; - justify-content: center; - align-items: center; - color: white; - font-size: 22px; - font-weight: bold; - box-shadow: 0 2px 10px rgba(0,0,0,0.1); - position: sticky; - top: 0; - z-index: 1000; -} - -/* 子页面特定的 Header 颜色 - 对应首页按钮色系 */ -.header-subject { background: linear-gradient(135deg, #0066cc 0%, #004499 100%) !important; } -.header-question { background: linear-gradient(135deg, #009944 0%, #007733 100%) !important; } -.header-paper { background: linear-gradient(135deg, #e67e22 0%, #d35400 100%) !important; } -.header-setting { background: linear-gradient(135deg, #8e44ad 0%, #732d91 100%) !important; } - -/* 主体容器 - 响应式 Flex 布局 */ -.container { - display: flex; - flex-wrap: wrap; /* 核心:允许在空间不足时换行 */ - flex-direction: row; - justify-content: center; - align-items: center; - gap: 20px; - padding: 40px 20px; - max-width: 1200px; - margin: 0 auto; -} - -/* 详情页容器 */ -.sub-container { - padding: 30px 20px; - text-align: center; - max-width: 800px; - margin: 0 auto; -} - -/* 卡片按钮基础样式 */ -.card { - flex: 1; /* 自动伸缩 */ - min-width: 240px; /* 保证电脑端不会太窄 */ - max-width: 280px; /* 保证电脑端不会太宽 */ - height: 85px; - background-color: white; - border-radius: 15px; - display: flex; - align-items: center; - justify-content: center; - font-size: 18px; - font-weight: 600; - cursor: pointer; - transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); - text-decoration: none; - box-shadow: 0 4px 12px rgba(0,0,0,0.08); - border: 1px solid rgba(0,0,0,0.05); -} - -.card:hover { - transform: translateY(-5px); - box-shadow: 0 12px 20px rgba(0,0,0,0.15); -} - -/* 四个模块的丰富配色方案 */ -.card-subject { color: #0066cc; border-left: 8px solid #0066cc; } -.card-subject:hover { background-color: #f0f7ff; } - -.card-question { color: #009944; border-left: 8px solid #009944; } -.card-question:hover { background-color: #f0fff4; } - -.card-paper { color: #e67e22; border-left: 8px solid #e67e22; } -.card-paper:hover { background-color: #fffaf5; } - -.card-setting { color: #8e44ad; border-left: 8px solid #8e44ad; } -.card-setting:hover { background-color: #f9f5fb; } - -/* 图标/数字样式 */ -.icon-num { - margin-right: 12px; - font-size: 22px; - font-weight: bold; - opacity: 0.7; -} - -/* 返回按钮样式 */ -.back-btn { - display: inline-block; - margin-top: 30px; - padding: 12px 30px; - background: #666; - color: white; - text-decoration: none; - border-radius: 25px; - font-weight: 500; - transition: background 0.3s; -} -.back-btn:hover { - background: #333; -} - -/* === 移动端适配 (iPhone 等) === */ -@media (max-width: 600px) { - .navbar { - font-size: 18px; - height: 50px; - } - - .container { - padding: 20px 15px; - gap: 15px; - } - - .card { - /* 在手机上,让按钮占据几乎全部宽度,防止并排重叠 */ - min-width: 100%; - max-width: 100%; - height: 75px; - font-size: 17px; - border-left-width: 10px; /* 手机上加粗色块更醒目 */ - } - - .sub-container h2 { - font-size: 20px; - } -} diff --git a/templates/index.html b/templates/index.html deleted file mode 100644 index 1e60134..0000000 --- a/templates/index.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - 错题智能整理系统 - - - - - - - - diff --git a/templates/papers/index.html b/templates/papers/index.html deleted file mode 100644 index 7d676ea..0000000 --- a/templates/papers/index.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - 试卷管理 - 错题系统 - - - - - -
-

这里是试卷管理内容区域

-

正在从原 Python 程序迁移逻辑...

- 返回首页 -
- - diff --git a/templates/questions/index.html b/templates/questions/index.html deleted file mode 100644 index 5c868b5..0000000 --- a/templates/questions/index.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - 科目面板 - 错题系统 - - - - - -
-

这里是科目面板内容区域

-

正在从原 Python 程序迁移逻辑...

- 返回首页 -
- - diff --git a/templates/questions/question_list.html b/templates/questions/question_list.html deleted file mode 100644 index a76aea1..0000000 --- a/templates/questions/question_list.html +++ /dev/null @@ -1,140 +0,0 @@ - - - - - - 试题列表 - - - - -
试题列表
-
- - - - - - - - - - -
- -
- - - - - - - - - - - - - - - - - - -
ID科目题目类型题干内容答案解析答题时间输入时间题目知识点
正在加载数据...
-
- - - - diff --git a/templates/settings/index.html b/templates/settings/index.html deleted file mode 100644 index b80b2d5..0000000 --- a/templates/settings/index.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - 系统设定 - 错题系统 - - - - - -
-

这里是系统设定内容区域

-

正在从原 Python 程序迁移逻辑...

- 返回首页 -
- - diff --git a/templates/subject/chapters.html b/templates/subject/chapters.html deleted file mode 100644 index 75a0eca..0000000 --- a/templates/subject/chapters.html +++ /dev/null @@ -1,103 +0,0 @@ - - - - - - 章节分析 - - - - - -
-
- - - - 返回 -
- -
- - - - - - - - - - -
科目章节总数待完成
-
-
- - - - diff --git a/templates/subject/index.html b/templates/subject/index.html deleted file mode 100644 index 8ec125e..0000000 --- a/templates/subject/index.html +++ /dev/null @@ -1,205 +0,0 @@ - - - - - - 科目概要面板 - - - - - - - -
- -
-
- - -
- - -
- {% for stage in stages %} -
- {{ stage.stage_name }} -
- {% endfor %} -
-
- - -
- -
-

错题情况回顾

- 0% -
总数0
-
已解决0
-
遗留0
-
- - -
-

强化推题追踪

- 0% -
总数0
-
已完成0
-
遗留0
-
-
- - - - - - -
- - - - diff --git a/templates/subject/knowledge.html b/templates/subject/knowledge.html deleted file mode 100644 index e2197cc..0000000 --- a/templates/subject/knowledge.html +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - 知识点分析 - - - - - -
-
- - - - 返回 -
- -
- - - - - - - - - - -
科目知识点内容总数待完成
-
-
- - - -