diff --git a/Html/apps/services/cos_service.py b/Html/apps/services/cos_service.py index b90b4cb..9508e45 100644 --- a/Html/apps/services/cos_service.py +++ b/Html/apps/services/cos_service.py @@ -1,10 +1,23 @@ from flask import current_app def upload_file(rb_file, file_name): + if file_name is not None and "/" in file_name: + file_name = file_name.split("/")[-1] cos_client = current_app.extensions["cos_client"] cos_client.put_object( Bucket=current_app.config["TENCENT_COS"]["bucket"], Body=rb_file, Key=file_name, ) - return f"https://{current_app.config['TENCENT_COS']['bucket']}.cos.{current_app.config['TENCENT_COS']['region']}.myqcloud.com/{file_name}" \ No newline at end of file + return f"https://{current_app.config['TENCENT_COS']['bucket']}.cos.{current_app.config['TENCENT_COS']['region']}.myqcloud.com/{file_name}" + + +def delete_file(file_name): + if file_name is not None and "/" in file_name: + file_name = file_name.split("/")[-1] + cos_client = current_app.extensions["cos_client"] + cos_client.delete_object( + Bucket=current_app.config["TENCENT_COS"]["bucket"], + Key=file_name, + ) + return True diff --git a/Html/apps/services/course_service.py b/Html/apps/services/course_service.py index f49a0a8..b335053 100644 --- a/Html/apps/services/course_service.py +++ b/Html/apps/services/course_service.py @@ -1,9 +1,12 @@ # myapp/services/course_service.py +import re from typing import Dict, List from flask import current_app from datetime import datetime from bson import ObjectId +from ..services.cos_service import upload_file, delete_file + from ..models.material import Material # 假设你已有这两个 loader @@ -64,28 +67,105 @@ def add_material_chapter(material_id, chapter_name, teacher_id): {'_id': ObjectId(material_id)}, {'$push': {'chapters': {'chapter_name': chapter_name, 'lessons': []}}}) return True - -def add_material_chapter_lesson(material_id, chapter_name, lesson_name, teacher_id): + +def delete_material_chapter_lesson(material_id, chapter_name, lesson_name, teacher_id): + """ + 删除指定课程的课时。 + 删除后,更新 MongoDB 中对应的章节中的 lesson 列表。 + """ mongo = current_app.extensions["mongo"] - lesson_doc = { - "lesson_name": lesson_name or "", - "markdown_lesson_file_link": "", - "markdown_prompt_file_name": "", - "markdown_score_prompt_file_link": "", - } + # 查询 material 是否存在,且确保该 teacher_id 对应的 material + material = mongo.db.materials.find_one({'_id': ObjectId(material_id), 'teacher_id': teacher_id}) + for chapter in material['chapters']: + if chapter['chapter_name'] == chapter_name: + for lesson in chapter['lessons']: + if lesson['lesson_name'] == lesson_name: + if lesson['markdown_lesson_file_link']!='': + delete_file(lesson['markdown_lesson_file_link']) + if lesson['markdown_prompt_file_name']!='': + delete_file(lesson['markdown_prompt_file_name']) + if lesson['markdown_score_prompt_file_link']!='': + delete_file(lesson['markdown_score_prompt_file_link']) + if not material: + return False # 找不到对应的 material 或 teacher_id 不匹配 + + # 删除 lesson 的条件 res = mongo.db.materials.update_one( { - '_id': ObjectId(material_id), - 'teacher_id': teacher_id, + "_id": ObjectId(material_id), + "teacher_id": teacher_id # 确保是该教师的课程 }, { - '$push': {'chapters.$[c].lessons': lesson_doc}, - '$set': {'updated_at': datetime.now()}, + "$pull": { + "chapters.$[c].lessons": { + "lesson_name": lesson_name # 根据 lesson_name 来删除 + } + }, + "$set": {"updated_at": datetime.now()} # 更新修改时间 }, - array_filters=[{'c.chapter_name': chapter_name}] # 命名占位符 c 指向命中的 chapter + array_filters=[{"c.chapter_name": chapter_name}] # 找到匹配的章节 ) + + # 如果删除操作成功,res.modified_count 应为 1 + return res.modified_count == 1 + + +def add_material_chapter_lesson(material_id, chapter_name, lesson_name, teacher_id): + """ + 1) 生成占位 Markdown(lesson_prompt_score_prompt) + 2) 上传到对象存储(返回 URL) + 3) 将 lesson 文档(含三份 URL)push 到指定章节 + """ + mongo = current_app.extensions["mongo"] + + # 组织对象存储的文件路径 + chapter_slug = _slugify(chapter_name or "") + lesson_slug = _slugify(lesson_name or "") + timestamp = datetime.now().strftime('%Y%m%dT%H%M%SZ') + base_prefix = f"materials_{material_id}_{chapter_slug}_{lesson_slug}_{timestamp}" + + # 占位文本 + lesson_md, prompt_md, score_prompt_md = _default_markdowns(lesson_name or "未命名课时") + + # 生成文件名(带层级路径) + lesson_key = f"{base_prefix}_lesson.md" + prompt_key = f"{base_prefix}_prompt.md" + score_key = f"{base_prefix}_score_prompt.md" + + # 上传(任意一个失败则终止,不写库) + try: + lesson_url = upload_file(lesson_md.encode("utf-8"), lesson_key) + prompt_url = upload_file(prompt_md.encode("utf-8"), prompt_key) + score_url = upload_file(score_prompt_md.encode("utf-8"), score_key) + except Exception as e: + current_app.logger.exception("Upload default markdown failed: %s", e) + return False + + # 组装 lesson_doc + lesson_doc = { + "lesson_name": lesson_name or "", + "markdown_lesson_file_link": lesson_url, # lesson 主体 + "markdown_prompt_file_name": prompt_url, # 给大模型的 prompt + "markdown_score_prompt_file_link": score_url, # 评分 prompt + } + + # 将 lesson push 到指定章节 + res = mongo.db.materials.update_one( + { + "_id": ObjectId(material_id), + "teacher_id": teacher_id, # 注意:若存的是 ObjectId,请确保传入的是同类型 + }, + { + "$push": {"chapters.$[c].lessons": lesson_doc}, + "$set": {"updated_at": datetime.now()}, + }, + array_filters=[{"c.chapter_name": chapter_name}], + ) + + # 可选:如果 res.modified_count != 1,说明 push 失败(可能章节名不匹配) + # 这里可以考虑是否回滚 COS 上刚上传的 3 个文件(需实现删除逻辑)。 return res.modified_count == 1 @@ -101,4 +181,56 @@ def get_materials_by_teacher_dict(teacher_id)->List[Dict]: for material in materials: material['_id'] = str(material['_id']) result.append(material) - return result \ No newline at end of file + return result + + + + + + + +def _slugify(name: str) -> str: + """将章节/课时名转为 URL 友好的 slug。""" + s = name.strip().lower() + s = re.sub(r'\s+', '-', s) # 空白 -> - + s = re.sub(r'[^a-z0-9\-_]+', '', s) # 仅保留安全字符 + return s or 'untitled' + +def _default_markdowns(lesson_name: str): + """三份占位 Markdown 文本。可按需调整模板内容。""" + ts = datetime.now().strftime('%Y-%m-%d %H:%M') + lesson_md = f"""# {lesson_name} +> Auto-generated at {ts} + +## 阶段一:示例阶段 +### 步骤A +在这里编写该步骤的教学内容(文字/代码/图片链接等)。 + +### 步骤B +继续补充该阶段的其它步骤内容。 +""" + + prompt_md = f"""# {lesson_name} +> Prompt for LLM (auto-generated at {ts}) + +## 阶段一:示例阶段 +### 步骤A +请基于“步骤A”的教学目标与材料,生成指导性提示词(尽量结构化,给出评估标准和示例)。 + +### 步骤B +请基于“步骤B”的教学目标与材料,生成指导性提示词。 +""" + + score_prompt_md = f"""# {lesson_name} +> Scoring Prompt for LLM (auto-generated at {ts}) + +## 评分准则 +- 正确性、完整性、可读性、效率(按需调整权重) + +## 阶段一:示例阶段 +### 步骤A +对学员在“步骤A”的输出进行评分与文字反馈,分数区间 0-100,并输出 JSON:```{{"score": , "reasons": "", "advices": ""}}``` +### 步骤B +同上,对“步骤B”输出进行评分与反馈。 +""" + return lesson_md, prompt_md, score_prompt_md diff --git a/Html/apps/static/css/lesson.css b/Html/apps/static/css/lesson.css new file mode 100644 index 0000000..3c60155 --- /dev/null +++ b/Html/apps/static/css/lesson.css @@ -0,0 +1,42 @@ +/* 侧边栏 */ +.course-details { + position: fixed; top:0; right:-360px; width:360px; height:100%; + background:#fff; box-shadow:-2px 0 10px rgba(0,0,0,.1); + transition:right .3s ease; padding:16px; overflow:auto; z-index: 1000; + } + .course-details.open { right:0; } + .details-header { display:flex; justify-content:space-between; align-items:center; } + .close-btn { border:none; background:transparent; font-size:22px; cursor:pointer; } + + /* 主体布局 */ + .lesson-editor { display:flex; height: calc(100vh - 20px); gap: 12px; padding: 12px; } + .outline { + width: 280px; border:1px solid #eee; border-radius:10px; padding:10px; + overflow:auto; background:#fafafa; + } + .outline-header { font-weight:700; margin-bottom:8px; } + #outline-tree { list-style:none; padding-left:0; } + #outline-tree li { margin:4px 0; cursor:pointer; } + #outline-tree .lvl1 { font-weight:700; } + #outline-tree .lvl2 { margin-left:12px; } + #outline-tree .lvl3 { margin-left:24px; } + #outline-tree li.active { background:#eaf6ff; border-radius:6px; padding:2px 6px; } + + .editor-pane { + flex:1; display:flex; flex-direction:column; min-width:0; + border:1px solid #eee; border-radius:10px; + } + .editor-tabs { display:flex; align-items:center; gap:8px; padding:8px; border-bottom:1px solid #eee; } + .tab-btn { border:1px solid #ddd; background:#fff; padding:6px 10px; border-radius:8px; cursor:pointer; } + .tab-btn.active { background:#f0f8ff; border-color:#a0c4ff; } + .actions { margin-left:auto; } + .actions button { padding:6px 12px; border:none; border-radius:8px; background:#4CAF50; color:#fff; cursor:pointer; } + .tab-content { flex:1; padding:8px; } + .tab { display:none; height:100%; } + .tab.active { display:block; height:100%; } + textarea { + width:100%; height: calc(100vh - 180px); + border:1px solid #ddd; border-radius:8px; padding:10px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + box-sizing:border-box; resize: vertical; + } + \ No newline at end of file diff --git a/Html/apps/static/js/lesson.js b/Html/apps/static/js/lesson.js new file mode 100644 index 0000000..6f8b598 --- /dev/null +++ b/Html/apps/static/js/lesson.js @@ -0,0 +1,204 @@ +window.addEventListener('load', ()=>{ + console.log(window.material); + console.log(window.lesson); + editLesson(window.material.material_id, window.lesson.chapter_name, window.lesson.lesson_name); +}); + +function parseHeadings(mdText) { + + const lines = mdText.split('\n'); + const tokens = []; + for (let i=0;i tokens[i].level) j++; + // 找到第一个 >= 当前level 的 heading 或到文末 + let k = i+1; + while (k tokens[i].level) k++; + const nextSameOrHigher = k x.level===level && x.text===headingText); + if (!t) return ''; + return parsed.lines.slice(t.start, t.end+1).join('\n'); + } + + // 根据三个 md,构造统一大纲树(一级唯一“主题”,其下多个阶段##,其下多个步骤###) + function buildUnifiedOutline(parsedLesson, parsedPrompt, parsedScore) { + // 取 lesson 的结构为主(也可做三者交集/并集策略,这里简单以 lesson 为基准) + const root1 = parsedLesson.tokens.find(t => t.level===1); + const phases = parsedLesson.tokens.filter(t => t.level===2); + const stepsByPhase = {}; + phases.forEach(ph => { + stepsByPhase[ph.text] = parsedLesson.tokens.filter(t => t.level===3 && t.start>ph.start && t.end<=ph.end) + .map(s => s.text); + }); + return { theme: root1 ? root1.text : '主题', phases, stepsByPhase }; + } + + let CURRENT = { + material_id: null, chapter_name: null, lesson_name: null, + links: { lesson:'', prompt:'', score:'' }, + raw: { lesson:'', prompt:'', score:'' }, + parsed:{ lesson:null, prompt:null, score:null }, + outline:null + }; + // 入口:从你的列表调用 + async function editLesson(material_id, chapter_name, lesson_name) { + + CURRENT.material_id = material_id; + CURRENT.chapter_name = chapter_name; + CURRENT.lesson_name = lesson_name; + + // 打开右侧课程详情先占位 + const details = document.getElementById('courseDetails'); + const detailsContent = document.getElementById('details-content'); + detailsContent.innerHTML = '

