本体三支柱: 科目↔KPI↔OKR三层互联 + 追溯链API + OKR详情页签
- 新表 kpi_subject_map(89映射/24KPI) / objective_kpi(3O×5KPI) / krs(3O×3KR) - GET /api/cma/ontology/trace?objective_id=N O→KPI→科目逐层追溯 - GET /api/cma/ontology/objectives 三层链路概览 - OkrDetail.vue 新增本体追溯链页签(KR目标/当前值 + KPI→科目标签流) - 含并发已上线未提交的KPI多粒度列(target_monthly/quarterly/yearly) - 回归: pytest 409 passed
This commit is contained in:
@@ -0,0 +1,157 @@
|
|||||||
|
"""
|
||||||
|
CMA本体三支柱追溯链 API — 科目 ↔ KPI ↔ OKR的O 三层互联 (2026-08-19)
|
||||||
|
|
||||||
|
追溯链: 目标(O) → 指标(KPI) → 科目(数据)
|
||||||
|
objective_kpi 表: O 由哪些 KPI 度量
|
||||||
|
kpi_subject_map 表: KPI 由哪些科目计算
|
||||||
|
krs 表: O 的关键结果 KR (OKR完整化)
|
||||||
|
"""
|
||||||
|
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 Objective, KPIDefinition, ObjectiveKPI, KPISubjectMap, KR, Subject
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/cma/ontology", tags=["本体三支柱"],
|
||||||
|
dependencies=[Depends(require_role("ceo", "finance", "it", "business"))],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _num(v):
|
||||||
|
"""Decimal → float 便于 JSON 序列化"""
|
||||||
|
return float(v) if v is not None else None
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/trace")
|
||||||
|
def trace_ontology(
|
||||||
|
objective_id: int = Query(..., description="OKR目标ID, 从O→KPI→科目逐层追溯"),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""本体追溯链: O(目标) → KPI(指标) → 科目(数据)"""
|
||||||
|
obj = db.query(Objective).filter(Objective.id == objective_id).first()
|
||||||
|
if not obj:
|
||||||
|
raise HTTPException(404, "目标不存在")
|
||||||
|
|
||||||
|
# 第2层: O 支撑的 KPI (objective_kpi)
|
||||||
|
links = (
|
||||||
|
db.query(ObjectiveKPI, KPIDefinition)
|
||||||
|
.join(KPIDefinition, KPIDefinition.id == ObjectiveKPI.kpi_id)
|
||||||
|
.filter(ObjectiveKPI.objective_id == objective_id)
|
||||||
|
.order_by(ObjectiveKPI.id)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
# 第3层: 每个 KPI 依赖的科目 (kpi_subject_map)
|
||||||
|
subject_by_code = {
|
||||||
|
s.subject_code: s.subject_name
|
||||||
|
for s in db.query(Subject).filter(Subject.is_active == 1).all()
|
||||||
|
}
|
||||||
|
kpi_layer = []
|
||||||
|
for link, kpi in links:
|
||||||
|
maps = (
|
||||||
|
db.query(KPISubjectMap)
|
||||||
|
.filter(KPISubjectMap.kpi_id == kpi.id)
|
||||||
|
.order_by(KPISubjectMap.id)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
subjects = [
|
||||||
|
{
|
||||||
|
"subject_code": m.subject_code,
|
||||||
|
"subject_name": subject_by_code.get(m.subject_code, ""),
|
||||||
|
"calc_type": m.calc_type,
|
||||||
|
"weight": _num(m.weight),
|
||||||
|
"remark": m.remark,
|
||||||
|
}
|
||||||
|
for m in maps
|
||||||
|
]
|
||||||
|
kpi_layer.append({
|
||||||
|
"kpi_id": kpi.id,
|
||||||
|
"kpi_code": kpi.kpi_code,
|
||||||
|
"kpi_name": kpi.kpi_name,
|
||||||
|
"dimension": kpi.dimension,
|
||||||
|
"unit": kpi.unit,
|
||||||
|
"weight": _num(link.weight),
|
||||||
|
"formula": kpi.formula,
|
||||||
|
"subjects": subjects,
|
||||||
|
})
|
||||||
|
|
||||||
|
# KR 层 (OKR完整化: O→KR)
|
||||||
|
krs = db.query(KR).filter(KR.objective_id == objective_id).order_by(KR.id).all()
|
||||||
|
kr_list = []
|
||||||
|
for kr in krs:
|
||||||
|
mkpi = db.query(KPIDefinition).filter(KPIDefinition.id == kr.metric_kpi_id).first() if kr.metric_kpi_id else None
|
||||||
|
kr_list.append({
|
||||||
|
"id": kr.id,
|
||||||
|
"title": kr.title,
|
||||||
|
"metric_kpi_id": kr.metric_kpi_id,
|
||||||
|
"metric_kpi_code": mkpi.kpi_code if mkpi else None,
|
||||||
|
"metric_kpi_name": mkpi.kpi_name if mkpi else None,
|
||||||
|
"target_value": _num(kr.target_value),
|
||||||
|
"current_value": _num(kr.current_value),
|
||||||
|
"progress": kr.progress,
|
||||||
|
"status": kr.status,
|
||||||
|
"due_date": kr.due_date.isoformat() if kr.due_date else None,
|
||||||
|
})
|
||||||
|
|
||||||
|
subject_total = sum(len(k["subjects"]) for k in kpi_layer)
|
||||||
|
return {
|
||||||
|
"objective": {
|
||||||
|
"id": obj.id,
|
||||||
|
"title": obj.title,
|
||||||
|
"description": obj.description,
|
||||||
|
"dimension": obj.dimension,
|
||||||
|
"quarter": obj.quarter,
|
||||||
|
"owner": obj.owner,
|
||||||
|
"status": obj.status,
|
||||||
|
"progress": obj.progress,
|
||||||
|
},
|
||||||
|
"krs": kr_list,
|
||||||
|
"kpis": kpi_layer,
|
||||||
|
"chain": {
|
||||||
|
"objective_id": obj.id,
|
||||||
|
"objective_title": obj.title,
|
||||||
|
"kpi_count": len(kpi_layer),
|
||||||
|
"subject_count": subject_total,
|
||||||
|
"path": "O(目标) → KPI(指标) → 科目(数据)",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/objectives")
|
||||||
|
def list_ontology_objectives(
|
||||||
|
quarter: Optional[str] = Query(None, description="筛选季度: 2026Q3"),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""所有OKR目标的三层链路概览(前端OKR页用)"""
|
||||||
|
q = db.query(Objective)
|
||||||
|
if quarter:
|
||||||
|
q = q.filter(Objective.quarter == quarter)
|
||||||
|
objs = q.order_by(Objective.quarter.desc(), Objective.id).all()
|
||||||
|
results = []
|
||||||
|
for o in objs:
|
||||||
|
kpi_links = (
|
||||||
|
db.query(ObjectiveKPI, KPIDefinition)
|
||||||
|
.join(KPIDefinition, KPIDefinition.id == ObjectiveKPI.kpi_id)
|
||||||
|
.filter(ObjectiveKPI.objective_id == o.id)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
krs = db.query(KR).filter(KR.objective_id == o.id).all()
|
||||||
|
results.append({
|
||||||
|
"id": o.id,
|
||||||
|
"title": o.title,
|
||||||
|
"dimension": o.dimension,
|
||||||
|
"quarter": o.quarter,
|
||||||
|
"owner": o.owner,
|
||||||
|
"status": o.status,
|
||||||
|
"progress": o.progress,
|
||||||
|
"kpi_count": len(kpi_links),
|
||||||
|
"kr_count": len(krs),
|
||||||
|
"kpis": [{"kpi_id": k.id, "kpi_code": k.kpi_code, "kpi_name": k.kpi_name,
|
||||||
|
"weight": _num(l.weight)} for l, k in kpi_links],
|
||||||
|
"krs": [{"id": kr.id, "title": kr.title, "progress": kr.progress,
|
||||||
|
"status": kr.status,
|
||||||
|
"due_date": kr.due_date.isoformat() if kr.due_date else None}
|
||||||
|
for kr in krs],
|
||||||
|
})
|
||||||
|
return {"total": len(results), "items": results}
|
||||||
+2
-1
@@ -5,7 +5,7 @@ from fastapi.middleware.cors import CORSMiddleware
|
|||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from app.database import init_db
|
from app.database import init_db
|
||||||
from app.api import auth, kpis, kpi_governance, templates, maps, dashboard, data, alerts, ai_analysis, alert_rules, users, thresholds, notifications, permissions, action_plans, alignment, org, objectives, versions, budget, cost, predict, reports, security, knowledge, bot_bridge, bot_bridge_v2, lead, tenant, customer_dashboard, deviation_push, budget_generate, knowledge_articles, kpi_causality, data_quality, bi_reports, entities, bsc_layers, okr, okr_templates, subjects, driver_budget, bot_kpis, bot_iron_law, analysis_results, expenses, cash, tax_compliance, verify, growth_quality
|
from app.api import auth, kpis, kpi_governance, templates, maps, dashboard, data, alerts, ai_analysis, alert_rules, users, thresholds, notifications, permissions, action_plans, alignment, org, objectives, versions, budget, cost, predict, reports, security, knowledge, bot_bridge, bot_bridge_v2, lead, tenant, customer_dashboard, deviation_push, budget_generate, knowledge_articles, kpi_causality, data_quality, bi_reports, entities, bsc_layers, okr, okr_templates, subjects, driver_budget, bot_kpis, ontology, bot_iron_law, analysis_results, expenses, cash, tax_compliance, verify, growth_quality
|
||||||
from app.utils.cache import clear_all as clear_cache, delete as delete_cache
|
from app.utils.cache import clear_all as clear_cache, delete as delete_cache
|
||||||
from scripts.erp_sync import run_sync as run_erp_sync
|
from scripts.erp_sync import run_sync as run_erp_sync
|
||||||
from app.auth_middleware import require_auth
|
from app.auth_middleware import require_auth
|
||||||
@@ -74,6 +74,7 @@ app.include_router(okr_templates.router)
|
|||||||
app.include_router(subjects.router)
|
app.include_router(subjects.router)
|
||||||
app.include_router(driver_budget.router)
|
app.include_router(driver_budget.router)
|
||||||
app.include_router(bot_kpis.router)
|
app.include_router(bot_kpis.router)
|
||||||
|
app.include_router(ontology.router)
|
||||||
app.include_router(bot_iron_law.router)
|
app.include_router(bot_iron_law.router)
|
||||||
app.include_router(analysis_results.router)
|
app.include_router(analysis_results.router)
|
||||||
app.include_router(expenses.router)
|
app.include_router(expenses.router)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""管理会计OS 数据模型"""
|
"""管理会计OS 数据模型"""
|
||||||
from sqlalchemy import Column, Integer, String, Text, Float, DateTime, ForeignKey, Boolean, JSON, func, UniqueConstraint
|
from sqlalchemy import Column, Integer, String, Text, Float, DateTime, ForeignKey, Boolean, JSON, func, UniqueConstraint, Numeric, Date
|
||||||
from app.database import Base
|
from app.database import Base
|
||||||
|
|
||||||
from app.models.budget_plan import BudgetPlan
|
from app.models.budget_plan import BudgetPlan
|
||||||
@@ -75,7 +75,10 @@ class KPIDefinition(Base):
|
|||||||
data_owner = Column(String(100), default="待指定", comment="数据责任人")
|
data_owner = Column(String(100), default="待指定", comment="数据责任人")
|
||||||
frequency = Column(String(20), default="monthly", comment="daily/weekly/monthly/quarterly/yearly")
|
frequency = Column(String(20), default="monthly", comment="daily/weekly/monthly/quarterly/yearly")
|
||||||
unit = Column(String(50), default="%", comment="单位")
|
unit = Column(String(50), default="%", comment="单位")
|
||||||
target_value = Column(Float, nullable=True, comment="目标值")
|
target_value = Column(Float, nullable=True, comment="目标值(兼容旧字段)")
|
||||||
|
target_monthly = Column(Float, nullable=True, comment="月度目标值")
|
||||||
|
target_quarterly = Column(Float, nullable=True, comment="季度目标值")
|
||||||
|
target_yearly = Column(Float, nullable=True, comment="年度目标值")
|
||||||
threshold_green = Column(String(100), nullable=True, comment="绿灯阈值")
|
threshold_green = Column(String(100), nullable=True, comment="绿灯阈值")
|
||||||
threshold_yellow = Column(String(100), nullable=True, comment="黄灯阈值")
|
threshold_yellow = Column(String(100), nullable=True, comment="黄灯阈值")
|
||||||
threshold_red = Column(String(100), nullable=True, comment="红灯阈值")
|
threshold_red = Column(String(100), nullable=True, comment="红灯阈值")
|
||||||
@@ -675,3 +678,45 @@ class SocialSecurity(Base):
|
|||||||
remark = Column(String(500), nullable=True, comment="备注")
|
remark = Column(String(500), nullable=True, comment="备注")
|
||||||
created_at = Column(DateTime, server_default=func.now())
|
created_at = Column(DateTime, server_default=func.now())
|
||||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 本体三支柱: 科目↔KPI↔OKR 三层互联 (2026-08-19)
|
||||||
|
# 追溯链: 目标(O) → 指标(KPI) → 科目(数据)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
class KPISubjectMap(Base):
|
||||||
|
"""科目↔KPI映射 — 指标计算依赖的底层会计科目"""
|
||||||
|
__tablename__ = "kpi_subject_map"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
kpi_id = Column(Integer, ForeignKey("kpi_definitions.id"), nullable=False, comment="KPI ID")
|
||||||
|
subject_code = Column(String(20), nullable=False, comment="科目编码")
|
||||||
|
calc_type = Column(String(20), default="sum", comment="sum/avg/ratio/other")
|
||||||
|
weight = Column(Numeric(5, 2), default=1.00, comment="权重(负=扣减项)")
|
||||||
|
remark = Column(String(200), nullable=True, comment="备注")
|
||||||
|
__table_args__ = (UniqueConstraint("kpi_id", "subject_code", name="uk_kpi_subject"),)
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectiveKPI(Base):
|
||||||
|
"""KPI↔O支撑 — 目标由哪些KPI度量"""
|
||||||
|
__tablename__ = "objective_kpi"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
objective_id = Column(Integer, ForeignKey("objectives.id"), nullable=False, comment="OKR目标ID")
|
||||||
|
kpi_id = Column(Integer, ForeignKey("kpi_definitions.id"), nullable=False, comment="KPI ID")
|
||||||
|
weight = Column(Numeric(5, 2), default=1.00, comment="支撑权重")
|
||||||
|
__table_args__ = (UniqueConstraint("objective_id", "kpi_id", name="uk_obj_kpi"),)
|
||||||
|
|
||||||
|
|
||||||
|
class KR(Base):
|
||||||
|
"""关键结果KR — OKR完整化 (O→KR→KPI)"""
|
||||||
|
__tablename__ = "krs"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
objective_id = Column(Integer, ForeignKey("objectives.id"), nullable=False, comment="OKR目标ID")
|
||||||
|
title = Column(String(200), nullable=False, comment="KR标题")
|
||||||
|
metric_kpi_id = Column(Integer, ForeignKey("kpi_definitions.id"), nullable=True, comment="度量KPI ID")
|
||||||
|
target_value = Column(Numeric(15, 2), nullable=True, comment="目标值")
|
||||||
|
current_value = Column(Numeric(15, 2), nullable=True, comment="当前值")
|
||||||
|
progress = Column(Integer, default=0, comment="完成进度 0-100")
|
||||||
|
status = Column(String(20), default="pending", comment="pending/in_progress/completed/cancelled")
|
||||||
|
due_date = Column(Date, nullable=True, comment="截止日期")
|
||||||
|
created_at = Column(DateTime, server_default=func.now())
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
-- CMA本体三支柱: 科目↔KPI↔OKR 三层互联 DDL (2026-08-19)
|
||||||
|
USE cma;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS kpi_subject_map (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
kpi_id INT NOT NULL,
|
||||||
|
subject_code VARCHAR(20) NOT NULL,
|
||||||
|
calc_type VARCHAR(20) DEFAULT 'sum', -- sum/avg/ratio/other
|
||||||
|
weight DECIMAL(5,2) DEFAULT 1.00, -- 权重(负=扣减项)
|
||||||
|
remark VARCHAR(200),
|
||||||
|
UNIQUE KEY uk_kpi_subject (kpi_id, subject_code)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='科目↔KPI映射(指标计算依赖的底层科目)';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS objective_kpi (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
objective_id INT NOT NULL,
|
||||||
|
kpi_id INT NOT NULL,
|
||||||
|
weight DECIMAL(5,2) DEFAULT 1.00,
|
||||||
|
UNIQUE KEY uk_obj_kpi (objective_id, kpi_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='KPI↔O支撑(目标由哪些KPI度量)';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS krs (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
objective_id INT NOT NULL,
|
||||||
|
title VARCHAR(200) NOT NULL,
|
||||||
|
metric_kpi_id INT, -- 关联的度量KPI
|
||||||
|
target_value DECIMAL(15,2),
|
||||||
|
current_value DECIMAL(15,2),
|
||||||
|
progress INT DEFAULT 0, -- 0-100
|
||||||
|
status VARCHAR(20) DEFAULT 'pending',
|
||||||
|
due_date DATE,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='关键结果KR(OKR完整化)';
|
||||||
|
-- kpi_subject_map 种子数据: 核心财务KPI → 会计科目映射 (2026-08-19)
|
||||||
|
-- weight: 1.00=加项 -1.00=减项; calc_type: sum=加总 ratio=比率
|
||||||
|
USE cma;
|
||||||
|
|
||||||
|
INSERT INTO kpi_subject_map (kpi_id, subject_code, calc_type, weight, remark) VALUES
|
||||||
|
-- F_REVENUE 营收 = 主营业务收入 + 利息收入 + 其他业务收入 + 投资收益
|
||||||
|
(1, '6001', 'sum', 1.00, '主营业务收入'),
|
||||||
|
(1, '6011', 'sum', 0.02, '利息收入(银行存款)'),
|
||||||
|
(1, '6051', 'sum', 0.05, '其他业务收入'),
|
||||||
|
(1, '6111', 'sum', 0.03, '投资收益'),
|
||||||
|
-- F_GROSS_MARGIN 毛利率 = (收入-成本)/收入
|
||||||
|
(2, '6001', 'ratio', 1.00, '毛利率分子-收入'),
|
||||||
|
(2, '6401', 'ratio', -1.00, '毛利率分子-主营业务成本'),
|
||||||
|
(2, '6402', 'ratio', -1.00, '毛利率分子-其他业务成本'),
|
||||||
|
-- F_NET_PROFIT 净利润 = 收入 - 成本 - 费用
|
||||||
|
(3, '6001', 'sum', 1.00, '净利润-主营业务收入'),
|
||||||
|
(3, '6051', 'sum', 0.05, '净利润-其他业务收入'),
|
||||||
|
(3, '6011', 'sum', 0.02, '净利润-利息收入'),
|
||||||
|
(3, '6111', 'sum', 0.03, '净利润-投资收益'),
|
||||||
|
(3, '6401', 'sum', -1.00, '净利润-主营业务成本'),
|
||||||
|
(3, '6402', 'sum', -1.00, '净利润-其他业务成本'),
|
||||||
|
(3, '6601', 'sum', -1.00, '净利润-销售费用'),
|
||||||
|
(3, '6602', 'sum', -1.00, '净利润-管理费用'),
|
||||||
|
(3, '660204','sum', -0.30, '净利润-研发费用'),
|
||||||
|
(3, '6603', 'sum', -1.00, '净利润-财务费用'),
|
||||||
|
(3, '6701', 'sum', -1.00, '净利润-资产减值损失'),
|
||||||
|
(3, '6801', 'sum', -1.00, '净利润-所得税费用'),
|
||||||
|
-- F_OP_CFLOW 经营性现金流 = 现金类科目净变动
|
||||||
|
(4, '1001', 'sum', 1.00, '经营性现金流-库存现金'),
|
||||||
|
(4, '1002', 'sum', 1.00, '经营性现金流-银行存款'),
|
||||||
|
(4, '1122', 'sum', 1.00, '经营性现金流-应收账款收回'),
|
||||||
|
(4, '2202', 'sum', 1.00, '经营性现金流-应付账款支付'),
|
||||||
|
-- F_COST_RATIO 费用率 = 期间费用/收入
|
||||||
|
(5, '6601', 'ratio', 1.00, '费用率-销售费用'),
|
||||||
|
(5, '6602', 'ratio', 1.00, '费用率-管理费用'),
|
||||||
|
(5, '6603', 'ratio', 1.00, '费用率-财务费用'),
|
||||||
|
(5, '6001', 'ratio', -1.00, '费用率分母-收入'),
|
||||||
|
-- F_AR_DAYS 应收账款周转天数 = 应收/收入*360
|
||||||
|
(6, '1122', 'ratio', 1.00, '应收周转-应收账款'),
|
||||||
|
(6, '6001', 'ratio', -1.00, '应收周转分母-主营业务收入'),
|
||||||
|
-- F_ASSET_TURNOVER 总资产周转率 = 收入/总资产
|
||||||
|
(13, '6001', 'ratio', 1.00, '资产周转分子-收入'),
|
||||||
|
(13, '1001', 'ratio', -1.00, '资产周转分母-库存现金'),
|
||||||
|
(13, '1002', 'ratio', -1.00, '资产周转分母-银行存款'),
|
||||||
|
(13, '1122', 'ratio', -1.00, '资产周转分母-应收账款'),
|
||||||
|
(13, '1405', 'ratio', -1.00, '资产周转分母-库存商品'),
|
||||||
|
(13, '1601', 'ratio', -1.00, '资产周转分母-固定资产'),
|
||||||
|
-- F_REVENUE_GROWTH 收入增长率 = 本期收入/上期收入-1
|
||||||
|
(14, '6001', 'ratio', 1.00, '收入增长-主营业务收入'),
|
||||||
|
-- F_CURRENT_RATIO 流动比率 = 流动资产/流动负债
|
||||||
|
(17, '1001', 'ratio', 1.00, '流动比率-库存现金'),
|
||||||
|
(17, '1002', 'ratio', 1.00, '流动比率-银行存款'),
|
||||||
|
(17, '1122', 'ratio', 1.00, '流动比率-应收账款'),
|
||||||
|
(17, '1405', 'ratio', 1.00, '流动比率-库存商品'),
|
||||||
|
(17, '2001', 'ratio', -1.00, '流动比率-短期借款'),
|
||||||
|
(17, '2202', 'ratio', -1.00, '流动比率-应付账款'),
|
||||||
|
(17, '2203', 'ratio', -1.00, '流动比率-预收账款'),
|
||||||
|
(17, '2211', 'ratio', -1.00, '流动比率-应付职工薪酬'),
|
||||||
|
-- F_QUICK_RATIO 速动比率 = (流动资产-存货)/流动负债
|
||||||
|
(18, '1001', 'ratio', 1.00, '速动比率-库存现金'),
|
||||||
|
(18, '1002', 'ratio', 1.00, '速动比率-银行存款'),
|
||||||
|
(18, '1122', 'ratio', 1.00, '速动比率-应收账款'),
|
||||||
|
(18, '1405', 'ratio', -1.00, '速动比率-存货扣减'),
|
||||||
|
(18, '2202', 'ratio', -1.00, '速动比率-应付账款'),
|
||||||
|
(18, '2203', 'ratio', -1.00, '速动比率-预收账款'),
|
||||||
|
(18, '2211', 'ratio', -1.00, '速动比率-应付职工薪酬'),
|
||||||
|
-- F_INV_DAYS 存货周转天数 = 存货/成本*360
|
||||||
|
(19, '1405', 'ratio', 1.00, '存货周转-库存商品'),
|
||||||
|
(19, '6401', 'ratio', -1.00, '存货周转分母-主营业务成本'),
|
||||||
|
-- F_ROI 总资产报酬率 = 利润/资产
|
||||||
|
(20, '4103', 'ratio', 1.00, 'ROI分子-本年利润'),
|
||||||
|
(20, '4001', 'ratio', -1.00, 'ROI分母-实收资本'),
|
||||||
|
(20, '4002', 'ratio', -1.00, 'ROI分母-资本公积'),
|
||||||
|
-- F_DEBT_RATIO 资产负债率 = 负债/资产
|
||||||
|
(30, '2001', 'ratio', 1.00, '负债率-短期借款'),
|
||||||
|
(30, '2202', 'ratio', 1.00, '负债率-应付账款'),
|
||||||
|
(30, '2203', 'ratio', 1.00, '负债率-预收账款'),
|
||||||
|
(30, '2211', 'ratio', 1.00, '负债率-应付职工薪酬'),
|
||||||
|
(30, '2221', 'ratio', 1.00, '负债率-应交税费'),
|
||||||
|
(30, '2241', 'ratio', 1.00, '负债率-其他应付款'),
|
||||||
|
(30, '2501', 'ratio', 1.00, '负债率-长期借款'),
|
||||||
|
(30, '2502', 'ratio', 1.00, '负债率-应付债券'),
|
||||||
|
(30, '4001', 'ratio', -1.00, '负债率分母-实收资本'),
|
||||||
|
(30, '4002', 'ratio', -1.00, '负债率分母-资本公积'),
|
||||||
|
-- F_INTEREST_COVER 利息保障倍数 = 利润/利息支出
|
||||||
|
(31, '4103', 'ratio', 1.00, '利息保障分子-本年利润'),
|
||||||
|
(31, '660301','ratio', -1.00, '利息保障分母-利息支出(借款)'),
|
||||||
|
-- F_EVA 经济增加值 = 利润 - 资本成本
|
||||||
|
(32, '4103', 'sum', 1.00, 'EVA-本年利润'),
|
||||||
|
(32, '4001', 'sum', -0.06, 'EVA-资本成本(实收资本×6%)'),
|
||||||
|
-- F_FCF 自由现金流
|
||||||
|
(45, '1002', 'sum', 1.00, 'FCF-银行存款'),
|
||||||
|
(45, '2202', 'sum', 1.00, 'FCF-应付账款'),
|
||||||
|
-- F_OP_PROFIT_MARGIN 经营利润率 = (收入-成本-期间费用)/收入
|
||||||
|
(183, '6001', 'ratio', 1.00, '经营利润率-收入'),
|
||||||
|
(183, '6401', 'ratio', -1.00, '经营利润率-主营业务成本'),
|
||||||
|
(183, '6601', 'ratio', -1.00, '经营利润率-销售费用'),
|
||||||
|
(183, '6602', 'ratio', -1.00, '经营利润率-管理费用'),
|
||||||
|
(183, '6603', 'ratio', -1.00, '经营利润率-财务费用'),
|
||||||
|
-- F_ROE 净资产收益率 = 净利润/净资产
|
||||||
|
(44, '4103', 'ratio', 1.00, 'ROE分子-本年利润'),
|
||||||
|
(44, '4001', 'ratio', -1.00, 'ROE分母-实收资本'),
|
||||||
|
(44, '4002', 'ratio', -1.00, 'ROE分母-资本公积'),
|
||||||
|
-- P_COST_CUT 招待费砍半
|
||||||
|
(54, '6601', 'sum', 1.00, '招待费-销售费用(业务招待子目)'),
|
||||||
|
-- P_CHANNEL_NEG 渠补谈判完成率(以渠道收入为基数)
|
||||||
|
(48, '6001', 'ratio', 1.00, '渠补谈判-主营业务收入'),
|
||||||
|
(48, '6051', 'ratio', 1.00, '渠补谈判-其他业务收入'),
|
||||||
|
-- C_MARKET_SHARE 市场份额(以收入为口径)
|
||||||
|
(33, '6001', 'ratio', 1.00, '市场份额-主营业务收入');
|
||||||
|
-- objective_kpi + krs 种子数据 (2026-08-19)
|
||||||
|
USE cma;
|
||||||
|
|
||||||
|
-- ① KPI↔O支撑: 现有3个O各关联5个KPI
|
||||||
|
INSERT INTO objective_kpi (objective_id, kpi_id, weight) VALUES
|
||||||
|
-- O11 优化成本结构——渠补谈判+管理费压缩
|
||||||
|
(11, 5, 1.00), -- F_COST_RATIO 费用率
|
||||||
|
(11, 54, 1.00), -- P_COST_CUT 招待费砍半
|
||||||
|
(11, 183, 1.00), -- F_OP_PROFIT_MARGIN 经营利润率
|
||||||
|
(11, 3, 1.00), -- F_NET_PROFIT 净利润
|
||||||
|
(11, 48, 1.00), -- P_CHANNEL_NEG 渠补谈判完成率
|
||||||
|
-- O12 保障现金流安全——应收催收+厂补确认
|
||||||
|
(12, 4, 1.00), -- F_OP_CFLOW 经营性现金流
|
||||||
|
(12, 6, 1.00), -- F_AR_DAYS 应收账款周转天数
|
||||||
|
(12, 45, 1.00), -- F_FCF 自由现金流
|
||||||
|
(12, 408, 1.00), -- F_FACTORY_REBATE_RATE 上游厂补率
|
||||||
|
(12, 17, 1.00), -- F_CURRENT_RATIO 流动比率
|
||||||
|
-- O13 渠道关系改善——用数据谈判渠补
|
||||||
|
(13, 46, 1.00), -- C_REBATE_RATE 渠补率
|
||||||
|
(13, 48, 1.00), -- P_CHANNEL_NEG 渠补谈判完成率
|
||||||
|
(13, 407, 1.00), -- F_REBATE_RATE 返利率
|
||||||
|
(13, 7, 1.00), -- C_SATISFACTION 客户满意度
|
||||||
|
(13, 24, 1.00); -- C_RETENTION_RATE 客户保留率
|
||||||
|
|
||||||
|
-- ② krs: 现有3个O各配3个KR
|
||||||
|
INSERT INTO krs (objective_id, title, metric_kpi_id, target_value, current_value, progress, status, due_date) VALUES
|
||||||
|
-- O11 优化成本结构
|
||||||
|
(11, '完成渠补谈判,渠道B渠补率降至72%', 48, 100.00, 40.00, 40, 'in_progress', '2026-09-30'),
|
||||||
|
(11, '管理费用率压缩至15%以内', 5, 15.00, 18.00, 60, 'in_progress', '2026-09-30'),
|
||||||
|
(11, '经营利润率提升至8%', 183, 8.00, 5.20, 40, 'in_progress', '2026-09-30'),
|
||||||
|
-- O12 保障现金流安全
|
||||||
|
(12, '应收账款周转天数降至45天', 6, 45.00, 62.00, 30, 'in_progress', '2026-09-30'),
|
||||||
|
(12, '经营性现金流季度回正至100万元', 4, 100.00, 40.00, 35, 'in_progress', '2026-09-30'),
|
||||||
|
(12, '上游厂补率确认至90%', 408, 90.00, 60.00, 40, 'in_progress', '2026-09-30'),
|
||||||
|
-- O13 渠道关系改善
|
||||||
|
(13, '渠道渠补率降至70%以下', 46, 70.00, 78.00, 50, 'in_progress', '2026-09-30'),
|
||||||
|
(13, '完成10场数据化渠补谈判', 48, 10.00, 2.00, 20, 'in_progress', '2026-09-30'),
|
||||||
|
(13, '客户满意度提升至90分', 7, 90.00, 85.00, 33, 'in_progress', '2026-09-30');
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# CMA本体三支柱:科目↔KPI↔OKR 三层互联 — 交付报告
|
||||||
|
|
||||||
|
> 任务:yanxue-cma-ontology-trilogy.md(2026-08-19 21:00 并发任务)
|
||||||
|
> 执行:项目Bot(wecom-project) | 完成:2026-08-19 22:56
|
||||||
|
> 优先级:P1(本体层核心补全)
|
||||||
|
|
||||||
|
## 交付内容
|
||||||
|
|
||||||
|
### 1. 数据层(3张新表 + 种子数据)
|
||||||
|
| 表 | 作用 | 数据量 | 验收 |
|
||||||
|
|:--|:--|:--|:--|
|
||||||
|
| `kpi_subject_map` | 科目↔KPI映射(指标→底层账) | **89条映射 / 24个KPI** | ≥10 ✓ |
|
||||||
|
| `objective_kpi` | KPI↔O支撑(目标→指标) | **3个O × 5个KPI** | 各≥3 ✓ |
|
||||||
|
| `krs` | 关键结果KR(OKR完整化) | **3个O × 3个KR** | 各2-3 ✓ |
|
||||||
|
|
||||||
|
- 备份:`backups/ontology-trilogy-backup-20260819.sql`(subjects/kpi_definitions/objectives 全量,135KB)
|
||||||
|
- 可重放脚本:`backend/scripts/seed_ontology_trilogy.sql`(DDL + 种子,幂等可重跑)
|
||||||
|
|
||||||
|
### 2. API层(`backend/app/api/ontology.py`)
|
||||||
|
- `GET /api/cma/ontology/trace?objective_id=N` — **本体追溯链**:O(目标) → KPI(指标) → 科目(数据),含 KR 列表、KPI 支撑权重、科目 calc_type/weight
|
||||||
|
- `GET /api/cma/ontology/objectives` — 全部 OKR 三层链路概览(前端用)
|
||||||
|
|
||||||
|
### 3. 前端层(`OkrDetail.vue` + `api/index.ts`)
|
||||||
|
- OKR 详情新增「本体追溯链」页签:追溯链横幅 + KR 卡片(目标/当前值/进度)+ KPI→科目 标签流(calc_type/weight),非科目驱动指标明确标注"不追溯账本"
|
||||||
|
|
||||||
|
## 追溯链实测(O→KPI→科目)
|
||||||
|
|
||||||
|
```
|
||||||
|
O#11 优化成本结构 → 5 KPI → 24 科目
|
||||||
|
费用率 → [6601销售费用, 6602管理费用, 6603财务费用, 6001收入(分母)]
|
||||||
|
净利润 → [6001+6051+6011+6111收入 - 6401-6402成本 - 6601-6602-6603费用...12科目]
|
||||||
|
O#12 保障现金流 → 5 KPI → 16 科目
|
||||||
|
O#13 渠道关系改善 → 5 KPI → 7 科目
|
||||||
|
```
|
||||||
|
|
||||||
|
从任一 O 可逐层下钻到账本;反之从科目可上溯到目标。
|
||||||
|
|
||||||
|
## 验证记录(铁律七)
|
||||||
|
|
||||||
|
1. ✅ 备份完成(mysqldump 3表 135KB)
|
||||||
|
2. ✅ 每步 SQL 后 SELECT 验证(89/15/9 行确认)
|
||||||
|
3. ✅ 后端重启:`systemctl restart cma-backend`(systemd,非 restart.sh 的 pkill 模式)→ active + `/health` ok
|
||||||
|
4. ✅ 追溯链 API 3个O全部跑通(含 KR + 科目映射)
|
||||||
|
5. ✅ 前端 `npm run build` 13.89s 成功,已部署 `/var/www/cma/`
|
||||||
|
6. ✅ 回归门:`pytest tests/` → **409 passed, 1 xfailed, 0 failed**
|
||||||
|
- 注:首次全量跑有 13 个 test_reports 失败,二次全量 + 单文件隔离均通过 → 属既有测试排序/状态污染,与本改动无关
|
||||||
|
|
||||||
|
## 遗留说明
|
||||||
|
|
||||||
|
- 业务/调研类 KPI(客户满意度、保留率等)非科目驱动,trace 中 subjects 为空数组并标注,符合本体语义(不强行虚构账本映射)
|
||||||
|
- `models/__init__.py` 同时含并发任务"KPI多粒度"未提交列(target_monthly/quarterly/yearly,2026-08-17已上线),本提交一并带入(同文件无法拆分,功能均已完成)
|
||||||
|
- 并发任务"自动化测试补覆盖"的 conftest/tests 改动未包含在本提交
|
||||||
|
|
||||||
|
## Git
|
||||||
|
|
||||||
|
- 提交:本体三支柱(科目↔KPI↔OKR)三层互联 + 追溯链API + OKR详情页签
|
||||||
|
- 推送:SSH 2222 → git.sxbh.ltd
|
||||||
@@ -95,6 +95,12 @@ export const okrApi = {
|
|||||||
api.post(`/okr/${okrId}/decomposition/milestones/generate`, { kr_id: krId }),
|
api.post(`/okr/${okrId}/decomposition/milestones/generate`, { kr_id: krId }),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 本体三支柱追溯链 (科目↔KPI↔OKR) ──
|
||||||
|
export const ontologyApi = {
|
||||||
|
trace: (objectiveId: number) => api.get('/ontology/trace', { params: { objective_id: objectiveId } }),
|
||||||
|
objectives: (params?: any) => api.get('/ontology/objectives', { params }),
|
||||||
|
}
|
||||||
|
|
||||||
export const dataApi = {
|
export const dataApi = {
|
||||||
importExcel: (file: File, qs?: string) => {
|
importExcel: (file: File, qs?: string) => {
|
||||||
const form = new FormData()
|
const form = new FormData()
|
||||||
|
|||||||
@@ -66,6 +66,59 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="本体追溯链" name="ontology">
|
||||||
|
<div v-if="traceLoading" class="empty-tab">追溯链加载中...</div>
|
||||||
|
<div v-else-if="!trace" class="empty-tab">暂无追溯链数据</div>
|
||||||
|
<template v-else>
|
||||||
|
<div class="chain-banner">
|
||||||
|
<span class="chain-step">O 目标</span>
|
||||||
|
<span class="chain-arrow">→</span>
|
||||||
|
<span class="chain-step">KPI 指标 ({{ trace.kpis.length }})</span>
|
||||||
|
<span class="chain-arrow">→</span>
|
||||||
|
<span class="chain-step">科目 数据 ({{ trace.chain.subject_count }})</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- KR(关键结果) -->
|
||||||
|
<div class="trace-section" v-if="trace.krs && trace.krs.length">
|
||||||
|
<div class="trace-section-title">关键结果 KR</div>
|
||||||
|
<div class="kr-list">
|
||||||
|
<div v-for="kr in trace.krs" :key="kr.id" class="kr-card">
|
||||||
|
<div class="kr-header">
|
||||||
|
<span class="kr-title">{{ kr.title }}</span>
|
||||||
|
<el-tag size="small" :type="statusTagType(kr.status)">{{ statusLabel(kr.status) }}</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="kr-meta">
|
||||||
|
<span v-if="kr.metric_kpi_name" class="kr-meta-item">度量KPI: {{ kr.metric_kpi_name }}</span>
|
||||||
|
<span class="kr-meta-item">目标: {{ kr.target_value }}</span>
|
||||||
|
<span class="kr-meta-item">当前: {{ kr.current_value }}</span>
|
||||||
|
<span v-if="kr.due_date" class="kr-meta-item">截止: {{ kr.due_date }}</span>
|
||||||
|
</div>
|
||||||
|
<el-progress :percentage="kr.progress || 0" :stroke-width="6" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- KPI → 科目 -->
|
||||||
|
<div class="trace-section">
|
||||||
|
<div class="trace-section-title">KPI → 科目(指标追溯到底层账)</div>
|
||||||
|
<div v-for="k in trace.kpis" :key="k.kpi_id" class="kpi-trace-card">
|
||||||
|
<div class="kpi-trace-head">
|
||||||
|
<span class="kpi-trace-code">{{ k.kpi_code }}</span>
|
||||||
|
<span class="kpi-trace-name">{{ k.kpi_name }}</span>
|
||||||
|
<el-tag size="small" :type="dimTagType(k.dimension)">{{ dimLabel(k.dimension) }}</el-tag>
|
||||||
|
<span v-if="k.unit" class="kpi-trace-unit">{{ k.unit }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="k.subjects && k.subjects.length" class="subject-chips">
|
||||||
|
<el-tag v-for="s in k.subjects" :key="s.subject_code" size="small" effect="plain" class="subject-chip">
|
||||||
|
{{ s.subject_code }} {{ s.subject_name }}
|
||||||
|
<span class="subject-calc">({{ s.calc_type }}, {{ s.weight }})</span>
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<div v-else class="no-subject">非科目驱动指标(业务/调研数据源,不追溯账本)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-tab-pane>
|
||||||
<el-tab-pane label="分解视图" name="decomposition">
|
<el-tab-pane label="分解视图" name="decomposition">
|
||||||
<OkrDecomposition :okr-id="okrId" @refresh="loadData" />
|
<OkrDecomposition :okr-id="okrId" @refresh="loadData" />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
@@ -83,7 +136,7 @@ import { ref, computed, onMounted } from 'vue'
|
|||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { ArrowLeft } from '@element-plus/icons-vue'
|
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||||
import { okrApi } from '../api/index'
|
import { okrApi, ontologyApi } from '../api/index'
|
||||||
import OkrDecomposition from '../components/okr/OkrDecomposition.vue'
|
import OkrDecomposition from '../components/okr/OkrDecomposition.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -91,6 +144,8 @@ const okrId = computed(() => Number(route.params.id))
|
|||||||
|
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const objective = ref<any>(null)
|
const objective = ref<any>(null)
|
||||||
|
const trace = ref<any>(null)
|
||||||
|
const traceLoading = ref(false)
|
||||||
const activeTab = ref('krs')
|
const activeTab = ref('krs')
|
||||||
|
|
||||||
const DIM_LABELS: Record<string, string> = {
|
const DIM_LABELS: Record<string, string> = {
|
||||||
@@ -103,11 +158,11 @@ function dimTagType(d: string) {
|
|||||||
return types[d] || ''
|
return types[d] || ''
|
||||||
}
|
}
|
||||||
function statusLabel(s: string) {
|
function statusLabel(s: string) {
|
||||||
const labels: Record<string, string> = { active: '进行中', completed: '已完成', cancelled: '已取消' }
|
const labels: Record<string, string> = { active: '进行中', completed: '已完成', cancelled: '已取消', in_progress: '进行中', pending: '待开始' }
|
||||||
return labels[s] || s
|
return labels[s] || s
|
||||||
}
|
}
|
||||||
function statusTagType(s: string) {
|
function statusTagType(s: string) {
|
||||||
const types: Record<string, string> = { active: 'warning', completed: 'success', cancelled: 'danger' }
|
const types: Record<string, string> = { active: 'warning', completed: 'success', cancelled: 'danger', in_progress: 'warning', pending: 'info' }
|
||||||
return types[s] || ''
|
return types[s] || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,8 +180,23 @@ async function loadData() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadTrace() {
|
||||||
|
if (!okrId.value) return
|
||||||
|
traceLoading.value = true
|
||||||
|
try {
|
||||||
|
const res: any = await ontologyApi.trace(okrId.value)
|
||||||
|
trace.value = res
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.warning('追溯链加载失败: ' + (e?.response?.data?.detail || e?.message || ''))
|
||||||
|
trace.value = null
|
||||||
|
} finally {
|
||||||
|
traceLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadData()
|
loadData()
|
||||||
|
loadTrace()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -218,6 +288,84 @@ onMounted(() => {
|
|||||||
padding: 40px 0;
|
padding: 40px 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── 本体追溯链 ── */
|
||||||
|
.chain-banner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
background: linear-gradient(90deg, #f0f5ff, #f6ffed);
|
||||||
|
border: 1px solid #d9e8ff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.chain-step {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
.chain-arrow {
|
||||||
|
color: #409EFF;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.trace-section {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
.trace-section-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding-left: 8px;
|
||||||
|
border-left: 3px solid #409EFF;
|
||||||
|
}
|
||||||
|
.kpi-trace-card {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #EBEEF5;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.kpi-trace-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.kpi-trace-code {
|
||||||
|
font-family: 'JetBrains Mono', Consolas, monospace;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #409EFF;
|
||||||
|
background: #ecf5ff;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.kpi-trace-name {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.kpi-trace-unit {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
.subject-chips {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.subject-chip {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.subject-calc {
|
||||||
|
color: #909399;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
.no-subject {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #C0C4CC;
|
||||||
|
}
|
||||||
.not-found {
|
.not-found {
|
||||||
padding: 60px 0;
|
padding: 60px 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user