重构: 前端 composable 拆分+详情页 Markdown 渲染+流式 memo+状态同步+bug 修复

- composables/useMarkdown 抽(AiChat 复用)+ai/* 流式核心+useAiSend catch 回滚 user msg(B-21)+useAiStream 超时文案(B-03)+useAiConversations 切换 token(FR-R1)
- views/TaskDetail 新建(F-02)+描述 md+mdReady 响应式+字段同行布局(B-24/31)
- views/ProjectDetail 多选审批 UI(F-01)+描述 md+mdReady+字段同行(B-25/31)
- views/Ideas+Knowledge 描述 md+mdReady(B-25)
- views/Tasks 项目切换联动(B-29)+task 详情跳转
- components/AiChat toast 失败提示(B-20)+流式块级 memo(ARC-08)+clean UI(AR-7)+stop 兜底(AR-5)
- components/ToolCard 审批卡片可读化(AR-3)
- stores useAiStore barrel(ARC-04)+del settings mock(ARC-01)+approve options(R-PD-5)
- i18n aiTool/nav 中英对称(AR-3/9)+删 nav.decisions 死链(ARC-02)
- router /tasks/:id(F-02)+global.css selection 可见(R-260615-01)+api task detail+App 删 decisions nav
This commit is contained in:
2026-06-15 05:14:56 +08:00
parent 2de0c6ecb7
commit 80b9243528
26 changed files with 997 additions and 207 deletions

View File

@@ -110,7 +110,13 @@
<div class="detail-field">
<label>{{ $t('knowledge.contentLabel') }}</label>
<div v-if="!editing" class="detail-content">{{ detail.knowledge.content }}</div>
<!-- B-260615-25:知识内容 Markdown 渲染(展示态),复用 useMarkdown composable( B-24 TaskDetail);编辑态保持 textarea -->
<div
v-if="!editing && detail.knowledge.content"
class="detail-content ai-md"
v-html="renderedContent"
></div>
<div v-else-if="!editing" class="detail-content"></div>
<textarea v-else v-model="editForm.content" class="edit-input edit-textarea"></textarea>
</div>
@@ -229,11 +235,22 @@
import { ref, computed, onMounted, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useKnowledgeStore, KNOWLEDGE_KINDS, parseTags } from '../stores/knowledge'
import { useMarkdown } from '../composables/useMarkdown'
import type { KnowledgeDetailPayload, KnowledgeEventRecord } from '../api/types'
const { t } = useI18n()
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(审核收件箱)
const topTab = ref<'library' | 'inbox'>('library')
@@ -495,6 +512,7 @@ function relativeTime(millisStr: string): string {
}
onMounted(() => {
loadMarkdown() // 后台预热 Markdown 渲染器(模块单例,与 AiChat/TaskDetail 共享),不阻塞
store.loadList()
store.loadCandidates()
})
@@ -592,6 +610,63 @@ onMounted(() => {
.detail-field { margin-bottom: 12px; }
.detail-field label { display: block; font-size: 12px; color: var(--df-text-secondary); margin-bottom: 4px; }
.detail-content { font-size: 13px; color: var(--df-text); line-height: 1.6; white-space: pre-wrap; }
/* ===== 知识内容 Markdown 渲染(B-260615-25,样式同 TaskDetail B-24 .ai-md 收敛) ===== */
.detail-content.ai-md { white-space: normal; }
.detail-content.ai-md :deep(p) { margin: 0 0 6px; }
.detail-content.ai-md :deep(p:last-child) { margin-bottom: 0; }
.detail-content.ai-md :deep(ul), .detail-content.ai-md :deep(ol) { margin: 4px 0; padding-left: 20px; }
.detail-content.ai-md :deep(li) { margin: 2px 0; line-height: 1.5; }
.detail-content.ai-md :deep(code) {
font-family: var(--df-font-mono);
font-size: 12px;
padding: 1px 5px;
background: rgba(255,255,255,0.06);
border-radius: var(--df-radius-sm);
color: var(--df-accent);
}
.detail-content.ai-md :deep(pre) {
margin: 8px 0;
padding: 10px 12px;
background: var(--df-bg);
border: 0.5px solid var(--df-border);
border-radius: var(--df-radius);
overflow-x: auto;
}
.detail-content.ai-md :deep(pre code) {
padding: 0;
background: transparent;
border-radius: 0;
color: var(--df-text-secondary);
font-size: 12px;
line-height: 1.5;
}
.detail-content.ai-md :deep(blockquote) {
margin: 6px 0;
padding: 4px 12px;
border-left: 2px solid var(--df-accent);
color: var(--df-text-secondary);
}
.detail-content.ai-md :deep(h1), .detail-content.ai-md :deep(h2), .detail-content.ai-md :deep(h3) {
font-weight: 500;
color: var(--df-text);
margin: 8px 0 4px;
}
.detail-content.ai-md :deep(h1) { font-size: 16px; }
.detail-content.ai-md :deep(h2) { font-size: 14px; }
.detail-content.ai-md :deep(h3) { font-size: 13px; }
.detail-content.ai-md :deep(a) { color: var(--df-accent); text-decoration: none; }
.detail-content.ai-md :deep(a:hover) { text-decoration: underline; }
.detail-content.ai-md :deep(strong) { font-weight: 500; color: var(--df-text); }
.detail-content.ai-md :deep(hr) { border: none; border-top: 0.5px solid var(--df-border); margin: 8px 0; }
.detail-content.ai-md :deep(table) {
width: 100%; border-collapse: collapse; margin: 6px 0;
font-size: 12px; overflow-x: auto; display: block;
}
.detail-content.ai-md :deep(th), .detail-content.ai-md :deep(td) {
padding: 4px 8px; border: 0.5px solid var(--df-border); text-align: left;
}
.detail-content.ai-md :deep(th) { font-weight: 500; background: var(--df-bg); }
.detail-tags { display: flex; gap: 6px; flex-wrap: wrap; }
.tag { font-size: 11px; padding: 2px 8px; border-radius: var(--df-radius-xs); background: rgba(108,99,255,0.1); color: var(--df-accent); }
.muted { color: var(--df-text-dim); font-size: 12px; }