Skip to content

动态数据组件

一个动态数据组件,通过与数据缓存建立绑定关系来动态展示数据。本例SVGDataText组件便是一个动态数据组件,从名称就可以看出它是一个以SVG来构建的数据展示组件。

代码如下

vue
<template>
  <div class="svg-container"
       :style="customStyle"
       @click="handleClick" @dblclick="handleDblClick" @mouseenter="showTooltip" @contextmenu="openContextMenu">
    <el-tooltip class="item" :placement="tooltipPlacement"
                :disabled="disableToolTip" :effect="tooltipEffect">
      <template #content>
        <div v-html="tooltipContent"></div>
      </template>
      <template #default>
        <svg xmlns="http://www.w3.org/2000/svg" :width="element.style.width" :height="element.style.height">
          <rect v-if="propValue.hasBackground" :x="0" :y="0" :width="element.style.width" :height="element.style.height"
                :fill="showBackgroundColor" :stroke="propValue.strokeColor"
                :stroke-width="propValue.strokeWidth"></rect>
          <text :x="textX" :y="textY" :dx="propValue.dx"
                :fill="showFontColor"
                :dominant-baseline="textBaseLine"
                :text-anchor="textAnchor"
                :font-family="propValue.fontFamily"
                :font-size="propValue.fontSize"
                :font-weight="propValue.fontWeight"
          >{{ showDataValue }}
          </text>
        </svg>
      </template>
    </el-tooltip>
  </div>
</template>

<script setup>

import {
  watch,
  onMounted,
  computed,
  onBeforeUnmount,
  ref,
  onBeforeMount,
  onUnmounted,
  reactive,
  onActivated,
  onDeactivated
} from "vue";


import {useRoute, useRouter} from "vue-router";
import {useEventHandler,useDataCacheStore,createSmartAccessor,CreateCompContext} from "hflvloader";
import {CommonUtils} from "hflvloader";
import {useDynamicColor} from "@/common/useDynamicColor.js";

import ContextMenu from '@imengyu/vue3-context-menu'
import '@imengyu/vue3-context-menu/lib/vue3-context-menu.css'
import {useMenuHandler} from "@/common/useMenuHandler.js";
import {getCursorByEvents} from "@/common/useCursorManager.js";

const {
  formatDate,
  getGMT0Date,
  getDate,
  getWeek,
  isNotEmpty,
  isNumber,
  isObject,
  isValid,
  hasOwnPathInObject,
  getValueByPath
} = CommonUtils;

defineOptions({
  name: 'SVGDataText'
});


const props = defineProps({
  propValue: {
    type: Object,
    required: true,
    default: () => {
    }
  },
  element: {
    type: Object,
    default: () => {
    }
  },
  designStatus: {
    type: Boolean,
    default: false
  }
})

const propValue = computed(() => {
  return props.propValue || {};
})

const element = computed(() => {
  return props.element || {};
})
const events = computed(() => {
  return element.value.events || [];
})


const dataCache = useDataCacheStore();
const smartData = createSmartAccessor(dataCache);
const eventHandler = ref(null);


const textX = ref(0);
const textY = ref(0);
const textBaseLine = ref('');
const textAnchor = ref('');
const hasTooltipContent = ref(false)
const showDataValue = ref('');

let dataValue = null;
let dataTime = null;
const tooltipContent = ref('');
let timer = null;


const bsData = smartData[propValue.value.dataCK];
const timeData = smartData[propValue.value.timeCK];

const _getValueFromObj = (obj, key, defaultVal) => {
  if (hasOwnPathInObject(obj, key)) {
    return getValueByPath(obj, key);
  } else {
    return defaultVal;
  }
}

function parseBsData(value) {
  if (isObject(value)) {
    dataValue = _getValueFromObj(value, propValue.value.valueKey, null);
    dataTime = _getValueFromObj(value, propValue.value.timeKey, null);
  } else {
    dataValue = value;
  }
}

function dealBsData(value) {
  parseBsData(value)
  showDataValue.value = showValue(dataValue);
}

watch(() => bsData.value, (value) => {
  if (isValid(value)) {
    dealBsData(value)
  }
})

watch(() => timeData.value, (value) => {
  if (isNotEmpty(propValue.value.timeCK) && isValid(value)) {
    if (isNotEmpty(propValue.value.timeKey)) {
      if (isObject(value)) {
        dataTime = _getValueFromObj(value, propValue.value.timeKey, null);
      }
    } else {
      dataTime = value;
    }
  }
})

const {dynamicColor} = useDynamicColor(propValue, smartData, dataCache.rules, CommonUtils);

const showFontColor = computed(() => {
  if (dynamicColor.dataColor) {
    return dynamicColor.dataColor;
  } else {
    return propValue.value.fontColor ? propValue.value.fontColor : 'none';
  }
})

const showBackgroundColor = computed(() => {
  if (dynamicColor.backgroundColor) {
    return dynamicColor.backgroundColor;
  } else {
    return propValue.value.fillColor ? propValue.value.fillColor : 'none';
  }
})

const handleClick = (event) => {
  eventHandler.value?.handleClick(event);
}

const handleDblClick = (event) => {
  eventHandler.value?.handleDblClick(event);
}

watch(() => element.value.style.width, (value) => {
  draw();
})

watch(() => element.value.style.height, (value) => {
  draw();
})

watch(() => propValue.value.horizontalAlign, (value) => {
  draw();
})

watch(() => propValue.value.offsetX, (value) => {
  draw();
})

