Phase 1: OKR模板 — 两步弹窗(O+KR结构) + 14个CMA模板 + okr_templates表 + API
MapCanvasDialogs.vue: - 节点编辑弹窗改为两步交互(模板选择→O+KR编辑) - 第一步:按维度展示对应模板(财务4/客户3/流程4/学习3) - 第二步:编辑O名称+描述+3个KR(名称/目标值/权重) - 14个CMA标准模板硬编码在前端 - 支持添加/删除KR、自定义目标跳过模板 MapCanvas.vue: - 传递 editingLayerKey 到弹窗 - onSaveDialog 适配新 O+KR 数据结构 - onNodeSave 存储 krs 和 template_name 字段 Backend: - OKRTemplate 模型 (okr_templates 表) - okr_templates API: GET(按维度筛选) + POST(用户自定义) + increment use_count - seed_okr_templates.py 迁移脚本(建表+14条种子数据) - 在 main.py 注册新路由
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
"""
|
||||
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
|
||||
|
||||
router = APIRouter(prefix="/api/cma/okr-templates", tags=["OKR模板库"],
|
||||
dependencies=[Depends(require_role("ceo", "finance", "it"))],
|
||||
)
|
||||
|
||||
|
||||
@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="按行业标签筛选"),
|
||||
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 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}
|
||||
Reference in New Issue
Block a user