From 0b191d278e09bbaeb244790a4ac6e067abfbefeb Mon Sep 17 00:00:00 2001 From: Hermes CI Fix Date: Tue, 11 Aug 2026 00:11:25 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20OKR=E6=A8=A1=E6=9D=BF=E5=BA=93=E6=94=B9?= =?UTF-8?q?=E9=80=A0=20=E2=80=94=20=E5=BC=B9=E7=AA=97=E6=A8=A1=E6=9D=BF?= =?UTF-8?q?=E5=BA=93=E8=81=94=E5=8A=A8+=E6=97=B6=E9=97=B4=E5=88=86?= =?UTF-8?q?=E8=A7=A3=E8=90=BD=E5=BA=93+=E5=88=B6=E9=80=A0=E4=B8=9A?= =?UTF-8?q?=E5=8C=856=E4=B8=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/action_plans.py | 30 +++++ backend/app/api/okr.py | 80 +++++++++++ backend/scripts/seed_manufacturing_pack.py | 124 ++++++++++++++++++ frontend/src/api/index.ts | 4 + .../map-canvas/MapCanvasDialogs.vue | 100 +++++++++++++- .../src/components/okr/OkrDecomposition.vue | 46 ++++++- frontend/src/views/MapCanvas.vue | 37 +++++- 7 files changed, 413 insertions(+), 8 deletions(-) create mode 100644 backend/scripts/seed_manufacturing_pack.py diff --git a/backend/app/api/action_plans.py b/backend/app/api/action_plans.py index 8f910463..23f89030 100644 --- a/backend/app/api/action_plans.py +++ b/backend/app/api/action_plans.py @@ -155,9 +155,39 @@ def create_plan( db.add(plan) db.commit() db.refresh(plan) + + # OKR时间分解:KR创建时自动生成3个月度里程碑(按截止日期向前均分) + if due_date and not plan.monthly_milestones: + try: + plan.monthly_milestones = _auto_build_milestones(due_date) + db.commit() + except Exception: + db.rollback() # 里程碑生成失败不影响KR创建 + db.refresh(plan) return plan_to_dict(plan) +def _auto_build_milestones(due_date: datetime) -> list: + """按截止日期向前均分3个月度里程碑""" + base = due_date.replace(day=1) + month_keys = [] + for i in range(3, 0, -1): + y, m = base.year, base.month - i + while m <= 0: + m += 12 + y -= 1 + month_keys.append(f"{y:04d}-{m:02d}") + return [ + { + "month": mk, + "label": f"里程碑{i + 1}", + "status": "pending", + "target": None, + } + for i, mk in enumerate(month_keys) + ] + + @router.put("/{plan_id}") def update_plan( plan_id: int, diff --git a/backend/app/api/okr.py b/backend/app/api/okr.py index 5adca005..5198c268 100644 --- a/backend/app/api/okr.py +++ b/backend/app/api/okr.py @@ -97,6 +97,86 @@ def get_objective(obj_id: int, db: Session = Depends(get_db)): } +@router.put("/{okr_id}/decomposition/milestones/{kr_id}") +def update_kr_milestones(okr_id: int, kr_id: int, data: dict, db: Session = Depends(get_db)): + """保存KR的月度里程碑(时间分解视图编辑) — 同步更新KR进度""" + obj = db.query(Objective).filter(Objective.id == okr_id).first() + if not obj: + raise HTTPException(404, "OKR不存在") + kr = db.query(ActionPlan).filter( + ActionPlan.id == kr_id, ActionPlan.objective_id == okr_id + ).first() + if not kr: + raise HTTPException(404, "KR不存在或不属于该OKR") + milestones = data.get("milestones") + if not isinstance(milestones, list): + raise HTTPException(400, "milestones 必须是数组") + # 规范化:只保留 month/label/status/target 字段 + normalized = [] + for ms in milestones: + normalized.append({ + "month": ms.get("month", ""), + "label": ms.get("label", ""), + "status": ms.get("status", "pending"), + "target": ms.get("target"), + }) + kr.monthly_milestones = normalized + # 里程碑完成 → KR进度同步更新(完成数/总数) + if normalized: + done = sum(1 for ms in normalized if ms.get("status") == "completed") + kr.progress = round(done / len(normalized) * 100) + if kr.progress >= 100: + kr.status = "completed" + elif kr.status == "completed" and kr.progress < 100: + kr.status = "in_progress" + db.commit() + return {"ok": True, "kr_id": kr.id, "progress": kr.progress, "milestones": kr.monthly_milestones} + + +@router.post("/{okr_id}/decomposition/milestones/generate") +def auto_generate_milestones(okr_id: int, data: dict, db: Session = Depends(get_db)): + """为KR自动生成月度里程碑(按季度剩余月份均分)""" + obj = db.query(Objective).filter(Objective.id == okr_id).first() + if not obj: + raise HTTPException(404, "OKR不存在") + kr_id = data.get("kr_id") + if not kr_id: + raise HTTPException(400, "缺少 kr_id") + kr = db.query(ActionPlan).filter( + ActionPlan.id == kr_id, ActionPlan.objective_id == okr_id + ).first() + if not kr: + raise HTTPException(404, "KR不存在或不属于该OKR") + # 重新生成:覆盖已有里程碑(幂等) + milestones = _build_auto_milestones(kr) + kr.monthly_milestones = milestones + db.commit() + return {"ok": True, "kr_id": kr.id, "milestones": kr.monthly_milestones} + + +def _build_auto_milestones(kr: ActionPlan) -> list: + """按截止日期向前均分3个月度里程碑""" + base = kr.due_date or datetime.now() + timedelta(days=90) + base = base.replace(day=1) + # 计算前3个月份(从最早的月份开始) + month_keys = [] + for i in range(3, 0, -1): + y, m = base.year, base.month - i + while m <= 0: + m += 12 + y -= 1 + month_keys.append(f"{y:04d}-{m:02d}") + return [ + { + "month": mk, + "label": f"里程碑{i + 1}", + "status": "pending", + "target": None, + } + for i, mk in enumerate(month_keys) + ] + + @router.patch("/{obj_id}") def update_objective(obj_id: int, db: Session = Depends(get_db)): """更新OKR进度(通过查询ActionPlan自动计算)""" diff --git a/backend/scripts/seed_manufacturing_pack.py b/backend/scripts/seed_manufacturing_pack.py new file mode 100644 index 00000000..81a812c3 --- /dev/null +++ b/backend/scripts/seed_manufacturing_pack.py @@ -0,0 +1,124 @@ +"""OKR模板库 — 制造业行业包(6个模板,source='industry_pack', industry_tag='manufacturing')""" +import sys, os, logging +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from app.database import get_session_local +from app.models import OKRTemplate + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger("seed_manufacturing_pack") + +MANUFACTURING_TEMPLATES = [ + { + "name": "提升产能利用率", + "description": "优化排产与设备管理,提升产能利用率", + "dimension": "process", + "layer": "level2", + "industry_tag": "manufacturing", + "source": "industry_pack", + "sort_order": 301, + "preset_krs": [ + {"name": "产能利用率≥85%", "target_value": "≥85%", "weight": 34, "sort_order": 1}, + {"name": "设备停机时间下降≥20%", "target_value": "≥20%", "weight": 33, "sort_order": 2}, + {"name": "OEE综合效率≥75%", "target_value": "≥75%", "weight": 33, "sort_order": 3}, + ], + }, + { + "name": "降低生产成本", + "description": "通过精益生产与供应链优化降低制造成本", + "dimension": "finance", + "layer": "level2", + "industry_tag": "manufacturing", + "source": "industry_pack", + "sort_order": 302, + "preset_krs": [ + {"name": "单位制造成本下降≥10%", "target_value": "≥10%", "weight": 34, "sort_order": 1}, + {"name": "原材料损耗率≤2%", "target_value": "≤2%", "weight": 33, "sort_order": 2}, + {"name": "采购成本下降≥5%", "target_value": "≥5%", "weight": 33, "sort_order": 3}, + ], + }, + { + "name": "提升产品一次合格率", + "description": "强化质量管控,降低不良品率", + "dimension": "process", + "layer": "level2", + "industry_tag": "manufacturing", + "source": "industry_pack", + "sort_order": 303, + "preset_krs": [ + {"name": "一次合格率≥98%", "target_value": "≥98%", "weight": 34, "sort_order": 1}, + {"name": "客户投诉率下降≥30%", "target_value": "≥30%", "weight": 33, "sort_order": 2}, + {"name": "质量返工成本下降≥20%", "target_value": "≥20%", "weight": 33, "sort_order": 3}, + ], + }, + { + "name": "缩短交付周期", + "description": "优化生产计划与供应链协同,缩短订单交付周期", + "dimension": "customer", + "layer": "level2", + "industry_tag": "manufacturing", + "source": "industry_pack", + "sort_order": 304, + "preset_krs": [ + {"name": "订单交付周期缩短≥15%", "target_value": "≥15%", "weight": 34, "sort_order": 1}, + {"name": "准时交付率≥95%", "target_value": "≥95%", "weight": 33, "sort_order": 2}, + {"name": "在制品库存下降≥20%", "target_value": "≥20%", "weight": 33, "sort_order": 3}, + ], + }, + { + "name": "设备智能化升级", + "description": "推进设备数字化改造与智能制造", + "dimension": "learning", + "layer": "level2", + "industry_tag": "manufacturing", + "source": "industry_pack", + "sort_order": 305, + "preset_krs": [ + {"name": "核心设备联网率≥70%", "target_value": "≥70%", "weight": 34, "sort_order": 1}, + {"name": "自动化覆盖率≥50%", "target_value": "≥50%", "weight": 33, "sort_order": 2}, + {"name": "数据采集点覆盖关键工序100%", "target_value": "100%", "weight": 33, "sort_order": 3}, + ], + }, + { + "name": "安全生产零事故", + "description": "完善安全管理体系,实现安全生产零事故", + "dimension": "learning", + "layer": "level2", + "industry_tag": "manufacturing", + "source": "industry_pack", + "sort_order": 306, + "preset_krs": [ + {"name": "重大安全事故0起", "target_value": "0起", "weight": 40, "sort_order": 1}, + {"name": "安全隐患整改率100%", "target_value": "100%", "weight": 30, "sort_order": 2}, + {"name": "安全培训覆盖率100%", "target_value": "100%", "weight": 30, "sort_order": 3}, + ], + }, +] + + +def seed_manufacturing_pack(): + """插入制造业行业包(幂等:按industry_tag去重)""" + db = get_session_local()() + try: + existing = db.query(OKRTemplate).filter( + OKRTemplate.industry_tag == "manufacturing", + OKRTemplate.source == "industry_pack", + ).count() + if existing > 0: + logger.info(f"已有 {existing} 条制造业行业包模板,跳过") + return + for item in MANUFACTURING_TEMPLATES: + db.add(OKRTemplate(**item)) + db.commit() + logger.info(f"✔ 已插入 {len(MANUFACTURING_TEMPLATES)} 条制造业行业包模板") + except Exception as e: + db.rollback() + logger.error(f"制造业行业包插入失败: {e}") + raise + finally: + db.close() + + +if __name__ == "__main__": + seed_manufacturing_pack() + logger.info("OKR制造业行业包初始化完成") diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 607b9ce1..6d527320 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -85,6 +85,10 @@ export const okrApi = { create: (data: any) => api.post('/okr', data), update: (id: number) => api.patch(`/okr/${id}`), decomposition: (id: number) => api.get(`/okr/${id}/decomposition`), + saveMilestones: (okrId: number, krId: number, milestones: any[]) => + api.put(`/okr/${okrId}/decomposition/milestones/${krId}`, { milestones }), + generateMilestones: (okrId: number, krId: number) => + api.post(`/okr/${okrId}/decomposition/milestones/generate`, { kr_id: krId }), } export const dataApi = { diff --git a/frontend/src/components/map-canvas/MapCanvasDialogs.vue b/frontend/src/components/map-canvas/MapCanvasDialogs.vue index b5e87436..566dfe46 100644 --- a/frontend/src/components/map-canvas/MapCanvasDialogs.vue +++ b/frontend/src/components/map-canvas/MapCanvasDialogs.vue @@ -10,7 +10,8 @@
{{ dimensionName }}
-
+
模板库加载中...
+
{{ tmpl.name }} - + + + {{ industryTagLabel(tmpl.industry_tag) }} + + 用户 + +
{{ tmpl.description }}
@@ -104,6 +111,7 @@