feat(data-tools): 新增入库类型选择功能并优化数据工具界面

- 在补数任务面板中添加入库类型单选按钮组,支持 MySQL 和 InfluxDB
- 更新 AddData 接口定义,添加 StorageType 相关类型和选项接口
- 修改补数 API 请求逻辑,根据入库类型动态调整接口路径前缀
- 重构台账设备表单,统一使用装置网络参数作为 MAC 和 NDID 的单一数据源
- 优化台账线路表单,仅当存在 ID 时才设置 lineId 字段,避免空值传递
- 添加入库类型列表获取接口和相关数据处理逻辑
- 更新台账字典代码常量,新增终端型号字典码
- 优化台账树节点添加逻辑,增加前置条件验证和禁用原因提示
- 添加 InfluxDB 配置文件到额外资源目录
- 更新稳定数据分析视图,优化台账树数据结构处理和样式布局
- 完善 API 调试契约检查,确保设备和线路数据映射正确性
- 优化趋势查询性能,禁用全局加载状态提升用户体验
This commit is contained in:
2026-05-21 14:07:10 +08:00
parent f1eaabae0e
commit b9ddfb5275
23 changed files with 2462 additions and 180 deletions

View File

@@ -0,0 +1,53 @@
/* eslint-env node */
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const currentDir = path.dirname(fileURLToPath(import.meta.url))
const viewFile = path.join(currentDir, '..', 'index.vue')
const viewSource = fs.readFileSync(viewFile, 'utf8')
const expectations = [
['waveform y-axis upper padding uses 1.15', /const\s+TREND_AXIS_EXPAND_RATIO\s*=\s*1\.15/],
['waveform y-axis lower padding uses 0.85', /const\s+TREND_AXIS_SHRINK_RATIO\s*=\s*0\.85/],
['waveform y-axis above-one upper padding uses 1.05', /const\s+TREND_AXIS_EXPAND_RATIO_ABOVE_ONE\s*=\s*1\.05/],
['waveform y-axis above-one lower padding uses 0.95', /const\s+TREND_AXIS_SHRINK_RATIO_ABOVE_ONE\s*=\s*0\.95/],
[
'waveform y-axis switches padding by absolute boundary value',
/Math\.abs\(value\)\s*>\s*1[\s\S]*TREND_AXIS_EXPAND_RATIO_ABOVE_ONE[\s\S]*TREND_AXIS_SHRINK_RATIO_ABOVE_ONE/
],
['waveform y-axis compact padding uses 1.015 for narrow above-one ranges', /TREND_AXIS_COMPACT_EXPAND_RATIO\s*=\s*1\.015/],
['waveform y-axis compact padding uses 0.985 for narrow above-one ranges', /TREND_AXIS_COMPACT_SHRINK_RATIO\s*=\s*0\.985/],
['waveform y-axis compact split penalty stays low', /TREND_AXIS_COMPACT_EXTRA_SPLIT_SCORE\s*=\s*0\.05/],
[
'waveform y-axis enables compact readable range for narrow above-one data',
/shouldUseCompactReadableAxisRange[\s\S]*maxAbs\s*>\s*1[\s\S]*TREND_AXIS_COMPACT_RANGE_RATIO/
],
[
'waveform integer ranges still enter readable-axis normalization',
/rawInterval\s*>=\s*TREND_AXIS_SMALL_INTERVAL_THRESHOLD/,
true
],
['waveform y-axis keeps readable interval normalization', /getReadableAxisInterval\(axisRange\s*\/\s*currentSplitCount\)/],
[
'waveform y-axis keeps upper boundary aligned to interval and split count',
/normalizedRange\s*=\s*normalizeAxisValue\(normalizedInterval\s*\*\s*currentSplitCount,\s*precision\)[\s\S]*normalizedMin\s*\+\s*normalizedRange/
],
['waveform y-axis keeps min label visible', /showMinLabel:\s*true/],
['waveform y-axis keeps max label visible', /showMaxLabel:\s*true/]
]
const failures = expectations.filter(([, pattern, shouldBeMissing]) => {
const exists = pattern.test(viewSource)
return shouldBeMissing ? exists : !exists
})
if (failures.length) {
console.error('waveform axis range contract check failed:')
for (const [name] of failures) {
console.error(`- ${name}`)
}
process.exit(1)
}
console.log('waveform axis range contract check passed')

