修复: gen_stream判重+重试分类+状态机最短路径+既有测试修复
This commit is contained in:
+147
-12
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user