|
|
@@ -0,0 +1,115 @@
|
|
|
+<template>
|
|
|
+ <div class="alignment-page">
|
|
|
+ <div class="alignment-header">
|
|
|
+ <h3>OKR 对齐视图</h3>
|
|
|
+ <el-select v-model="selectedPeriodId" placeholder="选择周期" @change="loadTree" style="width:240px">
|
|
|
+ <el-option v-for="p in periods" :key="p.id" :label="p.name" :value="p.id" />
|
|
|
+ </el-select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-row :gutter="20" v-if="selectedPeriodId">
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-card header="对齐树" class="tree-card">
|
|
|
+ <el-tree
|
|
|
+ :data="treeData" :props="{ children: 'children', label: 'title' }"
|
|
|
+ node-key="id" highlight-current
|
|
|
+ @node-click="onNodeClick"
|
|
|
+ >
|
|
|
+ <template #default="{ data }">
|
|
|
+ <div class="tree-node">
|
|
|
+ <el-tag size="small" :type="levelTag(data.level)">{{ levelLabel(data.level) }}</el-tag>
|
|
|
+ <span class="tree-title">{{ data.title }}</span>
|
|
|
+ <el-progress :percentage="data.progress || 0" :stroke-width="4" style="width:60px" />
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-tree>
|
|
|
+ <div v-if="orphans.length > 0" style="margin-top:12px">
|
|
|
+ <el-divider />
|
|
|
+ <el-tag type="warning">孤立目标 (未对齐)</el-tag>
|
|
|
+ <div v-for="o in orphans" :key="o.id" class="orphan-item">
|
|
|
+ {{ o.userName }} - {{ o.title }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="16">
|
|
|
+ <el-card v-if="selectedNode">
|
|
|
+ <template #header>{{ selectedNode.title }}</template>
|
|
|
+ <div class="detail-meta">
|
|
|
+ <el-tag>{{ levelLabel(selectedNode.level) }}</el-tag>
|
|
|
+ <span>负责人: {{ selectedNode.userName }}</span>
|
|
|
+ <span>进度: {{ selectedNode.progress || 0 }}%</span>
|
|
|
+ </div>
|
|
|
+ <el-table :data="selectedNode.keyResults || []" style="margin-top:16px">
|
|
|
+ <el-table-column prop="title" label="KR" />
|
|
|
+ <el-table-column label="进度" width="160">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-progress :percentage="krProgress(row)" :stroke-width="6" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="weight" label="权重" width="60" />
|
|
|
+ <el-table-column label="状态" width="90">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag size="small">{{ row.status }}</el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </el-card>
|
|
|
+ <el-empty v-else description="请选择左侧 OKR 节点查看详情" />
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, onMounted } from 'vue'
|
|
|
+import { okrApi, periodApi } from '../../api'
|
|
|
+
|
|
|
+const periods = ref([])
|
|
|
+const selectedPeriodId = ref(null)
|
|
|
+const treeData = ref([])
|
|
|
+const orphans = ref([])
|
|
|
+const selectedNode = ref(null)
|
|
|
+
|
|
|
+function levelLabel(l) {
|
|
|
+ const m = { COMPANY: '公司', DEPARTMENT: '部门', TEAM: '团队', INDIVIDUAL: '个人' }
|
|
|
+ return m[l] || l
|
|
|
+}
|
|
|
+function levelTag(l) {
|
|
|
+ return l === 'COMPANY' ? '' : l === 'DEPARTMENT' ? 'warning' : l === 'TEAM' ? 'success' : 'info'
|
|
|
+}
|
|
|
+function krProgress(kr) {
|
|
|
+ return kr.targetValue > 0 ? Math.round((kr.currentValue / kr.targetValue) * 100) : 0
|
|
|
+}
|
|
|
+
|
|
|
+async function loadTree() {
|
|
|
+ const [tree, orphanList] = await Promise.all([
|
|
|
+ okrApi.getTree(selectedPeriodId.value),
|
|
|
+ okrApi.getOrphans(selectedPeriodId.value)
|
|
|
+ ])
|
|
|
+ treeData.value = tree
|
|
|
+ orphans.value = orphanList
|
|
|
+}
|
|
|
+
|
|
|
+function onNodeClick(node) {
|
|
|
+ selectedNode.value = node
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(async () => {
|
|
|
+ periods.value = await periodApi.list()
|
|
|
+ if (periods.value.length > 0) {
|
|
|
+ selectedPeriodId.value = periods.value[0].id
|
|
|
+ await loadTree()
|
|
|
+ }
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.alignment-page { max-width: 1200px; margin: 0 auto; }
|
|
|
+.alignment-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
|
|
|
+.tree-card { max-height: calc(100vh - 200px); overflow-y: auto; }
|
|
|
+.tree-node { display: flex; align-items: center; gap: 8px; font-size: 13px; }
|
|
|
+.tree-title { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
|
+.orphan-item { padding: 4px 0; font-size: 12px; color: #999; }
|
|
|
+.detail-meta { display: flex; gap: 12px; align-items: center; font-size: 13px; color: #666; }
|
|
|
+</style>
|