@@ -2,11 +2,22 @@
< div class = "project-detail" >
< div class = "project-detail" >
<!-- 加载态 -- >
<!-- 加载态 -- >
< div v-if = "loading" class="loading-state" > {{ $ t ( ' common.loading ' ) }} < / div >
< div v-if = "loading" class="loading-state" > {{ $ t ( ' common.loading ' ) }} < / div >
< ! - - 加载失败 ( 列表拉取报错 , 单列给重试 , 不让失败伪装成 「 项目不存在 」 ) - - >
< div v-else-if = "loadFailed" class="empty-state" >
< p > { { store . error } } < / p >
< button class = "btn btn-primary" @click ="retryLoad" > {{ $ t ( ' common.retry ' ) }} < / button >
< / div >
< ! - - 项目不存在 - - >
< ! - - 项目不存在 - - >
< div v-else-if = "!currentProject" class="empty-state" >
< div v-else-if = "!currentProject" class="empty-state" >
< p > { { $t ( 'projectDetail.notFound' ) } } < / p >
< p > { { $t ( 'projectDetail.notFound' ) } } < / p >
< router-link to = "/projects" class = "btn btn-primary" > { { $t ( 'projectDetail.backToList' ) } } < / router-link >
< router-link to = "/projects" class = "btn btn-primary" > { { $t ( 'projectDetail.backToList' ) } } < / router-link >
< / div >
< / div >
<!-- 已归档 ( 归档项目不在主列表 , 直链可达 ; 给归档箱恢复入口 ) -- >
< div v-else-if = "currentProject.archived" class="empty-state" >
< p > { { $t ( 'projectDetail.archivedNotice' ) } } < / p >
< button class = "btn btn-primary" @click ="restoreFromArchive" > {{ $ t ( ' projectDetail.restoreFromArchive ' ) }} < / button >
< router -link to = "/projects" class = "btn btn-ghost" > { { $t ( 'projectDetail.backToList' ) } } < / router-link >
< / div >
<!-- 项目详情 -- >
<!-- 项目详情 -- >
< template v-else >
< template v-else >
< ! - - 页面头部 - - >
< ! - - 页面头部 - - >
@@ -17,7 +28,10 @@
< span v-if = "statusLabel" class="stage-badge" :class="'stage-' + stageKey" > {{ $ t ( statusLabel ) }} < / span >
< span v-if = "statusLabel" class="stage-badge" :class="'stage-' + stageKey" > {{ $ t ( statusLabel ) }} < / span >
< / div >
< / div >
< div class = "header-actions" >
< div class = "header-actions" >
< button class = "btn btn-ghost" @click ="handleSync" > {{ $ t ( ' projectDetail.sync ' ) }} < / button >
< button class = "btn btn-ghost" :disabled = "syncing" @click ="handleSync" >
< span v-if = "syncing" class="spinner" > < / span >
{{ $ t ( ' projectDetail.sync ' ) }}
< / button >
< button class = "btn btn-ghost" type = "button" @click ="handleImportDir" > {{ $ t ( ' projectDetail.importDir ' ) }} < / button >
< button class = "btn btn-ghost" type = "button" @click ="handleImportDir" > {{ $ t ( ' projectDetail.importDir ' ) }} < / button >
< button class = "btn btn-danger" @click ="handleDelete" > {{ $ t ( ' projectDetail.delete ' ) }} < / button >
< button class = "btn btn-danger" @click ="handleDelete" > {{ $ t ( ' projectDetail.delete ' ) }} < / button >
< button class = "btn btn-primary" @click ="showNewTaskModal = true" > {{ $ t ( ' projectDetail.newTask ' ) }} < / button >
< button class = "btn btn-primary" @click ="showNewTaskModal = true" > {{ $ t ( ' projectDetail.newTask ' ) }} < / button >
@@ -293,7 +307,7 @@
< section class = "panel" >
< section class = "panel" >
< div class = "panel-header" >
< div class = "panel-header" >
< h2 > { { $t ( 'projectDetail.taskListTitle' ) } } < / h2 >
< h2 > { { $t ( 'projectDetail.taskListTitle' ) } } < / h2 >
< span class = "task-count" > { { $t ( 'projectDetail.taskCount' , { n : projectTasks . length } ) } } < / span >
< span class = "task-count" > { { $t ( 'projectDetail.taskCount' , { n : filteredProjectTasks . length } ) } } < / span >
<!-- 全选 ( 勾选后触发底部批量操作栏 ) -- >
<!-- 全选 ( 勾选后触发底部批量操作栏 ) -- >
< label class = "select-all" :title = "$t('tasks.batch.selectHint')" >
< label class = "select-all" :title = "$t('tasks.batch.selectHint')" >
< input
< input
@@ -322,7 +336,7 @@
class = "task-card"
class = "task-card"
v-for = "task in filteredProjectTasks"
v-for = "task in filteredProjectTasks"
:key = "task.id"
:key = "task.id"
@click ="router.push('/tasks/' + task.id)"
@click ="openDrawer( task.id)"
>
>
< div class = "task-card-checkbox" >
< div class = "task-card-checkbox" >
< input
< input
@@ -348,6 +362,8 @@
< / div >
< / div >
< / div >
< / div >
< div v-if = "!tasksLoading && projectTasks.length === 0" class="empty-hint" > {{ $ t ( ' projectDetail.emptyTasks ' ) }} < / div >
< div v-if = "!tasksLoading && projectTasks.length === 0" class="empty-hint" > {{ $ t ( ' projectDetail.emptyTasks ' ) }} < / div >
< ! - - PD5 -12 : 筛选把列表清空后给提示 , 不留空白区 - - >
< div v-else-if = "!tasksLoading && filteredProjectTasks.length === 0" class="empty-hint" > {{ $ t ( ' projectDetail.filterEmptyTasks ' ) }} < / div >
< ! - - 批量操作栏 ( 选中任务后浮现 ) - - >
< ! - - 批量操作栏 ( 选中任务后浮现 ) - - >
< TaskBatchBar
< TaskBatchBar
v-if = "selection.count > 0"
v-if = "selection.count > 0"
@@ -418,6 +434,15 @@
<!-- 确认弹层 ( 删除 / 重定位确认 , 替代原生 window . confirm / alert ) -- >
<!-- 确认弹层 ( 删除 / 重定位确认 , 替代原生 window . confirm / alert ) -- >
< ConfirmDialog :visible = "confirmState.visible" :msg = "confirmState.msg" :danger-label = "confirmState.dangerLabel" @result ="answerConfirm" / >
< ConfirmDialog :visible = "confirmState.visible" :msg = "confirmState.msg" :danger-label = "confirmState.dangerLabel" @result ="answerConfirm" / >
<!-- 任务快开抽屉 ( PD5 - 22 ) : 点任务卡就地滑出详情 , 替代整页跳 / tasks / : id ( 其返回链固定回
/ tasks 会断项目上下文 ) ; 与 Tasks . vue 同款接线 , 推进成功后重拉本项目任务列表 -- >
< TaskDrawer
:task-id = "drawerTaskId"
:visible = "drawerVisible"
@close ="closeDrawer()"
@ advance -done = " loadProjectTasks ( ) "
/ >
<!-- 新增 / 编辑工程弹窗 ( 概览内 CRUD , 对标 FileExplorer 工程 modal ) -- >
<!-- 新增 / 编辑工程弹窗 ( 概览内 CRUD , 对标 FileExplorer 工程 modal ) -- >
< div v-if = "showModuleModal" class="modal-overlay" @click.self="showModuleModal = false" >
< div v-if = "showModuleModal" class="modal-overlay" @click.self="showModuleModal = false" >
< div class = "modal-box" >
< div class = "modal-box" >
@@ -463,7 +488,7 @@
< / template >
< / template >
< script setup lang = "ts" >
< script setup lang = "ts" >
import { ref , computed , reactive , nextTick , onBeforeUnmount , onMounted , onUnmounted , watch } from 'vue'
import { ref , computed , reactive , nextTick , onBeforeUnmount , onMounted , watch } from 'vue'
import { useRoute , useRouter } from 'vue-router'
import { useRoute , useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useI18n } from 'vue-i18n'
import { Message } from '@arco-design/web-vue'
import { Message } from '@arco-design/web-vue'
@@ -478,6 +503,7 @@ import { parseScores as parseScoresJson, assessmentClass, assessmentLabel as ass
import { projectStatusLabel , taskStatusLabel , taskStatusClass , TASK _STATUS _TRANSITIONS , TASK _STATUS _ORDER } from '../constants/project'
import { projectStatusLabel , taskStatusLabel , taskStatusClass , TASK _STATUS _TRANSITIONS , TASK _STATUS _ORDER } from '../constants/project'
import ConfirmDialog from '@/components/ConfirmDialog.vue'
import ConfirmDialog from '@/components/ConfirmDialog.vue'
import TaskBatchBar from '@/components/task/TaskBatchBar.vue'
import TaskBatchBar from '@/components/task/TaskBatchBar.vue'
import TaskDrawer from '@/components/task/TaskDrawer.vue'
import FileExplorer from '@/components/project/FileExplorer.vue'
import FileExplorer from '@/components/project/FileExplorer.vue'
import DependencyGraph from '@/components/project/DependencyGraph.vue'
import DependencyGraph from '@/components/project/DependencyGraph.vue'
import WorkflowDagDisplay from '@/components/workflow/WorkflowDagDisplay.vue'
import WorkflowDagDisplay from '@/components/workflow/WorkflowDagDisplay.vue'
@@ -485,7 +511,7 @@ import { useConfirm } from '@/composables/useConfirm'
import { useRendered } from '@/composables/useMarkdown'
import { useRendered } from '@/composables/useMarkdown'
import { useTaskBatchSelection } from '@/composables/task/useTaskBatchSelection'
import { useTaskBatchSelection } from '@/composables/task/useTaskBatchSelection'
import { useTaskBatchActions } from '@/composables/task/useTaskBatchActions'
import { useTaskBatchActions } from '@/composables/task/useTaskBatchActions'
import type { ProjectId , TaskRecord , DfDataChangedPayload } from '@/api/types'
import type { ProjectId , ProjectRecord , TaskRecord , DfDataChangedPayload } from '@/api/types'
const route = useRoute ( )
const route = useRoute ( )
const router = useRouter ( )
const router = useRouter ( )
@@ -520,7 +546,9 @@ function setActiveTab(tab: DetailTab) {
// A8[PD-P3-16]:弃字符数近似(渲染后 Markdown 列表/标题行高不一,字符数与 px 无线性关系),
// A8[PD-P3-16]:弃字符数近似(渲染后 Markdown 列表/标题行高不一,字符数与 px 无线性关系),
// 对齐 TaskDetail 用 descEl 实测 scrollHeight —— 首次渲染 + renderedDesc 变化后 nextTick 测量。
// 对齐 TaskDetail 用 descEl 实测 scrollHeight —— 首次渲染 + renderedDesc 变化后 nextTick 测量。
// descCollapsible 仅当真实渲染高度超过折叠阈值才为 true(避免短文本显无用的展开按钮)。
// descCollapsible 仅当真实渲染高度超过折叠阈值才为 true(避免短文本显无用的展开按钮)。
const DESC _COLLAPSE _PX = 96 // 折叠 max-height 6em(≈4 行 × 14px× 1.6)
// 折叠阈值必须与折叠裁剪高(.description-wrap.is-collapsed 的 max-height 6em = 14px× 6 = 84px)一致:
// 阈值高于裁剪高时,内容高落在 84~阈值 区间会「已被 CSS 裁掉尾部却无展开按钮」(4 行描述 89.6px 即命中)
const DESC _COLLAPSE _PX = 84
const descEl = ref < HTMLElement | null > ( null )
const descEl = ref < HTMLElement | null > ( null )
const descExpanded = ref ( false )
const descExpanded = ref ( false )
const descOverflow = ref ( false )
const descOverflow = ref ( false )
@@ -551,10 +579,54 @@ const { confirmState, confirmDialog, answerConfirm } = useConfirm()
// 状态文案/阶段进度统一走 ../constants/project(与 Projects/Tasks/Dashboard 一致,
// 状态文案/阶段进度统一走 ../constants/project(与 Projects/Tasks/Dashboard 一致,
// 根治此前 stageMap 含不存在的 testing 状态、三套映射互相矛盾)
// 根治此前 stageMap 含不存在的 testing 状态、三套映射互相矛盾)
const projectId = computed ( ( ) => route . params . id as string )
const projectId = computed ( ( ) => route . params . id as string )
// PD5-4:列表 find 不到时单查兜底(归档项目不在主列表/列表侧带 query 时直链仍可达),
// 同时消化 projectApi.get 死代码;「已归档/加载失败/不存在」三态由模板分支单列
const fallbackProject = ref < ProjectRecord | null > ( null )
const currentProject = computed ( ( ) =>
const currentProject = computed ( ( ) =>
store . projects . find ( p => p . id === projectId . value )
store . projects . find ( p => p . id === projectId . value ) ? ? fallbackProject . value
)
)
const loading = computed ( ( ) => store . projects . length === 0 && ! store . error )
// 兜底单查进行中也算加载中,避免先闪「项目不存在」
const fallbackLoading = ref ( false )
const loading = computed ( ( ) =>
( store . projects . length === 0 && ! store . error ) || fallbackLoading . value
)
// 列表拉取失败(store.error 置位且无项目可显)单列,给重试入口
const loadFailed = computed ( ( ) => ! loading . value && ! currentProject . value && ! ! store . error )
// 序号守卫(A4 同款):快速切项目时旧单查结果晚到不覆盖新项目
let projectReqSeq = 0
async function loadFallbackProject ( ) {
const seq = ++ projectReqSeq
const pid = projectId . value
if ( ! pid || store . projects . some ( p => p . id === pid ) ) { fallbackProject . value = null ; return }
fallbackLoading . value = true
try {
const record = await projectApi . get ( pid )
if ( seq !== projectReqSeq || pid !== projectId . value ) return
fallbackProject . value = record
} catch {
if ( seq !== projectReqSeq || pid !== projectId . value ) return
fallbackProject . value = null
} finally {
if ( seq === projectReqSeq ) fallbackLoading . value = false
}
}
// 加载失败重试(清 error 后重拉列表 + 兜底单查,期间 loading 态接管视图)
async function retryLoad ( ) {
store . clearError ( )
await store . loadProjects ( )
await loadFallbackProject ( )
}
// 归档箱恢复(取消归档回主列表;成功后 store.projects 已刷新,currentProject 自动命中列表)
async function restoreFromArchive ( ) {
const p = currentProject . value
if ( ! p ) return
await store . unarchiveProject ( p . id )
if ( store . error ) { Message . error ( store . error ) ; return }
Message . success ( t ( 'projectDetail.restoreSuccess' ) )
}
// 状态文案/阶段进度统一走 ../constants/project
// 状态文案/阶段进度统一走 ../constants/project
// 项目描述 Markdown 渲染(复用 AiChat/TaskDetail 同款渲染器,模块级单例),
// 项目描述 Markdown 渲染(复用 AiChat/TaskDetail 同款渲染器,模块级单例),
@@ -674,6 +746,9 @@ const batch = reactive(useTaskBatchActions({
async function afterBatch ( ) {
async function afterBatch ( ) {
selection . prune ( )
selection . prune ( )
}
}
// 幽灵勾选收敛:筛选切换/外部数据变更重拉后可见范围已变,选中集中被筛掉或已删除的 id 若残留,
// 批量栏计数含不可见任务、对其执行动作会落 failed —— 范围一变即收敛(afterBatch 只覆盖批量后时机)
watch ( filteredProjectTasks , ( ) => { selection . prune ( ) } )
const batchAdvanceTargets = computed ( ( ) => {
const batchAdvanceTargets = computed ( ( ) => {
const ids = [ ... selection . selected ]
const ids = [ ... selection . selected ]
if ( ids . length === 0 ) return [ ]
if ( ids . length === 0 ) return [ ]
@@ -725,6 +800,13 @@ const assigneeSuggestions = computed(() => {
return [ ... set ]
return [ ... set ]
} )
} )
// 任务快开抽屉状态(与 Tasks.vue 同款):打开是同步置 ref,无失败路径;
// 抽屉内数据拉取失败由 TaskDrawer 自身错误态兜底,页面行为不受影响
const drawerTaskId = ref < string | null > ( null )
const drawerVisible = computed ( ( ) => drawerTaskId . value !== null )
function openDrawer ( id : string ) { drawerTaskId . value = id }
function closeDrawer ( ) { drawerTaskId . value = null }
// ── 工程列表(概览内 CRUD)──
// ── 工程列表(概览内 CRUD)──
// 多工程(monorepo/多模块)在概览内增删改 + 扫描;复用 moduleApi + ConfirmDialog(删除确认)
// 多工程(monorepo/多模块)在概览内增删改 + 扫描;复用 moduleApi + ConfirmDialog(删除确认)
// + arco Message(反馈)。切到文件 Tab 时 FileExplorer 因 v-if 重挂载自动重拉,无需手动同步。
// + arco Message(反馈)。切到文件 Tab 时 FileExplorer 因 v-if 重挂载自动重拉,无需手动同步。
@@ -920,6 +1002,7 @@ const newTaskBranch = ref('')
const submitting = ref ( false )
const submitting = ref ( false )
async function submitNewTask ( ) {
async function submitNewTask ( ) {
if ( submitting . value ) return // 重入守卫:Enter 直调绕过按钮 disabled,IPC 期间二次 Enter 会重复建任务
if ( ! newTaskTitle . value . trim ( ) ) return
if ( ! newTaskTitle . value . trim ( ) ) return
submitting . value = true
submitting . value = true
try {
try {
@@ -929,7 +1012,11 @@ async function submitNewTask() {
description : newTaskDesc . value . trim ( ) ,
description : newTaskDesc . value . trim ( ) ,
branch _name : newTaskBranch . value . trim ( ) || undefined ,
branch _name : newTaskBranch . value . trim ( ) || undefined ,
} )
} )
if ( ! r ) return // 失败已 toast,保持弹窗不关
if ( ! r ) {
// store.createTask 失败只写 state.error 不弹提示(useStoreAction 统一吞错),此处补页面可见反馈
if ( store . error ) Message . error ( store . error )
return // 保持弹窗不关,便于修正后重试
}
// 同步到本地 ref(store.createTask 已 push 全局 tasks,但本视图不再读 store.tasks)
// 同步到本地 ref(store.createTask 已 push 全局 tasks,但本视图不再读 store.tasks)
projectTasks . value = [ r , ... projectTasks . value ]
projectTasks . value = [ r , ... projectTasks . value ]
showNewTaskModal . value = false
showNewTaskModal . value = false
@@ -942,25 +1029,41 @@ async function submitNewTask() {
}
}
// ── 同步 ──
// ── 同步 ──
// PD5-9:busy 态防并发同步(串行 await 期间按钮可重复点)+ 完成轻提示
const syncing = ref ( false )
async function handleSync ( ) {
async function handleSync ( ) {
await store . loadProjects ( )
if ( syncing . value ) return
await loadProjectTasks ( ) // 本项目任务独立拉取,不依赖 store.loadTasks
syncing . value = true
// A9[PD-P3-18]:同步补齐工程列表 + 灵感回溯,避免「新增/扫描工程后点同步不刷新」
try {
await loadModules ( )
await store . loadProjects ( )
await store . loadIdeas ( )
await loadFallbackProject ( ) // AI 工具可在别处归档本项目,列表外项目靠兜底单查刷新
await checkPath ( )
await loadProjectTasks ( ) // 本项目任务独立拉取,不依赖 store.loadTasks
// A9[PD-P3-18]:同步补齐工程列表 + 灵感回溯,避免「新增/扫描工程后点同步不刷新」
await loadModules ( )
await store . loadIdeas ( )
await checkPath ( )
Message . success ( t ( 'projectDetail.syncDone' ) )
} finally {
syncing . value = false
}
}
}
// ── 代码目录绑定 ──
// ── 代码目录绑定 ──
const pathExists = ref < boolean | null > ( null )
const pathExists = ref < boolean | null > ( null )
// 检查绑定目录是否存在(详情页「目录是否还在」)
// 检查绑定目录是否存在(详情页「目录是否还在」)
// PD5-14:请求序号守卫(A4 同款)—— 快速切项目时旧检测结果晚到,不覆盖新项目目录状态
let pathReqSeq = 0
async function checkPath ( ) {
async function checkPath ( ) {
const seq = ++ pathReqSeq
const p = currentProject . value
const p = currentProject . value
if ( ! p ? . path ) { pathExists . value = null ; return }
if ( ! p ? . path ) { pathExists . value = null ; return }
try {
try {
pathExists . value = await projectApi . checkPathExists ( p . path )
const exists = await projectApi . checkPathExists ( p . path )
if ( seq !== pathReqSeq ) return
pathExists . value = exists
} catch {
} catch {
if ( seq !== pathReqSeq ) return
pathExists . value = null
pathExists . value = null
}
}
}
}
@@ -1026,8 +1129,13 @@ async function handleImportDir() {
async function handleDelete ( ) {
async function handleDelete ( ) {
const p = currentProject . value
const p = currentProject . value
if ( ! p ) return
if ( ! p ) return
if ( ! await confirmDialog ( t ( 'projectDetail.confirmDelete' , { name : p . name } ) ) ) return
if ( ! await confirmDialog ( t ( 'projectDetail.confirmDelete' , { name : p . name } ) , t ( 'common.delete' ) ) ) return
await store . deleteProject ( p . id )
const ok = await store . deleteProject ( p . id )
if ( ! ok ) {
// 删除失败留在本页(store 已写 state.error,此处补可见提示),不能与成功同样跳走
if ( store . error ) Message . error ( store . error )
return
}
router . push ( '/projects' )
router . push ( '/projects' )
}
}
@@ -1045,13 +1153,10 @@ onMounted(async () => {
// 恢复持久化的 Tab 选择(按项目隔离)
// 恢复持久化的 Tab 选择(按项目隔离)
activeTab . value = readStoredTab ( projectId . value )
activeTab . value = readStoredTab ( projectId . value )
await store . loadProjects ( )
await store . loadProjects ( )
// 本项目任务独立拉取(不调 store.loadTasks,避免被 Tasks 视图分页/筛选污染)
// PD5-10:任务/工程/灵感/兜底单查互不依赖,并行拉取缩短首屏串行链;
await loadProjectTasks ( )
// checkPath 依赖项目记录就位(列表或兜底),保持原序殿后。
// 工程列表独立拉取(FileExplorer 文件 Tab 内 CRUD,概览只读展示)
// 任务独立拉取不读 store.tasks(避免被 Tasks 视图分页/筛选污染);灵感补拉供来源回溯
await loadModules ( )
await Promise . all ( [ loadProjectTasks ( ) , loadModules ( ) , store . loadIdeas ( ) , loadFallbackProject ( ) ] )
// 来源灵感回溯:项目有 idea_id 但 store.ideas 为空(本页未 load 过)时补拉,
// 否则 sourceIdea computed 永远找不到对应灵感记录
await store . loadIdeas ( )
await store . startEventListener ( )
await store . startEventListener ( )
await checkPath ( )
await checkPath ( )
// A8[PD-P3-16]:首次渲染后测描述真实高度(纯文本描述同步渲染,renderedDesc watch 可能不触发)
// A8[PD-P3-16]:首次渲染后测描述真实高度(纯文本描述同步渲染,renderedDesc watch 可能不触发)
@@ -1073,12 +1178,16 @@ onMounted(async () => {
// 路由切换到不同项目(URL /projects/:id 变化)时,重新拉取本项目任务 + 恢复 Tab + 检测目录
// 路由切换到不同项目(URL /projects/:id 变化)时,重新拉取本项目任务 + 恢复 Tab + 检测目录
watch ( projectId , async ( newId ) => {
watch ( projectId , async ( newId ) => {
if ( ! newId ) return
if ( ! newId ) return
// 切项目清空上一项目的勾选集:参数级导航组件复用不清状态,残留选中在新项目列表查不到,批量动作全落 failed
selection . clear ( )
activeTab . value = readStoredTab ( newId )
activeTab . value = readStoredTab ( newId )
// A5[PD-P2-5]:折叠态跨项目残留 —— 切项目重置描述展开 + 工作流日志折叠
// A5[PD-P2-5]:折叠态跨项目残留 —— 切项目重置描述展开 + 工作流日志折叠
descExpanded . value = false
descExpanded . value = false
workflowLogCollapsed . value = true
workflowLogCollapsed . value = true
await loadProjectTasks ( )
await loadProjectTasks ( )
await loadModules ( )
await loadModules ( )
// PD5-4:切项目后重定兜底单查(上一项目的兜底记录不能带进新项目)
await loadFallbackProject ( )
} )
} )
// A3[PD-P2-1]:概览工程列表与 FileExplorer 双向同步 —— 文件 tab 内增删工程后,切回概览自动重拉
// A3[PD-P2-1]:概览工程列表与 FileExplorer 双向同步 —— 文件 tab 内增删工程后,切回概览自动重拉
@@ -1098,11 +1207,10 @@ onBeforeUnmount(() => {
if ( _unlistenDataChanged ) { _unlistenDataChanged ( ) ; _unlistenDataChanged = null }
if ( _unlistenDataChanged ) { _unlistenDataChanged ( ) ; _unlistenDataChanged = null }
} )
} )
onUnmounted ( ( ) => {
// 不在此停工作流事件监听:该监听是 workflow store 的全局单例(_eventUnlisten 共享,
// 用 store.stopEventListener 统一停全局监听并复位句柄(直接调 unlisten() 停监听但
// 本页与 TaskDetail 幂等共用),App.vue 的全局审批弹窗/浮窗全靠它常驻收事件置 pendingApproval。
// 不复位 _eventUnlisten,后续 startEventListener 幂等会误返回已死句柄,共享化后必查)
// 卸载时 stop 会误杀全局监听——用户进过一次详情页再离开,任意页面审批弹窗即全失聪,
store . stopEventListener ( )
// 故对齐 App.vue data-changed「启动后常驻」模式,监听随应用生命周期存活,页面卸载不停。
} )
< / script >
< / script >
< style scoped >
< style scoped >
@@ -1325,7 +1433,7 @@ onUnmounted(() => {
/* P1-g+ 问题10③:描述折叠(对齐 TaskDetail 长内容)— 折叠时 max-height 截断 + 渐变遮罩 */
/* P1-g+ 问题10③:描述折叠(对齐 TaskDetail 长内容)— 折叠时 max-height 截断 + 渐变遮罩 */
. description - wrap { position : relative ; }
. description - wrap { position : relative ; }
. description - wrap . is - collapsed . description {
. description - wrap . is - collapsed . description {
max - height : 6 em ; /* 约 4 行(line-height 1.5) */
max - height : 6 em ; /* 14px× 6=84px,与脚本 DESC_COLLAPSE_PX 同源,改一处必同步另一处 */
overflow : hidden ;
overflow : hidden ;
}
}
. desc - toggle { margin - top : 6 px ; }
. desc - toggle { margin - top : 6 px ; }