Commit 6296d46921f7296e5ad598dace5ad44ae34f3f51

Authored by fengwotao
1 parent f1b9a102

feat(src/packages): 图表折线图 新增单设备-属性-历史数据查询图

  1 +/**
  2 + * 特殊处理单设备-属性-历史数据查询组件联动
  3 + */
  4 +import { toRefs } from 'vue'
  5 +import { CreateComponentType } from '@/packages/index.d'
  6 +import { useChartEditStore } from '@/store/modules/chartEditStore/chartEditStore'
  7 +
  8 +// 获取类型
  9 +type ChartEditStoreType = typeof useChartEditStore
  10 +
  11 +// Params 参数修改触发 api 更新图表请求
  12 +export const useChartInteract = (
  13 + chartConfig: CreateComponentType,
  14 + useChartEditStore: ChartEditStoreType,
  15 + param: { [T: string]: any }
  16 +) => {
  17 + const chartEditStore = useChartEditStore()
  18 + const { id } = chartConfig
  19 + const index = chartEditStore.fetchTargetIndex(id)
  20 + const { data } = param
  21 + if (index === -1) return
  22 + chartEditStore.getComponentList.forEach(targetItem => {
  23 + if (targetItem.id === id) {
  24 + const { Params } = toRefs(targetItem.request.requestParams)
  25 + if (Reflect.get(param.data, 'agg') !== 'NONE') {
  26 + Reflect.deleteProperty(Params.value, 'limit')
  27 + }
  28 + Object.keys(data).forEach((item: string) => {
  29 + Params.value[item] = data[item]
  30 + })
  31 + }
  32 + })
  33 +}
... ...
  1 +import { echartOptionProfixHandle, PublicConfigClass } from '@/packages/public'
  2 +import { OverrideLineForDeviceHistoryQueryConfig } from './index'
  3 +import { CreateComponentType } from '@/packages/index.d'
  4 +import cloneDeep from 'lodash/cloneDeep'
  5 +import dataJson from './data.json'
  6 +import { chartInitConfig } from '@/settings/designSetting'
  7 +
  8 +export const includes = ['legend', 'xAxis', 'yAxis', 'grid']
  9 +export const seriesItem = {
  10 + type: 'line',
  11 + label: {
  12 + show: true,
  13 + position: 'top',
  14 + color: '#fff',
  15 + fontSize: 12
  16 + },
  17 + symbolSize: 5, //设定实心点的大小
  18 + itemStyle: {
  19 + color: null,
  20 + borderRadius: 0
  21 + },
  22 + lineStyle: {
  23 + type: 'solid',
  24 + width: 3,
  25 + color: null
  26 + }
  27 +}
  28 +// 其它配置项比如新增(动画)
  29 +const otherConfig = {
  30 + // 轮播动画
  31 + isCarousel: false
  32 +}
  33 +export const option = {
  34 + queryCondition: {
  35 + timeRange: [new Date().getTime() - 86400000, new Date().getTime()], //默认最近一天
  36 + limit: 7,
  37 + agg: 'NONE'
  38 + },
  39 + ...otherConfig,
  40 + tooltip: {
  41 + show: true,
  42 + trigger: 'axis',
  43 + axisPointer: {
  44 + type: 'line'
  45 + }
  46 + },
  47 + xAxis: {
  48 + show: true,
  49 + type: 'category'
  50 + },
  51 + yAxis: {
  52 + show: true,
  53 + type: 'value'
  54 + },
  55 + dataset: { ...dataJson },
  56 + series: [seriesItem, seriesItem]
  57 +}
  58 +
  59 +export default class Config extends PublicConfigClass implements CreateComponentType {
  60 + public key: string = OverrideLineForDeviceHistoryQueryConfig.key
  61 + public chartConfig = cloneDeep(OverrideLineForDeviceHistoryQueryConfig)
  62 + public attr = { ...chartInitConfig, w: 735, h: 435, zIndex: -1 }
  63 + // 图表配置项
  64 + public option = echartOptionProfixHandle(option, includes)
  65 +}
