fix: 恢复部署内容丢失 — 补全api/index.ts缺失端点、修复import路径、重建后端KPITemplate兼容别名

This commit is contained in:
Hermes CI Fix
2026-07-12 18:00:23 +08:00
parent d247804c28
commit 9dfc3b9a6f
2 changed files with 115 additions and 267 deletions
+28
View File
@@ -125,4 +125,32 @@ export const predictApi = {
scenario: (data: any) => api.post('/predict/scenario', data), scenario: (data: any) => api.post('/predict/scenario', data),
} }
export const deviationPushApi = {
pushToMap: (data: any) => api.post('/deviation-push/push-to-map', data),
getMapNodes: (mapId: number) => api.get(`/deviation-push/map-nodes/${mapId}`),
}
export const budgetGenerateApi = {
getCandidates: () => api.get('/budget/kpi-budget-candidates'),
generateFromKpis: (data: any) => api.post('/budget/generate-from-kpis', data),
}
export const knowledgeArticleApi = {
list: (params?: any) => api.get('/knowledge-articles', { params }),
get: (id: number) => api.get(`/knowledge-articles/${id}`),
}
export const roleApi = {
list: () => api.get('/roles'),
create: (data: any) => api.post('/roles', data),
update: (id: number, data: any) => api.put(`/roles/${id}`, data),
delete: (id: number) => api.delete(`/roles/${id}`),
}
export const permissionApi = {
getMenuTree: () => api.get('/permissions/menu-tree'),
getUserRoles: () => api.get('/user-roles'),
setUserRoles: (data: any) => api.post('/user-roles', data),
}
export default api export default api
@@ -1,303 +1,123 @@
<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">
<marker <polygon points="0 0, 10 3.5, 0 7" fill="#409eff" />
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">
<marker <polygon points="0 0, 10 3.5, 0 7" fill="#e6a23c" />
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 crossLayerLines" v-for="(line, idx) in connectionLines"
:key="'cross-' + idx" :key="'conn-' + idx"
:x1="line.x1" :x1="line.x1" :y1="line.y1" :x2="line.x2" :y2="line.y2"
:y1="line.y1" :class="['conn-line', { 'conn-selected': selectedConnIdx === idx }]"
:x2="line.x2" :style="{ '--from-color': line.fromColor, '--to-color': line.toColor }"
:y2="line.y2" marker-end="url(#arrowhead)"
stroke="#909399" :stroke="selectedConnIdx === idx ? '#f56c6c' : '#409eff'"
stroke-width="2" :stroke-width="selectedConnIdx === idx ? 3 : 2"
stroke-dasharray="6,3" @click.stop="$emit('select-connection', idx)"
marker-end="url(#arrow-down)" @mouseenter="hoverConnIdx = idx"
class="cross-layer-line" @mouseleave="hoverConnIdx = null"
/> />
<!-- 同层用户手动连线箭头方向 --> <!-- 连线提示浮层 -->
<g v-for="(conn, idx) in layerConnections" :key="'conn-' + idx"> <g v-if="hoverConnIdx !== null && connectionLines[hoverConnIdx] && hoverConnIdx !== selectedConnIdx">
<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
v-if="hoverIdx === idx" :x="connectionLines[hoverConnIdx].mx - 60"
:x="conn.mx - 50" :y="connectionLines[hoverConnIdx].my - 36"
:y="conn.my - 10" width="120" height="22" rx="4" fill="rgba(0,0,0,0.65)"
width="100"
height="20"
rx="4"
fill="rgba(0,0,0,0.65)"
class="conn-hover-bg"
/> />
<text <text
v-if="hoverIdx === idx" :x="connectionLines[hoverConnIdx].mx"
:x="conn.mx" :y="connectionLines[hoverConnIdx].my - 21"
:y="conn.my + 4" text-anchor="middle" fill="#fff" font-size="11"
text-anchor="middle" >点击选中连线</text>
fill="#fff" </g>
font-size="11"
class="conn-hover-text" <!-- 删除按钮选中连线时 -->
> <g v-if="selectedConnIdx !== null && connectionLines[selectedConnIdx]">
点击选中 <rect
</text> :x="connectionLines[selectedConnIdx].mx - 18"
<!-- 删除按钮选中时 --> :y="connectionLines[selectedConnIdx].my - 24"
<g v-if="selectedIdx === idx"> width="88" height="24" rx="12" fill="#f56c6c" class="del-btn-bg"
<rect @click.stop="$emit('delete-connection', selectedConnIdx)"
:x="conn.mx - 20" />
:y="conn.my - 28" <text
width="88" :x="connectionLines[selectedConnIdx].mx"
height="24" :y="connectionLines[selectedConnIdx].my - 8"
rx="12" text-anchor="middle" fill="#fff" font-size="13" font-weight="bold"
fill="#f56c6c" class="del-btn-text"
class="del-btn-bg" @click.stop="$emit('delete-connection', selectedConnIdx)"
@click.stop="$emit('delete-connection', idx)" > 删除连线</text>
/>
<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 <line v-if="tempLine"
v-if="tempLine" :x1="tempLine.x1" :y1="tempLine.y1"
:x1="tempLine.x1" :x2="tempLine.x2" :y2="tempLine.y2"
:y1="tempLine.y1" stroke="#e6a23c" stroke-width="2" stroke-dasharray="6,3"
:x2="tempLine.x2" marker-end="url(#arrowhead-warn)"
:y2="tempLine.y2"
stroke="#e6a23c"
stroke-width="2"
stroke-dasharray="6,3"
marker-end="url(#arrow-down)"
/> />
</svg> </svg>
</template> </template>
<script setup> <script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue' import { ref, computed } from 'vue'
const props = defineProps({ const props = defineProps<{
/** 各层DOM元素引用,格式:{ financial: HTMLElement, customer: HTMLElement, ... } */ connectionLines: any[]
layerRefs: { type: Object, required: true }, selectedConnIdx: number | null
/** 节点DOM元素引用,格式:{ 'financial-0': HTMLElement, ... } */ tempLine: any
nodeRefs: { type: Object, default: () => ({}) }, fixedLayerArrows: { from: string; to: string; path: string }[]
/** 层顺序列表 */ }>()
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 },
/** 视角方向:cascade(级联↓)| causal(因果↑) */
viewDirection: { type: String, default: 'cascade' },
})
const emit = defineEmits(['select-connection', 'delete-connection']) const emit = defineEmits<{
(e: 'select-connection', idx: number): void
(e: 'delete-connection', idx: number): void
}>()
const svgRef = ref(null) const svgRef = ref<SVGSVGElement | null>(null)
const hoverIdx = ref(null) const hoverConnIdx = ref<number | null>(null)
const selectedIdx = ref(null)
const layerConnections = ref([])
const crossLayerLines = ref([])
/** defineExpose({ svgRef })
* 计算跨层固定箭头
* 级联视角(cascade):从上层底部中心 → 下层顶部中心(目标分解)
* 因果视角(causal):从下层顶部中心 → 上层底部中心(因果驱动)
*/
function calcCrossLayerLines() {
const lines = []
const keys = props.layerKeys
const isCausal = props.viewDirection === 'causal'
for (let i = 0; i < keys.length - 1; i++) {
const upperEl = props.layerRefs[keys[i]]
const lowerEl = props.layerRefs[keys[i + 1]]
if (!upperEl || !lowerEl) continue
const uRect = upperEl.getBoundingClientRect()
const lRect = lowerEl.getBoundingClientRect()
const svg = svgRef.value
if (!svg) continue
const svgRect = svg.getBoundingClientRect()
if (isCausal) {
// 因果视角:箭头从下层顶部 → 上层底部
lines.push({
x1: lRect.left + lRect.width / 2 - svgRect.left,
y1: lRect.top - svgRect.top,
x2: uRect.left + uRect.width / 2 - svgRect.left,
y2: uRect.bottom - svgRect.top,
})
} else {
// 级联视角:箭头从上层底部 → 下层顶部(原行为)
lines.push({
x1: uRect.left + uRect.width / 2 - svgRect.left,
y1: uRect.bottom - svgRect.top,
x2: lRect.left + lRect.width / 2 - svgRect.left,
y2: lRect.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; position: absolute; top: 0; left: 0; width: 100%; height: 100%;
top: 0; z-index: 5; pointer-events: none;
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; cursor: pointer; transition: stroke .15s, stroke-width .15s;
transition: stroke 0.15s, stroke-width 0.15s;
pointer-events: stroke; pointer-events: stroke;
} }
.conn-line:hover { .conn-line:hover { stroke: #e6a23c !important; stroke-width: 4 !important; cursor: pointer; }
stroke: #e6a23c !important; .conn-selected { stroke: #f56c6c !important; stroke-width: 3; }
stroke-width: 4 !important; .del-btn-bg { cursor: pointer; pointer-events: all; }
} .del-btn-bg:hover { fill: #e74c3c !important; }
.conn-selected { .del-btn-text { cursor: pointer; pointer-events: all; user-select: none; }
stroke: #f56c6c !important; .fixed-layer-arrow {
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>