feat: PDCA双循环显式化 — 导航PDCA标识 + 双会议模板 + 战略回顾会数据联动

This commit is contained in:
Hermes CI Fix
2026-07-21 18:10:48 +08:00
parent 3db7bef025
commit 270d7758f8
3 changed files with 232 additions and 21 deletions
+4 -4
View File
@@ -90,10 +90,10 @@ const iconMap: Record<string, any> = {
// 分组图标映射 — PDCA五组
const groupIcons: Record<string, any> = {
'基础数据': Document,
'规划与目标': DataBoard,
'执行与控制': Edit,
'监控与评价': TrendCharts,
'复盘与改进': DataAnalysis,
'🔵 P 规划与目标': DataBoard,
'🟢 D 执行与控制': Edit,
'🟡 C 监控与评价': TrendCharts,
'🔴 A 复盘与改进': DataAnalysis,
'系统与支持': Tools,
}
+17 -17
View File
@@ -34,34 +34,34 @@ export const MENU_ITEMS: MenuItem[] = [
{ path: '/data', label: '数据管理', icon: 'Connection', roles: ['ceo', 'finance', 'it'], group: '基础数据' },
// ── 规划与目标 ──
{ path: '/my-dashboard', label: '我的工作台', icon: 'Monitor', roles: ['ceo', 'finance', 'business', 'it'], group: '规划与目标' },
{ path: '/maps', label: '战略地图', icon: 'DataBoard', roles: ['ceo', 'finance'], group: '规划与目标' },
{ path: '/budget', label: '预算管理', icon: 'Coin', roles: ['ceo', 'finance', 'business', 'it'], group: '规划与目标' },
{ path: '/alignment', label: '战略执行看板', icon: 'Opportunity', roles: ['ceo', 'finance', 'business', 'it'], group: '规划与目标' },
{ path: '/my-dashboard', label: '我的工作台', icon: 'Monitor', roles: ['ceo', 'finance', 'business', 'it'], group: '🔵 P 规划与目标' },
{ path: '/maps', label: '战略地图', icon: 'DataBoard', roles: ['ceo', 'finance'], group: '🔵 P 规划与目标' },
{ path: '/budget', label: '预算管理', icon: 'Coin', roles: ['ceo', 'finance', 'business', 'it'], group: '🔵 P 规划与目标' },
{ path: '/alignment', label: '战略执行看板', icon: 'Opportunity', roles: ['ceo', 'finance', 'business', 'it'], group: '🔵 P 规划与目标' },
// ══════════════════════════════════════════════════════════════
// GROUP 2: 执行与控制(Do
// ══════════════════════════════════════════════════════════════
{ path: '/cost', label: '成本分析', icon: 'Money', roles: ['ceo', 'finance', 'it'], group: '执行与控制' },
{ path: '/deviations', label: '差异分析', icon: 'DataAnalysis', roles: ['ceo', 'finance', 'business', 'it'], group: '执行与控制' },
{ path: '/action-plans', label: '改善行动', icon: 'Edit', roles: ['ceo', 'finance', 'business', 'it'], group: '执行与控制' },
{ path: '/cost', label: '成本分析', icon: 'Money', roles: ['ceo', 'finance', 'it'], group: '🟢 D 执行与控制' },
{ path: '/deviations', label: '差异分析', icon: 'DataAnalysis', roles: ['ceo', 'finance', 'business', 'it'], group: '🟢 D 执行与控制' },
{ path: '/action-plans', label: '改善行动', icon: 'Edit', roles: ['ceo', 'finance', 'business', 'it'], group: '🟢 D 执行与控制' },
// ══════════════════════════════════════════════════════════════
// GROUP 3: 监控与评价(Check
// ══════════════════════════════════════════════════════════════
{ path: '/dashboard', label: '经营看板', icon: 'DataBoard', roles: ['ceo', 'finance', 'business', 'it'], group: '监控与评价' },
{ path: '/reports', label: '管理报表', icon: 'DataAnalysis', roles: ['ceo', 'finance', 'business'], group: '监控与评价' },
{ path: '/dupont-analysis', label: '杜邦分析', icon: 'TrendCharts', roles: ['ceo', 'finance', 'it'], group: '监控与评价' },
{ path: '/customer', label: '客户维度', icon: 'User', roles: ['ceo', 'finance', 'business', 'it'], group: '监控与评价' },
{ path: '/learning-dashboard', label: '学习成长看板', icon: 'Reading', roles: ['ceo', 'finance', 'it'], group: '监控与评价' },
{ path: '/alerts', label: '预警中心', icon: 'WarningFilled', roles: ['ceo', 'finance', 'business', 'it'], group: '监控与评价' },
{ path: '/data-quality', label: '数据质量', icon: 'WarningFilled', roles: ['ceo', 'finance', 'it'], group: '复盘与改进' },
{ path: '/dashboard', label: '经营看板', icon: 'DataBoard', roles: ['ceo', 'finance', 'business', 'it'], group: '🟡 C 监控与评价' },
{ path: '/reports', label: '管理报表', icon: 'DataAnalysis', roles: ['ceo', 'finance', 'business'], group: '🟡 C 监控与评价' },
{ path: '/dupont-analysis', label: '杜邦分析', icon: 'TrendCharts', roles: ['ceo', 'finance', 'it'], group: '🟡 C 监控与评价' },
{ path: '/customer', label: '客户维度', icon: 'User', roles: ['ceo', 'finance', 'business', 'it'], group: '🟡 C 监控与评价' },
{ path: '/learning-dashboard', label: '学习成长看板', icon: 'Reading', roles: ['ceo', 'finance', 'it'], group: '🟡 C 监控与评价' },
{ path: '/alerts', label: '预警中心', icon: 'WarningFilled', roles: ['ceo', 'finance', 'business', 'it'], group: '🟡 C 监控与评价' },
{ path: '/data-quality', label: '数据质量', icon: 'WarningFilled', roles: ['ceo', 'finance', 'it'], group: '🔴 A 复盘与改进' },
// ══════════════════════════════════════════════════════════════
// GROUP 4: 复盘与改进(Act
// ══════════════════════════════════════════════════════════════
{ path: '/maps-review', label: '战略回顾会', icon: 'TrendCharts', roles: ['ceo', 'finance'], group: '复盘与改进' },
{ path: '/predict', label: '预测模拟', icon: 'DataLine', roles: ['ceo', 'finance', 'it'], group: '复盘与改进' },
{ path: '/real-options', label: '实物期权计算器', icon: 'TrendCharts', roles: ['ceo', 'finance'], group: '复盘与改进' },
{ path: '/maps-review', label: '战略回顾会', icon: 'TrendCharts', roles: ['ceo', 'finance'], group: '🔴 A 复盘与改进' },
{ path: '/predict', label: '预测模拟', icon: 'DataLine', roles: ['ceo', 'finance', 'it'], group: '🔴 A 复盘与改进' },
{ path: '/real-options', label: '实物期权计算器', icon: 'TrendCharts', roles: ['ceo', 'finance'], group: '🔴 A 复盘与改进' },
// ══════════════════════════════════════════════════════════════
// GROUP 5: 系统与支持(Infra
+211
View File
@@ -19,6 +19,136 @@
</div>
</div>
<!-- 会议类型选择 -->
<div class="meeting-type-bar">
<el-radio-group v-model="meetingType" size="default" @change="onMeetingTypeChange">
<el-radio-button value="strategic">
<span style="font-weight:600;">🔵 战略回顾会季度</span>
</el-radio-button>
<el-radio-button value="operational">
<span style="font-weight:600;">🟢 运营复盘会月度</span>
</el-radio-button>
</el-radio-group>
<el-tag v-if="meetingType === 'strategic'" type="primary" size="small" style="margin-left:12px;">
议程战略地图O检查 因果链验证 O调整 下季度优先级
</el-tag>
<el-tag v-else type="success" size="small" style="margin-left:12px;">
议程①OKR进度检查 ②KPI达标情况 ③偏差分析 ④改善行动确认
</el-tag>
</div>
<!-- 运营复盘会特殊内容 -->
<template v-if="meetingType === 'operational' && !loading && summary">
<div class="op-agenda-cards">
<div class="agenda-card">
<div class="agenda-card-title"> OKR进度检查</div>
<div class="agenda-card-body">
<div v-for="dim in dimensions" :key="dim.key" class="agenda-dim-row">
<span class="agenda-dim-icon">{{ dim.icon }}</span>
<span class="agenda-dim-name">{{ dim.name }}</span>
<span class="agenda-dim-count">{{ dim.objectives?.length || 0 }} 个O</span>
</div>
</div>
</div>
<div class="agenda-card">
<div class="agenda-card-title"> KPI达标情况</div>
<div class="agenda-card-body">
<div class="kpi-summary-row"><span class="green-dot">🟢</span> 达成: {{ summary.green || 0 }} </div>
<div class="kpi-summary-row"><span class="yellow-dot">🟡</span> 预警: {{ summary.yellow || 0 }} </div>
<div class="kpi-summary-row"><span class="red-dot">🔴</span> 异常: {{ summary.red || 0 }} </div>
</div>
</div>
<div class="agenda-card">
<div class="agenda-card-title"> 偏差分析</div>
<div class="agenda-card-body">
<div v-if="deviations.length > 0">
<div v-for="d in deviations.slice(0,5)" :key="d.id" class="deviation-row">
<span class="deviation-badge" :class="'badge-' + (d.alert_level || 'yellow')">{{ d.alert_level === 'red' ? '🔴' : '🟡' }}</span>
<span class="deviation-kpi">{{ d.kpi_name || d.kpi_code }}</span>
<span class="deviation-rate">{{ d.deviation_rate != null ? d.deviation_rate.toFixed(1) + '%' : '-' }}</span>
</div>
<el-empty v-if="deviations.length === 0" description="本期无偏差" :image-size="60" />
</div>
<div v-else class="no-data">加载中...</div>
</div>
</div>
<div class="agenda-card">
<div class="agenda-card-title"> 改善行动确认</div>
<div class="agenda-card-body">
<div class="kpi-summary-row"> 已完成: {{ doneCount() }} </div>
<div class="kpi-summary-row">🔄 进行中: {{ inProgressCount() }} </div>
<div class="kpi-summary-row"> 待处理: {{ pendingCount() }} </div>
<div class="kpi-summary-row" v-if="overdueCount() > 0">🔴 逾期: {{ overdueCount() }} </div>
<el-button size="small" type="primary" style="margin-top:10px;" @click="$router.push('/action-plans')">查看改善行动 </el-button>
</div>
</div>
</div>
</template>
<!-- 战略回顾会数据联动卡片 -->
<template v-if="meetingType === 'strategic' && !loading && summary">
<div class="linkage-cards">
<div class="linkage-card">
<div class="linkage-card-title">📊 各层O完成情况</div>
<div class="linkage-card-body">
<div v-for="dim in dimensions" :key="dim.key" class="linkage-dim-row">
<span class="linkage-dim-icon">{{ dim.icon }}</span>
<div class="linkage-dim-info">
<span class="linkage-dim-name">{{ dim.name }}</span>
<span class="linkage-dim-ratio">
{{ dim.objectives?.filter((o:any) => o.level === 'green').length || 0 }}/{{ dim.objectives?.length || 0 }} 达成
</span>
</div>
</div>
<el-empty v-if="dimensions.length === 0" description="暂无O数据" :image-size="60" />
</div>
</div>
<div class="linkage-card">
<div class="linkage-card-title">📈 KR进度</div>
<div class="linkage-card-body">
<div v-if="krs.length > 0">
<div v-for="kr in krs.slice(0,8)" :key="kr.id" class="kr-row">
<span class="kr-name">{{ kr.name || kr.title }}</span>
<el-progress :percentage="kr.progress || 0" :stroke-width="10" style="width:100px;" />
</div>
</div>
<div v-else class="no-data">从OKR模块同步中...</div>
</div>
</div>
<div class="linkage-card">
<div class="linkage-card-title">🚨 偏差项</div>
<div class="linkage-card-body">
<div v-if="deviations.length > 0">
<div v-for="d in deviations.slice(0,8)" :key="d.id" class="deviation-row">
<span class="deviation-badge" :class="'badge-' + (d.alert_level || 'yellow')">{{ d.alert_level === 'red' ? '🔴' : '🟡' }}</span>
<span class="deviation-kpi">{{ d.kpi_name || d.kpi_code }}</span>
<span class="deviation-rate">{{ d.deviation_rate != null ? d.deviation_rate.toFixed(1) + '%' : '-' }}</span>
</div>
<el-button size="small" style="margin-top:8px;" @click="$router.push('/deviations')">查看全部偏差 </el-button>
</div>
<el-empty v-else-if="!loadingDeviations" description="本期无偏差" :image-size="60" />
<div v-else class="no-data">加载中...</div>
</div>
</div>
<div class="linkage-card">
<div class="linkage-card-title">📋 上期改善行动完成情况</div>
<div class="linkage-card-body">
<div class="kpi-summary-row"> 已完成: {{ doneCount() }} </div>
<div class="kpi-summary-row">🔄 进行中: {{ inProgressCount() }} </div>
<div class="kpi-summary-row"> 待处理: {{ pendingCount() }} </div>
<div class="kpi-summary-row" v-if="overdueCount() > 0">🔴 逾期: {{ overdueCount() }} </div>
<div v-if="actionPlans.length > 0" class="action-plan-preview">
<div v-for="p in actionPlans.slice(0,4)" :key="p.id" class="ap-row">
<span class="ap-status">{{ planIcon(p) }}</span>
<span class="ap-title">{{ p.title }}</span>
</div>
</div>
<el-empty v-else-if="actionPlans.length === 0 && !loading" description="无上期改善行动" :image-size="60" />
</div>
</div>
</div>
</template>
<!-- 加载中 -->
<div v-if="loading" class="loading-wrap">
<el-skeleton :rows="6" animated />
@@ -179,6 +309,12 @@ const showKpiDialog = ref(false)
const selectedObjName = ref("")
const selectedKpis = ref<any[]>([])
// ── 双会议模板状态 ──
const meetingType = ref("strategic")
const deviations = ref<any[]>([])
const krs = ref<any[]>([])
const loadingDeviations = ref(false)
// ── 计算属性 ──
const pendingCount = () => actionPlans.value.filter((p:any) => p.status === 'pending').length
const inProgressCount = () => actionPlans.value.filter((p:any) => p.status === 'in_progress').length
@@ -196,6 +332,9 @@ async function loadReview() {
dimensions.value = r.dimensions || []
focusItems.value = r.focus_items || []
actionPlans.value = r.action_plans || []
// 联动加载偏差数据
await loadDeviations()
await loadKrs()
} catch (e: any) {
ElMessage.error("加载战略回顾数据失败")
} finally {
@@ -203,6 +342,37 @@ async function loadReview() {
}
}
// ── 加载偏差数据(从差异分析模块联动) ──
async function loadDeviations() {
loadingDeviations.value = true
try {
const r: any = await api.get('/deviations', { params: { hierarchical: true, page_size: 20 } })
const data = r.data || r.items || r || []
deviations.value = Array.isArray(data) ? data : []
} catch (_) {
deviations.value = []
} finally {
loadingDeviations.value = false
}
}
// ── 加载KR进度数据 ──
async function loadKrs() {
try {
const r: any = await api.get('/maps/' + selectedMap.value + '/krs')
krs.value = r.data || r || []
} catch (_) {
krs.value = []
}
}
// ── 会议类型切换 ──
function onMeetingTypeChange() {
if (selectedMap.value) {
loadReview()
}
}
// ── 工具函数 ──
function badgeIcon(level: string) {
return level === 'green' ? '🟢' : level === 'yellow' ? '🟡' : level === 'red' ? '🔴' : '⚪'
@@ -350,4 +520,45 @@ onMounted(async () => {
.kpi-detail-item { display: flex; align-items: center; gap: 8px; padding: 4px 0; font-size: 13px; }
.kpi-detail-item .label { color: #999; width: 60px; }
.kpi-detail-item .val { font-weight: 500; }
/* ── 双会议模板 ── */
.meeting-type-bar { display: flex; align-items: center; margin-bottom: 20px; flex-wrap: wrap; gap: 8px; }
.op-agenda-cards,
.linkage-cards { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; margin-bottom: 24px; }
.agenda-card,
.linkage-card { background: #fff; border-radius: 12px; padding: 16px; box-shadow: 0 1px 4px rgba(0,0,0,0.08); border: 1px solid #f0f0f0; }
.agenda-card-title,
.linkage-card-title { font-size: 15px; font-weight: 600; margin-bottom: 12px; color: #333; padding-bottom: 8px; border-bottom: 2px solid #f0f2f5; }
.agenda-card-body,
.linkage-card-body { font-size: 13px; }
.agenda-dim-row,
.linkage-dim-row { display: flex; align-items: center; gap: 8px; padding: 6px 0; border-bottom: 1px solid #f5f5f5; }
.agenda-dim-row:last-child,
.linkage-dim-row:last-child { border-bottom: none; }
.agenda-dim-icon,
.linkage-dim-icon { font-size: 16px; }
.agenda-dim-name,
.linkage-dim-name { flex: 1; }
.agenda-dim-count { color: #999; font-size: 12px; }
.linkage-dim-info { flex: 1; display: flex; justify-content: space-between; align-items: center; }
.linkage-dim-ratio { color: #409EFF; font-size: 12px; font-weight: 500; }
.kpi-summary-row { padding: 4px 0; font-size: 13px; display: flex; align-items: center; gap: 6px; }
.deviation-row { display: flex; align-items: center; gap: 8px; padding: 6px 0; border-bottom: 1px solid #f5f5f5; font-size: 13px; }
.deviation-row:last-child { border-bottom: none; }
.deviation-badge { font-size: 12px; flex-shrink: 0; }
.deviation-kpi { flex: 1; color: #333; }
.deviation-rate { font-weight: 500; color: #e6a23c; }
.badge-red .deviation-rate { color: #f56c6c; }
.kr-row { display: flex; align-items: center; gap: 10px; padding: 6px 0; border-bottom: 1px solid #f5f5f5; }
.kr-row:last-child { border-bottom: none; }
.kr-name { flex: 1; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.action-plan-preview { margin-top: 8px; border-top: 1px solid #f0f0f0; padding-top: 8px; }
.ap-row { display: flex; align-items: center; gap: 6px; padding: 4px 0; font-size: 12px; }
.ap-status { flex-shrink: 0; }
.ap-title { flex: 1; color: #666; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.no-data { color: #999; padding: 12px; text-align: center; font-size: 13px; }
@media screen and (max-width: 768px) {
.op-agenda-cards,
.linkage-cards { grid-template-columns: 1fr; }
}
</style>