Commit 32b3b0d0b3485ec9475b075205d88be4a0985cc3

Authored by xp.Huang
2 parents 51b10678 0076f20b

Merge branch 'main_dev'

# Conflicts:
#	src/api/external/common/index.ts
#	src/assets/images/chart/photos/carousel1.jpeg
#	src/assets/images/chart/photos/carousel2.jpeg
#	src/packages/components/Informations/Mores/Image/index.vue
#	src/packages/components/external/Charts/Lines/OverrideLineForDeviceHistoryQuery/index.vue
#	src/packages/components/external/Charts/Maps/AddThreeDimensionalMap/config.ts
#	src/packages/components/external/Charts/Maps/AddThreeDimensionalMap/index.vue
#	src/packages/components/external/Charts/Maps/OverrideMapAmap/components/SearchBox.vue
#	src/packages/components/external/Charts/Maps/OverrideMapAmap/config.ts
#	src/packages/components/external/Charts/Maps/OverrideMapAmap/config.vue
#	src/packages/components/external/Charts/Maps/OverrideMapAmap/data.json
#	src/packages/components/external/Charts/Maps/OverrideMapAmap/images/djh.png
#	src/packages/components/external/Charts/Maps/OverrideMapAmap/images/lx1.png
#	src/packages/components/external/Charts/Maps/OverrideMapAmap/images/online.png
#	src/packages/components/external/Charts/Maps/OverrideMapAmap/index.ts
#	src/packages/components/external/Charts/Maps/OverrideMapAmap/index.vue
#	src/packages/components/external/Charts/Maps/OverrideMapBase/config.ts
#	src/packages/components/external/Charts/Maps/OverrideMapBase/index.vue
#	src/packages/components/external/Informations/Mores/SingleCamera/components/VideoPlay.vue
#	src/packages/components/external/Informations/Mores/SingleCamera/config.vue
#	src/packages/components/external/Tables/Tables/OverrideTableScrollBoard/index.vue
#	src/packages/index.ts
#	src/views/chart/ContentEdit/components/EditTools/external/index.ts
Showing 88 changed files with 1875 additions and 592 deletions
1 # Proxy 1 # Proxy
2 -VITE_GLOB_PROXY = [["/api", "http://222.180.200.114:48080/api"]]  
3 -# VITE_GLOB_PROXY = [["/api", "http://192.168.10.118:8080/api"]] 2 +VITE_GLOB_PROXY = [["/api", "http://locahost:8080/api"]]
4 3
5 # Api prefix 4 # Api prefix
6 VITE_GLOB_API_URL = /api 5 VITE_GLOB_API_URL = /api
7 6
8 -# 7 +#
9 VITE_GLOB_API_URL_PREFIX = /yt 8 VITE_GLOB_API_URL_PREFIX = /yt
10 9
11 # 内容安全协议 10 # 内容安全协议
1 # Proxy 1 # Proxy
2 -VITE_GLOB_PROXY = [["/api", "http://222.180.200.114:48080/api"]] 2 +VITE_GLOB_PROXY = /api
3 3
4 # Api prefix 4 # Api prefix
5 VITE_GLOB_API_URL = /api 5 VITE_GLOB_API_URL = /api
@@ -32,6 +32,6 @@ module.exports = { @@ -32,6 +32,6 @@ module.exports = {
32 'vue/no-unused-vars': 'off', 32 'vue/no-unused-vars': 'off',
33 'vue/multi-word-component-names': 'off', 33 'vue/multi-word-component-names': 'off',
34 'vue/valid-template-root': 'off', 34 'vue/valid-template-root': 'off',
35 - 'vue/no-mutating-props': 'off', 35 + 'vue/no-mutating-props': 'off'
36 } 36 }
37 } 37 }
@@ -4,3 +4,4 @@ dist @@ -4,3 +4,4 @@ dist
4 dist-ssr 4 dist-ssr
5 *.local 5 *.local
6 .vscode 6 .vscode
  7 +.idea
@@ -12,6 +12,7 @@ @@ -12,6 +12,7 @@
12 "preview": "vite preview", 12 "preview": "vite preview",
13 "preview:alone": "vite preview --mode alone.prod", 13 "preview:alone": "vite preview --mode alone.prod",
14 "new": "plop --plopfile ./plop/plopfile.js", 14 "new": "plop --plopfile ./plop/plopfile.js",
  15 + "newCom": "plop --plopfile ./plop/external/plopfile.js",
15 "postinstall": "husky install", 16 "postinstall": "husky install",
16 "lint": "eslint --ext .js,.jsx,.ts,.tsx,.vue src", 17 "lint": "eslint --ext .js,.jsx,.ts,.tsx,.vue src",
17 "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx,.vue src --fix" 18 "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx,.vue src --fix"
  1 +import { echartOptionProfixHandle, PublicConfigClass } from '@/packages/public'
  2 +import { {{name}}Config } from './index'
  3 +import { CreateComponentType } from '@/packages/index.d'
  4 +import cloneDeep from 'lodash/cloneDeep'
  5 +import dataJson from './data.json'
  6 +
  7 +export const includes = ['legend', 'xAxis', 'yAxis', 'grid']
  8 +
  9 +export const seriesItem = {
  10 +}
  11 +
  12 +export const option = {
  13 + dataset: { ...dataJson },
  14 + series: [seriesItem, seriesItem]
  15 +}
  16 +
  17 +export default class Config extends PublicConfigClass implements CreateComponentType {
  18 + public key = {{name}}Config.key
  19 + public chartConfig = cloneDeep({{name}}Config)
  20 + // 图表配置项
  21 + public option = echartOptionProfixHandle(option, includes)
  22 +}
  1 +<template>
  2 + <!-- Echarts 全局设置 -->
  3 + <global-setting :optionData="optionData"></global-setting>
  4 + <CollapseItem name="xxx" :expanded="true">
  5 + <SettingItemBox name="xx">
  6 + <SettingItem name="xx">
  7 + </SettingItem>
  8 + </SettingItemBox>
  9 + </CollapseItem>
  10 +</template>
  11 +
  12 +<script setup lang="ts">
  13 +import { PropType, computed } from 'vue'
  14 +import { GlobalSetting, CollapseItem, SettingItemBox, SettingItem } from '@/components/Pages/ChartItemSetting'
  15 +import { GlobalThemeJsonType } from '@/settings/chartThemes/index'
  16 +
  17 +const props = defineProps({
  18 + optionData: {
  19 + type: Object as PropType<GlobalThemeJsonType>,
  20 + required: true
  21 + }
  22 +})
  23 +
  24 +const seriesList = computed(() => {
  25 + return props.optionData.series
  26 +})
  27 +</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('{{name}}', true)
  6 +
  7 +export const {{name}}Config: ConfigType = {
  8 + key,
  9 + chartKey,
  10 + conKey,
  11 + title: 'xxx',
  12 + category: ChatCategoryEnum.BAR,
  13 + categoryName: ChatCategoryEnumName.BAR,
  14 + package: PackagesCategoryEnum.CHARTS,
  15 + chartFrame: ChartFrameEnum.ECHARTS,
  16 + image: 'bar_x.png'
  17 +}
  1 +<template>
  2 + <v-chart
  3 + ref="vChartRef"
  4 + :init-options="initOptions"
  5 + :theme="themeColor"
  6 + :option="option"
  7 + :manual-update="isPreview()"
  8 + :update-options="{
  9 + replaceMerge: replaceMergeArr
  10 + }"
  11 + autoresize
  12 + @mouseover="handleHighlight"
  13 + @mouseout="handleDownplay"
  14 + >
  15 + </v-chart>
  16 +</template>
  17 +
  18 +<script setup lang="ts">
  19 +import { ref, nextTick, computed, watch, PropType, onMounted } from 'vue'
  20 +import VChart from 'vue-echarts'
  21 +import { useCanvasInitOptions } from '@/hooks/useCanvasInitOptions.hook'
  22 +import { use } from 'echarts/core'
  23 +import { CanvasRenderer } from 'echarts/renderers'
  24 +import { BarChart } from 'echarts/charts'
  25 +import config, { includes, seriesItem } from './config'
  26 +import { mergeTheme } from '@/packages/public/chart'
  27 +import { useChartDataFetch } from '@/hooks'
  28 +import { useChartEditStore } from '@/store/modules/chartEditStore/chartEditStore'
  29 +import { isPreview } from '@/utils'
  30 +import { DatasetComponent, GridComponent, TooltipComponent, LegendComponent } from 'echarts/components'
  31 +import isObject from 'lodash/isObject'
  32 +import dataJson from './data.json'
  33 +import cloneDeep from 'lodash/cloneDeep'
  34 +import { useAssembleDataHooks } from '@/hooks/external/useAssembleData.hook'
  35 +import { SocketReceiveMessageType } from '@/store/external/modules/socketStore.d'
  36 +
  37 +const props = defineProps({
  38 + themeSetting: {
  39 + type: Object,
  40 + required: true
  41 + },
  42 + themeColor: {
  43 + type: Object,
  44 + required: true
  45 + },
  46 + chartConfig: {
  47 + type: Object as PropType<config>,
  48 + required: true
  49 + }
  50 +})
  51 +
  52 +const initOptions = useCanvasInitOptions(props.chartConfig.option, props.themeSetting)
  53 +
  54 +use([DatasetComponent, CanvasRenderer, BarChart, GridComponent, TooltipComponent, LegendComponent])
  55 +
  56 +const chartEditStore = useChartEditStore()
  57 +
  58 +const replaceMergeArr = ref<string[]>()
  59 +
  60 +const option = computed(() => {
  61 + return mergeTheme(props.chartConfig.option, props.themeSetting, includes)
  62 +})
  63 +
  64 +
  65 +// dataset 无法变更条数的补丁
  66 +watch(
  67 + () => props.chartConfig.option.dataset,
  68 + (newData: { dimensions: any }, oldData) => {
  69 + try {
  70 + if (!isObject(newData) || !('dimensions' in newData)) return
  71 + if (Array.isArray(newData?.dimensions)) {
  72 + const seriesArr = []
  73 + // 对oldData进行判断,防止传入错误数据之后对旧维度判断产生干扰
  74 + // 此处计算的是dimensions的Y轴维度,若是dimensions.length为0或1,则默认为1,排除X轴维度干扰
  75 + const oldDimensions =
  76 + Array.isArray(oldData?.dimensions) && oldData.dimensions.length >= 1 ? oldData.dimensions.length : 1
  77 + const newDimensions = newData.dimensions.length >= 1 ? newData.dimensions.length : 1
  78 + const dimensionsGap = newDimensions - oldDimensions
  79 + if (dimensionsGap < 0) {
  80 + props.chartConfig.option.series.splice(newDimensions - 1)
  81 + } else if (dimensionsGap > 0) {
  82 + if (!oldData || !oldData?.dimensions || !Array.isArray(oldData?.dimensions) || !oldData?.dimensions.length) {
  83 + props.chartConfig.option.series = []
  84 + }
  85 + for (let i = 0; i < dimensionsGap; i++) {
  86 + seriesArr.push(cloneDeep(seriesItem))
  87 + }
  88 + props.chartConfig.option.series.push(...seriesArr)
  89 + }
  90 + replaceMergeArr.value = ['series']
  91 + nextTick(() => {
  92 + replaceMergeArr.value = []
  93 + })
  94 + }
  95 + } catch (error) {
  96 + console.log(error)
  97 + }
  98 + },
  99 + {
  100 + deep: false
  101 + }
  102 +)
  103 +
  104 +//fix 修复v-chart图表绑定联动组件视图不更新问题
  105 +const updateVChart = async (newData: SocketReceiveMessageType) => {
  106 + //区分是普通请求还是ws请求
  107 + if (!isObject(newData) || !('dimensions' in newData)) {
  108 + const { data } = newData
  109 + const { keys, record } = useAssembleDataHooks(data)
  110 + vChartRef.value?.setOption({
  111 + dataset: {
  112 + dimensions: ['ts', ...keys],
  113 + source: [record]
  114 + }
  115 + })
  116 + } else {
  117 + //异步更新,同步更新会造成联动组件控制,图表不及时更新
  118 + await nextTick().then(() => {
  119 + vChartRef.value?.setOption(
  120 + {
  121 + ...option.value,
  122 + dataset: newData
  123 + },
  124 + {
  125 + notMerge: true
  126 + }
  127 + )
  128 + })
  129 + }
  130 +}
  131 +
  132 +const { vChartRef } = useChartDataFetch(props.chartConfig, useChartEditStore, newData => {
  133 + //联动支持分组
  134 + /**
  135 + * 修复多个分组,然后下拉框联动,会影响另一个组件
  136 + */
  137 + chartEditStore.getComponentList.forEach(targetItem => {
  138 + if (targetItem.isGroup) {
  139 + targetItem.groupList?.forEach(groupItem => {
  140 + if (groupItem.id === props.chartConfig.id) {
  141 + groupItem.option.dataset = newData
  142 + }
  143 + })
  144 + } else {
  145 + if (targetItem.id === props.chartConfig.id) {
  146 + targetItem.option.dataset = newData
  147 + }
  148 + }
  149 + })
  150 + //
  151 + updateVChart(newData)
  152 +})
  153 +</script>
  1 +module.exports = {
  2 + description:
  3 + 'create a component(在packages/components/external,一般是需要重写的组件,这里写的示例生成在src/packages/components/external/Charts/Bars目录下)',
  4 + prompts: [
  5 + {
  6 + type: 'input',
  7 + name: 'name',
  8 + message: 'Please enter component name,such as "OverrideComponentName":',
  9 + validate(value) {
  10 + if (!value || value.trim === '') {
  11 + return 'component name is required'
  12 + }
  13 + return true
  14 + }
  15 + }
  16 + ],
  17 + actions: data => {
  18 + const dataName = data.name
  19 +
  20 + const actions = [
  21 + {
  22 + type: 'add',
  23 + path: `${process.cwd()}/src/packages/components/external/Charts/Bars${dataName}/config.ts`,
  24 + templateFile: './component-template/config.ts.hbs',
  25 + data: {
  26 + name: data.name
  27 + }
  28 + },
  29 + {
  30 + type: 'add',
  31 + path: `${process.cwd()}/src/packages/components/external/Charts/Bars${dataName}/config.vue`,
  32 + templateFile: './component-template/config.vue.hbs',
  33 + data: {
  34 + name: data.name
  35 + }
  36 + },
  37 + {
  38 + type: 'add',
  39 + path: `${process.cwd()}/src/packages/components/external/Charts/Bars${dataName}/data.json`,
  40 + templateFile: './component-template/data.json.hbs',
  41 + data: {
  42 + name: data.name
  43 + }
  44 + },
  45 + {
  46 + type: 'add',
  47 + path: `${process.cwd()}/src/packages/components/external/Charts/Bars${dataName}/index.ts`,
  48 + templateFile: './component-template/index.ts.hbs',
  49 + data: {
  50 + name: data.name
  51 + }
  52 + },
  53 + {
  54 + type: 'add',
  55 + path: `${process.cwd()}/src/packages/components/external/Charts/Bars${dataName}/index.vue`,
  56 + templateFile: './component-template/index.vue.hbs',
  57 + data: {
  58 + name: data.name
  59 + }
  60 + }
  61 + ]
  62 +
  63 + return actions
  64 + }
  65 +}
  1 +const componentsGenerator = require('./component-template/prompt')
  2 +
  3 +module.exports = plop => {
  4 + plop.setGenerator('component', componentsGenerator)
  5 +}
@@ -16,7 +16,9 @@ enum Api { @@ -16,7 +16,9 @@ enum Api {
16 VIDEOURL = '/video/url/', 16 VIDEOURL = '/video/url/',
17 GEOJSONURL = '/map/geo_json/', 17 GEOJSONURL = '/map/geo_json/',
18 DEVICE_URL = '/device', 18 DEVICE_URL = '/device',
19 - GET_ATTRBUTELIST = '/device/attributes/' 19 + GET_ATTRBUTELIST = '/device/attributes/',
  20 + GET_DEVICE_LATEST = '/plugins/telemetry/DEVICE/',
  21 + DEVICE_ATTR = '/device/attributes',
20 } 22 }
21 23
22 export const getDictItemByCode = (value: string) => { 24 export const getDictItemByCode = (value: string) => {
@@ -111,7 +113,7 @@ export const getVideoUrl = (id: string) => @@ -111,7 +113,7 @@ export const getVideoUrl = (id: string) =>
111 }) 113 })
112 114
113 //获取行政区域 115 //获取行政区域
114 -export const getGeoJsonMap = (code: number, level: string) => 116 +export const getGeoJsonMap = (code: number | string, level: string) =>
115 defHttp.get({ 117 defHttp.get({
116 url: `${Api.GEOJSONURL}${code}/${level}` 118 url: `${Api.GEOJSONURL}${code}/${level}`
117 }) 119 })
@@ -127,3 +129,20 @@ export const getAttribute = (deviceProfileId: string) => @@ -127,3 +129,20 @@ export const getAttribute = (deviceProfileId: string) =>
127 defHttp.get({ 129 defHttp.get({
128 url: `${Api.GET_ATTRBUTELIST}${deviceProfileId}` 130 url: `${Api.GET_ATTRBUTELIST}${deviceProfileId}`
129 }) 131 })
  132 +
  133 +// 获取设备最新数据
  134 +export const getDeviceLatest = (tbDeviceId: string) =>
  135 +defHttp.get({
  136 + url: `${Api.GET_DEVICE_LATEST}${tbDeviceId}/values/timeseries`
  137 +}, {
  138 + joinPrefix: false
  139 +})
  140 +
  141 +//获取产品属性
  142 +export const getProfileAttrs = (params: { deviceProfileId: string; dataType?: string }) => {
  143 + const { deviceProfileId, dataType } = params;
  144 + return defHttp.get({
  145 + url: `${Api.DEVICE_ATTR}/${deviceProfileId}`,
  146 + params: { dataType },
  147 + });
  148 +};
1 -import { RequestBodyEnum, RequestParams } from "@/enums/httpEnum" 1 +import { RequestBodyEnum, RequestHttpEnum, RequestParams } from "@/enums/httpEnum"
2 import { ExtraRequestConfigType } from "@/store/external/modules/extraComponentInfo.d" 2 import { ExtraRequestConfigType } from "@/store/external/modules/extraComponentInfo.d"
3 import { RequestConfigType } from "@/store/modules/chartEditStore/chartEditStore.d" 3 import { RequestConfigType } from "@/store/modules/chartEditStore/chartEditStore.d"
4 import { isShareMode } from "@/views/share/hook" 4 import { isShareMode } from "@/views/share/hook"
@@ -115,6 +115,22 @@ const handleParams = (Params: Recordable) => { @@ -115,6 +115,22 @@ const handleParams = (Params: Recordable) => {
115 return Object.keys(Params).filter(Boolean).reduce((prev, next) => ({ ...prev, [next]: Params[next] }), {}) 115 return Object.keys(Params).filter(Boolean).reduce((prev, next) => ({ ...prev, [next]: Params[next] }), {})
116 } 116 }
117 117
  118 +//post请求动态追加query参数
  119 +const objConvertQuery= (data:Recordable)=> {
  120 + const _result = [];
  121 + for (const key in data) {
  122 + const value = data[key];
  123 + if (value.constructor == Array) {
  124 + value.forEach(function(_value) {
  125 + _result.push(key + "=" + _value);
  126 + });
  127 + } else {
  128 + _result.push(key + '=' + value);
  129 + }
  130 + }
  131 + return _result.join('&');
  132 +}
  133 +
