修复: TaskDetail 操作按钮缺陷(推进竞态/菜单状态机过滤/工作流残留重置/子任务校验/描述编辑) + 看板销账(aichat切换缺陷 14/16 已修核对)

This commit is contained in:
lxy
2026-08-09 21:34:52 +08:00
parent e7228231d1
commit 180d8e339f
4 changed files with 335 additions and 36 deletions
+14 -1
View File
@@ -47,7 +47,7 @@ export default {
advanceFailed: 'Failed to advance task: {msg}',
// F-260616-06 ①-1 / B-41 Workflow advance (linked to task, auto-selects pipeline template by target_status)
workflowAdvanceTitle: 'Workflow Advance',
workflowAdvance: 'Run Workflow',
workflowAdvance: 'AI Execute Advance',
workflowAdvancing: 'Workflow advancing...',
workflowAdvanceFailed: 'Workflow advance failed: {msg}',
// B-41 Inline progress (lightweight, listens to NodeStarted/NodeCompleted/WorkflowFailed)
@@ -75,5 +75,18 @@ export default {
addSubtask: '+ Add Subtask',
progressTitle: 'Progress',
addSubtaskFailed: 'Failed to create subtask: {msg}',
// P2-9 subtask hint
subtaskHint: 'This is a subtask of: {title}',
// P2-7 subtask creation validation
subtaskTitleTooLong: 'Title must not exceed 128 characters',
subtaskDuplicate: 'A subtask with the same title already exists',
// P2-10 description editing
editDesc: 'Edit Description',
addDesc: '+ Add Description',
descEmpty: 'No description yet',
descPlaceholder: 'Enter task description...',
descSaveFailed: 'Failed to save description: {msg}',
saveDesc: 'Save',
cancelEdit: 'Cancel',
},
}
+14 -1
View File
@@ -47,7 +47,7 @@ export default {
advanceFailed: '推进任务失败: {msg}',
// F-260616-06 ①-1 / B-41 工作流推进(联动任务,按 target_status 自动选推进链模板)
workflowAdvanceTitle: '工作流推进',
workflowAdvance: '工作流推进', // 按钮(默认入口)
workflowAdvance: 'AI 执行推进', // 区别于手动"开始":工作流推进会跑 AiNode 生成产出
workflowAdvancing: '工作流推进中...',
workflowAdvanceFailed: '工作流推进失败: {msg}',
// B-41 内嵌进度(轻量,听 NodeStarted/NodeCompleted/WorkflowFailed)
@@ -75,5 +75,18 @@ export default {
addSubtask: ' 添加子任务',
progressTitle: '完成进度',
addSubtaskFailed: '创建子任务失败: {msg}',
// P2-9 子任务提示
subtaskHint: '该任务为子任务,隶属于:{title}',
// P2-7 子任务创建校验
subtaskTitleTooLong: '标题不能超过 128 字',
subtaskDuplicate: '同父下已存在同名子任务',
// P2-10 描述编辑
editDesc: '编辑描述',
addDesc: ' 添加描述',
descEmpty: '暂无描述',
descPlaceholder: '输入任务描述...',
descSaveFailed: '保存描述失败: {msg}',
saveDesc: '保存',
cancelEdit: '取消',
},
}
+104 -18
View File
@@ -34,7 +34,7 @@
type="button"
class="btn btn-sm"
:class="act.variant"
:disabled="advancing"
:disabled="advancing || wfAdvancing"
@click="handleAdvance(act.target)"
>{{ advancing ? $t('taskDetail.advancing') : $t(act.label) }}</button>
</div>
@@ -49,21 +49,35 @@
<div v-else-if="task" class="detail-grid">
<!-- ============ 左栏 ============ -->
<div class="left-column">
<!-- 描述(可折叠:Problem4 400px 显展开按钮) -->
<section v-if="task.description" class="panel">
<!-- P2-9 子任务提示条(仅子任务显示,指明隶属父任务) -->
<div v-if="task.parent_id" class="subtask-hint">{{ $t('taskDetail.subtaskHint', { title: parentTaskTitle || task.parent_id }) }}</div>
<!-- 描述(可编辑:有描述渲染+折叠 / 无描述空态;编辑态 textarea,对齐 IdeaDetail 模式) -->
<section class="panel">
<div class="panel-header"><h2>{{ $t('taskDetail.description') }}</h2></div>
<div class="description-wrap" :style="descWrapStyle">
<!-- B-24:任务描述 Markdown 渲染(## 标题 / - 列表 ),v-html useMarkdown DOMPurify sanitize -->
<span ref="descEl" class="value description ai-md" v-html="renderedDesc"></span>
</div>
<button
v-if="descCollapsible"
class="btn btn-ghost btn-sm desc-toggle"
type="button"
@click="descExpanded = !descExpanded"
>{{ descExpanded ? $t('taskDetail.collapse') : $t('taskDetail.expand') }}</button>
<!-- 折叠态淡出渐变蒙层(展开态不显) -->
<div v-if="descCollapsible && !descExpanded" class="desc-fade"></div>
<template v-if="editingDesc">
<textarea v-model="editDesc" class="detail-desc-edit" rows="4" :placeholder="$t('taskDetail.descPlaceholder')"></textarea>
<div class="desc-edit-actions">
<button class="btn btn-primary btn-sm" :disabled="savingDesc" @click="saveEditDesc">{{ $t('taskDetail.saveDesc') }}</button>
<button class="btn btn-ghost btn-sm" :disabled="savingDesc" @click="cancelEditDesc">{{ $t('taskDetail.cancelEdit') }}</button>
</div>
</template>
<template v-else>
<div v-if="task.description" class="description-wrap" :style="descWrapStyle">
<span ref="descEl" class="value description ai-md" v-html="renderedDesc"></span>
<button
v-if="descCollapsible"
class="btn btn-ghost btn-sm desc-toggle"
type="button"
@click="descExpanded = !descExpanded"
>{{ descExpanded ? $t('taskDetail.collapse') : $t('taskDetail.expand') }}</button>
<div v-if="descCollapsible && !descExpanded" class="desc-fade"></div>
</div>
<div v-else class="empty-hint desc-empty">{{ $t('taskDetail.descEmpty') }}</div>
<button class="btn btn-ghost btn-sm desc-edit-btn" :disabled="savingDesc" @click="startEditDesc">
{{ task.description ? $t('taskDetail.editDesc') : $t('taskDetail.addDesc') }}
</button>
</template>
</section>
<!-- 关联信息(Problem4 去重 + 空值不显行)
@@ -153,7 +167,7 @@
<div v-if="childMenuId === child.id" class="quick-menu" @click.stop>
<div class="quick-menu-section">
<div class="quick-menu-label">{{ $t('tasks.quickStatus') }}</div>
<button v-for="s in quickStatuses" :key="s.key" class="quick-menu-item" @click="advanceChild(child, s.key)">
<button v-for="s in quickStatusesFor(child)" :key="s.key" class="quick-menu-item" @click="advanceChild(child, s.key)">
<span>{{ s.icon }}</span>{{ s.label }}
</button>
</div>
@@ -288,6 +302,11 @@ const quickStatuses = computed(() => [
{ key: 'done', icon: '✅', label: t('tasks.statusFilter.done') },
{ key: 'blocked', icon: '🚫', label: t('tasks.statusFilter.blocked') },
])
/** 子任务快捷推进目标 = 状态机合法下一态(ADVANCE_MAP 过滤,与顶部手动按钮同源) */
function quickStatusesFor(child: TaskRecord) {
const valid = new Set((ADVANCE_MAP[child.status] ?? []).map((a) => a.target))
return quickStatuses.value.filter((s) => valid.has(s.key))
}
const quickPriorities = computed(() => [
{ value: 0, label: t('tasks.modal.priorityCritical'), cls: 'priority-critical' },
{ value: 1, label: t('tasks.modal.priorityHigh'), cls: 'priority-high' },
@@ -364,11 +383,14 @@ function openSubtaskModal() {
async function confirmSubtask() {
const cur = task.value
if (!cur || !subtaskTitle.value.trim() || subtaskSubmitting.value) return
const title = subtaskTitle.value.trim()
if (title.length > 128) { errorMsg.value = t('taskDetail.subtaskTitleTooLong'); return }
if (children.value.some((c) => c.title === title)) { errorMsg.value = t('taskDetail.subtaskDuplicate'); return }
subtaskSubmitting.value = true
try {
const r = await taskApi.create({
project_id: cur.project_id,
title: subtaskTitle.value.trim(),
title,
description: subtaskDesc.value.trim() || undefined,
priority: subtaskPriority.value,
// project_id 继承当前任务、parent_id = 当前任务 id
@@ -376,6 +398,7 @@ async function confirmSubtask() {
})
if (!r) return
showSubtaskModal.value = false
errorMsg.value = ''
await loadChildren()
} catch (e: any) {
errorMsg.value = t('taskDetail.addSubtaskFailed', { msg: e?.toString() ?? t('common.unknownError') })
@@ -396,6 +419,15 @@ const wfResult = ref<'completed' | 'failed' | null>(null)
// SW-260618-21: 终态提示 timer 引用,卸载时清理防写已销毁 ref(对齐 Projects.vue _toastTimer 模式)
let _wfResultTimer: ReturnType<typeof setTimeout> | null = null
/** 重置工作流推进相关 UI 态(切任务/refresh/手动推进成功后调用。
注意:df-data-changed 触发的 load() 不调此函数,避免工作流推进中 AI 工具改任务触发刷新时进度面板被误清) */
function resetWorkflowUi() {
wfExecId.value = null
wfDagJson.value = ''
wfResult.value = null
if (_wfResultTimer) { clearTimeout(_wfResultTimer); _wfResultTimer = null }
}
// B-41: 共享工作流进度推导。workflow store 已全局收集 liveEvents,此处按 execId
// 派生节点状态/进度,与 ProjectDetail 同源(store.workflowProgress),替代原本地 onEvent 逐事件累加。
const wfProgress = computed(() => store.workflowProgress(wfExecId.value))
@@ -469,6 +501,30 @@ async function measureDescHeight() {
descOverflow.value = el.scrollHeight > DESC_COLLAPSE_PX
}
// P2-10: 描述编辑(对齐 IdeaDetail 模式;taskApi.update(id, 'description', value))
const editingDesc = ref(false)
const editDesc = ref('')
const savingDesc = ref(false)
function startEditDesc() {
editDesc.value = task.value?.description ?? ''
editingDesc.value = true
}
function cancelEditDesc() { editingDesc.value = false }
async function saveEditDesc() {
if (!task.value || savingDesc.value) return
savingDesc.value = true
try {
await taskApi.update(task.value.id, 'description', editDesc.value)
task.value = { ...task.value, description: editDesc.value }
editingDesc.value = false
errorMsg.value = ''
} catch (e: any) {
errorMsg.value = t('taskDetail.descSaveFailed', { msg: e?.toString() ?? t('common.unknownError') })
} finally {
savingDesc.value = false
}
}
// F-AiNodeSelfReview: output_json 解析 + AI 产出/自审渲染已抽至 TaskOutputCard 子组件
// (components/task/TaskOutputCard.vue),父级只传 output-json prop,零行为变更。
@@ -569,6 +625,8 @@ async function handleAdvance(target: string) {
// advance_task 返回更新后的 TaskRecord(含新 status / review_rounds)
const updated = await taskApi.advance(task.value.id, target)
task.value = updated
// P2-8: 手动推进成功后清工作流 UI 态(防上一轮工作流残留进度/终态提示污染新任务态)
resetWorkflowUi()
errorMsg.value = ''
} catch (e: any) {
errorMsg.value = t('taskDetail.advanceFailed', { msg: e?.toString() ?? t('common.unknownError') })
@@ -631,11 +689,12 @@ async function load() {
}
function refresh() {
resetWorkflowUi()
load()
}
// 路由参数变化(id 变化)时重新加载
watch(taskId, () => { load() })
watch(taskId, () => { resetWorkflowUi(); load() })
// Problem4 ②:切换 task / 描述渲染完成 → 重置折叠态 + 重测高度。
// renderedDesc 异步(Markdown 经 marked 渲染),内容变化后 nextTick 测真实 px。
@@ -974,6 +1033,33 @@ onBeforeUnmount(() => {
.subtask-empty { padding: 16px 8px; }
.subtask-add { margin-top: 8px; }
/* P2-9 子任务提示条 */
.subtask-hint {
font-size: 12px;
color: var(--df-text-secondary);
background: var(--df-accent-bg);
padding: 6px 12px;
border-radius: var(--df-radius-sm);
margin-bottom: var(--df-gap-grid);
}
/* P2-10 描述编辑(对齐 IdeaDetail .detail-desc-edit 模式) */
.detail-desc-edit {
width: 100%;
min-height: 80px;
background: var(--df-bg);
color: var(--df-text);
border: 0.5px solid var(--df-border);
border-radius: var(--df-radius-sm);
padding: 8px 10px;
font-size: 13px;
line-height: 1.5;
resize: vertical;
}
.desc-edit-actions { display: flex; gap: 8px; margin-top: 8px; }
.desc-empty { padding: 16px 8px; }
.desc-edit-btn { margin-top: 8px; }
/* Problem4 ⑤ 响应式:窄屏(<960px)两栏退单列,产出栏移到下方 */
@media (max-width: 960px) {
.detail-grid { grid-template-columns: 1fr; }