- 插入12个行业包模板:贸易经销(7)+IT服务(5) - 后端: name搜索+apply端点 - 前端: OKRTemplates.vue页面(搜索/筛选/三分区/应用弹窗) - 路由+菜单+权限配置
204 lines
6.5 KiB
Python
204 lines
6.5 KiB
Python
"""
|
|
OKR模板库 API — 按维度筛选 + 用户自定义模板
|
|
"""
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
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, 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 模板,支持按维度/来源/行业/名称搜索"""
|
|
q = db.query(OKRTemplate)
|
|
if dimension:
|
|
q = q.filter(OKRTemplate.dimension == dimension)
|
|
if source:
|
|
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()
|
|
return {
|
|
"total": len(templates),
|
|
"items": [
|
|
{
|
|
"id": t.id,
|
|
"name": t.name,
|
|
"description": t.description,
|
|
"dimension": t.dimension,
|
|
"layer": t.layer,
|
|
"industry_tag": t.industry_tag,
|
|
"preset_krs": t.preset_krs,
|
|
"source": t.source,
|
|
"use_count": t.use_count,
|
|
"sort_order": t.sort_order,
|
|
"is_active": t.is_active,
|
|
"created_at": t.created_at.isoformat() if t.created_at else None,
|
|
}
|
|
for t in templates
|
|
],
|
|
}
|
|
|
|
|
|
@router.get("/{template_id}")
|
|
def get_okr_template(template_id: int, db: Session = Depends(get_db)):
|
|
"""获取单个 OKR 模板详情"""
|
|
t = db.query(OKRTemplate).filter(OKRTemplate.id == template_id).first()
|
|
if not t:
|
|
raise HTTPException(404, "模板不存在")
|
|
return {
|
|
"id": t.id,
|
|
"name": t.name,
|
|
"description": t.description,
|
|
"dimension": t.dimension,
|
|
"layer": t.layer,
|
|
"industry_tag": t.industry_tag,
|
|
"preset_krs": t.preset_krs,
|
|
"source": t.source,
|
|
"use_count": t.use_count,
|
|
"sort_order": t.sort_order,
|
|
"is_active": t.is_active,
|
|
}
|
|
|
|
|
|
@router.post("")
|
|
def create_okr_template(data: dict, db: Session = Depends(get_db)):
|
|
"""用户自定义 OKR 模板"""
|
|
name = data.get("name", "").strip()
|
|
if not name:
|
|
raise HTTPException(400, "模板名称不能为空")
|
|
dimension = data.get("dimension", "")
|
|
if dimension not in ("finance", "customer", "process", "learning"):
|
|
raise HTTPException(400, "维度无效,必须是 finance/customer/process/learning")
|
|
preset_krs = data.get("preset_krs", [])
|
|
if not isinstance(preset_krs, list) or len(preset_krs) == 0:
|
|
raise HTTPException(400, "至少需要一个预设KR")
|
|
|
|
t = OKRTemplate(
|
|
name=name,
|
|
description=data.get("description", ""),
|
|
dimension=dimension,
|
|
layer=data.get("layer", "level3"),
|
|
industry_tag=data.get("industry_tag", "general"),
|
|
preset_krs=preset_krs,
|
|
source="user",
|
|
sort_order=data.get("sort_order", 0),
|
|
)
|
|
db.add(t)
|
|
db.commit()
|
|
db.refresh(t)
|
|
return {"ok": True, "id": t.id, "name": t.name}
|
|
|
|
|
|
@router.post("/{template_id}/use")
|
|
def increment_use_count(template_id: int, db: Session = Depends(get_db)):
|
|
"""增加模板使用次数"""
|
|
t = db.query(OKRTemplate).filter(OKRTemplate.id == template_id).first()
|
|
if not t:
|
|
raise HTTPException(404, "模板不存在")
|
|
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,
|
|
}
|