118 export const customRequest = async (request: RequestConfigType) => { 134 export const customRequest = async (request: RequestConfigType) => {
119 const { requestHttpType, requestParams, requestParamsBodyType, requestOriginUrl } = request as ExtraRequestConfigType 135 const { requestHttpType, requestParams, requestParamsBodyType, requestOriginUrl } = request as ExtraRequestConfigType
120 let { requestUrl } = request as ExtraRequestConfigType 136 let { requestUrl } = request as ExtraRequestConfigType
@@ -130,18 +146,18 @@ export const customRequest = async (request: RequestConfigType) => { @@ -130,18 +146,18 @@ export const customRequest = async (request: RequestConfigType) => {
130 const body = transformBodyValue(Body, requestParamsBodyType) 146 const body = transformBodyValue(Body, requestParamsBodyType)
131 147
132 /** 148 /**
133 - * ft 修改post请求针对这种接口(/api/yt/device,设备列表分页),是post请求,body体一定要携带内容,空内容也可以 149 + * ft 修改post请求针对这种接口(/api/yt/device,设备列表分页),是post请求,page和pageSize是query参数,body可以不传内容
134 * 修改后的代码在注释之间,并标注好源代码和修改后代码,方便回溯 150 * 修改后的代码在注释之间,并标注好源代码和修改后代码,方便回溯
135 - * 源代码 data: body,  
136 - * 修改后代码 data: !body? {"":null} :body, 151 + * 源代码 url: requestUrl
  152 + * 修改后代码 requestHttpType === RequestHttpEnum.GET.toUpperCase() ? requestUrl: `${requestUrl}?${objConvertQuery(Params)}
137 */ 153 */
138 Params = handleParams(Params) 154 Params = handleParams(Params)
139 return customHttp.request<any>({ 155 return customHttp.request<any>({
140 - url: requestUrl, 156 + url: requestHttpType === RequestHttpEnum.GET.toUpperCase() ? requestUrl: `${requestUrl}?${objConvertQuery(Params)}`,
141 baseURL: getOriginUrl(requestOriginUrl!), 157 baseURL: getOriginUrl(requestOriginUrl!),
142 method: requestHttpType, 158 method: requestHttpType,
143 - params: Params,  
144 - data: !body? {"":null} :body, 159 + params: requestHttpType === RequestHttpEnum.GET.toUpperCase() ? Params: null,
  160 + data: body,
145 headers: extraValue(Header) 161 headers: extraValue(Header)
146 }, { 162 }, {
147 joinPrefix: false, 163 joinPrefix: false,
  1 +import TKUpload from './index.vue';
  2 +
  3 +export { TKUpload };
  1 +<template>
  2 + <n-upload
  3 + v-model:file-list="fileList"
  4 + :show-file-list="true"
  5 + :customRequest="customRequest"
  6 + :onBeforeUpload="beforeUploadHandle"
  7 + :onRemove="remove"
  8 + >
  9 + <n-upload-dragger>
  10 + <img v-if="uploadImageUrl" class="upload-show" :src="uploadImageUrl" alt="上传的图片" />
  11 + <div class="upload-img" v-show="!uploadImageUrl">
  12 + <img src="@/assets/images/canvas/noImage.png" />
  13 + <n-text class="upload-desc" depth="3">
  14 + 上传文件需小于 {{ uploadSizeFormat.size }}M ,格式为 {{ uploadSizeFormat.format }} 的文件
  15 + </n-text>
  16 + </div>
  17 + </n-upload-dragger>
  18 + </n-upload>
  19 +</template>
  20 +
  21 +<script lang="ts" setup name="TKUpload">
  22 +import { ref, PropType, nextTick } from 'vue'
  23 +import type { UploadCustomRequestOptions, UploadFileInfo } from 'naive-ui'
  24 +import { fetchRouteParamsLocation } from '@/utils'
  25 +import { uploadFile } from '@/api/external/contentSave/content'
  26 +import { FileTypeEnum } from '@/enums/external/fileTypeEnum'
  27 +
  28 +interface uploadSizeFormatIF {
  29 + size: number
  30 + format: string
  31 +}
  32 +
  33 +const props = defineProps({
  34 + uploadImageUrl: {
  35 + type: String as PropType<string>,
  36 + default: ''
  37 + },
  38 + uploadSizeFormat: {
  39 + type: Object as PropType<uploadSizeFormatIF>,
  40 + default: () => ({
  41 + size: 5,
  42 + format: 'png/jpg/jpeg/gif'
  43 + })
  44 + }
  45 +})
  46 +
  47 +const emit = defineEmits(['sendFile', 'removeFile'])
  48 +
  49 +const fileList = ref<UploadFileInfo[]>()
  50 +
  51 +// 自定义上传操作
  52 +const customRequest = (options: UploadCustomRequestOptions) => {
  53 + const { file } = options
  54 + nextTick(async () => {
  55 + if (file.file) {
  56 + const newNameFile = new File([file.file], `${fetchRouteParamsLocation()}_index_upload.png`, {
  57 + type: file.file.type
  58 + })
  59 + let uploadParams = new FormData()
  60 + uploadParams.append('file', newNameFile)
  61 + const uploadRes = await uploadFile(uploadParams)
  62 + if (!uploadRes) return
  63 + emit('sendFile', uploadRes?.fileStaticUri)
  64 + window['$message'].success('上传文件成功!')
  65 + } else {
  66 + window['$message'].error('上传文件失败,请稍后重试!')
  67 + }
  68 + })
  69 +}
  70 +
  71 +// 文件上传前置处理
  72 +const beforeUploadHandle = (file: UploadFileInfo) => {
  73 + fileList.value = []
  74 + const type = file.file?.type
  75 + const size = file.file?.size as number
  76 + const typeSuffix = type?.split('/')?.at(-1)?.toUpperCase() as keyof typeof FileTypeEnum
  77 + if (size > 1024 * 1024 * props.uploadSizeFormat.size) {
  78 + window['$message'].warning(`文件超出 ${props.uploadSizeFormat.size}M限制,请重新上传!`)
  79 + return false
  80 + }
  81 + if (!FileTypeEnum[typeSuffix]) {
  82 + window['$message'].warning('文件格式不符合,请重新上传!')
  83 + return false
  84 + }
  85 + return true
  86 +}
  87 +
  88 +//单个点击删除
  89 +const remove = () => {
  90 + fileList.value = []
  91 + emit('removeFile', true)
  92 +}
  93 +</script>
  94 +
  95 +<style lang="scss" scoped>
  96 +$uploadHeight: 193px;
  97 +@include deep() {
  98 + .n-card__content {
  99 + padding: 0;
  100 + overflow: hidden;
  101 + }
  102 + .n-upload-dragger {
  103 + padding: 5px;
  104 + }
  105 +}
  106 +.upload-show {
  107 + width: -webkit-fill-available;
  108 + height: $uploadHeight;
  109 + border-radius: 5px;
  110 +}
  111 +.upload-img {
  112 + display: flex;
  113 + flex-direction: column;
  114 + align-items: center;
  115 + img {
  116 + height: 150px;
  117 + }
  118 + .upload-desc {
  119 + padding: 10px 0;
  120 + }
  121 +}
  122 +</style>
  1 +// 文件上传时的格式映射
  2 +export enum FileTypeEnum {
  3 + // 文档
  4 + TXT = 'text/plain',
  5 + JSON = 'application/json',
  6 + // 图片
  7 + PNG = 'image/png',
  8 + JPEG = 'image/jpeg',
  9 + JPG = 'image/jpg',
  10 + GIF = 'image/gif',
  11 +}
@@ -56,7 +56,10 @@ const initMap = (newData: any) => { @@ -56,7 +56,10 @@ const initMap = (newData: any) => {
56 pitch: pitch.value, // 地图俯仰角度,有效范围 0 度- 83 度 56 pitch: pitch.value, // 地图俯仰角度,有效范围 0 度- 83 度
57 skyColor: skyColor.value, 57 skyColor: skyColor.value,
58 viewMode: viewMode.value, // 地图模式 58 viewMode: viewMode.value, // 地图模式
59 - willReadFrequently: true 59 + willReadFrequently: true,
  60 + WebGLParams: {
  61 + preserveDrawingBuffer: true,
  62 + }
60 }) 63 })
61 dataHandle(props.chartConfig.option.dataset) 64 dataHandle(props.chartConfig.option.dataset)
62 let satellite = new AMap.TileLayer.Satellite() 65 let satellite = new AMap.TileLayer.Satellite()
@@ -68,7 +71,7 @@ const initMap = (newData: any) => { @@ -68,7 +71,7 @@ const initMap = (newData: any) => {
68 mapIns.setMapStyle(`amap://styles/${amapStyleKeyCustom.value !== '' ? amapStyleKeyCustom.value : amapStyleKey.value}`) 71 mapIns.setMapStyle(`amap://styles/${amapStyleKeyCustom.value !== '' ? amapStyleKeyCustom.value : amapStyleKey.value}`)
69 } 72 }
70 }) 73 })
71 - .catch(e => {}) 74 + .catch(e => { })
72 } 75 }
73 76
74 const dataHandle = (newData: any) => { 77 const dataHandle = (newData: any) => {
@@ -11,7 +11,10 @@ export const option = { @@ -11,7 +11,10 @@ export const option = {
11 dataset: dataJson, 11 dataset: dataJson,
12 mapRegion: { 12 mapRegion: {
13 adcode: 'china', 13 adcode: 'china',
14 - showHainanIsLands: true 14 + showHainanIsLands: true,
  15 + enter: false,
  16 + backSize: 20,
  17 + backColor: '#ffffff'
15 }, 18 },
16 tooltip: { 19 tooltip: {
17 show: true, 20 show: true,
@@ -103,19 +106,19 @@ export const option = { @@ -103,19 +106,19 @@ export const option = {
103 borderColor: 'rgba(147, 235, 248, 0.8)', 106 borderColor: 'rgba(147, 235, 248, 0.8)',
104 textStyle: { 107 textStyle: {
105 color: '#FFFFFF', 108 color: '#FFFFFF',
106 - fontSize: 12, 109 + fontSize: 12
107 } 110 }
108 }, 111 },
109 label: { 112 label: {
110 show: false, 113 show: false,
111 color: '#FFFFFF', 114 color: '#FFFFFF',
112 - fontSize: 12, 115 + fontSize: 12
113 }, 116 },
114 emphasis: { 117 emphasis: {
115 disabled: false, 118 disabled: false,
116 label: { 119 label: {
117 color: '#FFFFFF', 120 color: '#FFFFFF',
118 - fontSize: 12, 121 + fontSize: 12
119 }, 122 },
120 itemStyle: { 123 itemStyle: {
121 areaColor: '#389BB7', 124 areaColor: '#389BB7',
@@ -148,6 +151,26 @@ export const option = { @@ -148,6 +151,26 @@ export const option = {
148 shadowOffsetY: 2, 151 shadowOffsetY: 2,
149 shadowBlur: 10 152 shadowBlur: 10
150 } 153 }
  154 + },
  155 + {
  156 + type: 'lines',
  157 + zlevel: 2,
  158 + effect: {
  159 + show: true,
  160 + period: 4, //箭头指向速度,值越小速度越快
  161 + trailLength: 0.4, //特效尾迹长度[0,1]值越大,尾迹越长重
  162 + symbol: 'arrow', //箭头图标
  163 + symbolSize: 7 //图标大小
  164 + },
  165 + lineStyle: {
  166 + normal: {
  167 + color: '#4fb6d2',
  168 + width: 1, //线条宽度
  169 + opacity: 0.1, //尾迹线条透明度
  170 + curveness: 0.3 //尾迹线条曲直度
  171 + }
  172 + },
  173 + data: []
151 } 174 }
152 ] 175 ]
153 } 176 }
@@ -69,11 +69,7 @@ @@ -69,11 +69,7 @@
69 </n-space> 69 </n-space>
70 </SettingItem> 70 </SettingItem>
71 <SettingItem name="字体颜色"> 71 <SettingItem name="字体颜色">
72 - <n-color-picker  
73 - size="small"  
74 - :modes="['hex']"  
75 - v-model:value="seriesList[1].label.color"  
76 - ></n-color-picker> 72 + <n-color-picker size="small" :modes="['hex']" v-model:value="seriesList[1].label.color"></n-color-picker>
77 </SettingItem> 73 </SettingItem>
78 <SettingItem name="字体大小"> 74 <SettingItem name="字体大小">
79 <n-input-number 75 <n-input-number
@@ -129,7 +125,7 @@ @@ -129,7 +125,7 @@
129 ></n-color-picker> 125 ></n-color-picker>
130 </SettingItem> 126 </SettingItem>
131 </SettingItemBox> 127 </SettingItemBox>
132 - 128 +
133 <SettingItemBox name="悬浮弹窗"> 129 <SettingItemBox name="悬浮弹窗">
134 <SettingItem name="显示"> 130 <SettingItem name="显示">
135 <n-space> 131 <n-space>
@@ -180,6 +176,22 @@ @@ -180,6 +176,22 @@
180 <SettingItem> 176 <SettingItem>
181 <n-checkbox v-model:checked="mapRegion.showHainanIsLands" size="small">显示南海群岛</n-checkbox> 177 <n-checkbox v-model:checked="mapRegion.showHainanIsLands" size="small">显示南海群岛</n-checkbox>
182 </SettingItem> 178 </SettingItem>
  179 + <SettingItem v-if="seriesList[2]">
  180 + <n-checkbox v-model:checked="mapRegion.enter" size="small">点击进入下级</n-checkbox>
  181 + </SettingItem>
  182 + </SettingItemBox>
  183 + <SettingItemBox name="返回图标" v-if="mapRegion.enter">
  184 + <SettingItem name="颜色">
  185 + <n-color-picker size="small" :modes="['hex']" v-model:value="mapRegion.backColor"></n-color-picker>
  186 + </SettingItem>
  187 + <SettingItem name="大小">
  188 + <n-input-number
  189 + v-model:value="mapRegion.backSize"
  190 + :min="1"
  191 + size="small"
  192 + placeholder="请输入字体大小"
  193 + ></n-input-number>
  194 + </SettingItem>
183 </SettingItemBox> 195 </SettingItemBox>
184 </CollapseItem> 196 </CollapseItem>
185 <CollapseItem name="标记" :expanded="true"> 197 <CollapseItem name="标记" :expanded="true">
@@ -191,7 +203,7 @@ @@ -191,7 +203,7 @@
191 <n-color-picker size="small" :modes="['hex']" v-model:value="seriesList[0].itemStyle.color"></n-color-picker> 203 <n-color-picker size="small" :modes="['hex']" v-model:value="seriesList[0].itemStyle.color"></n-color-picker>
192 </SettingItem> 204 </SettingItem>
193 </SettingItemBox> 205 </SettingItemBox>
194 - 206 +
195 <SettingItemBox name="文本"> 207 <SettingItemBox name="文本">
196 <SettingItem name="显示"> 208 <SettingItem name="显示">
197 <n-space> 209 <n-space>
@@ -223,6 +235,47 @@ @@ -223,6 +235,47 @@
223 </SettingItem> 235 </SettingItem>
224 </SettingItemBox> 236 </SettingItemBox>
225 </CollapseItem> 237 </CollapseItem>
  238 +
  239 + <CollapseItem v-if="seriesList[2]" name="飞线" :expanded="true">
  240 + <SettingItemBox name="箭头">
  241 + <SettingItem name="速度">
  242 + <n-tooltip trigger="hover">
  243 + <template #trigger>
  244 + <n-input-number v-model:value="seriesList[2].effect.period" size="small" :min="0"></n-input-number>
  245 + </template>
  246 + 值越小速度越快
  247 + </n-tooltip>
  248 + </SettingItem>
  249 + <SettingItem name="尾迹">
  250 + <n-tooltip trigger="hover">
  251 + <template #trigger>
  252 + <n-input-number
  253 + v-model:value="seriesList[2].effect.trailLength"
  254 + size="small"
  255 + :min="0"
  256 + :max="1"
  257 + ></n-input-number>
  258 + </template>
  259 + 特效尾迹长度[0,1]值越大,尾迹越长重
  260 + </n-tooltip>
  261 + </SettingItem>
  262 + <SettingItem name="大小">
  263 + <n-input-number v-model:value="seriesList[2].effect.symbolSize" size="small" :min="0"></n-input-number>
  264 + </SettingItem>
  265 + </SettingItemBox>
  266 + <SettingItemBox name="配置">
  267 + <SettingItem name="颜色">
  268 + <n-color-picker
  269 + size="small"
  270 + :modes="['hex']"
  271 + v-model:value="seriesList[2].lineStyle.normal.color"
  272 + ></n-color-picker>
  273 + </SettingItem>
  274 + <SettingItem name="宽度">
  275 + <n-input-number v-model:value="seriesList[2].lineStyle.normal.width" size="small" :min="1"></n-input-number>
  276 + </SettingItem>
  277 + </SettingItemBox>
  278 + </CollapseItem>
226 </template> 279 </template>
227 280
228 <script setup lang="ts"> 281 <script setup lang="ts">
@@ -21,6 +21,32 @@ @@ -21,6 +21,32 @@
21 "value": [126.642464, 45.756967, 101] 21 "value": [126.642464, 45.756967, 101]
22 } 22 }
23 ], 23 ],
  24 + "line": [
  25 + {
  26 + "coords": [
  27 + [113.665412, 34.757975],
  28 + [116.405285, 39.904989]
  29 + ]
  30 + },
  31 + {
  32 + "coords": [
  33 + [101.778916, 36.623178],
  34 + [116.405285, 39.904989]
  35 + ]
  36 + },
  37 + {
  38 + "coords": [
  39 + [106.278179, 38.46637],
  40 + [116.405285, 39.904989]
  41 + ]
  42 + },
  43 + {
  44 + "coords": [
  45 + [126.642464, 45.756967],
  46 + [116.405285, 39.904989]
  47 + ]
  48 + }
  49 + ],
