diff --git a/src/views/iot/deviceParamAnalysis/index.vue b/src/views/iot/deviceParamAnalysis/index.vue index cf3bd5e0..cb36eed0 100644 --- a/src/views/iot/deviceParamAnalysis/index.vue +++ b/src/views/iot/deviceParamAnalysis/index.vue @@ -11,54 +11,66 @@ />
+ ref="treeRef" + v-loading="treeLoading" + :data="treeData" + :props="treeProps" + node-key="id" + show-checkbox + check-strictly + @check="handleTreeCheck" + />
- - - - + + + + + + + + + + + + + {{ t('DataCollection.DeviceParamAnalysis.searchButtonText') }} + + + + {{ t('DataCollection.DeviceParamAnalysis.resetButtonText') }} + + + + + +
+ - - - - - {{ t('DataCollection.DeviceParamAnalysis.searchButtonText') }} - - - - {{ t('DataCollection.DeviceParamAnalysis.resetButtonText') }} - - - - - - - -
- - - -
-
+ +
+
+
@@ -87,6 +99,7 @@ type DeviceTreeNode = { paramKey?: string unit?: string paramCount?: number + disabled?: boolean } type ApiTreeParameter = { @@ -116,27 +129,15 @@ const message = useMessage() const treeRef = ref() const treeLoading = ref(false) const keyword = ref('') -const treeProps = { children: 'children', label: 'label' } +const treeProps = { children: 'children', label: 'label', disabled: 'disabled' } const treeData = ref([]) -const selectedDeviceId = ref(undefined) -const selectedModelId = ref(undefined) - -const selectedParam = ref(null) -const chartLoading = ref(false) -const chartState = ref<'idle' | 'loading' | 'empty' | 'ready'>('idle') -const chartXAxis = ref([]) -const chartSeries = ref<{ name: string; data: Array }[]>([]) -const chartRenderKey = ref(0) - const buildDefaultDateRange = (): [string, string] => { const end = dayjs().endOf('day') const start = end.subtract(6, 'day').startOf('day') return [start.format('YYYY-MM-DD'), end.format('YYYY-MM-DD')] } -const dateRange = ref<[string, string]>(buildDefaultDateRange()) - const dateShortcuts = [ { text: t('DataCollection.DeviceParamAnalysis.shortcutLast7Days'), @@ -172,18 +173,67 @@ const dateShortcuts = [ } ] -const selectedParamTitle = computed(() => { - const param = selectedParam.value - if (!param) return '' +type ChartState = 'idle' | 'loading' | 'empty' | 'ready' + +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 }> + chartRenderKey: number +} + +const selectedGroups = ref([]) + +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})` : '' return t('DataCollection.DeviceParamAnalysis.selectedParamTitle', { label: param.label, unit: unitText }) -}) +} -const chartOption = computed(() => { - const unit = selectedParam.value?.unit +const buildChartOption = (group: SelectedGroup): EChartsOption => { + const unit = group.param?.unit return { tooltip: { trigger: 'axis' }, legend: { top: 10, left: 'center', type: 'scroll' }, @@ -191,13 +241,13 @@ const chartOption = computed(() => { xAxis: { type: 'category', boundaryGap: false, - data: chartXAxis.value + data: group.chartXAxis }, yAxis: { type: 'value', name: unit || '' }, - series: chartSeries.value.map((s) => ({ + series: group.chartSeries.map((s) => ({ type: 'line', name: s.name, smooth: false, @@ -205,25 +255,6 @@ const chartOption = computed(() => { 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[] => { @@ -258,7 +289,8 @@ const buildTreeFromApi = (orgs: ApiTreeOrg[]): DeviceTreeNode[] => { deviceId: toFiniteId(eq.id), modelId: toFiniteId(p.id), paramKey: p?.code ? String(p.code) : undefined, - unit: p?.unit ? String(p.unit) : undefined + unit: p?.unit ? String(p.unit) : undefined, + disabled: false })) return { @@ -267,7 +299,8 @@ const buildTreeFromApi = (orgs: ApiTreeOrg[]): DeviceTreeNode[] => { type: 'device', deviceId: toFiniteId(eq.id), 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}`, label: org?.name ?? String(org?.id ?? ''), 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)) if (keyword.value) { treeRef.value?.setCurrentKey?.(undefined) - selectedParam.value = null - selectedDeviceId.value = undefined - selectedModelId.value = undefined - chartState.value = 'idle' - chartXAxis.value = [] - chartSeries.value = [] + treeRef.value?.setCheckedKeys?.([]) + selectedGroups.value = [] } } catch { message.error(t('DataCollection.DeviceParamAnalysis.messageLoadTreeFailed')) @@ -312,16 +342,16 @@ const loadTree = async () => { } } -const ensureDateRange = () => { - if (!dateRange.value || dateRange.value.length !== 2) { - dateRange.value = buildDefaultDateRange() +const ensureDateRange = (group: SelectedGroup) => { + if (!group.dateRange || group.dateRange.length !== 2) { + group.dateRange = buildDefaultDateRange() } } -const resetChartData = () => { - chartXAxis.value = [] - chartSeries.value = [] - chartRenderKey.value++ +const resetChartData = (group: SelectedGroup) => { + group.chartXAxis = [] + group.chartSeries = [] + group.chartRenderKey++ } const toNumber = (value: any) => { @@ -339,37 +369,48 @@ const toNumber = (value: any) => { return 0 } -const fetchChart = async () => { - const param = selectedParam.value - if (typeof selectedDeviceId.value !== 'number' || !Number.isFinite(selectedDeviceId.value)) return +const extractChartRows = (res: any): Record[] => { + if (Array.isArray(res)) return res as Record[] + if (Array.isArray(res?.data)) return res.data as Record[] + if (Array.isArray(res?.result)) return res.result as Record[] + if (Array.isArray(res?.list)) return res.list as Record[] + return [] +} + +const fetchGroupChart = async (group: SelectedGroup, notify: boolean) => { + if (typeof group.deviceId !== 'number' || !Number.isFinite(group.deviceId)) return - ensureDateRange() - const [start, end] = dateRange.value + ensureDateRange(group) + const [start, end] = group.dateRange if (!start || !end) return const buildDateTime = (date: string, suffix: string) => { return date.includes(':') ? date : `${date} ${suffix}` } - chartState.value = 'loading' - chartLoading.value = true - resetChartData() + const requestSeq = ++group.requestSeq + group.chartState = 'loading' + group.chartLoading = true + resetChartData(group) try { const req: Record = { - deviceId: selectedDeviceId.value, + deviceId: group.deviceId, collectionStartTime: buildDateTime(start, '00:00:00'), collectionEndTime: buildDateTime(end, '23:59:59') } - if (typeof selectedModelId.value === 'number' && Number.isFinite(selectedModelId.value)) { - req.modelId = selectedModelId.value + if (typeof group.modelId === 'number' && Number.isFinite(group.modelId)) { + req.modelId = group.modelId } const data = await DeviceModelAttributeApi.operationAnalysisDetails(req as any) - const rows: Record[] = Array.isArray(data) ? data : [] + if (requestSeq !== group.requestSeq) return + const rows: Record[] = extractChartRows(data) if (!rows.length) { - chartXAxis.value = [] - chartSeries.value = [] - chartState.value = 'empty' - message.warning(t('DataCollection.DeviceParamAnalysis.messageNodeNoParams')) + group.chartXAxis = [] + group.chartSeries = [] + group.chartState = 'empty' + if (notify) { + message.warning(t('DataCollection.DeviceParamAnalysis.messageNodeNoParams')) + } return } @@ -380,7 +421,8 @@ const fetchChart = async () => { const sortedRows = [...rows].sort((a, b) => 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 seriesMap = new Map>() @@ -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 { - chartXAxis.value = keys + group.chartXAxis = keys const values = keys.map((k) => toNumber(firstRow[k])) - chartSeries.value = [ - { name: param?.label || t('DataCollection.DeviceParamAnalysis.defaultSeriesName'), data: values } + group.chartSeries = [ + { 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 { - message.error(t('DataCollection.DeviceParamAnalysis.messageFetchChartFailed')) - chartState.value = 'idle' + if (notify) { + message.error(t('DataCollection.DeviceParamAnalysis.messageFetchChartFailed')) + } + group.chartState = 'idle' } 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 - if (hasChildren) { - return - } - - - const isEmptyOrgNode = typeof data?.id === 'string' && data.id.startsWith('org-') - const isEquipmentNode = typeof data?.id === 'string' && data.id.startsWith('equipment-') - if (isEquipmentNode && (data.paramCount ?? 0) <= 0 || isEmptyOrgNode) { - selectedParam.value = data - selectedDeviceId.value = undefined - selectedModelId.value = undefined - chartState.value = 'empty' - resetChartData() - message.warning(t('DataCollection.DeviceParamAnalysis.messageDeviceNoParams')) - return - } +const syncGroupsByCheckedNodes = (checkedNodes: DeviceTreeNode[]) => { + const ids = checkedNodes.map((n) => String(n.id)) + const existingIds = new Set(selectedGroups.value.map((g) => g.id)) + const nextIds = new Set(ids) - 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 } + selectedGroups.value = selectedGroups.value.filter((g) => nextIds.has(g.id)) - 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 } + const toAdd = checkedNodes.filter((n) => !existingIds.has(String(n.id))) + const addedGroups: SelectedGroup[] = [] + for (const node of toAdd) { + const { deviceId, modelId } = toNodeIds(node) + if (typeof deviceId !== 'number') { + continue } - return { deviceId: parsed, modelId } + const group: SelectedGroup = { + id: String(node.id), + param: node, + deviceId, + modelId, + dateRange: buildDefaultDateRange(), + requestSeq: 0, + chartLoading: false, + chartState: 'idle', + chartXAxis: [], + chartSeries: [], + chartRenderKey: 0 + } + selectedGroups.value.push(group) + addedGroups.push(group) } + return addedGroups +} - const { deviceId, modelId } = toNodeIds(data) - selectedParam.value = data - selectedDeviceId.value = deviceId - selectedModelId.value = modelId - dateRange.value = buildDefaultDateRange() - await fetchChart() +const handleTreeCheck = async () => { + const checkedNodes = (treeRef.value?.getCheckedNodes?.(true) ?? []) as DeviceTreeNode[] + const addedGroups = syncGroupsByCheckedNodes(checkedNodes) + const needFetchGroups = selectedGroups.value.filter((g) => g.chartState === 'idle' && !g.chartLoading) + const groups = [...new Set([...addedGroups, ...needFetchGroups])] + await Promise.allSettled(groups.map((g) => fetchGroupChart(g, false))) } -const handleQuery = async () => { - await fetchChart() +const handleQuery = async (group: SelectedGroup) => { + await fetchGroupChart(group, true) } -const resetQuery = async () => { - dateRange.value = buildDefaultDateRange() - await fetchChart() +const resetQuery = async (group: SelectedGroup) => { + group.dateRange = buildDefaultDateRange() + await fetchGroupChart(group, true) } onMounted(async () => { @@ -486,10 +527,19 @@ onMounted(async () => {