const disableToolTip = computed(() => {
  // 设计状态不需要显示
  if (props.designStatus) {
    return true;
  }
  if (propValue.value.showTooltip) {
    return !hasTooltipContent.value;
  } else {
    return true;
  }
})

const tooltipPlacement = computed(() => {
  return propValue.value.tooltipPlacement || 'bottom-start';
})

const tooltipEffect = computed(() => {
  return propValue.value.tooltipEffect || 'dark';
})


const showTooltip = (event) => {
  if (props.designStatus) {
    return;
  }
  loadTipInfo();
  if (isNotEmpty(tooltipContent.value)) {
    hasTooltipContent.value = true;
  } else {
    hasTooltipContent.value = false;
  }
}

const loadTipInfo = () => {
  tooltipContent.value = '';
  if (propValue.value.showTooltip) {
    if (dataTime && isNumber(dataTime)) {
      let _date = getDate(dataTime);
      let _dateStr = '';
      if (_date) {
        _dateStr = formatDate(_date, 'yyyy-MM-dd HH:mm:ss');
        let line1 = "数值:" + showDataValue.value;
        let line2 = "时间:" + _dateStr;
        tooltipContent.value = line1 + '<br/>' + line2;
      }
    } else {
      let line1 = "数值:" + showDataValue.value;
      tooltipContent.value = line1;
    }
  }
}

const draw = () => {
  const {width, height} = props.element.style
  drawChart(width, height)
}


const drawChart = (width, height) => {
  let horizontalAlign = props.propValue.horizontalAlign || 'left';
  let offsetX = props.propValue.offsetX || 0;
  if ('center' == horizontalAlign) {
    textAnchor.value = 'middle'
    textX.value = width * 0.5
  } else if ('right' == horizontalAlign) {
    textX.value = width - offsetX
    textAnchor.value = 'end'
  } else {
    textX.value = 0 + offsetX
    textAnchor.value = 'start'
  }

  textBaseLine.value = 'central';
  textY.value = height * 0.5
}

const showValue = (value) => {
  let _showVal = value;
  if (value == null) {
    _showVal = propValue.value.nullShowTxt ?? '';
  } else {
    if (isNumber(value)) {
      let fractionDigits = propValue.value.fractionDigits ?? 0;
      let factor = propValue.value.factor;
      if (isNumber(factor)) {
        factor = Number(factor);
        value = Number(value) * factor;
      }
      _showVal = dataFormatter(Number(value), fractionDigits);
    } else {
      if (_showVal + '' == 'undefined') {
        _showVal = propValue.value.nullShowTxt ?? '';
      }
    }
    if (isNotEmpty(propValue.value.unit)) {
      _showVal = _showVal + propValue.value.unit;
    }
  }
  return _showVal;
}

const dataFormatter = (val, fractionDigits) => {
  let retVal = val;
  if (val) {
    const valueType = propValue.value.valueType;
    // 字符串类型默认返回原始值
    if (valueType == 2 && Number(val)) {
      // 数值类型
      if (fractionDigits && Number(fractionDigits)) {
        fractionDigits = parseInt(fractionDigits);
      } else {
        fractionDigits = 0;
      }
      retVal = val.toFixed(fractionDigits);
    } else if (valueType == 3) {
      // 时间类型
      let _date = getDate(val);
      // 转换成GMT0时间
      if (propValue.value.convertGMT0) {
        _date = getGMT0Date(_date);
      }
      const _dateformat = propValue.value.dateformat || 'yyyy-MM-dd';
      if (_date) {
        if (isNotEmpty(propValue.value.weekMode)) {
          retVal = getWeek(_date, isNumber(propValue.value.weekMode) ? Number(propValue.value.weekMode) : 0);
        } else {
          retVal = formatDate(_date, _dateformat);
        }
      }
    }
  }
  return retVal;
}

const _clearInterval = () => {
  if (timer) {
    clearInterval(timer);
    timer = null;
  }
}

const router = useRouter();
const route = useRoute();
const compContext = CreateCompContext(propValue, element, router, route);


onBeforeMount(() => {
  eventHandler.value = useEventHandler(events.value, props.designStatus, compContext);
  eventHandler.value?.beforeMount();
})

onMounted(() => {
  draw();
  timer = setInterval(() => {
    if (isValid(bsData.value)) {
      dealBsData(bsData.value);
      if(timer){
        clearInterval(timer);
        timer = null;
      }
    }
  }, 1000)
  eventHandler.value?.mounted();
})

onBeforeUnmount(() => {
  _clearInterval();
  eventHandler.value?.beforeUnmount();
})

onUnmounted(() => {
  eventHandler.value?.unmounted();
})

onActivated(() => {
  eventHandler.value?.activated();
})

onDeactivated(() => {
  eventHandler.value?.deactivated();
})

const {processMenus} = useMenuHandler();

const openContextMenu = (_event) => {
  _event.preventDefault();
  let _menuItems = propValue.value.menuItems || [];
  if (CommonUtils.isArray(_menuItems) && _menuItems.length > 0) {
    let _menuList = [];
    _menuList = processMenus(_menuItems, function (item, event) {
      eventHandler.value?.handleContextMenu(_event, item.value);
    })
    ContextMenu.showContextMenu({
      x: _event.x,
      y: _event.y,
      items: _menuList
    })
  }
}

const customStyle = computed(() => {
  let style = {}
  style['cursor'] = getCursorByEvents(events.value);
  return style;
})
</script>

<style lang="scss" scoped>
.svg-container {
  width: 100%;
  height: 100%;

  svg {
    width: 100%;
    height: 100%;
  }
}
</style>

基于 VitePress 构建