fix: 补注册缺失API模块+前端路由+ConnectionLines恢复
- 注册4个后端模块: customer_dashboard/deviation_push/budget_generate/knowledge_articles - 修复改善行动路由指向ActionPlanLibrary.vue(原指向AlertList.vue) - 新增3个路由: /reports(管理报表) /alignment(战略执行看板) /dupont-analysis - 恢复ConnectionLines.vue完整版(288行, 原被截断为123行) - 修复maps.py维度键 finance(原financial不匹配前端) - 补回models/__init__.py knowledge模型导入
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -14,7 +14,7 @@ router = APIRouter(prefix="/api/cma/maps", tags=["战略地图"],
|
|||||||
# ── 四维度模板 ──────────────────────────────
|
# ── 四维度模板 ──────────────────────────────
|
||||||
STRATEGIC_MAP_TEMPLATE = [
|
STRATEGIC_MAP_TEMPLATE = [
|
||||||
{
|
{
|
||||||
"key": "financial",
|
"key": "finance",
|
||||||
"name": "财务层",
|
"name": "财务层",
|
||||||
"icon": "💰",
|
"icon": "💰",
|
||||||
"color": "#F56C6C",
|
"color": "#F56C6C",
|
||||||
|
|||||||
+7
-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, templates, maps, dashboard, data, alerts, ai_analysis, alert_rules, users, thresholds, notifications, permissions, action_plans, alignment, org, objectives, versions, budget, cost, predict, reports, security
|
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
|
||||||
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
|
||||||
@@ -53,6 +53,12 @@ app.include_router(cost.router)
|
|||||||
app.include_router(predict.router)
|
app.include_router(predict.router)
|
||||||
app.include_router(reports.router)
|
app.include_router(reports.router)
|
||||||
app.include_router(security.router)
|
app.include_router(security.router)
|
||||||
|
app.include_router(knowledge.router)
|
||||||
|
app.include_router(bot_bridge.router)
|
||||||
|
app.include_router(customer_dashboard.router)
|
||||||
|
app.include_router(deviation_push.router)
|
||||||
|
app.include_router(budget_generate.router)
|
||||||
|
app.include_router(knowledge_articles.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):
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from app.database import Base
|
|||||||
|
|
||||||
from app.models.budget_plan import BudgetPlan
|
from app.models.budget_plan import BudgetPlan
|
||||||
from app.models.cost_model import StandardCost, ActualCost, AbcActivity, AbcAllocation
|
from app.models.cost_model import StandardCost, ActualCost, AbcActivity, AbcAllocation
|
||||||
|
from app.models.knowledge import KnowledgeEvent, KnowledgeSummary
|
||||||
|
|
||||||
|
|
||||||
class User(Base):
|
class User(Base):
|
||||||
|
|||||||
Binary file not shown.
@@ -1,123 +1,288 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg class="connection-svg" ref="svgRef">
|
<svg class="connection-svg" ref="svgRef">
|
||||||
<defs>
|
<defs>
|
||||||
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto">
|
<!-- 跨层箭头(↓方向) -->
|
||||||
<polygon points="0 0, 10 3.5, 0 7" fill="#409eff" />
|
<marker
|
||||||
|
id="arrow-down"
|
||||||
|
markerWidth="10"
|
||||||
|
markerHeight="7"
|
||||||
|
refX="5"
|
||||||
|
refY="7"
|
||||||
|
orient="auto"
|
||||||
|
>
|
||||||
|
<polygon points="0,0 10,3.5 0,7" fill="#909399" />
|
||||||
</marker>
|
</marker>
|
||||||
<marker id="arrowhead-warn" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto">
|
<!-- 同层箭头(→方向) -->
|
||||||
<polygon points="0 0, 10 3.5, 0 7" fill="#e6a23c" />
|
<marker
|
||||||
|
id="arrow-right"
|
||||||
|
markerWidth="10"
|
||||||
|
markerHeight="7"
|
||||||
|
refX="10"
|
||||||
|
refY="3.5"
|
||||||
|
orient="auto"
|
||||||
|
>
|
||||||
|
<polygon points="0,0 10,3.5 0,7" fill="#409eff" />
|
||||||
|
</marker>
|
||||||
|
<!-- 高亮箭头 -->
|
||||||
|
<marker
|
||||||
|
id="arrow-active"
|
||||||
|
markerWidth="10"
|
||||||
|
markerHeight="7"
|
||||||
|
refX="10"
|
||||||
|
refY="3.5"
|
||||||
|
orient="auto"
|
||||||
|
>
|
||||||
|
<polygon points="0,0 10,3.5 0,7" fill="#f56c6c" />
|
||||||
</marker>
|
</marker>
|
||||||
</defs>
|
</defs>
|
||||||
|
|
||||||
<!-- 固定跨层箭头:从下层泳道到上层泳道(因果连线) -->
|
<!-- 跨层固定箭头:每层底部 → 下层顶部 -->
|
||||||
<path
|
|
||||||
v-for="(arrow, idx) in fixedLayerArrows"
|
|
||||||
:key="'fixed-' + idx"
|
|
||||||
:d="arrow.path"
|
|
||||||
fill="none"
|
|
||||||
stroke="#c0c4cc"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke-dasharray="6,4"
|
|
||||||
marker-end="url(#arrowhead)"
|
|
||||||
class="fixed-layer-arrow"
|
|
||||||
>
|
|
||||||
<title>{{ arrow.from }} → {{ arrow.to }}</title>
|
|
||||||
</path>
|
|
||||||
|
|
||||||
<!-- 用户自定义连线 -->
|
|
||||||
<line
|
<line
|
||||||
v-for="(line, idx) in connectionLines"
|
v-for="(line, idx) in crossLayerLines"
|
||||||
:key="'conn-' + idx"
|
:key="'cross-' + idx"
|
||||||
:x1="line.x1" :y1="line.y1" :x2="line.x2" :y2="line.y2"
|
:x1="line.x1"
|
||||||
:class="['conn-line', { 'conn-selected': selectedConnIdx === idx }]"
|
:y1="line.y1"
|
||||||
:style="{ '--from-color': line.fromColor, '--to-color': line.toColor }"
|
:x2="line.x2"
|
||||||
marker-end="url(#arrowhead)"
|
:y2="line.y2"
|
||||||
:stroke="selectedConnIdx === idx ? '#f56c6c' : '#409eff'"
|
stroke="#909399"
|
||||||
:stroke-width="selectedConnIdx === idx ? 3 : 2"
|
stroke-width="2"
|
||||||
@click.stop="$emit('select-connection', idx)"
|
stroke-dasharray="6,3"
|
||||||
@mouseenter="hoverConnIdx = idx"
|
marker-end="url(#arrow-down)"
|
||||||
@mouseleave="hoverConnIdx = null"
|
class="cross-layer-line"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 连线提示浮层 -->
|
<!-- 同层用户手动连线箭头(→方向) -->
|
||||||
<g v-if="hoverConnIdx !== null && connectionLines[hoverConnIdx] && hoverConnIdx !== selectedConnIdx">
|
<g v-for="(conn, idx) in layerConnections" :key="'conn-' + idx">
|
||||||
|
<path
|
||||||
|
:d="conn.path"
|
||||||
|
:class="[
|
||||||
|
'conn-line',
|
||||||
|
{ 'conn-selected': selectedIdx === idx },
|
||||||
|
]"
|
||||||
|
:stroke="selectedIdx === idx ? '#f56c6c' : '#409eff'"
|
||||||
|
:stroke-width="selectedIdx === idx ? 3 : 2"
|
||||||
|
fill="none"
|
||||||
|
marker-end="url(#arrow-right)"
|
||||||
|
@click.stop="$emit('select-connection', idx)"
|
||||||
|
@mouseenter="hoverIdx = idx"
|
||||||
|
@mouseleave="hoverIdx = null"
|
||||||
|
/>
|
||||||
|
<!-- hover提示 -->
|
||||||
<rect
|
<rect
|
||||||
:x="connectionLines[hoverConnIdx].mx - 60"
|
v-if="hoverIdx === idx"
|
||||||
:y="connectionLines[hoverConnIdx].my - 36"
|
:x="conn.mx - 50"
|
||||||
width="120" height="22" rx="4" fill="rgba(0,0,0,0.65)"
|
:y="conn.my - 10"
|
||||||
|
width="100"
|
||||||
|
height="20"
|
||||||
|
rx="4"
|
||||||
|
fill="rgba(0,0,0,0.65)"
|
||||||
|
class="conn-hover-bg"
|
||||||
/>
|
/>
|
||||||
<text
|
<text
|
||||||
:x="connectionLines[hoverConnIdx].mx"
|
v-if="hoverIdx === idx"
|
||||||
:y="connectionLines[hoverConnIdx].my - 21"
|
:x="conn.mx"
|
||||||
text-anchor="middle" fill="#fff" font-size="11"
|
:y="conn.my + 4"
|
||||||
>点击选中连线</text>
|
text-anchor="middle"
|
||||||
</g>
|
fill="#fff"
|
||||||
|
font-size="11"
|
||||||
<!-- 删除按钮(选中连线时) -->
|
class="conn-hover-text"
|
||||||
<g v-if="selectedConnIdx !== null && connectionLines[selectedConnIdx]">
|
>
|
||||||
<rect
|
点击选中
|
||||||
:x="connectionLines[selectedConnIdx].mx - 18"
|
</text>
|
||||||
:y="connectionLines[selectedConnIdx].my - 24"
|
<!-- 删除按钮(选中时) -->
|
||||||
width="88" height="24" rx="12" fill="#f56c6c" class="del-btn-bg"
|
<g v-if="selectedIdx === idx">
|
||||||
@click.stop="$emit('delete-connection', selectedConnIdx)"
|
<rect
|
||||||
/>
|
:x="conn.mx - 20"
|
||||||
<text
|
:y="conn.my - 28"
|
||||||
:x="connectionLines[selectedConnIdx].mx"
|
width="88"
|
||||||
:y="connectionLines[selectedConnIdx].my - 8"
|
height="24"
|
||||||
text-anchor="middle" fill="#fff" font-size="13" font-weight="bold"
|
rx="12"
|
||||||
class="del-btn-text"
|
fill="#f56c6c"
|
||||||
@click.stop="$emit('delete-connection', selectedConnIdx)"
|
class="del-btn-bg"
|
||||||
>✕ 删除连线</text>
|
@click.stop="$emit('delete-connection', idx)"
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
:x="conn.mx + 24"
|
||||||
|
:y="conn.my - 12"
|
||||||
|
text-anchor="middle"
|
||||||
|
fill="#fff"
|
||||||
|
font-size="12"
|
||||||
|
font-weight="bold"
|
||||||
|
class="del-btn-text"
|
||||||
|
@click.stop="$emit('delete-connection', idx)"
|
||||||
|
>
|
||||||
|
✕ 删除连线
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
<!-- 绘制中的临时线 -->
|
<!-- 绘制中的临时线 -->
|
||||||
<line v-if="tempLine"
|
<line
|
||||||
:x1="tempLine.x1" :y1="tempLine.y1"
|
v-if="tempLine"
|
||||||
:x2="tempLine.x2" :y2="tempLine.y2"
|
:x1="tempLine.x1"
|
||||||
stroke="#e6a23c" stroke-width="2" stroke-dasharray="6,3"
|
:y1="tempLine.y1"
|
||||||
marker-end="url(#arrowhead-warn)"
|
:x2="tempLine.x2"
|
||||||
|
:y2="tempLine.y2"
|
||||||
|
stroke="#e6a23c"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-dasharray="6,3"
|
||||||
|
marker-end="url(#arrow-down)"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup>
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps({
|
||||||
connectionLines: any[]
|
/** 各层DOM元素引用,格式:{ financial: HTMLElement, customer: HTMLElement, ... } */
|
||||||
selectedConnIdx: number | null
|
layerRefs: { type: Object, required: true },
|
||||||
tempLine: any
|
/** 节点DOM元素引用,格式:{ 'financial-0': HTMLElement, ... } */
|
||||||
fixedLayerArrows: { from: string; to: string; path: string }[]
|
nodeRefs: { type: Object, default: () => ({}) },
|
||||||
}>()
|
/** 层顺序列表 */
|
||||||
|
layerKeys: { type: Array, default: () => ['financial', 'customer', 'process', 'learning'] },
|
||||||
|
/** 同层连线数据 [{from: 'financial-0', to: 'financial-1'}] */
|
||||||
|
connections: { type: Array, default: () => [] },
|
||||||
|
/** 临时连线 */
|
||||||
|
tempLine: { type: Object, default: null },
|
||||||
|
/** 容器尺寸变化触发重算 */
|
||||||
|
containerKey: { type: Number, default: 0 },
|
||||||
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits(['select-connection', 'delete-connection'])
|
||||||
(e: 'select-connection', idx: number): void
|
|
||||||
(e: 'delete-connection', idx: number): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const svgRef = ref<SVGSVGElement | null>(null)
|
const svgRef = ref(null)
|
||||||
const hoverConnIdx = ref<number | null>(null)
|
const hoverIdx = ref(null)
|
||||||
|
const selectedIdx = ref(null)
|
||||||
|
const layerConnections = ref([])
|
||||||
|
const crossLayerLines = ref([])
|
||||||
|
|
||||||
defineExpose({ svgRef })
|
/**
|
||||||
|
* 计算跨层固定箭头
|
||||||
|
* 从上层底部中心 → 下层顶部中心
|
||||||
|
*/
|
||||||
|
function calcCrossLayerLines() {
|
||||||
|
const lines = []
|
||||||
|
const keys = props.layerKeys
|
||||||
|
for (let i = 0; i < keys.length - 1; i++) {
|
||||||
|
const fromEl = props.layerRefs[keys[i]]
|
||||||
|
const toEl = props.layerRefs[keys[i + 1]]
|
||||||
|
if (!fromEl || !toEl) continue
|
||||||
|
|
||||||
|
const fr = fromEl.getBoundingClientRect()
|
||||||
|
const tr = toEl.getBoundingClientRect()
|
||||||
|
const svg = svgRef.value
|
||||||
|
if (!svg) continue
|
||||||
|
const svgRect = svg.getBoundingClientRect()
|
||||||
|
|
||||||
|
lines.push({
|
||||||
|
x1: fr.left + fr.width / 2 - svgRect.left,
|
||||||
|
y1: fr.bottom - svgRect.top,
|
||||||
|
x2: tr.left + tr.width / 2 - svgRect.left,
|
||||||
|
y2: tr.top - svgRect.top,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
crossLayerLines.value = lines
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算同层用户连线
|
||||||
|
* 使用贝塞尔曲线从源节点右侧 → 目标节点左侧
|
||||||
|
*/
|
||||||
|
function calcLayerConnections() {
|
||||||
|
const result = []
|
||||||
|
const svg = svgRef.value
|
||||||
|
if (!svg) return
|
||||||
|
const svgRect = svg.getBoundingClientRect()
|
||||||
|
|
||||||
|
for (const conn of props.connections) {
|
||||||
|
const fromEl = props.nodeRefs[conn.from]
|
||||||
|
const toEl = props.nodeRefs[conn.to]
|
||||||
|
if (!fromEl || !toEl) continue
|
||||||
|
|
||||||
|
const fr = fromEl.getBoundingClientRect()
|
||||||
|
const tr = toEl.getBoundingClientRect()
|
||||||
|
|
||||||
|
// 源节点右侧中点 → 目标节点左侧中点
|
||||||
|
const x1 = fr.right - svgRect.left
|
||||||
|
const y1 = fr.top + fr.height / 2 - svgRect.top
|
||||||
|
const x2 = tr.left - svgRect.left
|
||||||
|
const y2 = tr.top + tr.height / 2 - svgRect.top
|
||||||
|
|
||||||
|
// 贝塞尔曲线控制点
|
||||||
|
const dx = Math.abs(x2 - x1) * 0.5
|
||||||
|
const cp1x = x1 + dx
|
||||||
|
const cp1y = y1
|
||||||
|
const cp2x = x2 - dx
|
||||||
|
const cp2y = y2
|
||||||
|
|
||||||
|
const path = `M ${x1} ${y1} C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${x2} ${y2}`
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
path,
|
||||||
|
mx: (x1 + x2) / 2,
|
||||||
|
my: (y1 + y2) / 2,
|
||||||
|
from: conn.from,
|
||||||
|
to: conn.to,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
layerConnections.value = result
|
||||||
|
}
|
||||||
|
|
||||||
|
function recalcAll() {
|
||||||
|
calcCrossLayerLines()
|
||||||
|
calcLayerConnections()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暴露给父组件调用
|
||||||
|
defineExpose({ recalcAll })
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
nextTick(recalcAll)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.connection-svg {
|
.connection-svg {
|
||||||
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
position: absolute;
|
||||||
z-index: 5; pointer-events: none;
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 5;
|
||||||
|
pointer-events: none;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
.cross-layer-line {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
.conn-line {
|
.conn-line {
|
||||||
cursor: pointer; transition: stroke .15s, stroke-width .15s;
|
cursor: pointer;
|
||||||
|
transition: stroke 0.15s, stroke-width 0.15s;
|
||||||
pointer-events: stroke;
|
pointer-events: stroke;
|
||||||
}
|
}
|
||||||
.conn-line:hover { stroke: #e6a23c !important; stroke-width: 4 !important; cursor: pointer; }
|
.conn-line:hover {
|
||||||
.conn-selected { stroke: #f56c6c !important; stroke-width: 3; }
|
stroke: #e6a23c !important;
|
||||||
.del-btn-bg { cursor: pointer; pointer-events: all; }
|
stroke-width: 4 !important;
|
||||||
.del-btn-bg:hover { fill: #e74c3c !important; }
|
}
|
||||||
.del-btn-text { cursor: pointer; pointer-events: all; user-select: none; }
|
.conn-selected {
|
||||||
.fixed-layer-arrow {
|
stroke: #f56c6c !important;
|
||||||
|
stroke-width: 3;
|
||||||
|
}
|
||||||
|
.del-btn-bg {
|
||||||
|
cursor: pointer;
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
.del-btn-bg:hover {
|
||||||
|
fill: #e74c3c !important;
|
||||||
|
}
|
||||||
|
.del-btn-text {
|
||||||
|
cursor: pointer;
|
||||||
|
pointer-events: all;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.conn-hover-bg,
|
||||||
|
.conn-hover-text {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0.5;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
|
|
||||||
// 各角色可访问的路由列表
|
// 各角色可访问的路由列表
|
||||||
export const ROLE_ROUTES: Record<string, string[]> = {
|
export const ROLE_ROUTES: Record<string, string[]> = {
|
||||||
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'],
|
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'],
|
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'],
|
||||||
business: ['/my-dashboard', '/dashboard', '/kpis', '/alerts', '/deviations', '/budget', '/action-plans', '/knowledge', '/guide', '/customer'],
|
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'],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,6 +47,11 @@ export const MENU_ITEMS: MenuItem[] = [
|
|||||||
{ path: '/predict', label: '预测模拟', icon: 'DataLine', roles: ['ceo', 'finance', 'it'] },
|
{ path: '/predict', label: '预测模拟', icon: 'DataLine', roles: ['ceo', 'finance', 'it'] },
|
||||||
{ path: '/budget', label: '预算管理', icon: 'Coin', roles: ['ceo', 'finance', 'business', 'it'] },
|
{ path: '/budget', label: '预算管理', icon: 'Coin', roles: ['ceo', 'finance', 'business', 'it'] },
|
||||||
|
|
||||||
|
// ── 高级分析 ──
|
||||||
|
{ path: '/reports', label: '管理报表', icon: 'DataAnalysis', roles: ['ceo', 'finance', 'business'], group: '分析决策' },
|
||||||
|
{ path: '/alignment', label: '战略执行看板', icon: 'Opportunity', roles: ['ceo', 'finance', 'business', 'it'] },
|
||||||
|
{ path: '/dupont-analysis', label: '杜邦分析', icon: 'TrendCharts', roles: ['ceo', 'finance', 'it'] },
|
||||||
|
|
||||||
// ── 行动管理 ──
|
// ── 行动管理 ──
|
||||||
{ path: '/alerts', label: '预警中心', icon: 'WarningFilled', roles: ['ceo', 'finance', 'business', 'it'], group: '行动管理' },
|
{ path: '/alerts', label: '预警中心', icon: 'WarningFilled', roles: ['ceo', 'finance', 'business', 'it'], group: '行动管理' },
|
||||||
{ path: '/action-plans', label: '改善行动', icon: 'Edit', roles: ['ceo', 'finance', 'business', 'it'] },
|
{ path: '/action-plans', label: '改善行动', icon: 'Edit', roles: ['ceo', 'finance', 'business', 'it'] },
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ const routes = [
|
|||||||
{ path: 'deviations', name: 'DeviationDashboard', component: () => import('@/views/DeviationDashboard.vue'), meta: { title: '差异分析', roles: ['ceo', 'finance', 'business', 'it'] } },
|
{ path: 'deviations', name: 'DeviationDashboard', component: () => import('@/views/DeviationDashboard.vue'), meta: { title: '差异分析', roles: ['ceo', 'finance', 'business', 'it'] } },
|
||||||
{ path: 'cost', name: 'CostDashboard', component: () => import('@/views/CostDashboard.vue'), meta: { title: '成本分析', roles: ['ceo', 'finance', 'it'] } },
|
{ path: 'cost', name: 'CostDashboard', component: () => import('@/views/CostDashboard.vue'), meta: { title: '成本分析', roles: ['ceo', 'finance', 'it'] } },
|
||||||
{ path: 'predict', name: 'PredictDashboard', component: () => import('@/views/PredictDashboard.vue'), meta: { title: '预测模拟', roles: ['ceo', 'finance', 'it'] } },
|
{ path: 'predict', name: 'PredictDashboard', component: () => import('@/views/PredictDashboard.vue'), meta: { title: '预测模拟', roles: ['ceo', 'finance', 'it'] } },
|
||||||
{ path: 'action-plans', name: 'ActionPlans', component: () => import('@/views/AlertList.vue'), meta: { title: '改善行动', roles: ['ceo', 'finance', 'business', 'it'], tab: 'plans' } },
|
{ path: 'action-plans', name: 'ActionPlans', component: () => import('@/views/ActionPlanLibrary.vue'), meta: { title: '改善行动', roles: ['ceo', 'finance', 'business', 'it'] } },
|
||||||
|
{ path: 'reports', name: 'ReportCenter', component: () => import('@/views/ReportCenter.vue'), meta: { title: '管理报表', roles: ['ceo', 'finance', 'business'] } },
|
||||||
|
{ path: 'alignment', name: 'KPIAlignment', component: () => import('@/views/KPIAlignment.vue'), meta: { title: '战略执行看板', roles: ['ceo', 'finance', 'business', 'it'] } },
|
||||||
|
{ path: 'dupont-analysis', name: 'DupontAnalysis', component: () => import('@/views/DupontAnalysis.vue'), meta: { title: '杜邦分析', roles: ['ceo', 'finance', 'it'] } },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user