重构: 前端 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:
@@ -110,9 +110,15 @@
|
||||
<span class="label">{{ $t('projectDetail.updatedAt') }}</span>
|
||||
<span>{{ formatDate(currentProject.updated_at) }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-item info-block">
|
||||
<span class="label">{{ $t('projectDetail.description') }}</span>
|
||||
<span>{{ currentProject.description || '—' }}</span>
|
||||
<!-- B-260615-25:项目描述 Markdown 渲染,复用 useMarkdown composable(同 B-24 TaskDetail) -->
|
||||
<span
|
||||
v-if="currentProject.description"
|
||||
class="value description ai-md"
|
||||
v-html="renderedDesc"
|
||||
></span>
|
||||
<span v-else>—</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">{{ $t('projectDetail.projectStatus') }}</span>
|
||||
@@ -153,7 +159,6 @@
|
||||
<section class="panel">
|
||||
<div class="panel-header">
|
||||
<h2>{{ $t('projectDetail.workflowLogTitle') }}</h2>
|
||||
<button class="btn btn-ghost btn-sm" @click="runDemoWorkflow" :disabled="workflowRunning">{{ $t('projectDetail.runDemoWorkflow') }}</button>
|
||||
</div>
|
||||
<div class="log-list" ref="logListRef">
|
||||
<div class="log-item" v-for="(evt, idx) in formattedEvents" :key="idx" :class="'log-' + evt.level">
|
||||
@@ -176,17 +181,39 @@
|
||||
<p style="margin: 16px 0; color: var(--df-text-secondary);">{{ store.pendingApproval.description }}</p>
|
||||
<div style="margin-bottom: 20px;">
|
||||
<p style="margin-bottom: 8px; font-size: 14px;">{{ $t('projectDetail.approvalHint') }}</p>
|
||||
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
|
||||
<!-- F-260615-01: select_type=multiple → checkbox 多选,缺省 single → 按钮单选 -->
|
||||
<template v-if="isMultipleSelect">
|
||||
<div style="display: flex; flex-direction: column; gap: 8px; margin-bottom: 16px;">
|
||||
<label
|
||||
v-for="(option, idx) in store.pendingApproval.options"
|
||||
:key="idx"
|
||||
style="display: flex; align-items: center; gap: 8px; cursor: pointer;"
|
||||
>
|
||||
<input type="checkbox" :value="option" v-model="multiDecisions" />
|
||||
<span>{{ option }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<button
|
||||
v-for="(option, idx) in store.pendingApproval.options"
|
||||
:key="idx"
|
||||
class="btn"
|
||||
:class="idx === 0 ? 'btn-primary' : 'btn-ghost'"
|
||||
@click="handleApproval(option)"
|
||||
class="btn btn-primary"
|
||||
:disabled="multiDecisions.length === 0"
|
||||
@click="handleApprovalMulti"
|
||||
>
|
||||
{{ option }}
|
||||
确认({{ multiDecisions.length }})
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
|
||||
<button
|
||||
v-for="(option, idx) in store.pendingApproval.options"
|
||||
:key="idx"
|
||||
class="btn"
|
||||
:class="idx === 0 ? 'btn-primary' : 'btn-ghost'"
|
||||
@click="handleApproval(option)"
|
||||
>
|
||||
{{ option }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
@@ -214,6 +241,17 @@ import { parseStack } from '../utils/project'
|
||||
import { projectStatusLabel, projectStageInfo, taskStatusLabel, taskStatusClass } from '../constants/project'
|
||||
import ConfirmDialog from '../components/ConfirmDialog.vue'
|
||||
import { useConfirm } from '../composables/useConfirm'
|
||||
import { useMarkdown } 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 router = useRouter()
|
||||
@@ -253,29 +291,10 @@ const projectTasks = computed(() =>
|
||||
// taskStatusLabel / taskStatusClass 由 ../constants/project 提供
|
||||
|
||||
// ── 工作流 ──
|
||||
const workflowRunning = ref(false)
|
||||
|
||||
const demoDag = {
|
||||
nodes: [
|
||||
{ id: 'n1', node_type: 'script', label: '环境检查', config: { command: 'echo "Environment OK"', timeout_secs: 10 } },
|
||||
{ id: 'n2', node_type: 'script', label: '运行测试', config: { command: 'echo "Tests passed"', timeout_secs: 10 } },
|
||||
{ id: 'n3', node_type: 'script', label: '构建产物', config: { command: 'echo "Build success"', timeout_secs: 10 } },
|
||||
],
|
||||
edges: [
|
||||
{ from: 'n1', to: 'n2' },
|
||||
{ from: 'n2', to: 'n3' },
|
||||
],
|
||||
}
|
||||
|
||||
async function runDemoWorkflow() {
|
||||
workflowRunning.value = true
|
||||
store.clearLiveEvents()
|
||||
try {
|
||||
await store.runWorkflow('demo-pipeline', demoDag)
|
||||
} finally {
|
||||
workflowRunning.value = false
|
||||
}
|
||||
}
|
||||
// demoDag + runDemoWorkflow 已下线(R-PD-2):"script" 节点不再注册到 NodeRegistry,
|
||||
// 前端构造含 script 节点的 DagDef 会在后端 build_dag 失败报错。
|
||||
// 工作流执行日志面板保留,实时事件展示能力不依赖 demoDag。
|
||||
// 待真实工作流需求落地(独立 BuildNode + 审批链)时重建演示入口。
|
||||
|
||||
// ── 实时日志格式化 ──
|
||||
interface LogEntry {
|
||||
@@ -397,6 +416,18 @@ async function handleApproval(decision: string) {
|
||||
showApprovalDialog.value = false
|
||||
}
|
||||
|
||||
// F-260615-01: 多选审批
|
||||
const isMultipleSelect = computed(() => store.pendingApproval?.select_type === 'multiple')
|
||||
const multiDecisions = ref<string[]>([])
|
||||
|
||||
async function handleApprovalMulti() {
|
||||
if (multiDecisions.value.length === 0) return
|
||||
// decision 传首项(向后兼容),decisions 透传全量
|
||||
await store.approveHumanApproval(multiDecisions.value[0], undefined, [...multiDecisions.value])
|
||||
multiDecisions.value = []
|
||||
showApprovalDialog.value = false
|
||||
}
|
||||
|
||||
// ── 取消审批(调 cancel_workflow_node IPC 置节点 Cancelled)──
|
||||
async function handleCancelApproval() {
|
||||
await store.cancelHumanApproval()
|
||||
@@ -420,6 +451,8 @@ watch(formattedEvents, () => {
|
||||
// 监听审批请求
|
||||
watch(() => store.pendingApproval, (newApproval) => {
|
||||
if (newApproval) {
|
||||
// F-260615-01: 每次新审批清空多选状态(避免上轮残留)
|
||||
multiDecisions.value = []
|
||||
showApprovalDialog.value = true
|
||||
}
|
||||
}, { immediate: true })
|
||||
@@ -429,6 +462,7 @@ watch(() => store.pendingApproval, (newApproval) => {
|
||||
|
||||
// ── 生命周期 ──
|
||||
onMounted(async () => {
|
||||
loadMarkdown() // 后台预热 Markdown 渲染器(模块单例,与 AiChat/TaskDetail 共享),不阻塞
|
||||
await store.loadProjects()
|
||||
await store.loadTasks() // 全量加载,详情页用 computed 过滤本项目(避免覆盖全局 tasks 单例)
|
||||
unlisten = await store.startEventListener()
|
||||
@@ -456,10 +490,15 @@ onUnmounted(() => {
|
||||
gap: var(--df-gap-grid);
|
||||
}
|
||||
|
||||
/* B-260615-31:字段同行布局(label 固定宽 + 值占余),描述字段 info-block 保持块状 */
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
gap: 12px;
|
||||
}
|
||||
.info-item.info-block {
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.info-item .label {
|
||||
@@ -467,6 +506,8 @@ onUnmounted(() => {
|
||||
color: var(--df-text-dim);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
min-width: 88px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.info-item a.idea-link {
|
||||
@@ -508,6 +549,63 @@ onUnmounted(() => {
|
||||
background: rgba(108, 99, 255, 0.08); color: var(--df-text-secondary);
|
||||
border: 0.5px solid var(--df-border);
|
||||
}
|
||||
|
||||
/* ===== 项目描述 Markdown 渲染(B-260615-25,样式同 TaskDetail B-24 .ai-md 收敛) ===== */
|
||||
.description.ai-md { font-size: 14px; color: var(--df-text); line-height: 1.6; }
|
||||
.description.ai-md :deep(p) { margin: 0 0 6px; }
|
||||
.description.ai-md :deep(p:last-child) { margin-bottom: 0; }
|
||||
.description.ai-md :deep(ul), .description.ai-md :deep(ol) { margin: 4px 0; padding-left: 20px; }
|
||||
.description.ai-md :deep(li) { margin: 2px 0; line-height: 1.5; }
|
||||
.description.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);
|
||||
}
|
||||
.description.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;
|
||||
}
|
||||
.description.ai-md :deep(pre code) {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
color: var(--df-text-secondary);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.description.ai-md :deep(blockquote) {
|
||||
margin: 6px 0;
|
||||
padding: 4px 12px;
|
||||
border-left: 2px solid var(--df-accent);
|
||||
color: var(--df-text-secondary);
|
||||
}
|
||||
.description.ai-md :deep(h1), .description.ai-md :deep(h2), .description.ai-md :deep(h3) {
|
||||
font-weight: 500;
|
||||
color: var(--df-text);
|
||||
margin: 8px 0 4px;
|
||||
}
|
||||
.description.ai-md :deep(h1) { font-size: 16px; }
|
||||
.description.ai-md :deep(h2) { font-size: 14px; }
|
||||
.description.ai-md :deep(h3) { font-size: 13px; }
|
||||
.description.ai-md :deep(a) { color: var(--df-accent); text-decoration: none; }
|
||||
.description.ai-md :deep(a:hover) { text-decoration: underline; }
|
||||
.description.ai-md :deep(strong) { font-weight: 500; color: var(--df-text); }
|
||||
.description.ai-md :deep(hr) { border: none; border-top: 0.5px solid var(--df-border); margin: 8px 0; }
|
||||
.description.ai-md :deep(table) {
|
||||
width: 100%; border-collapse: collapse; margin: 6px 0;
|
||||
font-size: 12px; overflow-x: auto; display: block;
|
||||
}
|
||||
.description.ai-md :deep(th), .description.ai-md :deep(td) {
|
||||
padding: 4px 8px; border: 0.5px solid var(--df-border); text-align: left;
|
||||
}
|
||||
.description.ai-md :deep(th) { font-weight: 500; background: var(--df-bg); }
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
|
||||
Reference in New Issue
Block a user