From bc4092ce9a762cae01ef768c3665357a64d8d869 Mon Sep 17 00:00:00 2001 From: wisonic Date: Wed, 27 Nov 2024 11:58:58 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=9B=BE=E8=A1=A8):=20=E5=9C=B0=E5=9B=BE?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E8=87=AA=E5=AE=9A=E4=B9=89=E5=8C=BA=E5=9F=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dao/auto/entity/CoreCustomGeoArea.java | 52 ++ .../dao/auto/entity/CoreCustomGeoSubArea.java | 80 ++ .../auto/mapper/CoreCustomGeoAreaMapper.java | 18 + .../mapper/CoreCustomGeoSubAreaMapper.java | 18 + .../io/dataease/map/manage/MapManage.java | 74 +- .../dataease/map/server/CustomGeoServer.java | 55 ++ .../resources/db/desktop/V2.10.3__ddl.sql | 21 +- .../resources/db/migration/V2.10.3__ddl.sql | 22 +- .../src/main/resources/ehcache/ehcache.xml | 4 + core/core-frontend/src/api/map.ts | 28 + .../src/models/chart/chart-attr.d.ts | 2 +- core/core-frontend/src/models/chart/map.d.ts | 10 + .../editor-senior/components/MapMapping.vue | 3 + .../views/chart/components/editor/index.vue | 21 +- .../components/js/panel/charts/map/map.ts | 268 +++++-- .../components/js/panel/types/impl/l7plot.ts | 1 + .../views/components/ChartComponentG2Plot.vue | 15 +- .../views/system/parameter/map/Geometry.vue | 706 ++++++++++++++++-- .../io/dataease/api/map/CustomGeoApi.java | 44 ++ .../io/dataease/api/map/vo/CustomGeoArea.java | 13 + .../dataease/api/map/vo/CustomGeoSubArea.java | 16 + .../io/dataease/auth/filter/TokenFilter.java | 2 +- .../dataease/auth/interceptor/CorsConfig.java | 2 +- .../io/dataease/constant/CacheConstant.java | 1 + .../io/dataease/utils/WhitelistUtils.java | 1 + 25 files changed, 1358 insertions(+), 119 deletions(-) create mode 100644 core/core-backend/src/main/java/io/dataease/map/dao/auto/entity/CoreCustomGeoArea.java create mode 100644 core/core-backend/src/main/java/io/dataease/map/dao/auto/entity/CoreCustomGeoSubArea.java create mode 100644 core/core-backend/src/main/java/io/dataease/map/dao/auto/mapper/CoreCustomGeoAreaMapper.java create mode 100644 core/core-backend/src/main/java/io/dataease/map/dao/auto/mapper/CoreCustomGeoSubAreaMapper.java create mode 100644 core/core-backend/src/main/java/io/dataease/map/server/CustomGeoServer.java create mode 100644 sdk/api/api-base/src/main/java/io/dataease/api/map/CustomGeoApi.java create mode 100644 sdk/api/api-base/src/main/java/io/dataease/api/map/vo/CustomGeoArea.java create mode 100644 sdk/api/api-base/src/main/java/io/dataease/api/map/vo/CustomGeoSubArea.java diff --git a/core/core-backend/src/main/java/io/dataease/map/dao/auto/entity/CoreCustomGeoArea.java b/core/core-backend/src/main/java/io/dataease/map/dao/auto/entity/CoreCustomGeoArea.java new file mode 100644 index 0000000000..4a1b7097d0 --- /dev/null +++ b/core/core-backend/src/main/java/io/dataease/map/dao/auto/entity/CoreCustomGeoArea.java @@ -0,0 +1,52 @@ +package io.dataease.map.dao.auto.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import java.io.Serializable; + +/** + *

+ * 自定义地理区域 + *

+ * + * @author fit2cloud + * @since 2024-11-22 + */ +@TableName("core_custom_geo_area") +public class CoreCustomGeoArea implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * id + */ + private String id; + + /** + * 区域名称 + */ + private String name; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "CoreCustomGeoArea{" + + "id = " + id + + ", name = " + name + + "}"; + } +} diff --git a/core/core-backend/src/main/java/io/dataease/map/dao/auto/entity/CoreCustomGeoSubArea.java b/core/core-backend/src/main/java/io/dataease/map/dao/auto/entity/CoreCustomGeoSubArea.java new file mode 100644 index 0000000000..57015a6921 --- /dev/null +++ b/core/core-backend/src/main/java/io/dataease/map/dao/auto/entity/CoreCustomGeoSubArea.java @@ -0,0 +1,80 @@ +package io.dataease.map.dao.auto.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import java.io.Serializable; + +/** + *

+ * 自定义地理区域分区详情 + *

+ * + * @author fit2cloud + * @since 2024-11-22 + */ +@TableName("core_custom_geo_sub_area") +public class CoreCustomGeoSubArea implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * id + */ + private Long id; + + /** + * 名称 + */ + private String name; + + /** + * 区域范围 + */ + private String scope; + + /** + * 自定义地理区域id + */ + private String geoAreaId; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getScope() { + return scope; + } + + public void setScope(String scope) { + this.scope = scope; + } + + public String getGeoAreaId() { + return geoAreaId; + } + + public void setGeoAreaId(String geoAreaId) { + this.geoAreaId = geoAreaId; + } + + @Override + public String toString() { + return "CoreCustomGeoSubArea{" + + "id = " + id + + ", name = " + name + + ", scope = " + scope + + ", geoAreaId = " + geoAreaId + + "}"; + } +} diff --git a/core/core-backend/src/main/java/io/dataease/map/dao/auto/mapper/CoreCustomGeoAreaMapper.java b/core/core-backend/src/main/java/io/dataease/map/dao/auto/mapper/CoreCustomGeoAreaMapper.java new file mode 100644 index 0000000000..23986813e5 --- /dev/null +++ b/core/core-backend/src/main/java/io/dataease/map/dao/auto/mapper/CoreCustomGeoAreaMapper.java @@ -0,0 +1,18 @@ +package io.dataease.map.dao.auto.mapper; + +import io.dataease.map.dao.auto.entity.CoreCustomGeoArea; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** + *

