修复: 项目详情批量(任务列表联动刷新/工作流降级标注/中文git乱码/子目录错误行内/零工程空态/切tab保活) + 依赖图批量(节点渲染register/环高亮/防环防重/点击选中) + 销账
This commit is contained in:
+123
-20
@@ -77,14 +77,19 @@
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<!-- 文件浏览器 Tab -->
|
||||
<!-- 文件浏览器 Tab(B5[PD-P2-12]:KeepAlive 保活 —— 切 overview 不销毁文件 tab 的
|
||||
展开目录/选中文件/视图模式状态;首次激活才跑 onMounted,后续激活走 onActivated 复用状态) -->
|
||||
<section v-if="activeTab === 'files'" class="file-explorer-wrap">
|
||||
<FileExplorer :project-id="projectId" />
|
||||
<KeepAlive>
|
||||
<FileExplorer :project-id="projectId" />
|
||||
</KeepAlive>
|
||||
</section>
|
||||
|
||||
<!-- 依赖图 Tab -->
|
||||
<!-- 依赖图 Tab(B5:同样 KeepAlive 保活,避免每次切 tab 重挂载重拉) -->
|
||||
<section v-else-if="activeTab === 'graph'" class="file-explorer-wrap">
|
||||
<DependencyGraph :project-id="projectId" />
|
||||
<KeepAlive>
|
||||
<DependencyGraph :project-id="projectId" />
|
||||
</KeepAlive>
|
||||
</section>
|
||||
|
||||
<!-- 概览 Tab:两栏(项目信息 + 任务列表),工作流日志移至下方(P1-g+ 问题10 三栏拥挤) -->
|
||||
@@ -174,8 +179,8 @@
|
||||
</span>
|
||||
</div>
|
||||
<p v-if="m.description" class="module-desc-overview">{{ m.description }}</p>
|
||||
<div v-if="parseStack(m.stack).length" class="module-stack-overview">
|
||||
<span class="tech-tag" v-for="tag in parseStack(m.stack)" :key="tag">{{ tag }}</span>
|
||||
<div v-if="(moduleStack.get(m.id) ?? []).length" class="module-stack-overview">
|
||||
<span class="tech-tag" v-for="tag in moduleStack.get(m.id)" :key="tag">{{ tag }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -183,10 +188,10 @@
|
||||
</div>
|
||||
|
||||
<!-- 技术栈 -->
|
||||
<div class="info-item" v-if="parseStack(currentProject.stack).length">
|
||||
<div class="info-item" v-if="projectStack.length">
|
||||
<span class="label">{{ $t('projectDetail.techStackLabel') }}</span>
|
||||
<div class="info-tags">
|
||||
<span class="tech-tag" v-for="t in parseStack(currentProject.stack)" :key="t">{{ t }}</span>
|
||||
<span class="tech-tag" v-for="t in projectStack" :key="t">{{ t }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
@@ -203,6 +208,7 @@
|
||||
P1-g+ 问题10③:长描述折叠(对齐 TaskDetail 长内容),默认折叠截 4 行,展开看全文 -->
|
||||
<div v-if="currentProject.description" class="description-wrap" :class="{ 'is-collapsed': !descExpanded }">
|
||||
<span
|
||||
ref="descEl"
|
||||
class="value description ai-md"
|
||||
v-html="renderedDesc"
|
||||
></span>
|
||||
@@ -265,7 +271,7 @@
|
||||
<div
|
||||
class="score-bar-fill"
|
||||
:class="'fill-' + scoreTier(dim.score)"
|
||||
:style="{ width: dim.score + '%' }"
|
||||
:style="{ width: Math.min(100, Math.max(0, dim.score)) + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
<span class="score-bar-value">{{ dim.score }}</span>
|
||||
@@ -326,10 +332,13 @@
|
||||
<WorkflowDagDisplay v-if="wfDagJson" :dag-json="wfDagJson" :node-statuses="wfProgress.nodeStatuses" />
|
||||
</section>
|
||||
|
||||
<!-- 工作流日志:无记录时折叠为摘要(标题 + 计数),有记录才展开列表 -->
|
||||
<section v-if="formattedEvents.length > 0 || !workflowLogCollapsed" class="panel workflow-log-panel">
|
||||
<!-- 工作流日志:A2[PD-P1-3]默认折叠为摘要(标题 + 全局标注 + 计数),有记录才展开列表。
|
||||
不设 v-if 条件(否则空事件 + 默认折叠时整面板消失,而非「折叠」) -->
|
||||
<section class="panel workflow-log-panel">
|
||||
<div class="panel-header">
|
||||
<h2>{{ $t('projectDetail.workflowLogTitle') }}</h2>
|
||||
<!-- A2[PD-P1-3]:事件流无项目维度,标注「全局事件」避免误读为本项目日志 -->
|
||||
<span class="global-event-tag">{{ $t('projectDetail.workflowLogGlobal') }}</span>
|
||||
<span v-if="formattedEvents.length === 0" class="task-count">{{ $t('projectDetail.emptyWorkflowLog') }}</span>
|
||||
<button
|
||||
v-else
|
||||
@@ -406,11 +415,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { ref, computed, 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'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { listen, type UnlistenFn } from '@tauri-apps/api/event'
|
||||
import { useProjectStore } from '@/stores/project'
|
||||
import { projectApi, taskApi, workflowApi } from '@/api'
|
||||
import { moduleApi, type ProjectModuleRecord } from '@/api/module'
|
||||
@@ -425,7 +435,7 @@ 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 type { ProjectId, TaskRecord } from '@/api/types'
|
||||
import type { ProjectId, TaskRecord, DfDataChangedPayload } from '@/api/types'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -434,6 +444,12 @@ const { t, locale } = useI18n()
|
||||
const logListRef = ref<HTMLElement | null>(null)
|
||||
const showApprovalDialog = ref(false)
|
||||
|
||||
// A1[PD-P1-4]:任务列表随 AI 改动刷新 —— 本地 df-data-changed 监听(entity=task)重拉 loadProjectTasks。
|
||||
// 对齐 TaskDetail B-260616-18 模式:onMounted 注册、onBeforeUnmount 释放;连发事件防抖合并,
|
||||
// 且仅概览 tab 活跃(任务列表面板在概览)才刷新。
|
||||
let _unlistenDataChanged: UnlistenFn | null = null
|
||||
let _taskReloadTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
// Tab 导航:概览(项目信息/任务/工作流日志) vs 文件浏览器(Batch 10)。
|
||||
// P1-g+ 问题10④:activeTab 持久化 localStorage(按项目隔离,切换项目不串扰)。
|
||||
const TAB_STORAGE_PREFIX = 'df-project-tab-'
|
||||
@@ -452,14 +468,30 @@ function setActiveTab(tab: DetailTab) {
|
||||
}
|
||||
|
||||
// 描述折叠(P1-g+ 问题10③):长描述默认折叠截 4 行,展开看全文。
|
||||
// descCollapsible 由描述长度阈值决定(渲染后行数不可靠,用源字符数近似 ≥120 字符)。
|
||||
const DESC_COLLAPSE_THRESHOLD = 120
|
||||
// A8[PD-P3-16]:弃字符数近似(渲染后 Markdown 列表/标题行高不一,字符数与 px 无线性关系),
|
||||
// 对齐 TaskDetail 用 descEl 实测 scrollHeight —— 首次渲染 + renderedDesc 变化后 nextTick 测量。
|
||||
// descCollapsible 仅当真实渲染高度超过折叠阈值才为 true(避免短文本显无用的展开按钮)。
|
||||
const DESC_COLLAPSE_PX = 96 // 折叠 max-height 6em(≈4 行 ×14px×1.6)
|
||||
const descEl = ref<HTMLElement | null>(null)
|
||||
const descExpanded = ref(false)
|
||||
const descCollapsible = computed(() => (currentProject.value?.description?.length ?? 0) >= DESC_COLLAPSE_THRESHOLD)
|
||||
const descOverflow = ref(false)
|
||||
|
||||
const descCollapsible = computed(() => descOverflow.value)
|
||||
|
||||
async function measureDescHeight() {
|
||||
// descEl 是 <span>(描述内容),自身无 max-height 限制 → scrollHeight 即其真实渲染高度,
|
||||
// 不受父 .description-wrap 的 max-height 裁剪影响。
|
||||
await nextTick()
|
||||
const el = descEl.value
|
||||
if (!el) { descOverflow.value = false; return }
|
||||
descOverflow.value = el.scrollHeight > DESC_COLLAPSE_PX
|
||||
}
|
||||
|
||||
// 工作流日志折叠(P1-g+ 问题10②):无记录时折叠为摘要(标题 + emptyWorkflowLog 提示),
|
||||
// 有记录默认展开。collapse 由用户主动操作,避免空态占整栏。
|
||||
const workflowLogCollapsed = ref(false)
|
||||
// A2[PD-P1-3]:事件流无 project_id 维度,无法按项目过滤(workflow-event 负载仅 execution_id+event,
|
||||
// event 内无 task_id/project_id 映射),属全局事件。默认收起 + 标注「全局事件」,避免误读为本项目日志。
|
||||
const workflowLogCollapsed = ref(true)
|
||||
|
||||
// 确认弹层状态机抽至 composables/useConfirm(原 4 视图重复:Projects/ProjectDetail/Ideas/Settings)
|
||||
const { confirmState, confirmDialog, answerConfirm } = useConfirm()
|
||||
@@ -532,10 +564,18 @@ const assessmentLabel = (recommendation: string): string =>
|
||||
// 改为按 project_id 走后端 list_tasks {project_id} 独立拉取,存本地 ref。
|
||||
// TaskQuery.project_id 命中 idx_tasks_project_id(见 api/types.ts:191)。
|
||||
const projectTasks = ref<TaskRecord[]>([])
|
||||
// A4[PD-P2-2]:请求序号守卫 —— 快速切项目时旧请求晚到不覆盖新项目数据(闭包捕获 projectId,
|
||||
// resolve 后比对当前 projectId 相同才赋值)。
|
||||
let taskReqSeq = 0
|
||||
async function loadProjectTasks() {
|
||||
const seq = ++taskReqSeq
|
||||
const pid = projectId.value
|
||||
try {
|
||||
projectTasks.value = await taskApi.list({ project_id: projectId.value })
|
||||
const list = await taskApi.list({ project_id: pid })
|
||||
if (seq !== taskReqSeq || pid !== projectId.value) return
|
||||
projectTasks.value = list
|
||||
} catch (e) {
|
||||
if (seq !== taskReqSeq || pid !== projectId.value) return
|
||||
console.error('加载项目任务失败:', e)
|
||||
projectTasks.value = []
|
||||
}
|
||||
@@ -546,13 +586,24 @@ async function loadProjectTasks() {
|
||||
// 多工程(monorepo/多模块)在概览内增删改 + 扫描;复用 moduleApi + ConfirmDialog(删除确认)
|
||||
// + arco Message(反馈)。切到文件 Tab 时 FileExplorer 因 v-if 重挂载自动重拉,无需手动同步。
|
||||
const modules = ref<ProjectModuleRecord[]>([])
|
||||
// A4[PD-P2-2]:与 loadProjectTasks 同款请求序号守卫(快速切项目竞态)
|
||||
let moduleReqSeq = 0
|
||||
async function loadModules() {
|
||||
const seq = ++moduleReqSeq
|
||||
const pid = projectId.value
|
||||
try {
|
||||
modules.value = await moduleApi.listProjectModules(projectId.value)
|
||||
const list = await moduleApi.listProjectModules(pid)
|
||||
if (seq !== moduleReqSeq || pid !== projectId.value) return
|
||||
modules.value = list
|
||||
} catch {
|
||||
if (seq !== moduleReqSeq || pid !== projectId.value) return
|
||||
modules.value = []
|
||||
}
|
||||
}
|
||||
// A10[PD-P3-13]:parseStack 模板多次调用 → computed 缓存 —— 每项 m.stack 只 JSON.parse 一次,
|
||||
// 模板用 moduleStack.get(m.id) 取值;工程栈也单独缓存(切项目/增删工程后自动失效重算)。
|
||||
const moduleStack = computed(() => new Map(modules.value.map((m) => [m.id, parseStack(m.stack)])))
|
||||
const projectStack = computed(() => parseStack(currentProject.value?.stack))
|
||||
|
||||
/**
|
||||
* 工程状态归一:老工程 status 为 null 视作 active(默认活跃)。
|
||||
@@ -697,11 +748,19 @@ const latestWfExecId = computed(() => {
|
||||
const evs = store.liveEvents
|
||||
return evs.length > 0 ? evs[evs.length - 1].execution_id : null
|
||||
})
|
||||
// A2[PD-P1-3]:workflow-event 负载仅 { execution_id, event },event 内无 project_id/task_id 维度
|
||||
// 可映射到本项目 → 无法按项目过滤。概览的「工作流实时进度」面板改为仅当本页手动触发过工作流
|
||||
// (未来接推进入口时置 true)才显示,否则把其他项目的 DAG/进度误显在本项目下(降级标注,不强改事件协议)。
|
||||
const wfProgressEnabled = ref(false)
|
||||
const wfProgress = computed(() => store.workflowProgress(latestWfExecId.value))
|
||||
const hasWfProgress = computed(() => Object.keys(wfProgress.value.nodeStatuses).length > 0)
|
||||
const hasWfProgress = computed(() =>
|
||||
wfProgressEnabled.value && Object.keys(wfProgress.value.nodeStatuses).length > 0
|
||||
)
|
||||
watch(latestWfExecId, async (id) => {
|
||||
wfDagJson.value = ''
|
||||
if (!id) return
|
||||
// A2[PD-P1-3]:进度面板未启用(本页无工作流推进入口)时不拉 DAG,避免每次事件浪费一次 IPC
|
||||
if (!wfProgressEnabled.value) return
|
||||
try {
|
||||
const record = await workflowApi.getExecution(id)
|
||||
if (record?.dag_json) wfDagJson.value = record.dag_json
|
||||
@@ -743,6 +802,9 @@ async function submitNewTask() {
|
||||
async function handleSync() {
|
||||
await store.loadProjects()
|
||||
await loadProjectTasks() // 本项目任务独立拉取,不依赖 store.loadTasks
|
||||
// A9[PD-P3-18]:同步补齐工程列表 + 灵感回溯,避免「新增/扫描工程后点同步不刷新」
|
||||
await loadModules()
|
||||
await store.loadIdeas()
|
||||
await checkPath()
|
||||
}
|
||||
|
||||
@@ -860,19 +922,50 @@ onMounted(async () => {
|
||||
await store.loadIdeas()
|
||||
await store.startEventListener()
|
||||
await checkPath()
|
||||
// A8[PD-P3-16]:首次渲染后测描述真实高度(纯文本描述同步渲染,renderedDesc watch 可能不触发)
|
||||
await measureDescHeight()
|
||||
// A1[PD-P1-4]:注册任务数据变更监听(entity=task → 防抖重拉本项目任务)
|
||||
try {
|
||||
_unlistenDataChanged = await listen<DfDataChangedPayload>('df-data-changed', (event) => {
|
||||
if (event.payload.entity !== 'task') return
|
||||
// 仅概览 tab 活跃才刷(任务列表面板在概览;切到文件/图时后台事件不浪费请求)
|
||||
if (activeTab.value !== 'overview') return
|
||||
if (_taskReloadTimer) clearTimeout(_taskReloadTimer)
|
||||
_taskReloadTimer = setTimeout(() => { void loadProjectTasks() }, 200)
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('[ProjectDetail] 启动 df-data-changed 监听失败:', e)
|
||||
}
|
||||
})
|
||||
|
||||
// 路由切换到不同项目(URL /projects/:id 变化)时,重新拉取本项目任务 + 恢复 Tab + 检测目录
|
||||
watch(projectId, async (newId) => {
|
||||
if (!newId) return
|
||||
activeTab.value = readStoredTab(newId)
|
||||
// A5[PD-P2-5]:折叠态跨项目残留 —— 切项目重置描述展开 + 工作流日志折叠
|
||||
descExpanded.value = false
|
||||
workflowLogCollapsed.value = true
|
||||
await loadProjectTasks()
|
||||
await loadModules()
|
||||
})
|
||||
|
||||
// A3[PD-P2-1]:概览工程列表与 FileExplorer 双向同步 —— 文件 tab 内增删工程后,切回概览自动重拉
|
||||
watch(activeTab, (tab) => {
|
||||
if (tab === 'overview') void loadModules()
|
||||
})
|
||||
|
||||
// A8[PD-P3-16]:Markdown 异步渲染后 / 描述内容变化时重测折叠阈值
|
||||
watch(renderedDesc, () => { void measureDescHeight() })
|
||||
|
||||
// 目录变更(重定位后)重新检测存在性
|
||||
watch(() => currentProject.value?.path, () => { checkPath() })
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// A1[PD-P1-4]:释放本地 df-data-changed 监听 + 防抖定时器(防切页后定时器误刷已卸载视图)
|
||||
if (_taskReloadTimer) { clearTimeout(_taskReloadTimer); _taskReloadTimer = null }
|
||||
if (_unlistenDataChanged) { _unlistenDataChanged(); _unlistenDataChanged = null }
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 用 store.stopEventListener 统一停全局监听并复位句柄(直接调 unlisten() 停监听但
|
||||
// 不复位 _eventUnlisten,后续 startEventListener 幂等会误返回已死句柄,共享化后必查)
|
||||
@@ -1259,6 +1352,16 @@ onUnmounted(() => {
|
||||
此处仅保留本组件特有 .task-count。 */
|
||||
.task-count { font-size: 12px; color: var(--df-text-dim); }
|
||||
|
||||
/* A2[PD-P1-3]:工作流日志「全局事件」标注(次要小字标签) */
|
||||
.global-event-tag {
|
||||
font-size: 10px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 8px;
|
||||
color: var(--df-text-dim);
|
||||
background: rgba(136, 136, 136, 0.12);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ===== 任务卡片 ===== */
|
||||
.task-list { display: flex; flex-direction: column; }
|
||||
.task-card {
|
||||
|
||||
Reference in New Issue
Block a user