24 "map": [ 50 "map": [
25 { 51 {
26 "name": "北京市", 52 "name": "北京市",
1 <template> 1 <template>
2 - <v-chart ref="vChartRef" :init-options="initOptions" :theme="themeColor" :option="option.value" :manual-update="isPreview()" autoresize>  
3 - </v-chart> 2 + <div>
  3 + <div class="back-icon" v-if="(enter && levelHistory.length !== 0) || (enter && !isPreview())" @click="backLevel">
  4 + <n-icon :color="backColor" :size="backSize * 1.1">
  5 + <ArrowBackIcon />
  6 + </n-icon>
  7 + <span
  8 + :style="{
  9 + 'font-weight': 200,
  10 + color: backColor,
  11 + 'font-size': `${backSize}px`
  12 + }"
  13 + >
  14 + 返回上级
  15 + </span>
  16 + </div>
  17 + <v-chart
  18 + ref="vChartRef"
  19 + :init-options="initOptions"
  20 + :theme="themeColor"
  21 + :option="option.value"
  22 + :manual-update="isPreview()"
  23 + autoresize
  24 + @click="chartPEvents"
  25 + >
  26 + </v-chart>
  27 + </div>
4 </template> 28 </template>
5 29
6 <script setup lang="ts"> 30 <script setup lang="ts">
7 -import { PropType, reactive, watch, ref, nextTick } from 'vue' 31 +import { PropType, reactive, watch, ref, nextTick, toRefs } from 'vue'
8 import config, { includes } from './config' 32 import config, { includes } from './config'
9 import VChart from 'vue-echarts' 33 import VChart from 'vue-echarts'
  34 +import { icon } from '@/plugins'
10 import { useCanvasInitOptions } from '@/hooks/useCanvasInitOptions.hook' 35 import { useCanvasInitOptions } from '@/hooks/useCanvasInitOptions.hook'
11 import { use, registerMap } from 'echarts/core' 36 import { use, registerMap } from 'echarts/core'
12 import { EffectScatterChart, MapChart } from 'echarts/charts' 37 import { EffectScatterChart, MapChart } from 'echarts/charts'
@@ -16,6 +41,7 @@ import { mergeTheme, setOption } from '@/packages/public/chart' @@ -16,6 +41,7 @@ import { mergeTheme, setOption } from '@/packages/public/chart'
16 import { useChartEditStore } from '@/store/modules/chartEditStore/chartEditStore' 41 import { useChartEditStore } from '@/store/modules/chartEditStore/chartEditStore'
17 import { isPreview } from '@/utils' 42 import { isPreview } from '@/utils'
18 import mapJsonWithoutHainanIsLands from './mapWithoutHainanIsLands.json' 43 import mapJsonWithoutHainanIsLands from './mapWithoutHainanIsLands.json'
  44 +import mapChinaJson from './mapGeojson/china.json'
19 import { DatasetComponent, GridComponent, TooltipComponent, GeoComponent, VisualMapComponent } from 'echarts/components' 45 import { DatasetComponent, GridComponent, TooltipComponent, GeoComponent, VisualMapComponent } from 'echarts/components'
20 46
21 const props = defineProps({ 47 const props = defineProps({
@@ -33,6 +59,10 @@ const props = defineProps({ @@ -33,6 +59,10 @@ const props = defineProps({
33 } 59 }
34 }) 60 })
35 61
  62 +const { ArrowBackIcon } = icon.ionicons5
  63 +let levelHistory: any = ref([])
  64 +
  65 +const { backColor, backSize, enter } = toRefs(props.chartConfig.option.mapRegion)
36 const initOptions = useCanvasInitOptions(props.chartConfig.option, props.themeSetting) 66 const initOptions = useCanvasInitOptions(props.chartConfig.option, props.themeSetting)
37 67
38 use([ 68 use([
@@ -67,7 +97,7 @@ registerMap(`${props.chartConfig.option.mapRegion.adcode}`, { geoJSON: {} as any @@ -67,7 +97,7 @@ registerMap(`${props.chartConfig.option.mapRegion.adcode}`, { geoJSON: {} as any
67 // 进行更换初始化地图 如果为china 单独处理 97 // 进行更换初始化地图 如果为china 单独处理
68 const registerMapInitAsync = async () => { 98 const registerMapInitAsync = async () => {
69 await nextTick() 99 await nextTick()
70 - const adCode = `${props.chartConfig.option.mapRegion.adcode}`; 100 + const adCode = `${props.chartConfig.option.mapRegion.adcode}`
71 if (adCode !== 'china') { 101 if (adCode !== 'china') {
72 await getGeojson(adCode) 102 await getGeojson(adCode)
73 } else { 103 } else {
@@ -87,7 +117,16 @@ const vEchartsSetOption = () => { @@ -87,7 +117,16 @@ const vEchartsSetOption = () => {
87 const dataSetHandle = async (dataset: any) => { 117 const dataSetHandle = async (dataset: any) => {
88 props.chartConfig.option.series.forEach((item: any) => { 118 props.chartConfig.option.series.forEach((item: any) => {
89 if (item.type === 'effectScatter' && dataset.point) item.data = dataset.point 119 if (item.type === 'effectScatter' && dataset.point) item.data = dataset.point
90 - else if (item.type === 'map' && dataset.map) item.data = dataset.map 120 + else if (item.type === 'lines' && dataset.line) {
  121 + item.data = dataset.line.map((it: any) => {
  122 + return {
  123 + ...it,
  124 + lineStyle: {
  125 + color: props.chartConfig.option.series[2].lineStyle.normal.color
  126 + }
  127 + }
  128 + })
  129 + } else if (item.type === 'map' && dataset.map) item.data = dataset.map
91 }) 130 })
92 if (dataset.pieces) props.chartConfig.option.visualMap.pieces = dataset.pieces 131 if (dataset.pieces) props.chartConfig.option.visualMap.pieces = dataset.pieces
93 132
@@ -101,6 +140,45 @@ const hainanLandsHandle = async (newData: boolean) => { @@ -101,6 +140,45 @@ const hainanLandsHandle = async (newData: boolean) => {
101 registerMap('china', { geoJSON: mapJsonWithoutHainanIsLands as any, specialAreas: {} }) 140 registerMap('china', { geoJSON: mapJsonWithoutHainanIsLands as any, specialAreas: {} })
102 } 141 }
103 } 142 }
  143 +
  144 +// 点击区域
  145 +const chartPEvents = (e: any) => {
  146 + if (e.seriesType !== 'map') return
  147 + if (!props.chartConfig.option.mapRegion.enter) {
  148 + return
  149 + }
  150 + mapChinaJson.features.forEach(item => {
  151 + var pattern = new RegExp(e.name)
  152 + if (pattern.test(item.properties.name)) {
  153 + let code = String(item.properties.adcode)
  154 + levelHistory.value.push(code)
  155 + checkOrMap(code)
  156 + }
  157 + })
  158 +}
  159 +
  160 +// 返回上一级
  161 +const backLevel = () => {
  162 + levelHistory.value = []
  163 + if (levelHistory.value.length > 1) {
  164 + levelHistory.value.pop()
  165 + const code = levelHistory[levelHistory.value.length - 1]
  166 + checkOrMap(code)
  167 + } else {
  168 + checkOrMap('china')
  169 + }
  170 +}
  171 +
  172 +// 切换地图
  173 +const checkOrMap = async (newData: string) => {
  174 + await getGeojson(newData)
  175 + props.chartConfig.option.geo.map = newData
  176 + props.chartConfig.option.series.forEach((item: any) => {
  177 + if (item.type === 'map') item.map = newData
  178 + })
  179 + vEchartsSetOption()
  180 +}
  181 +
104 //监听 dataset 数据发生变化 182 //监听 dataset 数据发生变化
105 watch( 183 watch(
106 () => props.chartConfig.option.dataset, 184 () => props.chartConfig.option.dataset,
@@ -113,33 +191,42 @@ watch( @@ -113,33 +191,42 @@ watch(
113 } 191 }
114 ) 192 )
115 193
116 -//监听是否显示南海群岛  
117 -watch(  
118 - () => props.chartConfig.option.mapRegion.showHainanIsLands,  
119 - async newData => {  
120 - try {  
121 - await hainanLandsHandle(newData)  
122 - vEchartsSetOption()  
123 - } catch (error) {  
124 - console.log(error) 194 +// 监听线的颜色
  195 +if (props.chartConfig.option.series[2] && !isPreview()) {
  196 + watch(
  197 + () => props.chartConfig.option.series[2].lineStyle.normal.color,
  198 + () => {
  199 + dataSetHandle(props.chartConfig.option.dataset)
  200 + },
  201 + {
  202 + deep: false
125 } 203 }
126 - },  
127 - {  
128 - deep: false  
129 - }  
130 -) 204 + )
  205 +}
131 206
  207 +//监听是否显示南海群岛
  208 +if (!isPreview()) {
  209 + watch(
  210 + () => props.chartConfig.option.mapRegion.showHainanIsLands,
  211 + async newData => {
  212 + try {
  213 + await hainanLandsHandle(newData)
  214 + vEchartsSetOption()
  215 + } catch (error) {
  216 + console.log(error)
  217 + }
  218 + },
  219 + {
  220 + deep: false
  221 + }
  222 + )
  223 +}
132 //监听地图展示区域发生变化 224 //监听地图展示区域发生变化
133 watch( 225 watch(
134 () => `${props.chartConfig.option.mapRegion.adcode}`, 226 () => `${props.chartConfig.option.mapRegion.adcode}`,
135 - async newData => { 227 + newData => {
136 try { 228 try {
137 - await getGeojson(newData)  
138 - props.chartConfig.option.geo.map = newData  
139 - props.chartConfig.option.series.forEach((item: any) => {  
140 - if (item.type === 'map') item.map = newData  
141 - })  
142 - vEchartsSetOption() 229 + checkOrMap(newData)
143 } catch (error) { 230 } catch (error) {
144 console.log(error) 231 console.log(error)
145 } 232 }
@@ -154,3 +241,16 @@ useChartDataFetch(props.chartConfig, useChartEditStore, (newData: any) => { @@ -154,3 +241,16 @@ useChartDataFetch(props.chartConfig, useChartEditStore, (newData: any) => {
154 dataSetHandle(newData) 241 dataSetHandle(newData)
155 }) 242 })
156 </script> 243 </script>
  244 +
  245 +<style scope lang="scss">
  246 +.back-icon {
  247 + z-index: 50;
  248 + cursor: pointer;
  249 + position: absolute;
  250 + display: flex;
  251 + align-items: center;
  252 + top: 0;
  253 + left: 0;
  254 + gap: 2px;
  255 +}
  256 +</style>
@@ -9,7 +9,8 @@ export const includes = [] @@ -9,7 +9,8 @@ export const includes = []
9 // 关系图布局 9 // 关系图布局
10 export const GraphLayout = [ 10 export const GraphLayout = [
11 { label: '无', value: 'none' }, 11 { label: '无', value: 'none' },
12 - { label: '环形', value: 'circular' } 12 + { label: '环形', value: 'circular' },
  13 + { label: '力引导', value: 'force' }
13 ] 14 ]
14 15
15 // 标签开关 16 // 标签开关
@@ -24,7 +25,13 @@ export const LabelPosition = [ @@ -24,7 +25,13 @@ export const LabelPosition = [
24 { label: '右侧', value: 'right' }, 25 { label: '右侧', value: 'right' },
25 { label: '顶部', value: 'top' }, 26 { label: '顶部', value: 'top' },
26 { label: '底部', value: 'bottom' }, 27 { label: '底部', value: 'bottom' },
27 - { label: '内部', value: 'inside' }, 28 + { label: '内部', value: 'inside' }
  29 +]
  30 +
  31 +// 图-迭代动画
  32 +export const LayoutAnimation = [
  33 + { label: '开启', value: 1 },
  34 + { label: '关闭', value: 0 }
28 ] 35 ]
29 36
30 export const option = { 37 export const option = {
@@ -33,11 +40,11 @@ export const option = { @@ -33,11 +40,11 @@ export const option = {
33 legend:{ 40 legend:{
34 show:true, 41 show:true,
35 textStyle:{ 42 textStyle:{
36 - color:"#eee",  
37 - fontSize: 14 , 43 + color: '#eee',
  44 + fontSize: 14
38 }, 45 },
39 data: dataJson.categories.map(function (a) { 46 data: dataJson.categories.map(function (a) {
40 - return a.name; 47 + return a.name
41 }) 48 })
42 }, 49 },
43 series: [ 50 series: [
@@ -47,7 +54,7 @@ export const option = { @@ -47,7 +54,7 @@ export const option = {
47 data: dataJson.nodes, 54 data: dataJson.nodes,
48 links: dataJson.links, 55 links: dataJson.links,
49 categories: dataJson.categories, 56 categories: dataJson.categories,
50 - label: { // 标签 57 + label: {
51 show: 1, 58 show: 1,
52 position: 'right', 59 position: 'right',
53 formatter: '{b}' 60 formatter: '{b}'
@@ -58,10 +65,17 @@ export const option = { @@ -58,10 +65,17 @@ export const option = {
58 lineStyle: { 65 lineStyle: {
59 color: 'source', // 线条颜色 66 color: 'source', // 线条颜色
60 curveness: 0.2 // 线条卷曲程度 67 curveness: 0.2 // 线条卷曲程度
  68 + },
  69 + force: {
  70 + repulsion: 100,
  71 + gravity: 0.1,
  72 + edgeLength: 30,
  73 + layoutAnimation: 1,
  74 + friction: 0.6
61 } 75 }
62 } 76 }
63 ] 77 ]
64 - }; 78 +}
65 79
66 export default class Config extends PublicConfigClass implements CreateComponentType { 80 export default class Config extends PublicConfigClass implements CreateComponentType {
67 public key = GraphConfig.key 81 public key = GraphConfig.key
@@ -35,7 +35,58 @@ @@ -35,7 +35,58 @@
35 ></n-color-picker> 35 ></n-color-picker>
36 </SettingItem> 36 </SettingItem>
37 <SettingItem name="文本"> 37 <SettingItem name="文本">
38 - <n-input-number v-model:value="optionData.legend.textStyle.fontSize" :min="0" :step="1" size="small" placeholder="文字大小"> 38 + <n-input-number
  39 + v-model:value="optionData.legend.textStyle.fontSize"
  40 + :min="0"
  41 + :step="1"
  42 + size="small"
  43 + placeholder="文字大小"
  44 + >
  45 + </n-input-number>
  46 + </SettingItem>
  47 + </SettingItemBox>
  48 + <SettingItemBox name="力引导" v-if="optionData.series[0].force && graphConfig.layout == 'force'">
  49 + <SettingItem name="斥力因子" v-if="optionData.series[0].force.repulsion">
  50 + <n-input-number
  51 + v-model:value="optionData.series[0].force.repulsion"
  52 + :min="0"
  53 + :step="1"
  54 + size="small"
  55 + placeholder="斥力因子大小"
  56 + >
  57 + </n-input-number>
  58 + </SettingItem>
  59 + <SettingItem name="引力因子" v-if="optionData.series[0].force.gravity">
  60 + <n-input-number
  61 + v-model:value="optionData.series[0].force.gravity"
  62 + :min="0"
  63 + :step="0.1"
  64 + size="small"
  65 + placeholder="引力因子"
  66 + >
  67 + </n-input-number>
  68 + </SettingItem>
  69 + <SettingItem name="节点距离">
  70 + <n-input-number
  71 + v-model:value="optionData.series[0].force.edgeLength"
  72 + :min="0"
  73 + :step="1"
  74 + size="small"
  75 + placeholder="节点距离"
  76 + >
  77 + </n-input-number>
  78 + </SettingItem>
  79 + <SettingItem name="迭代动画">
  80 + <n-select v-model:value="graphConfig.force.layoutAnimation" :options="LayoutAnimation" size="small" />
  81 + </SettingItem>
  82 + <SettingItem name="节点速度">
  83 + <n-input-number
  84 + v-model:value="optionData.series[0].force.friction"
  85 + :min="0"
  86 + :step="0.1"
  87 + size="small"
  88 + placeholder="节点速度"
  89 + >
39 </n-input-number> 90 </n-input-number>
40 </SettingItem> 91 </SettingItem>
41 </SettingItemBox> 92 </SettingItemBox>
@@ -46,7 +97,7 @@ @@ -46,7 +97,7 @@
46 <script setup lang="ts"> 97 <script setup lang="ts">
47 import { PropType, computed } from 'vue' 98 import { PropType, computed } from 'vue'
48 import { CollapseItem, SettingItemBox, SettingItem } from '@/components/Pages/ChartItemSetting' 99 import { CollapseItem, SettingItemBox, SettingItem } from '@/components/Pages/ChartItemSetting'
49 -import { option, GraphLayout, LabelSwitch, LabelPosition } from './config' 100 +import { option, GraphLayout, LabelSwitch, LabelPosition, LayoutAnimation } from './config'
50 import { GlobalThemeJsonType } from '@/settings/chartThemes/index' 101 import { GlobalThemeJsonType } from '@/settings/chartThemes/index'
51 102
52 const props = defineProps({ 103 const props = defineProps({
@@ -56,7 +107,7 @@ const props = defineProps({ @@ -56,7 +107,7 @@ const props = defineProps({
56 } 107 }
57 }) 108 })
58 109
59 -const graphConfig = computed<typeof option.series[0]>(() => { 110 +const graphConfig = computed<(typeof option.series)[0]>(() => {
60 return props.optionData.series[0] 111 return props.optionData.series[0]
61 }) 112 })
62 </script> 113 </script>
1 <template> 1 <template>
2 - <v-chart ref="vChartRef" :init-options="initOptions" :theme="themeColor" :option="option" :manual-update="isPreview()" autoresize></v-chart> 2 + <v-chart
  3 + ref="vChartRef"
  4 + :init-options="initOptions"
  5 + :theme="themeColor"
  6 + :option="option"
  7 + :manual-update="isPreview()"
  8 + autoresize
  9 + ></v-chart>
3 </template> 10 </template>
4 11
5 <script setup lang="ts"> 12 <script setup lang="ts">
@@ -8,4 +8,14 @@ import { DialConfig } from './Dial/index' @@ -8,4 +8,14 @@ import { DialConfig } from './Dial/index'
8 import { SankeyConfig } from './Sankey/index' 8 import { SankeyConfig } from './Sankey/index'
9 import { GraphConfig } from './Graph/index' 9 import { GraphConfig } from './Graph/index'
10 10
11 -export default [ProcessConfig, RadarConfig, FunnelConfig, HeatmapConfig, WaterPoloConfig, TreeMapConfig, GraphConfig, SankeyConfig, DialConfig] 11 +export default [
  12 + ProcessConfig,
  13 + RadarConfig,
  14 + FunnelConfig,
  15 + HeatmapConfig,
  16 + WaterPoloConfig,
  17 + TreeMapConfig,
  18 + GraphConfig,
  19 + SankeyConfig,
  20 + DialConfig
  21 +]
@@ -33,8 +33,6 @@ const option = shallowReactive({ @@ -33,8 +33,6 @@ const option = shallowReactive({
33 dataset: '' 33 dataset: ''
34 }) 34 })
35 35
36 -  
37 -  
38 const getStyle = (radius: number) => { 36 const getStyle = (radius: number) => {
39 return { 37 return {
40 borderRadius: `${radius}px`, 38 borderRadius: `${radius}px`,
@@ -78,7 +76,6 @@ watch( @@ -78,7 +76,6 @@ watch(
78 return 76 return
79 } 77 }
80 option.dataset = newData 78 option.dataset = newData
81 -  
82 }, 79 },
83 { 80 {
84 immediate: true 81 immediate: true
1 import { ChartFrameEnum, PackagesCategoryEnum } from '@/packages/index.d' 1 import { ChartFrameEnum, PackagesCategoryEnum } from '@/packages/index.d'
2 import { ImageConfig } from '@/packages/components/Informations/Mores/Image/index' 2 import { ImageConfig } from '@/packages/components/Informations/Mores/Image/index'
3 import { ChatCategoryEnum, ChatCategoryEnumName } from '../index.d' 3 import { ChatCategoryEnum, ChatCategoryEnumName } from '../index.d'
  4 +import carousel1 from '@/assets/images/chart/photos/carousel1.jpeg'
  5 +import carousel2 from '@/assets/images/chart/photos/carousel2.jpeg'
4 6
5 // 远程共享库(调接口获取图像列表) 7 // 远程共享库(调接口获取图像列表)
6 const imageList = [ 8 const imageList = [
7 - { imageName: 'carousel1', imageUrl: 'https://naive-ui.oss-cn-beijing.aliyuncs.com/carousel-img/carousel1.jpeg' },  
8 - { imageName: 'carousel2', imageUrl: 'https://naive-ui.oss-cn-beijing.aliyuncs.com/carousel-img/carousel2.jpeg' } 9 + { imageName: 'carousel1', imageUrl: 'carousel1.jpeg' },
  10 + { imageName: 'carousel2', imageUrl: 'carousel2.jpeg' }
9 ] 11 ]
10 12
11 const photoConfigList = imageList.map(i => ({ 13 const photoConfigList = imageList.map(i => ({
@@ -15,7 +17,7 @@ const photoConfigList = imageList.map(i => ({ @@ -15,7 +17,7 @@ const photoConfigList = imageList.map(i => ({
15 package: PackagesCategoryEnum.PHOTOS, 17 package: PackagesCategoryEnum.PHOTOS,
16 chartFrame: ChartFrameEnum.STATIC, 18 chartFrame: ChartFrameEnum.STATIC,
17 image: i.imageUrl, 19 image: i.imageUrl,
18 - dataset: i.imageUrl, 20 + dataset: i.imageName==='carousel1' ? carousel1 : carousel2,
19 title: i.imageName, 21 title: i.imageName,
20 redirectComponent: `${ImageConfig.package}/${ImageConfig.category}/${ImageConfig.key}` // 跳转组件路径规则:packageName/categoryName/componentKey 22 redirectComponent: `${ImageConfig.package}/${ImageConfig.category}/${ImageConfig.key}` // 跳转组件路径规则:packageName/categoryName/componentKey
21 })) 23 }))
@@ -235,6 +235,7 @@ const updateVChart = async (newData: SocketReceiveMessageType) => { @@ -235,6 +235,7 @@ const updateVChart = async (newData: SocketReceiveMessageType) => {
235 } 235 }
236 236
237 const { vChartRef } = useChartDataFetch(props.chartConfig, useChartEditStore, (newData: any, targetComponent: any) => { 237 const { vChartRef } = useChartDataFetch(props.chartConfig, useChartEditStore, (newData: any, targetComponent: any) => {
  238 + props.chartConfig.option.queryCondition.timeRange=[targetComponent.requestParams.Params.startTs,targetComponent.requestParams.Params.endTs]
238 //联动支持分组 239 //联动支持分组
239 /** 240 /**
240 * 修复多个分组,然后下拉框联动,会影响另一个组件 241 * 修复多个分组,然后下拉框联动,会影响另一个组件
@@ -14,6 +14,26 @@ export const enum areaEnum { @@ -14,6 +14,26 @@ export const enum areaEnum {
14 TOWN = 'TOWN' //镇 14 TOWN = 'TOWN' //镇
15 } 15 }
16 16
  17 +//父级地区编码和级别接口
  18 +export interface HistoryParentType {
  19 + adcode: string | number
  20 + level: string
  21 +}
  22 +
  23 +//数据源接口
  24 +export interface dataPointI {
  25 + name: string
  26 + value: number[]
  27 + adcode: number
  28 + height: number
  29 + itemStyle: {
  30 + color: string
  31 + opacity: number
  32 + borderWidth: number
  33 + borderColor: string
  34 + }
  35 +}
  36 +
17 export const includes = [] 37 export const includes = []
18 38
19 export const option = { 39 export const option = {
@@ -23,6 +43,9 @@ export const option = { @@ -23,6 +43,9 @@ export const option = {
23 iconDistanceTop: 20, 43 iconDistanceTop: 20,
24 drillingIn: false, 44 drillingIn: false,
25 dataset: dataMaps, 45 dataset: dataMaps,
  46 + saveClickRegion: {
  47 + level: ''
  48 + },
26 mapRegion: { 49 mapRegion: {
27 adcode: 'china', 50 adcode: 'china',
28 showHainanIsLands: true, 51 showHainanIsLands: true,
@@ -41,6 +64,9 @@ export const option = { @@ -41,6 +64,9 @@ export const option = {
41 emphasis: { 64 emphasis: {
42 label: { 65 label: {
43 show: true, 66 show: true,
  67 + formatter: function (params: Recordable) {
  68 + return params.data.name ? params.data.name : ' '
  69 + },
44 textStyle: { 70 textStyle: {
45 color: '#000', 71 color: '#000',
46 fontSize: 14 72 fontSize: 14
@@ -59,6 +85,9 @@ export const option = { @@ -59,6 +85,9 @@ export const option = {
59 regionHeight: 3, 85 regionHeight: 3,
60 label: { 86 label: {
61 show: true, 87 show: true,
  88 + formatter: function (params: Recordable) {
  89 + return params.data.name ? params.data.name : ' '
  90 + },
62 textStyle: { 91 textStyle: {
63 color: '#fff', 92 color: '#fff',
64 fontSize: 14 93 fontSize: 14
@@ -12,7 +12,7 @@ import { onMounted, ref, nextTick, PropType, toRefs, watch, reactive } from 'vue @@ -12,7 +12,7 @@ import { onMounted, ref, nextTick, PropType, toRefs, watch, reactive } from 'vue
12 import * as echarts from 'echarts' 12 import * as echarts from 'echarts'
13 import { registerMap } from 'echarts/core' 13 import { registerMap } from 'echarts/core'
14 import 'echarts-gl' 14 import 'echarts-gl'
15 -import config, { areaEnum } from './config' 15 +import config, { areaEnum, dataPointI, HistoryParentType } from './config'
16 import { getGeoJsonMap } from '@/api/external/common' 16 import { getGeoJsonMap } from '@/api/external/common'
17 import dataMaps from './data.json' 17 import dataMaps from './data.json'
18 18
@@ -41,7 +41,7 @@ const toolBoxOption = ref({ @@ -41,7 +41,7 @@ const toolBoxOption = ref({
41 top: 20, 41 top: 20,
42 feature: { 42 feature: {
43 myFullButton: { 43 myFullButton: {
44 - show: true, 44 + show: false,
45 title: '返回', 45 title: '返回',
46 icon: iconStr.value, 46 icon: iconStr.value,
47 iconStyle: { 47 iconStyle: {
@@ -55,8 +55,34 @@ const toolBoxOption = ref({ @@ -55,8 +55,34 @@ const toolBoxOption = ref({
55 watch( 55 watch(
56 () => props.chartConfig.option, 56 () => props.chartConfig.option,
57 newData => { 57 newData => {
58 - const { iconColor, drillingIn, iconDistanceRight, iconDistanceTop } = newData  
59 - toolBoxOption.value.feature.myFullButton.show = drillingIn 58 + const { iconColor, drillingIn, iconDistanceRight, iconDistanceTop, mapRegion } = newData
  59 + if (drillingIn && !newData.saveClickRegion.level) {
  60 + toolBoxOption.value.feature.myFullButton.show = !drillingIn
  61 + } else if (
  62 + drillingIn &&
  63 + newData.saveClickRegion.level === areaEnum.PROVINCE &&
  64 + mapRegion.saveSelect.levelStr === areaEnum.COUNTRY
  65 + ) {
  66 + toolBoxOption.value.feature.myFullButton.show = drillingIn
  67 + } else if (
  68 + drillingIn &&
  69 + newData.saveClickRegion.level === areaEnum.COUNTRY &&
  70 + mapRegion.saveSelect.levelStr === areaEnum.COUNTRY
  71 + ) {
  72 + toolBoxOption.value.feature.myFullButton.show = !drillingIn
  73 + } else if (
  74 + drillingIn &&
  75 + newData.saveClickRegion.level === areaEnum.CITY &&
  76 + mapRegion.saveSelect.levelStr === areaEnum.PROVINCE
  77 + ) {
  78 + toolBoxOption.value.feature.myFullButton.show = drillingIn
  79 + } else if (
  80 + drillingIn &&
  81 + newData.saveClickRegion.level === areaEnum.PROVINCE &&
  82 + mapRegion.saveSelect.levelStr === areaEnum.PROVINCE
  83 + ) {
  84 + toolBoxOption.value.feature.myFullButton.show = !drillingIn
  85 + }
60 toolBoxOption.value.feature.myFullButton.iconStyle.color = iconColor 86 toolBoxOption.value.feature.myFullButton.iconStyle.color = iconColor
61 toolBoxOption.value.right = iconDistanceRight 87 toolBoxOption.value.right = iconDistanceRight
62 toolBoxOption.value.top = iconDistanceTop 88 toolBoxOption.value.top = iconDistanceTop
@@ -73,28 +99,27 @@ props.chartConfig.option = { @@ -73,28 +99,27 @@ props.chartConfig.option = {
73 99
74 //地图点击返回 100 //地图点击返回
75 const watchAdcode = async () => { 101 const watchAdcode = async () => {
  102 + stopWatch()
76 if (props.chartConfig.option.drillingIn) { 103 if (props.chartConfig.option.drillingIn) {
77 //如果是从右边配置里设置的,比如点击四川省,然后点击返回 104 //如果是从右边配置里设置的,比如点击四川省,然后点击返回
78 const savePopParent = saveHistoryParent.value.pop() 105 const savePopParent = saveHistoryParent.value.pop()
79 - let saveAdcode: any = savePopParent?.adcode  
80 - saveLevelStr.level = savePopParent?.level as string 106 + let saveAdcode = savePopParent?.adcode as string | number
  107 + saveLevelStr.level = savePopParent?.level
81 if (!savePopParent) { 108 if (!savePopParent) {
82 saveAdcode = getParentAdcode(props.chartConfig.option.mapRegion.adcode) 109 saveAdcode = getParentAdcode(props.chartConfig.option.mapRegion.adcode)
83 - saveLevelStr.level = (regionMapParentArea as any)[props.chartConfig.option.mapRegion.saveSelect.levelStr] 110 + saveLevelStr.level = (regionMapParentArea as Recordable)[props.chartConfig.option.mapRegion.saveSelect.levelStr]
84 } 111 }
85 if (saveAdcode === 0) { 112 if (saveAdcode === 0) {
86 saveAdcode = 'china' 113 saveAdcode = 'china'
87 saveLevelStr.level = 'COUNTRY' 114 saveLevelStr.level = 'COUNTRY'
88 } 115 }
89 - await getGeojson(saveAdcode) 116 + const exist = await getGeojson(saveAdcode)
90 const adcode = saveAdcode === 100000 ? 'china' : saveAdcode 117 const adcode = saveAdcode === 100000 ? 'china' : saveAdcode
91 - props.chartConfig.option.geo3D.map = adcode  
92 - props.chartConfig.option.series.forEach((item: any) => {  
93 - if (item.type === 'map3D') item.map = adcode  
94 - item.data = props.chartConfig.option.dataset  
95 - })  
96 - handleSetOption(chartInstance.value, props.chartConfig.option)  
97 - handleDataPoint(adcode) 118 + props.chartConfig.option.saveClickRegion.level = saveLevelStr.level
  119 + if (exist) {
  120 + //fix解决点击下钻返回后页面为空问题
  121 + props.chartConfig.option.mapRegion.adcode = adcode
  122 + }
98 } 123 }
99 } 124 }
100 125
@@ -106,46 +131,41 @@ const regionMapParentArea = { @@ -106,46 +131,41 @@ const regionMapParentArea = {
106 } 131 }
107 132
108 //地图点击 133 //地图点击
109 -const handleMap3DClick = async (params: any) => { 134 +const handleMap3DClick = async (params: Recordable) => {
110 if (props.chartConfig.option.drillingIn) { 135 if (props.chartConfig.option.drillingIn) {
111 const { name } = params 136 const { name } = params
112 - saveGeojson.value?.features.forEach((item: any) => { 137 + saveGeojson.value?.features.forEach((item: Recordable) => {
113 if (item.properties.name === name) { 138 if (item.properties.name === name) {
114 const level = item.properties.level.toUpperCase() 139 const level = item.properties.level.toUpperCase()
115 const adcode = item.properties.adcode 140 const adcode = item.properties.adcode
116 if (level === 'DISTRICT') return 141 if (level === 'DISTRICT') return
117 - if(String(adcode).startsWith('15') && level===areaEnum.CITY) return 142 + if (String(adcode).startsWith('15') && level === areaEnum.CITY) return
118 props.chartConfig.option.mapRegion.adcode = adcode 143 props.chartConfig.option.mapRegion.adcode = adcode
  144 + props.chartConfig.option.saveClickRegion.level = level
119 saveLevelStr.level = level 145 saveLevelStr.level = level
120 handleDataPoint(adcode) 146 handleDataPoint(adcode)
121 saveHistoryParent.value.push({ 147 saveHistoryParent.value.push({
122 adcode: item.properties.parent.adcode, 148 adcode: item.properties.parent.adcode,
123 - level: (regionMapParentArea as any)[level] 149 + level: (regionMapParentArea as Recordable)[level]
124 }) 150 })
125 } 151 }
126 }) 152 })
127 } 153 }
128 } 154 }
129 155
130 -const saveGeojson: any = ref({}) // 保存geojson 156 +const saveGeojson: Recordable = ref({}) // 保存geojson
131 157
132 const chinaDefaultRegionId = ref(100000) //如果是china则adcode为100000 158 const chinaDefaultRegionId = ref(100000) //如果是china则adcode为100000
133 159
134 -const saveLevelStr = reactive({  
135 - // 地区级别  
136 - level: '' 160 +// 保存地区级别
  161 +const saveLevelStr = reactive<{ level: string | undefined }>({
  162 + level: ''
137 }) 163 })
138 164
139 -//父级地区编码和级别接口  
140 -interface HistoryParentType {  
141 - adcode: number  
142 - level: string  
143 -}  
144 -  
145 const saveHistoryParent = ref<HistoryParentType[]>([]) 165 const saveHistoryParent = ref<HistoryParentType[]>([])
146 166
147 //动态注册地图 167 //动态注册地图
148 -const getGeojson = (regionId: any) => { 168 +const getGeojson = (regionId: number | string) => {
149 try { 169 try {
150 return new Promise<boolean>(resolve => { 170 return new Promise<boolean>(resolve => {
151 const { levelStr } = props.chartConfig.option.mapRegion.saveSelect //右侧配置项获取的行政级别 171 const { levelStr } = props.chartConfig.option.mapRegion.saveSelect //右侧配置项获取的行政级别
@@ -171,11 +191,10 @@ const getGeojson = (regionId: any) => { @@ -171,11 +191,10 @@ const getGeojson = (regionId: any) => {
171 } 191 }
172 } 192 }
173 193
174 -  
175 //传adcode 获取上级 194 //传adcode 获取上级
176 const getParentAdcode = (adcode: number) => { 195 const getParentAdcode = (adcode: number) => {
177 let adcodeNum = 100000 196 let adcodeNum = 100000
178 - saveGeojson.value?.features.forEach((item: any) => { 197 + saveGeojson.value?.features.forEach((item: Recordable) => {
179 if (item.properties.adcode === adcode) { 198 if (item.properties.adcode === adcode) {
180 adcodeNum = item.properties.parent.adcode 199 adcodeNum = item.properties.parent.adcode
181 } 200 }
@@ -200,13 +219,13 @@ const initMap = async () => { @@ -200,13 +219,13 @@ const initMap = async () => {
200 await nextTick().then(() => { 219 await nextTick().then(() => {
201 handleSetOption(chartInstance.value, props.chartConfig.option) 220 handleSetOption(chartInstance.value, props.chartConfig.option)
202 }) 221 })
203 - chartInstance.value.on('click', (e: any) => { 222 + chartInstance.value.on('click', (e: Recordable) => {
204 handleMap3DClick(e) 223 handleMap3DClick(e)
205 }) 224 })
206 } 225 }
207 226
208 // 手动触发渲染 227 // 手动触发渲染
209 -const handleSetOption = (instance: any, option: any) => { 228 +const handleSetOption = (instance: any, option: Recordable) => {
210 if (!instance) return 229 if (!instance) return
211 try { 230 try {
212 instance.clear() 231 instance.clear()
@@ -221,22 +240,32 @@ onMounted(() => { @@ -221,22 +240,32 @@ onMounted(() => {
221 }) 240 })
222 241
223 //处理数据标点 242 //处理数据标点
224 -const handleDataPoint = (newData: any) => { 243 +const handleDataPoint = (newData: string | number) => {
225 if (newData === 'china') { 244 if (newData === 'china') {
226 props.chartConfig.option.dataset = dataMaps 245 props.chartConfig.option.dataset = dataMaps
  246 + props.chartConfig.option.series.forEach((item: Recordable) => {
  247 + if (item.type === 'scatter3D') {
  248 + item.data = dataMaps
  249 + }
  250 + })
227 } else { 251 } else {
228 - props.chartConfig.option.dataset = dataMaps.filter((item: any) => item.adcode === newData) 252 + props.chartConfig.option.dataset = dataMaps.filter((item: dataPointI) => item.adcode === newData)
  253 + props.chartConfig.option.series.forEach((item: Recordable) => {
  254 + if (item.type === 'scatter3D') {
  255 + item.data = dataMaps.filter((item: dataPointI) => item.adcode === newData)
  256 + }
  257 + })
229 } 258 }
230 } 259 }
231 260
232 //监听地图展示区域发生变化 261 //监听地图展示区域发生变化
233 watch( 262 watch(
234 () => `${props.chartConfig.option.mapRegion.adcode}`, 263 () => `${props.chartConfig.option.mapRegion.adcode}`,
235 - async (newData: any) => { 264 + async (newData: string | number) => {
236 try { 265 try {
237 await getGeojson(newData) 266 await getGeojson(newData)
238 props.chartConfig.option.geo3D.map = newData 267 props.chartConfig.option.geo3D.map = newData
239 - props.chartConfig.option.series.forEach((item: any) => { 268 + props.chartConfig.option.series.forEach((item: Recordable) => {
240 if (item.type === 'map3D') { 269 if (item.type === 'map3D') {
241 item.map = newData 270 item.map = newData
242 item.data = props.chartConfig.option.dataset 271 item.data = props.chartConfig.option.dataset
@@ -254,7 +283,7 @@ watch( @@ -254,7 +283,7 @@ watch(
254 ) 283 )
255 284
256 // 监听地图右侧配置项变化 285 // 监听地图右侧配置项变化
257 -watch( 286 +const stopWatch = watch(
258 props.chartConfig.option, 287 props.chartConfig.option,
259 async newData => { 288 async newData => {
260 try { 289 try {
@@ -273,7 +302,7 @@ watch( @@ -273,7 +302,7 @@ watch(
273 () => props.chartConfig.option.dataset, 302 () => props.chartConfig.option.dataset,
274 newData => { 303 newData => {
275 try { 304 try {
276 - props.chartConfig.option.series.forEach((item: any) => { 305 + props.chartConfig.option.series.forEach((item: Recordable) => {
277 if (item.type === 'map3D') { 306 if (item.type === 'map3D') {
278 item.data = newData 307 item.data = newData
279 } 308 }
  1 +<template>
  2 + <n-drawer display-directive="if" v-model:show="modelShow" :width="502" :placement="placement">
  3 + <n-drawer-content :title="drawerTitle" closable>
  4 + <n-space vertical>
  5 + <n-data-table size="small" :columns="dimensions" :data="source" :pagination="paginationReactive" />
  6 + </n-space>
  7 + </n-drawer-content>
  8 + </n-drawer>
  9 +</template>
  10 +
  11 +<script lang="ts" setup>
  12 +import { ref, reactive } from 'vue'
  13 +import { NDataTable,NDrawer, NDrawerContent, NSpace } from 'naive-ui'
  14 +import { dimensions } from '../config'
  15 +import type { sourceInterface } from '../config'
  16 +import dayjs from 'dayjs'
  17 +import { Placement } from 'naive-ui/es/drawer/src/DrawerBodyWrapper'
  18 +
  19 +const placement = ref<Placement>('right')
  20 +
  21 +const modelShow =ref(false)
  22 +
  23 +const drawerTitle = ref('')
  24 +
  25 +const source =ref<sourceInterface[]>([])
  26 +
  27 +const paginationReactive = reactive({
  28 + page: 1,
  29 + pageSize: 10,
  30 + showSizePicker: true,
  31 + pageSizes: [10, 20],
  32 + onChange: (page: number) => {
  33 + paginationReactive.page = page
  34 + },
  35 + onUpdatePageSize: (pageSize: number) => {
  36 + paginationReactive.pageSize = pageSize
  37 + paginationReactive.page = 1
  38 + }
  39 + })
  40 +
  41 +const setValue = (value: Recordable, attrs: Recordable[]) => {
  42 + source.value = []
  43 + try {
  44 + if (!value) return
  45 + const deviceLatestList = Object.keys(value).map((item:string) => {
  46 + return {
  47 + key: item,
  48 + value: value[item][0]?.value,
  49 + lastUpdateTime: dayjs( value[item][0]?.ts)?.format('YYYY-MM-DD HH:mm:ss')
  50 + }
  51 + })
  52 + const list = deviceLatestList?.reduce((acc:sourceInterface[],curr: sourceInterface)=>{
  53 + const byKeyFindName = attrs?.find(item => item.identifier === curr.key)?.name
  54 + curr.key = byKeyFindName
  55 + acc.push({...curr})
  56 + return [...acc]
  57 + },[])
  58 + source.value = list.filter(item=>Boolean(item.key))
  59 + }catch(e){
  60 + console.log(e)
  61 + }
  62 +}
  63 +
  64 +const setDrawerTitle = (title:string) => drawerTitle.value = title
  65 +
  66 +const openDrawer = () => modelShow.value = true
  67 +
  68 +defineExpose({
  69 + setValue,
  70 + openDrawer,
  71 + setDrawerTitle
  72 +})
  73 +</script>
  74 +
  75 +<style lang="scss" scoped></style>
@@ -129,7 +129,7 @@ const loadList = async () => { @@ -129,7 +129,7 @@ const loadList = async () => {
129 } 129 }
130 130
131 const handleSubmit = () => { 131 const handleSubmit = () => {
132 - searchParams.deviceProfileIds = [searchParams.deviceProfileIds] as any 132 + // searchParams.deviceProfileIds = [searchParams.deviceProfileIds] as any
133 emit('searchParams', searchPage, searchParams) 133 emit('searchParams', searchPage, searchParams)
134 handleCancel() 134 handleCancel()
135 } 135 }
@@ -4,6 +4,7 @@ import { OverrideMapAmapConfig } from './index' @@ -4,6 +4,7 @@ import { OverrideMapAmapConfig } from './index'
4 import { chartInitConfig } from '@/settings/designSetting' 4 import { chartInitConfig } from '@/settings/designSetting'
5 import cloneDeep from 'lodash/cloneDeep' 5 import cloneDeep from 'lodash/cloneDeep'
6 import dataJson from './data.json' 6 import dataJson from './data.json'
  7 +import defaultImg from '../../../../../../assets/external/marker/1.png'
7 8
8 export type dataExtraInfoType = typeof dataJson.markers[number]['extraInfo'] //data.json下的extraInfo类型 9 export type dataExtraInfoType = typeof dataJson.markers[number]['extraInfo'] //data.json下的extraInfo类型
9 10
@@ -12,7 +13,7 @@ export type dataJsonType = typeof dataJson //data.json类型 @@ -12,7 +13,7 @@ export type dataJsonType = typeof dataJson //data.json类型
12 export type dataJsonMarkersType = typeof dataJson.markers[number] //data.json markers类型 13 export type dataJsonMarkersType = typeof dataJson.markers[number] //data.json markers类型
13 14
14 //标注数据格式 15 //标注数据格式
15 -export const fileterDevice = (items: any) => { 16 +export const filterDevice = (items: any) => {
16 const values = items.reduce((acc: any, curr: any) => { 17 const values = items.reduce((acc: any, curr: any) => {
17 acc.push({ 18 acc.push({
18 name: curr.alias, 19 name: curr.alias,
@@ -25,6 +26,7 @@ export const fileterDevice = (items: any) => { @@ -25,6 +26,7 @@ export const fileterDevice = (items: any) => {
25 organizationDTO: curr.organizationDTO, 26 organizationDTO: curr.organizationDTO,
26 deviceState: curr.deviceState, 27 deviceState: curr.deviceState,
27 deviceProfile: curr.deviceProfile, 28 deviceProfile: curr.deviceProfile,
  29 + deviceProfileId: curr.deviceProfileId,
28 deviceInfo: curr.deviceInfo 30 deviceInfo: curr.deviceInfo
29 } 31 }
30 }) 32 })
@@ -35,6 +37,34 @@ export const fileterDevice = (items: any) => { @@ -35,6 +37,34 @@ export const fileterDevice = (items: any) => {
35 } 37 }
36 } 38 }
37 39
  40 +export interface devicePartInfoInterface{
  41 + tbDeviceId:string
  42 + name:string
  43 + alias:string
  44 + deviceProfileId:string
  45 +}
  46 +
  47 +export const dimensions= [
  48 + {
  49 + "title": "键",
  50 + "key": "key"
  51 + },
  52 + {
  53 + "title": "值",
  54 + "key": "value"
  55 + },
  56 + {
  57 + "title": "最后更新时间",
  58 + "key": "lastUpdateTime"
  59 + }
  60 +]
  61 +
  62 +export interface sourceInterface {
  63 + key: string
  64 + value: string
  65 + lastUpdateTime: string
  66 +}
  67 +
38 export enum ThemeEnum { 68 export enum ThemeEnum {
39 NORMAL = 'normal', 69 NORMAL = 'normal',
40 DARK = 'dark', 70 DARK = 'dark',
@@ -88,6 +118,11 @@ export const option = { @@ -88,6 +118,11 @@ export const option = {
88 amapLon: 104.108689, 118 amapLon: 104.108689,
89 amapLat: 30.66176, 119 amapLat: 30.66176,
90 amapZindex: 11, 120 amapZindex: 11,
  121 + iconMarker: '1.png',
  122 + mpBorderConfig: {
  123 + value: 'Border01'
  124 + },
  125 + bgColor: 'rgba(255, 255, 255, 0.05)',
91 marker: { 126 marker: {
92 fillColor: '#E98984FF', 127 fillColor: '#E98984FF',
93 fillOpacity: 0.5, 128 fillOpacity: 0.5,
@@ -71,19 +71,60 @@ @@ -71,19 +71,60 @@
71 <setting-item name="类型"> 71 <setting-item name="类型">
72 <n-select size="small" v-model:value="optionData.mapOptions.mapMarkerType" :options="MarkerOptions" /> 72 <n-select size="small" v-model:value="optionData.mapOptions.mapMarkerType" :options="MarkerOptions" />
73 </setting-item> 73 </setting-item>
74 - <setting-item name="颜色"> 74 + <setting-item v-if="optionData.mapOptions.mapMarkerType === MarkerEnum.CIRCLE_MARKER" name="颜色">
75 <n-color-picker v-model:value="optionData.mapOptions.marker.fillColor" size="small"></n-color-picker> 75 <n-color-picker v-model:value="optionData.mapOptions.marker.fillColor" size="small"></n-color-picker>
76 </setting-item> 76 </setting-item>
77 </setting-item-box> 77 </setting-item-box>
  78 + <setting-item-box name="图标选择" v-if="optionData.mapOptions.mapMarkerType === MarkerEnum.MARKER">
  79 + <setting-item>
  80 + <NSelect
  81 + size="small"
  82 + placeholder="请选择您要使用的图标"
  83 + style="width: 250px"
  84 + :value="iconMarkerValue"
  85 + :options="iconMarkerOptions"
  86 + :render-label="renderOption"
  87 + clearable
  88 + filterable
  89 + @update:value="selectHandle"
  90 + />
  91 + </setting-item>
  92 + </setting-item-box>
  93 + <setting-item-box name="弹窗选择" v-if="optionData.mapOptions.mapMarkerType === MarkerEnum.MARKER">
  94 + <setting-item>
  95 + <NSelect
  96 + size="small"
  97 + placeholder="请选择您要使用的弹窗"
  98 + style="width: 250px"
  99 + :value="mapSelectBorderValue"
  100 + :options="mapBorderOptions"
  101 + :render-label="renderMapBorderOption"
  102 + clearable
  103 + filterable
  104 + @update:value="selectMapBorderHandle"
  105 + />
  106 + </setting-item>
  107 + </setting-item-box>
  108 + <setting-item-box name="弹窗背景" v-if="optionData.mapOptions.mapMarkerType === MarkerEnum.MARKER">
  109 + <div style="width: 10vw">
  110 + <n-color-picker :modes="['rgb']" v-model:value="optionData.mapOptions.bgColor" size="small"></n-color-picker>
  111 + </div>
  112 + </setting-item-box>
78 </collapse-item> 113 </collapse-item>
79 </template> 114 </template>
80 115
81 <script setup lang="ts"> 116 <script setup lang="ts">
82 -import { PropType } from 'vue' 117 +import { PropType, ref, computed, h, onMounted } from 'vue'
83 import { option, MarkerEnum, ThemeEnum, LangEnum, ViewModeEnum, FeaturesEnum } from './config' 118 import { option, MarkerEnum, ThemeEnum, LangEnum, ViewModeEnum, FeaturesEnum } from './config'
84 import { CollapseItem, SettingItemBox, SettingItem } from '@/components/Pages/ChartItemSetting' 119 import { CollapseItem, SettingItemBox, SettingItem } from '@/components/Pages/ChartItemSetting'
  120 +import { NEllipsis, NImage, NSelect, NSpace, SelectOption } from 'naive-ui'
  121 +import { getUrlBase64 } from '@/utils/external/imageUrlToBase64'
85 122
86 -defineProps({ 123 +interface BaseSelectBorderIF extends SelectOption {
  124 + config?: string
  125 +}
  126 +
  127 +const props = defineProps({
87 optionData: { 128 optionData: {
88 type: Object as PropType<typeof option>, 129 type: Object as PropType<typeof option>,
89 required: true 130 required: true
@@ -187,10 +228,10 @@ const featuresOptions = [ @@ -187,10 +228,10 @@ const featuresOptions = [
187 ] 228 ]
188 229
189 const MarkerOptions = [ 230 const MarkerOptions = [
190 - {  
191 - value: MarkerEnum.CIRCLE_MARKER,  
192 - label: '圆形标点'  
193 - }, 231 + // {
  232 + // value: MarkerEnum.CIRCLE_MARKER,
  233 + // label: '圆形标点' //在地图里点击无效,所以注释,有需要自行打开即可
  234 + // },
194 { 235 {
195 value: MarkerEnum.MARKER, 236 value: MarkerEnum.MARKER,
196 label: '定位标点' 237 label: '定位标点'
@@ -200,4 +241,103 @@ const MarkerOptions = [ @@ -200,4 +241,103 @@ const MarkerOptions = [
200 label: '隐藏标点' 241 label: '隐藏标点'
201 } 242 }
202 ] 243 ]
  244 +
  245 +const iconMarkerValue = ref<string | null>('1.png')
  246 +
  247 +const isHref = (url: string) => {
  248 + try {
  249 + new URL(url)
  250 + return true
  251 + } catch (error) {
  252 + return false
  253 + }
  254 +}
  255 +
  256 +// import.meta.glob 这个不能封装,必须是字符串,不能通过传值传进去
  257 +const iconMarkerOptions = computed(() => {
  258 + const pathList = import.meta.glob('../../../../../../assets/external/marker/*')
  259 + return Object.keys(pathList).map(item => {
  260 + const imgName = item.split('/').at(-1)
  261 + return {
  262 + label: imgName,
  263 + value: imgName
  264 + } as SelectOption
  265 + })
  266 +})
  267 +//
  268 +
  269 +const getMarkerImagePath = (name: string) => {
  270 + return isHref(name) ? name : new URL(`../../../../../../assets/external/marker/${name}`, import.meta.url).href
  271 +}
  272 +
  273 +const renderOption = (option: SelectOption) => {
  274 + return h(NSpace, { justify: 'space-between', style: 'padding: 0 15px; height: 28px; line-height: 28px;' }, () => [
  275 + h(NImage, {
  276 + width: 25,
  277 + src: getMarkerImagePath(option.value as string),
  278 + previewDisabled: true,
  279 + style: { height: '25px' }
  280 + } as Recordable),
  281 + h(NEllipsis, null, () => option.label)
  282 + ])
  283 +}
  284 +
  285 +const selectHandle = (value: string) => {
  286 + iconMarkerValue.value = value
  287 + getUrlBase64(getMarkerImagePath(value),'png',(baseEncodeText: string)=>{
  288 + props.optionData.mapOptions.iconMarker =baseEncodeText
  289 + })
  290 +}
  291 +
  292 +const mapSelectBorderValue = ref<string | null>('border01.png')
  293 +
  294 +const needBorder = ['border01.png', 'border02.png', 'border03.png', 'border05.png', 'border07.png']
  295 +
  296 +// import.meta.glob 这个不能封装,必须是字符串,不能通过传值传进去
  297 +const mapBorderOptions = computed(() => {
  298 + const pathList = import.meta.glob('../../../../../../assets/images/chart/decorates/*')
  299 + return Object.keys(pathList)
  300 + .map(item => {
  301 + const imgName = item.split('/').at(-1) as string
  302 + if (needBorder.includes(imgName)) {
  303 + return {
  304 + label: imgName,
  305 + value: imgName
  306 + } as SelectOption
  307 + }
  308 + })
  309 + .filter(Boolean) as SelectOption[]
  310 +})
  311 +//
  312 +
  313 +const getMapBorderImagePath = (name: string) => {
  314 + return isHref(name) ? name : new URL(`../../../../../../assets/images/chart/decorates/${name}`, import.meta.url).href
  315 +}
  316 +
  317 +const renderMapBorderOption = (option: SelectOption) => {
  318 + return h(NSpace, { justify: 'space-between', style: 'padding: 0 15px; height: 28px; line-height: 28px;' }, () => [
  319 + h(NImage, {
  320 + width: 25,
  321 + src: getMapBorderImagePath(option.value as string),
  322 + previewDisabled: true,
  323 + style: { height: '25px' }
  324 + } as Recordable),
  325 + h(NEllipsis, null, () => option.label)
  326 + ])
  327 +}
  328 +
  329 +const selectMapBorderHandle = (value: string) => {
  330 + mapSelectBorderValue.value = value
  331 + const toLowerValue = value.toLocaleLowerCase()
  332 + ;(props.optionData.mapOptions.mpBorderConfig as BaseSelectBorderIF) = {
  333 + value: toLowerValue[0]?.toUpperCase() + toLowerValue?.substr(1)?.split('.')[0]
  334 + }
  335 +}
  336 +
  337 +onMounted(() => {
  338 + getUrlBase64(getMarkerImagePath(props.optionData.mapOptions.iconMarker),'png',(baseEncodeText: string)=>{
  339 + props.optionData.mapOptions.iconMarker = baseEncodeText
  340 + })
  341 + mapSelectBorderValue.value = `${props.optionData.mapOptions.mpBorderConfig.value?.toLocaleLowerCase()}.png`
  342 +})
203 </script> 343 </script>
1 { 1 {
2 "markers": [ 2 "markers": [
3 { 3 {
4 - "name": "模拟11111111111", 4 + "name": "模拟数据1",
5 "value": 20, 5 "value": 20,
6 "position": [103.856504, 30.687278], 6 "position": [103.856504, 30.687278],
7 "extraInfo": { 7 "extraInfo": {
8 "tbDeviceId": "@xxxxxxxxxxx", 8 "tbDeviceId": "@xxxxxxxxxxx",
9 - "name": "模拟11111111111",  
10 - "alias": "模拟11111111111", 9 + "name": "模拟数据1",
  10 + "alias": "模拟数据1",
11 "organizationDTO": { 11 "organizationDTO": {
12 - "name": "模拟11111111111" 12 + "name": "模拟数据1"
13 }, 13 },
14 "deviceState": "INACTIVE", 14 "deviceState": "INACTIVE",
15 "deviceProfile": { 15 "deviceProfile": {
16 "transportType": "MQTT" 16 "transportType": "MQTT"
17 }, 17 },
  18 + "deviceProfileId":"",
18 "deviceInfo": { 19 "deviceInfo": {
19 "address": "四川省", 20 "address": "四川省",
20 "longitude": 103.856504, 21 "longitude": 103.856504,
@@ -23,20 +24,21 @@ @@ -23,20 +24,21 @@
23 } 24 }
24 }, 25 },
25 { 26 {
26 - "name": "模拟22222222222", 27 + "name": "模拟数据2",
27 "value": 30, 28 "value": 30,
28 "position": [104.095368, 30.716787], 29 "position": [104.095368, 30.716787],
29 "extraInfo": { 30 "extraInfo": {
30 "tbDeviceId": "@xxxxxxxxxxxxxxx", 31 "tbDeviceId": "@xxxxxxxxxxxxxxx",
31 - "name": "模拟22222222222",  
32 - "alias": "模拟22222222222", 32 + "name": "模拟数据2",
  33 + "alias": "模拟数据2",
33 "organizationDTO": { 34 "organizationDTO": {
34 - "name": "模拟22222222222" 35 + "name": "模拟数据2"
35 }, 36 },
36 "deviceState": "INACTIVE", 37 "deviceState": "INACTIVE",
37 "deviceProfile": { 38 "deviceProfile": {
38 "transportType": "TCP" 39 "transportType": "TCP"
39 }, 40 },
  41 + "deviceProfileId":"",
40 "deviceInfo": { 42 "deviceInfo": {
41 "address": "四川省", 43 "address": "四川省",
42 "longitude": 104.095368, 44 "longitude": 104.095368,
@@ -8,7 +8,7 @@ export const OverrideMapAmapConfig: ConfigType = { @@ -8,7 +8,7 @@ export const OverrideMapAmapConfig: ConfigType = {
8 key, 8 key,
9 chartKey, 9 chartKey,
10 conKey, 10 conKey,
11 - title: '高德地图', 11 + title: '设备分布',
12 category: ChatCategoryEnum.MAP, 12 category: ChatCategoryEnum.MAP,
13 categoryName: ChatCategoryEnumName.MAP, 13 categoryName: ChatCategoryEnumName.MAP,
14 package: PackagesCategoryEnum.CHARTS, 14 package: PackagesCategoryEnum.CHARTS,
1 <template> 1 <template>
2 - <div @mouseenter="handleMouseenter" @mouseleave="handleMouseleave" class="chart-amap" ref="vChartRef">  
3 - <div v-show="showSearchBox" @click.stop="handleOpenSearchBox" class="search-box"></div>  
4 - <search-box :modelShow="modelShow" @searchParams="handleSearchParams" @closeDrawer="handleCloseDrawer"></search-box> 2 + <div class="chart-amap" ref="vChartRef">
  3 + <device-latest-table ref="deviceLatestTableRef"></device-latest-table>
5 </div> 4 </div>
6 </template> 5 </template>
7 6
8 <script setup lang="ts"> 7 <script setup lang="ts">
9 -import { ref, PropType, toRefs, watch } from 'vue' 8 +import { ref, PropType, toRefs, watch, render, h, onMounted, onUnmounted, reactive } from 'vue'
10 import AMapLoader from '@amap/amap-jsapi-loader' 9 import AMapLoader from '@amap/amap-jsapi-loader'
11 import { CreateComponentType } from '@/packages/index.d' 10 import { CreateComponentType } from '@/packages/index.d'
12 import { useChartDataFetch } from '@/hooks' 11 import { useChartDataFetch } from '@/hooks'
13 import { useChartEditStore } from '@/store/modules/chartEditStore/chartEditStore' 12 import { useChartEditStore } from '@/store/modules/chartEditStore/chartEditStore'
14 -import { MarkerEnum, ThemeEnum, dataExtraInfoType, dataJsonType, dataJsonMarkersType, fileterDevice } from './config' 13 +import { MarkerEnum, ThemeEnum, dataExtraInfoType, dataJsonType, dataJsonMarkersType, devicePartInfoInterface } from './config'
15 import { isArray } from '@/utils' 14 import { isArray } from '@/utils'
16 -import djh from './images/djh.png' 15 +import inactive from './images/inactive.png'
17 import online from './images/online.png' 16 import online from './images/online.png'
18 -import lx1 from './images/lx1.png'  
19 -import { getDeviceActiveTime, getDeviceList } from '@/api/external/common/index' 17 +import offline from './images/offline.png'
  18 +import listView from './images/list.png'
  19 +import { getDeviceActiveTime } from '@/api/external/common/index'
20 import dayjs from 'dayjs' 20 import dayjs from 'dayjs'
21 -import SearchBox from './components/SearchBox.vue' 21 +import DeviceLatestTable from './components/DeviceLatestTable.vue'
  22 +import { getDeviceLatest, getProfileAttrs } from '@/api/external/common'
  23 +import { NButton } from 'naive-ui'
22 24
23 const props = defineProps({ 25 const props = defineProps({
24 chartConfig: { 26 chartConfig: {
@@ -27,11 +29,9 @@ const props = defineProps({ @@ -27,11 +29,9 @@ const props = defineProps({
27 } 29 }
28 }) 30 })
29 31
30 -const modelShow = ref(false) 32 +const deviceLatestTableRef = ref<Nullable<InstanceType<typeof DeviceLatestTable>>>(null);
31 33
32 -const showSearchBox = ref(false)  
33 -  
34 -let { 34 +const {
35 amapKey, 35 amapKey,
36 amapStyleKey, 36 amapStyleKey,
37 amapLon, 37 amapLon,
@@ -44,7 +44,10 @@ let { @@ -44,7 +44,10 @@ let {
44 viewMode, 44 viewMode,
45 pitch, 45 pitch,
46 skyColor, 46 skyColor,
47 - marker 47 + marker,
  48 + iconMarker,
  49 + mpBorderConfig,
  50 + bgColor
48 } = toRefs(props.chartConfig.option.mapOptions) 51 } = toRefs(props.chartConfig.option.mapOptions)
49 52
50 //官方没有高德地图api的ts,所以类型全用的any 53 //官方没有高德地图api的ts,所以类型全用的any
@@ -71,7 +74,10 @@ const initMap = (newData: any) => { @@ -71,7 +74,10 @@ const initMap = (newData: any) => {
71 pitch: pitch.value, // 地图俯仰角度,有效范围 0 度- 83 度 74 pitch: pitch.value, // 地图俯仰角度,有效范围 0 度- 83 度
72 skyColor: skyColor.value, 75 skyColor: skyColor.value,
73 viewMode: viewMode.value, // 地图模式 76 viewMode: viewMode.value, // 地图模式
74 - willReadFrequently: true 77 + willReadFrequently: true,
  78 + WebGLParams: {
  79 + preserveDrawingBuffer: true,
  80 + }
75 }) 81 })
76 dataHandle(props.chartConfig.option.dataset) //处理地图标点 82 dataHandle(props.chartConfig.option.dataset) //处理地图标点
77 let satellite = new AMap.TileLayer.Satellite() 83 let satellite = new AMap.TileLayer.Satellite()
@@ -84,7 +90,6 @@ const initMap = (newData: any) => { @@ -84,7 +90,6 @@ const initMap = (newData: any) => {
84 `amap://styles/${amapStyleKeyCustom.value !== '' ? amapStyleKeyCustom.value : amapStyleKey.value}` 90 `amap://styles/${amapStyleKeyCustom.value !== '' ? amapStyleKeyCustom.value : amapStyleKey.value}`
85 ) 91 )
86 } 92 }
87 - //点击地图任意地方关闭infoWindow窗体  
88 mapIns.on('click', () => { 93 mapIns.on('click', () => {
89 mapIns.clearInfoWindow() 94 mapIns.clearInfoWindow()
90 }) 95 })
@@ -94,81 +99,115 @@ const initMap = (newData: any) => { @@ -94,81 +99,115 @@ const initMap = (newData: any) => {
94 }) 99 })
95 } 100 }
96 101
  102 +
  103 +//携带设备的tbDeviceId和别名或者名称
  104 +const devicePartInfo = reactive<devicePartInfoInterface>({
  105 + tbDeviceId: '',
  106 + name: '',
  107 + alias: '',
  108 + deviceProfileId: '',
  109 +})
  110 +
97 //创建信息弹窗 111 //创建信息弹窗
98 const createInfoWindow = async (extraInfo: dataExtraInfoType) => { 112 const createInfoWindow = async (extraInfo: dataExtraInfoType) => {
99 try { 113 try {
100 - const { name, alias, organizationDTO, deviceState, deviceProfile, deviceInfo, tbDeviceId } = extraInfo 114 + const { name, alias, organizationDTO, deviceState, deviceProfile, deviceInfo, tbDeviceId, deviceProfileId } = extraInfo
  115 + devicePartInfo.tbDeviceId = tbDeviceId
  116 + devicePartInfo.alias = alias
  117 + devicePartInfo.name = name
  118 + devicePartInfo.deviceProfileId = deviceProfileId
101 if (tbDeviceId.startsWith('@')) return //假的模拟数据则终止 119 if (tbDeviceId.startsWith('@')) return //假的模拟数据则终止
102 const res = await getDeviceActiveTime(tbDeviceId) //查询设备最后离线时间 120 const res = await getDeviceActiveTime(tbDeviceId) //查询设备最后离线时间
103 let { lastUpdateTs } = res[0] 121 let { lastUpdateTs } = res[0]
104 const lastUpdateFormatTs = dayjs(lastUpdateTs).format('YYYY-MM-DD HH:mm:ss') 122 const lastUpdateFormatTs = dayjs(lastUpdateTs).format('YYYY-MM-DD HH:mm:ss')
  123 + //以render方式渲染小组件里的边框组件
  124 + const BorderInstance = await import(`../../../../Decorates/Borders/${mpBorderConfig.value.value}/index.vue`)
  125 + const config = await import(`../../../../Decorates/Borders/${mpBorderConfig.value.value}/config.ts`)
  126 + const BorderConfigInstance = new config.default()
  127 + const id = Date.now().toString()
  128 + setTimeout(() => {
  129 + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  130 + render(h(BorderInstance.default, { chartConfig: BorderConfigInstance }), document.getElementById(id)!)
  131 + }, 100)
  132 + //
  133 + const textOverflow = `width:16rem;text-overflow:ellipsis;overflow:hidden;word-break:break-all;white-space:nowrap;`
  134 + const textOverflowFontBold = `width:12rem;text-overflow:ellipsis;overflow:hidden;word-break:break-all;white-space:nowrap;font-size:15px;font-weight:bold;`
  135 + const deviceStateContainer = `display:flex;justify-content:space-between;align-items:center;`
  136 + const deviceStateImg = `width:1.2rem;height:1.2rem;`
  137 + const deviceStateText = `margin-left:0.6rem;font-weight:bold;`
105 return ` 138 return `
106 - <div style="width:15vw;height:18vh;background-color:white;">  
107 - <div style="margin:0px 10px">  
108 - <div style="display:flex;justify-content:space-between; margin:20px 0px;">  
109 - <div style="font-size:16px;font-weight:bold">${alias || name}</div>  
110 - ${  
111 - deviceState === 'INACTIVE'  
112 - ? `<div style="display:flex;align-items:center;"><img style="width:15px;height:15px" src="${djh}" class="mr-1">待激活</div>`  
113 - : deviceState === 'ONLINE'  
114 - ? `<div style="display:flex;align-items:center; ">  
115 - <img style="width:15px;height:15px" src="${online}" class="mr-1">在线</div>`  
116 - : `<div style="display:flex;align-items:center;"><img style="width:15px;height:15px" src="${lx1}" class="mr-1">离线</div>`  
117 - }  
118 - </div>  
119 - <div>所属组织:${organizationDTO.name}</div>  
120 - <div style="margin-top:6px;">接入协议:${deviceProfile.transportType}</div>  
121 - <div style="margin-top:6px;">  
122 - 设备位置:${!deviceInfo.address ? '该设备暂无地理位置' : deviceInfo.address}  
123 - </div>  
124 - <div style="margin-top:6px;">  
125 - ${  
126 - deviceState === 'ONLINE' ? '在线' : deviceState === 'INACTIVE' ? '创建' : '离线'  
127 - }时间:${lastUpdateFormatTs}  
128 - </div>  
129 - </div>  
130 - </div>  
131 - ` 139 + <div id="${id}" style="width:30rem;">
  140 + <div style="display:flex;flex-direction:column;margin:3.5rem 5.5rem 2rem 6.5rem;position:relative;">
  141 + <div style="display:flex;justify-content:space-between;align-items:center;color:white;">
  142 + <span style="${textOverflowFontBold}">${alias || name}</span>
  143 + ${deviceState === 'INACTIVE'
  144 + ? `<div style="${deviceStateContainer}">
  145 + <img onclick="handleOpenDrawer()" title="最新数据" style="${deviceStateImg};cursor:pointer;" src="${listView}"/>
  146 + <img style="${deviceStateImg};margin-left:0.3rem" src="${inactive}"/>
  147 + <span style="${deviceStateText}">待激活</span>
  148 + </div>`
  149 + : deviceState === 'ONLINE'
  150 + ? `<div style="${deviceStateContainer}">
  151 + <img onclick="handleOpenDrawer()" title="最新数据" style="${deviceStateImg};cursor:pointer;" src="${listView}"/>
  152 + <img style="${deviceStateImg};margin-left:0.3rem" src="${online}"/>
  153 + <span style="${deviceStateText}">在线</span>
  154 + </div>`
  155 + : `<div style="${deviceStateContainer}">
  156 + <img onclick="handleOpenDrawer()" title="最新数据" style="${deviceStateImg};cursor:pointer;" src="${listView}"/>
  157 + <img style="${deviceStateImg};margin-left:0.3rem" src="${offline}"/>
  158 + <span style="${deviceStateText}">离线</span>
  159 + </div>`
  160 + }
  161 + </div>
  162 + <div style="display:flex;flex-direction:column;justify-content:space-between;color:white;margin-top:1rem;gap:0.95rem;">
  163 + <div style="${textOverflow}">所属组织:${organizationDTO.name}</div>
  164 + <div style="${textOverflow}">接入协议:${deviceProfile.transportType}</div>
  165 + <div style="${textOverflow}">设备位置:${!deviceInfo.address ? '该设备暂无地理位置' : deviceInfo.address}</div>
  166 + <div style="${textOverflow}">${deviceState === 'ONLINE' ? '在线' : deviceState === 'INACTIVE' ? '创建' : '离线'}时间:${lastUpdateFormatTs}</div>
  167 + </div>
  168 + </div>
  169 + </div>
  170 + `
132 } catch (e) { 171 } catch (e) {
133 console.error(e) 172 console.error(e)
134 } 173 }
135 } 174 }
136 175
137 -const handleMouseenter = () => (showSearchBox.value = true)  
138 -  
139 -const handleMouseleave = () => (showSearchBox.value = false)  
140 -  
141 -const handleOpenSearchBox = () => (modelShow.value = true) 176 +const handleOpenDrawer = async () => {
  177 + deviceLatestTableRef.value?.openDrawer()
  178 + deviceLatestTableRef.value?.setDrawerTitle(devicePartInfo.alias || devicePartInfo.name)
  179 + if (!devicePartInfo.tbDeviceId) return
  180 + const resp = await getDeviceLatest(devicePartInfo.tbDeviceId)
  181 + const respGetAttrs = await getProfileAttrs({ deviceProfileId: devicePartInfo.deviceProfileId })
  182 + deviceLatestTableRef.value?.setValue(resp, respGetAttrs)
  183 +}
142 184
143 -const handleCloseDrawer = () => (modelShow.value = false) 185 +onMounted(() => {
  186 + (window as any).handleOpenDrawer = handleOpenDrawer;
  187 +});
144 188
145 -const handleSearchParams = async (searchPage: any, params: any) => {  
146 - try {  
147 - const { items } = await getDeviceList(searchPage, params)  
148 - const values = fileterDevice(items)  
149 - if (!values) return  
150 - dataHandle(values)  
151 - } finally {  
152 - handleCloseDrawer()  
153 - }  
154 -} 189 +onUnmounted(() => {
  190 + (window as any).handleOpenDrawer = null;
  191 +});
155 192
156 -//地图点击 193 +//地图鼠标hover
157 const mapClick = (markerInstance: any, markerItem: dataJsonMarkersType) => { 194 const mapClick = (markerInstance: any, markerItem: dataJsonMarkersType) => {
158 markerInstance.setExtData({ 195 markerInstance.setExtData({
159 extraInfo: markerItem.extraInfo 196 extraInfo: markerItem.extraInfo
160 }) 197 })
161 - markerInstance.setLabel({  
162 - content: markerItem.extraInfo.alias || markerItem.extraInfo.name  
163 - }) 198 + // mouseover
164 markerInstance.on('click', async (e: any) => { 199 markerInstance.on('click', async (e: any) => {
165 const { extraInfo } = e.target.getExtData() 200 const { extraInfo } = e.target.getExtData()
  201 + if (extraInfo.tbDeviceId.startsWith('@')) return //假的模拟数据则终止弹窗
166 let infoWindow = new AMapIns.InfoWindow({ 202 let infoWindow = new AMapIns.InfoWindow({
167 content: await createInfoWindow(extraInfo), 203 content: await createInfoWindow(extraInfo),
168 - offset: new AMapIns.Pixel(0, -50) 204 + offset: new AMapIns.Pixel(3, -30)
169 }) 205 })
170 infoWindow.open(mapIns, e.target.getPosition()) 206 infoWindow.open(mapIns, e.target.getPosition())
171 }) 207 })
  208 + // markerInstance.on('click', () => {
  209 + // mapIns.clearInfoWindow()
  210 + // })
172 } 211 }
173 212
174 const dataHandle = (newData: dataJsonType) => { 213 const dataHandle = (newData: dataJsonType) => {
@@ -185,11 +224,13 @@ const dataHandle = (newData: dataJsonType) => { @@ -185,11 +224,13 @@ const dataHandle = (newData: dataJsonType) => {
185 newData.markers.forEach((markerItem: dataJsonMarkersType) => { 224 newData.markers.forEach((markerItem: dataJsonMarkersType) => {
186 const markerInstance = new AMapIns.Marker({ 225 const markerInstance = new AMapIns.Marker({
187 position: [markerItem.position[0], markerItem.position[1]], 226 position: [markerItem.position[0], markerItem.position[1]],
188 - offset: new AMapIns.Pixel(-13, -30) 227 + offset: new AMapIns.Pixel(-13, 5),
  228 + icon: iconMarker.value
189 }) 229 })
190 - // markers.push(markerInstance) 原作者这种方式添加,属于JS API 1.4.8版本的  
191 - // markerInstance.setMap(mapIns)  
192 - mapIns.add(markerInstance) 230 + // 原作者这种方式添加,属于JS API 1.4.8版本的
  231 + markers.push(markerInstance)
  232 + markerInstance.setMap(mapIns)
  233 + // mapIns.add(markerInstance)
193 mapClick(markerInstance, markerItem) 234 mapClick(markerInstance, markerItem)
194 }) 235 })
195 } else if (mapMarkerType.value === MarkerEnum.CIRCLE_MARKER) { 236 } else if (mapMarkerType.value === MarkerEnum.CIRCLE_MARKER) {
@@ -239,13 +280,47 @@ watch( @@ -239,13 +280,47 @@ watch(
239 // 预览 280 // 预览
240 useChartDataFetch(props.chartConfig, useChartEditStore, (newData: any) => { 281 useChartDataFetch(props.chartConfig, useChartEditStore, (newData: any) => {
241 stopWatch() 282 stopWatch()
242 - dataHandle(newData) 283 + setTimeout(() => {
  284 + dataHandle(newData)
  285 + }, 1000)
243 }) 286 })
244 </script> 287 </script>
245 288
  289 +<style>
  290 +/**去除高德地图原生弹窗 */
  291 +.amap-info-outer,
  292 +.amap-menu-outer {
  293 + box-shadow: 0 1px 2px rgba(0, 0, 0, 0) !important;
  294 +}
  295 +
  296 +.amap-info-outer {
  297 + background-color: rgba(0, 0, 0, 0) !important;
  298 +}
  299 +
  300 +.amap-info-close {
  301 + display: none !important;
  302 +}
  303 +
  304 +.amap-info-content {
  305 + overflow: hidden !important;
  306 +}
  307 +
  308 +/**去除高德地图原生弹窗*/
  309 +.amap-info-content .go-border-box {
  310 + position: absolute;
  311 + transform: scale(0.68);
  312 + top: 0;
  313 + z-index: -100;
  314 +}
  315 +
  316 +.amap-info-content .go-border-box svg {
  317 + background-color: v-bind(bgColor);
  318 +}
  319 +</style>
246 <style lang="scss" scoped> 320 <style lang="scss" scoped>
247 .chart-amap { 321 .chart-amap {
248 position: relative; 322 position: relative;
  323 +
249 .search-box { 324 .search-box {
250 cursor: pointer; 325 cursor: pointer;
251 position: absolute; 326 position: absolute;
@@ -7,11 +7,11 @@ import dataJson from './data.json' @@ -7,11 +7,11 @@ import dataJson from './data.json'
7 7
8 //省市区枚举 8 //省市区枚举
9 export const enum areaEnum { 9 export const enum areaEnum {
10 - PROVINCE = 'PROVINCE',//省份  
11 - CITY = 'CITY',//城市  
12 - COUNTY = 'COUNTY',//县  
13 - COUNTRY = 'COUNTRY',//国家  
14 - TOWN = 'TOWN'//镇 10 + PROVINCE = 'PROVINCE', //省份
  11 + CITY = 'CITY', //城市
  12 + COUNTY = 'COUNTY', //县
  13 + COUNTRY = 'COUNTRY', //国家
  14 + TOWN = 'TOWN' //镇
15 } 15 }
16 export const includes = [] 16 export const includes = []
17 17
@@ -22,6 +22,9 @@ export const option = { @@ -22,6 +22,9 @@ export const option = {
22 iconDistanceTop: 20, 22 iconDistanceTop: 20,
23 drillingIn: false, 23 drillingIn: false,
24 dataset: dataJson, 24 dataset: dataJson,
  25 + saveClickRegion: {
  26 + level: ''
  27 + },
25 mapRegion: { 28 mapRegion: {
26 adcode: 'china', 29 adcode: 'china',
27 showHainanIsLands: true, 30 showHainanIsLands: true,
@@ -161,6 +164,26 @@ export const option = { @@ -161,6 +164,26 @@ export const option = {
161 shadowOffsetY: 2, 164 shadowOffsetY: 2,
162 shadowBlur: 10 165 shadowBlur: 10
163 } 166 }
  167 + },
  168 + {
  169 + type: 'lines',
  170 + zlevel: 2,
  171 + effect: {
  172 + show: true,
  173 + period: 4, //箭头指向速度,值越小速度越快
  174 + trailLength: 0.4, //特效尾迹长度[0,1]值越大,尾迹越长重
  175 + symbol: 'arrow', //箭头图标
  176 + symbolSize: 7 //图标大小
  177 + },
  178 + lineStyle: {
  179 + normal: {
  180 + color: '#4fb6d2',
  181 + width: 1, //线条宽度
  182 + opacity: 0.1, //尾迹线条透明度
  183 + curveness: 0.3 //尾迹线条曲直度
  184 + }
  185 + },
  186 + data: []
164 } 187 }
165 ] 188 ]
166 } 189 }
@@ -246,6 +246,46 @@ @@ -246,6 +246,46 @@
246 </SettingItem> 246 </SettingItem>
247 </SettingItemBox> 247 </SettingItemBox>
248 </CollapseItem> 248 </CollapseItem>
  249 + <CollapseItem v-if="seriesList[2]" name="飞线" :expanded="true">
  250 + <SettingItemBox name="箭头">
  251 + <SettingItem name="速度">
  252 + <n-tooltip trigger="hover">
  253 + <template #trigger>
  254 + <n-input-number v-model:value="seriesList[2].effect.period" size="small" :min="0"></n-input-number>
  255 + </template>
  256 + 值越小速度越快
  257 + </n-tooltip>
  258 + </SettingItem>
  259 + <SettingItem name="尾迹">
  260 + <n-tooltip trigger="hover">
  261 + <template #trigger>
  262 + <n-input-number
  263 + v-model:value="seriesList[2].effect.trailLength"
  264 + size="small"
  265 + :min="0"
  266 + :max="1"
  267 + ></n-input-number>
  268 + </template>
  269 + 特效尾迹长度[0,1]值越大,尾迹越长重
  270 + </n-tooltip>
  271 + </SettingItem>
  272 + <SettingItem name="大小">
  273 + <n-input-number v-model:value="seriesList[2].effect.symbolSize" size="small" :min="0"></n-input-number>
  274 + </SettingItem>
  275 + </SettingItemBox>
  276 + <SettingItemBox name="配置">
  277 + <SettingItem name="颜色">
  278 + <n-color-picker
  279 + size="small"
  280 + :modes="['hex']"
  281 + v-model:value="seriesList[2].lineStyle.normal.color"
  282 + ></n-color-picker>
  283 + </SettingItem>
  284 + <SettingItem name="宽度">
  285 + <n-input-number v-model:value="seriesList[2].lineStyle.normal.width" size="small" :min="1"></n-input-number>
  286 + </SettingItem>
  287 + </SettingItemBox>
  288 + </CollapseItem>
249 </template> 289 </template>
250 290
251 <script setup lang="ts"> 291 <script setup lang="ts">
@@ -26,6 +26,36 @@ @@ -26,6 +26,36 @@
26 "value": [126.642464, 45.756967, 101] 26 "value": [126.642464, 45.756967, 101]
27 } 27 }
28 ], 28 ],
  29 + "line": [
  30 + {
  31 + "adcode": 410000,
  32 + "coords": [
  33 + [113.665412, 34.757975],
  34 + [116.405285, 39.904989]
  35 + ]
  36 + },
  37 + {
  38 + "adcode": 630000,
  39 + "coords": [
  40 + [101.778916, 36.623178],
  41 + [116.405285, 39.904989]
  42 + ]
  43 + },
  44 + {
  45 + "adcode": 640000,
  46 + "coords": [
  47 + [106.278179, 38.46637],
  48 + [116.405285, 39.904989]
  49 + ]
  50 + },
  51 + {
  52 + "adcode": 230000,
  53 + "coords": [
  54 + [126.642464, 45.756967],
  55 + [116.405285, 39.904989]
  56 + ]
  57 + }
  58 + ],
29 "map": [ 59 "map": [
30 { 60 {
31 "name": "北京市", 61 "name": "北京市",
@@ -74,7 +74,7 @@ const toolBoxOption = ref({ @@ -74,7 +74,7 @@ const toolBoxOption = ref({
74 top: 20, 74 top: 20,
75 feature: { 75 feature: {
76 myFullButton: { 76 myFullButton: {
77 - show: true, 77 + show: false,
78 title: '返回', 78 title: '返回',
79 icon: iconStr.value, 79 icon: iconStr.value,
80 iconStyle: { 80 iconStyle: {
@@ -88,22 +88,49 @@ const toolBoxOption = ref({ @@ -88,22 +88,49 @@ const toolBoxOption = ref({
88 watch( 88 watch(
89 () => props.chartConfig.option, 89 () => props.chartConfig.option,
90 newData => { 90 newData => {
91 - const { iconColor, drillingIn, iconDistanceRight, iconDistanceTop } = newData  
92 - toolBoxOption.value.feature.myFullButton.show = drillingIn 91 + const { iconColor, drillingIn, iconDistanceRight, iconDistanceTop, mapRegion } = newData
  92 + if (drillingIn && !newData.saveClickRegion.level) {
  93 + toolBoxOption.value.feature.myFullButton.show = !drillingIn
  94 + } else if (
  95 + drillingIn &&
  96 + newData.saveClickRegion.level === areaEnum.PROVINCE &&
  97 + mapRegion.saveSelect.levelStr === areaEnum.COUNTRY
  98 + ) {
  99 + toolBoxOption.value.feature.myFullButton.show = drillingIn
  100 + } else if (
  101 + drillingIn &&
  102 + newData.saveClickRegion.level === areaEnum.COUNTRY &&
  103 + mapRegion.saveSelect.levelStr === areaEnum.COUNTRY
  104 + ) {
  105 + toolBoxOption.value.feature.myFullButton.show = !drillingIn
  106 + } else if (
  107 + drillingIn &&
  108 + newData.saveClickRegion.level === areaEnum.CITY &&
  109 + mapRegion.saveSelect.levelStr === areaEnum.PROVINCE
  110 + ) {
  111 + toolBoxOption.value.feature.myFullButton.show = drillingIn
  112 + } else if (
  113 + drillingIn &&
  114 + newData.saveClickRegion.level === areaEnum.PROVINCE &&
  115 + mapRegion.saveSelect.levelStr === areaEnum.PROVINCE
  116 + ) {
  117 + toolBoxOption.value.feature.myFullButton.show = !drillingIn
  118 + }
93 toolBoxOption.value.feature.myFullButton.iconStyle.color = iconColor 119 toolBoxOption.value.feature.myFullButton.iconStyle.color = iconColor
94 toolBoxOption.value.right = iconDistanceRight 120 toolBoxOption.value.right = iconDistanceRight
95 toolBoxOption.value.top = iconDistanceTop 121 toolBoxOption.value.top = iconDistanceTop
  122 + vEchartsSetOption()
96 }, 123 },
97 { 124 {
98 - deep: true 125 + deep: true,
99 } 126 }
100 ) 127 )
101 -  
102 props.chartConfig.option = { 128 props.chartConfig.option = {
103 ...props.chartConfig.option, 129 ...props.chartConfig.option,
104 ...{ toolbox: toolBoxOption.value } 130 ...{ toolbox: toolBoxOption.value }
105 } 131 }
106 132
  133 +
107 //地图点击返回 adcode=100000,名字必须是china 134 //地图点击返回 adcode=100000,名字必须是china
108 const watchAdcode = async () => { 135 const watchAdcode = async () => {
109 if (props.chartConfig.option.drillingIn) { 136 if (props.chartConfig.option.drillingIn) {
@@ -121,6 +148,7 @@ const watchAdcode = async () => { @@ -121,6 +148,7 @@ const watchAdcode = async () => {
121 } 148 }
122 await getGeojson(saveAdcode) 149 await getGeojson(saveAdcode)
123 const adcode = saveAdcode === 100000 ? 'china' : saveAdcode 150 const adcode = saveAdcode === 100000 ? 'china' : saveAdcode
  151 + props.chartConfig.option.saveClickRegion.level = saveLevelStr.level
124 props.chartConfig.option.geo.map = adcode 152 props.chartConfig.option.geo.map = adcode
125 props.chartConfig.option.series.forEach((item: any) => { 153 props.chartConfig.option.series.forEach((item: any) => {
126 if (item.type === 'map') item.map = adcode 154 if (item.type === 'map') item.map = adcode
@@ -202,9 +230,19 @@ const vEchartsSetOption = async () => { @@ -202,9 +230,19 @@ const vEchartsSetOption = async () => {
202 const dataSetHandle = async (dataset: any) => { 230 const dataSetHandle = async (dataset: any) => {
203 props.chartConfig.option.series.forEach((item: any) => { 231 props.chartConfig.option.series.forEach((item: any) => {
204 if (item.type === 'effectScatter' && dataset.point) item.data = dataset.point 232 if (item.type === 'effectScatter' && dataset.point) item.data = dataset.point
205 - else if (item.type === 'map' && dataset.map) item.data = dataset.map 233 + else if (item.type === 'lines' && dataset.line) {
  234 + item.data = dataset.line.map((it: any) => {
  235 + return {
  236 + ...it,
  237 + lineStyle: {
  238 + color: props.chartConfig.option.series[2].lineStyle.normal.color
  239 + }
  240 + }
  241 + })
  242 + } else if (item.type === 'map' && dataset.map) item.data = dataset.map
206 }) 243 })
207 if (dataset.pieces) props.chartConfig.option.visualMap.pieces = dataset.pieces 244 if (dataset.pieces) props.chartConfig.option.visualMap.pieces = dataset.pieces
  245 +
208 isPreview() && vEchartsSetOption() 246 isPreview() && vEchartsSetOption()
209 } 247 }
210 248
@@ -245,14 +283,16 @@ watch( @@ -245,14 +283,16 @@ watch(
245 } 283 }
246 ) 284 )
247 285
248 -//处理数据标点 286 +//处理数据标点包括飞线
249 const handleDataPoint = (newData: any) => { 287 const handleDataPoint = (newData: any) => {
250 if (newData === 'china') { 288 if (newData === 'china') {
251 dataSetHandle(dataJson) 289 dataSetHandle(dataJson)
252 } else { 290 } else {
253 const filterPoint = dataJson.point.filter((item: any) => item.adcode === newData) 291 const filterPoint = dataJson.point.filter((item: any) => item.adcode === newData)
  292 + // const filterLine = dataJson.line.filter((item: any) => item.adcode === newData)
254 dataSetHandle({ 293 dataSetHandle({
255 - point: filterPoint 294 + point: filterPoint,
  295 + line: [] //由于点击单个地图模块,所以去除飞线
256 }) 296 })
257 } 297 }
258 } 298 }
@@ -274,7 +314,8 @@ watch( @@ -274,7 +314,8 @@ watch(
274 } 314 }
275 }, 315 },
276 { 316 {
277 - deep: false 317 + deep: false,
  318 + immediate: true,
278 } 319 }
279 ) 320 )
280 321
@@ -310,8 +351,9 @@ const handleVChartClick = async (params: any) => { @@ -310,8 +351,9 @@ const handleVChartClick = async (params: any) => {
310 const level = item.properties.level.toUpperCase() 351 const level = item.properties.level.toUpperCase()
311 const adcode = item.properties.adcode 352 const adcode = item.properties.adcode
312 if (level === 'DISTRICT') return 353 if (level === 'DISTRICT') return
313 - if(String(adcode).startsWith('15') && level===areaEnum.CITY) return 354 + if (String(adcode).startsWith('15') && level === areaEnum.CITY) return
314 props.chartConfig.option.mapRegion.adcode = adcode 355 props.chartConfig.option.mapRegion.adcode = adcode
  356 + props.chartConfig.option.saveClickRegion.level = level
315 saveLevelStr.level = level 357 saveLevelStr.level = level
316 handleDataPoint(adcode) 358 handleDataPoint(adcode)
317 saveHistoryParent.value.push({ 359 saveHistoryParent.value.push({
@@ -5,7 +5,7 @@ import cloneDeep from 'lodash/cloneDeep' @@ -5,7 +5,7 @@ import cloneDeep from 'lodash/cloneDeep'
5 import { chartInitConfig } from '@/settings/designSetting' 5 import { chartInitConfig } from '@/settings/designSetting'
6 6
7 export const option = { 7 export const option = {
8 - dataset: [new URL('/src/assets/external/three/test.obj', import.meta.url).href],//三维数据源 8 + dataset: [new URL('/src/assets/external/three/test.obj', import.meta.url).href], //三维数据源
9 backgroundColor: '', //场景背景色 9 backgroundColor: '', //场景背景色
10 backgroundAlpha: 0, //场景透明度 10 backgroundAlpha: 0, //场景透明度
11 enableDamping: false, //是否启用阻尼 11 enableDamping: false, //是否启用阻尼
@@ -18,7 +18,9 @@ export const option = { @@ -18,7 +18,9 @@ export const option = {
18 */ 18 */
19 outputEncoding: 'liner', 19 outputEncoding: 'liner',
20 clearScene: false, //是否清空场景内容 20 clearScene: false, //是否清空场景内容
21 - lights: [//灯光为数组,type 为 环境光(AmbientLight) | 方向光(DirectionalLight) | 点光(PointLight) | 半球光(HemisphereLight) 21 + lightInput: 1,
  22 + lights: [
  23 + //灯光为数组,type 为 环境光(AmbientLight) | 方向光(DirectionalLight) | 点光(PointLight) | 半球光(HemisphereLight)
22 { 24 {
23 type: 'AmbientLight', 25 type: 'AmbientLight',
24 label: '环境光(只有颜色)', 26 label: '环境光(只有颜色)',
@@ -54,38 +56,41 @@ export const option = { @@ -54,38 +56,41 @@ export const option = {
54 size: 1, 56 size: 1,
55 show: false 57 show: false
56 }, 58 },
57 - position: [//模型位置 59 + position: [
  60 + //模型位置
58 { 61 {
59 x: 0, 62 x: 0,
60 y: 0, 63 y: 0,
61 z: 0 64 z: 0
62 } 65 }
63 ], 66 ],
64 - rotation: [//模型旋转 67 + rotation: [
  68 + //模型旋转
65 { 69 {
66 x: 0, 70 x: 0,
67 y: 0, 71 y: 0,
68 z: 0 72 z: 0
69 } 73 }
70 ], 74 ],
71 - showFps:false,//是否显示fps  
72 - labels:[ //添加图片/文字标签,暂且支持文字 75 + showFps: false, //是否显示fps
  76 + labels: [
  77 + //添加图片/文字标签,暂且支持文字
73 { 78 {
74 - image: "",  
75 - text: "", 79 + image: '',
  80 + text: '',
76 textStyle: { 81 textStyle: {
77 - fontFamily: "Arial", 82 + fontFamily: 'Arial',
78 fontSize: 18, 83 fontSize: 18,
79 - fontWeight: "normal", 84 + fontWeight: 'normal',
80 lineHeight: 1, 85 lineHeight: 1,
81 - color: "#ffffff", 86 + color: '#ffffff',
82 borderWidth: 8, 87 borderWidth: 8,
83 borderRadius: 4, 88 borderRadius: 4,
84 - borderColor: "rgba(0,0,0,1)",  
85 - backgroundColor: "rgba(0, 0, 0, 1)" 89 + borderColor: 'rgba(0,0,0,1)',
  90 + backgroundColor: 'rgba(0, 0, 0, 1)'
86 }, 91 },
87 - position: {x:0, y:0, z:0},  
88 - scale:{x:1, y:1, z:0}, 92 + position: { x: 0, y: 0, z: 0 },
  93 + scale: { x: 1, y: 1, z: 0 },
89 sid: null 94 sid: null
90 } 95 }
91 ] 96 ]
@@ -11,15 +11,15 @@ @@ -11,15 +11,15 @@
11 <n-input-number :min="0" v-model:value="optionData.borderConfig.size" size="small" /> 11 <n-input-number :min="0" v-model:value="optionData.borderConfig.size" size="small" />
12 </setting-item> 12 </setting-item>
13 </setting-item-box> 13 </setting-item-box>
14 - <setting-item-box name="上传文件"> 14 + <setting-item-box name="上传文件">
15 <setting-item> 15 <setting-item>
16 <FileUpload 16 <FileUpload
17 - :max="100"  
18 - :fileList="optionData.dataset"  
19 - :threeSupportFileFormat="threeSupportFileFormat"  
20 - :singleFileType="singleFileTypeNotMtl"  
21 - @fileStaticUri="handleFileStaticUri"  
22 - /> 17 + :max="100"
  18 + :fileList="optionData.dataset"
  19 + :threeSupportFileFormat="threeSupportFileFormat"
  20 + :singleFileType="singleFileTypeNotMtl"
  21 + @fileStaticUri="handleFileStaticUri"
  22 + />
23 </setting-item> 23 </setting-item>
24 </setting-item-box> 24 </setting-item-box>
25 <setting-item-box :alone="true"> 25 <setting-item-box :alone="true">
@@ -78,8 +78,13 @@ @@ -78,8 +78,13 @@
78 </template> 78 </template>
79 </setting-item> 79 </setting-item>
80 </setting-item-box> 80 </setting-item-box>
  81 + <setting-item-box name="灯光选择">
  82 + <setting-item name="灯光选择((0,环境光),(1,方向光),(2,点光),(3,半球光))">
  83 + <n-input-number :min="0" :max="3" v-model:value="optionData.lightInput" size="small" />
  84 + </setting-item>
  85 + </setting-item-box>
81 <setting-item-box name="灯光配置"> 86 <setting-item-box name="灯光配置">
82 - <setting-item v-for="(item, index) in optionData.lights" :name="item.label" :key="index"> 87 + <setting-item v-for="(item, index) in [optionData.lights[optionData.lightInput]]" :name="item.label" :key="index">
83 <n-color-picker 88 <n-color-picker
84 v-if="!includeHemisphereLight.includes(item.type)" 89 v-if="!includeHemisphereLight.includes(item.type)"
85 size="small" 90 size="small"
@@ -180,15 +185,9 @@ @@ -180,15 +185,9 @@
180 <SettingItem v-if="optionData.enableDamping" name="阻尼值"> 185 <SettingItem v-if="optionData.enableDamping" name="阻尼值">
181 <n-input-number v-model:value="optionData.dampingFactor" :min="0" :max="1" size="small"></n-input-number> 186 <n-input-number v-model:value="optionData.dampingFactor" :min="0" :max="1" size="small"></n-input-number>
182 </SettingItem> 187 </SettingItem>
183 - <SettingItem name="启用动画">  
184 - <n-switch v-model:value="optionData.autoPlay" size="small" />  
185 - </SettingItem>  
186 - <SettingItem name="输出编码"> 188 + <SettingItem name="输出编码,可取值为 liner 或 sRGB。linear 是 LinearEncoding 线性编码, sRGB 即 sRGBEncoding rgb 模式编码(sRGBEncoding 能更好的还原材质颜色)">
187 <n-select v-model:value="optionData.outputEncoding" size="small" :options="encodinghList"></n-select> 189 <n-select v-model:value="optionData.outputEncoding" size="small" :options="encodinghList"></n-select>
188 </SettingItem> 190 </SettingItem>
189 - <SettingItem name="是否清空场景">  
190 - <n-switch @change="handleChange" v-model:value="optionData.clearScene" size="small" />  
191 - </SettingItem>  
192 </setting-item-box> 191 </setting-item-box>
193 </collapse-item> 192 </collapse-item>
194 </template> 193 </template>
@@ -253,10 +252,6 @@ const encodinghList = [ @@ -253,10 +252,6 @@ const encodinghList = [
253 { label: 'sRGB ', value: 'sRGB ' } 252 { label: 'sRGB ', value: 'sRGB ' }
254 ] 253 ]
255 254
256 -const handleChange = (e: boolean) => {  
257 - if (e) props.optionData.dataset = ['']  
258 -}  
259 -  
260 const handleFileStaticUri = (value: UploadFileInfo[]) => { 255 const handleFileStaticUri = (value: UploadFileInfo[]) => {
261 props.optionData.dataset = value.map(item => item?.url)?.filter(Boolean) as any 256 props.optionData.dataset = value.map(item => item?.url)?.filter(Boolean) as any
262 if (Array.isArray(props.optionData.dataset) && props.optionData.dataset.length === 0) { 257 if (Array.isArray(props.optionData.dataset) && props.optionData.dataset.length === 0) {
1 <template> 1 <template>
2 - <div class="go-content-box" :style="{ border: !borderConfig.show ? 'none' : ''}">  
3 - <div v-if="useDetectWebGLContext()"> 2 + <div class="go-content-box" :style="{ border: !borderConfig.show ? 'none' : '' }">
  3 + <div>
4 <vue3dLoader 4 <vue3dLoader
5 ref="vue3dLoaderRef" 5 ref="vue3dLoaderRef"
6 :webGLRendererOptions="webGLRendererOptions" 6 :webGLRendererOptions="webGLRendererOptions"
@@ -20,7 +20,7 @@ @@ -20,7 +20,7 @@
20 @load="onLoad" 20 @load="onLoad"
21 :position="position" 21 :position="position"
22 :rotation="rotation" 22 :rotation="rotation"
23 - :lights="lights" 23 + :lights="[lights[lightInput]]"
24 :showFps="showFps" 24 :showFps="showFps"
25 :labels="labels" 25 :labels="labels"
26 /> 26 />
@@ -29,15 +29,13 @@ @@ -29,15 +29,13 @@
29 <n-progress type="line" :color="themeColor" :percentage="process" :indicator-placement="'inside'" processing /> 29 <n-progress type="line" :color="themeColor" :percentage="process" :indicator-placement="'inside'" processing />
30 </div> 30 </div>
31 </div> 31 </div>
32 - <div v-else>您的浏览器不支持WebGL!</div>  
33 </div> 32 </div>
34 </template> 33 </template>
35 <script setup lang="ts"> 34 <script setup lang="ts">
36 -import { PropType, toRefs, ref, nextTick, computed, watch } from 'vue' 35 +import { PropType, toRefs, ref, nextTick, computed } from 'vue'
37 import { CreateComponentType } from '@/packages/index.d' 36 import { CreateComponentType } from '@/packages/index.d'
38 import { vue3dLoader } from 'vue-3d-loader' 37 import { vue3dLoader } from 'vue-3d-loader'
39 import { useDesignStore } from '@/store/modules/designStore/designStore' 38 import { useDesignStore } from '@/store/modules/designStore/designStore'
40 -import { useDetectWebGLContext } from '@/utils/external/useSupportWebGL'  
41 39
42 const designStore = useDesignStore() 40 const designStore = useDesignStore()
43 41
@@ -54,7 +52,6 @@ const themeColor = computed(() => { @@ -54,7 +52,6 @@ const themeColor = computed(() => {
54 52
55 const vue3dLoaderRef = ref(null) 53 const vue3dLoaderRef = ref(null)
56 54
57 -//threejs配置  
58 const webGLRendererOptions = { 55 const webGLRendererOptions = {
59 alpha: true, // 透明 56 alpha: true, // 透明
60 antialias: true, // 抗锯齿 57 antialias: true, // 抗锯齿
@@ -96,13 +93,9 @@ const { @@ -96,13 +93,9 @@ const {
96 rotation, 93 rotation,
97 lights, 94 lights,
98 showFps, 95 showFps,
99 - labels  
100 -} = toRefs(props.chartConfig.option) as any  
101 -  
102 -watch(dataset, (newData: string) => {  
103 - //dateset为空则清除场景  
104 - if(!newData) clearScene.value=true  
105 -}) 96 + labels,
  97 + lightInput
  98 +} = toRefs(props.chartConfig.option) as any
106 </script> 99 </script>
107 100
108 <style lang="scss" scoped> 101 <style lang="scss" scoped>
1 <template> 1 <template>
2 <div> 2 <div>
3 <n-tree 3 <n-tree
  4 + ref="nTreeRef"
4 :accordion="treeConfig.accordion" 5 :accordion="treeConfig.accordion"
5 :checkable="treeConfig.checkable" 6 :checkable="treeConfig.checkable"
6 :default-expand-all="treeConfig.defaultExpandAll" 7 :default-expand-all="treeConfig.defaultExpandAll"
@@ -11,17 +12,20 @@ @@ -11,17 +12,20 @@
11 label-field="name" 12 label-field="name"
12 children-field="children" 13 children-field="children"
13 @update:selected-keys="onClick" 14 @update:selected-keys="onClick"
  15 + @update:checked-keys="onClick"
  16 + :checked-keys="checkedKeys"
14 /> 17 />
15 </div> 18 </div>
16 </template> 19 </template>
17 20
18 <script setup lang="ts"> 21 <script setup lang="ts">
19 -import { PropType, toRefs } from 'vue' 22 +import { PropType, toRefs, ref } from 'vue'
20 import { CreateComponentType } from '@/packages/index.d' 23 import { CreateComponentType } from '@/packages/index.d'
21 import { useChartEditStore } from '@/store/modules/chartEditStore/chartEditStore' 24 import { useChartEditStore } from '@/store/modules/chartEditStore/chartEditStore'
22 import { useChartInteract } from '@/hooks/external/useChartSelectInteract.hook' 25 import { useChartInteract } from '@/hooks/external/useChartSelectInteract.hook'
23 import { InteractEventOn } from '@/enums/eventEnum' 26 import { InteractEventOn } from '@/enums/eventEnum'
24 import { ComponentInteractParamsEnum } from './interact' 27 import { ComponentInteractParamsEnum } from './interact'
  28 +import { NTree } from 'naive-ui'
25 29
26 const props = defineProps({ 30 const props = defineProps({
27 chartConfig: { 31 chartConfig: {
@@ -32,7 +36,16 @@ const props = defineProps({ @@ -32,7 +36,16 @@ const props = defineProps({
32 36
33 const { dataset, treeConfig } = toRefs(props.chartConfig.option) 37 const { dataset, treeConfig } = toRefs(props.chartConfig.option)
34 38
  39 +const nTreeRef = ref<null | InstanceType<typeof NTree>>(null)
  40 +
  41 +const checkedKeys = ref([])
  42 +
35 const onClick = (v: string[]) => { 43 const onClick = (v: string[]) => {
  44 + // nTreeRef.value?.selectedKeys(v)
  45 + console.log(nTreeRef.value)
  46 + console.log(v)
  47 + // nTreeRef.value?.onUpdateCheckedKeys(v)
  48 + if (Array.isArray(v) && v.length == 0) return
36 useChartInteract( 49 useChartInteract(
37 props.chartConfig, 50 props.chartConfig,
38 useChartEditStore, 51 useChartEditStore,
@@ -2,24 +2,11 @@ @@ -2,24 +2,11 @@
2 <collapse-item name="属性" :expanded="true"> 2 <collapse-item name="属性" :expanded="true">
3 <setting-item-box name="上传图片" :alone="true"> 3 <setting-item-box name="上传图片" :alone="true">
4 <setting-item> 4 <setting-item>
5 - <n-card class="upload-box">  
6 - <n-upload  
7 - :show-file-list="false"  
8 - v-model:file-list="uploadFileListRef"  
9 - :customRequest="customRequest"  
10 - :onBeforeUpload="beforeUploadHandle"  
11 - >  
12 - <n-upload-dragger>  
13 - <img v-if="optionData.dataset" class="upload-show" :src="optionData.dataset" alt="背景" />  
14 - <div class="upload-img" v-show="!optionData.dataset">  
15 - <img src="@/assets/images/canvas/noImage.png" />  
16 - <n-text class="upload-desc" depth="3">  
17 - 图片需小于 {{ backgroundImageSize }}M ,格式为 png/jpg/gif 的文件  
18 - </n-text>  
19 - </div>  
20 - </n-upload-dragger>  
21 - </n-upload>  
22 - </n-card> 5 + <TKUpload
  6 + :uploadImageUrl="props.optionData.dataset"
  7 + @sendFile="handleSendFile"
  8 + @removeFile="handleRemoveFile"
  9 + />
23 </setting-item> 10 </setting-item>
24 </setting-item-box> 11 </setting-item-box>
25 <setting-item-box name="样式"> 12 <setting-item-box name="样式">
@@ -39,14 +26,10 @@ @@ -39,14 +26,10 @@
39 </template> 26 </template>
40 27
41 <script setup lang="ts"> 28 <script setup lang="ts">
42 -import { PropType, ref, nextTick } from 'vue' 29 +import { PropType } from 'vue'
43 import { option } from './config' 30 import { option } from './config'
44 import { CollapseItem, SettingItemBox, SettingItem } from '@/components/Pages/ChartItemSetting' 31 import { CollapseItem, SettingItemBox, SettingItem } from '@/components/Pages/ChartItemSetting'
45 -import { FileTypeEnum } from '@/enums/fileTypeEnum'  
46 -import { uploadFile } from '@/api/external/contentSave/content'  
47 -import { UploadCustomRequestOptions } from 'naive-ui'  
48 -import { backgroundImageSize } from '@/settings/designSetting'  
49 -import { fetchRouteParamsLocation } from '@/utils' 32 +import { TKUpload } from '@/components/external/Common/TKUpload'
50 33
51 const props = defineProps({ 34 const props = defineProps({
52 optionData: { 35 optionData: {
@@ -55,48 +38,12 @@ const props = defineProps({ @@ -55,48 +38,12 @@ const props = defineProps({
55 } 38 }
56 }) 39 })
57 40
58 -const uploadFileListRef = ref()  
59 -  
60 -// 上传图片前置处理  
61 -// eslint-disable-next-line @typescript-eslint/ban-ts-comment  
62 -//@ts-ignore  
63 -const beforeUploadHandle = async ({ file }) => {  
64 - uploadFileListRef.value = []  
65 - const type = file.file.type  
66 - const size = file.file.size  
67 -  
68 - if (size > 1024 * 1024 * backgroundImageSize) {  
69 - window['$message'].warning(`图片超出 ${backgroundImageSize}M 限制,请重新上传!`)  
70 - return false  
71 - }  
72 - if (type !== FileTypeEnum.PNG && type !== FileTypeEnum.JPEG && type !== FileTypeEnum.GIF) {  
73 - window['$message'].warning('文件格式不符合,请重新上传!')  
74 - return false  
75 - }  
76 - return true 41 +const handleSendFile = (file: string) => {
  42 + if (!file) return
  43 + props.optionData.dataset = file
77 } 44 }
78 45
79 -// 自定义上传操作  
80 -const customRequest = (options: UploadCustomRequestOptions) => {  
81 - const { file } = options  
82 - nextTick(async () => {  
83 - if (file.file) {  
84 - // 修改名称  
85 - const newNameFile = new File([file.file], `${fetchRouteParamsLocation()}_index_background.png`, {  
86 - type: file.file.type  
87 - })  
88 - let uploadParams = new FormData()  
89 - uploadParams.append('file', newNameFile)  
90 - const uploadRes = await uploadFile(uploadParams)  
91 - if (uploadRes) {  
92 - props.optionData.dataset = uploadRes?.fileStaticUri  
93 - window['$message'].success('添加图片成功!')  
94 - }  
95 - } else {  
96 - window['$message'].error('添加图片失败,请稍后重试!')  
97 - }  
98 - })  
99 -} 46 +const handleRemoveFile = (status: boolean) => (status ? (props.optionData.dataset = '') : null)
100 47
101 // 适应类型 48 // 适应类型
102 const fitList = [ 49 const fitList = [
@@ -122,35 +69,3 @@ const fitList = [ @@ -122,35 +69,3 @@ const fitList = [
122 } 69 }
123 ] 70 ]
124 </script> 71 </script>
125 -<style lang="scss" scoped>  
126 -$uploadHeight: 193px;  
127 -.upload-box {  
128 - cursor: pointer;  
129 - margin-bottom: 20px;  
130 - @include deep() {  
131 - .n-card__content {  
132 - padding: 0;  
133 - overflow: hidden;  
134 - }  
135 - .n-upload-dragger {  
136 - padding: 5px;  
137 - }  
138 - }  
139 - .upload-show {  
140 - width: -webkit-fill-available;  
141 - height: $uploadHeight;  
142 - border-radius: 5px;  
143 - }  
144 - .upload-img {  
145 - display: flex;  
146 - flex-direction: column;  
147 - align-items: center;  
148 - img {  
149 - height: 150px;  
150 - }  
151 - .upload-desc {  
152 - padding: 10px 0;  
153 - }  
154 - }  
155 -}  
156 -</style>  
1 <template> 1 <template>
2 - <div class="videoPlay"> 2 + <div class="go-content-box" :style="{ width: w + 'px', height: h + 'px' }">
3 <video 3 <video
4 - style="object-fit: cover"  
5 - :poster="poster"  
6 crossOrigin="anonymous" 4 crossOrigin="anonymous"
  5 + :id="`my-player`"
7 ref="videoRef" 6 ref="videoRef"
8 - class="video-js vjs-default-skin vjs-big-play-centered"  
9 - controls  
10 - >  
11 - <source :src="path" />  
12 - </video> 7 + class="video-js my-video vjs-theme-city vjs-big-play-centered"
  8 + ></video>
13 </div> 9 </div>
14 </template> 10 </template>
15 -<script lang="ts" setup>  
16 -import { nextTick, onBeforeUnmount, onMounted, ref, watch, toRefs } from 'vue'  
17 -import videojs, { VideoJsPlayer } from 'video.js' 11 +<script setup lang="ts">
  12 +import { onMounted, ref, onUnmounted, watch, unref } from 'vue'
  13 +import videojs from 'video.js'
  14 +import 'videojs-flvjs-es6'
18 import type { VideoJsPlayerOptions } from 'video.js' 15 import type { VideoJsPlayerOptions } from 'video.js'
19 -import 'video.js/dist/video-js.css'  
20 -import zh from 'video.js/dist/lang/zh-CN.json'  
21 -  
22 -const props = withDefaults(  
23 - defineProps<{  
24 - path: string  
25 - autoPlay?: boolean  
26 - h?: number  
27 - poster?: string  
28 - }>(),  
29 - { autoPlay: false }  
30 -) 16 +import 'video.js/dist/video-js.min.css'
  17 +import { getJwtToken, getShareJwtToken } from '@/utils/external/auth'
  18 +import { isShareMode } from '@/views/share/hook'
  19 +import { getOpenFlvPlayUrl, closeFlvPlay } from '@/api/external/flvPlay'
  20 +import { useFingerprint } from '@/utils/external/useFingerprint'
  21 +import { GetResult } from '@fingerprintjs/fingerprintjs'
31 22
32 -const videoRef = ref()  
33 -  
34 -let player: VideoJsPlayer  
35 -  
36 -const initPlay = async () => {  
37 - videojs.addLanguage('zh-CN', zh)  
38 - await nextTick()  
39 - const options: VideoJsPlayerOptions = {  
40 - muted: true,  
41 - controls: true,  
42 - autoplay: props.autoPlay,  
43 - src: props?.path,  
44 - poster: props?.poster,  
45 - language: 'zh-CN',  
46 - techOrder: ['html5'],  
47 - preload: 'none' 23 +const props = defineProps({
  24 + sourceSrc: {
  25 + type: String
  26 + },
  27 + autoplay: {
  28 + type: Boolean
  29 + },
  30 + name: {
  31 + type: String
  32 + },
  33 + avatar: {
  34 + type: String
  35 + },
  36 + w: {
  37 + type: Number,
  38 + default: 300
  39 + },
  40 + h: {
  41 + type: Number,
  42 + default: 300
48 } 43 }
49 - player = videojs(videoRef.value, options, () => {  
50 - videojs.log('Video is reading')  
51 - if (props.autoPlay) {  
52 - player.play() 44 +})
  45 +
  46 +enum VideoPlayerType {
  47 + m3u8 = 'application/x-mpegURL',
  48 + mp4 = 'video/mp4',
  49 + webm = 'video/webm',
  50 + flv = 'video/x-flv'
  51 +}
  52 +
  53 +const isRtspProtocol = (url: string) => {
  54 + const reg = /^rtsp:\/\//g
  55 + return reg.test(url)
  56 +}
  57 +
  58 +const getVideoTypeByUrl = (url = '') => {
  59 + try {
  60 + const { protocol, pathname } = new URL(url)
  61 + if (protocol.startsWith('rtsp:')) return VideoPlayerType.flv
  62 + const reg = /[^.]\w*$/
  63 + const mathValue = pathname.match(reg) || []
  64 + const ext = (mathValue[0] as keyof typeof VideoPlayerType) || 'webm'
  65 + const type = VideoPlayerType[ext]
  66 + return type ? type : VideoPlayerType.webm
  67 + } catch (error) {
  68 + console.error(error)
  69 + return VideoPlayerType.webm
  70 + }
  71 +}
  72 +
  73 +// video标签
  74 +const videoRef = ref<HTMLElement | null>(null)
  75 +
  76 +// video实例对象
  77 +let videoPlayer: videojs.Player | null = null
  78 +
  79 +const fingerprintResult = ref<Nullable<GetResult>>(null)
  80 +
  81 +//options配置
  82 +const options: VideoJsPlayerOptions & Recordable = {
  83 + language: 'zh-CN', // 设置语言
  84 + controls: true, // 是否显示控制条
  85 + preload: 'auto', // 预加载
  86 + autoplay: true, // 是否自动播放
  87 + fluid: false, // 自适应宽高
  88 + poster: props?.avatar || '',
  89 + // src: getSource() || '', // 要嵌入的视频源的源 URL
  90 + sources: [],
  91 + muted: true,
  92 + userActions: {
  93 + hotkeys: true
  94 + },
  95 + techOrder: ['html5', 'flvjs'],
  96 + flvjs: {
  97 + mediaDataSource: {
  98 + isLive: true,
  99 + cors: true,
  100 + withCredentials: false,
  101 + hasAudio: false
  102 + },
  103 + config: {
  104 + autoCleanupSourceBuffer: true
53 } 105 }
54 - player.on('ended', () => {  
55 - videojs.log('Play end')  
56 - })  
57 - player.on('error', () => {  
58 - player.errorDisplay.close()  
59 - videojs.log('Play parse error')  
60 - })  
61 - }) 106 + }
62 } 107 }
63 108
64 -onMounted(() => {  
65 - initPlay()  
66 -}) 109 +const { getResult } = useFingerprint()
  110 +async function getSource() {
  111 + fingerprintResult.value = await getResult()
  112 + let src = props.sourceSrc || ''
  113 + if (isRtspProtocol(props.sourceSrc!)) {
  114 + src = getOpenFlvPlayUrl(src, unref(fingerprintResult)?.visitorId || '')
  115 + }
  116 + return [
  117 + {
  118 + type: getVideoTypeByUrl(props.sourceSrc),
  119 + src
  120 + }
  121 + ]
  122 +}
67 123
68 -//直接改变路径测试  
69 -watch(  
70 - () => props.path,  
71 - () => {  
72 - if (props.path && props.autoPlay) {  
73 - try {  
74 - player?.pause()  
75 - player?.load()  
76 - player?.src(props.path)  
77 - player?.play()  
78 - } catch (e) {  
79 - console.log(e) 124 +// 初始化videojs
  125 +const initVideo = async () => {
  126 + if (videoRef.value) {
  127 + // 创建 video 实例
  128 + options.sources = await getSource()
  129 + if (options.sources && options.sources.length) {
  130 + if (isRtspProtocol(props.sourceSrc || '')) {
  131 + options.flvjs = {
  132 + ...(options.flvjs || {}),
  133 + config: {
  134 + headers: {
  135 + 'X-Authorization': `Bearer ${isShareMode() ? getShareJwtToken() : getJwtToken()}`
  136 + }
  137 + }
  138 + }
80 } 139 }
  140 + videoPlayer = videojs(videoRef.value, options)
81 } 141 }
  142 + }
  143 +}
  144 +
  145 +watch(
  146 + () => props.sourceSrc,
  147 + async (newData: any) => {
  148 + const result = await getSource()
  149 + // props.sourceSrc = newData
  150 + videoPlayer?.src(result) as any
  151 + videoPlayer?.play()
82 }, 152 },
83 { 153 {
84 immediate: true 154 immediate: true
85 } 155 }
86 ) 156 )
87 157
88 -onBeforeUnmount(() => {  
89 - player?.dispose() 158 +onMounted(() => {
  159 + initVideo()
  160 +})
  161 +
  162 +onUnmounted(() => {
  163 + if (props.sourceSrc) {
  164 + closeFlvPlay(props.sourceSrc, unref(fingerprintResult)!.visitorId!)
  165 + }
  166 + handleVideoDispose()
  167 +})
  168 +
  169 +//播放
  170 +const handleVideoPlay = () => videoPlayer?.play()
  171 +
  172 +const handleVideoDispose = () => videoPlayer?.dispose() && videoPlayer?.pause()
  173 +//暂停
  174 +defineExpose({
  175 + handleVideoPlay,
  176 + handleVideoDispose
90 }) 177 })
91 </script> 178 </script>
92 -<style>  
93 -.vjs-poster {  
94 - background-size: cover !important;  
95 -}  
96 -</style> 179 +
97 <style lang="scss" scoped> 180 <style lang="scss" scoped>
98 -.videoPlay {  
99 - flex: 1;  
100 - height: v-bind('`${h}px`');  
101 - .video-js {  
102 - height: 100%;  
103 - width: 100%; 181 +.go-content-box {
  182 + display: flex;
  183 + align-items: center;
  184 + justify-content: center;
  185 +
  186 + .my-video {
  187 + width: 100% !important;
  188 + height: 100% !important;
  189 + position: relative;
104 } 190 }
105 } 191 }
106 </style> 192 </style>
@@ -2,29 +2,12 @@ @@ -2,29 +2,12 @@
2 <CollapseItem name="播放器配置" :expanded="true"> 2 <CollapseItem name="播放器配置" :expanded="true">
3 <setting-item-box name="上传图片" :alone="true"> 3 <setting-item-box name="上传图片" :alone="true">
4 <setting-item> 4 <setting-item>
5 - <n-card class="upload-box">  
6 - <n-upload  
7 - :show-file-list="false"  
8 - v-model:file-list="uploadFileListRef"  
9 - :customRequest="customRequest"  
10 - :onBeforeUpload="beforeUploadHandle"  
11 - >  
12 - <n-upload-dragger>  
13 - <img v-if="optionData.poster" class="upload-show" :src="optionData.poster" alt="背景" />  
14 - <div class="upload-img" v-show="!optionData.poster">  
15 - <img src="@/assets/images/canvas/noImage.png" />  
16 - <n-text class="upload-desc" depth="3">  
17 - 图片需小于 {{ backgroundImageSize }}M ,格式为 png/jpg/gif 的文件  
18 - </n-text>  
19 - </div>  
20 - </n-upload-dragger>  
21 - </n-upload>  
22 - </n-card> 5 + <TKUpload :uploadImageUrl="optionData.poster" @sendFile="handleSendFile" @removeFile="handleRemoveFile" />
23 </setting-item> 6 </setting-item>
24 </setting-item-box> 7 </setting-item-box>
25 <setting-item-box name="源类型" :alone="true"> 8 <setting-item-box name="源类型" :alone="true">
26 <setting-item> 9 <setting-item>
27 - <n-radio-group @change="handleChecked" v-model:value="optionData.sourceType" name="radiogroup"> 10 + <n-radio-group @update:value="handleChecked" v-model:value="optionData.sourceType" name="radiogroup">
28 <n-space> 11 <n-space>
29 <n-radio v-for="(item, index) in sourceTypes" :key="item.value" :value="item.value"> 12 <n-radio v-for="(item, index) in sourceTypes" :key="item.value" :value="item.value">
30 {{ item.label }} 13 {{ item.label }}
@@ -55,7 +38,12 @@ @@ -55,7 +38,12 @@
55 </setting-item-box> 38 </setting-item-box>
56 <setting-item-box v-if="optionData.sourceType === sourceTypeEnum.PLATFORM" name="视频" :alone="true"> 39 <setting-item-box v-if="optionData.sourceType === sourceTypeEnum.PLATFORM" name="视频" :alone="true">
57 <setting-item> 40 <setting-item>
58 - <n-select @update:value="handleSelect" v-model:value="optionData.dataset" :options="videoOptions" /> 41 + <n-select
  42 + @update:value="handleSelect"
  43 + v-model:value="optionData.dataset"
  44 + :options="videoOptions"
  45 + placeholder="请选择视频地址"
  46 + />
59 </setting-item> 47 </setting-item>
60 </setting-item-box> 48 </setting-item-box>
61 <setting-item-box name="自动播放"> 49 <setting-item-box name="自动播放">
@@ -67,15 +55,21 @@ @@ -67,15 +55,21 @@
67 </template> 55 </template>
68 56
69 <script setup lang="ts"> 57 <script setup lang="ts">
70 -import { PropType, ref, nextTick, onMounted } from 'vue' 58 +import { PropType, ref, onMounted } from 'vue'
71 import { option, sourceTypeEnum } from './config' 59 import { option, sourceTypeEnum } from './config'
72 import { CollapseItem, SettingItemBox, SettingItem } from '@/components/Pages/ChartItemSetting' 60 import { CollapseItem, SettingItemBox, SettingItem } from '@/components/Pages/ChartItemSetting'
73 -import { FileTypeEnum } from '@/enums/fileTypeEnum'  
74 -import { uploadFile } from '@/api/external/contentSave/content'  
75 -import { UploadCustomRequestOptions, NTreeSelect } from 'naive-ui'  
76 -import { backgroundImageSize } from '@/settings/designSetting'  
77 -import { fetchRouteParamsLocation } from '@/utils' 61 +import { NTreeSelect } from 'naive-ui'
78 import { getOrganizationList, getVideoList, getVideoUrl } from '@/api/external/common/index' 62 import { getOrganizationList, getVideoList, getVideoUrl } from '@/api/external/common/index'
  63 +import { TKUpload } from '@/components/external/Common/TKUpload'
  64 +
  65 +interface videoListIF {
  66 + name: string
  67 + accessMode: number
  68 + id: string
  69 + videoUrl: string
  70 + label: string
  71 + value: string
  72 +}
79 73
80 const props = defineProps({ 74 const props = defineProps({
81 optionData: { 75 optionData: {
@@ -84,8 +78,6 @@ const props = defineProps({ @@ -84,8 +78,6 @@ const props = defineProps({
84 } 78 }
85 }) 79 })
86 80
87 -const uploadFileListRef = ref()  
88 -  
89 const sourceTypes = [ 81 const sourceTypes = [
90 { 82 {
91 value: 'custom', 83 value: 'custom',
@@ -99,7 +91,7 @@ const sourceTypes = [ @@ -99,7 +91,7 @@ const sourceTypes = [
99 91
100 const originationOption = ref([]) 92 const originationOption = ref([])
101 93
102 -const videoOptions = ref([]) 94 +const videoOptions = ref<videoListIF[]>([])
103 95
104 const getOriginationList = async () => { 96 const getOriginationList = async () => {
105 const res = await getOrganizationList() 97 const res = await getOrganizationList()
@@ -113,7 +105,8 @@ const handleUpdateTreeValue = (value: string) => { @@ -113,7 +105,8 @@ const handleUpdateTreeValue = (value: string) => {
113 105
114 const getVideoLists = async (organizationId: string) => { 106 const getVideoLists = async (organizationId: string) => {
115 const res = await getVideoList({ organizationId }) 107 const res = await getVideoList({ organizationId })
116 - videoOptions.value = res?.data?.map((item: any) => ({ 108 + if (!res) return
  109 + videoOptions.value = res?.data?.map((item: videoListIF) => ({
117 label: item.name, 110 label: item.name,
118 value: item.accessMode === 1 ? item.id : item.videoUrl, 111 value: item.accessMode === 1 ? item.id : item.videoUrl,
119 id: item.id, 112 id: item.id,
@@ -128,16 +121,16 @@ const getVideoUrlById = async (id: string) => { @@ -128,16 +121,16 @@ const getVideoUrlById = async (id: string) => {
128 props.optionData.url = url 121 props.optionData.url = url
129 } 122 }
130 123
131 -const handleChecked = ({ target }: any) => { 124 +const handleChecked = (value: string) => {
132 props.optionData.dataset = '' 125 props.optionData.dataset = ''
133 - const { value } = target  
134 if (value === sourceTypeEnum.PLATFORM) { 126 if (value === sourceTypeEnum.PLATFORM) {
135 getOriginationList() 127 getOriginationList()
136 } 128 }
137 } 129 }
138 130
139 -const handleSelect = (value: string, e: any) => { 131 +const handleSelect = (_: string, e: videoListIF) => {
140 const { accessMode, id } = e 132 const { accessMode, id } = e
  133 + //1表示,需要从服务端调取接口换取播放的地址,0则不需要
141 if (accessMode === 1) { 134 if (accessMode === 1) {
142 getVideoUrlById(id) 135 getVideoUrlById(id)
143 } else { 136 } else {
@@ -154,76 +147,10 @@ onMounted(() => { @@ -154,76 +147,10 @@ onMounted(() => {
154 } 147 }
155 }) 148 })
156 149
157 -// 上传图片前置处理  
158 -// eslint-disable-next-line @typescript-eslint/ban-ts-comment  
159 -//@ts-ignore  
160 -const beforeUploadHandle = async ({ file }) => {  
161 - uploadFileListRef.value = []  
162 - const type = file.file.type  
163 - const size = file.file.size  
164 -  
165 - if (size > 1024 * 1024 * backgroundImageSize) {  
166 - window['$message'].warning(`图片超出 ${backgroundImageSize}M 限制,请重新上传!`)  
167 - return false  
168 - }  
169 - if (type !== FileTypeEnum.PNG && type !== FileTypeEnum.JPEG && type !== FileTypeEnum.GIF) {  
170 - window['$message'].warning('文件格式不符合,请重新上传!')  
171 - return false  
172 - }  
173 - return true 150 +const handleSendFile = (file: string) => {
  151 + if (!file) return
  152 + props.optionData.poster = file
174 } 153 }
175 154
176 -// 自定义上传操作  
177 -const customRequest = (options: UploadCustomRequestOptions) => {  
178 - const { file } = options  
179 - nextTick(async () => {  
180 - if (file.file) {  
181 - // 修改名称  
182 - const newNameFile = new File([file.file], `${fetchRouteParamsLocation()}_index_background.png`, {  
183 - type: file.file.type  
184 - })  
185 - let uploadParams = new FormData()  
186 - uploadParams.append('file', newNameFile)  
187 - const uploadRes = await uploadFile(uploadParams)  
188 - if (uploadRes) {  
189 - props.optionData.poster = uploadRes?.fileStaticUri  
190 - window['$message'].success('添加图片成功!')  
191 - }  
192 - } else {  
193 - window['$message'].error('添加图片失败,请稍后重试!')  
194 - }  
195 - })  
196 -} 155 +const handleRemoveFile = (status: boolean) => (status ? (props.optionData.poster = '') : null)
197 </script> 156 </script>
198 -<style lang="scss" scoped>  
199 -$uploadHeight: 193px;  
200 -.upload-box {  
201 - cursor: pointer;  
202 - margin-bottom: 20px;  
203 - @include deep() {  
204 - .n-card__content {  
205 - padding: 0;  
206 - overflow: hidden;  
207 - }  
208 - .n-upload-dragger {  
209 - padding: 5px;  
210 - }  
211 - }  
212 - .upload-show {  
213 - width: -webkit-fill-available;  
214 - height: $uploadHeight;  
215 - border-radius: 5px;  
216 - }  
217 - .upload-img {  
218 - display: flex;  
219 - flex-direction: column;  
220 - align-items: center;  
221 - img {  
222 - height: 150px;  
223 - }  
224 - .upload-desc {  
225 - padding: 10px 0;  
226 - }  
227 - }  
228 -}  
229 -</style>  
1 <template> 1 <template>
2 <div> 2 <div>
3 - <VideoPlay :h="h" :path="option.dataset" :autoPlay="autoplay" :poster="option.poster" /> 3 + <VideoPlay :w="w" :h="h" :sourceSrc="option.dataset" :autoPlay="autoplay" :avatar="option.poster" />
4 </div> 4 </div>
5 </template> 5 </template>
6 <script setup lang="ts"> 6 <script setup lang="ts">
@@ -16,7 +16,7 @@ const props = defineProps({ @@ -16,7 +16,7 @@ const props = defineProps({
16 } 16 }
17 }) 17 })
18 18
19 -const { h } = toRefs(props.chartConfig.attr) 19 +const { w, h } = toRefs(props.chartConfig.attr)
20 20
21 const { autoplay, dataset, poster, url } = toRefs(props.chartConfig.option) 21 const { autoplay, dataset, poster, url } = toRefs(props.chartConfig.option)
22 22
@@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
2 <collapse-item name="信息" :expanded="true"> 2 <collapse-item name="信息" :expanded="true">
3 <setting-item-box name="文字" :alone="true"> 3 <setting-item-box name="文字" :alone="true">
4 <setting-item> 4 <setting-item>
5 - <n-input v-model:value="optionData.dataset" size="small"></n-input> 5 + <n-input v-model:value="optionData.dataset" type="textarea" size="small"></n-input>
6 </setting-item> 6 </setting-item>
7 </setting-item-box> 7 </setting-item-box>
8 </collapse-item> 8 </collapse-item>
@@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
2 <collapse-item name="信息" :expanded="true"> 2 <collapse-item name="信息" :expanded="true">
3 <setting-item-box name="文字" :alone="true"> 3 <setting-item-box name="文字" :alone="true">
4 <setting-item> 4 <setting-item>
5 - <n-input v-model:value="optionData.dataset" size="small"></n-input> 5 + <n-input v-model:value="optionData.dataset" type="textarea" size="small"></n-input>
6 </setting-item> 6 </setting-item>
7 </setting-item-box> 7 </setting-item-box>
8 <setting-item-box name="链接" :alone="true"> 8 <setting-item-box name="链接" :alone="true">
1 <template> 1 <template>
2 <div class="go-text-box"> 2 <div class="go-text-box">
3 <div class="content"> 3 <div class="content">
4 - <span style="cursor: pointer" v-if="link" @click="click">{{ option.dataset }}</span>  
5 - <span v-else>{{ option.dataset }}</span> 4 + <span style="cursor: pointer;white-space: pre-wrap" v-if="link" @click="click">{{ option.dataset }}</span>
  5 + <span style="white-space: pre-wrap" v-else>{{ option.dataset }}</span>
6 </div> 6 </div>
7 </div> 7 </div>
8 </template> 8 </template>
@@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
2 <collapse-item name="信息" :expanded="true"> 2 <collapse-item name="信息" :expanded="true">
3 <setting-item-box name="文字" :alone="true"> 3 <setting-item-box name="文字" :alone="true">
4 <setting-item> 4 <setting-item>
5 - <n-input v-model:value="optionData.dataset" size="small"></n-input> 5 + <n-input v-model:value="optionData.dataset" type="textarea" size="small"></n-input>
6 </setting-item> 6 </setting-item>
7 </setting-item-box> 7 </setting-item-box>
8 </collapse-item> 8 </collapse-item>
@@ -353,9 +353,9 @@ useChartDataFetch(props.chartConfig, useChartEditStore, async (resData: any[], r @@ -353,9 +353,9 @@ useChartDataFetch(props.chartConfig, useChartEditStore, async (resData: any[], r
353 attribute.forEach((item: any) => { 353 attribute.forEach((item: any) => {
354 if (item.identifier === curr[0]) { 354 if (item.identifier === curr[0]) {
355 curr[0] = item.name 355 curr[0] = item.name
  356 + acc.push(curr)
356 } 357 }
357 }) 358 })
358 - acc.push(curr)  
359 return [...acc] 359 return [...acc]
360 }, []) 360 }, [])
361 props.chartConfig.option.dataset = resDataFormat 361 props.chartConfig.option.dataset = resDataFormat
@@ -27,6 +27,16 @@ export let packagesList: PackagesType = { @@ -27,6 +27,16 @@ export let packagesList: PackagesType = {
27 [PackagesCategoryEnum.ICONS]: IconList 27 [PackagesCategoryEnum.ICONS]: IconList
28 } 28 }
29 29
  30 +// 组件缓存, 可以大幅度提升组件加载速度
  31 +const componentCacheMap = new Map<string, any>()
  32 +const loadConfig = (packageName: string, categoryName: string, keyName: string) => {
  33 + const key = packageName + categoryName + keyName
  34 + if (!componentCacheMap.has(key)) {
  35 + componentCacheMap.set(key, import(`./components/${packageName}/${categoryName}/${keyName}/config.ts`))
  36 + }
  37 + return componentCacheMap.get(key)
  38 +}
  39 +
30 /** 40 /**
31 * * 获取目标组件配置信息 41 * * 获取目标组件配置信息
32 * @param targetData 42 * @param targetData
@@ -36,10 +46,10 @@ export const createComponent = async (targetData: ConfigType) => { @@ -36,10 +46,10 @@ export const createComponent = async (targetData: ConfigType) => {
36 // redirectComponent 是给图片组件库和图标组件库使用的 46 // redirectComponent 是给图片组件库和图标组件库使用的
37 if (redirectComponent) { 47 if (redirectComponent) {
38 const [packageName, categoryName, keyName] = redirectComponent.split('/') 48 const [packageName, categoryName, keyName] = redirectComponent.split('/')
39 - const redirectChart = await import(`./components/${packageName}/${categoryName}/${keyName}/config.ts`) 49 + const redirectChart = await loadConfig(packageName, categoryName, keyName)
40 return new redirectChart.default() 50 return new redirectChart.default()
41 } 51 }
42 - const chart = await import(`./components/${targetData.package}/${category}/${key}/config.ts`) 52 + const chart = await loadConfig(targetData.package, category, key)
43 return new chart.default() 53 return new chart.default()
44 } 54 }
45 55
@@ -98,5 +108,4 @@ export const fetchImages = async (targetData?: ConfigType) => { @@ -98,5 +108,4 @@ export const fetchImages = async (targetData?: ConfigType) => {
98 } 108 }
99 return '' 109 return ''
100 } 110 }
101 -  
102 useInjectLib(packagesList as any) // THINGS_KIT 修改注册组件 111 useInjectLib(packagesList as any) // THINGS_KIT 修改注册组件
@@ -24,7 +24,6 @@ import cloneDeep from 'lodash/cloneDeep' @@ -24,7 +24,6 @@ import cloneDeep from 'lodash/cloneDeep'
24 * 修改后的代码在注释之间,并标注好源代码和修改后代码,方便回溯 24 * 修改后的代码在注释之间,并标注好源代码和修改后代码,方便回溯
25 * 源代码 requestDataType: RequestDataTypeEnum.STATIC, 25 * 源代码 requestDataType: RequestDataTypeEnum.STATIC,
26 * 修改后的代码 requestDataType: RequestDataTypeEnum.Pond, 26 * 修改后的代码 requestDataType: RequestDataTypeEnum.Pond,
27 - * 修改后代码在//ft之间  
28 */ 27 */
29 // 请求基础属性 28 // 请求基础属性
30 export const requestConfig: RequestConfigType = { 29 export const requestConfig: RequestConfigType = {
@@ -49,7 +48,6 @@ export const requestConfig: RequestConfigType = { @@ -49,7 +48,6 @@ export const requestConfig: RequestConfigType = {
49 Params: {} 48 Params: {}
50 } 49 }
51 } 50 }
52 -//ft之间  
53 51
54 // 单实例类 52 // 单实例类
55 export class PublicConfigClass implements PublicConfigType { 53 export class PublicConfigClass implements PublicConfigType {
@@ -162,17 +162,17 @@ export const useChartEditStore = defineStore({ @@ -162,17 +162,17 @@ export const useChartEditStore = defineStore({
162 }, 162 },
163 getComponentList(): Array<CreateComponentType | CreateComponentGroupType> { 163 getComponentList(): Array<CreateComponentType | CreateComponentGroupType> {
164 return this.componentList 164 return this.componentList
165 - },  
166 - // 获取需要存储的数据项 165 + }
  166 + },
  167 + actions: {
  168 + // * 获取需要存储的数据项
167 getStorageInfo(): ChartEditStorage { 169 getStorageInfo(): ChartEditStorage {
168 return { 170 return {
169 [ChartEditStoreEnum.EDIT_CANVAS_CONFIG]: this.getEditCanvasConfig, 171 [ChartEditStoreEnum.EDIT_CANVAS_CONFIG]: this.getEditCanvasConfig,
170 [ChartEditStoreEnum.COMPONENT_LIST]: this.getComponentList, 172 [ChartEditStoreEnum.COMPONENT_LIST]: this.getComponentList,
171 [ChartEditStoreEnum.REQUEST_GLOBAL_CONFIG]: this.getRequestGlobalConfig 173 [ChartEditStoreEnum.REQUEST_GLOBAL_CONFIG]: this.getRequestGlobalConfig
172 } 174 }
173 - }  
174 - },  
175 - actions: { 175 + },
176 // * 设置 editCanvas 数据项 176 // * 设置 editCanvas 数据项
177 setEditCanvas<T extends keyof EditCanvasType, K extends EditCanvasType[T]>(key: T, value: K) { 177 setEditCanvas<T extends keyof EditCanvasType, K extends EditCanvasType[T]>(key: T, value: K) {
178 this.editCanvas[key] = value 178 this.editCanvas[key] = value
  1 +/**
  2 + *
  3 + * @param url 图片路径
  4 + * @param ext 图片格式
  5 + * @param callback 结果回调
  6 + * 类型暂定any
  7 + */
  8 +export function getUrlBase64(url: any, ext: any, callback: any) {
  9 + let canvas = document.createElement("canvas") as any; //创建canvas DOM元素
  10 + const ctx = canvas.getContext("2d") as any;
  11 + const img = new Image;
  12 + img.crossOrigin = 'Anonymous';
  13 + img.src = url;
  14 + img.onload = function () {
  15 + canvas.height = 35; //指定画板的高度,自定义
  16 + canvas.width = 35; //指定画板的宽度,自定义
  17 + ctx.drawImage(img, 0, 0, 35, 35); //参数可自定义
  18 + const dataURL = canvas.toDataURL("image/" + ext);
  19 + callback.call(this, dataURL); //回掉函数获取Base64编码
  20 + canvas = null;
  21 + };
  22 +}
@@ -82,7 +82,9 @@ import { fetchConfigComponent, fetchChartComponent } from '@/packages/index' @@ -82,7 +82,9 @@ import { fetchConfigComponent, fetchChartComponent } from '@/packages/index'
82 import { componentInstall, loadingStart, loadingFinish, loadingError } from '@/utils' 82 import { componentInstall, loadingStart, loadingFinish, loadingError } from '@/utils'
83 import { ChartGlobImage } from '@/components/Pages/ChartGlobImage' 83 import { ChartGlobImage } from '@/components/Pages/ChartGlobImage'
84 import { Icon } from '@iconify/vue' 84 import { Icon } from '@iconify/vue'
85 - 85 +// THINGS_KIT 修改搜索未过滤隐藏的组件
  86 +import { hideAsideComponentsObj } from '../../external/components/ChartsOptionContent/config'
  87 +//
86 const props = defineProps({ 88 const props = defineProps({
87 menuOptions: { 89 menuOptions: {
88 type: Array, 90 type: Array,
@@ -136,6 +138,9 @@ const searchHandle = (key: string | null) => { @@ -136,6 +138,9 @@ const searchHandle = (key: string | null) => {
136 searchRes.value = List.filter( 138 searchRes.value = List.filter(
137 (e: ConfigType) => !e.disabled && (!key || e.title.toLowerCase().includes(key.toLowerCase())) 139 (e: ConfigType) => !e.disabled && (!key || e.title.toLowerCase().includes(key.toLowerCase()))
138 ) 140 )
  141 + // THINGS_KIT 修改搜索未过滤隐藏的组件
  142 + searchRes.value = searchRes.value.filter((e: ConfigType) =>!hideAsideComponentsObj['all'].includes(e.chartKey))
  143 + //
139 setTimeout(() => { 144 setTimeout(() => {
140 loading.value = undefined 145 loading.value = undefined
141 }, 500) 146 }, 500)
@@ -10,13 +10,7 @@ import { useInjectAside } from './external/useInjectAside' @@ -10,13 +10,7 @@ import { useInjectAside } from './external/useInjectAside'
10 10
11 // 图标 11 // 图标
12 const { AirPlaneOutlineIcon, ImageIcon, BarChartIcon } = icon.ionicons5 12 const { AirPlaneOutlineIcon, ImageIcon, BarChartIcon } = icon.ionicons5
13 -const {  
14 - TableSplitIcon,  
15 - RoadmapIcon,  
16 - SpellCheckIcon,  
17 - GraphicalDataFlowIcon,  
18 -} = icon.carbon  
19 - 13 +const { TableSplitIcon, RoadmapIcon, SpellCheckIcon, GraphicalDataFlowIcon } = icon.carbon
20 14
21 // 图表 15 // 图表
22 export type MenuOptionsType = { 16 export type MenuOptionsType = {
@@ -9,7 +9,7 @@ export const exportHandle = () => { @@ -9,7 +9,7 @@ export const exportHandle = () => {
9 9
10 // 导出数据 10 // 导出数据
11 downloadTextFile( 11 downloadTextFile(
12 - JSONStringify(chartEditStore.getStorageInfo || []), 12 + JSONStringify(chartEditStore.getStorageInfo() || []),
13 undefined, 13 undefined,
14 'json' 14 'json'
15 ) 15 )
@@ -19,14 +19,14 @@ export const syncData = () => { @@ -19,14 +19,14 @@ export const syncData = () => {
19 transformOrigin: 'center', 19 transformOrigin: 'center',
20 onPositiveCallback: () => { 20 onPositiveCallback: () => {
21 window['$message'].success('正在同步编辑器...') 21 window['$message'].success('正在同步编辑器...')
22 - dispatchEvent(new CustomEvent(SavePageEnum.CHART, { detail: chartEditStore.getStorageInfo })) 22 + dispatchEvent(new CustomEvent(SavePageEnum.CHART, { detail: chartEditStore.getStorageInfo() }))
23 } 23 }
24 }) 24 })
25 } 25 }
26 26
27 // 同步数据到预览页 27 // 同步数据到预览页
28 export const syncDataToPreview = () => { 28 export const syncDataToPreview = () => {
29 - dispatchEvent(new CustomEvent(SavePageEnum.CHART_TO_PREVIEW, { detail: chartEditStore.getStorageInfo })) 29 + dispatchEvent(new CustomEvent(SavePageEnum.CHART_TO_PREVIEW, { detail: chartEditStore.getStorageInfo() }))
30 } 30 }
31 31
32 // 侦听器更新 32 // 侦听器更新
@@ -160,7 +160,7 @@ const editHandle = () => { @@ -160,7 +160,7 @@ const editHandle = () => {
160 160
161 // 把内存中的数据同步到SessionStorage 便于传递给新窗口初始化数据 161 // 把内存中的数据同步到SessionStorage 便于传递给新窗口初始化数据
162 const updateToSession = (id: string) => { 162 const updateToSession = (id: string) => {
163 - const storageInfo = chartEditStore.getStorageInfo 163 + const storageInfo = chartEditStore.getStorageInfo()
164 const sessionStorageInfo = getLocalStorage(StorageEnum.GO_CHART_STORAGE_LIST) || [] 164 const sessionStorageInfo = getLocalStorage(StorageEnum.GO_CHART_STORAGE_LIST) || []
165 165
166 if (sessionStorageInfo?.length) { 166 if (sessionStorageInfo?.length) {
@@ -9,7 +9,7 @@ export const exportHandle = () => { @@ -9,7 +9,7 @@ export const exportHandle = () => {
9 9
10 // 导出数据 10 // 导出数据
11 downloadTextFile( 11 downloadTextFile(
12 - JSONStringify(chartEditStore.getStorageInfo || []), 12 + JSONStringify(chartEditStore.getStorageInfo() || []),
13 undefined, 13 undefined,
14 'json' 14 'json'
15 ) 15 )
@@ -32,7 +32,7 @@ const previewHandle = () => { @@ -32,7 +32,7 @@ const previewHandle = () => {
32 const { id } = routerParamsInfo.params 32 const { id } = routerParamsInfo.params
33 // id 标识 33 // id 标识
34 const previewId = typeof id === 'string' ? id : id[0] 34 const previewId = typeof id === 'string' ? id : id[0]
35 - const storageInfo = chartEditStore.getStorageInfo 35 + const storageInfo = chartEditStore.getStorageInfo()
36 const sessionStorageInfo = getLocalStorage(StorageEnum.GO_CHART_STORAGE_LIST) || [] 36 const sessionStorageInfo = getLocalStorage(StorageEnum.GO_CHART_STORAGE_LIST) || []
37 37
38 if (sessionStorageInfo?.length) { 38 if (sessionStorageInfo?.length) {
@@ -32,7 +32,7 @@ const previewHandle = () => { @@ -32,7 +32,7 @@ const previewHandle = () => {
32 const { id } = routerParamsInfo.params 32 const { id } = routerParamsInfo.params
33 // id 标识 33 // id 标识
34 const previewId = typeof id === 'string' ? id : id[0] 34 const previewId = typeof id === 'string' ? id : id[0]
35 - const storageInfo = chartEditStore.getStorageInfo 35 + const storageInfo = chartEditStore.getStorageInfo()
36 const sessionStorageInfo = getLocalStorage(StorageEnum.GO_CHART_STORAGE_LIST) || [] 36 const sessionStorageInfo = getLocalStorage(StorageEnum.GO_CHART_STORAGE_LIST) || []
37 37
38 if (sessionStorageInfo?.length) { 38 if (sessionStorageInfo?.length) {
@@ -76,7 +76,7 @@ const handleBlur = async () => { @@ -76,7 +76,7 @@ const handleBlur = async () => {
76 dataViewId, 76 dataViewId,
77 dataViewContent: { 77 dataViewContent: {
78 id: dataViewContent.id, 78 id: dataViewContent.id,
79 - content: JSON.stringify(chartEditStore.getStorageInfo) 79 + content: JSON.stringify(chartEditStore.getStorageInfo())
80 } 80 }
81 } as unknown as BaseUpdateContentParams 81 } as unknown as BaseUpdateContentParams
82 await contentUpdateApi(saveContent) 82 await contentUpdateApi(saveContent)
@@ -92,7 +92,7 @@ export const useSyncRemote = () => { @@ -92,7 +92,7 @@ export const useSyncRemote = () => {
92 const saveContent = { 92 const saveContent = {
93 dataViewContent: { 93 dataViewContent: {
94 id: dataViewContent.id, 94 id: dataViewContent.id,
95 - content: JSONStringify(chartEditStore.getStorageInfo || {}) 95 + content: JSONStringify(chartEditStore.getStorageInfo() || {})
96 }, 96 },
97 dataViewName, 97 dataViewName,
98 dataViewId 98 dataViewId
1 <template> 1 <template>
2 <div 2 <div
3 - class="chart-item"  
4 - v-for="item in groupData.groupList"  
5 - :class="animationsClass(item.styles.animations)"  
6 - :key="item.id" 3 + :class="animationsClass(groupData.styles.animations)"
7 :style="{ 4 :style="{
8 - ...getComponentAttrStyle(item.attr, groupIndex),  
9 - ...getFilterStyle(item.styles),  
10 - ...getTransformStyle(item.styles),  
11 - ...getStatusStyle(item.status),  
12 - ...getPreviewConfigStyle(item.preview),  
13 - ...getBlendModeStyle(item.styles) as any 5 + ...getSizeStyle(groupData.attr),
  6 + ...getFilterStyle(groupData.styles),
14 }" 7 }"
15 > 8 >
  9 + <div
  10 + class="chart-item"
  11 + v-for="item in groupData.groupList"
  12 + :class="animationsClass(item.styles.animations)"
  13 + :key="item.id"
  14 + :style="{
  15 + ...getComponentAttrStyle(item.attr, groupIndex),
  16 + ...getStatusStyle(item.status),
  17 + ...getPreviewConfigStyle(item.preview),
  18 + ...getBlendModeStyle(item.styles) as any
  19 + }"
  20 + >
16 <component 21 <component
17 :is="item.chartConfig.chartKey" 22 :is="item.chartConfig.chartKey"
18 :id="item.id" 23 :id="item.id"
19 :chartConfig="item" 24 :chartConfig="item"
20 :themeSetting="themeSetting" 25 :themeSetting="themeSetting"
21 :themeColor="themeColor" 26 :themeColor="themeColor"
22 - :style="{ ...getSizeStyle(item.attr) }"  
23 - v-on="useLifeHandler(item)"  
24 - ></component> 27 + :style="{
  28 + ...getSizeStyle(item.attr),
  29 + ...getFilterStyle(item.styles),
  30 + ...getTransformStyle(item.styles)
  31 + }"
  32 + v-on="useLifeHandler(item)"
  33 + ></component>
  34 + </div>
25 </div> 35 </div>
26 </template> 36 </template>
27 37
@@ -6,7 +6,6 @@ @@ -6,7 +6,6 @@
6 :key="item.id" 6 :key="item.id"
7 :style="{ 7 :style="{
8 ...getComponentAttrStyle(item.attr, index), 8 ...getComponentAttrStyle(item.attr, index),
9 - ...getFilterStyle(item.styles),  
10 ...getTransformStyle(item.styles), 9 ...getTransformStyle(item.styles),
11 ...getStatusStyle(item.status), 10 ...getStatusStyle(item.status),
12 ...getPreviewConfigStyle(item.preview), 11 ...getPreviewConfigStyle(item.preview),
@@ -31,7 +30,10 @@ @@ -31,7 +30,10 @@
31 :chartConfig="item" 30 :chartConfig="item"
32 :themeSetting="themeSetting" 31 :themeSetting="themeSetting"
33 :themeColor="themeColor" 32 :themeColor="themeColor"
34 - :style="{ ...getSizeStyle(item.attr) }" 33 + :style="{
  34 + ...getSizeStyle(item.attr),
  35 + ...getFilterStyle(item.styles)
  36 + }"
35 v-on="useLifeHandler(item)" 37 v-on="useLifeHandler(item)"
36 ></component> 38 ></component>
37 </div> 39 </div>
@@ -33,7 +33,7 @@ import { getFilterStyle, setTitle } from '@/utils' @@ -33,7 +33,7 @@ import { getFilterStyle, setTitle } from '@/utils'
33 33
34 // THINGS_KIT 重写预览逻辑,调用接口 34 // THINGS_KIT 重写预览逻辑,调用接口
35 import { dragCanvas } from './utils' 35 import { dragCanvas } from './utils'
36 -import { getEditCanvasConfigStyle,keyRecordHandle } from './utils' 36 +import { getEditCanvasConfigStyle, keyRecordHandle } from './utils'
37 import { getSessionStorageInfo } from './external/usePreview' 37 import { getSessionStorageInfo } from './external/usePreview'
38 38
39 import { useComInstall } from './hooks/useComInstall.hook' 39 import { useComInstall } from './hooks/useComInstall.hook'
@@ -52,6 +52,7 @@ setTitle(`预览-${chartEditStore.editCanvasConfig.projectName}`) @@ -52,6 +52,7 @@ setTitle(`预览-${chartEditStore.editCanvasConfig.projectName}`)
52 52
53 const previewRefStyle = computed(() => { 53 const previewRefStyle = computed(() => {
54 return { 54 return {
  55 + overflow: 'hidden',
55 ...getEditCanvasConfigStyle(chartEditStore.editCanvasConfig), 56 ...getEditCanvasConfigStyle(chartEditStore.editCanvasConfig),
56 ...getFilterStyle(chartEditStore.editCanvasConfig) 57 ...getFilterStyle(chartEditStore.editCanvasConfig)
57 } 58 }
@@ -15,7 +15,7 @@ let key = ref(Date.now()) @@ -15,7 +15,7 @@ let key = ref(Date.now())
15 15
16 // 数据变更 -> 组件销毁重建 16 // 数据变更 -> 组件销毁重建
17 ;[SavePageEnum.JSON, SavePageEnum.CHART_TO_PREVIEW].forEach((saveEvent: string) => { 17 ;[SavePageEnum.JSON, SavePageEnum.CHART_TO_PREVIEW].forEach((saveEvent: string) => {
18 - if (!window.opener) return 18 + if (!window.opener && !window.opener.addEventListener) return
19 window.opener.addEventListener(saveEvent, async (e: any) => { 19 window.opener.addEventListener(saveEvent, async (e: any) => {
20 const localStorageInfo: ChartEditStorageType = await getSessionStorageInfo() as unknown as ChartEditStorageType 20 const localStorageInfo: ChartEditStorageType = await getSessionStorageInfo() as unknown as ChartEditStorageType
21 setSessionStorage(StorageEnum.GO_CHART_STORAGE_LIST, [{ ...e.detail, id: localStorageInfo.id }]) 21 setSessionStorage(StorageEnum.GO_CHART_STORAGE_LIST, [{ ...e.detail, id: localStorageInfo.id }])