新增: Phase2 阶段收尾(Sprint 1-20)
重构:删 5 零引用 crate(df-evolve/plugin/stages/task/traceability)+ 清死模块、ai.rs 拆 11 子 module、ai.ts 拆 6 composable、i18n 拆目录 功能:知识库全栈(df-project/scan + CRUD + 时间线 + 前端)、Settings 拆分、appSettings KV 迁移、模型池、LLM 并发 Semaphore 修复:审批持久化根治、ConditionEngine 默认拒绝、NodeRegistry unimplemented 清除、promote 补偿删除、工具结果截断 50KB、路径校验防 symlink 逃逸 文档:B-03 人工审批设计、决策记录三分档、规格契约自检、经验记录、todo 看板、PROGRESS 更新 详见 PROGRESS.md。src-tauri/儿童每日打卡应用/ 与本项目无关,已排除。
This commit is contained in:
@@ -3,35 +3,36 @@
|
||||
<!-- 页面头部 -->
|
||||
<header class="page-header">
|
||||
<div class="header-left">
|
||||
<router-link to="/projects" class="back-link">← 项目列表</router-link>
|
||||
<router-link to="/projects" class="back-link">{{ $t('projectDetail.backToList') }}</router-link>
|
||||
<h1>{{ currentProject?.name ?? '...' }}</h1>
|
||||
<span class="stage-badge" :class="'stage-' + stageKey">{{ statusLabel }}</span>
|
||||
<span class="stage-badge" :class="'stage-' + stageKey">{{ $t(statusLabel) }}</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button class="btn btn-ghost" @click="handleSync">🔄 同步</button>
|
||||
<button class="btn btn-primary" @click="showNewTaskModal = true">+ 新任务</button>
|
||||
<button class="btn btn-ghost" @click="handleSync">{{ $t('projectDetail.sync') }}</button>
|
||||
<button class="btn btn-danger" @click="handleDelete">{{ $t('projectDetail.delete') }}</button>
|
||||
<button class="btn btn-primary" @click="showNewTaskModal = true">{{ $t('projectDetail.newTask') }}</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 新建任务模态框 -->
|
||||
<div v-if="showNewTaskModal" class="modal-overlay" @click.self="showNewTaskModal = false">
|
||||
<div class="modal-box">
|
||||
<h3 class="modal-title">新建任务</h3>
|
||||
<h3 class="modal-title">{{ $t('projectDetail.newTaskTitle') }}</h3>
|
||||
<div class="modal-field">
|
||||
<label>任务标题</label>
|
||||
<input v-model="newTaskTitle" placeholder="输入任务标题" @keyup.enter="submitNewTask" />
|
||||
<label>{{ $t('projectDetail.taskTitleLabel') }}</label>
|
||||
<input v-model="newTaskTitle" :placeholder="$t('projectDetail.taskTitlePlaceholder')" @keyup.enter="submitNewTask" />
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label>描述</label>
|
||||
<textarea v-model="newTaskDesc" placeholder="简要描述" rows="3"></textarea>
|
||||
<label>{{ $t('projectDetail.descLabel') }}</label>
|
||||
<textarea v-model="newTaskDesc" :placeholder="$t('projectDetail.descPlaceholder')" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label>分支名称</label>
|
||||
<label>{{ $t('projectDetail.branchLabel') }}</label>
|
||||
<input v-model="newTaskBranch" placeholder="feature/xxx" />
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-ghost" @click="showNewTaskModal = false">取消</button>
|
||||
<button class="btn btn-primary" @click="submitNewTask" :disabled="!newTaskTitle.trim()">确认创建</button>
|
||||
<button class="btn btn-ghost" @click="showNewTaskModal = false">{{ $t('common.cancel') }}</button>
|
||||
<button class="btn btn-primary" @click="submitNewTask" :disabled="!newTaskTitle.trim()">{{ $t('projectDetail.confirmCreate') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -51,7 +52,7 @@
|
||||
<span v-if="idx < currentStageIndex">✓</span>
|
||||
<span v-else>{{ idx + 1 }}</span>
|
||||
</div>
|
||||
<div class="stage-label">{{ stage.label }}</div>
|
||||
<div class="stage-label">{{ $t('projectDetail.' + stage.labelKey) }}</div>
|
||||
<div v-if="idx < stages.length - 1" class="stage-connector" :class="{ 'connector-done': idx < currentStageIndex }"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -61,27 +62,60 @@
|
||||
<!-- 左栏:项目信息 -->
|
||||
<section class="panel">
|
||||
<div class="panel-header">
|
||||
<h2>📋 项目信息</h2>
|
||||
<h2>{{ $t('projectDetail.infoTitle') }}</h2>
|
||||
</div>
|
||||
<div class="project-info" v-if="currentProject">
|
||||
<div class="info-item">
|
||||
<span class="label">来源想法</span>
|
||||
<span class="label">{{ $t('projectDetail.sourceIdea') }}</span>
|
||||
<router-link v-if="currentProject.idea_id" :to="`/ideas/${currentProject.idea_id}`" class="idea-link">
|
||||
查看想法详情
|
||||
{{ $t('projectDetail.viewIdea') }}
|
||||
</router-link>
|
||||
<span v-else class="no-idea">原始想法已删除</span>
|
||||
<span v-else class="no-idea">{{ $t('projectDetail.ideaDeleted') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 代码目录(绑定 + 重定位) -->
|
||||
<div class="info-item">
|
||||
<span class="label">{{ $t('projectDetail.codeDirLabel') }}</span>
|
||||
<div v-if="currentProject.path" class="path-row">
|
||||
<span class="path-text" :title="currentProject.path">{{ currentProject.path }}</span>
|
||||
<button class="btn btn-ghost btn-sm" type="button" @click="relocateDir">{{ $t('projectDetail.relocateDir') }}</button>
|
||||
</div>
|
||||
<div v-else class="path-row">
|
||||
<span class="no-idea">{{ $t('projectDetail.noDirBound') }}</span>
|
||||
<button class="btn btn-ghost btn-sm" type="button" @click="relocateDir">{{ $t('projectDetail.bindDir') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 目录状态(仅绑定时显示) -->
|
||||
<div class="info-item" v-if="currentProject.path">
|
||||
<span class="label">{{ $t('projectDetail.dirStatusLabel') }}</span>
|
||||
<span :class="pathExists === false ? 'path-missing' : 'path-ok'">
|
||||
{{ pathExists === null ? '检测中…' : pathExists ? '✓ 目录存在' : '⚠ 目录已移走,建议重定位' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 技术栈 -->
|
||||
<div class="info-item" v-if="parseStack(currentProject.stack).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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">创建时间</span>
|
||||
<span class="label">{{ $t('projectDetail.createdAt') }}</span>
|
||||
<span>{{ formatDate(currentProject.created_at) }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">更新时间</span>
|
||||
<span class="label">{{ $t('projectDetail.updatedAt') }}</span>
|
||||
<span>{{ formatDate(currentProject.updated_at) }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">项目状态</span>
|
||||
<span class="status-badge" :class="currentProject.status">{{ currentProject.status }}</span>
|
||||
<span class="label">{{ $t('projectDetail.description') }}</span>
|
||||
<span>{{ currentProject.description || '—' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">{{ $t('projectDetail.projectStatus') }}</span>
|
||||
<span class="status-badge" :class="currentProject.status">{{ $t(statusLabel) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -89,14 +123,14 @@
|
||||
<!-- 左栏:任务列表 -->
|
||||
<section class="panel">
|
||||
<div class="panel-header">
|
||||
<h2>🔀 任务列表</h2>
|
||||
<span class="task-count">{{ projectTasks.length }} 个任务</span>
|
||||
<h2>{{ $t('projectDetail.taskListTitle') }}</h2>
|
||||
<span class="task-count">{{ $t('projectDetail.taskCount', { n: projectTasks.length }) }}</span>
|
||||
</div>
|
||||
<div class="task-list">
|
||||
<div class="task-card" v-for="task in projectTasks" :key="task.id">
|
||||
<div class="task-top">
|
||||
<span class="task-title">{{ task.title }}</span>
|
||||
<span class="task-status" :class="taskStatusClass(task.status)">{{ taskStatusLabel(task.status) }}</span>
|
||||
<span class="task-status" :class="taskStatusClass(task.status)">{{ $t(taskStatusLabel(task.status)) }}</span>
|
||||
</div>
|
||||
<div class="task-branch" v-if="task.branch_name">
|
||||
<span class="branch-icon">⑂</span>
|
||||
@@ -109,7 +143,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="projectTasks.length === 0" class="empty-hint">暂无任务</div>
|
||||
<div v-if="projectTasks.length === 0" class="empty-hint">{{ $t('projectDetail.emptyTasks') }}</div>
|
||||
</section>
|
||||
|
||||
<!-- 右栏 -->
|
||||
@@ -117,8 +151,8 @@
|
||||
<!-- 工作流执行日志 -->
|
||||
<section class="panel">
|
||||
<div class="panel-header">
|
||||
<h2>📋 工作流日志</h2>
|
||||
<button class="btn btn-ghost btn-sm" @click="runDemoWorkflow" :disabled="workflowRunning">▶ 运行测试工作流</button>
|
||||
<h2>{{ $t('projectDetail.workflowLogTitle') }}</h2>
|
||||
<button class="btn btn-ghost btn-sm" @click="runDemoWorkflow" :disabled="workflowRunning">{{ $t('projectDetail.runDemoWorkflow') }}</button>
|
||||
</div>
|
||||
<div class="log-list" ref="logListRef">
|
||||
<div class="log-item" v-for="(evt, idx) in formattedEvents" :key="idx" :class="'log-' + evt.level">
|
||||
@@ -126,36 +160,7 @@
|
||||
<span class="log-level">{{ evt.level }}</span>
|
||||
<span class="log-msg">{{ evt.message }}</span>
|
||||
</div>
|
||||
<div v-if="formattedEvents.length === 0" class="empty-hint">点击"运行测试工作流"查看实时日志</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 项目上下文 -->
|
||||
<section class="panel">
|
||||
<div class="panel-header">
|
||||
<h2>🏷️ 项目信息</h2>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div class="info-row">
|
||||
<span class="info-label">状态</span>
|
||||
<span class="info-value">{{ statusLabel }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">描述</span>
|
||||
<span class="info-value">{{ currentProject?.description ?? '—' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">来源想法</span>
|
||||
<span class="info-value">{{ currentProject?.idea_id ?? '—' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">创建时间</span>
|
||||
<span class="info-value">{{ formatDate(currentProject?.created_at ?? '') }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">更新时间</span>
|
||||
<span class="info-value">{{ formatDate(currentProject?.updated_at ?? '') }}</span>
|
||||
</div>
|
||||
<div v-if="formattedEvents.length === 0" class="empty-hint">{{ $t('projectDetail.emptyWorkflowLog') }}</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -164,18 +169,18 @@
|
||||
<!-- 人工审批对话框 -->
|
||||
<div v-if="showApprovalDialog" class="modal-overlay">
|
||||
<div class="modal-box" style="width: 500px">
|
||||
<h3 class="modal-title">需要人工审批</h3>
|
||||
<h3 class="modal-title">{{ $t('projectDetail.approvalTitle') }}</h3>
|
||||
<div v-if="store.pendingApproval">
|
||||
<h3>{{ store.pendingApproval.title }}</h3>
|
||||
<p style="margin: 16px 0; color: var(--df-text-secondary);">{{ store.pendingApproval.description }}</p>
|
||||
<div style="margin-bottom: 20px;">
|
||||
<p style="margin-bottom: 8px; font-size: 14px;">请选择操作:</p>
|
||||
<p style="margin-bottom: 8px; font-size: 14px;">{{ $t('projectDetail.approvalHint') }}</p>
|
||||
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
|
||||
<button
|
||||
v-for="option in store.pendingApproval.options"
|
||||
:key="option"
|
||||
v-for="(option, idx) in store.pendingApproval.options"
|
||||
:key="idx"
|
||||
class="btn"
|
||||
:class="option === '同意' ? 'btn-primary' : 'btn-ghost'"
|
||||
:class="idx === 0 ? 'btn-primary' : 'btn-ghost'"
|
||||
@click="handleApproval(option)"
|
||||
>
|
||||
{{ option }}
|
||||
@@ -184,7 +189,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-ghost" @click="showApprovalDialog = false">稍后处理</button>
|
||||
<button class="btn btn-ghost" @click="showApprovalDialog = false">{{ $t('projectDetail.approvalLater') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -193,76 +198,48 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { useProjectStore } from '../stores/project'
|
||||
import { projectApi } from '../api'
|
||||
import { formatDate } from '../utils/time'
|
||||
import { projectStatusLabel, projectStageInfo, taskStatusLabel, taskStatusClass } from '../constants/project'
|
||||
import type { WorkflowEventPayload } from '../api/types'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const store = useProjectStore()
|
||||
const { t, locale } = useI18n()
|
||||
const logListRef = ref<HTMLElement | null>(null)
|
||||
const showApprovalDialog = ref(false)
|
||||
let unlisten: (() => void) | null = null
|
||||
|
||||
// ── 阶段定义 ──
|
||||
// ── 阶段定义(labelKey 对应 i18n projectDetail.stage*) ──
|
||||
const stages = [
|
||||
{ key: 'idea', label: '💡 想法' },
|
||||
{ key: 'requirement', label: '📋 需求' },
|
||||
{ key: 'coding', label: '💻 编码' },
|
||||
{ key: 'testing', label: '🧪 测试' },
|
||||
{ key: 'release', label: '🚀 发布' },
|
||||
{ key: 'idea', labelKey: 'stageIdea' },
|
||||
{ key: 'requirement', labelKey: 'stageRequirement' },
|
||||
{ key: 'coding', labelKey: 'stageCoding' },
|
||||
{ key: 'testing', labelKey: 'stageTesting' },
|
||||
{ key: 'release', labelKey: 'stageRelease' },
|
||||
]
|
||||
|
||||
// ── 状态映射 ──
|
||||
const stageMap: Record<string, number> = {
|
||||
planning: 0, in_progress: 2, testing: 3, completed: 4, paused: 2, cancelled: 0,
|
||||
}
|
||||
|
||||
const statusLabels: Record<string, string> = {
|
||||
planning: '📐 规划中',
|
||||
in_progress: '💻 进行中',
|
||||
paused: '⏸ 已暂停',
|
||||
completed: '✅ 已完成',
|
||||
cancelled: '❌ 已取消',
|
||||
}
|
||||
|
||||
const taskStatusLabels: Record<string, string> = {
|
||||
todo: '📋 待开始',
|
||||
in_progress: '🔄 进行中',
|
||||
review_ready: '👀 待审查',
|
||||
merged: '✅ 已合并',
|
||||
abandoned: '❌ 已放弃',
|
||||
}
|
||||
|
||||
const taskStatusClasses: Record<string, string> = {
|
||||
todo: 'status-todo',
|
||||
in_progress: 'status-progress',
|
||||
review_ready: 'status-review',
|
||||
merged: 'status-done',
|
||||
abandoned: 'status-todo',
|
||||
}
|
||||
|
||||
// ── 当前项目 ──
|
||||
// 状态文案/阶段进度统一走 ../constants/project(与 Projects/Tasks/Dashboard 一致,
|
||||
// 根治此前 stageMap 含不存在的 testing 状态、三套映射互相矛盾)
|
||||
const projectId = computed(() => route.params.id as string)
|
||||
const currentProject = computed(() =>
|
||||
store.projects.find(p => p.id === projectId.value)
|
||||
)
|
||||
const stageKey = computed(() => currentProject.value?.status ?? 'planning')
|
||||
const statusLabel = computed(() => statusLabels[stageKey.value] ?? stageKey.value)
|
||||
const currentStageIndex = computed(() => stageMap[stageKey.value] ?? 0)
|
||||
const statusLabel = computed(() => projectStatusLabel(stageKey.value))
|
||||
const currentStageIndex = computed(() => projectStageInfo(stageKey.value).stepIndex)
|
||||
|
||||
// ── 任务 ──
|
||||
const projectTasks = computed(() =>
|
||||
store.tasks.filter(t => t.project_id === projectId.value)
|
||||
)
|
||||
|
||||
function taskStatusLabel(status: string) {
|
||||
return taskStatusLabels[status] ?? status
|
||||
}
|
||||
|
||||
function taskStatusClass(status: string) {
|
||||
return taskStatusClasses[status] ?? 'status-todo'
|
||||
}
|
||||
// taskStatusLabel / taskStatusClass 由 ../constants/project 提供
|
||||
|
||||
// ── 工作流 ──
|
||||
const workflowRunning = ref(false)
|
||||
@@ -302,7 +279,7 @@ const formattedEvents = computed<LogEntry[]>(() => {
|
||||
const type = ev.type ?? 'unknown'
|
||||
const level = type.includes('error') || type.includes('fail') ? 'error'
|
||||
: type.includes('warn') ? 'warn' : 'info'
|
||||
const time = new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' })
|
||||
const time = new Date().toLocaleTimeString(locale.value, { hour: '2-digit', minute: '2-digit', second: '2-digit' })
|
||||
const message = ev.label
|
||||
? `[${ev.label}] ${type}: ${ev.output ?? ev.code ?? ev.message ?? JSON.stringify(ev)}`
|
||||
: `${type}: ${ev.output ?? ev.code ?? ev.message ?? JSON.stringify(ev)}`
|
||||
@@ -318,12 +295,13 @@ const newTaskBranch = ref('')
|
||||
|
||||
async function submitNewTask() {
|
||||
if (!newTaskTitle.value.trim()) return
|
||||
await store.createTask({
|
||||
const r = await store.createTask({
|
||||
project_id: projectId.value,
|
||||
title: newTaskTitle.value.trim(),
|
||||
description: newTaskDesc.value.trim(),
|
||||
branch_name: newTaskBranch.value.trim() || undefined,
|
||||
})
|
||||
if (!r) return // 失败已 toast,保持弹窗不关
|
||||
showNewTaskModal.value = false
|
||||
newTaskTitle.value = ''
|
||||
newTaskDesc.value = ''
|
||||
@@ -332,7 +310,58 @@ async function submitNewTask() {
|
||||
|
||||
// ── 同步 ──
|
||||
async function handleSync() {
|
||||
await Promise.all([store.loadProjects(), store.loadTasks(projectId.value)])
|
||||
await Promise.all([store.loadProjects(), store.loadTasks()])
|
||||
await checkPath()
|
||||
}
|
||||
|
||||
// ── 代码目录绑定 ──
|
||||
const pathExists = ref<boolean | null>(null)
|
||||
|
||||
// 解析技术栈 JSON 数组字符串(防崩)
|
||||
function parseStack(stack: string | null): string[] {
|
||||
if (!stack) return []
|
||||
try {
|
||||
const arr = JSON.parse(stack)
|
||||
return Array.isArray(arr) ? arr : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// 检查绑定目录是否存在(详情页「目录是否还在」)
|
||||
async function checkPath() {
|
||||
const p = currentProject.value
|
||||
if (!p?.path) { pathExists.value = null; return }
|
||||
try {
|
||||
pathExists.value = await projectApi.checkPathExists(p.path)
|
||||
} catch {
|
||||
pathExists.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// 重定位/绑定目录(选目录 → 查重 → 确认 → 后端重探测 stack)
|
||||
async function relocateDir() {
|
||||
const p = currentProject.value
|
||||
if (!p) return
|
||||
try {
|
||||
const selected = await open({ directory: true, multiple: false })
|
||||
if (!selected || Array.isArray(selected)) return
|
||||
const dir = selected as string
|
||||
// 查重(排除自身)
|
||||
try {
|
||||
const conflict = await projectApi.checkBinding(dir, p.id)
|
||||
if (conflict) {
|
||||
alert(`该目录已被项目「${conflict.name}」绑定`)
|
||||
return
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
if (!confirm(`将项目目录${currentProject.value?.path ? '重定位' : '绑定'}到:\n${dir}\n\n技术栈将重新探测。确认?`)) return
|
||||
await store.relocateProjectPath(p.id, dir)
|
||||
await checkPath()
|
||||
} catch (e: any) {
|
||||
console.error('重定位失败:', e)
|
||||
alert('重定位失败: ' + (e?.toString() ?? '未知错误'))
|
||||
}
|
||||
}
|
||||
|
||||
// ── 审批处理 ──
|
||||
@@ -341,6 +370,20 @@ async function handleApproval(decision: string) {
|
||||
showApprovalDialog.value = false
|
||||
}
|
||||
|
||||
// ── 删除项目(软删 → 回收站,可恢复)──
|
||||
async function handleDelete() {
|
||||
const p = currentProject.value
|
||||
if (!p) return
|
||||
if (!confirm(t('projectDetail.confirmDelete', { name: p.name }))) return
|
||||
await store.deleteProject(p.id)
|
||||
router.push('/projects')
|
||||
}
|
||||
|
||||
// 日志自动滚到底部(让 logListRef 不再是死 ref)
|
||||
watch(formattedEvents, () => {
|
||||
if (logListRef.value) logListRef.value.scrollTop = logListRef.value.scrollHeight
|
||||
})
|
||||
|
||||
// 监听审批请求
|
||||
watch(() => store.pendingApproval, (newApproval) => {
|
||||
if (newApproval) {
|
||||
@@ -354,10 +397,14 @@ watch(() => store.pendingApproval, (newApproval) => {
|
||||
// ── 生命周期 ──
|
||||
onMounted(async () => {
|
||||
await store.loadProjects()
|
||||
await store.loadTasks(projectId.value)
|
||||
unlisten = (await store.startEventListener()) as any
|
||||
await store.loadTasks() // 全量加载,详情页用 computed 过滤本项目(避免覆盖全局 tasks 单例)
|
||||
unlisten = await store.startEventListener()
|
||||
await checkPath()
|
||||
})
|
||||
|
||||
// 目录变更(重定位后)重新检测存在性
|
||||
watch(() => currentProject.value?.path, () => { checkPath() })
|
||||
|
||||
onUnmounted(() => {
|
||||
if (unlisten) {
|
||||
unlisten()
|
||||
@@ -413,6 +460,21 @@ onUnmounted(() => {
|
||||
background: rgba(108,99,255,0.1);
|
||||
color: var(--df-accent);
|
||||
}
|
||||
|
||||
/* ===== 代码目录 / 技术栈 ===== */
|
||||
.path-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
.path-text {
|
||||
font-size: 12px; font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
color: var(--df-text-secondary); word-break: break-all;
|
||||
}
|
||||
.path-ok { color: var(--df-success); font-size: 12px; }
|
||||
.path-missing { color: var(--df-danger); font-size: 12px; }
|
||||
.info-tags { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.info-tags .tech-tag {
|
||||
font-size: 11px; padding: 2px 8px; border-radius: var(--df-radius-xs);
|
||||
background: rgba(108, 99, 255, 0.08); color: var(--df-text-secondary);
|
||||
border: 0.5px solid var(--df-border);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
@@ -457,6 +519,8 @@ onUnmounted(() => {
|
||||
.btn-ghost { background: transparent; color: var(--df-text-secondary); border: 0.5px solid var(--df-border); }
|
||||
.btn-ghost:hover { background: var(--df-bg-card); color: var(--df-text); }
|
||||
.btn-sm { padding: 4px 12px; font-size: 12px; }
|
||||
.btn-danger { background: transparent; color: var(--df-danger); border: 0.5px solid rgba(240,101,101,0.4); }
|
||||
.btn-danger:hover { background: rgba(240,101,101,0.12); }
|
||||
|
||||
/* ===== 阶段进度条 ===== */
|
||||
.stage-pipeline {
|
||||
@@ -609,17 +673,7 @@ onUnmounted(() => {
|
||||
.log-error .log-level { color: var(--df-danger); }
|
||||
.log-msg { color: var(--df-text-secondary); flex: 1; }
|
||||
|
||||
/* ===== 项目信息 ===== */
|
||||
.info-grid { display: flex; flex-direction: column; gap: 0; }
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px 0;
|
||||
border-bottom: 0.5px solid var(--df-border);
|
||||
}
|
||||
.info-row:last-child { border-bottom: none; }
|
||||
.info-label { font-size: 13px; color: var(--df-text-dim); }
|
||||
.info-value { font-size: 13px; color: var(--df-text-secondary); font-weight: 500; }
|
||||
/* ===== 项目信息(左栏 .project-info/.info-item,右栏重复面板已删)===== */
|
||||
|
||||
/* ===== 模态框 ===== */
|
||||
.modal-overlay {
|
||||
|
||||
Reference in New Issue
Block a user