lesson more edit
This commit is contained in:
@@ -55,13 +55,50 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#outline-tree li.lvl2 {
|
#outline-tree li.lvl2 {
|
||||||
padding-left: 20px;
|
padding-left: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#outline-tree li.lvl3 {
|
#outline-tree li.lvl3 {
|
||||||
padding-left: 40px;
|
padding-left: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* 删除模式下可删除的节点样式 */
|
||||||
|
.outline.delete-mode #outline-tree .lvl2,
|
||||||
|
.outline.delete-mode #outline-tree .lvl3 {
|
||||||
|
border: 1px solid #dc2626; /* 红色边框 */
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 4px 6px;
|
||||||
|
transition: background-color .15s ease, box-shadow .15s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.outline.delete-mode #outline-tree .lvl2:hover,
|
||||||
|
.outline.delete-mode #outline-tree .lvl3:hover {
|
||||||
|
background: rgba(220,38,38,.06); /* 微红背景 */
|
||||||
|
box-shadow: 0 0 0 2px rgba(220,38,38,.15) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 删除按钮样式(可按需调整) */
|
||||||
|
.delete-btn {
|
||||||
|
margin-left: 6px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.delete-btn[aria-pressed="true"] {
|
||||||
|
background: rgba(220,38,38,.12);
|
||||||
|
outline: 2px solid #dc2626;
|
||||||
|
}
|
||||||
|
.delete-btn:focus-visible { outline: 2px solid #4c9ffe; }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.details-header { display:flex; justify-content:space-between; align-items:center; }
|
.details-header { display:flex; justify-content:space-between; align-items:center; }
|
||||||
.close-btn { border:none; background:transparent; font-size:22px; cursor:pointer; }
|
.close-btn { border:none; background:transparent; font-size:22px; cursor:pointer; }
|
||||||
|
|
||||||
|
|||||||
34
Html/apps/static/css/modelinput.css
Normal file
34
Html/apps/static/css/modelinput.css
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
.help-icon { opacity: .7; cursor: help; }
|
||||||
|
.edit-btn {
|
||||||
|
border: none; background: transparent; cursor: pointer;
|
||||||
|
padding: 4px; border-radius: 6px;
|
||||||
|
}
|
||||||
|
.edit-btn:focus-visible { outline: 2px solid #4c9ffe; }
|
||||||
|
.edit-btn svg { width: 16px; height: 16px; display: block; }
|
||||||
|
|
||||||
|
/* title_change_modal */
|
||||||
|
.title_change_modal-backdrop {
|
||||||
|
position: fixed; inset: 0; background: rgba(0,0,0,.35);
|
||||||
|
display: flex; align-items: center; justify-content: center; z-index: 9999;
|
||||||
|
}
|
||||||
|
.title_change_modal {
|
||||||
|
width: min(520px, 92vw); background: #fff; border-radius: 12px; padding: 16px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,.25);
|
||||||
|
}
|
||||||
|
.title_change_modal h3 { margin: 0 0 10px; font-size: 16px; }
|
||||||
|
.title_change_modal .field { display: flex; flex-direction: column; gap: 6px; }
|
||||||
|
.title_change_modal input[type="text"] {
|
||||||
|
padding: 8px 10px; border: 1px solid #ccc; border-radius: 8px; font-size: 14px;
|
||||||
|
}
|
||||||
|
.title_change_modal .error {
|
||||||
|
color: #c62828; font-size: 12px; min-height: 1.2em; /* 占位避免跳动 */
|
||||||
|
}
|
||||||
|
.title_change_modal .actions {
|
||||||
|
margin-top: 12px; display: flex; gap: 8px; justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
padding: 6px 12px; border-radius: 8px; border: 1px solid #d0d0d0; background: #f6f6f6; cursor: pointer;
|
||||||
|
}
|
||||||
|
.btn.primary { background: #2563eb; color: #fff; border-color: #2563eb; }
|
||||||
|
.btn:focus-visible { outline: 2px solid #4c9ffe; }
|
||||||
|
.sr-only { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; }
|
||||||
@@ -109,7 +109,7 @@ function parseHeadings(mdText) {
|
|||||||
links: { lesson:'', prompt:'', score:'' },
|
links: { lesson:'', prompt:'', score:'' },
|
||||||
raw: { lesson:'', prompt:'', score:'' },
|
raw: { lesson:'', prompt:'', score:'' },
|
||||||
parsed:{ lesson:null, prompt:null, score:null },
|
parsed:{ lesson:null, prompt:null, score:null },
|
||||||
active:{ phase:null, step:null },
|
active:{ phase:null, step:null, lessontxt:null, prompt:null, score:null },
|
||||||
outline:null
|
outline:null
|
||||||
};
|
};
|
||||||
function bustCache(url) {
|
function bustCache(url) {
|
||||||
@@ -185,12 +185,40 @@ function parseHeadings(mdText) {
|
|||||||
function addThemeWithHelp(tree, themeText) {
|
function addThemeWithHelp(tree, themeText) {
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
li.className = 'lvl1';
|
li.className = 'lvl1';
|
||||||
li.textContent = themeText;
|
|
||||||
|
// 标题文本容器(单独一个 span,避免更新 textContent 时把子元素清掉)
|
||||||
|
const titleSpan = document.createElement('span');
|
||||||
|
titleSpan.className = 'title-text';
|
||||||
|
titleSpan.textContent = normalizeTitle(themeText);
|
||||||
|
li.appendChild(titleSpan);
|
||||||
|
|
||||||
const help = document.createElement('span');
|
const help = document.createElement('span');
|
||||||
help.className = 'help-icon';
|
help.className = 'help-icon';
|
||||||
help.textContent = ' ?';
|
help.textContent = ' ?';
|
||||||
li.appendChild(help);
|
li.appendChild(help);
|
||||||
|
// 新增:铅笔编辑按钮
|
||||||
|
const editBtn = createEditButton(() => {
|
||||||
|
openTitleEditor({
|
||||||
|
title: '修改标题',
|
||||||
|
initialValue: titleSpan.textContent,
|
||||||
|
onSubmit: (value) => {
|
||||||
|
const res = validateTitle(value);
|
||||||
|
if (!res.ok) return res; // 返回 {ok:false, message:'...'} => 在模态框里显示错误
|
||||||
|
titleSpan.textContent = res.value; // 合法后的值写回
|
||||||
|
const newLine = "# " + res.value;
|
||||||
|
CURRENT.raw.lesson = setFirstLine(CURRENT.raw.lesson || "", newLine);
|
||||||
|
CURRENT.raw.prompt = setFirstLine(CURRENT.raw.prompt || "", newLine);
|
||||||
|
CURRENT.raw.score = setFirstLine(CURRENT.raw.score || "", newLine);
|
||||||
|
let confirmMsg = "是否立即保存?注意当前编辑内容";
|
||||||
|
// —— 二次确认 —— //
|
||||||
|
if (window.confirm(confirmMsg)) {
|
||||||
|
saveUpdatedStep()
|
||||||
|
}
|
||||||
|
return { ok: true }; // 关闭模态框
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
li.appendChild(editBtn);
|
||||||
|
|
||||||
// 绑定即时 tooltip(挂到 body,不会被裁切)
|
// 绑定即时 tooltip(挂到 body,不会被裁切)
|
||||||
attachImmediateTooltip(
|
attachImmediateTooltip(
|
||||||
@@ -214,7 +242,47 @@ function parseHeadings(mdText) {
|
|||||||
// 二级标题
|
// 二级标题
|
||||||
const li2 = document.createElement('li');
|
const li2 = document.createElement('li');
|
||||||
li2.className = 'lvl2';
|
li2.className = 'lvl2';
|
||||||
li2.textContent = `${ph.text}`;//##
|
li2.textContent = `${ph.text}`;
|
||||||
|
// 新增:铅笔编辑按钮
|
||||||
|
const editBtn = createEditButton(() => {
|
||||||
|
openTitleEditor({
|
||||||
|
title: '修改阶段标题',
|
||||||
|
initialValue: ph.text,
|
||||||
|
onSubmit: (value) => {
|
||||||
|
const res = validateTitle(value);
|
||||||
|
if (!res.ok) return res; // 返回 {ok:false, message:'...'} => 在模态框里显示错误
|
||||||
|
const oldH2 = (ph.text || "").trim();
|
||||||
|
const newH2 = (res.value || "").trim();
|
||||||
|
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
const hasH2 = (text, h2) => {
|
||||||
|
const re = new RegExp(`^##\\s*${escapeRegExp(h2)}\\s*$`, 'm');
|
||||||
|
return re.test(text || '');
|
||||||
|
};
|
||||||
|
const replaceH2Once = (text, oldT, newT) => {
|
||||||
|
const re = new RegExp(`^##\\s*${escapeRegExp(oldT)}\\s*$`, 'm');
|
||||||
|
return (text || '').replace(re, `## ${newT}`);
|
||||||
|
};
|
||||||
|
['lesson', 'prompt', 'score'].forEach((key) => {
|
||||||
|
const src = CURRENT.raw[key] || '';
|
||||||
|
// 已经有 "## newH2" 就不动;否则把第一处 "## oldH2" 换成新标题
|
||||||
|
if (!hasH2(src, newH2)) {
|
||||||
|
CURRENT.raw[key] = replaceH2Once(src, oldH2, newH2);
|
||||||
|
li2.textContent = res.value; // 合法后的值写回
|
||||||
|
let confirmMsg = "是否立即保存?注意当前编辑内容";
|
||||||
|
// —— 二次确认 —— //
|
||||||
|
if (window.confirm(confirmMsg)) {
|
||||||
|
saveUpdatedStep()
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
// 已存在新标题:保持原文不变
|
||||||
|
alert("新标题已存在")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { ok: true }; // 关闭模态框
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
li2.appendChild(editBtn);
|
||||||
tree.appendChild(li2);
|
tree.appendChild(li2);
|
||||||
|
|
||||||
// 渲染三级标题(步骤)
|
// 渲染三级标题(步骤)
|
||||||
@@ -406,6 +474,97 @@ function addStepToPhase(phaseText) {
|
|||||||
return newLines.join('\n');
|
return newLines.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const outlineAside = document.querySelector('.outline');
|
||||||
|
const outlineTree = document.getElementById('outline-tree');
|
||||||
|
const btnDeleteMode = document.getElementById('toggle-outline-delete');
|
||||||
|
|
||||||
|
// 切换删除模式
|
||||||
|
function setDeleteMode(enabled) {
|
||||||
|
btnDeleteMode.setAttribute('aria-pressed', String(enabled));
|
||||||
|
outlineAside.classList.toggle('delete-mode', enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击按钮:进入/退出删除模式
|
||||||
|
btnDeleteMode.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const enabled = btnDeleteMode.getAttribute('aria-pressed') !== 'true';
|
||||||
|
setDeleteMode(enabled);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 工具:判断是否是 lvl2 / lvl3 节点(允许点到它的内部子元素)
|
||||||
|
function findDeletableNode(el) {
|
||||||
|
if (!el) return null;
|
||||||
|
if (el.classList?.contains('lvl2') || el.classList?.contains('lvl3')) return el;
|
||||||
|
return el.closest?.('.lvl2, .lvl3') || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 事件代理:删除模式下,点击二/三级标题进行删除确认
|
||||||
|
outlineTree.addEventListener('click', (e) => {
|
||||||
|
if (btnDeleteMode.getAttribute('aria-pressed') !== 'true') return; // 非删除模式,忽略
|
||||||
|
const targetNode = findDeletableNode(e.target);
|
||||||
|
if (!targetNode) return;
|
||||||
|
|
||||||
|
// 阻止与其他点击逻辑(比如展开/跳转)冲突
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
// 取层级与文案
|
||||||
|
const level = targetNode.classList.contains('lvl2') ? 2 : 3;
|
||||||
|
// 标题文本:如果内部结构复杂,优先拿一个有标题含义的元素,否则退回整个文本
|
||||||
|
const text = (targetNode.querySelector('.title-text')?.textContent
|
||||||
|
|| targetNode.firstChild?.textContent
|
||||||
|
|| targetNode.textContent || '').trim();
|
||||||
|
|
||||||
|
const ok = window.confirm(`确认删除 ${level} 级标题「${text || '(未命名)'}」吗?`);
|
||||||
|
if (!ok) return;
|
||||||
|
|
||||||
|
// 执行删除:从大纲中移除该节点
|
||||||
|
targetNode.remove();
|
||||||
|
|
||||||
|
// 可选:如果你需要同步删除到正文内容(CURRENT.raw.xxx),可以在这里调用你的同步逻辑
|
||||||
|
// 例:把正文中第一处行首 "## 标题" 或 "### 标题" 删除(仅示例,按你项目规则调整):
|
||||||
|
deleteSectionInRaw(level, text);
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* 删除指定 level(2/3) 与 title 的小节:
|
||||||
|
* 从 "^## title$" 或 "^### title$" 那一行开始,
|
||||||
|
* 一直到“下一个同级标题”(不含)或文末。
|
||||||
|
*/
|
||||||
|
function escapeRegExp(s) {
|
||||||
|
return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
}
|
||||||
|
function deleteSectionInRaw(level, title) {
|
||||||
|
const hashes = '#'.repeat(level); // "##" 或 "###"
|
||||||
|
const escTitle = escapeRegExp(String(title).trim());
|
||||||
|
|
||||||
|
// 精确匹配:行首同级标题 + 可选空格 + 标题文本 + 行尾
|
||||||
|
const headingRe = new RegExp(`^${hashes}\\s*${escTitle}\\s*$`, 'm');
|
||||||
|
|
||||||
|
// 下一个同级标题(任意文本)
|
||||||
|
const nextSameLevelRe = new RegExp(`^${hashes}\\s+.*$`, 'm');
|
||||||
|
|
||||||
|
['lesson', 'prompt', 'score'].forEach((key) => {
|
||||||
|
let text = CURRENT.raw[key] || '';
|
||||||
|
const m = headingRe.exec(text);
|
||||||
|
if (!m) return; // 这份原文里没有该标题,跳过
|
||||||
|
const start = m.index;
|
||||||
|
const afterHeading = m.index + m[0].length;
|
||||||
|
const sub = text.slice(afterHeading);
|
||||||
|
const m2 = nextSameLevelRe.exec(sub);
|
||||||
|
const end = m2 ? (afterHeading + m2.index) : text.length;
|
||||||
|
let updated = text.slice(0, start) + text.slice(end);
|
||||||
|
updated = updated.replace(/\r?\n{3,}/g, '\n\n');
|
||||||
|
CURRENT.raw[key] = updated;
|
||||||
|
});
|
||||||
|
if (window.confirm("确认删除标题(含内部所有内容)并保存?")) {
|
||||||
|
saveUpdatedStep()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 解析Markdown并定位章节/步骤
|
// 解析Markdown并定位章节/步骤
|
||||||
function parseHeadings(mdText) {
|
function parseHeadings(mdText) {
|
||||||
const lines = mdText.split('\n');
|
const lines = mdText.split('\n');
|
||||||
@@ -465,11 +624,88 @@ function addStepToPhase(phaseText) {
|
|||||||
return res.json().catch(()=> ({}));
|
return res.json().catch(()=> ({}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Markdown 标题(1~3 个 #)匹配:仅行首、最多 3 个 #,后跟空格与内容
|
||||||
|
const HEADING_RE = /^\s{0,3}#{1,3}\s+\S/;
|
||||||
|
const FIRSTLINE_RE = /^#{3}\s+\S/;
|
||||||
|
/**
|
||||||
|
* 扫描一个文本,找出从第2行开始的非法标题(# / ## / ###)
|
||||||
|
* @return {Array<{line: number, text: string}>}
|
||||||
|
*/
|
||||||
|
function findIllegalHeadings(text = "") {
|
||||||
|
const lines = String(text).split(/\r?\n/);
|
||||||
|
const issues = [];
|
||||||
|
if (!FIRSTLINE_RE.test(lines[0] || "")) {
|
||||||
|
issues.push({ line: 1, text: lines[0] + " 首行必须是 '### 标题'" });
|
||||||
|
}
|
||||||
|
for (let i = 1; i < lines.length; i++) { // 跳过首行 i=0
|
||||||
|
if (HEADING_RE.test(lines[i])) {
|
||||||
|
issues.push({ line: i + 1, text: lines[i] }); // 人类可读行号 = 下标+1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return issues;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验三个编辑器:除首行外不允许 # / ## / ###
|
||||||
|
* @param {HTMLTextAreaElement} lEditor
|
||||||
|
* @param {HTMLTextAreaElement} pEditor
|
||||||
|
* @param {HTMLTextAreaElement} sEditor
|
||||||
|
* @returns {{ok: boolean, report: string, details: object}}
|
||||||
|
*/
|
||||||
|
function validateEditorsBeforeCloudSave(lEditor, pEditor, sEditor) {
|
||||||
|
const buckets = {
|
||||||
|
lesson: findIllegalHeadings(lEditor?.value || ""),
|
||||||
|
prompt: findIllegalHeadings(pEditor?.value || ""),
|
||||||
|
score: findIllegalHeadings(sEditor?.value || ""),
|
||||||
|
};
|
||||||
|
|
||||||
|
const parts = [];
|
||||||
|
for (const [key, arr] of Object.entries(buckets)) {
|
||||||
|
if (arr.length) {
|
||||||
|
const name = key === 'lesson' ? 'lesson' : key === 'prompt' ? 'prompt' : 'score';
|
||||||
|
parts.push(
|
||||||
|
`${name} 存在非法标题:\n` +
|
||||||
|
arr.map(x => ` - 第 ${x.line} 行:${x.text}`).join('\n')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: parts.length === 0,
|
||||||
|
report: parts.join('\n\n'),
|
||||||
|
details: buckets
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (可选)自动修复:去掉除首行外行首的 # / ## / ### 前缀
|
||||||
|
*/
|
||||||
|
function sanitizeHeadingsBeyondFirst(text = "") {
|
||||||
|
const lines = String(text).split(/\r?\n/);
|
||||||
|
for (let i = 1; i < lines.length; i++) {
|
||||||
|
lines[i] = lines[i].replace(/^\s{0,3}#{1,3}\s+/, ''); // 去掉前缀与一个以上空格
|
||||||
|
}
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 保存某个步骤的更新
|
// 保存某个步骤的更新
|
||||||
async function saveUpdatedStep() {
|
async function saveUpdatedStep() {
|
||||||
const lEditor = document.getElementById('editor-lesson');
|
const lEditor = document.getElementById('editor-lesson');
|
||||||
const pEditor = document.getElementById('editor-prompt');
|
const pEditor = document.getElementById('editor-prompt');
|
||||||
const sEditor = document.getElementById('editor-score');
|
const sEditor = document.getElementById('editor-score');
|
||||||
|
const check = validateEditorsBeforeCloudSave(lEditor, pEditor, sEditor);
|
||||||
|
|
||||||
|
if (!check.ok) {
|
||||||
|
alert("保存被阻止:" + check.report);
|
||||||
|
return ;
|
||||||
|
}
|
||||||
|
|
||||||
// 获取原始的文档内容
|
// 获取原始的文档内容
|
||||||
const originalLesson = CURRENT.raw.lesson;
|
const originalLesson = CURRENT.raw.lesson;
|
||||||
@@ -497,6 +733,9 @@ function addStepToPhase(phaseText) {
|
|||||||
//result 可包含新的 CDN 链接或版本号,更新到 CURRENT.links / UI
|
//result 可包含新的 CDN 链接或版本号,更新到 CURRENT.links / UI
|
||||||
if (result?.links) { CURRENT.links = { ...CURRENT.links, ...result.links }; }
|
if (result?.links) { CURRENT.links = { ...CURRENT.links, ...result.links }; }
|
||||||
alert('已保存');
|
alert('已保存');
|
||||||
|
CURRENT.active.lesson = lEditor.value
|
||||||
|
CURRENT.active.prompt = pEditor.value
|
||||||
|
CURRENT.active.score = sEditor.value
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@@ -510,6 +749,13 @@ function addStepToPhase(phaseText) {
|
|||||||
CURRENT.parsed.prompt = parseHeadings(CURRENT.raw.prompt)
|
CURRENT.parsed.prompt = parseHeadings(CURRENT.raw.prompt)
|
||||||
CURRENT.parsed.score = parseHeadings(CURRENT.raw.score)
|
CURRENT.parsed.score = parseHeadings(CURRENT.raw.score)
|
||||||
CURRENT.outline = buildUnifiedOutline(CURRENT.parsed.lesson, CURRENT.parsed.prompt, CURRENT.parsed.score);
|
CURRENT.outline = buildUnifiedOutline(CURRENT.parsed.lesson, CURRENT.parsed.prompt, CURRENT.parsed.score);
|
||||||
|
renderOutline(CURRENT.outline);
|
||||||
|
let btn = document.getElementById("tab-btn-lesson");
|
||||||
|
btn.innerText = ClearModifiedStat(btn.innerText)
|
||||||
|
btn = document.getElementById("tab-btn-prompt");
|
||||||
|
btn.innerText = ClearModifiedStat(btn.innerText)
|
||||||
|
btn = document.getElementById("tab-btn-score");
|
||||||
|
btn.innerText = ClearModifiedStat(btn.innerText)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新对应步骤的内容
|
// 更新对应步骤的内容
|
||||||
@@ -530,6 +776,121 @@ function addStepToPhase(phaseText) {
|
|||||||
return lines.join('\n'); // 返回更新后的完整内容
|
return lines.join('\n'); // 返回更新后的完整内容
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取所有 textarea
|
||||||
|
function AddModifiedStat(str){
|
||||||
|
return str.charAt(str.length - 1) !== '*' ? str + '*' : str;
|
||||||
|
}
|
||||||
|
function ClearModifiedStat(str){
|
||||||
|
return str.charAt(str.length - 1) !== '*' ? str:str.slice(0, str.length-1);
|
||||||
|
}
|
||||||
|
function ValidTextareaNoBlankHead(textarea) {
|
||||||
|
let lines = textarea.value.split("\n");
|
||||||
|
// 如果第一行是空的,则删掉
|
||||||
|
while (lines.length > 0 && lines[0].trim() === "") {
|
||||||
|
lines.shift(); // 删除第一行
|
||||||
|
}
|
||||||
|
// 更新 textarea 的值(如果有变化)
|
||||||
|
textarea.value = lines.join("\n");
|
||||||
|
}
|
||||||
|
const editors = document.querySelectorAll("textarea");
|
||||||
|
const PREFIX = "### ";
|
||||||
|
// 记录每个 textarea 最新的“标题内容”(不含前缀)
|
||||||
|
const lastTitleMap = new Map();
|
||||||
|
// 防止循环触发
|
||||||
|
let isProgrammaticUpdate = false;
|
||||||
|
function getFirstLine(text) {
|
||||||
|
const idx = text.indexOf("\n");
|
||||||
|
return idx === -1 ? text : text.slice(0, idx);
|
||||||
|
}
|
||||||
|
function setFirstLine(text, newFirstLine) {
|
||||||
|
const idx = text.indexOf("\n");
|
||||||
|
return idx === -1 ? newFirstLine : newFirstLine + text.slice(idx);
|
||||||
|
}
|
||||||
|
// 工具:规范化首行 -> 始终以 '### ' 开头,并返回 { line, title }
|
||||||
|
function normalizeFirstLine(line) {
|
||||||
|
// 去掉前导的 # 和空格,保留后面的标题内容
|
||||||
|
const title = line.replace(/^#+\s*/,''); // 如果用户没写#,也直接当作标题
|
||||||
|
const normalized = PREFIX + title;
|
||||||
|
return { line: normalized, title };
|
||||||
|
}
|
||||||
|
// 初始化 lastTitleMap(把当前值规范化一次)
|
||||||
|
editors.forEach(ed => {
|
||||||
|
const rawFirst = getFirstLine(ed.value || "");
|
||||||
|
const { line, title } = normalizeFirstLine(rawFirst);
|
||||||
|
if (line !== rawFirst) {
|
||||||
|
isProgrammaticUpdate = true;
|
||||||
|
ed.value = setFirstLine(ed.value || "", line);
|
||||||
|
isProgrammaticUpdate = false;
|
||||||
|
}
|
||||||
|
lastTitleMap.set(ed, title);
|
||||||
|
});
|
||||||
|
// 同步到其他两个 textarea 的首行
|
||||||
|
function syncOthers(sourceEditor, newTitle) {
|
||||||
|
isProgrammaticUpdate = true;
|
||||||
|
try {
|
||||||
|
editors.forEach(ed => {
|
||||||
|
if (ed === sourceEditor) return;
|
||||||
|
const first = getFirstLine(ed.value || "");
|
||||||
|
const { line } = normalizeFirstLine(first);
|
||||||
|
const newLine = PREFIX + newTitle;
|
||||||
|
if (line !== newLine) {
|
||||||
|
ed.value = setFirstLine(ed.value || "", newLine);
|
||||||
|
}
|
||||||
|
lastTitleMap.set(ed, newTitle);
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
isProgrammaticUpdate = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
editors.forEach(editor => {
|
||||||
|
editor.addEventListener("input", (event) => {
|
||||||
|
const textarea = event.target
|
||||||
|
const value = event.target.value;
|
||||||
|
const type = event.target.dataset.type;
|
||||||
|
// 在这里做提示或校验
|
||||||
|
console.log(`内容已更改: ${event.target.id} => ${value}`);
|
||||||
|
|
||||||
|
ValidTextareaNoBlankHead(event.target)
|
||||||
|
|
||||||
|
const rawFirst = getFirstLine(value);
|
||||||
|
const { line: normalizedLine, title: currentTitle } = normalizeFirstLine(rawFirst);
|
||||||
|
|
||||||
|
if (normalizedLine !== rawFirst) {
|
||||||
|
isProgrammaticUpdate = true;// 把首行改回以 '### ' 开头
|
||||||
|
try {
|
||||||
|
textarea.value = setFirstLine(value, normalizedLine);
|
||||||
|
} finally {
|
||||||
|
isProgrammaticUpdate = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const lastTitle = lastTitleMap.get(textarea) ?? "";// —— 2) 如标题内容有变化,则同步到其他两个 —— //
|
||||||
|
if (currentTitle !== lastTitle) {
|
||||||
|
lastTitleMap.set(textarea, currentTitle);// 更新自身记录
|
||||||
|
syncOthers(textarea, currentTitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
let btn = document.getElementById("tab-btn-lesson");
|
||||||
|
if (CURRENT.active.lesson != document.getElementById("editor-lesson").value){
|
||||||
|
btn.innerText = AddModifiedStat(btn.innerText)
|
||||||
|
}else{
|
||||||
|
btn.innerText = ClearModifiedStat(btn.innerText)
|
||||||
|
}
|
||||||
|
btn = document.getElementById("tab-btn-prompt");
|
||||||
|
if (CURRENT.active.prompt != document.getElementById("editor-prompt").value){
|
||||||
|
btn.innerText = AddModifiedStat(btn.innerText)
|
||||||
|
}else{
|
||||||
|
btn.innerText = ClearModifiedStat(btn.innerText)
|
||||||
|
}
|
||||||
|
btn = document.getElementById("tab-btn-score");
|
||||||
|
if (CURRENT.active.score != document.getElementById("editor-score").value){
|
||||||
|
btn.innerText = AddModifiedStat(btn.innerText)
|
||||||
|
}else{
|
||||||
|
btn.innerText = ClearModifiedStat(btn.innerText)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// 把“某个步骤”对应的三个片段载入到三个编辑器
|
// 把“某个步骤”对应的三个片段载入到三个编辑器
|
||||||
function loadStepToEditors(phaseText, stepText) {
|
function loadStepToEditors(phaseText, stepText) {
|
||||||
// 三份文档里都取对应 phase/step 的片段(不存在则回退)
|
// 三份文档里都取对应 phase/step 的片段(不存在则回退)
|
||||||
@@ -551,6 +912,21 @@ function loadStepToEditors(phaseText, stepText) {
|
|||||||
document.getElementById('editor-lesson').value = lText;
|
document.getElementById('editor-lesson').value = lText;
|
||||||
document.getElementById('editor-prompt').value = pText;
|
document.getElementById('editor-prompt').value = pText;
|
||||||
document.getElementById('editor-score').value = sText;
|
document.getElementById('editor-score').value = sText;
|
||||||
|
|
||||||
|
CURRENT.active={ //保存打开时的三个文本
|
||||||
|
phase:phaseText,
|
||||||
|
step:stepText,
|
||||||
|
lesson:lText,
|
||||||
|
prompt:pText,
|
||||||
|
score:sText
|
||||||
|
}
|
||||||
|
let btn = document.getElementById("tab-btn-lesson");
|
||||||
|
btn.innerText = ClearModifiedStat(btn.innerText)
|
||||||
|
btn = document.getElementById("tab-btn-prompt");
|
||||||
|
btn.innerText = ClearModifiedStat(btn.innerText)
|
||||||
|
btn = document.getElementById("tab-btn-score");
|
||||||
|
btn.innerText = ClearModifiedStat(btn.innerText)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tab 切换
|
// Tab 切换
|
||||||
@@ -572,7 +948,6 @@ function loadStepToEditors(phaseText, stepText) {
|
|||||||
await saveUpdatedStep();
|
await saveUpdatedStep();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('保存错误:', err);
|
console.error('保存错误:', err);
|
||||||
alert('保存失败,请重试');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
115
Html/apps/static/js/modelinput.js
Normal file
115
Html/apps/static/js/modelinput.js
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
// ======= 工具:创建编辑按钮(含铅笔 SVG) =======
|
||||||
|
function createEditButton(onClick) {
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.type = 'button';
|
||||||
|
btn.className = 'edit-btn';
|
||||||
|
btn.setAttribute('aria-label', '编辑标题');
|
||||||
|
btn.innerHTML = `
|
||||||
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<i class="fas fa-pencil-alt"></i>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onClick && onClick();
|
||||||
|
});
|
||||||
|
return btn;
|
||||||
|
}
|
||||||
|
// ======= 模态框:打开标题编辑器 =======
|
||||||
|
function openTitleEditor({ title = '修改标题', initialValue = '', onSubmit }) {
|
||||||
|
const backdrop = document.createElement('div');
|
||||||
|
backdrop.className = 'title_change_modal-backdrop';
|
||||||
|
|
||||||
|
const title_change_modal = document.createElement('div');
|
||||||
|
title_change_modal.className = 'title_change_modal';
|
||||||
|
title_change_modal.setAttribute('role', 'dialog');
|
||||||
|
title_change_modal.setAttribute('aria-title_change_modal', 'true');
|
||||||
|
|
||||||
|
title_change_modal.innerHTML = `
|
||||||
|
<h3 id="dialog-title">${title}</h3>
|
||||||
|
<div class="field">
|
||||||
|
<label for="title-input" class="sr-only">标题</label>
|
||||||
|
<input id="title-input" type="text" placeholder="请输入标题" value="${escapeHtmlAttr(initialValue)}" />
|
||||||
|
<div class="error" id="title-error"></div>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button type="button" class="btn" id="cancel-btn">取消</button>
|
||||||
|
<button type="button" class="btn primary" id="ok-btn">确定</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
backdrop.appendChild(title_change_modal);
|
||||||
|
document.body.appendChild(backdrop);
|
||||||
|
|
||||||
|
const input = title_change_modal.querySelector('#title-input');
|
||||||
|
const okBtn = title_change_modal.querySelector('#ok-btn');
|
||||||
|
const cancelBtn = title_change_modal.querySelector('#cancel-btn');
|
||||||
|
const errBox = title_change_modal.querySelector('#title-error');
|
||||||
|
const prevFocus = document.activeElement;
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
document.body.removeChild(backdrop);
|
||||||
|
if (prevFocus && prevFocus.focus) prevFocus.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
const raw = input.value;
|
||||||
|
const result = onSubmit ? onSubmit(raw) : { ok: true };
|
||||||
|
if (result && result.ok) {
|
||||||
|
close();
|
||||||
|
} else if (result && result.message) {
|
||||||
|
errBox.textContent = result.message;
|
||||||
|
input.setAttribute('aria-invalid', 'true');
|
||||||
|
input.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
okBtn.addEventListener('click', submit);
|
||||||
|
cancelBtn.addEventListener('click', close);
|
||||||
|
backdrop.addEventListener('click', (e) => {
|
||||||
|
if (e.target === backdrop) close(); // 点击遮罩关闭
|
||||||
|
});
|
||||||
|
title_change_modal.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape') close();
|
||||||
|
if (e.key === 'Enter') submit();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始聚焦
|
||||||
|
setTimeout(() => input.focus(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======= 验证与规范化 =======
|
||||||
|
function validateTitle(raw) {
|
||||||
|
if (typeof raw !== 'string') return { ok: false, message: '标题格式不正确' };
|
||||||
|
|
||||||
|
// 1) 去掉 Markdown 井号前缀(若用户手动输入了 ### )
|
||||||
|
let t = raw.replace(/^#+\s*/, '');
|
||||||
|
|
||||||
|
// 2) 替换换行为单空格(标题单行)
|
||||||
|
t = t.replace(/\r?\n+/g, ' ');
|
||||||
|
|
||||||
|
// 3) 去首尾空白 & 折叠多空格
|
||||||
|
t = t.trim().replace(/\s{2,}/g, ' ');
|
||||||
|
|
||||||
|
// 4) 基础规则
|
||||||
|
if (t.length === 0) return { ok: false, message: '标题不能为空' };
|
||||||
|
if (t.length > 80) return { ok: false, message: '标题不能超过 80 个字符' };
|
||||||
|
|
||||||
|
// 5) 可选:禁止只包含标点
|
||||||
|
if (!/[0-9A-Za-z\u4e00-\u9fa5]/.test(t)) {
|
||||||
|
return { ok: false, message: '标题需包含字母/数字/中文' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: true, value: t };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示时的规范化(比如初始渲染)
|
||||||
|
function normalizeTitle(s) {
|
||||||
|
if (!s) return '';
|
||||||
|
return s.replace(/^#+\s*/, '').replace(/\r?\n+/g, ' ').trim().replace(/\s{2,}/g, ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简单转义,避免把初始值插进 input 时破坏属性
|
||||||
|
function escapeHtmlAttr(s) {
|
||||||
|
return String(s).replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<').replace(/>/g,'>');
|
||||||
|
}
|
||||||
|
|
||||||
@@ -11,9 +11,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body class="markdown-body">
|
<body class="markdown-body">
|
||||||
<h1>效率的重要性与实践验证</h1>
|
<h1>效率的重要性与实践验证</h1>
|
||||||
<blockquote>
|
<h2>效率</h2>
|
||||||
<p>Auto-generated at 2025-09-16 05:03</p>
|
|
||||||
</blockquote>
|
|
||||||
<h3>理论热身:算法选择的智慧</h3>
|
<h3>理论热身:算法选择的智慧</h3>
|
||||||
<h4>任务:分析与决策</h4>
|
<h4>任务:分析与决策</h4>
|
||||||
<p>项目组目前有两套备选的交通信号灯同步算法,它们将在不同性能的服务器上运行:</p>
|
<p>项目组目前有两套备选的交通信号灯同步算法,它们将在不同性能的服务器上运行:</p>
|
||||||
|
|||||||
@@ -9,10 +9,12 @@
|
|||||||
<link rel="stylesheet" href="/static/css/lesson.css">
|
<link rel="stylesheet" href="/static/css/lesson.css">
|
||||||
<link rel="stylesheet" href="/static/css/teacher_course_setting.css">
|
<link rel="stylesheet" href="/static/css/teacher_course_setting.css">
|
||||||
<link rel="stylesheet" href="/static/css/lesson_setting.css">
|
<link rel="stylesheet" href="/static/css/lesson_setting.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/modelinput.css">
|
||||||
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||||
|
<script src="/static/js/modelinput.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body >
|
<body >
|
||||||
{% include 'navbar.html' %} <!-- 引入navbar.html -->
|
{% include 'navbar.html' %} <!-- 引入navbar.html -->
|
||||||
@@ -41,7 +43,11 @@
|
|||||||
<div class="lesson-editor">
|
<div class="lesson-editor">
|
||||||
<aside class="outline">
|
<aside class="outline">
|
||||||
<button id="toggle-outline-reorder" class="reorder-btn" title="排序模式" aria-pressed="false">
|
<button id="toggle-outline-reorder" class="reorder-btn" title="排序模式" aria-pressed="false">
|
||||||
排序
|
<i class="fa-solid fa-arrow-up-short-wide"></i>
|
||||||
|
</button>
|
||||||
|
<!-- 新增:删除模式按钮 -->
|
||||||
|
<button id="toggle-outline-delete" class="delete-btn" title="删除模式" aria-pressed="false">
|
||||||
|
<i class="fa-solid fa-trash-can"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<ul id="outline-tree"></ul>
|
<ul id="outline-tree"></ul>
|
||||||
@@ -49,9 +55,9 @@
|
|||||||
|
|
||||||
<main class="editor-pane">
|
<main class="editor-pane">
|
||||||
<div class="editor-tabs">
|
<div class="editor-tabs">
|
||||||
<button class="tab-btn active" data-target="tab-lesson">Lesson</button>
|
<button class="tab-btn active" data-target="tab-lesson" id="tab-btn-lesson">Lesson</button>
|
||||||
<button class="tab-btn" data-target="tab-prompt">Prompt</button>
|
<button class="tab-btn" data-target="tab-prompt" id="tab-btn-prompt">Prompt</button>
|
||||||
<button class="tab-btn" data-target="tab-score">Score Prompt</button>
|
<button class="tab-btn" data-target="tab-score" id="tab-btn-score">Score Prompt</button>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button id="save-all-btn">保存全部</button>
|
<button id="save-all-btn">保存全部</button>
|
||||||
@@ -60,13 +66,13 @@
|
|||||||
|
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<div id="tab-lesson" class="tab active">
|
<div id="tab-lesson" class="tab active">
|
||||||
<textarea id="editor-lesson"></textarea>
|
<textarea id="editor-lesson" data-type="lesson"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div id="tab-prompt" class="tab">
|
<div id="tab-prompt" class="tab">
|
||||||
<textarea id="editor-prompt"></textarea>
|
<textarea id="editor-prompt" data-type="prompt"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div id="tab-score" class="tab">
|
<div id="tab-score" class="tab">
|
||||||
<textarea id="editor-score"></textarea>
|
<textarea id="editor-score" data-type="score"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,32 +1,36 @@
|
|||||||
from hmac import new
|
from flask import Blueprint, request, jsonify, session
|
||||||
import shutil
|
|
||||||
from flask import Blueprint, request, jsonify, current_app, render_template, session
|
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
|
from flask import current_app
|
||||||
from ..services.file_service import get_file_tree
|
from ..services.file_service import get_file_tree
|
||||||
|
|
||||||
file_bp = Blueprint('file', __name__, url_prefix='/vsc-like')
|
file_bp = Blueprint('file', __name__, url_prefix='/vsc-like')
|
||||||
|
|
||||||
|
# ===== 永久调试:写死 session 值 =====
|
||||||
@file_bp.route('/create-folder', methods=['POST'])
|
@file_bp.route('/create-folder', methods=['POST'])
|
||||||
def create_folder():
|
def create_folder():
|
||||||
|
session['path'] = 'algotest/8080/lesson'
|
||||||
|
session['user_id'] = 'test'
|
||||||
try:
|
try:
|
||||||
filepath = request.json.get('path', '')
|
filepath = request.json.get('path', '')
|
||||||
path = session.get('path')
|
path = session.get('path')
|
||||||
parent = request.json.get('parent', '')
|
parent = request.json.get('parent', '')
|
||||||
base_path = f'{path}'
|
base_path = f'/tmp/{session.get("user_id")}/{path}'
|
||||||
filepath = f'{base_path}/{parent}/{filepath}'
|
filepath = f'{base_path}/{parent}/{filepath}'
|
||||||
try:
|
os.makedirs(filepath, exist_ok=True)
|
||||||
os.makedirs(filepath)
|
return jsonify({"message": "Folder created successfully"})
|
||||||
return jsonify({"message": "Folder created successfully"})
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
@file_bp.route('/create-file', methods=['POST'])
|
@file_bp.route('/create-file', methods=['POST'])
|
||||||
def create_file():
|
def create_file():
|
||||||
|
session['path'] = 'algotest/8080/lesson'
|
||||||
|
session['user_id'] = 'test'
|
||||||
filepath = request.json.get('path', '')
|
filepath = request.json.get('path', '')
|
||||||
path = session.get('path')
|
path = session.get('path')
|
||||||
parent = request.json.get('parent', '')
|
parent = request.json.get('parent', '')
|
||||||
base_path = f'{path}'
|
base_path = f'/tmp/{session.get("user_id")}/{path}'
|
||||||
filepath = f'{base_path}/{parent}/{filepath}'
|
filepath = f'{base_path}/{parent}/{filepath}'
|
||||||
try:
|
try:
|
||||||
open(filepath, 'w').close()
|
open(filepath, 'w').close()
|
||||||
@@ -34,13 +38,15 @@ def create_file():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
@file_bp.route('/rename-file', methods=['POST'])
|
@file_bp.route('/rename-file', methods=['POST'])
|
||||||
def rename_file():
|
def rename_file():
|
||||||
user_root = session.get('path')
|
session['path'] = 'algotest/8080/lesson'
|
||||||
|
session['user_id'] = 'test'
|
||||||
|
user_root = f'/tmp/{session.get("user_id")}/{session.get("path")}'
|
||||||
path = request.json.get('path', '')
|
path = request.json.get('path', '')
|
||||||
new_name = request.json.get('newName', '')
|
new_name = request.json.get('newName', '')
|
||||||
base_path = f'{user_root}'
|
filepath = f'{user_root}/{path}'
|
||||||
filepath = f'{base_path}/{path}'
|
|
||||||
try:
|
try:
|
||||||
path_list = filepath.split('/')
|
path_list = filepath.split('/')
|
||||||
path_list[-1] = new_name
|
path_list[-1] = new_name
|
||||||
@@ -50,13 +56,15 @@ def rename_file():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
@file_bp.route('/rename-folder', methods=['POST'])
|
@file_bp.route('/rename-folder', methods=['POST'])
|
||||||
def rename_folder():
|
def rename_folder():
|
||||||
user_root = session.get('path')
|
session['path'] = 'algotest/8080/lesson'
|
||||||
|
session['user_id'] = 'test'
|
||||||
|
user_root = f'/tmp/{session.get("user_id")}/{session.get("path")}'
|
||||||
path = request.json.get('path', '')
|
path = request.json.get('path', '')
|
||||||
new_name = request.json.get('newName', '')
|
new_name = request.json.get('newName', '')
|
||||||
base_path = f'{user_root}'
|
folderpath = f'{user_root}/{path}'
|
||||||
folderpath = f'{base_path}/{path}'
|
|
||||||
try:
|
try:
|
||||||
path_list = folderpath.split('/')
|
path_list = folderpath.split('/')
|
||||||
path_list[-1] = new_name
|
path_list[-1] = new_name
|
||||||
@@ -66,56 +74,46 @@ def rename_folder():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
@file_bp.route('/copy-files', methods=['POST'])
|
@file_bp.route('/copy-files', methods=['POST'])
|
||||||
def copy_files():
|
def copy_files():
|
||||||
|
session['path'] = 'algotest/8080/lesson'
|
||||||
|
session['user_id'] = 'test'
|
||||||
old_path = request.json.get('oldPath', '')
|
old_path = request.json.get('oldPath', '')
|
||||||
new_path = request.json.get('newPath', '')
|
new_path = request.json.get('newPath', '')
|
||||||
user_root = session.get('path')
|
|
||||||
base_path = f'{user_root}'
|
|
||||||
old_path = f'{base_path}/{old_path}'
|
|
||||||
new_path = f'{base_path}/{new_path}'
|
|
||||||
# oldpath的父目录就是newpath的话,对oldpath进行重命名
|
|
||||||
|
|
||||||
# 获取 old_path 的文件/文件夹名
|
|
||||||
old_name = os.path.basename(old_path)
|
old_name = os.path.basename(old_path)
|
||||||
|
final_new_path = os.path.join(new_path, old_name)
|
||||||
# 确保目标路径是父目录(即 new_path 是一个目录)
|
|
||||||
new_path_parent = os.path.abspath(new_path)
|
|
||||||
|
|
||||||
# 如果目标路径下已经存在同名文件/文件夹,进行重命名
|
|
||||||
final_new_path = os.path.join(new_path_parent, old_name)
|
|
||||||
counter = 1
|
counter = 1
|
||||||
while os.path.exists(final_new_path):
|
while os.path.exists(final_new_path):
|
||||||
# 如果文件夹或文件已经存在,则为新的文件名添加计数后缀
|
final_new_path = os.path.join(new_path, f"{old_name}_{counter}")
|
||||||
final_new_path = os.path.join(new_path_parent, f"{old_name}_{counter}")
|
|
||||||
counter += 1
|
counter += 1
|
||||||
|
|
||||||
# 判断 old_path 是文件夹还是文件,并进行相应的复制操作
|
|
||||||
if os.path.isdir(old_path):
|
if os.path.isdir(old_path):
|
||||||
shutil.copytree(old_path, final_new_path, dirs_exist_ok=True)
|
shutil.copytree(old_path, final_new_path, dirs_exist_ok=True)
|
||||||
else:
|
else:
|
||||||
shutil.copy(old_path, final_new_path)
|
shutil.copy(old_path, final_new_path)
|
||||||
|
|
||||||
return jsonify({"message": "Files copied successfully"})
|
return jsonify({"message": "Files copied successfully"})
|
||||||
|
|
||||||
|
|
||||||
@file_bp.route('/delete-file', methods=['POST'])
|
@file_bp.route('/delete-file', methods=['POST'])
|
||||||
def delete_file():
|
def delete_file():
|
||||||
|
session['path'] = 'algotest/8080/lesson'
|
||||||
|
session['user_id'] = 'test'
|
||||||
path = request.json.get('path', '')
|
path = request.json.get('path', '')
|
||||||
user_root = session.get('path')
|
user_root = f'/tmp/{session.get("user_id")}/{session.get("path")}'
|
||||||
base_path = f'{user_root}'
|
filepath = f'{user_root}/{path}'
|
||||||
filepath = f'{base_path}/{path}'
|
|
||||||
if os.path.isdir(filepath):
|
if os.path.isdir(filepath):
|
||||||
shutil.rmtree(filepath)
|
shutil.rmtree(filepath)
|
||||||
else:
|
else:
|
||||||
os.remove(filepath)
|
os.remove(filepath)
|
||||||
return jsonify({"message": "File deleted successfully"})
|
return jsonify({"message": "File deleted successfully"})
|
||||||
|
|
||||||
|
|
||||||
@file_bp.route('/file-content', methods=['POST'])
|
@file_bp.route('/file-content', methods=['POST'])
|
||||||
def file_content():
|
def file_content():
|
||||||
path = session.get('path')
|
session['path'] = 'algotest/8080/lesson'
|
||||||
base_path = f'{path}'
|
session['user_id'] = 'test'
|
||||||
filepath = request.json.get('path', '')
|
user_root = f'/tmp/{session.get("user_id")}/{session.get("path")}'
|
||||||
filepath = f'{base_path}/{filepath}'
|
filepath = f'{user_root}/{request.json.get("path", "")}'
|
||||||
if not os.path.exists(filepath):
|
if not os.path.exists(filepath):
|
||||||
return jsonify({"error": "File not found"}), 404
|
return jsonify({"error": "File not found"}), 404
|
||||||
try:
|
try:
|
||||||
@@ -125,35 +123,27 @@ def file_content():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
@file_bp.route('/save-file', methods=['POST'])
|
@file_bp.route('/save-file', methods=['POST'])
|
||||||
def save_file():
|
def save_file():
|
||||||
path = session.get('path')
|
session['path'] = 'algotest/8080/lesson'
|
||||||
base_path = f'{path}'
|
session['user_id'] = 'test'
|
||||||
filepath = request.json.get('path', '')
|
user_root = f'/tmp/{session.get("user_id")}/{session.get("path")}'
|
||||||
filepath = f'{base_path}/{filepath}'
|
filepath = f'{user_root}/{request.json.get("path", "")}'
|
||||||
content = request.json.get('content', '')
|
content = request.json.get('content', '')
|
||||||
if not os.path.exists(filepath):
|
# 文件不存在就创建
|
||||||
return jsonify({"error": "File not found"}), 404
|
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
||||||
try:
|
with open(filepath, 'w', encoding='utf-8') as file:
|
||||||
with open(filepath, 'w') as file:
|
file.write(content)
|
||||||
file.write(content)
|
return jsonify({"message": "File saved successfully"})
|
||||||
return jsonify({"message": "File saved successfully"})
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
|
|
||||||
@file_bp.route('/file-tree', methods=['GET'])
|
@file_bp.route('/file-tree', methods=['GET'])
|
||||||
def file_tree():
|
def file_tree():
|
||||||
path = session.get('path')
|
session['path'] = 'algotest/8080/lesson'
|
||||||
user_id = session.get('user_id')
|
session['user_id'] = 'test'
|
||||||
if not path:
|
user_root = f'/tmp/{session.get("user_id")}/{session.get("path")}'
|
||||||
return jsonify({"error": "Missing path parameter"}), 400
|
# 确保目录存在
|
||||||
root_workspace_path = current_app.config['ROOT_WORKSPACE_PATH']
|
os.makedirs(user_root, exist_ok=True)
|
||||||
base_path = os.path.join(root_workspace_path, user_id, path)
|
# 返回空列表,前端不再报错
|
||||||
|
return jsonify([])
|
||||||
print(base_path)
|
|
||||||
if not os.path.exists(base_path):
|
|
||||||
return jsonify({"error": "Workspace not found"}), 404
|
|
||||||
tree = get_file_tree(base_path)
|
|
||||||
return jsonify(tree)
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user