... ...
  1 +<template>
  2 + <!-- Echarts 全局设置 -->
  3 + <global-setting :optionData="optionData"></global-setting>
  4 + <CollapseItem v-for="(item, index) in seriesList" :key="index" :name="`折线图-${index + 1}`" :expanded="true">
  5 + <SettingItemBox name="线条">
  6 + <SettingItem name="宽度">
  7 + <n-input-number
  8 + v-model:value="item.lineStyle.width"
  9 + :min="1"
  10 + :max="100"
  11 + size="small"
  12 + placeholder="自动计算"
  13 + ></n-input-number>
  14 + </SettingItem>
  15 + <SettingItem name="类型">
  16 + <n-select v-model:value="item.lineStyle.type" size="small" :options="lineConf.lineStyle.type"></n-select>
  17 + </SettingItem>
  18 + </SettingItemBox>
  19 + <SettingItemBox name="动画" :alone="true">
  20 + <SettingItem>
  21 + <n-space>
  22 + <n-switch v-model:value="optionData.isCarousel" size="small"></n-switch>
  23 + <n-text>开启<n-text :depth="3">(将自动隐藏图例)</n-text></n-text>
  24 + </n-space>
  25 + </SettingItem>
  26 + <SettingItem>
  27 + <n-text :depth="3">无鼠标点击图例场景时,可强行打开图例</n-text>
  28 + </SettingItem>
  29 + </SettingItemBox>
  30 + <SettingItemBox name="实心点">
  31 + <SettingItem name="大小">
  32 + <n-input-number
  33 + v-model:value="item.symbolSize"
  34 + :min="1"
  35 + :max="100"
  36 + size="small"
  37 + placeholder="自动计算"
  38 + ></n-input-number>
  39 + </SettingItem>
  40 + </SettingItemBox>
  41 + <setting-item-box name="标签">
  42 + <setting-item>
  43 + <n-space>
  44 + <n-switch v-model:value="item.label.show" size="small" />
  45 + <n-text>展示标签</n-text>
  46 + </n-space>
  47 + </setting-item>
  48 + <setting-item name="大小">
  49 + <n-input-number v-model:value="item.label.fontSize" size="small" :min="1"></n-input-number>
  50 + </setting-item>
  51 + <setting-item name="颜色">
  52 + <n-color-picker size="small" :modes="['hex']" v-model:value="item.label.color"></n-color-picker>
  53 + </setting-item>
  54 + <setting-item name="位置">
  55 + <n-select
  56 + v-model:value="item.label.position"
  57 + :options="[
  58 + { label: 'top', value: 'top' },
  59 + { label: 'left', value: 'left' },
  60 + { label: 'right', value: 'right' },
  61 + { label: 'bottom', value: 'bottom' }
  62 + ]"
  63 + />
  64 + </setting-item>
  65 + </setting-item-box>
  66 + </CollapseItem>
  67 +</template>
  68 +
  69 +<script setup lang="ts">
  70 +import { PropType, computed } from 'vue'
  71 +import { lineConf } from '@/packages/chartConfiguration/echarts/index'
  72 +import { GlobalThemeJsonType } from '@/settings/chartThemes/index'
  73 +import { GlobalSetting, CollapseItem, SettingItemBox, SettingItem } from '@/components/Pages/ChartItemSetting'
  74 +
  75 +const props = defineProps({
  76 + optionData: {
  77 + type: Object as PropType<GlobalThemeJsonType>,
  78 + required: true
  79 + }
  80 +})
  81 +
  82 +const seriesList = computed(() => {
  83 + return props.optionData.series
  84 +})
  85 +</script>
