修复: gen_stream判重+重试分类+状态机最短路径+既有测试修复

This commit is contained in:
lxy
2026-08-12 00:29:06 +08:00
parent 9407f821b9
commit d31a27512c
36 changed files with 2614 additions and 264 deletions
+147 -12
View File
@@ -294,6 +294,16 @@
<div class="panel-header">
<h2>{{ $t('projectDetail.taskListTitle') }}</h2>
<span class="task-count">{{ $t('projectDetail.taskCount', { n: projectTasks.length }) }}</span>
<!-- 全选(勾选后触发底部批量操作栏) -->
<label class="select-all" :title="$t('tasks.batch.selectHint')">
<input
type="checkbox"
:checked="selection.allSelected"
:indeterminate.prop="selection.someSelected"
@change="selection.toggleAll(($event.target as HTMLInputElement).checked)"
/>
{{ $t('tasks.filter.all') }}
</label>
</div>
<div class="task-filter-chips">
<button
@@ -313,6 +323,14 @@
:key="task.id"
@click="router.push('/tasks/' + task.id)"
>
<div class="task-card-checkbox">
<input
type="checkbox"
:checked="selection.isSelected(task.id)"
@click.stop
@change="selection.toggle(task.id, ($event.target as HTMLInputElement).checked)"
/>
</div>
<div class="task-top">
<span class="task-title">{{ task.title }}</span>
<span class="task-status" :class="taskStatusClass(task.status)">{{ $t(taskStatusLabel(task.status)) }}</span>
@@ -329,6 +347,27 @@
</div>
</div>
<div v-if="projectTasks.length === 0" class="empty-hint">{{ $t('projectDetail.emptyTasks') }}</div>
<!-- 批量操作栏(选中任务后浮现) -->
<TaskBatchBar
v-if="selection.count > 0"
:count="selection.count"
:busy="batch.busy"
:advance-targets="batchAdvanceTargets"
:cancel-available="batchAvail.cancel"
:defer-available="batchAvail.defer"
:resume-available="batchAvail.resume"
:cascade-estimate="batchCascadeEstimate"
:toast="batch.toast"
:assignee-suggestions="assigneeSuggestions"
@advance="batch.advanceMany([...selection.selected], $event).then(afterBatch)"
@cancel="batch.cancelMany([...selection.selected]).then(afterBatch)"
@defer="batch.deferMany([...selection.selected]).then(afterBatch)"
@resume="batch.resumeMany([...selection.selected]).then(afterBatch)"
@priority="batch.updateMany([...selection.selected], 'priority', $event).then(afterBatch)"
@assignee="batch.updateMany([...selection.selected], 'assignee', $event).then(afterBatch)"
@delete="batch.deleteMany([...selection.selected]).then(afterBatch)"
@clear="selection.clear()"
/>
</section>
</div>
@@ -423,7 +462,7 @@
</template>
<script setup lang="ts">
import { ref, computed, nextTick, onBeforeUnmount, onMounted, onUnmounted, watch } from 'vue'
import { ref, computed, reactive, nextTick, onBeforeUnmount, onMounted, onUnmounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { Message } from '@arco-design/web-vue'
@@ -435,13 +474,16 @@ import { moduleApi, type ProjectModuleRecord } from '@/api/module'
import { formatDate } from '@/utils/time'
import { parseStack } from '@/utils/project'
import { parseScores as parseScoresJson, assessmentClass, assessmentLabel as assessmentLabelI18n, scoreTier } from '@/utils/ideaEval'
import { projectStatusLabel, taskStatusLabel, taskStatusClass } from '../constants/project'
import { projectStatusLabel, taskStatusLabel, taskStatusClass, TASK_STATUS_TRANSITIONS, TASK_STATUS_ORDER } from '../constants/project'
import ConfirmDialog from '@/components/ConfirmDialog.vue'
import TaskBatchBar from '@/components/task/TaskBatchBar.vue'
import FileExplorer from '@/components/project/FileExplorer.vue'
import DependencyGraph from '@/components/project/DependencyGraph.vue'
import WorkflowDagDisplay from '@/components/workflow/WorkflowDagDisplay.vue'
import { useConfirm } from '@/composables/useConfirm'
import { useRendered } from '@/composables/useMarkdown'
import { useTaskBatchSelection } from '@/composables/task/useTaskBatchSelection'
import { useTaskBatchActions } from '@/composables/task/useTaskBatchActions'
import type { ProjectId, TaskRecord, DfDataChangedPayload } from '@/api/types'
const route = useRoute()
@@ -588,16 +630,20 @@ async function loadProjectTasks() {
}
// taskStatusLabel / taskStatusClass 由 ../constants/project 提供
// 任务状态多选过滤
const taskStatusFilters = computed(() => [
{ key: 'todo', icon: '📝', label: t('tasks.statusFilter.todo') },
{ key: 'in_progress', icon: '🔨', label: t('tasks.statusFilter.in_progress') },
{ key: 'in_review', icon: '👀', label: t('tasks.statusFilter.in_review') },
{ key: 'testing', icon: '🧪', label: t('tasks.statusFilter.testing') },
{ key: 'done', icon: '✅', label: t('tasks.statusFilter.done') },
{ key: 'blocked', icon: '🚫', label: t('tasks.statusFilter.blocked') },
{ key: 'cancelled', icon: '🗑️', label: t('tasks.statusFilter.cancelled') },
])
// 任务状态多选过滤(排序由 TASK_STATUS_ORDER 统一驱动)
const STATUS_FILTER_META: Record<string, { icon: string; labelKey: string }> = {
todo: { icon: '📝', labelKey: 'tasks.statusFilter.todo' },
in_progress: { icon: '🔨', labelKey: 'tasks.statusFilter.in_progress' },
in_review: { icon: '👀', labelKey: 'tasks.statusFilter.in_review' },
testing: { icon: '🧪', labelKey: 'tasks.statusFilter.testing' },
done: { icon: '✅', labelKey: 'tasks.statusFilter.done' },
blocked: { icon: '🚫', labelKey: 'tasks.statusFilter.blocked' },
deferred: { icon: '', labelKey: 'tasks.statusFilter.deferred' },
cancelled: { icon: '🗑️', labelKey: 'tasks.statusFilter.cancelled' },
}
const taskStatusFilters = computed(() =>
TASK_STATUS_ORDER.map(key => ({ key, icon: STATUS_FILTER_META[key].icon, label: t(STATUS_FILTER_META[key].labelKey) })),
)
const activeTaskStatuses = ref<string[]>([])
function toggleTaskStatus(key: string) {
const idx = activeTaskStatuses.value.indexOf(key)
@@ -612,6 +658,68 @@ const filteredProjectTasks = computed(() => {
return projectTasks.value.filter(t => activeTaskStatuses.value.includes(t.status))
})
// ── 任务批量操作(本项目任务列表);reactive 包装使模板属性自动解包 ──
const selection = reactive(useTaskBatchSelection({
scopeIds: computed(() => filteredProjectTasks.value.map(t => t.id)),
}))
const batch = reactive(useTaskBatchActions({
getTask: id => projectTasks.value.find(t => t.id === id),
onRefresh: () => loadProjectTasks(),
}))
async function afterBatch() {
selection.prune()
}
const batchAdvanceTargets = computed(() => {
const ids = [...selection.selected]
if (ids.length === 0) return []
let common: Set<string> | null = null
for (const id of ids) {
const task = projectTasks.value.find(t => t.id === id)
if (!task) continue
const legal = new Set(TASK_STATUS_TRANSITIONS[task.status] ?? [])
if (common === null) {
common = legal
} else {
const next = new Set<string>()
for (const s of common) if (legal.has(s)) next.add(s)
common = next
}
if (common.size === 0) return []
}
return taskStatusFilters.value.filter(s => common?.has(s.key))
})
const RESUME_STATES = new Set(['deferred', 'cancelled', 'blocked'])
const batchAvail = computed(() => {
let cancel = false
let defer = false
let resume = false
for (const id of selection.selected) {
const task = projectTasks.value.find(t => t.id === id)
if (!task) continue
if (TASK_STATUS_TRANSITIONS[task.status]?.includes('cancelled')) cancel = true
if (TASK_STATUS_TRANSITIONS[task.status]?.includes('deferred')) defer = true
if (RESUME_STATES.has(task.status)) resume = true
}
return { cancel, defer, resume }
})
const batchCascadeEstimate = computed(() => {
const selected = selection.selected
let n = 0
for (const id of selected) {
const task = projectTasks.value.find(t => t.id === id)
if (!task || task.parent_id) continue
for (const c of projectTasks.value) {
if (c.parent_id === id && !selected.has(c.id)) n++
}
}
return n
})
const assigneeSuggestions = computed(() => {
const set = new Set<string>()
for (const t of projectTasks.value) if (t.assignee) set.add(t.assignee)
return [...set]
})
// ── 工程列表(概览内 CRUD)──
// 多工程(monorepo/多模块)在概览内增删改 + 扫描;复用 moduleApi + ConfirmDialog(删除确认)
// + arco Message(反馈)。切到文件 Tab 时 FileExplorer 因 v-if 重挂载自动重拉,无需手动同步。
@@ -1400,6 +1508,8 @@ onUnmounted(() => {
color: var(--df-accent);
border-color: var(--df-accent);
}
/* 已暂缓状态 badge */
.status-deferred { background: rgba(255,193,7,0.15); color: #f0a030; }
/* A2[PD-P1-3]:工作流日志「全局事件」标注(次要小字标签) */
.global-event-tag {
@@ -1413,6 +1523,31 @@ onUnmounted(() => {
/* ===== 任务卡片 ===== */
.task-list { display: flex; flex-direction: column; }
/* 批量操作:全选 + 行勾选(checkbox 绝对定位,卡片内容右移避开) */
.select-all {
display: flex;
align-items: center;
gap: 4px;
margin-left: auto;
font-size: 12px;
color: var(--df-text-dim);
cursor: pointer;
user-select: none;
white-space: nowrap;
}
.select-all input { width: 14px; height: 14px; accent-color: var(--df-accent); cursor: pointer; }
.task-card { position: relative; }
.task-card-checkbox {
position: absolute;
left: 2px;
top: 50%;
transform: translateY(-50%);
z-index: 1;
display: flex;
}
.task-card-checkbox input { width: 14px; height: 14px; accent-color: var(--df-accent); cursor: pointer; }
.task-top, .task-branch, .task-meta { padding-left: 18px; }
.task-card {
padding: 14px 0;
border-bottom: 0.5px solid var(--df-border);