View File

@@ -0,0 +1,32 @@
/* eslint-env node */
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const currentDir = path.dirname(fileURLToPath(import.meta.url))
const viewFile = path.join(currentDir, '..', 'index.vue')
const viewSource = fs.readFileSync(viewFile, 'utf8')
const expectations = [
['waveform trend max line width is 1.3', /const\s+TREND_LINE_MAX_WIDTH\s*=\s*1\.3/],
[
'waveform trend line width uses visible point buckets including the final three widths',
/resolveTrendLineWidth[\s\S]*200000\)\s*return\s*0\.35[\s\S]*100000\)\s*return\s*0\.45[\s\S]*50000\)\s*return\s*0\.55[\s\S]*20000\)\s*return\s*0\.65[\s\S]*10000\)\s*return\s*0\.75[\s\S]*5000\)\s*return\s*0\.9[\s\S]*2000\)\s*return\s*1[\s\S]*800\)\s*return\s*1\.1[\s\S]*200\)\s*return\s*1\.2[\s\S]*return\s*TREND_LINE_MAX_WIDTH/
],
[
'waveform trend series applies line width from visible point count',
/width:\s*resolveTrendLineWidth\(visiblePointCount\)/
]
]
const failures = expectations.filter(([, pattern]) => !pattern.test(viewSource))
if (failures.length) {
console.error('waveform line width contract check failed:')
for (const [name] of failures) {
console.error(`- ${name}`)
}
process.exit(1)
}
console.log('waveform line width contract check passed')

View File

