diff --git a/backend/app/api/okr_templates.py b/backend/app/api/okr_templates.py index 967545f4..a15e4325 100644 --- a/backend/app/api/okr_templates.py +++ b/backend/app/api/okr_templates.py @@ -6,22 +6,44 @@ from sqlalchemy.orm import Session from typing import Optional from app.database import get_db from app.auth_middleware import require_role -from app.models import OKRTemplate +from app.models import OKRTemplate, StrategicMap router = APIRouter(prefix="/api/cma/okr-templates", tags=["OKR模板库"], dependencies=[Depends(require_role("ceo", "finance", "it"))], ) +DIMENSION_LAYER_NAMES = { + "finance": "财务层", + "customer": "客户层", + "process": "内部流程层", + "learning": "学习成长层", +} + +DIMENSION_LAYER_ICONS = { + "finance": "💰", + "customer": "👥", + "process": "⚙️", + "learning": "📚", +} + +DIMENSION_LAYER_COLORS = { + "finance": "#F56C6C", + "customer": "#409EFF", + "process": "#67C23A", + "learning": "#E6A23C", +} + @router.get("") def list_okr_templates( dimension: Optional[str] = Query(None, description="按维度筛选: finance/customer/process/learning"), source: Optional[str] = Query(None, description="按来源筛选: system/user/industry_pack"), industry_tag: Optional[str] = Query(None, description="按行业标签筛选"), + search: Optional[str] = Query(None, description="按O名称关键词搜索"), active_only: bool = Query(True, description="仅返回启用模板"), db: Session = Depends(get_db), ): - """列出 OKR 模板,支持按维度筛选""" + """列出 OKR 模板,支持按维度/来源/行业/名称搜索""" q = db.query(OKRTemplate) if dimension: q = q.filter(OKRTemplate.dimension == dimension) @@ -29,6 +51,8 @@ def list_okr_templates( q = q.filter(OKRTemplate.source == source) if industry_tag: q = q.filter(OKRTemplate.industry_tag == industry_tag) + if search: + q = q.filter(OKRTemplate.name.like(f"%{search}%")) if active_only: q = q.filter(OKRTemplate.is_active == 1) templates = q.order_by(OKRTemplate.sort_order, OKRTemplate.id).all() @@ -113,3 +137,67 @@ def increment_use_count(template_id: int, db: Session = Depends(get_db)): t.use_count = (t.use_count or 0) + 1 db.commit() return {"ok": True, "use_count": t.use_count} + + +@router.post("/{template_id}/apply") +def apply_okr_template(template_id: int, data: dict, db: Session = Depends(get_db)): + """应用 OKR 模板 — 创建战略地图并填入 O+KR""" + t = db.query(OKRTemplate).filter(OKRTemplate.id == template_id).first() + if not t: + raise HTTPException(404, "模板不存在") + + map_title = data.get("title", t.name) + dim = t.dimension + preset_krs = t.preset_krs or [] + + # 构建 dimensions: 仅包含模板所在的维度层 + dimensions = [] + for dk in ("finance", "customer", "process", "learning"): + objectives = [] + if dk == dim: + objectives.append({ + "name": t.name, + "description": t.description or "", + "kpis": [], + "krs": [ + { + "name": kr.get("name", ""), + "target_value": kr.get("target_value", ""), + "weight": kr.get("weight", 33), + } + for kr in preset_krs + ], + }) + dimensions.append({ + "key": dk, + "name": DIMENSION_LAYER_NAMES.get(dk, dk), + "icon": DIMENSION_LAYER_ICONS.get(dk, "📌"), + "color": DIMENSION_LAYER_COLORS.get(dk, "#909399"), + "objectives": objectives, + }) + + m = StrategicMap( + title=map_title, + version=data.get("version", "v1.0"), + status="draft", + dimensions=dimensions, + canvas_data={"connections": []}, + ) + db.add(m) + db.commit() + db.refresh(m) + + # 使用 maps API 的 _sync_map_objectives 同步到 map_objectives 表 + from app.api.maps import _sync_map_objectives + _sync_map_objectives(m, db) + + # 增加模板使用次数 + t.use_count = (t.use_count or 0) + 1 + db.commit() + + return { + "ok": True, + "map_id": m.id, + "title": m.title, + "template_id": template_id, + } diff --git a/backend/scripts/seed_industry_packs.py b/backend/scripts/seed_industry_packs.py new file mode 100644 index 00000000..787fc06f --- /dev/null +++ b/backend/scripts/seed_industry_packs.py @@ -0,0 +1,217 @@ +"""OKR模板库 Phase 2 — 行业扩展包(12个模板) +source='industry_pack' +""" +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_industry_packs") + +# ── 贸易经销包(7个,industry_tag='trading')─ +# PRD参考:优化渠补结构 / 加速应收周转 / 建立渠道分级体系 / 提升核心渠道忠诚度 / 建立渠补谈判SOP / 进销存系统全覆盖 / 渠道赋能培训 +TRADING_TEMPLATES = [ + { + "name": "优化渠补结构", + "description": "优化渠道补贴结构,提升渠道投入产出比", + "dimension": "finance", + "layer": "level2", + "industry_tag": "trading", + "source": "industry_pack", + "sort_order": 101, + "preset_krs": [ + {"name": "渠补率≤75%", "target_value": "≤75%", "weight": 40, "sort_order": 1}, + {"name": "渠补成本下降≥20%", "target_value": "≥20%", "weight": 30, "sort_order": 2}, + {"name": "渠道综合利润率提升≥5%", "target_value": "≥5%", "weight": 30, "sort_order": 3}, + ], + }, + { + "name": "加速应收周转", + "description": "加速应收账款周转,改善现金流健康度", + "dimension": "finance", + "layer": "level2", + "industry_tag": "trading", + "source": "industry_pack", + "sort_order": 102, + "preset_krs": [ + {"name": "应收账款周转天数≤30天", "target_value": "≤30天", "weight": 40, "sort_order": 1}, + {"name": "逾期账款占比≤5%", "target_value": "≤5%", "weight": 30, "sort_order": 2}, + {"name": "回款及时率≥95%", "target_value": "≥95%", "weight": 30, "sort_order": 3}, + ], + }, + { + "name": "建立渠道分级体系", + "description": "建立科学渠道分级管理体系,差异化赋能", + "dimension": "customer", + "layer": "level2", + "industry_tag": "trading", + "source": "industry_pack", + "sort_order": 103, + "preset_krs": [ + {"name": "渠道分级覆盖率100%", "target_value": "100%", "weight": 34, "sort_order": 1}, + {"name": "A级渠道占比≥30%", "target_value": "≥30%", "weight": 33, "sort_order": 2}, + {"name": "分级规则满意度≥85%", "target_value": "≥85%", "weight": 33, "sort_order": 3}, + ], + }, + { + "name": "提升核心渠道忠诚度", + "description": "提升核心渠道伙伴忠诚度和合作粘性", + "dimension": "customer", + "layer": "level2", + "industry_tag": "trading", + "source": "industry_pack", + "sort_order": 104, + "preset_krs": [ + {"name": "核心渠道流失率≤3%", "target_value": "≤3%", "weight": 34, "sort_order": 1}, + {"name": "核心渠道续约率≥90%", "target_value": "≥90%", "weight": 33, "sort_order": 2}, + {"name": "渠道满意度评分≥4.5分", "target_value": "≥4.5分", "weight": 33, "sort_order": 3}, + ], + }, + { + "name": "建立渠补谈判SOP", + "description": "建立标准化渠补谈判流程与SOP,减少灰色地带", + "dimension": "process", + "layer": "level2", + "industry_tag": "trading", + "source": "industry_pack", + "sort_order": 105, + "preset_krs": [ + {"name": "渠补谈判SOP覆盖率100%", "target_value": "100%", "weight": 34, "sort_order": 1}, + {"name": "谈判周期缩短≥30%", "target_value": "≥30%", "weight": 33, "sort_order": 2}, + {"name": "渠补争议事件≤3起/季", "target_value": "≤3起/季", "weight": 33, "sort_order": 3}, + ], + }, + { + "name": "进销存系统全覆盖", + "description": "实现进销存系统全渠道覆盖,数据实时可视", + "dimension": "process", + "layer": "level2", + "industry_tag": "trading", + "source": "industry_pack", + "sort_order": 106, + "preset_krs": [ + {"name": "进销存系统覆盖率≥90%", "target_value": "≥90%", "weight": 34, "sort_order": 1}, + {"name": "库存数据实时更新率100%", "target_value": "100%", "weight": 33, "sort_order": 2}, + {"name": "进销存报表自动生成率≥80%", "target_value": "≥80%", "weight": 33, "sort_order": 3}, + ], + }, + { + "name": "渠道赋能培训", + "description": "系统化渠道伙伴赋能培训,提升渠道综合能力", + "dimension": "learning", + "layer": "level2", + "industry_tag": "trading", + "source": "industry_pack", + "sort_order": 107, + "preset_krs": [ + {"name": "渠道培训覆盖率≥80%", "target_value": "≥80%", "weight": 34, "sort_order": 1}, + {"name": "培训考核通过率≥85%", "target_value": "≥85%", "weight": 33, "sort_order": 2}, + {"name": "培训后渠道业绩提升≥10%", "target_value": "≥10%", "weight": 33, "sort_order": 3}, + ], + }, +] + +# ── IT服务包(5个,industry_tag='it_service')─ +# PRD参考:提升人均产值 / 提高NPS净推荐值 / 知识管理体系化 / 交付标准化 / 工程师认证体系 +ITSERVICE_TEMPLATES = [ + { + "name": "提升人均产值", + "description": "提升技术团队人均产值和项目利润率", + "dimension": "finance", + "layer": "level2", + "industry_tag": "it_service", + "source": "industry_pack", + "sort_order": 201, + "preset_krs": [ + {"name": "人均产值≥30万/季", "target_value": "≥30万/季", "weight": 40, "sort_order": 1}, + {"name": "项目毛利率≥25%", "target_value": "≥25%", "weight": 30, "sort_order": 2}, + {"name": "人效同比增长≥15%", "target_value": "≥15%", "weight": 30, "sort_order": 3}, + ], + }, + { + "name": "提高NPS净推荐值", + "description": "提高客户NPS净推荐值,建立良好口碑", + "dimension": "customer", + "layer": "level2", + "industry_tag": "it_service", + "source": "industry_pack", + "sort_order": 202, + "preset_krs": [ + {"name": "NPS得分≥70", "target_value": "≥70", "weight": 34, "sort_order": 1}, + {"name": "客户满意度评分≥4.5分", "target_value": "≥4.5分", "weight": 33, "sort_order": 2}, + {"name": "客户推荐率≥40%", "target_value": "≥40%", "weight": 33, "sort_order": 3}, + ], + }, + { + "name": "交付标准化", + "description": "实现项目交付流程标准化和可复制", + "dimension": "process", + "layer": "level2", + "industry_tag": "it_service", + "source": "industry_pack", + "sort_order": 203, + "preset_krs": [ + {"name": "交付SOP覆盖率100%", "target_value": "100%", "weight": 34, "sort_order": 1}, + {"name": "项目按时交付率≥90%", "target_value": "≥90%", "weight": 33, "sort_order": 2}, + {"name": "项目验收一次通过率≥85%", "target_value": "≥85%", "weight": 33, "sort_order": 3}, + ], + }, + { + "name": "知识管理体系化", + "description": "建设体系化知识管理平台,沉淀项目经验", + "dimension": "learning", + "layer": "level2", + "industry_tag": "it_service", + "source": "industry_pack", + "sort_order": 204, + "preset_krs": [ + {"name": "知识库文章数量≥500篇", "target_value": "≥500篇", "weight": 34, "sort_order": 1}, + {"name": "知识复用率≥40%", "target_value": "≥40%", "weight": 33, "sort_order": 2}, + {"name": "知识贡献覆盖率≥80%", "target_value": "≥80%", "weight": 33, "sort_order": 3}, + ], + }, + { + "name": "工程师认证体系", + "description": "建立工程师技术认证与职业成长体系", + "dimension": "learning", + "layer": "level2", + "industry_tag": "it_service", + "source": "industry_pack", + "sort_order": 205, + "preset_krs": [ + {"name": "认证覆盖率≥60%", "target_value": "≥60%", "weight": 34, "sort_order": 1}, + {"name": "高级工程师占比≥30%", "target_value": "≥30%", "weight": 33, "sort_order": 2}, + {"name": "认证通过率≥80%", "target_value": "≥80%", "weight": 33, "sort_order": 3}, + ], + }, +] + +ALL_INDUSTRY_TEMPLATES = TRADING_TEMPLATES + ITSERVICE_TEMPLATES + + +def seed_industry_packs(): + """插入行业包模板(如果不存在)""" + db = get_session_local()() + try: + existing = db.query(OKRTemplate).filter(OKRTemplate.source == "industry_pack").count() + if existing > 0: + logger.info(f"已有 {existing} 条行业包模板,跳过") + return + for item in ALL_INDUSTRY_TEMPLATES: + t = OKRTemplate(**item) + db.add(t) + db.commit() + logger.info(f"✔ 已插入 {len(ALL_INDUSTRY_TEMPLATES)} 条行业包模板(贸易经销7 + IT服务5)") + except Exception as e: + db.rollback() + logger.error(f"行业包种子数据插入失败: {e}") + raise + finally: + db.close() + + +if __name__ == "__main__": + seed_industry_packs() + logger.info("OKR行业扩展包初始化完成") diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 3efe72ce..791bdc82 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -71,6 +71,14 @@ export const templateApi = { delete: (id: number) => api.delete(`/templates/${id}`), } +export const okrTemplateApi = { + list: (params?: any) => api.get('/okr-templates', { params }), + get: (id: number) => api.get(`/okr-templates/${id}`), + create: (data: any) => api.post('/okr-templates', data), + incrementUse: (id: number) => api.post(`/okr-templates/${id}/use`), + applyTemplate: (id: number, data: any) => api.post(`/okr-templates/${id}/apply`, data), +} + export const dataApi = { importExcel: (file: File, qs?: string) => { const form = new FormData() diff --git a/frontend/src/permission.ts b/frontend/src/permission.ts index 5ad8e974..240141a6 100644 --- a/frontend/src/permission.ts +++ b/frontend/src/permission.ts @@ -5,10 +5,10 @@ // 各角色可访问的路由列表 export const ROLE_ROUTES: Record = { - ceo: ['/my-dashboard', '/dashboard', '/kpis', '/maps', '/maps-review', '/maps/canvas', '/maps/review', '/alerts', '/notifications', '/org', '/users', '/permissions', '/budget', '/deviations', '/cost', '/predict', '/action-plans', '/knowledge', '/guide', '/customer', '/learning-dashboard', '/reports', '/alignment', '/dupont-analysis'], - finance: ['/my-dashboard', '/dashboard', '/kpis', '/maps', '/maps-review', '/maps/canvas', '/maps/review', '/alerts', '/data', '/budget', '/deviations', '/cost', '/predict', '/action-plans', '/knowledge', '/guide', '/customer', '/learning-dashboard', '/reports', '/alignment', '/dupont-analysis'], + ceo: ['/my-dashboard', '/dashboard', '/kpis', '/maps', '/maps-review', '/maps/canvas', '/maps/review', '/alerts', '/notifications', '/org', '/users', '/permissions', '/budget', '/deviations', '/cost', '/predict', '/action-plans', '/knowledge', '/guide', '/customer', '/learning-dashboard', '/reports', '/alignment', '/dupont-analysis', '/okr-templates'], + finance: ['/my-dashboard', '/dashboard', '/kpis', '/maps', '/maps-review', '/maps/canvas', '/maps/review', '/alerts', '/data', '/budget', '/deviations', '/cost', '/predict', '/action-plans', '/knowledge', '/guide', '/customer', '/learning-dashboard', '/reports', '/alignment', '/dupont-analysis', '/okr-templates'], business: ['/my-dashboard', '/dashboard', '/kpis', '/alerts', '/deviations', '/budget', '/action-plans', '/knowledge', '/guide', '/customer', '/reports', '/alignment'], - it: ['/my-dashboard', '/dashboard', '/kpis', '/alerts', '/data', '/org', '/users', '/permissions', '/budget', '/deviations', '/cost', '/predict', '/action-plans', '/knowledge', '/guide', '/customer', '/learning-dashboard'], + it: ['/my-dashboard', '/dashboard', '/kpis', '/alerts', '/data', '/org', '/users', '/permissions', '/budget', '/deviations', '/cost', '/predict', '/action-plans', '/knowledge', '/guide', '/customer', '/learning-dashboard', '/okr-templates'], } // 可操作的CRUD权限 @@ -67,6 +67,7 @@ export const MENU_ITEMS: MenuItem[] = [ // GROUP 5: 系统与支持(Infra) // ══════════════════════════════════════════════════════════════ { path: '/knowledge', label: 'CMA知识库', icon: 'Document', roles: ['ceo', 'finance', 'business', 'it'], group: '系统与支持' }, + { path: '/okr-templates', label: 'OKR模板库', icon: 'Collection', roles: ['ceo', 'finance', 'it'], group: '系统与支持' }, { path: '/guide', label: '新手引导', icon: 'Edit', roles: ['ceo', 'finance', 'business', 'it'], group: '系统与支持' }, { path: '/org', label: '组织管理', icon: 'Collection', roles: ['ceo', 'it'], group: '系统与支持' }, { path: '/users', label: '用户管理', icon: 'User', roles: ['ceo', 'it'], group: '系统与支持' }, diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 4dad71b1..94718309 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -34,6 +34,7 @@ const routes = [ { path: 'dupont-analysis', name: 'DupontAnalysis', component: () => import('@/views/DupontAnalysis.vue'), meta: { title: '杜邦分析', roles: ['ceo', 'finance', 'it'] } }, { path: 'data-quality', name: 'DataQuality', component: () => import('@/views/DataQuality.vue'), meta: { title: '数据质量', roles: ['ceo', 'finance', 'it'] } }, { path: 'bi-reports', name: 'BiReports', component: () => import('@/views/BIReportCenter.vue'), meta: { title: 'BI报表', roles: ['ceo', 'finance', 'business'] } }, + { path: 'okr-templates', name: 'OKRTemplates', component: () => import('@/views/OKRTemplates.vue'), meta: { title: 'OKR模板库', roles: ['ceo', 'finance', 'it'] } }, ] }, ] diff --git a/frontend/src/views/OKRTemplates.vue b/frontend/src/views/OKRTemplates.vue new file mode 100644 index 00000000..78678f61 --- /dev/null +++ b/frontend/src/views/OKRTemplates.vue @@ -0,0 +1,390 @@ + + + + +