重构+优化: 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:
@@ -353,22 +353,34 @@ const streamingHtml = ref('')
|
|||||||
let rafId: number | null = null
|
let rafId: number | null = null
|
||||||
let lastStreamText = ''
|
let lastStreamText = ''
|
||||||
|
|
||||||
/// 切块:代码围栏(```...```)整体一块(跨双换行不切),非代码段按双换行切段。
|
/// 切块:代码围栏(```...``` 或 3+ 反引号 + 行首可选空格 + info 字符串)整体一块
|
||||||
|
/// (跨双换行不切),非代码段按双换行切段。
|
||||||
/// 流式期末块可能不完整(未闭合围栏/半截段落),交给 parseBlockNoCache 每次重 parse。
|
/// 流式期末块可能不完整(未闭合围栏/半截段落),交给 parseBlockNoCache 每次重 parse。
|
||||||
|
///
|
||||||
|
/// CR-260615-04:旧实现用正则 /```[^\n]*\n[\s\S]*?(?:```|$)/ 切块,与 marked 围栏规则
|
||||||
|
/// 不一致——不要求行首、固定 3 反引号,导致行中裸三反引号 / 4+ 反引号嵌套围栏切错,
|
||||||
|
/// 前块缓存固化错误 html。改用 marked.lexer()(经 getMarked() 取 marked 实例)做围栏
|
||||||
|
/// 感知切块:lexer 把代码围栏识别为单个 type==='code' token(.raw 含整段围栏),
|
||||||
|
/// 段落/标题等其余 token 的 .raw 按 \n\n 切段。lexer 与 parse 用同一份 marked 实例 +
|
||||||
|
/// 同一份围栏规则,切块边界与渲染边界一致。
|
||||||
function splitBlocks(text: string): string[] {
|
function splitBlocks(text: string): string[] {
|
||||||
|
const marked = getMarked()
|
||||||
const blocks: string[] = []
|
const blocks: string[] = []
|
||||||
const fenceRe = /```[^\n]*\n[\s\S]*?(?:```|$)/g
|
// marked 未就绪时 splitBlocks 不应被调用(renderStreamingMd 已 mdReady 守卫),
|
||||||
let last = 0
|
// 此处兜底返回原文,不引入正则。
|
||||||
let m: RegExpExecArray | null
|
if (!marked) return [text]
|
||||||
while ((m = fenceRe.exec(text)) !== null) {
|
const tokens = marked.lexer(text)
|
||||||
if (m.index > last) {
|
for (const tok of tokens) {
|
||||||
for (const b of text.slice(last, m.index).split(/\n{2,}/)) if (b.trim()) blocks.push(b)
|
if (!tok || typeof (tok as { raw?: string }).raw !== 'string') continue
|
||||||
|
const raw = (tok as { raw: string }).raw
|
||||||
|
if ((tok as { type: string }).type === 'code') {
|
||||||
|
// 代码围栏 token:整段一块(含行首可选空格 + 3+ 反引号 + info 字符串)
|
||||||
|
if (raw.trim()) blocks.push(raw)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
blocks.push(m[0])
|
// 非代码 token(paragraph/space/heading/list/blockquote/...):
|
||||||
last = m.index + m[0].length
|
// 按 \n\n 切段(段落内单换行不切,inline code 自然保留在段内)
|
||||||
}
|
for (const b of raw.split(/\n{2,}/)) if (b.trim()) blocks.push(b)
|
||||||
if (last < text.length) {
|
|
||||||
for (const b of text.slice(last).split(/\n{2,}/)) if (b.trim()) blocks.push(b)
|
|
||||||
}
|
}
|
||||||
return blocks.length ? blocks : [text]
|
return blocks.length ? blocks : [text]
|
||||||
}
|
}
|
||||||
@@ -718,9 +730,16 @@ watch(() => store.state.currentText, onContentChange)
|
|||||||
// 流式 Markdown 渲染驱动(ARC-260615-08 块级 memo):
|
// 流式 Markdown 渲染驱动(ARC-260615-08 块级 memo):
|
||||||
// - currentText 变化(每 delta):rAF 节流 scheduleStreamParse,块级 memo 只重 parse 末块
|
// - currentText 变化(每 delta):rAF 节流 scheduleStreamParse,块级 memo 只重 parse 末块
|
||||||
// - streaming 翻转:结束清 rAF + 清 streamingHtml(回 renderMd 走 final marked + 缓存)
|
// - streaming 翻转:结束清 rAF + 清 streamingHtml(回 renderMd 走 final marked + 缓存)
|
||||||
|
// - mdReady 翻转(CR-260615-06):首屏 marked 慢 + 暂无新 delta 时末段停留 escapeFallback
|
||||||
|
// 纯文本;marked 就绪后主动重算一次,使纯文本转格式化
|
||||||
watch(() => store.state.currentText, (text) => {
|
watch(() => store.state.currentText, (text) => {
|
||||||
if (store.state.streaming && text) scheduleStreamParse(text)
|
if (store.state.streaming && text) scheduleStreamParse(text)
|
||||||
})
|
})
|
||||||
|
watch(mdReady, (ready) => {
|
||||||
|
if (ready && store.state.streaming && store.state.currentText) {
|
||||||
|
scheduleStreamParse(store.state.currentText)
|
||||||
|
}
|
||||||
|
})
|
||||||
watch(() => store.state.streaming, (s) => {
|
watch(() => store.state.streaming, (s) => {
|
||||||
if (rafId !== null) { cancelAnimationFrame(rafId); rafId = null }
|
if (rafId !== null) { cancelAnimationFrame(rafId); rafId = null }
|
||||||
if (!s) { streamingHtml.value = ''; lastStreamText = '' }
|
if (!s) { streamingHtml.value = ''; lastStreamText = '' }
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
//! scheduleStreamParse)继续调用 useMarkdown 暴露的 mdReady/escapeFallback,
|
//! scheduleStreamParse)继续调用 useMarkdown 暴露的 mdReady/escapeFallback,
|
||||||
//! 渲染结果不变。
|
//! 渲染结果不变。
|
||||||
|
|
||||||
import { ref } from 'vue'
|
import { computed, ref, type ComputedRef } from 'vue'
|
||||||
|
|
||||||
// ═══ 模块级单例:AiChat 流式核心与历史 renderMd 共享同一份渲染器/缓存 ═══
|
// ═══ 模块级单例:AiChat 流式核心与历史 renderMd 共享同一份渲染器/缓存 ═══
|
||||||
let _marked: typeof import('marked')['marked'] | null = null
|
let _marked: typeof import('marked')['marked'] | null = null
|
||||||
@@ -26,17 +26,17 @@ const mdReady = ref(false)
|
|||||||
// 历史消息整段缓存(文本不变,重渲染命中省去 marked+sanitize 全量解析)
|
// 历史消息整段缓存(文本不变,重渲染命中省去 marked+sanitize 全量解析)
|
||||||
const _mdCache = new Map<string, string>()
|
const _mdCache = new Map<string, string>()
|
||||||
|
|
||||||
export function useMarkdown() {
|
/** 异步加载 marked + DOMPurify;加载完成后 mdReady 翻转触发重渲染(模块级,所有调用方共享) */
|
||||||
/** 异步加载 marked + DOMPurify;加载完成后 mdReady 翻转触发重渲染 */
|
async function loadMarkdown() {
|
||||||
async function loadMarkdown() {
|
const [{ marked }, dp] = await Promise.all([import('marked'), import('dompurify')])
|
||||||
const [{ marked }, dp] = await Promise.all([import('marked'), import('dompurify')])
|
marked.setOptions({ breaks: true, gfm: true })
|
||||||
marked.setOptions({ breaks: true, gfm: true })
|
_marked = marked
|
||||||
_marked = marked
|
_purify = dp.default
|
||||||
_purify = dp.default
|
_mdCache.clear() // 渲染器就绪,清兜底缓存触发完整重渲染
|
||||||
_mdCache.clear() // 渲染器就绪,清兜底缓存触发完整重渲染
|
mdReady.value = true
|
||||||
mdReady.value = true
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
export function useMarkdown() {
|
||||||
return {
|
return {
|
||||||
mdReady,
|
mdReady,
|
||||||
loadMarkdown,
|
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 {
|
function escapeHtml(text: string): string {
|
||||||
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -214,19 +214,9 @@ import { useProjectStore } from '../stores/project'
|
|||||||
import { formatDate } from '../utils/time'
|
import { formatDate } from '../utils/time'
|
||||||
import ConfirmDialog from '../components/ConfirmDialog.vue'
|
import ConfirmDialog from '../components/ConfirmDialog.vue'
|
||||||
import { useConfirm } from '../composables/useConfirm'
|
import { useConfirm } from '../composables/useConfirm'
|
||||||
import { useMarkdown } from '../composables/useMarkdown'
|
import { useRendered } from '../composables/useMarkdown'
|
||||||
import type { IdeaRecord } from '../api/types'
|
import type { IdeaRecord } from '../api/types'
|
||||||
|
|
||||||
// B-260615-25:灵感描述 Markdown 渲染(复用 AiChat/TaskDetail 同款渲染器,模块级单例)
|
|
||||||
const { renderMd, loadMarkdown, mdReady } = useMarkdown()
|
|
||||||
|
|
||||||
// 描述渲染依赖 mdReady 响应式:loadMarkdown 完成前 escapeFallback 纯文本,完成后 mdReady 翻转
|
|
||||||
// 触发 computed 重算(B-25 漏响应式致 v-html=renderMd 首次纯文本后不重算,代码块不渲染)
|
|
||||||
const renderedDesc = computed(() => {
|
|
||||||
void mdReady.value
|
|
||||||
return renderMd(currentIdea.value?.description ?? '')
|
|
||||||
})
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const store = useProjectStore()
|
const store = useProjectStore()
|
||||||
@@ -295,6 +285,12 @@ const currentIdea = computed(() => {
|
|||||||
return store.ideas.find(i => i.id === selectedId.value) ?? null
|
return store.ideas.find(i => i.id === selectedId.value) ?? null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// B-260615-25:灵感描述 Markdown 渲染(复用 AiChat/TaskDetail 同款渲染器,模块级单例),
|
||||||
|
// useRendered 封装 computed(读 mdReady 触发响应式 + renderMd)+ ensureLoaded(幂等预热)
|
||||||
|
const { rendered: renderedDesc, ensureLoaded } = useRendered(
|
||||||
|
() => currentIdea.value?.description ?? '',
|
||||||
|
)
|
||||||
|
|
||||||
const currentStatus = computed(() => {
|
const currentStatus = computed(() => {
|
||||||
return currentIdea.value?.status || 'draft'
|
return currentIdea.value?.status || 'draft'
|
||||||
})
|
})
|
||||||
@@ -450,7 +446,7 @@ async function onStatusChange(e: Event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
loadMarkdown() // 后台预热 Markdown 渲染器(模块单例,与 AiChat/TaskDetail 共享),不阻塞
|
ensureLoaded() // 后台预热 Markdown 渲染器(模块单例,与 AiChat/TaskDetail 共享),不阻塞
|
||||||
await store.loadIdeas()
|
await store.loadIdeas()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -235,22 +235,12 @@
|
|||||||
import { ref, computed, onMounted, watch } from 'vue'
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useKnowledgeStore, KNOWLEDGE_KINDS, parseTags } from '../stores/knowledge'
|
import { useKnowledgeStore, KNOWLEDGE_KINDS, parseTags } from '../stores/knowledge'
|
||||||
import { useMarkdown } from '../composables/useMarkdown'
|
import { useRendered } from '../composables/useMarkdown'
|
||||||
import type { KnowledgeDetailPayload, KnowledgeEventRecord } from '../api/types'
|
import type { KnowledgeDetailPayload, KnowledgeEventRecord } from '../api/types'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const store = useKnowledgeStore()
|
const store = useKnowledgeStore()
|
||||||
|
|
||||||
// B-260615-25:知识内容 Markdown 渲染(复用 AiChat/TaskDetail 同款渲染器,模块级单例)
|
|
||||||
const { renderMd, loadMarkdown, mdReady } = useMarkdown()
|
|
||||||
|
|
||||||
// 内容渲染依赖 mdReady 响应式:loadMarkdown 完成前 escapeFallback 纯文本,完成后 mdReady 翻转
|
|
||||||
// 触发 computed 重算(B-25 漏响应式致 v-html=renderMd 首次纯文本后不重算,代码块不渲染)
|
|
||||||
const renderedContent = computed(() => {
|
|
||||||
void mdReady.value
|
|
||||||
return renderMd(detail.value?.knowledge.content ?? '')
|
|
||||||
})
|
|
||||||
|
|
||||||
// 顶层 Tab: library(知识库) | inbox(审核收件箱)
|
// 顶层 Tab: library(知识库) | inbox(审核收件箱)
|
||||||
const topTab = ref<'library' | 'inbox'>('library')
|
const topTab = ref<'library' | 'inbox'>('library')
|
||||||
|
|
||||||
@@ -309,6 +299,11 @@ function getCategoryCount(key: string): number {
|
|||||||
// ===== 详情选中 + 加载 =====
|
// ===== 详情选中 + 加载 =====
|
||||||
const selectedId = ref<string | null>(null)
|
const selectedId = ref<string | null>(null)
|
||||||
const detail = ref<KnowledgeDetailPayload | null>(null)
|
const detail = ref<KnowledgeDetailPayload | null>(null)
|
||||||
|
// B-260615-25:知识内容 Markdown 渲染(复用 AiChat/TaskDetail 同款渲染器,模块级单例),
|
||||||
|
// useRendered 封装 computed(读 mdReady 触发响应式 + renderMd)+ ensureLoaded(幂等预热)
|
||||||
|
const { rendered: renderedContent, ensureLoaded } = useRendered(
|
||||||
|
() => detail.value?.knowledge.content ?? '',
|
||||||
|
)
|
||||||
const refLimit = ref(10)
|
const refLimit = ref(10)
|
||||||
|
|
||||||
async function selectKnowledge(id: string) {
|
async function selectKnowledge(id: string) {
|
||||||
@@ -512,7 +507,7 @@ function relativeTime(millisStr: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadMarkdown() // 后台预热 Markdown 渲染器(模块单例,与 AiChat/TaskDetail 共享),不阻塞
|
ensureLoaded() // 后台预热 Markdown 渲染器(模块单例,与 AiChat/TaskDetail 共享),不阻塞
|
||||||
store.loadList()
|
store.loadList()
|
||||||
store.loadCandidates()
|
store.loadCandidates()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -241,17 +241,7 @@ import { parseStack } from '../utils/project'
|
|||||||
import { projectStatusLabel, projectStageInfo, taskStatusLabel, taskStatusClass } from '../constants/project'
|
import { projectStatusLabel, projectStageInfo, taskStatusLabel, taskStatusClass } from '../constants/project'
|
||||||
import ConfirmDialog from '../components/ConfirmDialog.vue'
|
import ConfirmDialog from '../components/ConfirmDialog.vue'
|
||||||
import { useConfirm } from '../composables/useConfirm'
|
import { useConfirm } from '../composables/useConfirm'
|
||||||
import { useMarkdown } from '../composables/useMarkdown'
|
import { useRendered } from '../composables/useMarkdown'
|
||||||
|
|
||||||
// B-260615-25:项目描述 Markdown 渲染(复用 AiChat/TaskDetail 同款渲染器,模块级单例)
|
|
||||||
const { renderMd, loadMarkdown, mdReady } = useMarkdown()
|
|
||||||
|
|
||||||
// 描述渲染依赖 mdReady 响应式:loadMarkdown 完成前 escapeFallback 纯文本,完成后 mdReady 翻转
|
|
||||||
// 触发 computed 重算(B-25 漏响应式致 v-html=renderMd 首次纯文本后不重算,代码块不渲染)
|
|
||||||
const renderedDesc = computed(() => {
|
|
||||||
void mdReady.value
|
|
||||||
return renderMd(currentProject.value?.description ?? '')
|
|
||||||
})
|
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -280,6 +270,11 @@ const projectId = computed(() => route.params.id as string)
|
|||||||
const currentProject = computed(() =>
|
const currentProject = computed(() =>
|
||||||
store.projects.find(p => p.id === projectId.value)
|
store.projects.find(p => p.id === projectId.value)
|
||||||
)
|
)
|
||||||
|
// B-260615-25:项目描述 Markdown 渲染(复用 AiChat/TaskDetail 同款渲染器,模块级单例),
|
||||||
|
// useRendered 封装 computed(读 mdReady 触发响应式 + renderMd)+ ensureLoaded(幂等预热)
|
||||||
|
const { rendered: renderedDesc, ensureLoaded: ensureMdLoaded } = useRendered(
|
||||||
|
() => currentProject.value?.description ?? '',
|
||||||
|
)
|
||||||
const stageKey = computed(() => currentProject.value?.status ?? 'planning')
|
const stageKey = computed(() => currentProject.value?.status ?? 'planning')
|
||||||
const statusLabel = computed(() => projectStatusLabel(stageKey.value))
|
const statusLabel = computed(() => projectStatusLabel(stageKey.value))
|
||||||
const currentStageIndex = computed(() => projectStageInfo(stageKey.value).stepIndex)
|
const currentStageIndex = computed(() => projectStageInfo(stageKey.value).stepIndex)
|
||||||
@@ -462,7 +457,7 @@ watch(() => store.pendingApproval, (newApproval) => {
|
|||||||
|
|
||||||
// ── 生命周期 ──
|
// ── 生命周期 ──
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
loadMarkdown() // 后台预热 Markdown 渲染器(模块单例,与 AiChat/TaskDetail 共享),不阻塞
|
ensureMdLoaded() // 后台预热 Markdown 渲染器(模块单例,与 AiChat/TaskDetail 共享),不阻塞
|
||||||
await store.loadProjects()
|
await store.loadProjects()
|
||||||
await store.loadTasks() // 全量加载,详情页用 computed 过滤本项目(避免覆盖全局 tasks 单例)
|
await store.loadTasks() // 全量加载,详情页用 computed 过滤本项目(避免覆盖全局 tasks 单例)
|
||||||
unlisten = await store.startEventListener()
|
unlisten = await store.startEventListener()
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ import { ref, computed, onMounted, watch } from 'vue'
|
|||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { taskApi, projectApi } from '../api'
|
import { taskApi, projectApi } from '../api'
|
||||||
import { formatDate } from '../utils/time'
|
import { formatDate } from '../utils/time'
|
||||||
import { useMarkdown } from '../composables/useMarkdown'
|
import { useRendered } from '../composables/useMarkdown'
|
||||||
import {
|
import {
|
||||||
taskStatusLabel,
|
taskStatusLabel,
|
||||||
taskStatusClass,
|
taskStatusClass,
|
||||||
@@ -108,16 +108,6 @@ import {
|
|||||||
} from '../constants/project'
|
} from '../constants/project'
|
||||||
import type { TaskRecord, ProjectRecord } from '../api/types'
|
import type { TaskRecord, ProjectRecord } from '../api/types'
|
||||||
|
|
||||||
// B-24:任务描述 Markdown 渲染(复用 AiChat 同款渲染器,模块级单例)
|
|
||||||
const { renderMd, loadMarkdown, mdReady } = useMarkdown()
|
|
||||||
|
|
||||||
// 描述渲染依赖 mdReady 响应式:loadMarkdown 完成前 escapeFallback 纯文本,完成后 mdReady 翻转
|
|
||||||
// 触发 computed 重算(B-24 漏响应式致 v-html=renderMd 首次纯文本后不重算,代码块不渲染)
|
|
||||||
const renderedDescription = computed(() => {
|
|
||||||
void mdReady.value
|
|
||||||
return renderMd(task.value?.description ?? '')
|
|
||||||
})
|
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
@@ -127,6 +117,12 @@ const projects = ref<ProjectRecord[]>([])
|
|||||||
|
|
||||||
const taskId = computed(() => route.params.id as string)
|
const taskId = computed(() => route.params.id as string)
|
||||||
|
|
||||||
|
// B-24:任务描述 Markdown 渲染(复用 AiChat 同款渲染器,模块级单例),useRendered 封装
|
||||||
|
// computed(读 mdReady 触发响应式 + renderMd)+ ensureLoaded(幂等预热)
|
||||||
|
const { rendered: renderedDescription, ensureLoaded } = useRendered(
|
||||||
|
() => task.value?.description ?? '',
|
||||||
|
)
|
||||||
|
|
||||||
const projectName = computed(() => {
|
const projectName = computed(() => {
|
||||||
const p = projects.value.find(p => p.id === task.value?.project_id)
|
const p = projects.value.find(p => p.id === task.value?.project_id)
|
||||||
return p?.name ?? task.value?.project_id ?? '—'
|
return p?.name ?? task.value?.project_id ?? '—'
|
||||||
@@ -159,7 +155,7 @@ function refresh() {
|
|||||||
watch(taskId, () => { load() })
|
watch(taskId, () => { load() })
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadMarkdown() // 后台预热 Markdown 渲染器(模块单例,与 AiChat 共享),不阻塞 load
|
ensureLoaded() // 后台预热 Markdown 渲染器(模块单例,与 AiChat 共享),不阻塞 load
|
||||||
load()
|
load()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user