@@ -348,12 +348,17 @@ const disabledTrendToolStates = computed<Partial<Record<TrendToolAction, boolean
reset: !canResetTrendChart.value
}))
const TREND_AXIS_EXPAND_RATIO = 1.2
const TREND_AXIS_SHRINK_RATIO = 0.8
const TREND_AXIS_EXPAND_RATIO = 1.15
const TREND_AXIS_SHRINK_RATIO = 0.85
const TREND_AXIS_EXPAND_RATIO_ABOVE_ONE = 1.05
const TREND_AXIS_SHRINK_RATIO_ABOVE_ONE = 0.95
const TREND_AXIS_COMPACT_EXPAND_RATIO = 1.015
const TREND_AXIS_COMPACT_SHRINK_RATIO = 0.985
const TREND_AXIS_COMPACT_RANGE_RATIO = 0.25
const TREND_AXIS_COMPACT_EXTRA_SPLIT_SCORE = 0.05
const TREND_AXIS_BALANCED_RATIO = 0.9
const TREND_AXIS_DEFAULT_SPLIT_COUNT = 4
const TREND_AXIS_COMPACT_SPLIT_COUNT = 2
const TREND_AXIS_SMALL_INTERVAL_THRESHOLD = 1
const TREND_AXIS_EXTRA_SPLIT_SCORE = 0.25
const TREND_AXIS_READABLE_INTERVAL_STEPS = [1, 2, 2.5, 5, 10]
const TREND_GRID_TOP = '6px'
@@ -363,7 +368,13 @@ const TREND_GRID_BOTTOM = {
withTimeAxis: '34px',
withoutTimeAxis: '6px'
}
const TREND_LINE_MAX_WIDTH = 1.6
const TREND_LINE_MAX_WIDTH = 1.3
interface ReadableAxisRangeOptions {
preferCompact?: boolean
compactMin?: number
compactMax?: number
}
const getAxisPrecision = (step: number) => {
const absStep = Math.abs(step)
@@ -435,8 +446,8 @@ const resolveTrendLineWidth = (pointCount: number) => {
if (pointCount >= 10000) return 0.75
if (pointCount >= 5000) return 0.9
if (pointCount >= 2000) return 1
if (pointCount >= 800) return 1.2
if (pointCount >= 200) return 1.4
if (pointCount >= 800) return 1.1
if (pointCount >= 200) return 1.2
return TREND_LINE_MAX_WIDTH
}
@@ -510,26 +521,113 @@ const applyYAxisZoom = (yAxisConfig: Record<string, unknown>) => {
const halfRange = ((axisMax - axisMin) * scale) / 2
const nextMin = center - halfRange
const nextMax = center + halfRange
const splitNumber = Number(yAxisConfig.splitNumber) || TREND_AXIS_DEFAULT_SPLIT_COUNT
const interval = (nextMax - nextMin) / splitNumber
const precision = getAxisBoundaryPrecision(nextMin, nextMax, interval)
const normalizedInterval = normalizeAxisValue(interval, precision)
const splitNumber = Math.max(Math.round(Number(yAxisConfig.splitNumber) || TREND_AXIS_DEFAULT_SPLIT_COUNT), 1)
const readableAxisRange = resolveReadableAxisRange(nextMin, nextMax, splitNumber)
const precision = getAxisBoundaryPrecision(
readableAxisRange.axisMin,
readableAxisRange.axisMax,
readableAxisRange.interval
)
const normalizedInterval = normalizeAxisValue(readableAxisRange.interval, precision)
return {
...yAxisConfig,
min: normalizeAxisValue(nextMin, precision),
max: normalizeAxisValue(nextMax, precision),
min: normalizeAxisValue(readableAxisRange.axisMin, precision),
max: normalizeAxisValue(readableAxisRange.axisMax, precision),
interval: normalizedInterval,
minInterval: normalizedInterval,
maxInterval: normalizedInterval
maxInterval: normalizedInterval,
splitNumber: readableAxisRange.splitCount
}
}
const resolveReadableAxisRange = (axisMin: number, axisMax: number, splitCount: number) => {
const getReadableAxisIntervalCandidates = (value: number) => {
return Array.from(new Set([getReadableAxisInterval(value), getReadableAxisInterval(value / 2)])).filter(
item => Number.isFinite(item) && item > 0
)
}
const buildReadableAxisRangeCandidate = (
readableMin: number,
interval: number,
currentSplitCount: number,
baseSplitCount: number,
coverMin: number,
coverMax: number,
scoreRange: number,
extraSplitScore = TREND_AXIS_EXTRA_SPLIT_SCORE
) => {
const precision = getAxisPrecision(interval)
const normalizedMin = normalizeAxisValue(readableMin, precision)
const normalizedInterval = normalizeAxisValue(interval, precision)
const normalizedRange = normalizeAxisValue(normalizedInterval * currentSplitCount, precision)
const normalizedMax = normalizeAxisValue(normalizedMin + normalizedRange, precision)
const candidateRange = normalizedMax - normalizedMin
const epsilon = Math.max(Math.abs(normalizedInterval), 1) * 1e-10
if (normalizedMin - coverMin > epsilon || coverMax - normalizedMax > epsilon || candidateRange <= 0) {
return null
}
const extraSplitCost = Math.max(currentSplitCount - baseSplitCount, 0) * extraSplitScore
const wasteRatio = scoreRange > 0 ? Math.max((candidateRange - scoreRange) / scoreRange, 0) : 0
return {
axisMin: normalizedMin,
axisMax: normalizedMax,
interval: normalizedInterval,
splitCount: currentSplitCount,
score: getAxisPrecision(normalizedInterval) * 10 + extraSplitCost + wasteRatio
}
}
const resolveCenteredReadableAxisRange = (
coverMin: number,
coverMax: number,
interval: number,
currentSplitCount: number,
baseSplitCount: number,
scoreRange: number,
extraSplitScore = TREND_AXIS_EXTRA_SPLIT_SCORE
) => {
const candidateRange = interval * currentSplitCount
const coverRange = coverMax - coverMin
if (!Number.isFinite(candidateRange) || candidateRange < coverRange) return null
const center = (coverMin + coverMax) / 2
let readableMin = Math.floor((center - candidateRange / 2) / interval) * interval
let readableMax = readableMin + candidateRange
if (readableMin > coverMin) {
const shiftCount = Math.ceil((readableMin - coverMin) / interval)
readableMin -= shiftCount * interval
readableMax -= shiftCount * interval
}
if (readableMax < coverMax) {
const shiftCount = Math.ceil((coverMax - readableMax) / interval)
readableMin += shiftCount * interval
readableMax += shiftCount * interval
}
return buildReadableAxisRangeCandidate(
readableMin,
interval,
currentSplitCount,
baseSplitCount,
coverMin,
coverMax,
scoreRange,
extraSplitScore
)
}
const resolveReadableAxisRange = (axisMin: number, axisMax: number, splitCount: number, options: ReadableAxisRangeOptions = {}) => {
const axisRange = axisMax - axisMin
const rawInterval = axisRange / splitCount
if (!Number.isFinite(axisRange) || axisRange <= 0 || rawInterval >= TREND_AXIS_SMALL_INTERVAL_THRESHOLD) {
if (!Number.isFinite(axisRange) || axisRange <= 0) {
return {
axisMin,
axisMax,
@@ -538,12 +636,22 @@ const resolveReadableAxisRange = (axisMin: number, axisMax: number, splitCount:
}
}
const splitCountCandidates =
splitCount >= TREND_AXIS_DEFAULT_SPLIT_COUNT ? [splitCount, splitCount + 1] : [splitCount]
return splitCountCandidates.reduce(
(currentBest, currentSplitCount) => {
let interval = getReadableAxisInterval(axisRange / currentSplitCount)
const splitCountCandidates = splitCount >= TREND_AXIS_DEFAULT_SPLIT_COUNT ? [splitCount, splitCount + 1] : [splitCount]
const compactMin = Number(options.compactMin)
const compactMax = Number(options.compactMax)
const canUseCompact =
options.preferCompact && Number.isFinite(compactMin) && Number.isFinite(compactMax) && compactMax > compactMin
const scoreRange = canUseCompact ? compactMax - compactMin : axisRange
const candidates = splitCountCandidates.reduce<
Array<{
axisMin: number
axisMax: number
interval: number
splitCount: number
score: number
}>
>((result, currentSplitCount) => {
let interval = getReadableAxisInterval(axisRange / currentSplitCount)
let readableMin = axisMin
let readableMax = axisMax
@@ -561,24 +669,41 @@ const resolveReadableAxisRange = (axisMin: number, axisMax: number, splitCount:
interval = getReadableAxisInterval(interval * 1.01)
}
const precision = getAxisPrecision(interval)
const normalizedMin = normalizeAxisValue(readableMin, precision)
const normalizedMax = normalizeAxisValue(readableMax, precision)
const normalizedInterval = normalizeAxisValue(interval, precision)
const extraSplitCost = Math.max(currentSplitCount - splitCount, 0) * TREND_AXIS_EXTRA_SPLIT_SCORE
const wasteRatio = (normalizedMax - normalizedMin - axisRange) / axisRange
const score = getAxisPrecision(normalizedInterval) * 10 + extraSplitCost + wasteRatio
const candidate = buildReadableAxisRangeCandidate(
readableMin,
interval,
currentSplitCount,
splitCount,
axisMin,
axisMax,
scoreRange
)
if (candidate) result.push(candidate)
if (score >= currentBest.score) return currentBest
return result
}, [])
return {
axisMin: normalizedMin,
axisMax: normalizedMax,
interval: normalizedInterval,
splitCount: currentSplitCount,
score
}
},
if (canUseCompact) {
const compactSplitCountCandidates = [splitCount + 2, splitCount + 3, splitCount + 4]
compactSplitCountCandidates.forEach(currentSplitCount => {
getReadableAxisIntervalCandidates(axisRange / currentSplitCount).forEach(interval => {
const candidate = resolveCenteredReadableAxisRange(
compactMin,
compactMax,
interval,
currentSplitCount,
splitCount,
scoreRange,
TREND_AXIS_COMPACT_EXTRA_SPLIT_SCORE
)
if (candidate) candidates.push(candidate)
})
})
}
return candidates.reduce(
(currentBest, candidate) => (candidate.score < currentBest.score ? candidate : currentBest),
{
axisMin,
axisMax,
@@ -592,13 +717,30 @@ const resolveReadableAxisRange = (axisMin: number, axisMax: number, splitCount:
const resolveExpandedAxisBoundary = (value: number, isMin: boolean) => {
if (value === 0) return 0
const useAboveOneRatio = Math.abs(value) > 1
const expandRatio = useAboveOneRatio ? TREND_AXIS_EXPAND_RATIO_ABOVE_ONE : TREND_AXIS_EXPAND_RATIO
const shrinkRatio = useAboveOneRatio ? TREND_AXIS_SHRINK_RATIO_ABOVE_ONE : TREND_AXIS_SHRINK_RATIO
const ratio = isMin
? value < 0
? TREND_AXIS_EXPAND_RATIO
: TREND_AXIS_SHRINK_RATIO
? expandRatio
: shrinkRatio
: value > 0
? TREND_AXIS_EXPAND_RATIO
: TREND_AXIS_SHRINK_RATIO
? expandRatio
: shrinkRatio
return value * ratio
}
const resolveCompactAxisBoundary = (value: number, isMin: boolean) => {
if (value === 0) return 0
const ratio = isMin
? value < 0
? TREND_AXIS_COMPACT_EXPAND_RATIO
: TREND_AXIS_COMPACT_SHRINK_RATIO
: value > 0
? TREND_AXIS_COMPACT_EXPAND_RATIO
: TREND_AXIS_COMPACT_SHRINK_RATIO
return value * ratio
}
@@ -618,6 +760,13 @@ const shouldUseBalancedAxisBoundary = (min: number, max: number) => {
return largerAbs > 0 && smallerAbs / largerAbs >= TREND_AXIS_BALANCED_RATIO
}
const shouldUseCompactReadableAxisRange = (min: number, max: number) => {
const dataRange = max - min
const maxAbs = Math.max(Math.abs(min), Math.abs(max))
return maxAbs > 1 && dataRange > 0 && dataRange / maxAbs <= TREND_AXIS_COMPACT_RANGE_RATIO
}
const buildTrendAxisConfig = (trendPayload: WaveformTrendPayload, splitCount = TREND_AXIS_DEFAULT_SPLIT_COUNT) => {
const min = Number(trendPayload.min)
const max = Number(trendPayload.max)
@@ -645,7 +794,12 @@ const buildTrendAxisConfig = (trendPayload: WaveformTrendPayload, splitCount = T
}
const safeSplitCount = Math.max(Math.round(splitCount), 1)
const readableAxisRange = resolveReadableAxisRange(axisMin, axisMax, safeSplitCount)
const useCompactReadableAxisRange = shouldUseCompactReadableAxisRange(min, max)
const readableAxisRange = resolveReadableAxisRange(axisMin, axisMax, safeSplitCount, {
preferCompact: useCompactReadableAxisRange,
compactMin: useCompactReadableAxisRange ? resolveCompactAxisBoundary(min, true) : undefined,
compactMax: useCompactReadableAxisRange ? resolveCompactAxisBoundary(max, false) : undefined
})
const precision = getAxisBoundaryPrecision(
readableAxisRange.axisMin,
readableAxisRange.axisMax,

View File

@@ -1,15 +1,5 @@
const readThemeColor = (name: string, fallback: string) => {
if (typeof window === 'undefined') return fallback
const value = getComputedStyle(document.documentElement).getPropertyValue(name).trim()
return value || fallback
}
import { getDefaultPhaseThemeColors, readThemeColor } from '@/utils/phaseColors'
const phaseColors = {
a: readThemeColor('--cn-color-phase-a', '#daa520'),
b: readThemeColor('--cn-color-phase-b', '#2e8b57'),
c: readThemeColor('--cn-color-phase-c', '#a52a2a')
}
export const defaultPhaseColors = [phaseColors.a, phaseColors.b, phaseColors.c]
export const defaultPhaseColors = getDefaultPhaseThemeColors()
export const axisTextColor = readThemeColor('--el-text-color-regular', '#606266')
export const axisLineColor = readThemeColor('--el-border-color', '#dcdfe6')