|
|
@@ -44,15 +44,19 @@
|
|
|
<div v-if="drawerS4" v-loading="!s4PanelReady" class="s0-drawer-s4">
|
|
|
<template v-if="s4PanelReady">
|
|
|
<p class="s0-s4__desc s0-drawer-s4__intro">
|
|
|
- 勾选首页展示的 L1(最多 {{ MAX_S4_L1 }} 个),编辑显示名与公式说明。二级指标请在图谱中点击对应 <strong>S4 的 L1 节点</strong> 进行配置。保存后九宫格与采购看板将自动刷新。
|
|
|
+ 勾选首页展示的 L1(最多 {{ MAX_S4_L1 }} 个),编辑显示名与计算公式(结构化 DSL:支持 <code>@指标</code>、<code>$事实</code>、+ - × ÷ ( ))。
|
|
|
+ <template v-if="currentModuleCode === 'S4'">
|
|
|
+ 二级指标请在图谱中点击对应 <strong>S4 的 L1 节点</strong> 进行配置。
|
|
|
+ </template>
|
|
|
+ 保存后九宫格与对应看板将自动刷新。
|
|
|
</p>
|
|
|
<div class="s0-s4__actions s0-drawer-s4__actions">
|
|
|
- <el-button type="primary" size="small" :loading="s4Saving" @click="saveS4LayoutClick">保存 S4 布局</el-button>
|
|
|
+ <el-button type="primary" size="small" :loading="s4Saving" @click="saveS4LayoutClick">保存 {{ currentModuleCode }} 布局</el-button>
|
|
|
<el-button size="small" @click="loadS4Panel">重新加载</el-button>
|
|
|
</div>
|
|
|
<div class="s0-s4__l1">
|
|
|
- <div class="s0-s4__subhd">一级指标 L1(首页九宫格 S4)</div>
|
|
|
- <p class="s0-s4__hint">勾选后可编辑显示名称与计算公式说明。</p>
|
|
|
+ <div class="s0-s4__subhd">一级指标 L1(首页九宫格 {{ currentModuleCode }})</div>
|
|
|
+ <p class="s0-s4__hint">勾选后可编辑显示名称与结构化公式。公式默认回填自 KpiMaster 的 FormulaExpr(租户公式事实源)。</p>
|
|
|
<el-table :data="catalogL1" size="small" border stripe class="s0-s4__table">
|
|
|
<el-table-column label="首页" width="56" align="center">
|
|
|
<template #default="{ row }">
|
|
|
@@ -75,16 +79,16 @@
|
|
|
/>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
- <el-table-column label="计算公式说明" min-width="160">
|
|
|
+ <el-table-column label="计算公式(结构化)" min-width="260">
|
|
|
<template #default="{ row }">
|
|
|
- <el-input
|
|
|
+ <FormulaEditor
|
|
|
+ v-if="pickedL1.includes(row.metricCode)"
|
|
|
v-model="l1Formula[row.metricCode]"
|
|
|
- type="textarea"
|
|
|
- :rows="2"
|
|
|
- placeholder="例:Σ准时到货行 / Σ应到货行"
|
|
|
- size="small"
|
|
|
- :disabled="!pickedL1.includes(row.metricCode)"
|
|
|
+ :module-code="currentModuleCode"
|
|
|
+ :exclude-kpi-id="row.id"
|
|
|
+ placeholder="例:@PO_ON_TIME_ROWS / @PO_DUE_ROWS"
|
|
|
/>
|
|
|
+ <span v-else class="s0-s4__formula-off">(未勾选)</span>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
</el-table>
|
|
|
@@ -94,7 +98,7 @@
|
|
|
<div v-else-if="drawerS4L1" v-loading="!s4PanelReady" class="s0-drawer-s4">
|
|
|
<template v-if="s4PanelReady">
|
|
|
<p class="s0-s4__desc s0-drawer-s4__intro">
|
|
|
- 以下为字典中<strong>父级</strong>指向「{{ s4FocusedServerMetric || '(加载中或未映射)' }}」的 L2。勾选、指定左右栏并填写说明后,与上方共用「保存 S4 布局」。
|
|
|
+ 以下为字典中<strong>父级</strong>指向「{{ s4FocusedServerMetric || '(加载中或未映射)' }}」的 L2。勾选、指定左右栏并填写结构化公式后,与上方共用「保存 S4 布局」。
|
|
|
</p>
|
|
|
<el-form label-position="top" size="small" class="s0-drawer-s4__l1local">
|
|
|
<el-form-item label="图谱 L1 显示名(本地草稿)">
|
|
|
@@ -173,16 +177,16 @@
|
|
|
</el-select>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
- <el-table-column label="计算公式说明" min-width="140">
|
|
|
+ <el-table-column label="计算公式(结构化)" min-width="260">
|
|
|
<template #default="{ row }">
|
|
|
- <el-input
|
|
|
+ <FormulaEditor
|
|
|
+ v-if="l2On[row.metricCode]"
|
|
|
v-model="l2Formula[row.metricCode]"
|
|
|
- type="textarea"
|
|
|
- :rows="2"
|
|
|
- placeholder="详情主 KPI 行下方"
|
|
|
- size="small"
|
|
|
- :disabled="!l2On[row.metricCode]"
|
|
|
+ :module-code="currentModuleCode"
|
|
|
+ :exclude-kpi-id="row.id"
|
|
|
+ placeholder="详情主 KPI 行下方(支持 @指标 / $事实)"
|
|
|
/>
|
|
|
+ <span v-else class="s0-s4__formula-off">(未选用)</span>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
</el-table>
|
|
|
@@ -197,8 +201,13 @@
|
|
|
<el-form-item label="显示名称">
|
|
|
<el-input v-model="editForm.name" type="textarea" :rows="2" @input="markDirty" />
|
|
|
</el-form-item>
|
|
|
- <el-form-item v-if="selectedNode.level === 'l2'" label="计算模型 / 公式说明">
|
|
|
- <el-input v-model="editForm.formula" type="textarea" :rows="5" @input="markDirty" />
|
|
|
+ <el-form-item v-if="selectedNode.level === 'l2'" label="计算模型 / 结构化公式">
|
|
|
+ <FormulaEditor
|
|
|
+ v-model="editForm.formula"
|
|
|
+ :module-code="nodeModuleCode(selectedNode)"
|
|
|
+ @update:model-value="markDirty"
|
|
|
+ placeholder="例:@METRIC_A / @METRIC_B 或 $FACT_A + $FACT_B"
|
|
|
+ />
|
|
|
</el-form-item>
|
|
|
<el-form-item v-if="selectedNode.level === 'l2'" label="上卷 / 聚合方式">
|
|
|
<el-input v-model="editForm.aggregation" type="textarea" :rows="3" @input="markDirty" />
|
|
|
@@ -218,12 +227,12 @@
|
|
|
@update:model-value="(v) => patchGraphL2Local(row.id, 'detailLabel', v)"
|
|
|
/>
|
|
|
</el-form-item>
|
|
|
- <el-form-item label="计算模型 / 公式说明">
|
|
|
- <el-input
|
|
|
- type="textarea"
|
|
|
- :rows="3"
|
|
|
+ <el-form-item label="计算模型 / 结构化公式">
|
|
|
+ <FormulaEditor
|
|
|
:model-value="l2LocalField(row.id, 'formula', row.formula)"
|
|
|
+ :module-code="nodeModuleCode(selectedNode)"
|
|
|
@update:model-value="(v) => patchGraphL2Local(row.id, 'formula', v)"
|
|
|
+ placeholder="支持 @指标 / $事实 的结构化公式"
|
|
|
/>
|
|
|
</el-form-item>
|
|
|
<el-form-item label="上卷 / 聚合方式">
|
|
|
@@ -262,12 +271,15 @@ import {
|
|
|
} from './data/operationModelSchema'
|
|
|
import { resolveS4ServerMetricForGraphL1 } from './data/s4GraphServerMap'
|
|
|
import {
|
|
|
- fetchS4MetricCatalog,
|
|
|
- fetchS4OperationLayout,
|
|
|
- saveS4OperationLayout
|
|
|
+ fetchMetricCatalog,
|
|
|
+ fetchOperationLayout,
|
|
|
+ saveOperationLayout
|
|
|
} from '../api/kanbanData'
|
|
|
-import { notifyS4LayoutSaved } from './utils/s4LayoutEvents'
|
|
|
+import { notifyLayoutSaved } from './utils/s4LayoutEvents'
|
|
|
+import FormulaEditor from './components/FormulaEditor.vue'
|
|
|
|
|
|
+/** 当前打开的模块级/模块下 L1 抽屉对应的模块编码;非空表示"通用模块建模" */
|
|
|
+const currentModuleCode = ref('')
|
|
|
const MAX_S4_L1 = 8
|
|
|
const s4PanelReady = ref(false)
|
|
|
const s4Saving = ref(false)
|
|
|
@@ -282,6 +294,9 @@ const l1DisplayName = reactive({})
|
|
|
const l1Formula = reactive({})
|
|
|
const l2DisplayName = reactive({})
|
|
|
|
|
|
+/** 支持通用建模的模块集合(S8 以异常监控页独立处理,不纳入建模抽屉) */
|
|
|
+const MODELABLE_MODULES = ['S1', 'S2', 'S3', 'S4', 'S5', 'S6', 'S7', 'S9']
|
|
|
+
|
|
|
const route = useRoute()
|
|
|
const chartRef = ref(null)
|
|
|
const moduleFilter = ref('')
|
|
|
@@ -309,17 +324,25 @@ function applyModelingRouteQuery() {
|
|
|
itemStyle: { color: 'rgba(59, 130, 246, 0.3)', borderColor: '#60a5fa', borderWidth: 2 },
|
|
|
label: { color: '#eff6ff', fontSize: 10 }
|
|
|
}
|
|
|
- loadS4Panel()
|
|
|
+ loadModulePanel('S4')
|
|
|
})
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function parentRowIdToMetricCode(pid) {
|
|
|
if (!pid || typeof pid !== 'string') return ''
|
|
|
- const m = pid.match(/^S4-L\d+-(.+)$/)
|
|
|
+ const m = pid.match(/^S\d+-L\d+-(.+)$/)
|
|
|
return m ? m[1] : ''
|
|
|
}
|
|
|
|
|
|
+function nodeModuleCode(node) {
|
|
|
+ if (!node) return currentModuleCode.value || ''
|
|
|
+ const raw = String(node.rawId || '')
|
|
|
+ const m = raw.match(/^(S\d+)/i)
|
|
|
+ if (m) return m[1].toUpperCase()
|
|
|
+ return currentModuleCode.value || ''
|
|
|
+}
|
|
|
+
|
|
|
function l1Name(code) {
|
|
|
const c = catalogL1.value.find((x) => x.metricCode === code)
|
|
|
return c ? `${c.defaultName} (${code})` : code
|
|
|
@@ -378,12 +401,25 @@ function effectiveL2DisplayName(row) {
|
|
|
}
|
|
|
|
|
|
async function loadS4Panel(opts = {}) {
|
|
|
+ return loadModulePanel(currentModuleCode.value || 'S4', opts)
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 通用模块建模面板加载:按 moduleCode 拉取 KpiMaster 字典 + LayoutItem,
|
|
|
+ * 回填 pickedL1 / l1DisplayName / l1Formula / l2On / ... 等 UI 状态。
|
|
|
+ * 说明:formula 字段优先使用 LayoutItem.FormulaText(租户级 override),
|
|
|
+ * 若为空则回落到 KpiMaster.FormulaExpr(方案 α:KpiMaster 为公式事实源)。
|
|
|
+ */
|
|
|
+async function loadModulePanel(moduleCode, opts = {}) {
|
|
|
+ const mc = String(moduleCode || '').toUpperCase()
|
|
|
+ if (!mc) return
|
|
|
+ currentModuleCode.value = mc
|
|
|
const keepVisible = Boolean(opts.keepVisible)
|
|
|
if (!keepVisible) s4PanelReady.value = false
|
|
|
try {
|
|
|
const [rawAll, layout] = await Promise.all([
|
|
|
- fetchS4MetricCatalog(),
|
|
|
- fetchS4OperationLayout(1, 1)
|
|
|
+ fetchMetricCatalog(mc),
|
|
|
+ fetchOperationLayout(mc, 1)
|
|
|
])
|
|
|
const rawL1 = (rawAll || []).filter((x) => x.metricLevel === 1)
|
|
|
const rawL2 = (rawAll || []).filter((x) => x.metricLevel >= 2)
|
|
|
@@ -392,13 +428,13 @@ async function loadS4Panel(opts = {}) {
|
|
|
|
|
|
for (const row of catalogL1.value) {
|
|
|
l1DisplayName[row.metricCode] = ''
|
|
|
- l1Formula[row.metricCode] = ''
|
|
|
+ l1Formula[row.metricCode] = row.formulaExpr ?? ''
|
|
|
}
|
|
|
for (const row of catalogL2.value) {
|
|
|
if (l2On[row.metricCode] === undefined) l2On[row.metricCode] = false
|
|
|
if (l2ParentMetric[row.metricCode] === undefined) l2ParentMetric[row.metricCode] = ''
|
|
|
if (l2Zone[row.metricCode] === undefined) l2Zone[row.metricCode] = 'left'
|
|
|
- if (l2Formula[row.metricCode] === undefined) l2Formula[row.metricCode] = ''
|
|
|
+ if (l2Formula[row.metricCode] === undefined) l2Formula[row.metricCode] = row.formulaExpr ?? ''
|
|
|
if (l2DisplayName[row.metricCode] === undefined) l2DisplayName[row.metricCode] = ''
|
|
|
}
|
|
|
|
|
|
@@ -409,7 +445,7 @@ async function loadS4Panel(opts = {}) {
|
|
|
.map((x) => x.metricCode)
|
|
|
for (const x of layout.l1) {
|
|
|
l1DisplayName[x.metricCode] = x.displayName ?? ''
|
|
|
- l1Formula[x.metricCode] = x.formulaText ?? ''
|
|
|
+ if (x.formulaText) l1Formula[x.metricCode] = x.formulaText
|
|
|
}
|
|
|
} else {
|
|
|
pickedL1.value = []
|
|
|
@@ -424,7 +460,7 @@ async function loadS4Panel(opts = {}) {
|
|
|
l2On[row.metricCode] = false
|
|
|
l2ParentMetric[row.metricCode] = row.parentId ? (idToCode[row.parentId] || '') : ''
|
|
|
l2Zone[row.metricCode] = 'left'
|
|
|
- l2Formula[row.metricCode] = ''
|
|
|
+ l2Formula[row.metricCode] = row.formulaExpr ?? ''
|
|
|
l2DisplayName[row.metricCode] = ''
|
|
|
}
|
|
|
|
|
|
@@ -435,17 +471,17 @@ async function loadS4Panel(opts = {}) {
|
|
|
if (layoutParent) l2ParentMetric[row.metricCode] = layoutParent
|
|
|
const z = String(row.panelZone || 'left').toLowerCase()
|
|
|
l2Zone[row.metricCode] = z === 'right' ? 'right' : 'left'
|
|
|
- l2Formula[row.metricCode] = row.formulaText ?? ''
|
|
|
+ if (row.formulaText) l2Formula[row.metricCode] = row.formulaText
|
|
|
l2DisplayName[row.metricCode] = row.displayName ?? ''
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (!catalogL1.value.length) {
|
|
|
- ElMessage.warning('未加载到 S4 指标字典,请确认后端已初始化 KpiMaster(S4)种子数据')
|
|
|
+ ElMessage.warning(`未加载到 ${mc} 指标字典,请确认后端已初始化 KpiMaster(${mc})数据`)
|
|
|
}
|
|
|
} catch (e) {
|
|
|
console.error(e)
|
|
|
- ElMessage.error('加载 S4 指标配置失败,请检查接口 /api/AidopKanban/s4-metric-catalog 与网络')
|
|
|
+ ElMessage.error(`加载 ${mc} 指标配置失败,请检查接口 /api/AidopKanban/metric-catalog/${mc} 与网络`)
|
|
|
catalogL1.value = []
|
|
|
catalogL2.value = []
|
|
|
pickedL1.value = []
|
|
|
@@ -455,6 +491,12 @@ async function loadS4Panel(opts = {}) {
|
|
|
}
|
|
|
|
|
|
async function saveS4LayoutClick() {
|
|
|
+ return saveModuleLayoutClick(currentModuleCode.value || 'S4')
|
|
|
+}
|
|
|
+
|
|
|
+async function saveModuleLayoutClick(moduleCode) {
|
|
|
+ const mc = String(moduleCode || '').toUpperCase()
|
|
|
+ if (!mc) return
|
|
|
if (pickedL1.value.length > MAX_S4_L1) {
|
|
|
ElMessage.warning(`L1 最多 ${MAX_S4_L1} 个`)
|
|
|
return
|
|
|
@@ -462,7 +504,7 @@ async function saveS4LayoutClick() {
|
|
|
const L1 = catalogL1.value
|
|
|
.filter((c) => pickedL1.value.includes(c.metricCode))
|
|
|
.map((c, i) => ({
|
|
|
- RowId: `S4-L1-${c.metricCode}`,
|
|
|
+ RowId: `${mc}-L1-${c.metricCode}`,
|
|
|
MetricCode: c.metricCode,
|
|
|
DisplayName: effectiveL1DisplayName(c.metricCode),
|
|
|
FormulaText: effectiveL1Formula(c.metricCode),
|
|
|
@@ -473,10 +515,10 @@ async function saveS4LayoutClick() {
|
|
|
if (!l2On[row.metricCode]) continue
|
|
|
const parent = String(l2ParentMetric[row.metricCode] || '').trim()
|
|
|
const lvl = row.metricLevel ?? 2
|
|
|
- const parentPrefix = lvl >= 3 ? 'S4-L2-' : 'S4-L1-'
|
|
|
+ const parentPrefix = lvl >= 3 ? `${mc}-L2-` : `${mc}-L1-`
|
|
|
const parentRowId = parent ? `${parentPrefix}${parent}` : null
|
|
|
L2.push({
|
|
|
- RowId: `S4-L${lvl}-${row.metricCode}`,
|
|
|
+ RowId: `${mc}-L${lvl}-${row.metricCode}`,
|
|
|
MetricCode: row.metricCode,
|
|
|
DisplayName: effectiveL2DisplayName(row),
|
|
|
SortNo: (L2.length + 1) * 10,
|
|
|
@@ -487,11 +529,11 @@ async function saveS4LayoutClick() {
|
|
|
}
|
|
|
s4Saving.value = true
|
|
|
try {
|
|
|
- const r = await saveS4OperationLayout({ L1, L2 }, 1, 1)
|
|
|
+ const r = await saveOperationLayout(mc, { L1, L2 }, 1)
|
|
|
if (r.ok) {
|
|
|
- notifyS4LayoutSaved()
|
|
|
- await loadS4Panel({ keepVisible: true })
|
|
|
- ElMessage.success('S4 布局已保存;首页九宫格与 S4 采购看板已自动刷新数据')
|
|
|
+ notifyLayoutSaved(mc)
|
|
|
+ await loadModulePanel(mc, { keepVisible: true })
|
|
|
+ ElMessage.success(`${mc} 布局已保存;首页九宫格${mc === 'S4' ? '与 S4 采购看板' : '与对应详情看板'}已自动刷新数据`)
|
|
|
} else {
|
|
|
ElMessage.error(r.message || '保存失败')
|
|
|
}
|
|
|
@@ -526,10 +568,23 @@ const s4FocusedServerMetric = computed(() => {
|
|
|
return resolveS4ServerMetricForGraphL1(rid, sortedS4CatalogL1.value)
|
|
|
})
|
|
|
|
|
|
+/**
|
|
|
+ * 模块级抽屉:S1~S7/S9(S8 不纳入建模)。S4 为历史试点,此处保持完全兼容并
|
|
|
+ * 使用同一套 UI + 通用 API;区别仅在于 currentModuleCode。
|
|
|
+ */
|
|
|
const drawerS4 = computed(
|
|
|
- () => Boolean(selectedNode.value?.level === 'module' && selectedNode.value?.rawId === 'S4')
|
|
|
+ () =>
|
|
|
+ Boolean(
|
|
|
+ selectedNode.value?.level === 'module' &&
|
|
|
+ MODELABLE_MODULES.includes(selectedNode.value?.rawId)
|
|
|
+ )
|
|
|
)
|
|
|
|
|
|
+/**
|
|
|
+ * 模块 L1 子抽屉:仅 S4 保留(因其 L1 已完整配好图谱 rawId → KpiMaster metricCode
|
|
|
+ * 的映射表 resolveS4ServerMetricForGraphL1)。其他模块的 L1 子抽屉在 drawerS4L1
|
|
|
+ * 之外走通用节点编辑表单(本地 override 草稿 + 公式编辑器)。
|
|
|
+ */
|
|
|
const drawerS4L1 = computed(() => {
|
|
|
if (selectedNode.value?.level !== 'l1') return false
|
|
|
return Boolean(selectedNode.value.rawId?.startsWith('S4-L1-'))
|
|
|
@@ -913,10 +968,11 @@ function onChartClick(params) {
|
|
|
if (d.level === 'module') {
|
|
|
drawerVisible.value = true
|
|
|
selectedNode.value = { ...d, name: d.name.replace(/\n/g, ' ') }
|
|
|
- if (d.rawId === 'S4') {
|
|
|
- loadS4Panel()
|
|
|
+ if (MODELABLE_MODULES.includes(d.rawId)) {
|
|
|
+ loadModulePanel(d.rawId)
|
|
|
return
|
|
|
}
|
|
|
+ currentModuleCode.value = ''
|
|
|
editForm.value = { name: d.name.replace(/\n/g, ' '), formula: '', aggregation: '' }
|
|
|
return
|
|
|
}
|
|
|
@@ -932,7 +988,7 @@ function onChartClick(params) {
|
|
|
aggregation: ''
|
|
|
}
|
|
|
if (block?.moduleId === 'S4') {
|
|
|
- loadS4Panel()
|
|
|
+ loadModulePanel('S4')
|
|
|
}
|
|
|
} else if (l2) {
|
|
|
selectedNode.value = { ...d, level: 'l2', formula: l2.formula, aggregation: l2.aggregation }
|