...

'; + details.classList.add('open'); + + // 拉课时详情 + metaRes = window.lesson; + const meta = metaRes; + + document.getElementById('course-title').textContent = meta.name || meta.material_name || lesson_name; + // 右侧面板描述/目录(简化) + detailsContent.innerHTML = `

描述

${meta.description || meta.material_description || '暂无描述'}

`; + const chapterList = document.getElementById('chapter-list'); + chapterList.innerHTML = ''; + (meta.chapters||[]).forEach(ch=>{ + const li = document.createElement('li'); + li.textContent = ch.chapter_name; + chapterList.appendChild(li); + }); + + // 保存三份链接 + CURRENT.links.lesson = meta.markdown_lesson_file_link; + CURRENT.links.prompt = meta.markdown_prompt_file_name; + CURRENT.links.score = meta.markdown_score_prompt_file_link; + console.log(CURRENT.links); + // 拉三份 Markdown + + const [t1, t2, t3] = await Promise.all([ fetch(CURRENT.links.lesson).then(r=>r.text()), fetch(CURRENT.links.prompt).then(r=>r.text()), fetch(CURRENT.links.score ).then(r=>r.text()) ]); + + + CURRENT.raw.lesson = t1; CURRENT.raw.prompt = t2; CURRENT.raw.score = t3; + CURRENT.parsed.lesson = parseHeadings(t1); + CURRENT.parsed.prompt = parseHeadings(t2); + CURRENT.parsed.score = parseHeadings(t3); + + // 构建大纲并渲染 + CURRENT.outline = buildUnifiedOutline(CURRENT.parsed.lesson, CURRENT.parsed.prompt, CURRENT.parsed.score); + renderOutline(CURRENT.outline); + + // 默认选中第一个“步骤”(若无步骤则整个文档) + const firstPhase = CURRENT.outline.phases?.[0]?.text; + const firstStep = firstPhase && CURRENT.outline.stepsByPhase[firstPhase]?.[0]; + if (firstPhase && firstStep) { + loadStepToEditors(firstPhase, firstStep); + highlightActive(firstPhase, firstStep); + } else { + // 无###时,直接把全文放到 Lesson 标签 + document.getElementById('editor-lesson').value = CURRENT.raw.lesson; + document.getElementById('editor-prompt').value = CURRENT.raw.prompt; + document.getElementById('editor-score').value = CURRENT.raw.score; + } + } + + // 渲染左侧大纲 + function renderOutline(outline) { + const tree = document.getElementById('outline-tree'); + tree.innerHTML = ''; + if (outline.theme) { + const li = document.createElement('li'); li.className='lvl1'; li.textContent = `# ${outline.theme}`; tree.appendChild(li); + } + outline.phases.forEach(ph=>{ + const li2 = document.createElement('li'); li2.className='lvl2'; li2.textContent = `## ${ph.text}`; + tree.appendChild(li2); + const steps = outline.stepsByPhase[ph.text] || []; + steps.forEach(st=>{ + const li3 = document.createElement('li'); + li3.className='lvl3'; + li3.textContent = `### ${st}`; + li3.dataset.phase = ph.text; + li3.dataset.step = st; + li3.onclick = () => { + loadStepToEditors(ph.text, st); + highlightActive(ph.text, st); + }; + tree.appendChild(li3); + }); + }); + } + + function highlightActive(phase, step) { + document.querySelectorAll('#outline-tree li').forEach(li=>li.classList.remove('active')); + const target = Array.from(document.querySelectorAll('#outline-tree li.lvl3')) + .find(li => li.dataset.phase===phase && li.dataset.step===step); + if (target) target.classList.add('active'); + } + + // 把“某个步骤”对应的三个片段载入到三个编辑器 + function loadStepToEditors(phaseText, stepText) { + // 三份文档里都取对应 phase/step 的片段(不存在则回退) + const L = CURRENT.parsed.lesson, P = CURRENT.parsed.prompt, S = CURRENT.parsed.score; + + const lessonPhase = sliceSection(L, phaseText, 2) || CURRENT.raw.lesson; + const promptPhase = sliceSection(P, phaseText, 2) || CURRENT.raw.prompt; + const scorePhase = sliceSection(S, phaseText, 2) || CURRENT.raw.score; + + // 在各 phase 片段里再找 ### step + const subL = parseHeadings(lessonPhase); + const subP = parseHeadings(promptPhase); + const subS = parseHeadings(scorePhase); + + const lText = sliceSection(subL, stepText, 3) || lessonPhase; + const pText = sliceSection(subP, stepText, 3) || promptPhase; + const sText = sliceSection(subS, stepText, 3) || scorePhase; + + document.getElementById('editor-lesson').value = lText; + document.getElementById('editor-prompt').value = pText; + document.getElementById('editor-score').value = sText; + } + + // Tab 切换 + document.addEventListener('click', (e)=>{ + if (e.target.classList.contains('tab-btn')) { + document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active')); + e.target.classList.add('active'); + const id = e.target.dataset.target; + document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active')); + document.getElementById(id).classList.add('active'); + } + }); + + // 保存全部(把当前三个编辑器的文本整体 PUT 回各自文件) + document.getElementById('save-all-btn')?.addEventListener('click', async ()=>{ + const l = document.getElementById('editor-lesson').value; + const p = document.getElementById('editor-prompt').value; + const s = document.getElementById('editor-score').value; + try { + const [r1, r2, r3] = await Promise.all([ + fetch(CURRENT.links.lesson, {method:'PUT', body:l}), + fetch(CURRENT.links.prompt, {method:'PUT', body:p}), + fetch(CURRENT.links.score , {method:'PUT', body:s}), + ]); + if (r1.ok && r2.ok && r3.ok) alert('已保存'); + else alert('保存失败,请重试'); + } catch (err) { + console.error(err); alert('保存出错'); + } + }); + + // 右侧详情开/关 + function closeCourseDetails(){ document.getElementById('courseDetails').classList.remove('open'); } + function showInputForNewChapter(){ /* 省略:你的新增逻辑 */ } diff --git a/Html/apps/static/js/teacherboard.js b/Html/apps/static/js/teacherboard.js index 72bb6c2..b1a6cf8 100644 --- a/Html/apps/static/js/teacherboard.js +++ b/Html/apps/static/js/teacherboard.js @@ -1,16 +1,18 @@ let material_id = null; -function lessonTemplate(lesson_name) { +function lessonTemplate(material_id,chapter_name,lesson_name) { return ` -
  • - ${lesson_name} - - +
  • `; } @@ -140,7 +142,7 @@ function renderChapters(courseId) { const lessonList = document.createElement('ul'); chapter.lessons.forEach(lesson => { const lessonItem = document.createElement('li'); - lessonItem.innerHTML = lessonTemplate(lesson.lesson_name); + lessonItem.innerHTML = lessonTemplate(material_id,chapter.chapter_name,lesson.lesson_name); lessonList.appendChild(lessonItem); }); chapterItem.appendChild(lessonList); @@ -195,7 +197,7 @@ function checkAndAddLesson(input, chapterTitle, addButton) { const lessonsList = chapterItem.querySelector('ul'); const lessonItem = document.createElement('span'); - lessonItem.innerHTML = lessonTemplate(lessonName); + lessonItem.innerHTML = lessonTemplate(material_id, chapterTitle, lessonName); lessonsList.appendChild(lessonItem); // 将新课时添加到章节下 input.remove(); // 删除输入框 @@ -208,19 +210,37 @@ function checkAndAddLesson(input, chapterTitle, addButton) { body: JSON.stringify({chapter_name: chapterTitle, lesson_name: lessonName}) }) } -function deleteLesson(lesson) { + +function deleteLesson(material_id,chapter_name,lesson_name) { const lessonItems = document.querySelectorAll('.lesson-item'); lessonItems.forEach(item => { - if (item.querySelector('.lesson-text').textContent === lesson) { - item.remove(); // 删除对应的课时项 + if (item.dataset.lessonName === lesson_name && item.dataset.chapterName === chapter_name && item.dataset.materialId === material_id) { + fetch(`/materials/deletelesson/${material_id}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({chapter_name: chapter_name, lesson_name: lesson_name}) + }) + .then(response => response.json()) + .then(data => { + alert(data.message); + item.remove(); // 删除对应的课时项 + renderChapters(material_id); + return; + }) + .catch(error => { + console.error('Error deleting lesson:', error); + }); } }); } -function editLesson(lesson) { +function editLesson(material_id,chapter_name,lesson_name) { // 编辑课时的逻辑 - alert("编辑课时: " + lesson); + alert("编辑课时: " + lesson_name); + window.location.href = `/api/materials/${material_id}/chapters/${chapter_name}/lessons/${lesson_name}`; } function closeCourseDetails() { diff --git a/Html/apps/templates/lesson.html b/Html/apps/templates/lesson.html new file mode 100644 index 0000000..4b98057 --- /dev/null +++ b/Html/apps/templates/lesson.html @@ -0,0 +1,77 @@ + + + + + + 课时编辑 + + + + + + + + + + + {% include 'navbar.html' %} + + + +
    +
    + + +
    +
    +

    ...

    +
    +

    课程章节列表:

    +
      +

      加载中...

      +
    + +
    + + +
    + + +
    +
    + + + + +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    + + + + + + diff --git a/Html/apps/templates/teacherboard.html b/Html/apps/templates/teacherboard.html index e4e9850..d9b49b7 100644 --- a/Html/apps/templates/teacherboard.html +++ b/Html/apps/templates/teacherboard.html @@ -124,14 +124,6 @@ - + diff --git a/Html/apps/views/__init__.py b/Html/apps/views/__init__.py index 603b95f..2057f64 100644 --- a/Html/apps/views/__init__.py +++ b/Html/apps/views/__init__.py @@ -4,9 +4,11 @@ from .vscode import bp as vscode_bp from .auth import bp as auth_bp from .dashboard import bp as main_bp from .material import bp as material_bp +from .lessons import bp as lessons_bp def register_blueprints(app: Flask): app.register_blueprint(markdown_bp) # 默认就是挂在根路径 app.register_blueprint(vscode_bp) app.register_blueprint(auth_bp) app.register_blueprint(main_bp) - app.register_blueprint(material_bp) \ No newline at end of file + app.register_blueprint(material_bp) + app.register_blueprint(lessons_bp) \ No newline at end of file diff --git a/Html/apps/views/lessons.py b/Html/apps/views/lessons.py new file mode 100644 index 0000000..3603980 --- /dev/null +++ b/Html/apps/views/lessons.py @@ -0,0 +1,41 @@ +# views/lesson.py +from flask import Blueprint, jsonify, request, render_template, session, current_app +from bson import ObjectId +from datetime import datetime +from ..auth.decorators import require_role +from ..services.course_service import load_material + +bp = Blueprint('lesson', __name__) + +# 返回课时元信息(含三份 markdown 的可访问链接或直接内容) +@bp.route('/api/materials//chapters//lessons/', methods=['GET']) +@require_role(roles="teacher") +def edit_lesson_detail(material_id, chapter_name, lesson_name): + teacher_uuid = session.get("user_uuid") + teacher_id = current_app.extensions["uuid2username"][teacher_uuid] + material = load_material(material_id) + for chapter in material.chapters: + if chapter.chapter_name == chapter_name: + for lesson in chapter.lessons: + if lesson.lesson_name == lesson_name: + return render_template('lesson.html', lesson=lesson.model_dump(), role="teacher", material=material.model_dump()) + return jsonify({"message": "课时不存在"}), 404 + +# 返回某份 markdown 文件内容(你也可以直接返回 S3/OSS 的直链) +@bp.route('/api/lesson_file////', methods=['GET']) +def get_markdown_file(material_id, chapter_name, lesson_name, filename): + # TODO: 真实环境从存储读取 + demo_map = { + "lesson.md": "# 主题\n## 阶段一\n### 步骤A\n内容A...\n### 步骤B\n内容B...\n", + "prompt.md": "# 主题\n## 阶段一\n### 步骤A\n提示A...\n### 步骤B\n提示B...\n", + "score_prompt.md": "# 主题\n## 阶段一\n### 步骤A\n评分A...\n### 步骤B\n评分B...\n" + } + return demo_map.get(filename, ""), 200 + +# 保存(更新)某一份 markdown(前端会把编辑后的文本整体传回) +@bp.route('/api/lesson_file////', methods=['PUT']) +def save_markdown_file(material_id, chapter_name, lesson_name, filename): + text = request.get_data(as_text=True) + # TODO: 写入存储,并做版本/审计 + # 返回更新时间等 + return jsonify({"saved": True, "updated_at": datetime.utcnow().isoformat()}), 200 diff --git a/Html/apps/views/material.py b/Html/apps/views/material.py index 6aa7244..c8ad272 100644 --- a/Html/apps/views/material.py +++ b/Html/apps/views/material.py @@ -1,5 +1,5 @@ from flask import Blueprint, request, jsonify, current_app, session, render_template, session -from ..services.course_service import create_material, update_material, get_materials_by_teacher, load_material, get_materials_by_teacher_dict, add_material_chapter, add_material_chapter_lesson +from ..services.course_service import create_material, update_material, get_materials_by_teacher, load_material, get_materials_by_teacher_dict, add_material_chapter, add_material_chapter_lesson, delete_material_chapter_lesson from ..auth.decorators import require_role from ..services.cos_service import upload_file bp = Blueprint('material', __name__) @@ -79,6 +79,18 @@ def add_lesson(material_id): return jsonify({"message": "课时添加成功"}), 200 return jsonify({"message": "课时添加失败"}), 404 +@bp.route('/materials/deletelesson/', methods=['DELETE']) +@require_role(roles="teacher") +def delete_lesson(material_id): + data = request.get_json() + chapter_name = data.get('chapter_name') + lesson_name = data.get('lesson_name') + teacher_uuid = session.get("user_uuid") + teacher_id = current_app.extensions["uuid2username"][teacher_uuid] + if delete_material_chapter_lesson(material_id, chapter_name, lesson_name, teacher_id): + return jsonify({"message": "课时删除成功"}), 200 + return jsonify({"message": "课时删除失败"}), 404 + @bp.route('/teacher/materials', methods=['GET']) @require_role(roles="teacher") def get_materials_by_teacher_view():