重构+优化: CR-04+06 AiChat 流式渲染改进 + CR-10 useRendered DRY 抽

- CR-04: splitBlocks 正则切块改 marked.lexer() tokens 遍历(type=code 的 .raw 整块,其余按双换行切),正确处理行首空格/4+反引号嵌套/未闭合围栏——lexer 与 parse 同 marked 实例同 gfm 规则,切块边界与渲染边界一致。
- CR-06: watch(mdReady) 翻 true 时主动 scheduleStreamParse 重算,修首屏 marked 慢+暂无 delta 致末段停留 escapeFallback 纯文本。
- CR-10: useMarkdown 抽 useRendered(computed 读 mdReady+renderMd + ensureLoaded 幂等预热),4 view(TaskDetail/ProjectDetail/Ideas/Knowledge)删手写 renderedDesc computed + onMounted loadMarkdown 重复,loadMarkdown 提模块级。DRY 行为零变化。
批4 wlnazlvdc,vue-tsc 0 err
This commit is contained in:
2026-06-15 05:40:37 +08:00
parent dfb459a8d6
commit 9bf9e12058
6 changed files with 103 additions and 71 deletions

View File

@@ -17,7 +17,7 @@
//! scheduleStreamParse)继续调用 useMarkdown 暴露的 mdReady/escapeFallback,
//! 渲染结果不变。
import { ref } from 'vue'
import { computed, ref, type ComputedRef } from 'vue'
// ═══ 模块级单例:AiChat 流式核心与历史 renderMd 共享同一份渲染器/缓存 ═══
let _marked: typeof import('marked')['marked'] | null = null
@@ -26,17 +26,17 @@ const mdReady = ref(false)
// 历史消息整段缓存(文本不变,重渲染命中省去 marked+sanitize 全量解析)
const _mdCache = new Map<string, string>()
export function useMarkdown() {
/** 异步加载 marked + DOMPurify;加载完成后 mdReady 翻转触发重渲染 */
async function loadMarkdown() {
const [{ marked }, dp] = await Promise.all([import('marked'), import('dompurify')])
marked.setOptions({ breaks: true, gfm: true })
_marked = marked
_purify = dp.default
_mdCache.clear() // 渲染器就绪,清兜底缓存触发完整重渲染
mdReady.value = true
}
/** 异步加载 marked + DOMPurify;加载完成后 mdReady 翻转触发重渲染(模块级,所有调用方共享) */
async function loadMarkdown() {
const [{ marked }, dp] = await Promise.all([import('marked'), import('dompurify')])
marked.setOptions({ breaks: true, gfm: true })
_marked = marked
_purify = dp.default
_mdCache.clear() // 渲染器就绪,清兜底缓存触发完整重渲染
mdReady.value = true
}
export function useMarkdown() {
return {
mdReady,
loadMarkdown,
@@ -50,6 +50,37 @@ export function useMarkdown() {
}
}
/**
* 详情页 Markdown 渲染辅助:封装 computed(读 mdReady 触发响应式 + renderMd(getText))
* + ensureLoaded(幂等预热 loadMarkdown)。消除 TaskDetail/ProjectDetail/Ideas/Knowledge
* 四视图手写 computed + onMounted(loadMarkdown) 重复。
*
* - getText: 返回待渲染文本(通常读响应式 source.value?.description ?? ''),
* 文本变化时 computed 重算。
* - rendered: computed<string>,mdReady 翻转/文本变化触发重渲染,行为与抽前一致。
* - ensureLoaded():幂等触发后台预热,首次调用执行 loadMarkdown,后续 no-op。
*
* 行为零变化:渲染/预热逻辑未改,仅 DRY 抽取。
*/
export function useRendered(getText: () => string): {
rendered: ComputedRef<string>
ensureLoaded: () => void
} {
const rendered = computed(() => {
void mdReady.value // 触发响应式依赖:mdReady 翻转时 computed 重算
return renderMd(getText())
})
let loaded = false
function ensureLoaded() {
if (loaded) return
loaded = true
loadMarkdown()
}
return { rendered, ensureLoaded }
}
function escapeHtml(text: string): string {
return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
}