feat(data-tools): 新增入库类型选择功能并优化数据工具界面
- 在补数任务面板中添加入库类型单选按钮组,支持 MySQL 和 InfluxDB - 更新 AddData 接口定义,添加 StorageType 相关类型和选项接口 - 修改补数 API 请求逻辑,根据入库类型动态调整接口路径前缀 - 重构台账设备表单,统一使用装置网络参数作为 MAC 和 NDID 的单一数据源 - 优化台账线路表单,仅当存在 ID 时才设置 lineId 字段,避免空值传递 - 添加入库类型列表获取接口和相关数据处理逻辑 - 更新台账字典代码常量,新增终端型号字典码 - 优化台账树节点添加逻辑,增加前置条件验证和禁用原因提示 - 添加 InfluxDB 配置文件到额外资源目录 - 更新稳定数据分析视图,优化台账树数据结构处理和样式布局 - 完善 API 调试契约检查,确保设备和线路数据映射正确性 - 优化趋势查询性能,禁用全局加载状态提升用户体验
This commit is contained in:
@@ -6,14 +6,6 @@
|
||||
circle
|
||||
@click="emit('update:collapsed', !collapsed)"
|
||||
/>
|
||||
<button
|
||||
v-if="collapsed"
|
||||
class="indicator-collapsed-trigger"
|
||||
type="button"
|
||||
@click="emit('update:collapsed', false)"
|
||||
>
|
||||
{{ collapsedLabel }}
|
||||
</button>
|
||||
<div v-show="!collapsed" class="indicator-panel-body">
|
||||
<SteadyIndicatorTree
|
||||
:key="selectorResetKey"
|
||||
@@ -49,8 +41,6 @@ const emit = defineEmits<{
|
||||
refresh: []
|
||||
change: [nodes: SteadyDataView.SteadyIndicatorNode[]]
|
||||
}>()
|
||||
|
||||
const collapsedLabel = '\u7a33\u6001\u6307\u6807'
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -60,12 +50,12 @@ const collapsedLabel = '\u7a33\u6001\u6307\u6807'
|
||||
right: 12px;
|
||||
bottom: 12px;
|
||||
z-index: 2;
|
||||
width: 360px;
|
||||
width: 300px;
|
||||
transition: width 0.2s ease;
|
||||
}
|
||||
|
||||
.indicator-floating-panel.is-collapsed {
|
||||
width: 44px;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.indicator-toggle {
|
||||
@@ -85,33 +75,13 @@ const collapsedLabel = '\u7a33\u6001\u6307\u6807'
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
}
|
||||
|
||||
.indicator-collapsed-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 8px 0;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 4px;
|
||||
background: var(--el-bg-color);
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
color: var(--el-text-color-primary);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
writing-mode: vertical-rl;
|
||||
}
|
||||
|
||||
.indicator-collapsed-trigger:hover {
|
||||
border-color: var(--el-color-primary-light-5);
|
||||
color: var(--el-color-primary);
|
||||
.indicator-floating-panel.is-collapsed .indicator-panel-body {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 1360px) {
|
||||
.indicator-floating-panel {
|
||||
width: 320px;
|
||||
width: 280px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -96,7 +96,6 @@ watch(
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.panel-header,
|
||||
.tree-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -104,6 +103,13 @@ watch(
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
|
||||
@@ -34,8 +34,8 @@
|
||||
<span class="node-name">{{ data.name }}</span>
|
||||
</span>
|
||||
<span class="node-count">
|
||||
<template v-if="Number(data.deviceCount) || Number(data.lineCount)">
|
||||
{{ Number(data.deviceCount || 0) }} / {{ Number(data.lineCount || 0) }}
|
||||
<template v-if="shouldShowLedgerCount(data)">
|
||||
{{ resolveLedgerCountText(data) }}
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
@@ -91,6 +91,18 @@ const resolveLedgerIcon = (value: unknown) => {
|
||||
return ledgerIcons[normalizeLedgerLevel(value)]
|
||||
}
|
||||
|
||||
const shouldShowLedgerCount = (data: SteadyDataView.SteadyLedgerNode) => {
|
||||
return Number(data.level) < 3 && (Number(data.deviceCount) > 0 || Number(data.lineCount) > 0)
|
||||
}
|
||||
|
||||
const resolveLedgerCountText = (data: SteadyDataView.SteadyLedgerNode) => {
|
||||
if (normalizeLedgerLevel(data.level) === 2) {
|
||||
return String(Number(data.lineCount || 0))
|
||||
}
|
||||
|
||||
return `${Number(data.deviceCount || 0)} / ${Number(data.lineCount || 0)}`
|
||||
}
|
||||
|
||||
const handleKeywordChange = (value: string) => {
|
||||
emit('search', value)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
<template>
|
||||
<section class="card trend-chart-panel" v-loading="loading">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">趋势图</span>
|
||||
<div v-if="trendResult" class="panel-header">
|
||||
<span class="panel-meta">
|
||||
<template v-if="trendResult">
|
||||
{{ trendResult.bucket || '-' }} / {{ trendResult.displayPointCount || 0 }} 点
|
||||
</template>
|
||||
{{ trendResult.bucket || '-' }} / {{ trendResult.displayPointCount || 0 }} 点
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -41,6 +38,7 @@ const chartOptions = computed(() => buildSteadyTrendChartOptions(props.trendResu
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
@@ -48,16 +46,11 @@ const chartOptions = computed(() => buildSteadyTrendChartOptions(props.trendResu
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.panel-meta {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
|
||||
@@ -118,7 +118,7 @@ const handleTimeBaseDateChange = (value: Date) => {
|
||||
<style scoped lang="scss">
|
||||
.trend-toolbar {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(312px, 1.4fr) repeat(2, minmax(178px, 0.8fr)) auto;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr)) auto;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
@@ -132,7 +132,7 @@ const handleTimeBaseDateChange = (value: Date) => {
|
||||
}
|
||||
|
||||
.toolbar-field--time {
|
||||
min-width: 312px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.toolbar-field__label {
|
||||
@@ -149,15 +149,16 @@ const handleTimeBaseDateChange = (value: Date) => {
|
||||
|
||||
.trend-toolbar__time {
|
||||
flex: 1 1 0;
|
||||
min-width: 260px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.harmonic-select {
|
||||
grid-column: span 2;
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
.toolbar-actions {
|
||||
display: flex;
|
||||
grid-column: 5;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@@ -107,6 +107,7 @@ const indicatorPanelCollapsedProxy = computed({
|
||||
.selector-column {
|
||||
display: grid;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.trend-main {
|
||||
@@ -115,12 +116,14 @@ const indicatorPanelCollapsedProxy = computed({
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.trend-content {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.trend-content :deep(.trend-chart-panel) {
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
/* eslint-env node */
|
||||
import fs from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import path from 'node:path'
|
||||
import vm from 'node:vm'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import ts from 'typescript'
|
||||
|
||||
const currentDir = path.dirname(fileURLToPath(import.meta.url))
|
||||
const require = createRequire(import.meta.url)
|
||||
const utilsFile = path.join(currentDir, '..', 'utils', 'ledgerTree.ts')
|
||||
const pageFile = path.join(currentDir, '..', 'index.vue')
|
||||
const ledgerTreeComponentFile = path.join(currentDir, '..', 'components', 'SteadyLedgerTree.vue')
|
||||
|
||||
const read = file => fs.readFileSync(file, 'utf8')
|
||||
|
||||
if (!fs.existsSync(utilsFile)) {
|
||||
console.error('steadyDataView ledger tree normalize contract failed:')
|
||||
console.error('- ledgerTree utility should exist')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const source = read(utilsFile)
|
||||
const compiled = ts.transpileModule(source, {
|
||||
compilerOptions: {
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
target: ts.ScriptTarget.ES2020,
|
||||
esModuleInterop: true
|
||||
}
|
||||
}).outputText
|
||||
|
||||
const moduleContext = { exports: {} }
|
||||
const sandbox = {
|
||||
exports: moduleContext.exports,
|
||||
module: moduleContext,
|
||||
require
|
||||
}
|
||||
|
||||
vm.runInNewContext(compiled, sandbox, { filename: utilsFile })
|
||||
|
||||
const { normalizeSteadyLedgerTree } = moduleContext.exports
|
||||
|
||||
if (typeof normalizeSteadyLedgerTree !== 'function') {
|
||||
console.error('steadyDataView ledger tree normalize contract failed:')
|
||||
console.error('- normalizeSteadyLedgerTree should be exported')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const flatNodes = [
|
||||
{ id: 'line-1', parentId: 'device-1', name: '监测点_1', level: 3, lineCount: 1, selectable: true },
|
||||
{ id: 'line-disabled', parentId: 'device-1', name: '不可选监测点', level: 3, lineCount: 1, selectable: false },
|
||||
{ id: 'engineering-1', name: '工程', level: 0, deviceCount: 1, lineCount: 2 },
|
||||
{ id: 'device-1', parentId: 'project-1', name: '设备', level: 2, lineCount: 2 },
|
||||
{ id: 'project-1', parentId: 'engineering-1', name: '项目', level: 1, deviceCount: 1, lineCount: 2 },
|
||||
{ id: 'line-2', parentId: 'device-1', name: '监测点_2', level: 3, lineCount: 1, selectable: true }
|
||||
]
|
||||
|
||||
const normalized = normalizeSteadyLedgerTree(flatNodes)
|
||||
const expectedPath = normalized[0]?.children?.[0]?.children?.[0]?.children?.map(item => item.name)
|
||||
|
||||
const expectations = [
|
||||
['flat nodes rebuild to one root engineering node', normalized.length === 1 && normalized[0].id === 'engineering-1'],
|
||||
['project is nested under engineering', normalized[0]?.children?.[0]?.id === 'project-1'],
|
||||
['device is nested under project', normalized[0]?.children?.[0]?.children?.[0]?.id === 'device-1'],
|
||||
['lines are nested under device', JSON.stringify(expectedPath) === JSON.stringify(['监测点_1', '监测点_2'])],
|
||||
[
|
||||
'line nodes keep selectable query identity',
|
||||
normalized[0]?.children?.[0]?.children?.[0]?.children?.every(item => item.selectable && item.level === 3)
|
||||
],
|
||||
[
|
||||
'unselectable line nodes are removed from steady query tree',
|
||||
!normalized[0]?.children?.[0]?.children?.[0]?.children?.some(item => item.id === 'line-disabled')
|
||||
],
|
||||
[
|
||||
'ledger tree component hides count text on monitor point leaves',
|
||||
/shouldShowLedgerCount[\s\S]*Number\(data\.level\)\s*<\s*3/.test(read(ledgerTreeComponentFile))
|
||||
],
|
||||
['page uses normalized ledger tree data', /normalizeSteadyLedgerTree/.test(read(pageFile))]
|
||||
]
|
||||
|
||||
const failures = expectations.filter(([, matched]) => !matched)
|
||||
|
||||
if (failures.length) {
|
||||
console.error('steadyDataView ledger tree normalize contract failed:')
|
||||
for (const [name] of failures) {
|
||||
console.error(`- ${name}`)
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log('steadyDataView ledger tree normalize contract passed')
|
||||
@@ -45,6 +45,8 @@ const expectations = [
|
||||
['API exposes indicator tree endpoint', /\/steady\/data-view\/indicator-tree/],
|
||||
['API exposes trend query endpoint', /\/steady\/data-view\/trend\/query/],
|
||||
['API exposes trend day endpoint', /\/steady\/data-view\/trend\/day/],
|
||||
['API disables global loading for trend query', /querySteadyTrend[\s\S]*\/steady\/data-view\/trend\/query[\s\S]*loading:\s*false/],
|
||||
['API disables global loading for trend day query', /querySteadyTrendDay[\s\S]*\/steady\/data-view\/trend\/day[\s\S]*loading:\s*false/],
|
||||
['API does not expose trend summary endpoint', /\/steady\/data-view\/trend\/summary/],
|
||||
['interfaces define trend query params', /interface\s+SteadyTrendQueryParams/],
|
||||
['interfaces define trend series', /interface\s+SteadyTrendSeries/],
|
||||
@@ -89,6 +91,8 @@ const sourceByExpectation = [
|
||||
apiSource,
|
||||
apiSource,
|
||||
apiSource,
|
||||
apiSource,
|
||||
apiSource,
|
||||
interfaceSource,
|
||||
interfaceSource,
|
||||
interfaceSource,
|
||||
|
||||
@@ -17,6 +17,11 @@ const componentSource = fs.existsSync(componentDir)
|
||||
.map(file => fs.readFileSync(path.join(componentDir, file), 'utf8'))
|
||||
.join('\n')
|
||||
: ''
|
||||
const readComponent = file => fs.readFileSync(path.join(componentDir, file), 'utf8')
|
||||
const toolbarSource = readComponent('SteadyTrendToolbar.vue')
|
||||
const chartPanelSource = readComponent('SteadyTrendChartPanel.vue')
|
||||
const floatingPanelSource = readComponent('SteadyIndicatorFloatingPanel.vue')
|
||||
const indicatorTreeSource = readComponent('SteadyIndicatorTree.vue')
|
||||
const viewSource = `${source}\n${componentSource}`
|
||||
const apiSource = fs.readFileSync(apiFile, 'utf8')
|
||||
const interfaceSource = fs.readFileSync(interfaceFile, 'utf8')
|
||||
@@ -40,7 +45,10 @@ const forbiddenPatterns = [
|
||||
'trend summary types are removed',
|
||||
/SteadyTrendSummary|SteadyTrendSummaryItem/,
|
||||
interfaceSource
|
||||
]
|
||||
],
|
||||
['chart panel title text is removed', /panel-title/, chartPanelSource],
|
||||
['collapsed indicator vertical trigger is removed', /indicator-collapsed-trigger/, floatingPanelSource],
|
||||
['collapsed indicator label is removed', /collapsedLabel/, floatingPanelSource]
|
||||
]
|
||||
|
||||
const requiredPatterns = [
|
||||
@@ -52,7 +60,17 @@ const requiredPatterns = [
|
||||
['components keep right floating indicator panel', /indicator-floating-panel/, viewSource],
|
||||
['indicator panel defaults expanded', /indicatorPanelCollapsed\s*=\s*ref\(false\)/, source],
|
||||
['indicator panel supports collapsed state', /is-collapsed/, viewSource],
|
||||
['API keeps trend query endpoint', /\/steady\/data-view\/trend\/query/, apiSource]
|
||||
['API keeps trend query endpoint', /\/steady\/data-view\/trend\/query/, apiSource],
|
||||
[
|
||||
'trend toolbar reserves four evenly distributed search columns',
|
||||
/grid-template-columns:\s*repeat\(4,\s*minmax\(0,\s*1fr\)\)\s*auto/,
|
||||
toolbarSource
|
||||
],
|
||||
['trend toolbar keeps actions after four search columns', /grid-column:\s*5/, toolbarSource],
|
||||
['floating indicator panel expanded width is reduced', /width:\s*300px/, floatingPanelSource],
|
||||
['floating indicator collapsed state keeps icon only', /width:\s*0/, floatingPanelSource],
|
||||
['floating indicator body is hidden when collapsed', /\.indicator-floating-panel\.is-collapsed\s+\.indicator-panel-body/, floatingPanelSource],
|
||||
['indicator tree header separates title and refresh icon', /justify-content:\s*flex-start/, indicatorTreeSource]
|
||||
]
|
||||
|
||||
const failures = [
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
resolveAvailableStats,
|
||||
validateTrendSelection
|
||||
} from './utils/selectionRules'
|
||||
import { normalizeSteadyLedgerTree } from './utils/ledgerTree'
|
||||
import { buildSteadyTrendQueryPayload, defaultTrendFormState } from './utils/trendPayload'
|
||||
|
||||
defineOptions({
|
||||
@@ -80,7 +81,8 @@ const loadLedgerTree = async (keyword = ledgerKeyword.value) => {
|
||||
loading.ledger = true
|
||||
try {
|
||||
const response = await getSteadyTrendLedgerTree(keyword ? { keyword } : undefined)
|
||||
ledgerTree.value = unwrapData(response) || []
|
||||
// 台账树接口在搜索场景可能返回扁平节点,前端统一恢复工程、项目、设备、监测点层级。
|
||||
ledgerTree.value = normalizeSteadyLedgerTree(unwrapData(response) || [])
|
||||
const firstLedgerNode = findFirstSelectableLedgerNode(ledgerTree.value)
|
||||
// 台账树首次加载后默认选中第一个可查询监测点,避免趋势查询初始状态为空。
|
||||
selectedLedgerNodes.value = firstLedgerNode ? [firstLedgerNode] : []
|
||||
@@ -184,6 +186,7 @@ onMounted(() => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.steady-data-view-page {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
170
frontend/src/views/steady/steadyDataView/utils/ledgerTree.ts
Normal file
170
frontend/src/views/steady/steadyDataView/utils/ledgerTree.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import type { SteadyDataView } from '@/api/steady/steadyDataView/interface'
|
||||
|
||||
type LedgerLevel = SteadyDataView.SteadyLedgerNode['level']
|
||||
type RawLedgerNode = Partial<SteadyDataView.SteadyLedgerNode> & Record<string, unknown>
|
||||
type IndexedLedgerNode = SteadyDataView.SteadyLedgerNode & {
|
||||
parentIds?: string
|
||||
__order: number
|
||||
}
|
||||
|
||||
const resolveText = (data: RawLedgerNode, ...keys: string[]) => {
|
||||
for (const key of keys) {
|
||||
const value = data[key]
|
||||
if (value === null || value === undefined) continue
|
||||
const text = String(value).trim()
|
||||
if (text) return text
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
const resolveNumber = (data: RawLedgerNode, ...keys: string[]) => {
|
||||
for (const key of keys) {
|
||||
const value = data[key]
|
||||
if (value === null || value === undefined || value === '') continue
|
||||
const parsed = Number(value)
|
||||
if (Number.isFinite(parsed)) return parsed
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const resolveBoolean = (data: RawLedgerNode, ...keys: string[]) => {
|
||||
for (const key of keys) {
|
||||
const value = data[key]
|
||||
if (value === null || value === undefined || value === '') continue
|
||||
if (typeof value === 'boolean') return value
|
||||
if (typeof value === 'number') return value !== 0
|
||||
|
||||
const text = String(value).trim().toLowerCase()
|
||||
if (text === 'true' || text === '1') return true
|
||||
if (text === 'false' || text === '0') return false
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const normalizeLevel = (value: unknown): LedgerLevel => {
|
||||
const level = Number(value)
|
||||
if (level === 0 || level === 1 || level === 2 || level === 3) {
|
||||
return level
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
const splitParentIds = (parentIds?: string) => {
|
||||
if (!parentIds) return []
|
||||
|
||||
return parentIds
|
||||
.split(/[,\s/|>]+/)
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
const flattenLedgerNodes = (
|
||||
nodes: SteadyDataView.SteadyLedgerNode[],
|
||||
output: IndexedLedgerNode[] = [],
|
||||
inheritedParentId = ''
|
||||
) => {
|
||||
nodes.forEach(node => {
|
||||
const rawNode = node as RawLedgerNode
|
||||
const children = Array.isArray(node.children) ? node.children : []
|
||||
const id = resolveText(rawNode, 'id', 'Id')
|
||||
const parentId = resolveText(rawNode, 'parentId', 'pid', 'Pid') || inheritedParentId
|
||||
const level = normalizeLevel(rawNode.level ?? rawNode.Level)
|
||||
const rawSelectable = resolveBoolean(rawNode, 'selectable', 'Selectable')
|
||||
|
||||
if (!id) return
|
||||
if (level === 3 && rawSelectable === false) return
|
||||
|
||||
output.push({
|
||||
id,
|
||||
parentId,
|
||||
parentIds: resolveText(rawNode, 'parentIds', 'pids', 'Pids'),
|
||||
name: resolveText(rawNode, 'name', 'Name') || id,
|
||||
level,
|
||||
sort: resolveNumber(rawNode, 'sort', 'Sort'),
|
||||
deviceCount: resolveNumber(rawNode, 'deviceCount', 'DeviceCount'),
|
||||
lineCount: resolveNumber(rawNode, 'lineCount', 'LineCount'),
|
||||
selectable: level === 3 ? rawSelectable !== false : rawSelectable === true,
|
||||
children: [],
|
||||
__order: output.length
|
||||
})
|
||||
|
||||
flattenLedgerNodes(children, output, id)
|
||||
})
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
const resolveExpectedParentId = (node: IndexedLedgerNode, nodeMap: Map<string, IndexedLedgerNode>) => {
|
||||
if (node.level === 0) return ''
|
||||
|
||||
const parentNode = node.parentId ? nodeMap.get(node.parentId) : undefined
|
||||
if (parentNode && parentNode.level === node.level - 1) {
|
||||
return parentNode.id
|
||||
}
|
||||
|
||||
// 后端搜索可能返回扁平节点或错误嵌套,优先按 parentIds 中的上一层节点恢复固定台账层级。
|
||||
const parentIds = splitParentIds(node.parentIds).reverse()
|
||||
const matchedParent = parentIds.map(id => nodeMap.get(id)).find(item => item && item.level === node.level - 1)
|
||||
|
||||
return matchedParent?.id || node.parentId || ''
|
||||
}
|
||||
|
||||
const sortLedgerNodes = (left: IndexedLedgerNode, right: IndexedLedgerNode) => {
|
||||
const leftSort = Number.isFinite(left.sort) ? Number(left.sort) : left.__order
|
||||
const rightSort = Number.isFinite(right.sort) ? Number(right.sort) : right.__order
|
||||
if (leftSort !== rightSort) return leftSort - rightSort
|
||||
|
||||
return left.__order - right.__order
|
||||
}
|
||||
|
||||
const stripInternalFields = (node: IndexedLedgerNode): SteadyDataView.SteadyLedgerNode => {
|
||||
return {
|
||||
id: node.id,
|
||||
parentId: node.parentId,
|
||||
name: node.name,
|
||||
level: node.level,
|
||||
sort: node.sort,
|
||||
deviceCount: node.deviceCount,
|
||||
lineCount: node.lineCount,
|
||||
selectable: node.selectable,
|
||||
children: node.children?.map(item => stripInternalFields(item as IndexedLedgerNode))
|
||||
}
|
||||
}
|
||||
|
||||
export const normalizeSteadyLedgerTree = (
|
||||
nodes: SteadyDataView.SteadyLedgerNode[] = []
|
||||
): SteadyDataView.SteadyLedgerNode[] => {
|
||||
const flatNodes = flattenLedgerNodes(nodes)
|
||||
const nodeMap = new Map(flatNodes.map(node => [node.id, node]))
|
||||
const roots: IndexedLedgerNode[] = []
|
||||
|
||||
flatNodes.forEach(node => {
|
||||
node.children = []
|
||||
})
|
||||
|
||||
flatNodes.forEach(node => {
|
||||
const parentId = resolveExpectedParentId(node, nodeMap)
|
||||
const parentNode = parentId ? nodeMap.get(parentId) : undefined
|
||||
|
||||
if (!parentNode || parentNode.id === node.id) {
|
||||
roots.push(node)
|
||||
return
|
||||
}
|
||||
|
||||
node.parentId = parentNode.id
|
||||
parentNode.children = [...(parentNode.children || []), node]
|
||||
})
|
||||
|
||||
const sortChildren = (items: IndexedLedgerNode[]) => {
|
||||
items.sort(sortLedgerNodes)
|
||||
items.forEach(item => sortChildren((item.children || []) as IndexedLedgerNode[]))
|
||||
}
|
||||
|
||||
sortChildren(roots)
|
||||
|
||||
return roots.map(stripInternalFields)
|
||||
}
|
||||
@@ -3,11 +3,15 @@ import type { SteadyDataView } from '@/api/steady/steadyDataView/interface'
|
||||
export const MAX_TREND_SERIES_COUNT = 24
|
||||
export const MAX_HARMONIC_ORDER_COUNT = 6
|
||||
|
||||
const isSelectableLineNode = (node: SteadyDataView.SteadyLedgerNode) => {
|
||||
return node.level === 3 && node.selectable !== false
|
||||
}
|
||||
|
||||
export const collectSelectedLineIds = (nodes: SteadyDataView.SteadyLedgerNode[]) => {
|
||||
const lineIds = new Set<string>()
|
||||
|
||||
const collect = (node: SteadyDataView.SteadyLedgerNode) => {
|
||||
if (node.level === 3 || node.selectable) {
|
||||
if (isSelectableLineNode(node)) {
|
||||
lineIds.add(node.id)
|
||||
}
|
||||
|
||||
@@ -42,7 +46,7 @@ export const findFirstSelectableLedgerNode = (
|
||||
nodes: SteadyDataView.SteadyLedgerNode[]
|
||||
): SteadyDataView.SteadyLedgerNode | null => {
|
||||
for (const node of nodes) {
|
||||
if (node.level === 3 || node.selectable) {
|
||||
if (isSelectableLineNode(node)) {
|
||||
return node
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user