+ * 自定义地理区域 Mapper 接口 + *

+ * + * @author fit2cloud + * @since 2024-11-22 + */ +@Mapper +public interface CoreCustomGeoAreaMapper extends BaseMapper { + +} diff --git a/core/core-backend/src/main/java/io/dataease/map/dao/auto/mapper/CoreCustomGeoSubAreaMapper.java b/core/core-backend/src/main/java/io/dataease/map/dao/auto/mapper/CoreCustomGeoSubAreaMapper.java new file mode 100644 index 0000000000..2281591c19 --- /dev/null +++ b/core/core-backend/src/main/java/io/dataease/map/dao/auto/mapper/CoreCustomGeoSubAreaMapper.java @@ -0,0 +1,18 @@ +package io.dataease.map.dao.auto.mapper; + +import io.dataease.map.dao.auto.entity.CoreCustomGeoSubArea; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** + *

+ * 自定义地理区域分区详情 Mapper 接口 + *

+ * + * @author fit2cloud + * @since 2024-11-22 + */ +@Mapper +public interface CoreCustomGeoSubAreaMapper extends BaseMapper { + +} diff --git a/core/core-backend/src/main/java/io/dataease/map/manage/MapManage.java b/core/core-backend/src/main/java/io/dataease/map/manage/MapManage.java index 2e6894c3d3..f51775769d 100644 --- a/core/core-backend/src/main/java/io/dataease/map/manage/MapManage.java +++ b/core/core-backend/src/main/java/io/dataease/map/manage/MapManage.java @@ -3,17 +3,20 @@ package io.dataease.map.manage; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import io.dataease.api.map.dto.GeometryNodeCreator; import io.dataease.api.map.vo.AreaNode; +import io.dataease.api.map.vo.CustomGeoArea; +import io.dataease.api.map.vo.CustomGeoSubArea; import io.dataease.constant.StaticResourceConstants; import io.dataease.exception.DEException; import io.dataease.map.bo.AreaBO; import io.dataease.map.dao.auto.entity.Area; +import io.dataease.map.dao.auto.entity.CoreCustomGeoArea; +import io.dataease.map.dao.auto.entity.CoreCustomGeoSubArea; import io.dataease.map.dao.auto.mapper.AreaMapper; +import io.dataease.map.dao.auto.mapper.CoreCustomGeoAreaMapper; +import io.dataease.map.dao.auto.mapper.CoreCustomGeoSubAreaMapper; import io.dataease.map.dao.ext.entity.CoreAreaCustom; import io.dataease.map.dao.ext.mapper.CoreAreaCustomMapper; -import io.dataease.utils.BeanUtils; -import io.dataease.utils.CommonBeanFactory; -import io.dataease.utils.FileUtils; -import io.dataease.utils.LogUtil; +import io.dataease.utils.*; import jakarta.annotation.Resource; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.ObjectUtils; @@ -32,6 +35,7 @@ import java.util.List; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; +import static io.dataease.constant.CacheConstant.CommonCacheConstant.CUSTOM_GEO_CACHE; import static io.dataease.constant.CacheConstant.CommonCacheConstant.WORLD_MAP_CACHE; @Component @@ -51,6 +55,12 @@ public class MapManage { @Resource private AreaMapper areaMapper; + @Resource + private CoreCustomGeoAreaMapper coreCustomGeoAreaMapper; + + @Resource + private CoreCustomGeoSubAreaMapper coreCustomGeoSubAreaMapper; + @Resource private CoreAreaCustomMapper coreAreaCustomMapper; @@ -175,6 +185,62 @@ public class MapManage { }); } + @Cacheable(value = CUSTOM_GEO_CACHE, key = "'custom_geo_area'") + public List listCustomGeoArea() { + return coreCustomGeoAreaMapper.selectList(null).stream().map(o -> BeanUtils.copyBean(new CustomGeoArea(), o)).toList(); + } + + public List getCustomGeoArea(String areaId) { + var query = new QueryWrapper(); + query.eq("geo_area_id", areaId); + return coreCustomGeoSubAreaMapper.selectList(query).stream().map(o -> BeanUtils.copyBean(new CustomGeoSubArea(), o)).toList(); + } + + @CacheEvict(cacheNames = CUSTOM_GEO_CACHE, key = "'custom_geo_area'") + @Transactional + public void deleteCustomGeoArea(String areaId) { + coreCustomGeoAreaMapper.deleteById(areaId); + var q = new QueryWrapper(); + q.eq("geo_area_id", areaId); + coreCustomGeoSubAreaMapper.delete(q); + } + + @CacheEvict(cacheNames = CUSTOM_GEO_CACHE, key = "'custom_geo_area'") + @Transactional + public void saveCustomGeoArea(CustomGeoArea geoArea) { + var coreCustomGeoArea = new CoreCustomGeoArea(); + BeanUtils.copyBean(coreCustomGeoArea, geoArea); + if (ObjectUtils.isEmpty(coreCustomGeoArea.getId())) { + coreCustomGeoArea.setId("custom_" + IDUtils.snowID()); + coreCustomGeoAreaMapper.insert(coreCustomGeoArea); + } else { + coreCustomGeoAreaMapper.updateById(coreCustomGeoArea); + } + } + + @Transactional + public void deleteCustomGeoSubArea(long areaId) { + coreCustomGeoSubAreaMapper.deleteById(areaId); + } + + @Transactional + public void saveCustomGeoSubArea(CustomGeoSubArea customGeoSubArea) { + var geoSubArea = new CoreCustomGeoSubArea(); + BeanUtils.copyBean(geoSubArea, customGeoSubArea); + if (ObjectUtils.isEmpty(geoSubArea.getId())) { + geoSubArea.setId(IDUtils.snowID()); + coreCustomGeoSubAreaMapper.insert(geoSubArea); + } else { + coreCustomGeoSubAreaMapper.updateById(geoSubArea); + } + } + + public List getCustomGeoSubAreaOptions() { + var q = new QueryWrapper(); + q.eq("pid", "156"); + return areaMapper.selectList(q).stream().map(a -> BeanUtils.copyBean(AreaNode.builder().build(), a)).toList(); + } + public void childTreeIdList(List pidList, List resultList) { QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.in("pid", pidList); diff --git a/core/core-backend/src/main/java/io/dataease/map/server/CustomGeoServer.java b/core/core-backend/src/main/java/io/dataease/map/server/CustomGeoServer.java new file mode 100644 index 0000000000..0c94c86660 --- /dev/null +++ b/core/core-backend/src/main/java/io/dataease/map/server/CustomGeoServer.java @@ -0,0 +1,55 @@ +package io.dataease.map.server; + +import io.dataease.api.map.CustomGeoApi; +import io.dataease.api.map.vo.AreaNode; +import io.dataease.api.map.vo.CustomGeoArea; +import io.dataease.api.map.vo.CustomGeoSubArea; +import io.dataease.map.manage.MapManage; +import jakarta.annotation.Resource; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/customGeo") +public class CustomGeoServer implements CustomGeoApi { + + @Resource + private MapManage mapManage; + + @Override + public List listCustomGeoArea() { + return mapManage.listCustomGeoArea(); + } + + @Override + public List getCustomGeoArea(String id) { + return mapManage.getCustomGeoArea(id); + } + + @Override + public void deleteCustomGeoArea(String id) { + mapManage.deleteCustomGeoArea(id); + } + + @Override + public void saveCustomGeoArea(CustomGeoArea geoArea) { + mapManage.saveCustomGeoArea(geoArea); + } + + @Override + public void deleteCustomGeoSubArea(long id) { + mapManage.deleteCustomGeoSubArea(id); + } + + @Override + public void saveCustomGeoSubArea(CustomGeoSubArea geoSubArea) { + mapManage.saveCustomGeoSubArea(geoSubArea); + } + + @Override + public List getCustomGeoSubAreaOptions() { + return mapManage.getCustomGeoSubAreaOptions(); + } +} diff --git a/core/core-backend/src/main/resources/db/desktop/V2.10.3__ddl.sql b/core/core-backend/src/main/resources/db/desktop/V2.10.3__ddl.sql index 2fe81c2a7f..17d7f58d60 100644 --- a/core/core-backend/src/main/resources/db/desktop/V2.10.3__ddl.sql +++ b/core/core-backend/src/main/resources/db/desktop/V2.10.3__ddl.sql @@ -28,4 +28,23 @@ UPDATE `visualization_background` SET `name` = 'Board5' WHERE `id` = 'board_5'; UPDATE `visualization_background` SET `name` = 'Board6' WHERE `id` = 'board_6'; UPDATE `visualization_background` SET `name` = 'Board7' WHERE `id` = 'board_7'; UPDATE `visualization_background` SET `name` = 'Board8' WHERE `id` = 'board_8'; -UPDATE `visualization_background` SET `name` = 'Board9' WHERE `id` = 'board_9'; \ No newline at end of file +UPDATE `visualization_background` SET `name` = 'Board9' WHERE `id` = 'board_9'; + +DROP TABLE IF EXISTS `core_custom_geo_area`; +CREATE TABLE core_custom_geo_area +( + id varchar(50) not null comment 'id' + primary key, + name varchar(50) null comment '区域名称' +) + comment '自定义地理区域'; +DROP TABLE IF EXISTS `core_custom_geo_sub_area`; +create table core_custom_geo_sub_area +( + id bigint not null comment 'id' + primary key, + name varchar(50) not null comment '名称', + scope varchar(1024) null comment '区域范围', + geo_area_id varchar(50) not null comment '自定义地理区域id' +) + comment '自定义地理区域分区详情'; \ No newline at end of file diff --git a/core/core-backend/src/main/resources/db/migration/V2.10.3__ddl.sql b/core/core-backend/src/main/resources/db/migration/V2.10.3__ddl.sql index 9b7a96b1f2..ddea0c7be1 100644 --- a/core/core-backend/src/main/resources/db/migration/V2.10.3__ddl.sql +++ b/core/core-backend/src/main/resources/db/migration/V2.10.3__ddl.sql @@ -31,4 +31,24 @@ UPDATE `visualization_background` SET `name` = 'Board5' WHERE `id` = 'board_5'; UPDATE `visualization_background` SET `name` = 'Board6' WHERE `id` = 'board_6'; UPDATE `visualization_background` SET `name` = 'Board7' WHERE `id` = 'board_7'; UPDATE `visualization_background` SET `name` = 'Board8' WHERE `id` = 'board_8'; -UPDATE `visualization_background` SET `name` = 'Board9' WHERE `id` = 'board_9'; \ No newline at end of file +UPDATE `visualization_background` SET `name` = 'Board9' WHERE `id` = 'board_9'; + +DROP TABLE IF EXISTS `core_custom_geo_area`; +CREATE TABLE core_custom_geo_area +( + id varchar(50) not null comment 'id' + primary key, + name varchar(50) null comment '区域名称' +) + comment '自定义地理区域'; +DROP TABLE IF EXISTS `core_custom_geo_sub_area`; +create table core_custom_geo_sub_area +( + id bigint not null comment 'id' + primary key, + name varchar(50) not null comment '名称', + scope varchar(1024) null comment '区域范围', + geo_area_id varchar(50) not null comment '自定义地理区域id' +) + comment '自定义地理区域分区详情'; + diff --git a/core/core-backend/src/main/resources/ehcache/ehcache.xml b/core/core-backend/src/main/resources/ehcache/ehcache.xml index fdaae43386..80802ab0b4 100644 --- a/core/core-backend/src/main/resources/ehcache/ehcache.xml +++ b/core/core-backend/src/main/resources/ehcache/ehcache.xml @@ -90,6 +90,10 @@ java.lang.String java.lang.Object + + java.lang.String + java.util.List + java.lang.String diff --git a/core/core-frontend/src/api/map.ts b/core/core-frontend/src/api/map.ts index 7023c28c49..8df1721926 100644 --- a/core/core-frontend/src/api/map.ts +++ b/core/core-frontend/src/api/map.ts @@ -23,3 +23,31 @@ const isCustomGeo = (id: string) => { const getBusiGeoCode = (id: string) => { return id.substring(4) } + +export const listCustomGeoArea = (): Promise> => { + return request.get({ url: '/customGeo/geoArea/list' }) +} + +export const getCustomGeoArea = (id: string): Promise> => { + return request.get({ url: `/customGeo/geoArea/${id}` }) +} + +export const deleteCustomGeoArea = (id: string) => { + return request.delete({ url: `/customGeo/geoArea/${id}` }) +} + +export const saveCustomGeoArea = (area: CustomGeoArea) => { + return request.post({ url: '/customGeo/geoArea/save', data: area }) +} + +export const deleteCustomGeoSubArea = (id: string) => { + return request.delete({ url: `/customGeo/geoSubArea/${id}` }) +} + +export const saveCustomGeoSubArea = (area: CustomGeoSubArea) => { + return request.post({ url: '/customGeo/geoSubArea/save', data: area }) +} + +export const listSubAreaOptions = (): Promise> => { + return request.get({ url: '/customGeo/geoSubArea/options' }) +} diff --git a/core/core-frontend/src/models/chart/chart-attr.d.ts b/core/core-frontend/src/models/chart/chart-attr.d.ts index bf788a7382..5057da5e45 100644 --- a/core/core-frontend/src/models/chart/chart-attr.d.ts +++ b/core/core-frontend/src/models/chart/chart-attr.d.ts @@ -333,7 +333,7 @@ declare interface ChartBasicStyle { /** * 最大行数 */ - maxLines?: boolean + maxLines?: number } /** * 表头属性 diff --git a/core/core-frontend/src/models/chart/map.d.ts b/core/core-frontend/src/models/chart/map.d.ts index e2fc4c2bfd..914269ba46 100644 --- a/core/core-frontend/src/models/chart/map.d.ts +++ b/core/core-frontend/src/models/chart/map.d.ts @@ -5,3 +5,13 @@ interface AreaNode { pid: string children: AreaNode[] } + +interface CustomGeoArea { + id: string + name: string +} + +type CustomGeoSubArea = CustomGeoArea & { + geoAreaId: string + scope: string +} diff --git a/core/core-frontend/src/views/chart/components/editor/editor-senior/components/MapMapping.vue b/core/core-frontend/src/views/chart/components/editor/editor-senior/components/MapMapping.vue index 98755de092..1ba14a552c 100644 --- a/core/core-frontend/src/views/chart/components/editor/editor-senior/components/MapMapping.vue +++ b/core/core-frontend/src/views/chart/components/editor/editor-senior/components/MapMapping.vue @@ -69,6 +69,9 @@ const getAreaMapping = async areaId => { if (!areaId) { return {} } + if (areaId.startsWith('custom_')) { + areaId = '156' + } const geoJson = await getGeoJsonFile(areaId) return geoJson.features.reduce((p, n) => { p[n.properties.name] = n.properties.name diff --git a/core/core-frontend/src/views/chart/components/editor/index.vue b/core/core-frontend/src/views/chart/components/editor/index.vue index ba28a655a8..8592f7cafd 100644 --- a/core/core-frontend/src/views/chart/components/editor/index.vue +++ b/core/core-frontend/src/views/chart/components/editor/index.vue @@ -55,7 +55,7 @@ import CalcFieldEdit from '@/views/visualized/data/dataset/form/CalcFieldEdit.vu import { getFieldName, guid } from '@/views/visualized/data/dataset/form/util' import { cloneDeep, forEach, get } from 'lodash-es' import { deleteField, saveField } from '@/api/dataset' -import { getWorldTree } from '@/api/map' +import { getWorldTree, listCustomGeoArea } from '@/api/map' import chartViewManager from '@/views/chart/components/js/panel' import DatasetSelect from '@/views/chart/components/editor/dataset-select/DatasetSelect.vue' import { useDraggable } from '@vueuse/core' @@ -293,8 +293,17 @@ watch( newVal => { if (showAxis('area')) { if (!state.worldTree?.length) { - getWorldTree().then(res => { - state.worldTree.splice(0, state.worldTree.length, res.data) + getWorldTree().then(async res => { + const customAreaList = (await listCustomGeoArea()).data + const customRoot = { + id: 'customRoot', + name: '自定义区域', + disabled: true + } + if (customAreaList.length) { + customRoot.children = customAreaList + } + state.worldTree.splice(0, state.worldTree.length, res.data, customRoot) state.areaId = view.value?.customAttr?.map?.id }) } else { @@ -311,7 +320,8 @@ watch( ) const treeProps = { label: 'name', - children: 'children' + children: 'children', + disabled: 'disabled' } const recordSnapshotInfo = type => { @@ -869,6 +879,9 @@ const renderChart = view => { } const onAreaChange = val => { + if (val.id === 'customRoot') { + return + } view.value.customAttr.map = { id: val.id, level: val.level } renderChart(view.value) } diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/map/map.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/map/map.ts index 3f27def1da..11264ff31c 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/charts/map/map.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/map/map.ts @@ -19,7 +19,7 @@ import { mapRendering } from '@/views/chart/components/js/panel/common/common_antv' import type { FeatureCollection } from '@antv/l7plot/dist/esm/plots/choropleth/types' -import { cloneDeep, defaultsDeep } from 'lodash-es' +import { cloneDeep, defaultsDeep, isEmpty } from 'lodash-es' import { useI18n } from '@/hooks/web/useI18n' import { valueFormatter } from '../../../formatter' import { @@ -37,6 +37,9 @@ import { } from '@antv/l7plot-component/dist/esm/legend/category/constants' import substitute from '@antv/util/esm/substitute' import { configCarouselTooltip } from '@/views/chart/components/js/panel/charts/map/tooltip-carousel' +import { getCustomGeoArea } from '@/api/map' +import { centroid } from '@turf/centroid' +import { TextLayer } from '@antv/l7plot/dist/esm' const { t } = useI18n() @@ -77,15 +80,55 @@ export class Map extends L7PlotChartView { } async drawChart(drawOption: L7PlotDrawOptions): Promise { - const { chart, level, areaId, container, action } = drawOption + const { chart, level, container, action, scope } = drawOption + const { areaId } = drawOption if (!areaId) { return } chart.container = container - const sourceData = JSON.parse(JSON.stringify(chart.data?.data || [])) - let data = [] + let sourceData = JSON.parse(JSON.stringify(chart.data?.data || [])) const { misc } = parseJson(chart.customAttr) const { legend } = parseJson(chart.customStyle) + let geoJson = {} as FeatureCollection + // 自定义区域,去除非区域数据,优先级最高 + let customSubArea: CustomGeoSubArea[] = [] + if (areaId.startsWith('custom_')) { + customSubArea = (await getCustomGeoArea(areaId)).data || [] + geoJson = cloneDeep(await getGeoJsonFile('156')) + const areaNameMap = geoJson.features.reduce((p, n) => { + p['156' + n.properties.adcode] = n.properties.name + return p + }, {}) + const areaMap = customSubArea.reduce((p, n) => { + p[n.name] = n + n.scopeArr = n.scope?.split(',') || [] + return p + }, {}) + const fakeData = [] + sourceData.forEach(d => { + const area = areaMap[d.name] + if (area) { + area.scopeArr.forEach(adcode => { + fakeData.push({ + ...d, + name: areaNameMap[adcode], + field: areaNameMap[adcode], + scope: area.scopeArr, + areaName: d.name + }) + }) + } + }) + sourceData = fakeData + } else { + if (scope) { + geoJson = cloneDeep(await getGeoJsonFile('156')) + geoJson.features = geoJson.features.filter(f => scope.includes('156' + f.properties.adcode)) + } else { + geoJson = cloneDeep(await getGeoJsonFile(areaId)) + } + } + let data = [] // 自定义图例 if (!misc.mapAutoLegend && legend.show) { let minValue = misc.mapLegendMin @@ -112,7 +155,6 @@ export class Map extends L7PlotChartView { } else { data = sourceData } - const geoJson = cloneDeep(await getGeoJsonFile(areaId)) let options: ChoroplethOptions = { preserveDrawingBuffer: true, map: { @@ -157,7 +199,7 @@ export class Map extends L7PlotChartView { // 禁用线上地图数据 customFetchGeoData: () => null } - const context = { drawOption, geoJson } + const context: Record = { drawOption, geoJson, customSubArea } options = this.setupOptions(chart, options, context) const { Choropleth } = await import('@antv/l7plot/dist/esm/plots/choropleth') const view = new Choropleth(container, options) @@ -165,15 +207,25 @@ export class Map extends L7PlotChartView { mapRendering(container) view.once('loaded', () => { mapRendered(container) + const { layers } = context + if (layers) { + layers.forEach(l => { + view.addLayer(l) + }) + } view.scene.map['keyboard'].disable() view.on('fillAreaLayer:click', (ev: MapMouseEvent) => { const data = ev.feature.properties + if (areaId.startsWith('custom_')) { + data.name = data.areaName + data.adcode = '156' + } action({ x: ev.x, y: ev.y, data: { data, - extra: { adcode: data.adcode } + extra: { adcode: data.adcode, scope: data.scope } } }) }) @@ -206,16 +258,15 @@ export class Map extends L7PlotChartView { options.label && (options.label.field = 'name') return options } - const sourceData = JSON.parse(JSON.stringify(chart.data.data)) + const sourceData = options.source.data const colors = basicStyle.colors.map(item => hexColorToRGBA(item, basicStyle.alpha)) const { legend } = parseJson(chart.customStyle) - let data = [] - data = sourceData + let data = sourceData let colorScale = [] let minValue = misc.mapLegendMin let maxValue = misc.mapLegendMax + let mapLegendNumber = misc.mapLegendNumber if (legend.show) { - let mapLegendNumber = misc.mapLegendNumber getMaxAndMinValueByData(sourceData, 'value', maxValue, minValue, (max, min) => { maxValue = max minValue = min @@ -262,10 +313,38 @@ export class Map extends L7PlotChartView { return options } + // 内部函数 创建自定义图例的内容 + private createLegendCustomContent = showItems => { + const containerDom = createDom(CONTAINER_TPL) as HTMLElement + const listDom = containerDom.getElementsByClassName(LIST_CLASS)[0] as HTMLElement + showItems.forEach(item => { + let value = '-' + if (item.value !== '') { + if (Array.isArray(item.value)) { + item.value.forEach((v, i) => { + item.value[i] = Number.isNaN(v) || v === 'NaN' ? 'NaN' : parseFloat(v).toFixed(0) + }) + value = item.value.join('-') + } else { + const tmp = item.value as string + value = Number.isNaN(tmp) || tmp === 'NaN' ? 'NaN' : parseFloat(tmp).toFixed(0) + } + } + const substituteObj = { ...item, value } + + const domStr = substitute(ITEM_TPL, substituteObj) + const itemDom = createDom(domStr) + // 给 legend 形状用的 + itemDom.style.setProperty('--bgColor', item.color) + listDom.appendChild(itemDom) + }) + return listDom + } + private customConfigLegend( chart: Chart, options: ChoroplethOptions, - _context: Record + context: Record ): ChoroplethOptions { const { basicStyle, misc } = parseJson(chart.customAttr) const colors = basicStyle.colors.map(item => hexColorToRGBA(item, basicStyle.alpha)) @@ -276,33 +355,6 @@ export class Map extends L7PlotChartView { if (!legend.show) { return options } - // 内部函数 创建自定义图例的内容 - const createLegendCustomContent = showItems => { - const containerDom = createDom(CONTAINER_TPL) as HTMLElement - const listDom = containerDom.getElementsByClassName(LIST_CLASS)[0] as HTMLElement - showItems.forEach(item => { - let value = '-' - if (item.value !== '') { - if (Array.isArray(item.value)) { - item.value.forEach((v, i) => { - item.value[i] = Number.isNaN(v) || v === 'NaN' ? 'NaN' : parseFloat(v).toFixed(0) - }) - value = item.value.join('-') - } else { - const tmp = item.value as string - value = Number.isNaN(tmp) || tmp === 'NaN' ? 'NaN' : parseFloat(tmp).toFixed(0) - } - } - const substituteObj = { ...item, value } - - const domStr = substitute(ITEM_TPL, substituteObj) - const itemDom = createDom(domStr) - // 给 legend 形状用的 - itemDom.style.setProperty('--bgColor', item.color) - listDom.appendChild(itemDom) - }) - return listDom - } const LEGEND_SHAPE_STYLE_MAP = { circle: { borderRadius: '50%' @@ -358,7 +410,7 @@ export class Map extends L7PlotChartView { }) customLegend['customContent'] = (_: string, _items: CategoryLegendListItem[]) => { if (items?.length) { - return createLegendCustomContent(items) + return this.createLegendCustomContent(items) } return '' } @@ -371,7 +423,7 @@ export class Map extends L7PlotChartView { customLegend['customContent'] = (_: string, items: CategoryLegendListItem[]) => { const showItems = items?.length > 30 ? items.slice(0, 30) : items if (showItems?.length) { - return createLegendCustomContent(showItems) + return this.createLegendCustomContent(showItems) } return '' } @@ -386,6 +438,135 @@ export class Map extends L7PlotChartView { return options } + protected configCustomArea( + chart: Chart, + options: ChoroplethOptions, + context: Record + ): ChoroplethOptions { + const { drawOption, customSubArea, geoJson } = context + if (!drawOption.areaId.startsWith('custom_')) { + return options + } + const customAttr = parseJson(chart.customAttr) + const { label } = customAttr + const data = chart.data.data + const areaMap = data.reduce((obj, value) => { + obj[value['field']] = value + return obj + }, {}) + //处理label + options.label = false + if (label.show) { + const geoJsonMap = geoJson.features.reduce((p, n) => { + if (n.properties['adcode']) { + p['156' + n.properties['adcode']] = n + } + return p + }, {}) + const labelLocation = [] + customSubArea.forEach(area => { + const areaJsonArr = [] + area.scopeArr?.forEach(adcode => { + const json = geoJsonMap[adcode] + json && areaJsonArr.push(json) + }) + if (areaJsonArr.length) { + const areaJson: FeatureCollection = { + type: 'FeatureCollection', + features: areaJsonArr + } + const content = [] + if (label.showDimension) { + content.push(area.name) + } + if (label.showQuota) { + areaMap[area.name] && + content.push(valueFormatter(areaMap[area.name].value, label.quotaLabelFormatter)) + } + const center = centroid(areaJson) + labelLocation.push({ + name: content.join('\n\n'), + x: center.geometry.coordinates[0], + y: center.geometry.coordinates[1] + }) + } + }) + const areaLabelLayer = new TextLayer({ + name: 'areaLabelLayer', + source: { + data: labelLocation, + parser: { + type: 'json', + x: 'x', + y: 'y' + } + }, + field: 'name', + style: { + fill: label.color, + fontSize: label.fontSize, + opacity: 1, + fontWeight: 'bold', + textAnchor: 'center', + textAllowOverlap: label.fullDisplay, + padding: !label.fullDisplay ? [2, 2] : undefined + } + }) + context.layers = [areaLabelLayer] + } + // 处理tooltip + const subAreaMap = customSubArea.reduce((p, n) => { + n.scopeArr.forEach(a => { + p[a] = n.name + }) + return p + }, {}) + if (options.tooltip && options.tooltip.showComponent) { + options.tooltip.items = ['name', 'adcode', 'value'] + options.tooltip.customTitle = ({ name, adcode }) => { + adcode = '156' + adcode + return subAreaMap[adcode] ?? name + } + const tooltip = customAttr.tooltip + const formatterMap = tooltip.seriesTooltipFormatter + ?.filter(i => i.show) + .reduce((pre, next) => { + pre[next.id] = next + return pre + }, {}) as Record + options.tooltip.customItems = originalItem => { + const result = [] + if (isEmpty(formatterMap)) { + return result + } + const head = originalItem.properties + const { adcode } = head + const areaName = subAreaMap['156' + adcode] + const valItem = areaMap[areaName] + if (!valItem) { + return result + } + const formatter = formatterMap[valItem.quotaList?.[0]?.id] + if (!isEmpty(formatter)) { + const originValue = parseFloat(valItem.value as string) + const value = valueFormatter(originValue, formatter.formatterCfg) + const name = isEmpty(formatter.chartShowName) ? formatter.name : formatter.chartShowName + result.push({ ...valItem, name, value: `${value ?? ''}` }) + } + valItem.dynamicTooltipValue?.forEach(item => { + const formatter = formatterMap[item.fieldId] + if (formatter) { + const value = valueFormatter(parseFloat(item.value), formatter.formatterCfg) + const name = isEmpty(formatter.chartShowName) ? formatter.name : formatter.chartShowName + result.push({ color: 'grey', name, value: `${value ?? ''}` }) + } + }) + return result + } + } + return options + } + setupDefaultOptions(chart: ChartObj): ChartObj { chart.customAttr.basicStyle.areaBaseColor = '#f4f4f4' return chart @@ -402,7 +583,8 @@ export class Map extends L7PlotChartView { this.configStyle, this.configTooltip, this.configBasicStyle, - this.customConfigLegend - )(chart, options, context) + this.customConfigLegend, + this.configCustomArea + )(chart, options, context, this) } } diff --git a/core/core-frontend/src/views/chart/components/js/panel/types/impl/l7plot.ts b/core/core-frontend/src/views/chart/components/js/panel/types/impl/l7plot.ts index cc1aba1702..ef6ca144dc 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/types/impl/l7plot.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/types/impl/l7plot.ts @@ -21,6 +21,7 @@ export interface L7PlotDrawOptions

extends AntVDrawOptions

{ areaId?: string level?: ViewLevel['level'] geoJson?: FeatureCollection + scope?: string[] } // S2 or others to be defined next export abstract class L7PlotChartView< diff --git a/core/core-frontend/src/views/chart/components/views/components/ChartComponentG2Plot.vue b/core/core-frontend/src/views/chart/components/views/components/ChartComponentG2Plot.vue index e8c60059f8..032def29d7 100644 --- a/core/core-frontend/src/views/chart/components/views/components/ChartComponentG2Plot.vue +++ b/core/core-frontend/src/views/chart/components/views/components/ChartComponentG2Plot.vue @@ -184,11 +184,16 @@ const calcData = async (view, callback) => { if (!res?.drillFilters?.length) { dynamicAreaId.value = '' } else { - dynamicAreaId.value = - view.chartExtRequest?.drill?.[res?.drillFilters?.length - 1].extra?.adcode + '' + const extra = view.chartExtRequest?.drill?.[res?.drillFilters?.length - 1].extra + dynamicAreaId.value = extra?.adcode + '' + scope = extra?.scope // 地图 if (!dynamicAreaId.value?.startsWith(country.value)) { - dynamicAreaId.value = country.value + dynamicAreaId.value + if (country.value === 'cus') { + dynamicAreaId.value = '156' + dynamicAreaId.value + } else { + dynamicAreaId.value = country.value + dynamicAreaId.value + } } } dvMainStore.setViewDataDetails(view.id, res) @@ -268,6 +273,7 @@ const dynamicAreaId = ref('') const country = ref('') const appStore = useAppStoreWithOut() const chartContainer = ref(null) +let scope let mapTimer: number const renderL7Plot = async (chart: ChartObj, chartView: L7PlotChartView, callback) => { const map = parseJson(chart.customAttr).map @@ -293,7 +299,8 @@ const renderL7Plot = async (chart: ChartObj, chartView: L7PlotChartView

{{ t('online_map.geometry') }} - - - - -
-
- -
-
-
- {{ selectedData.name }} + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sdk/api/api-base/src/main/java/io/dataease/api/map/CustomGeoApi.java b/sdk/api/api-base/src/main/java/io/dataease/api/map/CustomGeoApi.java new file mode 100644 index 0000000000..2911f975be --- /dev/null +++ b/sdk/api/api-base/src/main/java/io/dataease/api/map/CustomGeoApi.java @@ -0,0 +1,44 @@ +package io.dataease.api.map; + +import com.github.xiaoymin.knife4j.annotations.ApiSupport; +import io.dataease.api.map.vo.AreaNode; +import io.dataease.api.map.vo.CustomGeoArea; +import io.dataease.api.map.vo.CustomGeoSubArea; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Tag(name = "系统设置:自定义地理区域") +@ApiSupport(order = 799) +public interface CustomGeoApi { + + @Operation(summary = "查询自定义地理区域") + @GetMapping("/geoArea/list") + List listCustomGeoArea(); + + @Operation(summary = "查询自定义地理区域详情") + @GetMapping("/geoArea/{id}") + List getCustomGeoArea(@PathVariable("id") String id); + + @Operation(summary = "删除自定义地理区域") + @DeleteMapping("/geoArea/{id}") + void deleteCustomGeoArea(@PathVariable("id") String id); + + @Operation(summary = "保存自定义地理区域") + @PostMapping("/geoArea/save") + void saveCustomGeoArea(@RequestBody CustomGeoArea geoArea); + + @Operation(summary = "删除自定义地理子区域") + @DeleteMapping("/geoSubArea/{id}") + void deleteCustomGeoSubArea(@PathVariable("id") long id); + + @Operation(summary = "保存自定义地理子区域") + @PostMapping("/geoSubArea/save") + void saveCustomGeoSubArea(@RequestBody CustomGeoSubArea geoSubArea); + + @Operation(summary = "获取子区域下拉框可选列表") + @GetMapping("/geoSubArea/options") + List getCustomGeoSubAreaOptions(); +} diff --git a/sdk/api/api-base/src/main/java/io/dataease/api/map/vo/CustomGeoArea.java b/sdk/api/api-base/src/main/java/io/dataease/api/map/vo/CustomGeoArea.java new file mode 100644 index 0000000000..f9f6b6069c --- /dev/null +++ b/sdk/api/api-base/src/main/java/io/dataease/api/map/vo/CustomGeoArea.java @@ -0,0 +1,13 @@ +package io.dataease.api.map.vo; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import lombok.Data; + +import java.io.Serializable; + +@Data +public class CustomGeoArea implements Serializable { + private String id; + private String name; +} diff --git a/sdk/api/api-base/src/main/java/io/dataease/api/map/vo/CustomGeoSubArea.java b/sdk/api/api-base/src/main/java/io/dataease/api/map/vo/CustomGeoSubArea.java new file mode 100644 index 0000000000..77ee1c2637 --- /dev/null +++ b/sdk/api/api-base/src/main/java/io/dataease/api/map/vo/CustomGeoSubArea.java @@ -0,0 +1,16 @@ +package io.dataease.api.map.vo; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import lombok.Data; + +import java.io.Serializable; + +@Data +public class CustomGeoSubArea implements Serializable { + @JsonSerialize(using = ToStringSerializer.class) + private Long id; + private String name; + private String scope; + private String geoAreaId; +} diff --git a/sdk/common/src/main/java/io/dataease/auth/filter/TokenFilter.java b/sdk/common/src/main/java/io/dataease/auth/filter/TokenFilter.java index 7e27e50782..d07673351c 100644 --- a/sdk/common/src/main/java/io/dataease/auth/filter/TokenFilter.java +++ b/sdk/common/src/main/java/io/dataease/auth/filter/TokenFilter.java @@ -18,7 +18,7 @@ public class TokenFilter implements Filter { public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; String method = request.getMethod(); - if (!StringUtils.equalsAny(method, "GET", "POST", "OPTIONS")) { + if (!StringUtils.equalsAny(method, "GET", "POST", "OPTIONS", "DELETE")) { HttpServletResponse res = (HttpServletResponse) servletResponse; res.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED); return; diff --git a/sdk/common/src/main/java/io/dataease/auth/interceptor/CorsConfig.java b/sdk/common/src/main/java/io/dataease/auth/interceptor/CorsConfig.java index 336f54420b..f3eb9d01f7 100644 --- a/sdk/common/src/main/java/io/dataease/auth/interceptor/CorsConfig.java +++ b/sdk/common/src/main/java/io/dataease/auth/interceptor/CorsConfig.java @@ -33,7 +33,7 @@ public class CorsConfig implements WebMvcConfigurer { .allowedOrigins(originList.toArray(new String[0])) .allowedHeaders("*") .maxAge(3600) - .allowedMethods("GET", "POST"); + .allowedMethods("GET", "POST", "DELETE"); } public void addAllowedOrigins(List origins) { diff --git a/sdk/common/src/main/java/io/dataease/constant/CacheConstant.java b/sdk/common/src/main/java/io/dataease/constant/CacheConstant.java index 65e0cd8468..bbeeca217f 100644 --- a/sdk/common/src/main/java/io/dataease/constant/CacheConstant.java +++ b/sdk/common/src/main/java/io/dataease/constant/CacheConstant.java @@ -25,6 +25,7 @@ public class CacheConstant { public static class CommonCacheConstant { public static final String WORLD_MAP_CACHE = "de_v2_world_map"; + public static final String CUSTOM_GEO_CACHE = "de_v2_custom_geo"; public static final String RSA_CACHE = "de_v2_rsa"; public static final String PER_MENU_ID_CACHE = "de_v2_per_menu_id"; } diff --git a/sdk/common/src/main/java/io/dataease/utils/WhitelistUtils.java b/sdk/common/src/main/java/io/dataease/utils/WhitelistUtils.java index 55f75bd43e..7473f88ff8 100644 --- a/sdk/common/src/main/java/io/dataease/utils/WhitelistUtils.java +++ b/sdk/common/src/main/java/io/dataease/utils/WhitelistUtils.java @@ -68,6 +68,7 @@ public class WhitelistUtils { || StringUtils.startsWithAny(requestURI, "/xpackComponent/content") || StringUtils.startsWithAny(requestURI, "/xpackComponent/pluginStaticInfo") || StringUtils.startsWithAny(requestURI, "/geo/") + || StringUtils.startsWithAny(requestURI, "/customGeo/") || StringUtils.startsWithAny(requestURI, "/websocket") || StringUtils.startsWithAny(requestURI, "/map/") || StringUtils.startsWithAny(requestURI, "/oauth2/")