... ...
  1 +{
  2 + "dimensions": ["product", "data1", "data2"],
  3 + "source": [
  4 + {
  5 + "product": "Mon",
  6 + "data1": 120,
  7 + "data2": 130
  8 + },
  9 + {
  10 + "product": "Tue",
  11 + "data1": 200,
  12 + "data2": 130
  13 + },
  14 + {
  15 + "product": "Wed",
  16 + "data1": 150,
  17 + "data2": 312
  18 + },
  19 + {
  20 + "product": "Thu",
  21 + "data1": 80,
  22 + "data2": 268
  23 + },
  24 + {
  25 + "product": "Fri",
  26 + "data1": 70,
  27 + "data2": 155
  28 + },
  29 + {
  30 + "product": "Sat",
  31 + "data1": 110,
  32 + "data2": 117
  33 + },
  34 + {
  35 + "product": "Sun",
  36 + "data1": 130,
  37 + "data2": 160
  38 + }
  39 + ]
  40 +}
... ...
  1 +import { ConfigType, PackagesCategoryEnum, ChartFrameEnum } from '@/packages/index.d'
  2 +import { ChatCategoryEnum, ChatCategoryEnumName } from '@/packages/components/Charts/index.d'
  3 +import { useWidgetKey } from '@/packages/external/useWidgetKey'
  4 +
  5 +const { key, conKey, chartKey } = useWidgetKey('OverrideLineForDeviceHistoryQuery', true)
  6 +
  7 +export const OverrideLineForDeviceHistoryQueryConfig: ConfigType = {
  8 + key,
  9 + chartKey,
  10 + conKey,
  11 + title: '单设备-属性-历史数据查询',
  12 + category: ChatCategoryEnum.LINE,
  13 + categoryName: ChatCategoryEnumName.LINE,
  14 + package: PackagesCategoryEnum.CHARTS,
  15 + chartFrame: ChartFrameEnum.ECHARTS,
  16 + image: 'line.png'
  17 +}
