修复: 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
+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; }