@@ -176,17 +181,39 @@
{{ store.pendingApproval.description }}
{{ $t('projectDetail.approvalHint') }}
-
+
+
+
+
+
+
@@ -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([])
+
+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); }
diff --git a/src/views/Tasks.vue b/src/views/Tasks.vue
index 1bbdd9e..7efb6c5 100644
--- a/src/views/Tasks.vue
+++ b/src/views/Tasks.vue
@@ -49,7 +49,7 @@
{{ $t('tasks.group.taskCount', { n: group.tasks.length }) }}
-
+
{{ task.title }}
@@ -101,13 +101,15 @@