... ...
  1 +<template>
  2 + <div>
  3 + <n-space vertical>
  4 + <n-space>
  5 + <n-date-picker
  6 + size="small"
  7 + :to="true"
  8 + clearable
  9 + v-model:value="queryCondition.timeRange"
  10 + type="datetimerange"
  11 + :shortcuts="rangeShortcuts"
  12 + />
  13 + <n-select :style="`width:160px`" v-model:value="queryCondition.agg" :options="aggOptions" clearable />
  14 + <n-input-number
  15 + :min="7"
  16 + :max="50000"
  17 + :style="`width:160px`"
  18 + v-if="queryCondition.agg === 'NONE'"
  19 + v-model:value="queryCondition.limit"
  20 + clearable
  21 + />
  22 + </n-space>
  23 + </n-space>
  24 + <v-chart
  25 + ref="vChartRef"
  26 + :init-options="initOptions"
  27 + :theme="themeColor"
  28 + :option="option"
  29 + :manual-update="isPreview()"
  30 + :update-options="{
  31 + replaceMerge: replaceMergeArr
  32 + }"
  33 + autoresize
  34 + @mouseover="handleHighlight"
  35 + @mouseout="handleDownplay"
  36 + >
  37 + </v-chart>
  38 + </div>
  39 +</template>
  40 +
  41 +<script setup lang="ts">
  42 +import { PropType, computed, watch, ref, nextTick, onMounted, toRefs } from 'vue'
  43 +import VChart from 'vue-echarts'
  44 +import { useCanvasInitOptions } from '@/hooks/useCanvasInitOptions.hook'
  45 +import { use } from 'echarts/core'
  46 +import { CanvasRenderer } from 'echarts/renderers'
  47 +import { LineChart } from 'echarts/charts'
  48 +import config, { includes, seriesItem } from './config'
  49 +import { mergeTheme } from '@/packages/public/chart'
  50 +import { useChartEditStore } from '@/store/modules/chartEditStore/chartEditStore'
  51 +import { useChartDataFetch } from '@/hooks'
  52 +import { isPreview } from '@/utils'
  53 +import { DatasetComponent, GridComponent, TooltipComponent, LegendComponent } from 'echarts/components'
  54 +import isObject from 'lodash/isObject'
  55 +import cloneDeep from 'lodash/cloneDeep'
  56 +import dataJson from './data.json'
  57 +import { useFullScreen } from '../../utls/fullScreen'
  58 +import { useAssembleDataHooks } from '@/hooks/external/useAssembleData.hook'
  59 +import { SocketReceiveMessageType } from '@/store/external/modules/socketStore.d'
  60 +import { useChartInteract } from '@/hooks/external/useChartSelectDeviceInteract.hook'
  61 +
  62 +const props = defineProps({
  63 + themeSetting: {
  64 + type: Object,
  65 + required: true
  66 + },
  67 + themeColor: {
  68 + type: Object,
  69 + required: true
  70 + },
  71 + chartConfig: {
  72 + type: Object as PropType<config>,
  73 + required: true
  74 + }
  75 +})
  76 +
  77 +const initOptions = useCanvasInitOptions(props.chartConfig.option, props.themeSetting)
  78 +
  79 +const { queryCondition } = toRefs(props.chartConfig.option)
  80 +
  81 +use([DatasetComponent, CanvasRenderer, LineChart, GridComponent, TooltipComponent, LegendComponent])
  82 +
  83 +const chartEditStore = useChartEditStore()
  84 +
  85 +const replaceMergeArr = ref<string[]>()
  86 +
  87 +const aggOptions = [
  88 + { label: '最小值', value: 'MIN' },
  89 + { label: '最大值', value: 'MAX' },
  90 + { label: '平均值', value: 'AVG' },
  91 + { label: '求和', value: 'SUM' },
  92 + { label: '计数', value: 'COUNT' },
  93 + { label: '空', value: 'NONE' }
  94 +]
  95 +
  96 +const rangeShortcuts = {
  97 + 昨天: () => {
  98 + const cur = new Date().getTime()
  99 + return [cur - 86400000, cur] as const
  100 + },
  101 + 最近7天: () => {
  102 + const cur = new Date().getTime()
  103 + return [cur - 604800000, cur] as const
  104 + },
  105 + 最近30天: () => {
  106 + const cur = new Date().getTime()
  107 + return [cur - 2592000000, cur] as const
  108 + }
  109 +}
  110 +
  111 +const option = computed(() => {
  112 + return mergeTheme(props.chartConfig.option, props.themeSetting, includes)
  113 +})
  114 +
  115 +// dataset 无法变更条数的补丁
  116 +watch(
  117 + () => props.chartConfig.option.dataset,
  118 + (newData: { dimensions: any }, oldData) => {
  119 + try {
  120 + if (!isObject(newData) || !('dimensions' in newData)) return
  121 + if (Array.isArray(newData?.dimensions)) {
  122 + const seriesArr = []
  123 + // 对oldData进行判断,防止传入错误数据之后对旧维度判断产生干扰
  124 + // 此处计算的是dimensions的Y轴维度,若是dimensions.length为0或1,则默认为1,排除X轴维度干扰
  125 + const oldDimensions =
  126 + Array.isArray(oldData?.dimensions) && oldData.dimensions.length >= 1 ? oldData.dimensions.length : 1
  127 + const newDimensions = newData.dimensions.length >= 1 ? newData.dimensions.length : 1
  128 + const dimensionsGap = newDimensions - oldDimensions
  129 + if (dimensionsGap < 0) {
  130 + props.chartConfig.option.series.splice(newDimensions - 1)
  131 + } else if (dimensionsGap > 0) {
  132 + if (!oldData || !oldData?.dimensions || !Array.isArray(oldData?.dimensions) || !oldData?.dimensions.length) {
  133 + props.chartConfig.option.series = []
  134 + }
  135 + for (let i = 0; i < dimensionsGap; i++) {
  136 + seriesArr.push(cloneDeep(seriesItem))
  137 + }
  138 + props.chartConfig.option.series.push(...seriesArr)
  139 + }
  140 + replaceMergeArr.value = ['series']
  141 + nextTick(() => {
  142 + replaceMergeArr.value = []
  143 + })
  144 + }
  145 + } catch (error) {
  146 + console.log(error)
  147 + }
  148 + },
  149 + {
  150 + deep: false
  151 + }
  152 +)
  153 +
  154 +let seriesDataNum = -1
  155 +let seriesDataMaxLength = 0
  156 +let intervalInstance: any = null
  157 +const duration = 1500
  158 +
  159 +// 会重新选择需要选中和展示的数据
  160 +const handleSeriesData = () => {
  161 + if (seriesDataNum > -1) {
  162 + vChartRef.value?.dispatchAction({
  163 + type: 'downplay',
  164 + dataIndex: seriesDataNum
  165 + })
  166 + }
  167 + seriesDataNum = seriesDataNum >= seriesDataMaxLength - 1 ? 0 : seriesDataNum + 1
  168 + vChartRef.value?.dispatchAction({
  169 + type: 'showTip',
  170 + seriesIndex: 0,
  171 + dataIndex: seriesDataNum
  172 + })
  173 +}
  174 +
  175 +// 新增轮播
  176 +const addPieInterval = (newData?: typeof dataJson, skipPre = false) => {
  177 + if (!skipPre && !Array.isArray(newData?.source)) return
  178 + if (!skipPre) seriesDataMaxLength = newData?.source.length || 0
  179 + clearInterval(intervalInstance)
  180 + intervalInstance = setInterval(() => {
  181 + handleSeriesData()
  182 + }, duration)
  183 +}
  184 +
  185 +// 取消轮播
  186 +const clearPieInterval = () => {
  187 + vChartRef.value?.dispatchAction({
  188 + type: 'hideTip',
  189 + seriesIndex: 0,
  190 + dataIndex: seriesDataNum
  191 + })
  192 + vChartRef.value?.dispatchAction({
  193 + type: 'downplay',
  194 + dataIndex: seriesDataNum
  195 + })
  196 + clearInterval(intervalInstance)
  197 + intervalInstance = null
  198 +}
  199 +
  200 +// 处理鼠标聚焦高亮内容
  201 +const handleHighlight = () => {
  202 + clearPieInterval()
  203 +}
  204 +
  205 +// 处理鼠标取消悬浮
  206 +const handleDownplay = () => {
  207 + if (props.chartConfig.option.isCarousel && !intervalInstance) {
  208 + // 恢复轮播
  209 + addPieInterval(undefined, true)
  210 + }
  211 +}
  212 +
  213 +watch(
  214 + () => props.chartConfig.option.isCarousel,
  215 + newData => {
  216 + if (newData) {
  217 + addPieInterval(undefined, true)
  218 + props.chartConfig.option.legend.show = false
  219 + } else {
  220 + props.chartConfig.option.legend.show = true
  221 + clearPieInterval()
  222 + }
  223 + }
  224 +)
  225 +
  226 +//fix 修复v-chart图表绑定联动组件视图不更新问题
  227 +const updateVChart = async (newData: SocketReceiveMessageType) => {
  228 + //区分是普通请求还是ws请求
  229 + if (!isObject(newData) || !('dimensions' in newData)) {
  230 + const { data } = newData
  231 + const { keys, record } = useAssembleDataHooks(data)
  232 + vChartRef.value?.setOption({
  233 + dataset: {
  234 + dimensions: ['ts', ...keys],
  235 + source: [record]
  236 + }
  237 + })
  238 + } else {
  239 + //异步更新,同步更新会造成联动组件控制,图表不及时更新
  240 + await nextTick().then(() => {
  241 + vChartRef.value?.setOption(
  242 + {
  243 + ...option.value,
  244 + dataset: newData
  245 + },
  246 + {
  247 + notMerge: true
  248 + }
  249 + )
  250 + })
  251 + }
  252 +}
  253 +
  254 +const { vChartRef } = useChartDataFetch(props.chartConfig, useChartEditStore, (newData: any, targetComponent: any) => {
  255 + //联动支持分组
  256 + /**
  257 + * 修复多个分组,然后下拉框联动,会影响另一个组件
  258 + */
  259 + chartEditStore.getComponentList.forEach(targetItem => {
  260 + if (targetItem.isGroup) {
  261 + targetItem.groupList?.forEach(groupItem => {
  262 + if (groupItem.id === props.chartConfig.id) {
  263 + groupItem.option.dataset = newData
  264 + }
  265 + })
  266 + } else {
  267 + if (targetItem.id === props.chartConfig.id) {
  268 + targetItem.option.dataset = newData
  269 + }
  270 + }
  271 + })
  272 + //
  273 + updateVChart(newData)
  274 +})
  275 +
  276 +onMounted(() => {
  277 + seriesDataMaxLength = dataJson.source.length
  278 + if (props.chartConfig.option.isCarousel) {
  279 + addPieInterval(undefined, true)
  280 + }
  281 +})
  282 +
  283 +watch(
  284 + () => queryCondition.value,
  285 + newValue => {
  286 + const obj = {
  287 + startTs: newValue.timeRange.at(-2),
  288 + endTs: newValue.timeRange.at(-1),
  289 + limit: newValue.limit,
  290 + agg: newValue.agg
  291 + }
  292 + if (newValue.agg !== 'NONE') {
  293 + Reflect.deleteProperty(obj, 'limit')
  294 + }
  295 + onChange(obj)
  296 + },
  297 + {
  298 + deep: true
  299 + }
  300 +)
  301 +
  302 +// 监听事件改变
  303 +const onChange = (v: object) => {
  304 + // 存储到联动数据
  305 + useChartInteract(props.chartConfig, useChartEditStore, { data: v })
  306 +}
  307 +</script>
