fix: 行动方案库统计卡片—补全API+overdue计算
This commit is contained in:
@@ -0,0 +1,372 @@
|
||||
<template>
|
||||
<div class="okr-decomposition">
|
||||
<div v-if="loading" style="text-align:center;padding:40px;">
|
||||
<el-icon class="is-loading" :size="24"><Loading /></el-icon> 加载中...
|
||||
</div>
|
||||
|
||||
<template v-if="!loading && data">
|
||||
<!-- 时间轴头部:年 → 季 -->
|
||||
<div class="timeline-header">
|
||||
<el-tag type="info" size="large" class="tag-annual">
|
||||
<el-icon><Calendar /></el-icon> {{ data.annual_o || '年度目标' }}
|
||||
</el-tag>
|
||||
<span class="arrow">→</span>
|
||||
<el-tag type="primary" size="large" class="tag-quarterly">
|
||||
<el-icon><TrendCharts /></el-icon> {{ data.quarterly_o }}
|
||||
</el-tag>
|
||||
</div>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<!-- KR 月度里程碑 -->
|
||||
<div v-if="data.krs.length === 0" class="empty-section">
|
||||
暂无关联KR
|
||||
</div>
|
||||
<div v-for="kr in data.krs" :key="kr.kr_id" class="kr-timeline">
|
||||
<div class="kr-header">
|
||||
<div class="kr-title">{{ kr.title }}</div>
|
||||
<div class="kr-progress-wrap">
|
||||
<el-progress :percentage="kr.progress" :stroke-width="6" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="milestones-row">
|
||||
<div
|
||||
v-for="(ms, i) in kr.milestones"
|
||||
:key="i"
|
||||
:class="['milestone-card', 'ms-' + (ms.status || 'pending')]"
|
||||
@click="editMilestone(kr, ms, i)"
|
||||
>
|
||||
<div class="ms-dot" :class="'dot-' + (ms.status || 'pending')"></div>
|
||||
<div class="ms-body">
|
||||
<div class="ms-label">{{ ms.label }}</div>
|
||||
<div class="ms-month">{{ formatMonth(ms.month) }}</div>
|
||||
</div>
|
||||
<el-tag size="small" :type="msStatusType(ms.status)" class="ms-status">
|
||||
{{ msStatusLabel(ms.status) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div v-if="kr.milestones.length === 0" class="no-milestones">
|
||||
暂无里程碑
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<!-- 本周行动 -->
|
||||
<div class="weekly-section">
|
||||
<div class="section-title">
|
||||
<el-icon color="#67C23A"><Checked /></el-icon> 本周行动 ({{ data.weekly_actions.length }})
|
||||
</div>
|
||||
<div v-if="data.weekly_actions.length === 0" class="empty-section">
|
||||
本周暂无行动计划
|
||||
</div>
|
||||
<div v-for="action in data.weekly_actions" :key="action.id" class="action-item" :class="'act-' + action.status">
|
||||
<el-checkbox :model-value="action.status === 'completed'" size="small" />
|
||||
<span class="action-title">{{ action.title }}</span>
|
||||
<span v-if="action.deadline" class="action-deadline">{{ action.deadline.slice(0, 10) }}</span>
|
||||
<el-tag size="small" :type="actionStatusType(action.status)" class="action-tag">
|
||||
{{ actionStatusLabel(action.status) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 里程碑编辑弹窗 -->
|
||||
<el-dialog v-model="showEditDialog" title="编辑里程碑" width="400px" destroy-on-close>
|
||||
<el-form label-width="80px" v-if="editingMs">
|
||||
<el-form-item label="标签">
|
||||
<el-input v-model="editingMs.label" />
|
||||
</el-form-item>
|
||||
<el-form-item label="月份">
|
||||
<el-input v-model="editingMs.month" placeholder="如 2026-07" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="editingMs.status" style="width:100%">
|
||||
<el-option label="待开始" value="pending" />
|
||||
<el-option label="进行中" value="in_progress" />
|
||||
<el-option label="已完成" value="completed" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button size="small" @click="showEditDialog = false">取消</el-button>
|
||||
<el-button size="small" type="primary" :loading="saving" @click="saveMilestone">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Calendar, TrendCharts, Checked, Loading } from '@element-plus/icons-vue'
|
||||
import { okrApi } from '../../api/index'
|
||||
|
||||
const props = defineProps<{
|
||||
okrId: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'refresh'): void
|
||||
}>()
|
||||
|
||||
const loading = ref(true)
|
||||
const data = ref<any>(null)
|
||||
const showEditDialog = ref(false)
|
||||
const editingKr = ref<any>(null)
|
||||
const editingMsIdx = ref(-1)
|
||||
const editingMs = ref<any>(null)
|
||||
const saving = ref(false)
|
||||
|
||||
function formatMonth(m: string) {
|
||||
if (!m) return ''
|
||||
// 2026-07 → 2026年7月
|
||||
const parts = m.split('-')
|
||||
if (parts.length === 2) return `${parts[0]}年${parseInt(parts[1])}月`
|
||||
return m
|
||||
}
|
||||
|
||||
function msStatusType(s: string) {
|
||||
if (s === 'completed') return 'success'
|
||||
if (s === 'in_progress') return 'warning'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
function msStatusLabel(s: string) {
|
||||
if (s === 'completed') return '已完成'
|
||||
if (s === 'in_progress') return '进行中'
|
||||
return '待开始'
|
||||
}
|
||||
|
||||
function actionStatusType(s: string) {
|
||||
if (s === 'completed') return 'success'
|
||||
if (s === 'in_progress') return 'warning'
|
||||
if (s === 'cancelled') return 'danger'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
function actionStatusLabel(s: string) {
|
||||
const labels: Record<string, string> = { pending: '待处理', in_progress: '进行中', completed: '已完成', cancelled: '已取消' }
|
||||
return labels[s] || s
|
||||
}
|
||||
|
||||
function editMilestone(kr: any, ms: any, idx: number) {
|
||||
editingKr.value = kr
|
||||
editingMsIdx.value = idx
|
||||
editingMs.value = { ...ms }
|
||||
showEditDialog.value = true
|
||||
}
|
||||
|
||||
async function saveMilestone() {
|
||||
if (!editingKr.value || !editingMs.value) return
|
||||
saving.value = true
|
||||
try {
|
||||
// Update in local data first
|
||||
const kr = data.value.krs.find((k: any) => k.kr_id === editingKr.value.kr_id)
|
||||
if (kr && kr.milestones[editingMsIdx.value]) {
|
||||
kr.milestones[editingMsIdx.value] = { ...editingMs.value }
|
||||
}
|
||||
ElMessage.success('里程碑已更新')
|
||||
showEditDialog.value = false
|
||||
emit('refresh')
|
||||
} catch (e: any) {
|
||||
ElMessage.error('保存失败: ' + (e?.message || ''))
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
if (!props.okrId) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await okrApi.decomposition(props.okrId)
|
||||
data.value = res
|
||||
} catch (e: any) {
|
||||
ElMessage.error('加载分解视图失败: ' + (e?.response?.data?.detail || e?.message || ''))
|
||||
data.value = null
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.okrId, (val) => {
|
||||
if (val) loadData()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (props.okrId) loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.okr-decomposition {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
/* ── 时间轴头部 ── */
|
||||
.timeline-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 12px 0;
|
||||
}
|
||||
.tag-annual {
|
||||
font-size: 14px;
|
||||
padding: 6px 14px;
|
||||
}
|
||||
.tag-quarterly {
|
||||
font-size: 14px;
|
||||
padding: 6px 14px;
|
||||
}
|
||||
.arrow {
|
||||
font-size: 22px;
|
||||
color: #C0C4CC;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
/* ── KR 时间轴 ── */
|
||||
.kr-timeline {
|
||||
background: #FAFBFC;
|
||||
border: 1px solid #EBEEF5;
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
margin-bottom: 14px;
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
.kr-timeline:hover {
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||
}
|
||||
.kr-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.kr-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
min-width: 0;
|
||||
}
|
||||
.kr-progress-wrap {
|
||||
flex-shrink: 0;
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
/* ── 里程碑卡片 ── */
|
||||
.milestones-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.milestone-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #EBEEF5;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
flex: 1;
|
||||
min-width: 180px;
|
||||
}
|
||||
.milestone-card:hover {
|
||||
border-color: #409EFF;
|
||||
box-shadow: 0 2px 8px rgba(64,158,255,0.12);
|
||||
}
|
||||
.ms-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.dot-pending {
|
||||
background: #C0C4CC;
|
||||
}
|
||||
.dot-in_progress {
|
||||
background: #E6A23C;
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
.dot-completed {
|
||||
background: #67C23A;
|
||||
}
|
||||
.ms-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.ms-label {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.ms-month {
|
||||
font-size: 11px;
|
||||
color: #909399;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.ms-status {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.no-milestones {
|
||||
color: #C0C4CC;
|
||||
font-size: 12px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
/* ── 本周行动 ── */
|
||||
.weekly-section {
|
||||
margin-top: 4px;
|
||||
}
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.action-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #EBEEF5;
|
||||
margin-bottom: 8px;
|
||||
background: #fff;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.action-item.act-completed {
|
||||
opacity: 0.65;
|
||||
background: #F5F7FA;
|
||||
}
|
||||
.action-title {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
}
|
||||
.action-deadline {
|
||||
font-size: 11px;
|
||||
color: #909399;
|
||||
}
|
||||
.action-tag {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.empty-section {
|
||||
color: #C0C4CC;
|
||||
font-size: 13px;
|
||||
padding: 20px 0;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user