耐受图调整、屏蔽非必要的菜单、登录后跳转到变频器页面

This commit is contained in:
caozehui
2026-04-27 08:39:30 +08:00
parent 8744dfb0d8
commit 8ab1a35f3b
8 changed files with 512 additions and 340 deletions

View File

@@ -130,6 +130,7 @@ const initChart = () => {
chart.resize() chart.resize()
}, 0) }, 0)
} }
const getChartInstance = () => chart
const handlerBar = (options: any) => { const handlerBar = (options: any) => {
if (Array.isArray(options.series)) { if (Array.isArray(options.series)) {
options.series.forEach((item: any) => { options.series.forEach((item: any) => {
@@ -253,7 +254,7 @@ onMounted(() => {
initChart() initChart()
resizeObserver.observe(chartRef.value!) resizeObserver.observe(chartRef.value!)
}) })
defineExpose({ initChart }) defineExpose({ initChart, getChartInstance })
onBeforeUnmount(() => { onBeforeUnmount(() => {
resizeObserver.unobserve(chartRef.value!) resizeObserver.unobserve(chartRef.value!)
chart?.dispose() chart?.dispose()

View File

@@ -1,7 +1,7 @@
// ? 全局默认配置项 // ? 全局默认配置项
// 首页地址(默认) // 首页地址(默认)
export const HOME_URL: string = "/home/index"; export const HOME_URL: string = "/machine/freqConverter";
// export const HOME_URL: string = "/machine/controlSource"; // export const HOME_URL: string = "/machine/controlSource";

View File

@@ -26,6 +26,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue' import { computed } from 'vue'
import { HOME_URL } from '@/config'
import { useAuthStore } from '@/stores/modules/auth' import { useAuthStore } from '@/stores/modules/auth'
import { useModeStore } from '@/stores/modules/mode' // 引入模式 store import { useModeStore } from '@/stores/modules/mode' // 引入模式 store
import { useTabsStore } from '@/stores/modules/tabs' import { useTabsStore } from '@/stores/modules/tabs'
@@ -74,8 +75,8 @@ const handelOpen = async (item: string, key: string) => {
await initDynamicRouter() await initDynamicRouter()
// 只有当目标路径与当前路径不同时才跳转 // 只有当目标路径与当前路径不同时才跳转
if (router.currentRoute.value.path !== '/home/index') { if (router.currentRoute.value.path !== HOME_URL) {
await router.push({ path: '/home/index' }) await router.push({ path: HOME_URL })
} else { } else {
// 如果已在目标页面,手动触发组件更新 // 如果已在目标页面,手动触发组件更新
window.location.reload() // 或者采用其他方式刷新数据 window.location.reload() // 或者采用其他方式刷新数据

View File

@@ -70,7 +70,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { LOGIN_URL } from '@/config' import { HOME_URL, LOGIN_URL } from '@/config'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/modules/user' import { useUserStore } from '@/stores/modules/user'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
@@ -132,7 +132,7 @@ const changeScene = async (value: string) => {
//模式切换 //模式切换
const changeMode = async () => { const changeMode = async () => {
authStore.changeModel() authStore.changeModel()
await router.push('/home/index') await router.push(HOME_URL)
} }
</script> </script>

View File

@@ -29,6 +29,7 @@ import { useRoute, useRouter } from 'vue-router'
import { useGlobalStore } from '@/stores/modules/global' import { useGlobalStore } from '@/stores/modules/global'
import { useTabsStore } from '@/stores/modules/tabs' import { useTabsStore } from '@/stores/modules/tabs'
import { useAuthStore } from '@/stores/modules/auth' import { useAuthStore } from '@/stores/modules/auth'
import { HOME_URL } from '@/config'
import { TabPaneName, TabsPaneContext } from 'element-plus' import { TabPaneName, TabsPaneContext } from 'element-plus'
import MoreButton from './components/MoreButton.vue' import MoreButton from './components/MoreButton.vue'
@@ -73,13 +74,13 @@ watch(
// 初始化需要固定的 tabs // 初始化需要固定的 tabs
const initTabs = () => { const initTabs = () => {
authStore.flatMenuListGet.forEach(item => { authStore.flatMenuListGet.forEach(item => {
if (item.meta.isAffix && !item.meta.isHide && !item.meta.isFull) { if (item.path === HOME_URL && !item.meta.isHide && !item.meta.isFull) {
const tabsParams = { const tabsParams = {
icon: item.meta.icon, icon: item.meta.icon,
title: item.meta.title, title: item.meta.title,
path: item.path, path: item.path,
name: item.name, name: item.name,
close: !item.meta.isAffix, close: false,
isKeepAlive: item.meta.isKeepAlive, isKeepAlive: item.meta.isKeepAlive,
unshift: true unshift: true
} }

View File

@@ -3,6 +3,7 @@ import { type AuthState } from '@/stores/interface'
import { getAuthButtonListApi, getAuthMenuListApi } from '@/api/user/login' import { getAuthButtonListApi, getAuthMenuListApi } from '@/api/user/login'
import { getAllBreadcrumbList, getFlatMenuList, getShowMenuList } from '@/utils' import { getAllBreadcrumbList, getFlatMenuList, getShowMenuList } from '@/utils'
import { AUTH_STORE_KEY } from '@/stores/constant' import { AUTH_STORE_KEY } from '@/stores/constant'
import { HOME_URL } from '@/config'
import { useModeStore } from '@/stores/modules/mode' import { useModeStore } from '@/stores/modules/mode'
import { getLicense } from '@/api/activate' import { getLicense } from '@/api/activate'
import type { Activate } from '@/api/activate/interface' import type { Activate } from '@/api/activate/interface'
@@ -52,7 +53,7 @@ export const useAuthStore = defineStore(AUTH_STORE_KEY, {
? filterMenuByExcludedNames(menuData, ['testSource', 'testScript', 'controlSource']) ? filterMenuByExcludedNames(menuData, ['testSource', 'testScript', 'controlSource'])
: filterMenuByExcludedNames(menuData, ['standardDevice']) : filterMenuByExcludedNames(menuData, ['standardDevice'])
this.authMenuList = filteredMenu this.authMenuList = normalizeHomeAffix(filteredMenu)
}, },
// Set RouteName // Set RouteName
async setRouteName(name: string) { async setRouteName(name: string) {
@@ -112,3 +113,22 @@ function filterMenuByExcludedNames(menuList: any[], excludedNames: string[]): an
return !excludedNames.includes(menu.name) return !excludedNames.includes(menu.name)
}) })
} }
function normalizeHomeAffix(menuList: any[]): any[] {
return menuList.map(menu => {
const nextMenu = { ...menu }
if (nextMenu.meta) {
nextMenu.meta = {
...nextMenu.meta,
isAffix: nextMenu.path === HOME_URL
}
}
if (Array.isArray(nextMenu.children) && nextMenu.children.length > 0) {
nextMenu.children = normalizeHomeAffix(nextMenu.children)
}
return nextMenu
})
}

View File

@@ -120,7 +120,7 @@ const login = (formEl: FormInstance | undefined) => {
await tabsStore.setTabs([]) await tabsStore.setTabs([])
await keepAliveStore.setKeepAliveName([]) await keepAliveStore.setKeepAliveName([])
// 登录默认不显示菜单和导航栏 // 登录默认不显示菜单和导航栏
await authStore.resetAuthStore() await authStore.setShowMenu()
// 跳转到首页 // 跳转到首页
await router.push(HOME_URL) await router.push(HOME_URL)
} finally { } finally {

View File

@@ -1,4 +1,4 @@
<template> <template>
<el-card class="dip-chart-card" shadow="never"> <el-card class="dip-chart-card" shadow="never">
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
@@ -9,50 +9,59 @@
</div> </div>
</div> </div>
<el-button <div class="header-actions">
<el-button type="primary" plain :icon="Download" :disabled="!hasChartData" @click="downloadChartImage">
下载图片
</el-button>
<el-button type="primary" plain :icon="Document" :disabled="!hasChartData" @click="exportChartData">
导出数据
</el-button>
<el-button
type="primary" type="primary"
plain plain
:loading="curveLoading" :loading="curveLoading"
class="draw-curve-button" class="draw-curve-button"
@click="drawCharacteristicCurve" @click="drawCharacteristicCurve"
> >
绘制特性曲线 绘制特性曲线
</el-button> </el-button>
</div>
</div> </div>
</template> </template>
<div class="chart-wrapper"> <div class="chart-wrapper">
<MyEchart :options="chartOptions"/> <MyEchart ref="chartRef" :options="chartOptions" />
</div> </div>
</el-card> </el-card>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {computed, ref, watch} from 'vue' import { computed, ref, watch } from 'vue'
import {ElMessage} from 'element-plus' import { ElMessage } from 'element-plus'
import { Document, Download } from '@element-plus/icons-vue'
import * as XLSX from 'xlsx'
import MyEchart from '@/components/echarts/line/index.vue' import MyEchart from '@/components/echarts/line/index.vue'
import {getFreqConverterSCurve} from '@/api/device/freqConverter' import { getFreqConverterSCurve } from '@/api/device/freqConverter'
type ChartPointStatus = 'pass' | 'fail' type ChartPointStatus = 'pass' | 'fail'
interface ChartPoint { interface ChartPoint {
duration: number; duration: number
residualVoltage: number; residualVoltage: number
status: ChartPointStatus; status: ChartPointStatus
} }
interface NormalizedTolerantPoint { interface NormalizedTolerantPoint {
duration: number; duration: number
residualVoltage: number; residualVoltage: number
tolerant: number | null; tolerant: number | null
status: ChartPointStatus; status: ChartPointStatus
} }
const props = defineProps<{ const props = defineProps<{
selectedMapping?: Record<string, any> | null; selectedMapping?: Record<string, any> | null
webMsgSend?: any; webMsgSend?: any
resultData?: any; resultData?: any
}>() }>()
const STATUS_COLOR_MAP: Record<ChartPointStatus, string> = { const STATUS_COLOR_MAP: Record<ChartPointStatus, string> = {
@@ -63,6 +72,7 @@ const STATUS_COLOR_MAP: Record<ChartPointStatus, string> = {
const chartPoints = ref<ChartPoint[]>([]) const chartPoints = ref<ChartPoint[]>([])
const characteristicCurveData = ref<Array<[number, number]>>([]) const characteristicCurveData = ref<Array<[number, number]>>([])
const curveLoading = ref(false) const curveLoading = ref(false)
const chartRef = ref<any>(null)
const selectedMappingText = computed(() => { const selectedMappingText = computed(() => {
if (!props.selectedMapping) { if (!props.selectedMapping) {
@@ -72,6 +82,91 @@ const selectedMappingText = computed(() => {
return `变频器:${props.selectedMapping.freqConverterName || '-'}` return `变频器:${props.selectedMapping.freqConverterName || '-'}`
}) })
const positiveDurations = computed(() => {
return [
...chartPoints.value.map(item => item.duration),
...characteristicCurveData.value.map(item => item[0])
].filter(item => Number.isFinite(item) && item > 0)
})
const xAxisMin = computed(() => {
if (!positiveDurations.value.length) {
return 0.001
}
const minValue = Math.min(...positiveDurations.value)
return Math.min(0.001, Number(minValue.toFixed(3)))
})
const xAxisMax = computed(() => {
if (!positiveDurations.value.length) {
return 60
}
const maxValue = Math.max(...positiveDurations.value)
return Math.max(Number((maxValue * 1.05).toFixed(3)), 60)
})
const sortedChartPoints = computed(() => {
return [...chartPoints.value].sort((a, b) => {
if (a.duration !== b.duration) {
return a.duration - b.duration
}
return a.residualVoltage - b.residualVoltage
})
})
const sortedCharacteristicCurveData = computed(() => {
return [...characteristicCurveData.value].sort((a, b) => {
if (a[0] !== b[0]) {
return a[0] - b[0]
}
return a[1] - b[1]
})
})
const hasChartData = computed(() => {
return sortedChartPoints.value.length > 0 || sortedCharacteristicCurveData.value.length > 0
})
const formatLogDurationLabel = (value: number) => {
if (!Number.isFinite(value)) {
return ''
}
if (value >= 1) {
return `${Number(value.toFixed(2))}`
}
return `${Number(value.toFixed(3))}`
}
const sanitizeFileName = (value: string) => {
return value.replace(/[\\/:*?"<>|]/g, '_').trim() || '未命名变频器'
}
const buildFileName = (prefix: string, suffix: string) => {
const freqConverterName = sanitizeFileName(props.selectedMapping?.freqConverterName || '未命名变频器')
return `${prefix}_${freqConverterName}.${suffix}`
}
const triggerDownload = (url: string, fileName: string) => {
const link = document.createElement('a')
link.style.display = 'none'
link.href = url
link.download = fileName
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
const toNumber = (value: unknown) => {
const result = Number(value)
return Number.isFinite(result) ? result : null
}
const normalizeTolerantValue = (value: unknown) => { const normalizeTolerantValue = (value: unknown) => {
if (value === undefined || value === null || value === '') { if (value === undefined || value === null || value === '') {
return null return null
@@ -89,246 +184,12 @@ const normalizeDuration = (source: Record<string, any>) => {
return toNumber( return toNumber(
source.durationMs !== undefined && source.durationMs !== null source.durationMs !== undefined && source.durationMs !== null
? Number(source.durationMs) / 1000 ? Number(source.durationMs) / 1000
: source.duration ?? : source.duration ?? source.x ?? source.dipDuration ?? source.retainTime ?? source.durationValue
source.x ??
source.dipDuration ??
source.retainTime ??
source.durationValue
) )
} }
const normalizeResidualVoltageValue = (source: Record<string, any>) => { const normalizeResidualVoltageValue = (source: Record<string, any>) => {
return toNumber( return toNumber(source.residualVoltage ?? source.y ?? source.residual ?? source.voltage ?? source.residual_value)
source.residualVoltage ??
source.y ??
source.residual ??
source.voltage ??
source.residual_value
)
}
const normalizeCurveData = (source: unknown) => {
if (!Array.isArray(source)) {
return [] as Array<[number, number]>
}
return source
.map(item => {
if (Array.isArray(item) && item.length >= 2) {
const duration = Number(item[0])
const residualVoltage = Number(item[1])
if (Number.isFinite(duration) && Number.isFinite(residualVoltage)) {
return [duration, residualVoltage] as [number, number]
}
}
if (item && typeof item === 'object') {
const record = item as Record<string, any>
const tolerant = normalizeTolerantValue(record.tolerant)
if (tolerant !== null && tolerant !== 2) {
return null
}
const duration = normalizeDuration(record)
const residualVoltage = normalizeResidualVoltageValue(record)
if (duration !== null && residualVoltage !== null) {
return [duration, residualVoltage] as [number, number]
}
}
return null
})
.filter((item): item is [number, number] => !!item)
}
const extractCurveData = (payload: any) => {
if (!payload) {
return [] as Array<[number, number]>
}
const candidates = [
payload,
payload?.data,
payload?.data?.records,
payload?.data?.points,
payload?.points,
payload?.records,
payload?.list
]
for (const candidate of candidates) {
const normalized = normalizeCurveData(candidate)
if (normalized.length) {
return normalized
}
}
return [] as Array<[number, number]>
}
const drawCharacteristicCurve = async () => {
const freqConverterId = props.selectedMapping?.freqConverterId
if (!freqConverterId) {
ElMessage.warning('未获取到变频器ID')
return
}
curveLoading.value = true
try {
const result = await getFreqConverterSCurve({
converterId: freqConverterId
})
const normalizedCurveData = extractCurveData(result)
if (!normalizedCurveData.length) {
characteristicCurveData.value = []
ElMessage.warning('未获取到特性曲线数据')
return
}
characteristicCurveData.value = normalizedCurveData
} catch (error) {
console.error('绘制特性曲线失败:', error)
characteristicCurveData.value = []
} finally {
curveLoading.value = false
}
}
const chartOptions = computed(() => {
const maxDuration = 2
// const maxDuration = Math.max(
// 2,
// ...chartPoints.value.map(item => item.duration).filter(item => Number.isFinite(item)),
// ...characteristicCurveData.value.map(item => item[0]).filter(item => Number.isFinite(item))
// )
return {
title: {
text: ''
},
grid: {
top: 30,
left: 48,
right: 22,
bottom: 52
},
tooltip: {
trigger: 'item',
formatter(params: any) {
if (params.seriesType === 'line') {
return ''
}
const [duration, residualVoltage, statusText] = params.value
return [
`持续时间: ${duration} s`,
`残余电压: ${residualVoltage} %`,
`状态: ${statusText}`
].join('<br/>')
}
},
legend: {
top: 0,
right: 0,
data: ['特性测试曲线']
},
xAxis: {
type: 'log',
name: '持续时间(s)',
nameLocation: 'middle',
nameGap: 34,
min: 0.01,
max: 60,
logBase: 10,
minorTick: {
show: true,
splitNumber: 10
},
minorSplitLine: {
show: true,
lineStyle: {
color: '#e8edf6'
}
},
splitLine: {
lineStyle: {
color: '#cfd8e6'
}
},
axisLabel: {
formatter(value: number) {
return value.toFixed(2)
}
}
},
yAxis: {
type: 'value',
name: '暂降幅值',
min: 0,
max: 100,
interval: 10,
minorTick: {
show: true,
splitNumber: 2
},
minorSplitLine: {
show: true,
lineStyle: {
color: '#e8edf6'
}
},
splitLine: {
lineStyle: {
color: '#cfd8e6'
}
},
axisLabel: {
formatter(value: number) {
return `${value}%`
}
}
},
dataZoom: [],
series: [
{
name: '特性测试曲线',
type: 'line',
smooth: true,
showSymbol: true,
symbolSize: 7,
lineStyle: {
color: '#ff2a2a',
width: 3
},
itemStyle: {
color: '#ff2a2a'
},
data: characteristicCurveData.value
},
{
name: '暂降点',
type: 'scatter',
symbolSize: 10,
data: chartPoints.value.map(item => ({
value: [item.duration, item.residualVoltage, getStatusText(item.status)],
itemStyle: {
color: STATUS_COLOR_MAP[item.status]
}
}))
}
],
options: {
animation: false
}
}
})
const toNumber = (value: unknown) => {
const result = Number(value)
return Number.isFinite(result) ? result : null
} }
const normalizeStatus = (value: unknown): ChartPointStatus => { const normalizeStatus = (value: unknown): ChartPointStatus => {
@@ -362,14 +223,14 @@ const normalizeTolerantPoint = (source: Record<string, any>): NormalizedTolerant
const tolerant = normalizeTolerantValue( const tolerant = normalizeTolerantValue(
source.tolerant ?? source.tolerant ??
source.endure ?? source.endure ??
source.isEndure ?? source.isEndure ??
source.tolerable ?? source.tolerable ??
source.isTolerable ?? source.isTolerable ??
source.status ?? source.status ??
source.pointStatus ?? source.pointStatus ??
source.result ?? source.result ??
source.state source.state
) )
return { return {
@@ -382,25 +243,21 @@ const normalizeTolerantPoint = (source: Record<string, any>): NormalizedTolerant
: tolerant === 1 : tolerant === 1
? 'pass' ? 'pass'
: normalizeStatus( : normalizeStatus(
source.tolerant ?? source.tolerant ??
source.endure ?? source.endure ??
source.isEndure ?? source.isEndure ??
source.tolerable ?? source.tolerable ??
source.isTolerable ?? source.isTolerable ??
source.status ?? source.status ??
source.pointStatus ?? source.pointStatus ??
source.result ?? source.result ??
source.state source.state
) )
} }
} }
const getStatusText = (status: ChartPointStatus) => { const getStatusText = (status: ChartPointStatus) => {
if (status === 'fail') { return status === 'fail' ? '不耐受' : '耐受'
return '不耐受'
}
return '耐受'
} }
const normalizePoint = (source: Record<string, any>): ChartPoint | null => { const normalizePoint = (source: Record<string, any>): ChartPoint | null => {
@@ -416,6 +273,65 @@ const normalizePoint = (source: Record<string, any>): ChartPoint | null => {
} }
} }
const normalizeCurveData = (source: unknown) => {
if (!Array.isArray(source)) {
return [] as Array<[number, number]>
}
return source
.map(item => {
if (Array.isArray(item) && item.length >= 2) {
const duration = Number(item[0])
const residualVoltage = Number(item[1])
if (Number.isFinite(duration) && Number.isFinite(residualVoltage) && duration > 0) {
return [duration, residualVoltage] as [number, number]
}
}
if (item && typeof item === 'object') {
const record = item as Record<string, any>
const tolerant = normalizeTolerantValue(record.tolerant)
if (tolerant !== null && tolerant !== 2) {
return null
}
const duration = normalizeDuration(record)
const residualVoltage = normalizeResidualVoltageValue(record)
if (duration !== null && residualVoltage !== null && duration > 0) {
return [duration, residualVoltage] as [number, number]
}
}
return null
})
.filter((item): item is [number, number] => !!item)
}
const extractCurveData = (payload: any) => {
if (!payload) {
return [] as Array<[number, number]>
}
const candidates = [
payload,
payload?.data,
payload?.data?.records,
payload?.data?.points,
payload?.points,
payload?.records,
payload?.list
]
for (const candidate of candidates) {
const normalized = normalizeCurveData(candidate)
if (normalized.length) {
return normalized
}
}
return [] as Array<[number, number]>
}
const extractCharacteristicCurvePoints = (payload: any) => { const extractCharacteristicCurvePoints = (payload: any) => {
const result: Array<[number, number]> = [] const result: Array<[number, number]> = []
const seen = new Set<string>() const seen = new Set<string>()
@@ -494,63 +410,288 @@ const extractPoints = (payload: any) => {
return result return result
} }
watch( const drawCharacteristicCurve = async () => {
() => props.webMsgSend, const freqConverterId = props.selectedMapping?.freqConverterId
newValue => { if (!freqConverterId) {
if (!newValue) { ElMessage.warning('未获取到变频器ID')
return return
} }
const nextPoints = extractPoints(newValue) curveLoading.value = true
if (!nextPoints.length) {
const nextCurvePoints = extractCharacteristicCurvePoints(newValue)
if (nextCurvePoints.length) {
characteristicCurveData.value = nextCurvePoints
}
return
}
const existingPointMap = new Map( try {
chartPoints.value.map(item => [`${item.duration}|${item.residualVoltage}`, item] as const) const result = await getFreqConverterSCurve({
) converterId: freqConverterId
})
nextPoints.forEach(item => { const normalizedCurveData = extractCurveData(result)
const key = `${item.duration}|${item.residualVoltage}`
existingPointMap.set(key, item)
})
chartPoints.value = Array.from(existingPointMap.values()) if (!normalizedCurveData.length) {
const nextCurvePoints = extractCharacteristicCurvePoints(newValue)
if (nextCurvePoints.length) {
characteristicCurveData.value = nextCurvePoints
}
},
{deep: true}
)
watch(
() => props.resultData,
newValue => {
if (!newValue) {
return
}
chartPoints.value = extractPoints(newValue)
const nextCurvePoints = extractCharacteristicCurvePoints(newValue)
if (nextCurvePoints.length) {
characteristicCurveData.value = nextCurvePoints
}
},
{deep: true, immediate: true}
)
watch(
() => props.selectedMapping,
() => {
chartPoints.value = []
characteristicCurveData.value = [] characteristicCurveData.value = []
ElMessage.warning('未获取到特性曲线数据')
return
} }
characteristicCurveData.value = normalizedCurveData
} catch (error) {
console.error('draw characteristic curve failed:', error)
characteristicCurveData.value = []
} finally {
curveLoading.value = false
}
}
const downloadChartImage = async () => {
if (!hasChartData.value) {
ElMessage.warning('暂无可下载的图表数据')
return
}
await nextTick()
const chartInstance = chartRef.value?.getChartInstance?.()
if (!chartInstance) {
ElMessage.warning('图表尚未渲染完成')
return
}
const imageUrl = chartInstance.getDataURL({
type: 'png',
pixelRatio: 2,
backgroundColor: '#ffffff'
})
triggerDownload(imageUrl, buildFileName('变频器耐受图', 'png'))
ElMessage.success('图表图片导出成功')
}
const exportChartData = () => {
if (!hasChartData.value) {
ElMessage.warning('暂无可导出的点位数据')
return
}
const workbook = XLSX.utils.book_new()
if (sortedChartPoints.value.length) {
const pointSheet = XLSX.utils.json_to_sheet(
sortedChartPoints.value.map((item, index) => ({
序号: index + 1,
持续时间_s: item.duration,
暂降幅值_pct: item.residualVoltage,
状态: getStatusText(item.status)
}))
)
XLSX.utils.book_append_sheet(workbook, pointSheet, '暂降点')
}
if (sortedCharacteristicCurveData.value.length) {
const curveSheet = XLSX.utils.json_to_sheet(
sortedCharacteristicCurveData.value.map((item, index) => ({
序号: index + 1,
持续时间_s: item[0],
暂降幅值_pct: item[1]
}))
)
XLSX.utils.book_append_sheet(workbook, curveSheet, '特性曲线')
}
const workbookBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' })
const blob = new Blob([workbookBuffer], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
})
const blobUrl = window.URL.createObjectURL(blob)
try {
triggerDownload(blobUrl, buildFileName('变频器耐受图数据', 'xlsx'))
ElMessage.success('点位数据导出成功')
} finally {
window.URL.revokeObjectURL(blobUrl)
}
}
const chartOptions = computed(() => {
return {
title: {
text: ''
},
grid: {
top: 30,
left: 48,
right: 22,
bottom: 52
},
tooltip: {
trigger: 'item',
formatter(params: any) {
const rawValue = Array.isArray(params.value) ? params.value : params.value?.value
if (!Array.isArray(rawValue)) {
return ''
}
const [duration, residualVoltage, statusText] = rawValue
return [
`类型: ${params.seriesName}`,
`持续时间: ${duration} s`,
`残余电压: ${residualVoltage} %`,
...(statusText ? [`状态: ${statusText}`] : [])
].join('<br/>')
}
},
legend: {
top: 0,
right: 0,
data: ['特性测试曲线']
},
xAxis: {
type: 'log',
name: '持续时间(s)',
nameLocation: 'middle',
nameGap: 34,
min: xAxisMin.value,
max: xAxisMax.value,
logBase: 10,
minorTick: {
show: true,
splitNumber: 10
},
minorSplitLine: {
show: false,
lineStyle: {
color: '#e8edf6'
}
},
splitLine: {
show: true,
lineStyle: {
color: '#cfd8e6'
}
},
axisLabel: {
formatter(value: number) {
return formatLogDurationLabel(value)
}
}
},
yAxis: {
type: 'value',
name: '暂降幅值',
min: 0,
max: 100,
interval: 10,
minorTick: {
show: true,
splitNumber: 2
},
minorSplitLine: {
show: true,
lineStyle: {
color: '#e8edf6'
}
},
splitLine: {
lineStyle: {
color: '#cfd8e6'
}
},
axisLabel: {
formatter(value: number) {
return `${value}%`
}
}
},
dataZoom: [],
series: [
{
name: '特性测试曲线',
type: 'line',
smooth: true,
showSymbol: true,
symbolSize: 7,
lineStyle: {
color: '#ff2a2a',
width: 3
},
itemStyle: {
color: '#ff2a2a'
},
data: characteristicCurveData.value.map(item => [item[0], item[1], '特性曲线'])
},
{
name: '暂降点',
type: 'scatter',
symbolSize: 10,
data: chartPoints.value.map(item => ({
value: [item.duration, item.residualVoltage, getStatusText(item.status)],
itemStyle: {
color: STATUS_COLOR_MAP[item.status]
}
}))
}
],
options: {
animation: false
}
}
})
watch(
() => props.webMsgSend,
newValue => {
if (!newValue) {
return
}
const nextPoints = extractPoints(newValue)
if (!nextPoints.length) {
const nextCurvePoints = extractCharacteristicCurvePoints(newValue)
if (nextCurvePoints.length) {
characteristicCurveData.value = nextCurvePoints
}
return
}
const existingPointMap = new Map(
chartPoints.value.map(item => [`${item.duration}|${item.residualVoltage}`, item] as const)
)
nextPoints.forEach(item => {
const key = `${item.duration}|${item.residualVoltage}`
existingPointMap.set(key, item)
})
chartPoints.value = Array.from(existingPointMap.values())
const nextCurvePoints = extractCharacteristicCurvePoints(newValue)
if (nextCurvePoints.length) {
characteristicCurveData.value = nextCurvePoints
}
},
{ deep: true }
)
watch(
() => props.resultData,
newValue => {
if (!newValue) {
return
}
chartPoints.value = extractPoints(newValue)
const nextCurvePoints = extractCharacteristicCurvePoints(newValue)
if (nextCurvePoints.length) {
characteristicCurveData.value = nextCurvePoints
}
},
{ deep: true, immediate: true }
)
watch(
() => props.selectedMapping,
() => {
chartPoints.value = []
characteristicCurveData.value = []
}
) )
</script> </script>
@@ -574,6 +715,14 @@ watch(
gap: 12px; gap: 12px;
} }
.header-actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
flex-wrap: wrap;
}
.card-header-main { .card-header-main {
min-width: 0; min-width: 0;
} }