修复: TaskDetail 操作按钮缺陷(推进竞态/菜单状态机过滤/工作流残留重置/子任务校验/描述编辑) + 看板销账(aichat切换缺陷 14/16 已修核对)
This commit is contained in:
+104
-18
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user