新增: F-260619-01 任务可关联灵感(tasks.idea_id 1对1 单向)

- df-storage V20 迁移:tasks 加 idea_id TEXT REFERENCES ideas(id)(填预留空位,
  幂等 column_exists 探测)+ V9_SQL 同步 + TaskRecord.idea_id + task_repo SQL/SELECT + 白名单
- AI 工具 create_task 加 idea_id 参数;update_task 经白名单自动支持
- IPC commands/task.rs CreateTaskInput 加 idea_id
- 前端 types TaskRecord/CreateTaskInput idea_id + TaskDetail.vue 关联灵感展示
  (router-link 友好 title 非裸 id)+ i18n
- 补 idea_id 字段:df-mcp tools / df-nodes 测试 helper
- 单向 1对1,Option 可空,复用 projects.idea_id 模式,老任务 None 兼容

自验: df-storage 47 passed(含 V20 2)+ workspace EXIT 0 + vue-tsc EXIT 0
This commit is contained in:
2026-06-19 21:03:18 +08:00
parent e5a8fec1fe
commit 4a87c552cc
15 changed files with 158 additions and 17 deletions

View File

@@ -104,6 +104,16 @@
<span v-else></span>
</span>
</div>
<!-- F-260619-01:关联灵感(1对1 单向,idea_id 解析为友好 title,非裸 id) -->
<div class="info-item">
<span class="label">{{ $t('taskDetail.relatedIdea') }}</span>
<span class="value">
<router-link v-if="task.idea_id" :to="`/ideas/${task.idea_id}`" class="project-link">
{{ ideaTitle }}
</router-link>
<span v-else></span>
</span>
</div>
<div class="info-item">
<span class="label">{{ $t('taskDetail.branch') }}</span>
<span class="value">
@@ -146,7 +156,7 @@ import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
import { listen } from '@tauri-apps/api/event'
import { taskApi, projectApi } from '@/api'
import { taskApi, projectApi, ideaApi } from '@/api'
import { workflowApi } from '@/api'
import { formatDate } from '@/utils/time'
import { useRendered } from '@/composables/useMarkdown'
@@ -156,7 +166,7 @@ import {
priorityLabel,
priorityClass,
} from '../constants/project'
import type { TaskRecord, ProjectRecord, DfDataChangedPayload, WorkflowEventPayload } from '@/api/types'
import type { TaskRecord, ProjectRecord, IdeaRecord, DfDataChangedPayload, WorkflowEventPayload } from '@/api/types'
import TaskOutputCard from '@/components/task/TaskOutputCard.vue'
const { t } = useI18n()
@@ -166,6 +176,8 @@ const loading = ref(true)
const errorMsg = ref('')
const task = ref<TaskRecord | null>(null)
const projects = ref<ProjectRecord[]>([])
// F-260619-01:灵感列表用于解析 task.idea_id → 灵感 title(独立入口,同 projects 模式)
const ideas = ref<IdeaRecord[]>([])
const advancing = ref(false)
// ============================================================
@@ -204,6 +216,14 @@ const projectName = computed(() => {
return p?.name ?? task.value?.project_id ?? '—'
})
// F-260619-01:解析 task.idea_id → 灵感 title(找不到时回退 id,保证非空可点击)
const ideaTitle = computed(() => {
const ideaId = task.value?.idea_id
if (!ideaId) return '—'
const idea = ideas.value.find(i => i.id === ideaId)
return idea?.title ?? ideaId
})
// ============================================================
// F-05 推进按钮 — 状态机合法下一态映射
// ------------------------------------------------------------
@@ -379,13 +399,16 @@ async function load() {
loading.value = true
errorMsg.value = ''
try {
const [t, ps] = await Promise.all([
const [t, ps, ideasList] = await Promise.all([
taskApi.get(taskId.value),
// 项目列表用于解析 project_id → 项目名(独立入口,不依赖全局 store)
projectApi.list(),
// F-260619-01:灵感列表用于解析 idea_id → 灵感 title
ideaApi.list(),
])
task.value = t
projects.value = ps
ideas.value = ideasList
} catch (e: any) {
task.value = null
errorMsg.value = t('taskDetail.loadFailed', { msg: e?.toString() ?? t('common.unknownError') })
@@ -417,7 +440,7 @@ onMounted(async () => {
try {
_unlistenDataChanged = await listen<DfDataChangedPayload>('df-data-changed', (event) => {
const { entity } = event.payload
if (entity === 'task' || entity === 'project') {
if (entity === 'task' || entity === 'project' || entity === 'idea') {
load()
}
})