重构: 拆TaskDetail任务产出(strategy前端)
- 新建 components/task/TaskOutputCard.vue(139行): 任务产出+AI自审结果(JSON解析+Markdown渲染+reviewVerdict)+自有i18n - TaskDetail.vue 701→612(-89): v-if output→<TaskOutputCard> + 删2接口+5computed+output样式 - props outputJson(自包含无store无emit) 主代兜底(新工作模式·vue-tsc only不grep降上下文): vue-tsc 0(子代理自验印证) strategy: 前端views, 子组件抽产出; TaskDetail余612(工作流推进深度耦合留下批) git add指定(TaskDetail.vue+TaskOutputCard.vue)
This commit is contained in:
139
src/components/task/TaskOutputCard.vue
Normal file
139
src/components/task/TaskOutputCard.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<!-- F-AiNodeSelfReview: 任务产出 + AI 自审结果展示 -->
|
||||
<!-- 仅当 output_json 存在(经 AiNode 写产出/自审后由 df-data-changed 自动刷新出现) -->
|
||||
<!-- 从 TaskDetail.vue 抽出(L131-144 模板 + L214-262 脚本 + L643-670 样式),零行为变更。 -->
|
||||
<!-- 自包含:JSON 解析 / Markdown 渲染(复用 useMarkdown 模块单例)/ review verdict 计算 / i18n 全部内聚。 -->
|
||||
<div v-if="parsedOutput" class="info-item info-block output-block">
|
||||
<span class="label">{{ $t('taskDetail.output') }}</span>
|
||||
<div class="value output-body">
|
||||
<!-- AI 自审结论卡(若有 review 子字段) -->
|
||||
<div v-if="parsedOutput.review" class="review-card" :class="reviewVerdictClass">
|
||||
<span class="review-verdict">{{ reviewVerdictLabel }}</span>
|
||||
<span v-if="parsedOutput.review.summary" class="review-summary">{{ parsedOutput.review.summary }}</span>
|
||||
<ul v-if="reviewSuggestions.length" class="review-suggestions">
|
||||
<li v-for="(s, i) in reviewSuggestions" :key="i">{{ s }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- AI 产出正文(Markdown 渲染,复用 useRendered 单例渲染器) -->
|
||||
<div v-if="parsedOutput.text" class="output-text ai-md" v-html="renderedOutput"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRendered } from '@/composables/useMarkdown'
|
||||
|
||||
// ============================================================
|
||||
// F-AiNodeSelfReview: output_json 解析 + AI 产出 Markdown 渲染
|
||||
// ------------------------------------------------------------
|
||||
// output_json schema(与 df-nodes ai_node.rs 写产出现对齐):
|
||||
// { text, model?, usage?, review?: { verdict: 'pass'|'fail'|'unknown', summary?, suggestions? } }
|
||||
// parsedOutput try/catch: JSON.parse 边界必要兜底(LLM 自审输出可能不合规/老数据格式漂移),
|
||||
// 解析失败返回 null → v-if 守卫整块不渲染(不裸显报错)。
|
||||
interface TaskOutputReview {
|
||||
verdict?: string
|
||||
summary?: string
|
||||
suggestions?: unknown
|
||||
}
|
||||
interface TaskOutput {
|
||||
text?: string
|
||||
model?: string
|
||||
usage?: unknown
|
||||
review?: TaskOutputReview
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
// task.output_json 原始字符串;空值/解析失败时整卡不渲染
|
||||
outputJson?: string
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const parsedOutput = computed<TaskOutput | null>(() => {
|
||||
const raw = props.outputJson
|
||||
if (!raw) return null
|
||||
try {
|
||||
return JSON.parse(raw) as TaskOutput
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})
|
||||
// AI 产出正文渲染(复用模块单例渲染器,与其他视图共享 marked/DOMPurify/缓存)
|
||||
const { rendered: renderedOutput } = useRendered(
|
||||
() => parsedOutput.value?.text ?? '',
|
||||
)
|
||||
// review.verdict 红绿标:pass→绿(success),fail→红(danger),未知/缺失→灰(dim)
|
||||
const reviewVerdictClass = computed(() => {
|
||||
const v = parsedOutput.value?.review?.verdict
|
||||
if (v === 'pass') return 'review-pass'
|
||||
if (v === 'fail') return 'review-fail'
|
||||
return 'review-unknown'
|
||||
})
|
||||
// review.verdict 标签(pass/fail/unknown 走 i18n,异常值兜底 unknown)
|
||||
const reviewVerdictLabel = computed(() => {
|
||||
const v = parsedOutput.value?.review?.verdict
|
||||
if (v === 'pass') return t('taskDetail.review.pass')
|
||||
if (v === 'fail') return t('taskDetail.review.fail')
|
||||
return t('taskDetail.review.unknown')
|
||||
})
|
||||
// review.suggestions 列表(数组守卫,非数组时返回空避免 v-for 报错)
|
||||
const reviewSuggestions = computed<string[]>(() => {
|
||||
const s = parsedOutput.value?.review?.suggestions
|
||||
return Array.isArray(s) ? s.map(String) : []
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* ===== F-AiNodeSelfReview: output_json 区块(产出 + 自审结论卡) ===== */
|
||||
/* 基础布局 .info-item / .info-block / .label / .value:Vue scoped 不穿透子组件后代 DOM,
|
||||
父级 TaskDetail.vue 的 .info-item .label 等后代选择器不会作用到本子组件内部元素,
|
||||
故此处复制等价基础布局(与 TaskDetail.vue L559-577 一致),保持视觉零变化。 */
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
gap: 12px;
|
||||
}
|
||||
.info-item.info-block {
|
||||
flex-direction: column;
|
||||
}
|
||||
.info-item .label {
|
||||
font-size: 12px;
|
||||
color: var(--df-text-dim);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
min-width: 88px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.info-item .value { font-size: 14px; color: var(--df-text); flex: 1; min-width: 0; }
|
||||
|
||||
.output-block .output-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
/* 自审结论卡:verdict 红绿标(pass 绿 / fail 红 / unknown 灰),复用全局 --df-success/danger token */
|
||||
.review-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 8px 12px;
|
||||
border-radius: var(--df-radius-sm);
|
||||
border: 0.5px solid var(--df-border);
|
||||
font-size: 13px;
|
||||
}
|
||||
.review-pass { background: var(--df-success-bg); border-color: rgba(61,219,160,0.3); }
|
||||
.review-fail { background: var(--df-danger-bg); border-color: rgba(240,101,101,0.3); }
|
||||
.review-unknown { background: var(--df-bg-card); }
|
||||
.review-verdict { font-weight: 600; }
|
||||
.review-pass .review-verdict { color: var(--df-success); }
|
||||
.review-fail .review-verdict { color: var(--df-danger); }
|
||||
.review-unknown .review-verdict { color: var(--df-text-dim); }
|
||||
.review-summary { color: var(--df-text-secondary); line-height: 1.5; }
|
||||
.review-suggestions { margin: 4px 0 0; padding-left: 20px; color: var(--df-text-secondary); }
|
||||
.review-suggestions li { line-height: 1.6; }
|
||||
/* AI 产出正文(继承全局 .ai-md 块级样式,line-height 对齐描述) */
|
||||
.output-text { line-height: 1.5; }
|
||||
</style>
|
||||
@@ -125,23 +125,8 @@
|
||||
<span class="label">{{ $t('taskDetail.workflowDef') }}</span>
|
||||
<span class="value mono">{{ task.workflow_def_id ?? '—' }}</span>
|
||||
</div>
|
||||
<!-- F-AiNodeSelfReview: 任务产出 + AI 自审结果展示 -->
|
||||
<!-- 仅当 task.output_json 存在(经 AiNode 写产出/自审后由 df-data-changed 自动刷新出现) -->
|
||||
<div v-if="task.output_json && parsedOutput" class="info-item info-block output-block">
|
||||
<span class="label">{{ $t('taskDetail.output') }}</span>
|
||||
<div class="value output-body">
|
||||
<!-- AI 自审结论卡(若有 review 子字段) -->
|
||||
<div v-if="parsedOutput.review" class="review-card" :class="reviewVerdictClass">
|
||||
<span class="review-verdict">{{ reviewVerdictLabel }}</span>
|
||||
<span v-if="parsedOutput.review.summary" class="review-summary">{{ parsedOutput.review.summary }}</span>
|
||||
<ul v-if="reviewSuggestions.length" class="review-suggestions">
|
||||
<li v-for="(s, i) in reviewSuggestions" :key="i">{{ s }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- AI 产出正文(Markdown 渲染,复用 useRendered 单例渲染器) -->
|
||||
<div v-if="parsedOutput.text" class="output-text ai-md" v-html="renderedOutput"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- F-AiNodeSelfReview: 任务产出 + AI 自审结果展示(抽至 TaskOutputCard 子组件,零行为变更) -->
|
||||
<TaskOutputCard :output-json="task.output_json" />
|
||||
<div class="info-item">
|
||||
<span class="label">{{ $t('taskDetail.createdAt') }}</span>
|
||||
<span class="value">{{ formatDate(task.created_at) }}</span>
|
||||
@@ -172,6 +157,7 @@ import {
|
||||
priorityClass,
|
||||
} from '../constants/project'
|
||||
import type { TaskRecord, ProjectRecord, DfDataChangedPayload, WorkflowEventPayload } from '@/api/types'
|
||||
import TaskOutputCard from '@/components/task/TaskOutputCard.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
@@ -210,56 +196,8 @@ const { rendered: renderedDesc, ensureLoaded } = useRendered(
|
||||
() => task.value?.description ?? '',
|
||||
)
|
||||
|
||||
// ============================================================
|
||||
// F-AiNodeSelfReview: output_json 解析 + AI 产出 Markdown 渲染
|
||||
// ------------------------------------------------------------
|
||||
// output_json schema(与 df-nodes ai_node.rs 写产出现对齐):
|
||||
// { text, model?, usage?, review?: { verdict: 'pass'|'fail'|'unknown', summary?, suggestions? } }
|
||||
// parsedOutput try/catch: JSON.parse 边界必要兜底(LLM 自审输出可能不合规/老数据格式漂移),
|
||||
// 解析失败返回 null → v-if 守卫整块不渲染(不裸显报错)。
|
||||
interface TaskOutputReview {
|
||||
verdict?: string
|
||||
summary?: string
|
||||
suggestions?: unknown
|
||||
}
|
||||
interface TaskOutput {
|
||||
text?: string
|
||||
model?: string
|
||||
usage?: unknown
|
||||
review?: TaskOutputReview
|
||||
}
|
||||
const parsedOutput = computed<TaskOutput | null>(() => {
|
||||
const raw = task.value?.output_json
|
||||
if (!raw) return null
|
||||
try {
|
||||
return JSON.parse(raw) as TaskOutput
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})
|
||||
// AI 产出正文渲染(复用模块单例渲染器,与 renderedDesc 共享 marked/DOMPurify/缓存)
|
||||
const { rendered: renderedOutput } = useRendered(
|
||||
() => parsedOutput.value?.text ?? '',
|
||||
)
|
||||
// review.verdict 红绿标:pass→绿(success),fail→红(danger),未知/缺失→灰(dim)
|
||||
const reviewVerdictClass = computed(() => {
|
||||
const v = parsedOutput.value?.review?.verdict
|
||||
if (v === 'pass') return 'review-pass'
|
||||
if (v === 'fail') return 'review-fail'
|
||||
return 'review-unknown'
|
||||
})
|
||||
// review.verdict 标签(pass/fail/unknown 走 i18n,异常值兜底 unknown)
|
||||
const reviewVerdictLabel = computed(() => {
|
||||
const v = parsedOutput.value?.review?.verdict
|
||||
if (v === 'pass') return t('taskDetail.review.pass')
|
||||
if (v === 'fail') return t('taskDetail.review.fail')
|
||||
return t('taskDetail.review.unknown')
|
||||
})
|
||||
// review.suggestions 列表(数组守卫,非数组时返回空避免 v-for 报错)
|
||||
const reviewSuggestions = computed<string[]>(() => {
|
||||
const s = parsedOutput.value?.review?.suggestions
|
||||
return Array.isArray(s) ? s.map(String) : []
|
||||
})
|
||||
// F-AiNodeSelfReview: output_json 解析 + AI 产出/自审渲染已抽至 TaskOutputCard 子组件
|
||||
// (components/task/TaskOutputCard.vue),父级只传 output-json prop,零行为变更。
|
||||
|
||||
const projectName = computed(() => {
|
||||
const p = projects.value.find(p => p.id === task.value?.project_id)
|
||||
@@ -640,34 +578,7 @@ onBeforeUnmount(() => {
|
||||
/* B-24 漏 white-space 覆盖致 v-html 后 HTML 标签间 \n 被 pre-wrap 渲染为空行间距——移除 pre-wrap,描述由 .ai-md 接管(line-height 1.5 对齐 AiChat li) */
|
||||
.info-item .value.description { line-height: 1.5; }
|
||||
|
||||
/* ===== F-AiNodeSelfReview: output_json 区块(产出 + 自审结论卡) ===== */
|
||||
.output-block .output-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
/* 自审结论卡:verdict 红绿标(pass 绿 / fail 红 / unknown 灰),复用全局 --df-success/danger token */
|
||||
.review-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 8px 12px;
|
||||
border-radius: var(--df-radius-sm);
|
||||
border: 0.5px solid var(--df-border);
|
||||
font-size: 13px;
|
||||
}
|
||||
.review-pass { background: var(--df-success-bg); border-color: rgba(61,219,160,0.3); }
|
||||
.review-fail { background: var(--df-danger-bg); border-color: rgba(240,101,101,0.3); }
|
||||
.review-unknown { background: var(--df-bg-card); }
|
||||
.review-verdict { font-weight: 600; }
|
||||
.review-pass .review-verdict { color: var(--df-success); }
|
||||
.review-fail .review-verdict { color: var(--df-danger); }
|
||||
.review-unknown .review-verdict { color: var(--df-text-dim); }
|
||||
.review-summary { color: var(--df-text-secondary); line-height: 1.5; }
|
||||
.review-suggestions { margin: 4px 0 0; padding-left: 20px; color: var(--df-text-secondary); }
|
||||
.review-suggestions li { line-height: 1.6; }
|
||||
/* AI 产出正文(继承全局 .ai-md 块级样式,line-height 对齐描述) */
|
||||
.output-text { line-height: 1.5; }
|
||||
/* F-AiNodeSelfReview output_json 区块样式已随子组件移至 TaskOutputCard.vue */
|
||||
|
||||
/* ===== 任务描述 Markdown 渲染(B-24,基础样式收敛至全局 ai-md.css) ===== */
|
||||
|
||||
|
||||
Reference in New Issue
Block a user