fix: 行动方案库统计卡片—补全API+overdue计算
This commit is contained in:
@@ -420,7 +420,7 @@ async function loadData() {
|
||||
async function loadStats() {
|
||||
try {
|
||||
const r: any = await actionPlanApi.stats()
|
||||
stats.value = r
|
||||
stats.value = { '': r.total || 0, ...r, overdue: r.overdue || 0 }
|
||||
} catch {}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
<template>
|
||||
<div class="okr-detail-page">
|
||||
<div class="page-header">
|
||||
<el-button text @click="$router.back()">
|
||||
<el-icon><ArrowLeft /></el-icon> 返回
|
||||
</el-button>
|
||||
<h3>OKR 详情</h3>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading-wrap">
|
||||
<el-skeleton :rows="6" animated />
|
||||
</div>
|
||||
|
||||
<template v-if="!loading && objective">
|
||||
<!-- 基本信息 -->
|
||||
<div class="info-card">
|
||||
<div class="info-row">
|
||||
<span class="info-label">目标</span>
|
||||
<span class="info-value">{{ objective.objective.title }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">描述</span>
|
||||
<span class="info-value">{{ objective.objective.description || '无描述' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">维度</span>
|
||||
<el-tag size="small" :type="dimTagType(objective.objective.dimension)">
|
||||
{{ dimLabel(objective.objective.dimension) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">季度</span>
|
||||
<span class="info-value">{{ objective.objective.quarter }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">进度</span>
|
||||
<el-progress :percentage="objective.objective.progress || 0" :stroke-width="8" style="flex:1;max-width:300px;" />
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">状态</span>
|
||||
<el-tag :type="statusTagType(objective.objective.status)">{{ statusLabel(objective.objective.status) }}</el-tag>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">负责人</span>
|
||||
<span class="info-value">{{ objective.objective.owner || '未指定' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 标签页 -->
|
||||
<el-tabs v-model="activeTab" class="detail-tabs">
|
||||
<el-tab-pane label="KR 列表" name="krs">
|
||||
<div v-if="!objective.key_results || objective.key_results.length === 0" class="empty-tab">
|
||||
暂无关联关键结果
|
||||
</div>
|
||||
<div v-else class="kr-list">
|
||||
<div v-for="kr in objective.key_results" :key="kr.id" class="kr-card">
|
||||
<div class="kr-header">
|
||||
<span class="kr-title">{{ kr.title }}</span>
|
||||
<el-tag size="small" :type="statusTagType(kr.status)">{{ statusLabel(kr.status) }}</el-tag>
|
||||
</div>
|
||||
<div class="kr-meta">
|
||||
<span v-if="kr.assignee" class="kr-meta-item">负责人: {{ kr.assignee }}</span>
|
||||
<span v-if="kr.due_date" class="kr-meta-item">截止: {{ kr.due_date.slice(0, 10) }}</span>
|
||||
</div>
|
||||
<el-progress :percentage="kr.progress || 0" :stroke-width="6" />
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="分解视图" name="decomposition">
|
||||
<OkrDecomposition :okr-id="okrId" @refresh="loadData" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
|
||||
<div v-if="!loading && !objective" class="not-found">
|
||||
<el-empty description="OKR不存在" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||
import { okrApi } from '../api/index'
|
||||
import OkrDecomposition from '../components/okr/OkrDecomposition.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const okrId = computed(() => Number(route.params.id))
|
||||
|
||||
const loading = ref(true)
|
||||
const objective = ref<any>(null)
|
||||
const activeTab = ref('krs')
|
||||
|
||||
const DIM_LABELS: Record<string, string> = {
|
||||
finance: '财务层', customer: '客户层', process: '流程层', learning: '学习成长层',
|
||||
}
|
||||
|
||||
function dimLabel(d: string) { return DIM_LABELS[d] || d }
|
||||
function dimTagType(d: string) {
|
||||
const types: Record<string, string> = { finance: 'danger', customer: 'primary', process: 'success', learning: 'warning' }
|
||||
return types[d] || ''
|
||||
}
|
||||
function statusLabel(s: string) {
|
||||
const labels: Record<string, string> = { active: '进行中', completed: '已完成', cancelled: '已取消' }
|
||||
return labels[s] || s
|
||||
}
|
||||
function statusTagType(s: string) {
|
||||
const types: Record<string, string> = { active: 'warning', completed: 'success', cancelled: 'danger' }
|
||||
return types[s] || ''
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
if (!okrId.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await okrApi.get(okrId.value)
|
||||
objective.value = res
|
||||
} catch (e: any) {
|
||||
ElMessage.error('加载失败: ' + (e?.response?.data?.detail || e?.message || ''))
|
||||
objective.value = null
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.okr-detail-page {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.page-header h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
.loading-wrap {
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
/* ── 信息卡片 ── */
|
||||
.info-card {
|
||||
background: #FAFBFC;
|
||||
border: 1px solid #EBEEF5;
|
||||
border-radius: 10px;
|
||||
padding: 16px 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #F0F0F0;
|
||||
}
|
||||
.info-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.info-label {
|
||||
width: 60px;
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.info-value {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ── 标签页 ── */
|
||||
.detail-tabs {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* ── KR列表 ── */
|
||||
.kr-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.kr-card {
|
||||
background: #fff;
|
||||
border: 1px solid #EBEEF5;
|
||||
border-radius: 8px;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
.kr-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.kr-title {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
.kr-meta {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.kr-meta-item {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
.empty-tab {
|
||||
text-align: center;
|
||||
color: #C0C4CC;
|
||||
padding: 40px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
.not-found {
|
||||
padding: 60px 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user