diff --git a/web/src/components/AppNav.vue b/web/src/components/AppNav.vue index 0a1b6b4..6d2ec90 100644 --- a/web/src/components/AppNav.vue +++ b/web/src/components/AppNav.vue @@ -61,8 +61,8 @@ ⚙️ SOP - - 🎯 OKR + + 🗂️ 任务看板 📁 Files diff --git a/web/src/router/index.js b/web/src/router/index.js index eb0a784..eb370ee 100644 --- a/web/src/router/index.js +++ b/web/src/router/index.js @@ -36,7 +36,10 @@ const routes = [ component: () => import('@/views/CompanyDashboard.vue'), }, { - path: '/:id/:tab(overview|agents|pipelines|goals|sop|okr|team|files|messages|skills|jobs|workspace)', + // 39 号文档 §二 P2:'goals'/'okr' 两个 tab 值已摘掉 —— 前者是 OKR 树的历史命名残留 + // (真正的 goal/task 看板走下面 'task-board' 独立路由,不挤在这条 :tab 正则里), + // 后者是随 /okr 一起废弃的孤儿页面。 + path: '/:id/:tab(overview|agents|pipelines|sop|team|files|messages|skills|jobs|workspace)', name: 'CompanyDetail', component: () => import('@/views/CompanyDashboard.vue'), props: true @@ -50,9 +53,11 @@ const routes = [ props: true, }, { - path: '/okr', - name: 'OkrDashboard', - component: () => import('@/views/OkrDashboard.vue'), + // 39 号文档 §二 P2:goal/task 两级看板独立页,同样不挤进 :tab 正则(信息密度高,值得一整页)。 + path: '/:id/task-board', + name: 'TaskBoard', + component: () => import('@/views/TaskBoard.vue'), + props: true, }, // 新增独立页面 { diff --git a/web/src/views/CompanyDashboard.vue b/web/src/views/CompanyDashboard.vue index 9a8f983..18c3c61 100644 --- a/web/src/views/CompanyDashboard.vue +++ b/web/src/views/CompanyDashboard.vue @@ -43,6 +43,11 @@ + + @@ -355,103 +360,6 @@ - -
-
- -
-
- 🎯 OKR 目标 - {{ goals.length }} -
-
-
-
- {{ g.id }} - {{ okrStatusLabel(g.status) }} -
-
{{ g.title }}
-
-
- {{ Math.round(g.progress * 100) }}% -
-
- 👤 {{ g.owner }} - {{ g.doneCount || 0 }}/{{ g.totalCount || 0 }} -
-
-
暂无目标
-
-
- - -
-
-
- {{ selectedGoalDetail.id }} - {{ okrStatusLabel(selectedGoalDetail.status) }} - 👤 {{ selectedGoalDetail.owner }} -
-

{{ selectedGoalDetail.title }}

-

{{ selectedGoalDetail.description || '暂无详细描述' }}

-
-
- 目标总体进度 - {{ Math.round(selectedGoalProgress.progress * 100) }}% -
-
-
- 总: {{ selectedGoalProgress.total }} - ✅ {{ selectedGoalProgress.done }} - 🔄 {{ selectedGoalProgress.inProgress }} - ⏳ {{ selectedGoalProgress.pending }} -
-
-
-
-

OKR 拆解树

-
-
-
-
-
-
- {{ node.id }} - {{ okrLevelName(node.level) }} - {{ okrStatusLabel(node.status) }} -
-
{{ node.title }}
-
- 👤 {{ node.owner }} - {{ node.priority }} - 🔗 {{ node.taskId }} -
-
摘要: {{ node.summary }}
-
产出: {{ node.output }}
-
-
-
-
暂无 OKR 节点
-
-
-
-
-
-
🎯
-

OKR 目标看板

-

从左侧选择一个目标查看拆解树与进度

-
-
-
-
-
@@ -852,10 +760,6 @@ const companyAgents = ref([]) const pipelines = ref([]) const pipelineRuns = ref([]) const selectedPipeline = ref(null) -const goals = ref([]) -const selectedGoalId = ref(null) -const selectedGoalDetail = ref(null) -const selectedGoalProgress = ref(null) const templates = ref([]) const activeTab = ref('overview') const showTemplates = ref(false) @@ -1039,7 +943,6 @@ async function loadCompanyData(id, tab) { if (activeTab.value === 'overview') await loadCompanyStatus(id) if (activeTab.value === 'agents') { await loadCompanyAgents(id); await loadOrgGraph(id) } if (activeTab.value === 'pipelines') await loadPipelines(id) - if (activeTab.value === 'goals') await loadGoals(id) if (activeTab.value === 'files') await loadCompanyFiles(id) if (activeTab.value === 'messages') await loadMessages(id) if (activeTab.value === 'skills') await loadSkillsList(id) @@ -1064,6 +967,11 @@ function goToGraphConsole() { router.push(`/${selectedCompany.value.id}/graph-console`) } +function goToTaskBoard() { + if (!selectedCompany.value) return + router.push(`/${selectedCompany.value.id}/task-board`) +} + watch(() => route?.params, async (params) => { if (!params) return if (route.name === 'CompanyDetail' && params.id) { @@ -1216,54 +1124,6 @@ async function retryPipelineRun(runId) { } catch (e) { alert(`❌ 重试失败: ${e.message}`) } } -async function loadGoals(id) { - try { - const { data } = await axios.get(`/api/company/${id}/okr`) - goals.value = data - // Auto-select first goal - if (data.length > 0 && !selectedGoalId.value) { - selectGoalDetail(data[0].id) - } - } catch (e) { console.error(e) } -} - -async function selectGoalDetail(goalId) { - if (!selectedCompany.value) return - selectedGoalId.value = goalId - try { - const [goalRes, progRes] = await Promise.all([ - axios.get(`/api/company/${selectedCompany.value.id}/okr/${goalId}`), - axios.get(`/api/company/${selectedCompany.value.id}/okr/${goalId}/progress`) - ]) - selectedGoalDetail.value = goalRes.data - selectedGoalProgress.value = progRes.data - } catch (e) { console.error(e) } -} - -function okrStatusLabel(status) { - const map = { active: '运转中', in_progress: '进行中', pending: '待开始', done: '已完成', failed: '失败', blocked: '已阻塞' } - return map[status] || status -} - -function okrLevelName(level) { - const names = ['Goal', 'Objective', 'Key Result', 'Sub-Task', 'Micro-Task'] - return names[Math.min(level - 1, 4)] -} - -const okrSortedNodes = computed(() => { - if (!selectedGoalDetail.value?.nodes) return [] - const nodes = selectedGoalDetail.value.nodes - const keys = Object.keys(nodes).sort((a, b) => { - const partsA = a.replace('G-','').split('.').map(Number) - const partsB = b.replace('G-','').split('.').map(Number) - for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) { - if ((partsA[i] || 0) !== (partsB[i] || 0)) return (partsA[i] || 0) - (partsB[i] || 0) - } - return 0 - }) - return keys.map(id => ({ id, ...nodes[id], level: id.split('.').length })) -}) - async function loadCompanyStatus(id) { try { const { data } = await axios.get(`/api/company/${id}/status`) @@ -1848,84 +1708,6 @@ function closeDropdown() { .run-meta { display: flex; gap: 16px; font-size: 12px; color: var(--text-muted); } .run-actions { margin-top: 8px; } -/* ═══ OKR Full Layout ═══ */ -.okr-layout { display: flex; height: calc(100vh - 140px); overflow: hidden; } -.okr-sidebar { width: 280px; min-width: 260px; border-right: 1px solid var(--border-primary); background: var(--bg-overlay); display: flex; flex-direction: column; } -.okr-sidebar-header { padding: 12px 16px; font-size: 13px; font-weight: 700; color: var(--text-secondary); border-bottom: 1px solid var(--border-primary); display: flex; align-items: center; justify-content: space-between; background: var(--bg-secondary); } -.okr-count { background: var(--bg-surface-active); color: var(--accent); padding: 2px 8px; border-radius: 12px; font-size: 11px; } -.okr-goal-list { flex: 1; overflow-y: auto; padding: 12px; display: flex; flex-direction: column; gap: 8px; } -.okr-goal-card { padding: 12px 14px; border-radius: 10px; cursor: pointer; background: var(--bg-surface); border: 1px solid var(--border-primary); transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); } -.okr-goal-card:hover { transform: translateY(-2px); border-color: var(--accent-light); box-shadow: 0 4px 12px rgba(16,185,129,0.08); } -.okr-goal-card.active { background: var(--accent-bg); border-color: var(--border-active); box-shadow: 0 0 0 1px var(--accent) inset; } -.okr-goal-top { display: flex; justify-content: space-between; margin-bottom: 6px; } -.okr-gid { font-size: 11px; font-family: 'JetBrains Mono', monospace; font-weight: 600; color: var(--text-tertiary); } -.okr-gstatus { font-size: 10px; font-weight: 600; padding: 2px 6px; border-radius: 4px; } -.okr-gstatus.active { background: rgba(59,130,246,0.15); color: #60a5fa; } -.okr-gstatus.done { background: rgba(16,185,129,0.15); color: #34d399; } -.okr-gtitle { font-size: 13px; font-weight: 600; margin-bottom: 10px; color: var(--text-primary); line-height: 1.4; } -.okr-gprogress { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; } -.okr-pbar { flex: 1; height: 6px; background: var(--bg-input-focus); border-radius: 3px; overflow: hidden; } -.okr-pfill { height: 100%; background: linear-gradient(90deg, var(--accent), var(--accent-light)); border-radius: 3px; transition: width 0.5s ease; } -.okr-ppct { font-size: 11px; font-weight: 600; color: var(--text-secondary); width: 32px; text-align: right; } -.okr-gmeta { display: flex; justify-content: space-between; font-size: 10px; color: var(--text-faint); } - -/* OKR Detail Panel */ -.okr-detail { flex: 1; overflow-y: auto; background: var(--bg-primary); display: flex; flex-direction: column; } -.okr-detail.okr-empty { align-items: center; justify-content: center; background: var(--bg-overlay); } -.okr-detail.okr-empty h3 { font-size: 20px; font-weight: 600; margin-bottom: 8px; color: var(--text-secondary); } -.okr-detail.okr-empty p { color: var(--text-muted); font-size: 13px; } - -.okr-hero { padding: 30px 40px; border-bottom: 1px solid var(--border-primary); background: linear-gradient(180deg, var(--accent-bg) 0%, transparent 100%); } -.okr-hero-row { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; } -.okr-hero-id { font-size: 14px; font-family: 'JetBrains Mono', monospace; font-weight: 700; color: var(--accent-light); } -.okr-hero-status { font-size: 12px; font-weight: 600; padding: 3px 8px; border-radius: 6px; } -.okr-hero-status.active { background: rgba(59,130,246,0.2); color: #60a5fa; border: 1px solid rgba(59,130,246,0.3); } -.okr-hero-status.done { background: rgba(16,185,129,0.2); color: #34d399; border: 1px solid rgba(16,185,129,0.3); } -.okr-hero-owner { font-size: 12px; font-weight: 500; color: var(--text-secondary); margin-left: auto; background: var(--bg-surface); padding: 4px 10px; border-radius: 12px; border: 1px solid var(--border-primary); } -.okr-hero-title { font-size: 26px; font-weight: 800; margin: 0 0 10px; letter-spacing: -0.5px; } -.okr-hero-desc { font-size: 14px; color: var(--text-tertiary); line-height: 1.6; max-width: 800px; margin: 0; } - -.okr-hero-progress { margin-top: 24px; max-width: 600px; background: var(--bg-surface); padding: 16px 20px; border-radius: 12px; border: 1px solid var(--border-primary); } -.okr-hp-top { display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 10px; font-size: 13px; font-weight: 600; color: var(--text-secondary); } -.okr-hp-pct { font-size: 24px; font-weight: 800; color: var(--accent-light); line-height: 1; } -.okr-hp-bar { height: 8px; background: var(--bg-input-focus); border-radius: 4px; overflow: hidden; margin-bottom: 12px; } -.okr-hp-fill { height: 100%; background: linear-gradient(90deg, #10b981, #34d399); border-radius: 4px; transition: width 0.8s cubic-bezier(0.16, 1, 0.3, 1); } -.okr-hp-stats { display: flex; gap: 16px; font-size: 11px; font-weight: 500; color: var(--text-tertiary); } -.okr-hp-stats .stat-d { color: #34d399; } -.okr-hp-stats .stat-p { color: #60a5fa; } -.okr-hp-stats .stat-w { color: var(--text-muted); } - -/* OKR Tree */ -.okr-tree-area { padding: 30px 40px; max-width: 1000px; } -.okr-tree-title { font-size: 18px; font-weight: 600; margin: 0 0 20px; color: var(--text-primary); } -.okr-tree { display: flex; flex-direction: column; gap: 6px; } -.okr-tnode-line { display: flex; width: 100%; } -.okr-tnode-indent { position: relative; } -.okr-tnode-indent::before { content: ''; position: absolute; top: -16px; bottom: 50%; left: -14px; width: 14px; border-left: 1.5px solid var(--border-active); border-bottom: 1.5px solid var(--border-active); border-bottom-left-radius: 6px; z-index: 1; opacity: 0.3; } -.okr-tnode-card { flex: 1; background: var(--bg-surface); border: 1px solid var(--border-primary); border-radius: 8px; padding: 12px 16px; transition: all 0.2s; } -.okr-tnode-card:hover { border-color: rgba(255,255,255,0.1); background: var(--bg-surface-hover); } -.okr-tnode-card.okr-lv-2 { background: rgba(59,130,246,0.03); border-color: rgba(59,130,246,0.1); } -.okr-tnode-card.okr-lv-3 { background: rgba(16,185,129,0.03); border-color: rgba(16,185,129,0.1); } -.okr-tnode-hdr { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; } -.okr-nid { font-family: 'JetBrains Mono', monospace; font-size: 11px; font-weight: 700; color: var(--text-tertiary); } -.okr-ntype { font-size: 9px; font-weight: 600; padding: 2px 6px; border-radius: 10px; background: rgba(255,255,255,0.05); color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; } -.okr-nstatus { font-size: 10px; font-weight: 600; padding: 2px 6px; border-radius: 4px; border: 1px solid transparent; } -.okr-nstatus.done { background: rgba(16,185,129,0.1); color: #34d399; border-color: rgba(16,185,129,0.2); } -.okr-nstatus.in_progress { background: rgba(59,130,246,0.1); color: #60a5fa; border-color: rgba(59,130,246,0.2); animation: okr-pulse 2s infinite; } -.okr-nstatus.pending { background: rgba(255,255,255,0.05); color: var(--text-muted); border-color: var(--border-primary); } -@keyframes okr-pulse { 0% { box-shadow: 0 0 0 0 rgba(96,165,250,0.2); } 70% { box-shadow: 0 0 0 4px rgba(96,165,250,0); } 100% { box-shadow: 0 0 0 0 rgba(96,165,250,0); } } -.okr-ntitle { font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 8px; line-height: 1.4; } -.okr-nfooter { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-bottom: 4px; } -.okr-nowner { font-size: 11px; font-weight: 500; color: var(--accent-light); display: flex; align-items: center; gap: 4px; background: var(--accent-bg); padding: 2px 8px; border-radius: 10px; } -.okr-nprio { font-size: 10px; font-weight: 700; font-family: 'JetBrains Mono', monospace; } -.okr-nprio.P0 { color: #ef4444; } -.okr-nprio.P1 { color: #f59e0b; } -.okr-nprio.P2 { color: var(--text-muted); } -.okr-ntask { font-size: 10px; font-family: 'JetBrains Mono', monospace; color: var(--blue-text); background: rgba(59,130,246,0.1); padding: 2px 6px; border-radius: 4px; } -.okr-nsummary { margin-top: 8px; padding-top: 8px; border-top: 1px dashed var(--border-primary); font-size: 11px; color: var(--text-tertiary); line-height: 1.5; } -.okr-nsummary strong { color: var(--text-secondary); } - -.empty-state-large { text-align: center; color: var(--text-muted); } /* ═══ Modal ═══ */ .modal-overlay { diff --git a/web/src/views/OkrDashboard.vue b/web/src/views/OkrDashboard.vue deleted file mode 100644 index ac5dd47..0000000 --- a/web/src/views/OkrDashboard.vue +++ /dev/null @@ -1,352 +0,0 @@ - - - - - diff --git a/web/src/views/TaskBoard.vue b/web/src/views/TaskBoard.vue new file mode 100644 index 0000000..de7e3fd --- /dev/null +++ b/web/src/views/TaskBoard.vue @@ -0,0 +1,112 @@ + + + + + +