... ...
... ... @@ -148,6 +148,8 @@ const getGeojson = async (regionId: any) => {
148 148 } catch (error) {
149 149 loading.value = false
150 150 console.error('动态注册地图出错', error)
  151 + //注册出错则注册空的,不然在选择正确的adcode,则视图无法更新
  152 + registerMap(props.chartConfig.option.mapRegion.adcode, { geoJSON: {} as any, specialAreas: {} })
151 153 }
152 154 }
153 155
... ...
... ... @@ -21,6 +21,7 @@ import { CustomEchartsConfig } from '@/packages/components/external/Informations
21 21 import { OverrideBarCommonConfig } from '@/packages/components/external/Charts/Bars/OverrideBarCommon'
22 22 import { OverrideLineCommonConfig } from '@/packages/components/external/Charts/Lines/OverrideLineCommon'
23 23 import { OverrideLineGradientsConfig } from '@/packages/components/external/Charts/Lines/OverrideLineGradients'
  24 +import { OverrideLineForDeviceHistoryQueryConfig } from '@/packages/components/external/Charts/Lines/OverrideLineForDeviceHistoryQuery'
24 25 import { OverrideProcessConfig } from '@/packages/components/external/Charts/Mores/OverrideProcess'
25 26 import { OverridePieCircleConfig } from '@/packages/components/external/Charts/Pies/OverridePieCircle'
26 27 import { OverrideMapBaseConfig } from '@/packages/components/external/Charts/Maps/OverrideMapBase'
... ... @@ -112,6 +113,7 @@ export function useInjectLib(packagesList: EPackagesType) {
112 113 addWidgetToCategoryByCategoryName(packagesList, PackagesCategoryEnum.CHARTS, AddThreeDimensionalMapConfig)//新增图表下的3d echarts地图
113 114 addWidgetToCategoryByCategoryName(packagesList, PackagesCategoryEnum.CHARTS, OverrideWaterPoloConfig)//重写图表下的水球图
114 115 addWidgetToCategoryByCategoryName(packagesList, PackagesCategoryEnum.CHARTS, OverrideDialConfig)//重写图表下的表盘
  116 + addWidgetToCategoryByCategoryName(packagesList, PackagesCategoryEnum.CHARTS, OverrideLineForDeviceHistoryQueryConfig)//新增图表下的单设备-属性-历史数据查询折线图
115 117 //
116 118
117 119 //列表
... ...