@@ -1,4 +1,4 @@
window . addEventListener ( 'load' , ( ) => {
window . addEventListener ( 'load' , ( ) => {
console . log ( window . material ) ;
console . log ( window . material ) ;
console . log ( window . lesson ) ;
console . log ( window . lesson ) ;
editLesson ( window . material . material _id , window . chapter . chapter _name , window . lesson . lesson _name ) ;
editLesson ( window . material . material _id , window . chapter . chapter _name , window . lesson . lesson _name ) ;
@@ -12,58 +12,58 @@ function parseHeadings(mdText) {
const lines = mdText . split ( '\n' ) ;
const lines = mdText . split ( '\n' ) ;
const tokens = [ ] ;
const tokens = [ ] ;
for ( let i = 0 ; i < lines . length ; i ++ ) {
for ( let i = 0 ; i < lines . length ; i ++ ) {
const m = lines [ i ] . match ( /^(#{1,3})\s+(.*)$/ ) ;
const m = lines [ i ] . match ( /^(#{1,3})\s+(.*)$/ ) ;
if ( m ) tokens . push ( { level : m [ 1 ] . length , text : m [ 2 ] . trim ( ) , line : i } ) ;
if ( m ) tokens . push ( { level : m [ 1 ] . length , text : m [ 2 ] . trim ( ) , line : i } ) ;
}
}
// 确定每个heading的区间( 到下一个同级或上级前一行)
// 确定每个heading的区间( 到下一个同级或上级前一行)
for ( let i = 0 ; i < tokens . length ; i ++ ) {
for ( let i = 0 ; i < tokens . length ; i ++ ) {
let j = i + 1 ;
let j = i + 1 ;
while ( j < tokens . length && tokens [ j ] . level > tokens [ i ] . level ) j ++ ;
while ( j < tokens . length && tokens [ j ] . level > tokens [ i ] . level ) j ++ ;
// 找到第一个 >= 当前level 的 heading 或到文末
// 找到第一个 >= 当前level 的 heading 或到文末
let k = i + 1 ;
let k = i + 1 ;
while ( k < tokens . length && tokens [ k ] . level > tokens [ i ] . level ) k ++ ;
while ( k < tokens . length && tokens [ k ] . level > tokens [ i ] . level ) k ++ ;
const nextSameOrHigher = k < tokens . length ? tokens [ k ] . line : lines . length ;
const nextSameOrHigher = k < tokens . length ? tokens [ k ] . line : lines . length ;
tokens [ i ] . start = tokens [ i ] . line ;
tokens [ i ] . start = tokens [ i ] . line ;
tokens [ i ] . end = nextSameOrHigher - 1 ;
tokens [ i ] . end = nextSameOrHigher - 1 ;
}
}
return { lines , tokens } ;
return { lines , tokens } ;
}
}
// 从解析结果中抽取一个 heading 段落文本
// 从解析结果中抽取一个 heading 段落文本
function sliceSection ( parsed , headingText , level ) {
function sliceSection ( parsed , headingText , level ) {
const t = parsed . tokens . find ( x => x . level === level && x . text === headingText ) ;
const t = parsed . tokens . find ( x => x . level === level && x . text === headingText ) ;
if ( ! t ) return '' ;
if ( ! t ) return '' ;
return parsed . lines . slice ( t . start , t . end + 1 ) . join ( '\n' ) ;
return parsed . lines . slice ( t . start , t . end + 1 ) . join ( '\n' ) ;
}
}
// 根据三个 md, 构造统一大纲树( 一级唯一“主题”, 其下多个阶段##,其下多个步骤###)
// 根据三个 md, 构造统一大纲树( 一级唯一“主题”, 其下多个阶段##,其下多个步骤###)
function buildUnifiedOutline ( parsedLesson , parsedPrompt , parsedScore ) {
function buildUnifiedOutline ( parsedLesson , parsedPrompt , parsedScore ) {
// 取 lesson 的结构为主(也可做三者交集/并集策略,这里简单以 lesson 为基准)
// 取 lesson 的结构为主(也可做三者交集/并集策略,这里简单以 lesson 为基准)
const root1 = parsedLesson . tokens . find ( t => t . level === 1 ) ;
const root1 = parsedLesson . tokens . find ( t => t . level === 1 ) ;
const phases = parsedLesson . tokens . filter ( t => t . level === 2 ) ;
const phases = parsedLesson . tokens . filter ( t => t . level === 2 ) ;
const stepsByPhase = { } ;
const stepsByPhase = { } ;
phases . forEach ( ph => {
phases . forEach ( ph => {
stepsByPhase [ ph . text ] = parsedLesson . tokens . filter ( t => t . level === 3 && t . start > ph . start && t . end <= ph . end )
stepsByPhase [ ph . text ] = parsedLesson . tokens . filter ( t => t . level === 3 && t . start > ph . start && t . end <= ph . end )
. map ( s => s . text ) ;
. map ( s => s . text ) ;
} ) ;
} ) ;
return { theme : root1 ? root1 . text : '主题' , phases , stepsByPhase } ;
return { theme : root1 ? root1 . text : '主题' , phases , stepsByPhase } ;
}
}
let CURRENT = {
let CURRENT = {
material _id : null , chapter _name : null , lesson _name : null ,
material _id : null , chapter _name : null , lesson _name : null ,
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 , lessontxt : null , prompt : null , score : null } ,
active : { phase : null , step : null , lessontxt : null , prompt : null , score : null } ,
outline : null
outline : null
} ;
} ;
function bustCache ( url ) {
function bustCache ( url ) {
const sep = url . includes ( "?" ) ? "&" : "?" ;
const sep = url . includes ( "?" ) ? "&" : "?" ;
return url + sep + "_ts=" + Date . now ( ) ;
return url + sep + "_ts=" + Date . now ( ) ;
}
}
// 入口:从你的列表调用
// 入口:从你的列表调用
async function editLesson ( material _id , chapter _name , lesson _name ) {
async function editLesson ( material _id , chapter _name , lesson _name ) {
CURRENT . material _id = material _id ;
CURRENT . material _id = material _id ;
CURRENT . chapter _name = chapter _name ;
CURRENT . chapter _name = chapter _name ;
@@ -84,7 +84,7 @@ function parseHeadings(mdText) {
detailsContent . innerHTML = ` <h4>描述</h4><p> ${ meta . description || meta . material _description || '暂无描述' } </p> ` ;
detailsContent . innerHTML = ` <h4>描述</h4><p> ${ meta . description || meta . material _description || '暂无描述' } </p> ` ;
const chapterList = document . getElementById ( 'chapter-list' ) ;
const chapterList = document . getElementById ( 'chapter-list' ) ;
chapterList . innerHTML = '' ;
chapterList . innerHTML = '' ;
( meta . chapters || [ ] ) . forEach ( ch => {
( meta . chapters || [ ] ) . forEach ( ch => {
const li = document . createElement ( 'li' ) ;
const li = document . createElement ( 'li' ) ;
li . textContent = ch . chapter _name ;
li . textContent = ch . chapter _name ;
chapterList . appendChild ( li ) ;
chapterList . appendChild ( li ) ;
@@ -125,10 +125,10 @@ function parseHeadings(mdText) {
document . getElementById ( 'editor-prompt' ) . value = CURRENT . raw . prompt ;
document . getElementById ( 'editor-prompt' ) . value = CURRENT . raw . prompt ;
document . getElementById ( 'editor-score' ) . value = CURRENT . raw . score ;
document . getElementById ( 'editor-score' ) . value = CURRENT . raw . score ;
}
}
}
}
// ===== 在渲染一级标题时调用 =====
// ===== 在渲染一级标题时调用 =====
function addThemeWithHelp ( tree , themeText ) {
function addThemeWithHelp ( tree , themeText ) {
const li = document . createElement ( 'li' ) ;
const li = document . createElement ( 'li' ) ;
li . className = 'lvl1' ;
li . className = 'lvl1' ;
@@ -175,9 +175,9 @@ function parseHeadings(mdText) {
) ;
) ;
tree . appendChild ( li ) ;
tree . appendChild ( li ) ;
}
}
function renderOutline ( outline ) {
function renderOutline ( outline ) {
const tree = document . getElementById ( 'outline-tree' ) ;
const tree = document . getElementById ( 'outline-tree' ) ;
tree . innerHTML = '' ; // 清空大纲内容
tree . innerHTML = '' ; // 清空大纲内容
@@ -220,7 +220,7 @@ function parseHeadings(mdText) {
if ( window . confirm ( confirmMsg ) ) {
if ( window . confirm ( confirmMsg ) ) {
saveUpdatedStep ( )
saveUpdatedStep ( )
}
}
} else {
} else {
// 已存在新标题:保持原文不变
// 已存在新标题:保持原文不变
alert ( "新标题已存在" )
alert ( "新标题已存在" )
}
}
@@ -339,21 +339,21 @@ function addStepToPhase(phaseText) {
renderOutline ( CURRENT . outline ) ;
renderOutline ( CURRENT . outline ) ;
loadStepToEditors ( phaseText , stepName ) ;
loadStepToEditors ( phaseText , stepName ) ;
highlightActive ( phaseText , stepName ) ;
highlightActive ( phaseText , stepName ) ;
}
}
function highlightActive ( phase , step ) {
function highlightActive ( phase , step ) {
document . querySelectorAll ( '#outline-tree li' ) . forEach ( li => li . classList . remove ( 'active' ) ) ;
document . querySelectorAll ( '#outline-tree li' ) . forEach ( li => li . classList . remove ( 'active' ) ) ;
const target = Array . from ( document . querySelectorAll ( '#outline-tree li.lvl3' ) )
const target = Array . from ( document . querySelectorAll ( '#outline-tree li.lvl3' ) )
. find ( li => li . dataset . phase === phase && li . dataset . step === step ) ;
. find ( li => li . dataset . phase === phase && li . dataset . step === step ) ;
if ( target ) target . classList . add ( 'active' ) ;
if ( target ) target . classList . add ( 'active' ) ;
CURRENT . active . phase = phase
CURRENT . active . phase = phase
CURRENT . active . step = step
CURRENT . active . step = step
}
}
/**
/**
* 在 mdText 中,将 `### stepName` 插入到 `## phaseText` 阶段的末尾。
* 在 mdText 中,将 `### stepName` 插入到 `## phaseText` 阶段的末尾。
* 若 phase 不存在,则创建 `## phaseText` 再插入。
* 若 phase 不存在,则创建 `## phaseText` 再插入。
* skeleton 为插入文本模板(含### 标题本身及正文)。
* skeleton 为插入文本模板(含### 标题本身及正文)。
*/
*/
function insertStepIntoMarkdown ( mdText , phaseText , stepName , skeleton , { allowDuplicate = false } = { } ) {
function insertStepIntoMarkdown ( mdText , phaseText , stepName , skeleton , { allowDuplicate = false } = { } ) {
const parsed = parseHeadings ( mdText ) ;
const parsed = parseHeadings ( mdText ) ;
const { lines , tokens } = parsed ;
const { lines , tokens } = parsed ;
// 找到目标阶段(##)
// 找到目标阶段(##)
@@ -419,35 +419,35 @@ function addStepToPhase(phaseText) {
. concat ( insertChunk )
. concat ( insertChunk )
. concat ( lines . slice ( insertPos ) ) ;
. concat ( lines . slice ( insertPos ) ) ;
return newLines . join ( '\n' ) ;
return newLines . join ( '\n' ) ;
}
}
const outlineAside = document . querySelector ( '.outline' ) ;
const outlineAside = document . querySelector ( '.outline' ) ;
const outlineTree = document . getElementById ( 'outline-tree' ) ;
const outlineTree = document . getElementById ( 'outline-tree' ) ;
const btnDeleteMode = document . getElementById ( 'toggle-outline-delete' ) ;
const btnDeleteMode = document . getElementById ( 'toggle-outline-delete' ) ;
// 切换删除模式
// 切换删除模式
function setDeleteMode ( enabled ) {
function setDeleteMode ( enabled ) {
btnDeleteMode . setAttribute ( 'aria-pressed' , String ( enabled ) ) ;
btnDeleteMode . setAttribute ( 'aria-pressed' , String ( enabled ) ) ;
outlineAside . classList . toggle ( 'delete-mode' , enabled ) ;
outlineAside . classList . toggle ( 'delete-mode' , enabled ) ;
}
}
// 点击按钮:进入/退出删除模式
// 点击按钮:进入/退出删除模式
btnDeleteMode . addEventListener ( 'click' , ( e ) => {
btnDeleteMode . addEventListener ( 'click' , ( e ) => {
e . preventDefault ( ) ;
e . preventDefault ( ) ;
const enabled = btnDeleteMode . getAttribute ( 'aria-pressed' ) !== 'true' ;
const enabled = btnDeleteMode . getAttribute ( 'aria-pressed' ) !== 'true' ;
setDeleteMode ( enabled ) ;
setDeleteMode ( enabled ) ;
} ) ;
} ) ;
// 工具:判断是否是 lvl2 / lvl3 节点(允许点到它的内部子元素)
// 工具:判断是否是 lvl2 / lvl3 节点(允许点到它的内部子元素)
function findDeletableNode ( el ) {
function findDeletableNode ( el ) {
if ( ! el ) return null ;
if ( ! el ) return null ;
if ( el . classList ? . contains ( 'lvl2' ) || el . classList ? . contains ( 'lvl3' ) ) return el ;
if ( el . classList ? . contains ( 'lvl2' ) || el . classList ? . contains ( 'lvl3' ) ) return el ;
return el . closest ? . ( '.lvl2, .lvl3' ) || null ;
return el . closest ? . ( '.lvl2, .lvl3' ) || null ;
}
}
// 事件代理:删除模式下,点击二/三级标题进行删除确认
// 事件代理:删除模式下,点击二/三级标题进行删除确认
outlineTree . addEventListener ( 'click' , ( e ) => {
outlineTree . addEventListener ( 'click' , ( e ) => {
if ( btnDeleteMode . getAttribute ( 'aria-pressed' ) !== 'true' ) return ; // 非删除模式,忽略
if ( btnDeleteMode . getAttribute ( 'aria-pressed' ) !== 'true' ) return ; // 非删除模式,忽略
const targetNode = findDeletableNode ( e . target ) ;
const targetNode = findDeletableNode ( e . target ) ;
if ( ! targetNode ) return ;
if ( ! targetNode ) return ;
@@ -472,7 +472,7 @@ function addStepToPhase(phaseText) {
// 可选: 如果你需要同步删除到正文内容( CURRENT.raw.xxx) , 可以在这里调用你的同步逻辑
// 可选: 如果你需要同步删除到正文内容( CURRENT.raw.xxx) , 可以在这里调用你的同步逻辑
// 例:把正文中第一处行首 "## 标题" 或 "### 标题" 删除(仅示例,按你项目规则调整):
// 例:把正文中第一处行首 "## 标题" 或 "### 标题" 删除(仅示例,按你项目规则调整):
deleteSectionInRaw ( level , text ) ;
deleteSectionInRaw ( level , text ) ;
} ) ;
} ) ;
/**
/**
* 删除指定 level(2/3) 与 title 的小节:
* 删除指定 level(2/3) 与 title 的小节:
* 从 "^## title$" 或 "^### title$" 那一行开始,
* 从 "^## title$" 或 "^### title$" 那一行开始,
@@ -512,8 +512,8 @@ function deleteSectionInRaw(level, title) {
// 解析Markdown并定位章节/步骤
// 解析Markdown并定位章节/步骤
function parseHeadings ( mdText ) {
function parseHeadings ( mdText ) {
const lines = mdText . split ( '\n' ) ;
const lines = mdText . split ( '\n' ) ;
const tokens = [ ] ;
const tokens = [ ] ;
for ( let i = 0 ; i < lines . length ; i ++ ) {
for ( let i = 0 ; i < lines . length ; i ++ ) {
@@ -531,19 +531,27 @@ function deleteSectionInRaw(level, title) {
tokens [ i ] . end = nextSameOrHigher - 1 ;
tokens [ i ] . end = nextSameOrHigher - 1 ;
}
}
return { lines , tokens } ;
return { lines , tokens } ;
}
}
// 从解析结果中抽取一个 heading 段落文本
// 从解析结果中抽取一个 heading 段落文本
function sliceSection ( parsed , headingText , level ) {
function sliceSection ( parsed , headingText , level ) {
const t = parsed . tokens . find ( x => x . level === level && x . text === headingText ) ;
const t = parsed . tokens . find ( x => x . level === level && x . text === headingText ) ;
if ( ! t ) return '' ;
if ( ! t ) return '' ;
return parsed . lines . slice ( t . start , t . end + 1 ) . join ( '\n' ) ;
return parsed . lines . slice ( t . start , t . end + 1 ) . join ( '\n' ) ;
}
}
function buildOriginSaveUrl ( ) {
return ` /api/materials/save/ ${ encodeURIComponent ( CURRENT . material _id ) } / ${ encodeURIComponent ( CURRENT . chapter _name ) } / ${ encodeURIComponent ( CURRENT . lesson _name ) } ` ;
}
/**
// 加载某个步骤到编辑器
function buildOriginSaveUrl ( ) {
const mid = typeof CURRENT . material _id === 'string' && CURRENT . material _id
|| ( window . material && window . material . material _id ) || '' ;
const ch = typeof CURRENT . chapter _name === 'string' && CURRENT . chapter _name
|| ( window . chapter && window . chapter . chapter _name ) || '' ;
const ln = typeof CURRENT . lesson _name === 'string' && CURRENT . lesson _name
|| ( window . lesson && window . lesson . lesson _name ) || '' ;
return ` /api/materials/save/ ${ encodeURIComponent ( mid ) } / ${ encodeURIComponent ( ch ) } / ${ encodeURIComponent ( ln ) } ` ;
}
/**
* 把三份文档( lesson/prompt/score) 的 "CDN 外链 + 原文内容" 一次性提交给源站。
* 把三份文档( lesson/prompt/score) 的 "CDN 外链 + 原文内容" 一次性提交给源站。
* @param {Object} payload 形如:
* @param {Object} payload 形如:
* {
* {
@@ -552,7 +560,7 @@ function deleteSectionInRaw(level, title) {
* score: { cdn_link, content }
* score: { cdn_link, content }
* }
* }
*/
*/
async function saveToOrigin ( payload ) {
async function saveToOrigin ( payload ) {
const res = await fetch ( buildOriginSaveUrl ( ) , {
const res = await fetch ( buildOriginSaveUrl ( ) , {
method : 'POST' ,
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
headers : { 'Content-Type' : 'application/json' } ,
@@ -565,11 +573,11 @@ function deleteSectionInRaw(level, title) {
credentials : 'include' // 若需要带 cookie/会话,保留;不需要可移除
credentials : 'include' // 若需要带 cookie/会话,保留;不需要可移除
} ) ;
} ) ;
if ( ! res . ok ) {
if ( ! res . ok ) {
const text = await res . text ( ) . catch ( ( ) => '' ) ;
const text = await res . text ( ) . catch ( ( ) => '' ) ;
throw new Error ( ` 保存失败( ${ res . status } ): ${ text } ` ) ;
throw new Error ( ` 保存失败( ${ res . status } ): ${ text } ` ) ;
}
}
return res . json ( ) . catch ( ( ) => ( { } ) ) ;
return res . json ( ) . catch ( ( ) => ( { } ) ) ;
}
}
@@ -642,8 +650,8 @@ function sanitizeHeadingsBeyondFirst(text = "") {
// 保存某个步骤的更新
// 保存某个步骤的更新
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' ) ;
@@ -651,7 +659,7 @@ function sanitizeHeadingsBeyondFirst(text = "") {
if ( ! check . ok ) {
if ( ! check . ok ) {
alert ( "保存被阻止:" + check . report ) ;
alert ( "保存被阻止:" + check . report ) ;
return ;
return ;
}
}
// 获取原始的文档内容
// 获取原始的文档内容
@@ -707,10 +715,10 @@ function sanitizeHeadingsBeyondFirst(text = "") {
btn . innerText = ClearModifiedStat ( btn . innerText )
btn . innerText = ClearModifiedStat ( btn . innerText )
btn = document . getElementById ( "tab-btn-score" ) ;
btn = document . getElementById ( "tab-btn-score" ) ;
btn . innerText = ClearModifiedStat ( btn . innerText )
btn . innerText = ClearModifiedStat ( btn . innerText )
}
}
// 更新对应步骤的内容
// 更新对应步骤的内容
function updateMarkdownContent ( parsed , phaseText , stepText , newContent ) {
function updateMarkdownContent ( parsed , phaseText , stepText , newContent ) {
const phaseIndex = parsed . tokens . findIndex ( token => token . level === 2 && token . text === phaseText ) ;
const phaseIndex = parsed . tokens . findIndex ( token => token . level === 2 && token . text === phaseText ) ;
if ( phaseIndex === - 1 ) return parsed . lines . join ( '\n' ) ; // 找不到阶段
if ( phaseIndex === - 1 ) return parsed . lines . join ( '\n' ) ; // 找不到阶段
@@ -730,14 +738,14 @@ function sanitizeHeadingsBeyondFirst(text = "") {
lines . splice ( startLine , endLine - startLine + 1 , newContent ) ; // 用新内容替换
lines . splice ( startLine , endLine - startLine + 1 , newContent ) ; // 用新内容替换
return lines . join ( '\n' ) ; // 返回更新后的完整内容
return lines . join ( '\n' ) ; // 返回更新后的完整内容
}
}
// 获取所有 textarea
// 获取所有 textarea
function AddModifiedStat ( str ) {
function AddModifiedStat ( str ) {
return str . charAt ( str . length - 1 ) !== '*' ? str + '*' : str ;
return str . charAt ( str . length - 1 ) !== '*' ? str + '*' : str ;
}
}
function ClearModifiedStat ( str ) {
function ClearModifiedStat ( str ) {
return str . charAt ( str . length - 1 ) !== '*' ? str : str . slice ( 0 , str . length - 1 ) ;
return str . charAt ( str . length - 1 ) !== '*' ? str : str . slice ( 0 , str . length - 1 ) ;
}
}
function ValidTextareaNoBlankHead ( textarea ) {
function ValidTextareaNoBlankHead ( textarea ) {
let lines = textarea . value . split ( "\n" ) ;
let lines = textarea . value . split ( "\n" ) ;
@@ -753,25 +761,25 @@ const PREFIX = "### ";
// 记录每个 textarea 最新的“标题内容”(不含前缀)
// 记录每个 textarea 最新的“标题内容”(不含前缀)
const lastTitleMap = new Map ( ) ;
const lastTitleMap = new Map ( ) ;
// 防止循环触发
// 防止循环触发
let isProgrammaticUpdate = false ;
let isProgrammaticUpdate = false ;
function getFirstLine ( text ) {
function getFirstLine ( text ) {
const idx = text . indexOf ( "\n" ) ;
const idx = text . indexOf ( "\n" ) ;
return idx === - 1 ? text : text . slice ( 0 , idx ) ;
return idx === - 1 ? text : text . slice ( 0 , idx ) ;
}
}
function setFirstLine ( text , newFirstLine ) {
function setFirstLine ( text , newFirstLine ) {
const idx = text . indexOf ( "\n" ) ;
const idx = text . indexOf ( "\n" ) ;
return idx === - 1 ? newFirstLine : newFirstLine + text . slice ( idx ) ;
return idx === - 1 ? newFirstLine : newFirstLine + text . slice ( idx ) ;
}
}
// 工具:规范化首行 -> 始终以 '### ' 开头,并返回 { line, title }
// 工具:规范化首行 -> 始终以 '### ' 开头,并返回 { line, title }
function normalizeFirstLine ( line ) {
function normalizeFirstLine ( line ) {
// 去掉前导的 # 和空格,保留后面的标题内容
// 去掉前导的 # 和空格,保留后面的标题内容
const title = line . replace ( /^#+\s*/ , '' ) ; // 如果用户没写#,也直接当作标题
const title = line . replace ( /^#+\s*/ , '' ) ; // 如果用户没写#,也直接当作标题
const normalized = PREFIX + title ;
const normalized = PREFIX + title ;
return { line : normalized , title } ;
return { line : normalized , title } ;
}
}
// 初始化 lastTitleMap( 把当前值规范化一次)
// 初始化 lastTitleMap( 把当前值规范化一次)
editorsDoc . forEach ( ed => {
editorsDoc . forEach ( ed => {
const rawFirst = getFirstLine ( ed . value || "" ) ;
const rawFirst = getFirstLine ( ed . value || "" ) ;
const { line , title } = normalizeFirstLine ( rawFirst ) ;
const { line , title } = normalizeFirstLine ( rawFirst ) ;
if ( line !== rawFirst ) {
if ( line !== rawFirst ) {
@@ -780,9 +788,9 @@ const lastTitleMap = new Map();
isProgrammaticUpdate = false ;
isProgrammaticUpdate = false ;
}
}
lastTitleMap . set ( ed , title ) ;
lastTitleMap . set ( ed , title ) ;
} ) ;
} ) ;
// 同步到其他两个 textarea 的首行
// 同步到其他两个 textarea 的首行
function syncOthers ( sourceEditor , newTitle ) {
function syncOthers ( sourceEditor , newTitle ) {
isProgrammaticUpdate = true ;
isProgrammaticUpdate = true ;
try {
try {
editorsDoc . forEach ( ed => {
editorsDoc . forEach ( ed => {
@@ -798,7 +806,7 @@ const lastTitleMap = new Map();
} finally {
} finally {
isProgrammaticUpdate = false ;
isProgrammaticUpdate = false ;
}
}
}
}
// ==== 前置:你的三编辑器实例与 id 映射(按你实际为准)====
// ==== 前置:你的三编辑器实例与 id 映射(按你实际为准)====
const ID _LESSON = 'editor-lesson' ;
const ID _LESSON = 'editor-lesson' ;
@@ -1018,12 +1026,12 @@ function loadStepToEditors(phaseText, stepText) {
editors [ 'editor-prompt' ] . value ( pText ) ;
editors [ 'editor-prompt' ] . value ( pText ) ;
editors [ 'editor-score' ] . value ( sText ) ;
editors [ 'editor-score' ] . value ( sText ) ;
CURRENT . active = { //保存打开时的三个文本
CURRENT . active = { //保存打开时的三个文本
phase : phaseText ,
phase : phaseText ,
step : stepText ,
step : stepText ,
lesson : lText ,
lesson : lText ,
prompt : pText ,
prompt : pText ,
score : sText
score : sText
}
}
let btn = document . getElementById ( "tab-btn-lesson" ) ;
let btn = document . getElementById ( "tab-btn-lesson" ) ;
btn . innerText = ClearModifiedStat ( btn . innerText )
btn . innerText = ClearModifiedStat ( btn . innerText )
@@ -1045,9 +1053,9 @@ document.addEventListener('click', (e) => {
// 广播 tab 变更(告诉别人当前激活的 tab id / 以及要用哪个 editorId)
// 广播 tab 变更(告诉别人当前激活的 tab id / 以及要用哪个 editorId)
const editorIdMap = {
const editorIdMap = {
'tab-lesson' : 'editor-lesson' ,
'tab-lesson' : 'editor-lesson' ,
'tab-prompt' : 'editor-prompt' ,
'tab-prompt' : 'editor-prompt' ,
'tab-score' : 'editor-score' ,
'tab-score' : 'editor-score' ,
} ;
} ;
const activeBtnId = e . target . id ; // 如 'tab-btn-lesson'
const activeBtnId = e . target . id ; // 如 'tab-btn-lesson'
const editorId =
const editorId =
@@ -1061,8 +1069,8 @@ document.addEventListener('click', (e) => {
}
}
} ) ;
} ) ;
// 保存全部:将三个编辑器中的文本保存回相应的 markdown 文件
// 保存全部:将三个编辑器中的文本保存回相应的 markdown 文件
document . getElementById ( 'save-all-btn' ) ? . addEventListener ( 'click' , async ( ) => {
document . getElementById ( 'save-all-btn' ) ? . addEventListener ( 'click' , async ( ) => {
try {
try {
// 将各自的步骤更新保存回文件
// 将各自的步骤更新保存回文件
@@ -1070,10 +1078,10 @@ document.addEventListener('click', (e) => {
} catch ( err ) {
} catch ( err ) {
console . error ( '保存错误:' , err ) ;
console . error ( '保存错误:' , err ) ;
}
}
} ) ;
} ) ;
const STEP _TEMPLATES = {
const STEP _TEMPLATES = {
lesson : ` 在这里编写本步骤的课程内容... ` ,
lesson : ` 在这里编写本步骤的课程内容... ` ,
prompt : ` #### 指令
prompt : ` #### 指令
请严格遵循本步骤的教学目标,输出所需内容。
请严格遵循本步骤的教学目标,输出所需内容。
@@ -1093,8 +1101,8 @@ document.addEventListener('click', (e) => {
#### 打分
#### 打分
请输出 JSON:
请输出 JSON:
{"score": 0-100, "reasons": ["...","..."]} `
{"score": 0-100, "reasons": ["...","..."]} `
} ;
} ;
const PHASE _TEMPLATES = {
const PHASE _TEMPLATES = {
lesson : ` ### 新步骤
lesson : ` ### 新步骤
本阶段描述:请在此补充阶段说明。
本阶段描述:请在此补充阶段说明。
@@ -1112,7 +1120,7 @@ document.addEventListener('click', (e) => {
- 维度A: ...
- 维度A: ...
- 维度B: ...
- 维度B: ...
`
`
} ;
} ;
@@ -1174,7 +1182,7 @@ function buildGlobalStepIndex(lines, tokens) {
const phases = tokens . filter ( t => t . level === 2 ) ;
const phases = tokens . filter ( t => t . level === 2 ) ;
for ( let i = 0 ; i < phases . length ; i ++ ) {
for ( let i = 0 ; i < phases . length ; i ++ ) {
const ph = phases [ i ] ;
const ph = phases [ i ] ;
const phNextStart = ( i + 1 < phases . length ) ? phases [ i + 1 ] . start : lines . length ;
const phNextStart = ( i + 1 < phases . length ) ? phases [ i + 1 ] . start : lines . length ;
// 该阶段内的所有 ###:
// 该阶段内的所有 ###:
const steps = tokens . filter ( t => t . level === 3 && t . start > ph . start && t . start < phNextStart ) ;
const steps = tokens . filter ( t => t . level === 3 && t . start > ph . start && t . start < phNextStart ) ;
for ( const st of steps ) {
for ( const st of steps ) {
@@ -1186,7 +1194,7 @@ function buildGlobalStepIndex(lines, tokens) {
}
}
// ===== 选择器与小工具 =====
// ===== 选择器与小工具 =====
const treeEl = document . getElementById ( 'outline-tree' ) ;
const treeEl = document . getElementById ( 'outline-tree' ) ;
const toggleOutlineBtn = document . getElementById ( 'toggle-outline-reorder' ) ;
const toggleOutlineBtn = document . getElementById ( 'toggle-outline-reorder' ) ;