feat(data-tools): 新增入库类型选择功能并优化数据工具界面
- 在补数任务面板中添加入库类型单选按钮组,支持 MySQL 和 InfluxDB - 更新 AddData 接口定义,添加 StorageType 相关类型和选项接口 - 修改补数 API 请求逻辑,根据入库类型动态调整接口路径前缀 - 重构台账设备表单,统一使用装置网络参数作为 MAC 和 NDID 的单一数据源 - 优化台账线路表单,仅当存在 ID 时才设置 lineId 字段,避免空值传递 - 添加入库类型列表获取接口和相关数据处理逻辑 - 更新台账字典代码常量,新增终端型号字典码 - 优化台账树节点添加逻辑,增加前置条件验证和禁用原因提示 - 添加 InfluxDB 配置文件到额外资源目录 - 更新稳定数据分析视图,优化台账树数据结构处理和样式布局 - 完善 API 调试契约检查,确保设备和线路数据映射正确性 - 优化趋势查询性能,禁用全局加载状态提升用户体验
This commit is contained in:
@@ -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')
|
||||
@@ -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')
|
||||
@@ -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,
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user