优化: 所有剩余UI/UX待办一批完成(持久化+AuditLog+解耦+total+原12大改+P2)
持久化(P1-c):新建 usePersistedRef composable,Tasks/AuditLog/ProjectDetail 等接入 localStorage
AuditLog(P1-d):后端 list_tool_executions 加 WHERE 筛选+返 {items,total,has_more},前端对接+长列折叠+筛选持久化
数据源解耦(P1-g):ProjectDetail projectTasks 按 project_id 独立加载 + ChatInput @项目联想独立加载(不读 store.tasks 当前页)
GitChanges(12a):后端 get_module_commits 加 git rev-list --count 返 total,前端显真实总数
原12大改:Dashboard 统计卡压底行(1)/Projects 列表卡片视图(2)/project_event 埋点排序(3)/TaskDetail 重设计(4)/IdeaDetail 重设计(5)/KnowledgeDetail 重设计(6)/界面持久化+侧栏Ctrl+B+审批数字键(7)/ProjectDetail 三栏改两栏(10)
P2打磨:文件浏览器(FileTree去重/FilePreview行号.md Diff/selectedFilePath归位)/settings反馈(假保存/端口校验)/AI会话(try-catch/scrollIntoView)/后端计数(move_queue事件/timeline total/workflow分页/import_batch分块)/杂项(TopBar/ConfirmDialog键盘/CIStatus i18n/ToolResultBody/ModuleNode/ApprovalDialog全选)
This commit is contained in:
+121
-42
@@ -55,7 +55,7 @@
|
||||
class="tab-btn"
|
||||
:class="{ 'tab-active': activeTab === 'overview' }"
|
||||
type="button"
|
||||
@click="activeTab = 'overview'"
|
||||
@click="setActiveTab('overview')"
|
||||
>
|
||||
{{ $t('projectDetail.tabOverview') }}
|
||||
</button>
|
||||
@@ -63,7 +63,7 @@
|
||||
class="tab-btn"
|
||||
:class="{ 'tab-active': activeTab === 'files' }"
|
||||
type="button"
|
||||
@click="activeTab = 'files'"
|
||||
@click="setActiveTab('files')"
|
||||
>
|
||||
{{ $t('fileExplorer.tabTitle') }}
|
||||
</button>
|
||||
@@ -71,7 +71,7 @@
|
||||
class="tab-btn"
|
||||
:class="{ 'tab-active': activeTab === 'graph' }"
|
||||
type="button"
|
||||
@click="activeTab = 'graph'"
|
||||
@click="setActiveTab('graph')"
|
||||
>
|
||||
{{ $t('dependencyGraph.tabTitle') }}
|
||||
</button>
|
||||
@@ -87,8 +87,9 @@
|
||||
<DependencyGraph :project-id="projectId" />
|
||||
</section>
|
||||
|
||||
<!-- 概览 Tab:原有主体两栏 -->
|
||||
<div v-else class="detail-grid">
|
||||
<!-- 概览 Tab:两栏(项目信息 + 任务列表),工作流日志移至下方(P1-g+ 问题10 三栏拥挤) -->
|
||||
<div v-else class="detail-overview">
|
||||
<div class="detail-grid">
|
||||
<!-- 左栏:项目信息 -->
|
||||
<section class="panel">
|
||||
<div class="panel-header">
|
||||
@@ -133,12 +134,22 @@
|
||||
</div>
|
||||
<div class="info-item info-block">
|
||||
<span class="label">{{ $t('projectDetail.description') }}</span>
|
||||
<!-- B-260615-25:项目描述 Markdown 渲染,复用 useMarkdown composable(同 B-24 TaskDetail) -->
|
||||
<span
|
||||
v-if="currentProject.description"
|
||||
class="value description ai-md"
|
||||
v-html="renderedDesc"
|
||||
></span>
|
||||
<!-- B-260615-25:项目描述 Markdown 渲染,复用 useMarkdown composable(同 B-24 TaskDetail)
|
||||
P1-g+ 问题10③:长描述折叠(对齐 TaskDetail 长内容),默认折叠截 4 行,展开看全文 -->
|
||||
<div v-if="currentProject.description" class="description-wrap" :class="{ 'is-collapsed': !descExpanded }">
|
||||
<span
|
||||
class="value description ai-md"
|
||||
v-html="renderedDesc"
|
||||
></span>
|
||||
<button
|
||||
v-if="descCollapsible"
|
||||
class="btn btn-ghost btn-sm desc-toggle"
|
||||
type="button"
|
||||
@click="descExpanded = !descExpanded"
|
||||
>
|
||||
{{ descExpanded ? $t('common.collapse') : $t('common.expand') }}
|
||||
</button>
|
||||
</div>
|
||||
<span v-else>—</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
@@ -207,7 +218,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 左栏:任务列表 -->
|
||||
<!-- 右栏:任务列表(本项目独立 fetch,不读 store.tasks,避免 Tasks 分页污染) -->
|
||||
<section class="panel">
|
||||
<div class="panel-header">
|
||||
<h2>{{ $t('projectDetail.taskListTitle') }}</h2>
|
||||
@@ -232,24 +243,30 @@
|
||||
</div>
|
||||
<div v-if="projectTasks.length === 0" class="empty-hint">{{ $t('projectDetail.emptyTasks') }}</div>
|
||||
</section>
|
||||
|
||||
<!-- 右栏 -->
|
||||
<div class="right-column">
|
||||
<!-- 工作流执行日志 -->
|
||||
<section class="panel">
|
||||
<div class="panel-header">
|
||||
<h2>{{ $t('projectDetail.workflowLogTitle') }}</h2>
|
||||
</div>
|
||||
<div class="log-list" ref="logListRef">
|
||||
<div class="log-item" v-for="(evt, idx) in formattedEvents" :key="idx" :class="'log-' + evt.level">
|
||||
<span class="log-time">{{ evt.time }}</span>
|
||||
<span class="log-level">{{ evt.level }}</span>
|
||||
<span class="log-msg">{{ evt.message }}</span>
|
||||
</div>
|
||||
<div v-if="formattedEvents.length === 0" class="empty-hint">{{ $t('projectDetail.emptyWorkflowLog') }}</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- 工作流日志:无记录时折叠为摘要(标题 + 计数),有记录才展开列表 -->
|
||||
<section v-if="formattedEvents.length > 0 || !workflowLogCollapsed" class="panel workflow-log-panel">
|
||||
<div class="panel-header">
|
||||
<h2>{{ $t('projectDetail.workflowLogTitle') }}</h2>
|
||||
<span v-if="formattedEvents.length === 0" class="task-count">{{ $t('projectDetail.emptyWorkflowLog') }}</span>
|
||||
<button
|
||||
v-else
|
||||
class="btn btn-ghost btn-sm"
|
||||
type="button"
|
||||
@click="workflowLogCollapsed = !workflowLogCollapsed"
|
||||
>
|
||||
{{ workflowLogCollapsed ? $t('common.expand') : $t('common.collapse') }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="formattedEvents.length > 0 && !workflowLogCollapsed" class="log-list" ref="logListRef">
|
||||
<div class="log-item" v-for="(evt, idx) in formattedEvents" :key="idx" :class="'log-' + evt.level">
|
||||
<span class="log-time">{{ evt.time }}</span>
|
||||
<span class="log-level">{{ evt.level }}</span>
|
||||
<span class="log-msg">{{ evt.message }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- 人工审批对话框(抽离至 components/project/ApprovalDialog.vue)-->
|
||||
@@ -273,7 +290,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { useProjectStore } from '@/stores/project'
|
||||
import { projectApi } from '@/api'
|
||||
import { projectApi, taskApi } from '@/api'
|
||||
import { formatDate } from '@/utils/time'
|
||||
import { parseStack } from '@/utils/project'
|
||||
import { parseScores as parseScoresJson, assessmentClass, assessmentLabel as assessmentLabelI18n, scoreTier } from '@/utils/ideaEval'
|
||||
@@ -284,7 +301,7 @@ import FileExplorer from '@/components/project/FileExplorer.vue'
|
||||
import DependencyGraph from '@/components/project/DependencyGraph.vue'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
import { useRendered } from '@/composables/useMarkdown'
|
||||
import type { ProjectId } from '@/api/types'
|
||||
import type { ProjectId, TaskRecord } from '@/api/types'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -295,7 +312,31 @@ const showApprovalDialog = ref(false)
|
||||
let unlisten: (() => void) | null = null
|
||||
|
||||
// Tab 导航:概览(项目信息/任务/工作流日志) vs 文件浏览器(Batch 10)。
|
||||
const activeTab = ref<'overview' | 'files' | 'graph'>('overview')
|
||||
// P1-g+ 问题10④:activeTab 持久化 localStorage(按项目隔离,切换项目不串扰)。
|
||||
const TAB_STORAGE_PREFIX = 'df-project-tab-'
|
||||
const validTabs = ['overview', 'files', 'graph'] as const
|
||||
type DetailTab = typeof validTabs[number]
|
||||
function readStoredTab(projectId: string): DetailTab {
|
||||
try {
|
||||
const v = localStorage.getItem(TAB_STORAGE_PREFIX + projectId)
|
||||
return (v && (validTabs as readonly string[]).includes(v)) ? (v as DetailTab) : 'overview'
|
||||
} catch { return 'overview' }
|
||||
}
|
||||
const activeTab = ref<DetailTab>('overview')
|
||||
function setActiveTab(tab: DetailTab) {
|
||||
activeTab.value = tab
|
||||
try { localStorage.setItem(TAB_STORAGE_PREFIX + projectId.value, tab) } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
// 描述折叠(P1-g+ 问题10③):长描述默认折叠截 4 行,展开看全文。
|
||||
// descCollapsible 由描述长度阈值决定(渲染后行数不可靠,用源字符数近似 ≥120 字符)。
|
||||
const DESC_COLLAPSE_THRESHOLD = 120
|
||||
const descExpanded = ref(false)
|
||||
const descCollapsible = computed(() => (currentProject.value?.description?.length ?? 0) >= DESC_COLLAPSE_THRESHOLD)
|
||||
|
||||
// 工作流日志折叠(P1-g+ 问题10②):无记录时折叠为摘要(标题 + emptyWorkflowLog 提示),
|
||||
// 有记录默认展开。collapse 由用户主动操作,避免空态占整栏。
|
||||
const workflowLogCollapsed = ref(false)
|
||||
|
||||
// 确认弹层状态机抽至 composables/useConfirm(原 4 视图重复:Projects/ProjectDetail/Ideas/Settings)
|
||||
const { confirmState, confirmDialog, answerConfirm } = useConfirm()
|
||||
@@ -364,9 +405,18 @@ const assessmentLabel = (recommendation: string): string =>
|
||||
assessmentLabelI18n(t, recommendation)
|
||||
|
||||
// ── 任务 ──
|
||||
const projectTasks = computed(() =>
|
||||
store.tasks.filter(t => t.project_id === projectId.value)
|
||||
)
|
||||
// P1-g+ 问题10①:projectTasks 不再读 store.tasks(被 Tasks 视图分页/筛选污染),
|
||||
// 改为按 project_id 走后端 list_tasks {project_id} 独立拉取,存本地 ref。
|
||||
// TaskQuery.project_id 命中 idx_tasks_project_id(见 api/types.ts:191)。
|
||||
const projectTasks = ref<TaskRecord[]>([])
|
||||
async function loadProjectTasks() {
|
||||
try {
|
||||
projectTasks.value = await taskApi.list({ project_id: projectId.value })
|
||||
} catch (e) {
|
||||
console.error('加载项目任务失败:', e)
|
||||
projectTasks.value = []
|
||||
}
|
||||
}
|
||||
// taskStatusLabel / taskStatusClass 由 ../constants/project 提供
|
||||
|
||||
// ── 工作流 ──
|
||||
@@ -417,6 +467,8 @@ async function submitNewTask() {
|
||||
branch_name: newTaskBranch.value.trim() || undefined,
|
||||
})
|
||||
if (!r) return // 失败已 toast,保持弹窗不关
|
||||
// 同步到本地 ref(store.createTask 已 push 全局 tasks,但本视图不再读 store.tasks)
|
||||
projectTasks.value = [r, ...projectTasks.value]
|
||||
showNewTaskModal.value = false
|
||||
newTaskTitle.value = ''
|
||||
newTaskDesc.value = ''
|
||||
@@ -428,7 +480,8 @@ async function submitNewTask() {
|
||||
|
||||
// ── 同步 ──
|
||||
async function handleSync() {
|
||||
await Promise.all([store.loadProjects(), store.loadTasks()])
|
||||
await store.loadProjects()
|
||||
await loadProjectTasks() // 本项目任务独立拉取,不依赖 store.loadTasks
|
||||
await checkPath()
|
||||
}
|
||||
|
||||
@@ -534,8 +587,11 @@ watch(() => store.pendingApproval, (newApproval) => {
|
||||
// ── 生命周期 ──
|
||||
onMounted(async () => {
|
||||
ensureLoaded() // 后台预热 Markdown 渲染器(模块单例,与 AiChat/TaskDetail 共享),不阻塞
|
||||
// 恢复持久化的 Tab 选择(按项目隔离)
|
||||
activeTab.value = readStoredTab(projectId.value)
|
||||
await store.loadProjects()
|
||||
await store.loadTasks() // 全量加载,详情页用 computed 过滤本项目(避免覆盖全局 tasks 单例)
|
||||
// 本项目任务独立拉取(不调 store.loadTasks,避免被 Tasks 视图分页/筛选污染)
|
||||
await loadProjectTasks()
|
||||
// 来源灵感回溯:项目有 idea_id 但 store.ideas 为空(本页未 load 过)时补拉,
|
||||
// 否则 sourceIdea computed 永远找不到对应灵感记录
|
||||
await store.loadIdeas()
|
||||
@@ -543,6 +599,13 @@ onMounted(async () => {
|
||||
await checkPath()
|
||||
})
|
||||
|
||||
// 路由切换到不同项目(URL /projects/:id 变化)时,重新拉取本项目任务 + 恢复 Tab + 检测目录
|
||||
watch(projectId, async (newId) => {
|
||||
if (!newId) return
|
||||
activeTab.value = readStoredTab(newId)
|
||||
await loadProjectTasks()
|
||||
})
|
||||
|
||||
// 目录变更(重定位后)重新检测存在性
|
||||
watch(() => currentProject.value?.path, () => { checkPath() })
|
||||
|
||||
@@ -611,6 +674,13 @@ onUnmounted(() => {
|
||||
|
||||
/* ===== 项目描述 Markdown 渲染(B-260615-25,基础样式收敛至全局 ai-md.css) ===== */
|
||||
.description.ai-md { font-size: 14px; color: var(--df-text); line-height: 1.6; }
|
||||
/* P1-g+ 问题10③:描述折叠(对齐 TaskDetail 长内容)— 折叠时 max-height 截断 + 渐变遮罩 */
|
||||
.description-wrap { position: relative; }
|
||||
.description-wrap.is-collapsed .description {
|
||||
max-height: 6em; /* 约 4 行(line-height 1.5) */
|
||||
overflow: hidden;
|
||||
}
|
||||
.desc-toggle { margin-top: 6px; }
|
||||
|
||||
/* ===== 来源灵感卡片(晋升携带评估结论回溯)===== */
|
||||
.source-idea-card {
|
||||
@@ -727,17 +797,25 @@ onUnmounted(() => {
|
||||
|
||||
/* .btn 系列(btn/btn-primary/btn-ghost/btn-sm/btn-danger)已提取到 styles/global.css 全局 */
|
||||
|
||||
/* ===== 两栏布局 — 自己滚动,不撑大父级 ===== */
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 380px;
|
||||
/* ===== 概览布局(P1-g+ 问题10②:三栏拥挤 → 两栏 + 日志下方)=====
|
||||
.detail-overview 整体滚动;内部 .detail-grid 两栏(项目信息 + 任务列表),
|
||||
.workflow-log-panel 跨整宽放下方。无日志记录时折叠为摘要不占整栏。 */
|
||||
.detail-overview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--df-gap-page);
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
max-height: 100%;
|
||||
}
|
||||
/* 两栏布局 — 项目信息 + 任务列表 */
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--df-gap-page);
|
||||
align-content: start;
|
||||
}
|
||||
.right-column { display: flex; flex-direction: column; gap: var(--df-gap-grid); }
|
||||
.workflow-log-panel { flex: 0 0 auto; }
|
||||
|
||||
/* ===== 面板 ===== */
|
||||
/* .panel / .panel-header 基础样式已收敛至全局 components.css(DRY 收口 B-260619),
|
||||
@@ -826,5 +904,6 @@ onUnmounted(() => {
|
||||
.detail-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.log-list { max-height: 200px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user