feat:设备运行参数分析-树形改成多选、右侧显示多组图表

main
黄伟杰 3 weeks ago
parent 83b9717df2
commit da1c48c7eb

@ -11,18 +11,30 @@
/> />
<div class="mt-12px"> <div class="mt-12px">
<el-tree <el-tree
ref="treeRef" v-loading="treeLoading" :data="treeData" :props="treeProps" node-key="id" ref="treeRef"
highlight-current @node-click="handleTreeNodeClick" /> v-loading="treeLoading"
:data="treeData"
:props="treeProps"
node-key="id"
show-checkbox
check-strictly
@check="handleTreeCheck"
/>
</div> </div>
</ContentWrap> </ContentWrap>
</el-col> </el-col>
<el-col :span="18" :xs="24"> <el-col :span="18" :xs="24">
<ContentWrap> <div class="analysis-groups">
<ContentWrap v-if="!selectedGroups.length">
<el-empty :description="t('DataCollection.DeviceParamAnalysis.emptySelectNodeDescription')" />
</ContentWrap>
<ContentWrap v-for="group in selectedGroups" :key="group.id">
<el-form class="-mb-15px device-param-analysis-form" :inline="true" label-width="auto"> <el-form class="-mb-15px device-param-analysis-form" :inline="true" label-width="auto">
<el-form-item :label="t('DataCollection.DeviceParamAnalysis.formTimeLabel')"> <el-form-item :label="t('DataCollection.DeviceParamAnalysis.formTimeLabel')">
<el-date-picker <el-date-picker
v-model="dateRange" v-model="group.dateRange"
type="daterange" type="daterange"
:start-placeholder="t('DataCollection.DeviceParamAnalysis.formTimeStartPlaceholder')" :start-placeholder="t('DataCollection.DeviceParamAnalysis.formTimeStartPlaceholder')"
:end-placeholder="t('DataCollection.DeviceParamAnalysis.formTimeEndPlaceholder')" :end-placeholder="t('DataCollection.DeviceParamAnalysis.formTimeEndPlaceholder')"
@ -33,32 +45,32 @@ ref="treeRef" v-loading="treeLoading" :data="treeData" :props="treeProps" node-k
/> />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button :disabled="!selectedParam" @click="handleQuery"> <el-button :disabled="group.chartLoading" @click="handleQuery(group)">
<Icon icon="ep:search" class="mr-5px" /> <Icon icon="ep:search" class="mr-5px" />
{{ t('DataCollection.DeviceParamAnalysis.searchButtonText') }} {{ t('DataCollection.DeviceParamAnalysis.searchButtonText') }}
</el-button> </el-button>
<el-button :disabled="!selectedParam" @click="resetQuery"> <el-button :disabled="group.chartLoading" @click="resetQuery(group)">
<Icon icon="ep:refresh" class="mr-5px" /> <Icon icon="ep:refresh" class="mr-5px" />
{{ t('DataCollection.DeviceParamAnalysis.resetButtonText') }} {{ t('DataCollection.DeviceParamAnalysis.resetButtonText') }}
</el-button> </el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</ContentWrap>
<ContentWrap> <el-alert :title="buildParamTitle(group.param)" type="info" :closable="false" class="mb-12px" />
<el-alert v-if="selectedParam" :title="selectedParamTitle" type="info" :closable="false" class="mb-12px" /> <div v-loading="group.chartLoading">
<div v-loading="chartLoading">
<el-empty <el-empty
v-if="chartState === 'empty'" v-if="group.chartState === 'empty'"
:description="t('DataCollection.DeviceParamAnalysis.emptyDescription')" :description="t('DataCollection.DeviceParamAnalysis.emptyDescription')"
/> />
<el-empty <EChart
v-else-if="!selectedParam" v-else-if="group.chartState === 'ready'"
:description="t('DataCollection.DeviceParamAnalysis.emptySelectNodeDescription')" :key="group.chartRenderKey"
:options="buildChartOption(group)"
height="360px"
/> />
<EChart v-else-if="chartState === 'ready'" :key="chartRenderKey" :options="chartOption" height="70vh" />
</div> </div>
</ContentWrap> </ContentWrap>
</div>
</el-col> </el-col>
</el-row> </el-row>
</template> </template>
@ -87,6 +99,7 @@ type DeviceTreeNode = {
paramKey?: string paramKey?: string
unit?: string unit?: string
paramCount?: number paramCount?: number
disabled?: boolean
} }
type ApiTreeParameter = { type ApiTreeParameter = {
@ -116,27 +129,15 @@ const message = useMessage()
const treeRef = ref() const treeRef = ref()
const treeLoading = ref(false) const treeLoading = ref(false)
const keyword = ref('') const keyword = ref('')
const treeProps = { children: 'children', label: 'label' } const treeProps = { children: 'children', label: 'label', disabled: 'disabled' }
const treeData = ref<DeviceTreeNode[]>([]) const treeData = ref<DeviceTreeNode[]>([])
const selectedDeviceId = ref<number | undefined>(undefined)
const selectedModelId = ref<number | undefined>(undefined)
const selectedParam = ref<DeviceTreeNode | null>(null)
const chartLoading = ref(false)
const chartState = ref<'idle' | 'loading' | 'empty' | 'ready'>('idle')
const chartXAxis = ref<string[]>([])
const chartSeries = ref<{ name: string; data: Array<number | null> }[]>([])
const chartRenderKey = ref(0)
const buildDefaultDateRange = (): [string, string] => { const buildDefaultDateRange = (): [string, string] => {
const end = dayjs().endOf('day') const end = dayjs().endOf('day')
const start = end.subtract(6, 'day').startOf('day') const start = end.subtract(6, 'day').startOf('day')
return [start.format('YYYY-MM-DD'), end.format('YYYY-MM-DD')] return [start.format('YYYY-MM-DD'), end.format('YYYY-MM-DD')]
} }
const dateRange = ref<[string, string]>(buildDefaultDateRange())
const dateShortcuts = [ const dateShortcuts = [
{ {
text: t('DataCollection.DeviceParamAnalysis.shortcutLast7Days'), text: t('DataCollection.DeviceParamAnalysis.shortcutLast7Days'),
@ -172,18 +173,67 @@ const dateShortcuts = [
} }
] ]
const selectedParamTitle = computed(() => { type ChartState = 'idle' | 'loading' | 'empty' | 'ready'
const param = selectedParam.value
if (!param) return '' type SelectedGroup = {
id: string
param: DeviceTreeNode
deviceId?: number
modelId?: number
dateRange: [string, string]
requestSeq: number
chartLoading: boolean
chartState: ChartState
chartXAxis: string[]
chartSeries: Array<{ name: string; data: Array<number | null> }>
chartRenderKey: number
}
const selectedGroups = ref<SelectedGroup[]>([])
let keywordTimer: number | undefined
const handleKeywordChange = () => {
if (keywordTimer) window.clearTimeout(keywordTimer)
keywordTimer = window.setTimeout(() => {
keywordTimer = undefined
loadTree()
}, 300)
}
const toFiniteId = (value: any): number | undefined => {
if (typeof value === 'number') return Number.isFinite(value) ? value : undefined
if (typeof value === 'string') {
if (/^\d+$/.test(value.trim())) return Number(value)
const n = Number(value)
return Number.isFinite(n) ? n : undefined
}
return undefined
}
const toNodeIds = (node: DeviceTreeNode): { deviceId?: number; modelId?: number } => {
const modelId = typeof node.modelId === 'number' && Number.isFinite(node.modelId) ? node.modelId : undefined
const deviceId = typeof node.deviceId === 'number' && Number.isFinite(node.deviceId) ? node.deviceId : undefined
if (typeof deviceId === 'number') return { deviceId, modelId }
const parts = String(node.id ?? '').split('-').filter(Boolean)
const last = parts.length ? parts[parts.length - 1] : undefined
const parsed = toFiniteId(last)
if (typeof parsed !== 'number' || !Number.isFinite(parsed) || parsed <= 0) {
return { modelId }
}
return { deviceId: parsed, modelId }
}
const buildParamTitle = (param: DeviceTreeNode) => {
const unitText = param.unit ? ` (${param.unit})` : '' const unitText = param.unit ? ` (${param.unit})` : ''
return t('DataCollection.DeviceParamAnalysis.selectedParamTitle', { return t('DataCollection.DeviceParamAnalysis.selectedParamTitle', {
label: param.label, label: param.label,
unit: unitText unit: unitText
}) })
}) }
const chartOption = computed<EChartsOption>(() => { const buildChartOption = (group: SelectedGroup): EChartsOption => {
const unit = selectedParam.value?.unit const unit = group.param?.unit
return { return {
tooltip: { trigger: 'axis' }, tooltip: { trigger: 'axis' },
legend: { top: 10, left: 'center', type: 'scroll' }, legend: { top: 10, left: 'center', type: 'scroll' },
@ -191,13 +241,13 @@ const chartOption = computed<EChartsOption>(() => {
xAxis: { xAxis: {
type: 'category', type: 'category',
boundaryGap: false, boundaryGap: false,
data: chartXAxis.value data: group.chartXAxis
}, },
yAxis: { yAxis: {
type: 'value', type: 'value',
name: unit || '' name: unit || ''
}, },
series: chartSeries.value.map((s) => ({ series: group.chartSeries.map((s) => ({
type: 'line', type: 'line',
name: s.name, name: s.name,
smooth: false, smooth: false,
@ -205,25 +255,6 @@ const chartOption = computed<EChartsOption>(() => {
data: s.data data: s.data
})) }))
} }
})
let keywordTimer: number | undefined
const handleKeywordChange = () => {
if (keywordTimer) window.clearTimeout(keywordTimer)
keywordTimer = window.setTimeout(() => {
keywordTimer = undefined
loadTree()
}, 300)
}
const toFiniteId = (value: any): number | undefined => {
if (typeof value === 'number') return Number.isFinite(value) ? value : undefined
if (typeof value === 'string') {
if (/^\d+$/.test(value.trim())) return Number(value)
const n = Number(value)
return Number.isFinite(n) ? n : undefined
}
return undefined
} }
const buildTreeFromApi = (orgs: ApiTreeOrg[]): DeviceTreeNode[] => { const buildTreeFromApi = (orgs: ApiTreeOrg[]): DeviceTreeNode[] => {
@ -258,7 +289,8 @@ const buildTreeFromApi = (orgs: ApiTreeOrg[]): DeviceTreeNode[] => {
deviceId: toFiniteId(eq.id), deviceId: toFiniteId(eq.id),
modelId: toFiniteId(p.id), modelId: toFiniteId(p.id),
paramKey: p?.code ? String(p.code) : undefined, paramKey: p?.code ? String(p.code) : undefined,
unit: p?.unit ? String(p.unit) : undefined unit: p?.unit ? String(p.unit) : undefined,
disabled: false
})) }))
return { return {
@ -267,7 +299,8 @@ const buildTreeFromApi = (orgs: ApiTreeOrg[]): DeviceTreeNode[] => {
type: 'device', type: 'device',
deviceId: toFiniteId(eq.id), deviceId: toFiniteId(eq.id),
paramCount: params.length, paramCount: params.length,
children: paramNodes.length ? paramNodes : undefined children: paramNodes.length ? paramNodes : undefined,
disabled: true
} }
}) })
@ -276,7 +309,8 @@ const buildTreeFromApi = (orgs: ApiTreeOrg[]): DeviceTreeNode[] => {
id: `org-${org.id}`, id: `org-${org.id}`,
label: org?.name ?? String(org?.id ?? ''), label: org?.name ?? String(org?.id ?? ''),
type: 'device', type: 'device',
children: children.length ? children : undefined children: children.length ? children : undefined,
disabled: true
} }
} }
@ -298,12 +332,8 @@ const loadTree = async () => {
treeData.value = buildTreeFromApi(extractApiOrgs(res)) treeData.value = buildTreeFromApi(extractApiOrgs(res))
if (keyword.value) { if (keyword.value) {
treeRef.value?.setCurrentKey?.(undefined) treeRef.value?.setCurrentKey?.(undefined)
selectedParam.value = null treeRef.value?.setCheckedKeys?.([])
selectedDeviceId.value = undefined selectedGroups.value = []
selectedModelId.value = undefined
chartState.value = 'idle'
chartXAxis.value = []
chartSeries.value = []
} }
} catch { } catch {
message.error(t('DataCollection.DeviceParamAnalysis.messageLoadTreeFailed')) message.error(t('DataCollection.DeviceParamAnalysis.messageLoadTreeFailed'))
@ -312,16 +342,16 @@ const loadTree = async () => {
} }
} }
const ensureDateRange = () => { const ensureDateRange = (group: SelectedGroup) => {
if (!dateRange.value || dateRange.value.length !== 2) { if (!group.dateRange || group.dateRange.length !== 2) {
dateRange.value = buildDefaultDateRange() group.dateRange = buildDefaultDateRange()
} }
} }
const resetChartData = () => { const resetChartData = (group: SelectedGroup) => {
chartXAxis.value = [] group.chartXAxis = []
chartSeries.value = [] group.chartSeries = []
chartRenderKey.value++ group.chartRenderKey++
} }
const toNumber = (value: any) => { const toNumber = (value: any) => {
@ -339,37 +369,48 @@ const toNumber = (value: any) => {
return 0 return 0
} }
const fetchChart = async () => { const extractChartRows = (res: any): Record<string, any>[] => {
const param = selectedParam.value if (Array.isArray(res)) return res as Record<string, any>[]
if (typeof selectedDeviceId.value !== 'number' || !Number.isFinite(selectedDeviceId.value)) return if (Array.isArray(res?.data)) return res.data as Record<string, any>[]
if (Array.isArray(res?.result)) return res.result as Record<string, any>[]
if (Array.isArray(res?.list)) return res.list as Record<string, any>[]
return []
}
const fetchGroupChart = async (group: SelectedGroup, notify: boolean) => {
if (typeof group.deviceId !== 'number' || !Number.isFinite(group.deviceId)) return
ensureDateRange() ensureDateRange(group)
const [start, end] = dateRange.value const [start, end] = group.dateRange
if (!start || !end) return if (!start || !end) return
const buildDateTime = (date: string, suffix: string) => { const buildDateTime = (date: string, suffix: string) => {
return date.includes(':') ? date : `${date} ${suffix}` return date.includes(':') ? date : `${date} ${suffix}`
} }
chartState.value = 'loading' const requestSeq = ++group.requestSeq
chartLoading.value = true group.chartState = 'loading'
resetChartData() group.chartLoading = true
resetChartData(group)
try { try {
const req: Record<string, any> = { const req: Record<string, any> = {
deviceId: selectedDeviceId.value, deviceId: group.deviceId,
collectionStartTime: buildDateTime(start, '00:00:00'), collectionStartTime: buildDateTime(start, '00:00:00'),
collectionEndTime: buildDateTime(end, '23:59:59') collectionEndTime: buildDateTime(end, '23:59:59')
} }
if (typeof selectedModelId.value === 'number' && Number.isFinite(selectedModelId.value)) { if (typeof group.modelId === 'number' && Number.isFinite(group.modelId)) {
req.modelId = selectedModelId.value req.modelId = group.modelId
} }
const data = await DeviceModelAttributeApi.operationAnalysisDetails(req as any) const data = await DeviceModelAttributeApi.operationAnalysisDetails(req as any)
const rows: Record<string, any>[] = Array.isArray(data) ? data : [] if (requestSeq !== group.requestSeq) return
const rows: Record<string, any>[] = extractChartRows(data)
if (!rows.length) { if (!rows.length) {
chartXAxis.value = [] group.chartXAxis = []
chartSeries.value = [] group.chartSeries = []
chartState.value = 'empty' group.chartState = 'empty'
if (notify) {
message.warning(t('DataCollection.DeviceParamAnalysis.messageNodeNoParams')) message.warning(t('DataCollection.DeviceParamAnalysis.messageNodeNoParams'))
}
return return
} }
@ -380,7 +421,8 @@ const fetchChart = async () => {
const sortedRows = [...rows].sort((a, b) => const sortedRows = [...rows].sort((a, b) =>
String(a?.[xKey] ?? '').localeCompare(String(b?.[xKey] ?? '')) String(a?.[xKey] ?? '').localeCompare(String(b?.[xKey] ?? ''))
) )
chartXAxis.value = sortedRows.map((r) => String(r?.[xKey] ?? '')) if (requestSeq !== group.requestSeq) return
group.chartXAxis = sortedRows.map((r) => String(r?.[xKey] ?? ''))
const seriesKeys = keys.filter((k) => k !== xKey) const seriesKeys = keys.filter((k) => k !== xKey)
const seriesMap = new Map<string, Array<number | null>>() const seriesMap = new Map<string, Array<number | null>>()
@ -407,77 +449,76 @@ const fetchChart = async () => {
}) })
}) })
chartSeries.value = Array.from(seriesMap.entries()).map(([name, data]) => ({ name, data })) group.chartSeries = Array.from(seriesMap.entries()).map(([name, data]) => ({ name, data }))
} else { } else {
chartXAxis.value = keys group.chartXAxis = keys
const values = keys.map((k) => toNumber(firstRow[k])) const values = keys.map((k) => toNumber(firstRow[k]))
chartSeries.value = [ group.chartSeries = [
{ name: param?.label || t('DataCollection.DeviceParamAnalysis.defaultSeriesName'), data: values } { name: group.param?.label || t('DataCollection.DeviceParamAnalysis.defaultSeriesName'), data: values }
] ]
} }
chartState.value = chartXAxis.value.length && chartSeries.value.length ? 'ready' : 'empty' group.chartState = group.chartXAxis.length && group.chartSeries.length ? 'ready' : 'empty'
} catch { } catch {
if (notify) {
message.error(t('DataCollection.DeviceParamAnalysis.messageFetchChartFailed')) message.error(t('DataCollection.DeviceParamAnalysis.messageFetchChartFailed'))
chartState.value = 'idle' }
group.chartState = 'idle'
} finally { } finally {
chartLoading.value = false if (requestSeq === group.requestSeq) {
group.chartLoading = false
} }
}
const handleTreeNodeClick = async (data: DeviceTreeNode) => {
if (keywordTimer) {
window.clearTimeout(keywordTimer)
keywordTimer = undefined
} }
}
const hasChildren = Array.isArray(data?.children) && data.children.length > 0 const syncGroupsByCheckedNodes = (checkedNodes: DeviceTreeNode[]) => {
if (hasChildren) { const ids = checkedNodes.map((n) => String(n.id))
return const existingIds = new Set(selectedGroups.value.map((g) => g.id))
} const nextIds = new Set(ids)
selectedGroups.value = selectedGroups.value.filter((g) => nextIds.has(g.id))
const isEmptyOrgNode = typeof data?.id === 'string' && data.id.startsWith('org-') const toAdd = checkedNodes.filter((n) => !existingIds.has(String(n.id)))
const isEquipmentNode = typeof data?.id === 'string' && data.id.startsWith('equipment-') const addedGroups: SelectedGroup[] = []
if (isEquipmentNode && (data.paramCount ?? 0) <= 0 || isEmptyOrgNode) { for (const node of toAdd) {
selectedParam.value = data const { deviceId, modelId } = toNodeIds(node)
selectedDeviceId.value = undefined if (typeof deviceId !== 'number') {
selectedModelId.value = undefined continue
chartState.value = 'empty'
resetChartData()
message.warning(t('DataCollection.DeviceParamAnalysis.messageDeviceNoParams'))
return
} }
const group: SelectedGroup = {
const toNodeIds = (node: DeviceTreeNode): { deviceId?: number; modelId?: number } => { id: String(node.id),
const modelId = typeof node.modelId === 'number' && Number.isFinite(node.modelId) ? node.modelId : undefined param: node,
const deviceId = typeof node.deviceId === 'number' && Number.isFinite(node.deviceId) ? node.deviceId : undefined deviceId,
if (typeof deviceId === 'number') return { deviceId, modelId } modelId,
dateRange: buildDefaultDateRange(),
const parts = String(node.id ?? '').split('-').filter(Boolean) requestSeq: 0,
const last = parts.length ? parts[parts.length - 1] : undefined chartLoading: false,
const parsed = toFiniteId(last) chartState: 'idle',
if (typeof parsed !== 'number' || !Number.isFinite(parsed) || parsed <= 0) { chartXAxis: [],
return { modelId } chartSeries: [],
chartRenderKey: 0
} }
return { deviceId: parsed, modelId } selectedGroups.value.push(group)
addedGroups.push(group)
} }
return addedGroups
}
const { deviceId, modelId } = toNodeIds(data) const handleTreeCheck = async () => {
selectedParam.value = data const checkedNodes = (treeRef.value?.getCheckedNodes?.(true) ?? []) as DeviceTreeNode[]
selectedDeviceId.value = deviceId const addedGroups = syncGroupsByCheckedNodes(checkedNodes)
selectedModelId.value = modelId const needFetchGroups = selectedGroups.value.filter((g) => g.chartState === 'idle' && !g.chartLoading)
dateRange.value = buildDefaultDateRange() const groups = [...new Set([...addedGroups, ...needFetchGroups])]
await fetchChart() await Promise.allSettled(groups.map((g) => fetchGroupChart(g, false)))
} }
const handleQuery = async () => { const handleQuery = async (group: SelectedGroup) => {
await fetchChart() await fetchGroupChart(group, true)
} }
const resetQuery = async () => { const resetQuery = async (group: SelectedGroup) => {
dateRange.value = buildDefaultDateRange() group.dateRange = buildDefaultDateRange()
await fetchChart() await fetchGroupChart(group, true)
} }
onMounted(async () => { onMounted(async () => {
@ -486,10 +527,19 @@ onMounted(async () => {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.device-param-analysis-form{
margin-bottom: 0px
}
.device-param-analysis-form :deep(.el-form-item__label) { .device-param-analysis-form :deep(.el-form-item__label) {
min-width: 100px; min-width: 100px;
} }
.analysis-groups {
display: flex;
flex-direction: column;
gap: 12px;
}
:deep(.el-tree) { :deep(.el-tree) {
max-height: calc(100vh - 280px); max-height: calc(100vh - 280px);
overflow: auto; overflow: auto;

Loading…
Cancel
Save