KPI通用化: bsc_layer_config表+API+企业选择器+企业CRUD

This commit is contained in:
Hermes CI Fix
2026-07-19 10:07:46 +08:00
parent 59f4ab2c76
commit c57f377e04
7 changed files with 194 additions and 10 deletions
+7 -6
View File
@@ -1,11 +1,12 @@
node_modules app.db
dist dist
*.local dist/
.env
.DS_Store .DS_Store
*.tsbuildinfo .env
*.local
node_modules
node_modules/
*.pyc *.pyc
__pycache__/ __pycache__/
*.tsbuildinfo
venv/ venv/
node_modules/
dist/
+41
View File
@@ -0,0 +1,41 @@
"""BSC四层配置 API"""
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from typing import List
from app.database import get_db
from app.auth_middleware import require_auth
from app.models import BscLayerConfig, Entity
router = APIRouter(prefix="/api/cma/bsc-layers", tags=["BSC层配置"],
dependencies=[Depends(require_auth)],
)
@router.get("")
def list_bsc_layers(entity_id: int = Query(1, description="企业ID"), db: Session = Depends(get_db)):
"""获取某企业的BSC四层权重配置"""
# 验证企业存在
entity = db.query(Entity).filter(Entity.id == entity_id).first()
if not entity:
from fastapi.responses import JSONResponse
return JSONResponse(status_code=404, content={"detail": "企业不存在"})
layers = db.query(BscLayerConfig).filter(
BscLayerConfig.entity_id == entity_id
).order_by(BscLayerConfig.id).all()
return {
"entity_id": entity_id,
"entity_name": entity.short_name or entity.name,
"layers": [
{
"id": l.id,
"layer": l.layer,
"weight": float(l.weight),
"kpi_count_min": l.kpi_count_min,
"kpi_count_max": l.kpi_count_max,
}
for l in layers
]
}
+65 -1
View File
@@ -1,7 +1,8 @@
"""企业实体 API""" """企业实体 API"""
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from typing import List from typing import List, Optional
from pydantic import BaseModel
from app.database import get_db from app.database import get_db
from app.auth_middleware import require_auth from app.auth_middleware import require_auth
@@ -12,6 +13,20 @@ router = APIRouter(prefix="/api/cma/entities", tags=["企业实体"],
) )
class EntityCreate(BaseModel):
name: str
short_name: Optional[str] = None
industry: Optional[str] = None
status: Optional[str] = "active"
class EntityUpdate(BaseModel):
name: Optional[str] = None
short_name: Optional[str] = None
industry: Optional[str] = None
status: Optional[str] = None
@router.get("") @router.get("")
def list_entities(db: Session = Depends(get_db)): def list_entities(db: Session = Depends(get_db)):
"""获取企业列表""" """获取企业列表"""
@@ -27,3 +42,52 @@ def list_entities(db: Session = Depends(get_db)):
for e in entities for e in entities
] ]
} }
@router.post("")
def create_entity(data: EntityCreate, db: Session = Depends(get_db)):
"""创建企业"""
entity = Entity(
name=data.name,
short_name=data.short_name,
industry=data.industry,
status=data.status or "active",
)
db.add(entity)
db.commit()
db.refresh(entity)
return {
"id": entity.id,
"name": entity.name,
"short_name": entity.short_name,
"industry": entity.industry,
"message": "企业创建成功",
}
@router.put("/{entity_id}")
def update_entity(entity_id: int, data: EntityUpdate, db: Session = Depends(get_db)):
"""更新企业信息"""
entity = db.query(Entity).filter(Entity.id == entity_id).first()
if not entity:
from fastapi.responses import JSONResponse
return JSONResponse(status_code=404, content={"detail": "企业不存在"})
if data.name is not None:
entity.name = data.name
if data.short_name is not None:
entity.short_name = data.short_name
if data.industry is not None:
entity.industry = data.industry
if data.status is not None:
entity.status = data.status
db.commit()
db.refresh(entity)
return {
"id": entity.id,
"name": entity.name,
"short_name": entity.short_name,
"industry": entity.industry,
"message": "企业更新成功",
}
+2 -1
View File
@@ -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, 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, customer_dashboard, deviation_push, budget_generate, knowledge_articles, kpi_causality, data_quality, bi_reports, entities from app.api import auth, kpis, 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, customer_dashboard, deviation_push, budget_generate, knowledge_articles, kpi_causality, data_quality, bi_reports, entities, bsc_layers
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
@@ -63,6 +63,7 @@ app.include_router(kpi_causality.router)
app.include_router(data_quality.router) app.include_router(data_quality.router)
app.include_router(bi_reports.router) app.include_router(bi_reports.router)
app.include_router(entities.router) app.include_router(entities.router)
app.include_router(bsc_layers.router)
@app.exception_handler(Exception) @app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception): async def global_exception_handler(request: Request, exc: Exception):
+11
View File
@@ -282,6 +282,17 @@ class BiReport(Base):
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
class BscLayerConfig(Base):
"""BSC四层配置 — 不同企业的权重配置"""
__tablename__ = "bsc_layer_config"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, ForeignKey("entities.id"), nullable=False, comment="企业ID")
layer = Column(String(20), nullable=False, comment="financial/customer/process/learning")
weight = Column(Float, nullable=False, comment="该层权重(%")
kpi_count_min = Column(Integer, default=2, comment="最少KPI数")
kpi_count_max = Column(Integer, default=5, comment="最多KPI数")
# 兼容性: P2开发新增的模板API需要的模型 # 兼容性: P2开发新增的模板API需要的模型
# KPIDefinition 已存在,KPITemplate映射到同一定义 # KPIDefinition 已存在,KPITemplate映射到同一定义
KPITemplate = KPIDefinition KPITemplate = KPIDefinition
+6
View File
@@ -220,6 +220,12 @@ export const biReportApi = {
exportReport: (data: any) => api.post('/bi-reports/export', data), exportReport: (data: any) => api.post('/bi-reports/export', data),
} }
export const entityApi = {
list: () => api.get('/entities'),
create: (data: any) => api.post('/entities', data),
update: (id: number, data: any) => api.put(`/entities/${id}`, data),
}
export const ethicsQuizApi = { export const ethicsQuizApi = {
getQuestions: () => api.get('/knowledge/ethics-quiz'), getQuestions: () => api.get('/knowledge/ethics-quiz'),
} }
+62 -2
View File
@@ -35,6 +35,20 @@
<!-- 右侧搜索+表格 --> <!-- 右侧搜索+表格 -->
<div class="right-content"> <div class="right-content">
<!-- 企业选择器 -->
<div class="entity-selector">
<span class="entity-label">🌐 企业</span>
<el-select v-model="currentEntityId" @change="onEntityChange" style="width:200px">
<el-option
v-for="e in entities"
:key="e.id"
:label="e.short_name + ' / ' + e.name"
:value="e.id"
/>
</el-select>
<span class="entity-name-hint">{{ currentEntityName }}</span>
</div>
<!-- 搜索栏 --> <!-- 搜索栏 -->
<div class="search-bar"> <div class="search-bar">
<el-input v-model="searchKeyword" placeholder="搜索KPI名称/编码" clearable style="width:220px" @clear="loadKpis" @keyup.enter="loadKpis" /> <el-input v-model="searchKeyword" placeholder="搜索KPI名称/编码" clearable style="width:220px" @clear="loadKpis" @keyup.enter="loadKpis" />
@@ -337,7 +351,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue' import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { kpiApi, templateApi, dashboardApi } from '../api/index' import { kpiApi, templateApi, dashboardApi, entityApi } from '../api/index'
import MyDialog from '../components/MyDialog.vue' import MyDialog from '../components/MyDialog.vue'
// ── 数据 ── // ── 数据 ──
@@ -352,6 +366,11 @@ const searchKeyword = ref('')
const searchDimension = ref('') const searchDimension = ref('')
const searchCategory = ref('') const searchCategory = ref('')
// ── 企业选择 ──
const entities = ref<any[]>([])
const currentEntityId = ref(1)
const currentEntityName = ref('酣客')
// ── 分类树 ── // ── 分类树 ──
const categoryTree = ref<any[]>([]) const categoryTree = ref<any[]>([])
const defaultExpanded = ref<string[]>([]) const defaultExpanded = ref<string[]>([])
@@ -457,7 +476,7 @@ function onTreeCheck(data: any, { checkedKeys }: any) {
async function loadKpis() { async function loadKpis() {
loading.value = true loading.value = true
try { try {
const params: any = { page: page.value, page_size: pageSize } const params: any = { page: page.value, page_size: pageSize, entity_id: currentEntityId.value }
if (searchKeyword.value) params.keyword = searchKeyword.value if (searchKeyword.value) params.keyword = searchKeyword.value
if (searchDimension.value) params.dimension = searchDimension.value if (searchDimension.value) params.dimension = searchDimension.value
if (searchCategory.value) params.category = searchCategory.value if (searchCategory.value) params.category = searchCategory.value
@@ -622,7 +641,29 @@ async function doInstantiate() {
} }
} }
// ── 企业选择 ──
async function loadEntities() {
try {
const r: any = await entityApi.list()
entities.value = r.data || []
// Find current entity name
const cur = entities.value.find((e: any) => e.id === currentEntityId.value)
if (cur) currentEntityName.value = cur.short_name || cur.name
} catch (e) {
console.error('加载企业列表失败', e)
}
}
function onEntityChange(val: number) {
const cur = entities.value.find((e: any) => e.id === val)
currentEntityName.value = cur ? (cur.short_name || cur.name) : ''
page.value = 1
loadKpis()
loadCategories()
}
onMounted(() => { onMounted(() => {
loadEntities()
loadCategories() loadCategories()
loadKpis() loadKpis()
}) })
@@ -693,6 +734,25 @@ onMounted(() => {
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
} }
.entity-selector {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
padding: 8px 12px;
background: #f0f9ff;
border-radius: 4px;
border: 1px solid #d0e8ff;
}
.entity-label {
font-size: 14px;
font-weight: 500;
color: #303133;
}
.entity-name-hint {
font-size: 13px;
color: #909399;
}
.search-bar { .search-bar {
display: flex; display: flex;
gap: 8px; gap: 8px;