feat(图表): 地图支持自定义区域

This commit is contained in:
wisonic 2024-11-27 11:58:58 +08:00
parent a6847abeb5
commit bc4092ce9a
25 changed files with 1358 additions and 119 deletions

View File

@ -0,0 +1,52 @@
package io.dataease.map.dao.auto.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
/**
* <p>
* 自定义地理区域
* </p>
*
* @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 +
"}";
}
}

View File

@ -0,0 +1,80 @@
package io.dataease.map.dao.auto.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
/**
* <p>
* 自定义地理区域分区详情
* </p>
*
* @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 +
"}";
}
}

View File

@ -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;
/**
* <p>
* 自定义地理区域 Mapper 接口
* </p>
*
* @author fit2cloud
* @since 2024-11-22
*/
@Mapper
public interface CoreCustomGeoAreaMapper extends BaseMapper<CoreCustomGeoArea> {
}

View File

@ -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;
/**
* <p>
* 自定义地理区域分区详情 Mapper 接口
* </p>
*
* @author fit2cloud
* @since 2024-11-22
*/
@Mapper
public interface CoreCustomGeoSubAreaMapper extends BaseMapper<CoreCustomGeoSubArea> {
}

View File

@ -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<CustomGeoArea> listCustomGeoArea() {
return coreCustomGeoAreaMapper.selectList(null).stream().map(o -> BeanUtils.copyBean(new CustomGeoArea(), o)).toList();
}
public List<CustomGeoSubArea> getCustomGeoArea(String areaId) {
var query = new QueryWrapper<CoreCustomGeoSubArea>();
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<CoreCustomGeoSubArea>();
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<AreaNode> getCustomGeoSubAreaOptions() {
var q = new QueryWrapper<Area>();
q.eq("pid", "156");
return areaMapper.selectList(q).stream().map(a -> BeanUtils.copyBean(AreaNode.builder().build(), a)).toList();
}
public void childTreeIdList(List<String> pidList, List<String> resultList) {
QueryWrapper<CoreAreaCustom> queryWrapper = new QueryWrapper<>();
queryWrapper.in("pid", pidList);

View File

@ -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<CustomGeoArea> listCustomGeoArea() {
return mapManage.listCustomGeoArea();
}
@Override
public List<CustomGeoSubArea> 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<AreaNode> getCustomGeoSubAreaOptions() {
return mapManage.getCustomGeoSubAreaOptions();
}
}

View File

@ -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';
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 '自定义地理区域分区详情';

View File

@ -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';
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 '自定义地理区域分区详情';

View File

@ -90,6 +90,10 @@
<key-type>java.lang.String</key-type>
<value-type>java.lang.Object</value-type>
</cache>
<cache alias="de_v2_custom_geo" uses-template="common-cache">
<key-type>java.lang.String</key-type>
<value-type>java.util.List</value-type>
</cache>
<cache alias="de_v2_user_token_cache">
<key-type>java.lang.String</key-type>

View File

@ -23,3 +23,31 @@ const isCustomGeo = (id: string) => {
const getBusiGeoCode = (id: string) => {
return id.substring(4)
}
export const listCustomGeoArea = (): Promise<IResponse<CustomGeoArea[]>> => {
return request.get({ url: '/customGeo/geoArea/list' })
}
export const getCustomGeoArea = (id: string): Promise<IResponse<CustomGeoSubArea[]>> => {
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<IResponse<AreaNode[]>> => {
return request.get({ url: '/customGeo/geoSubArea/options' })
}

View File

@ -333,7 +333,7 @@ declare interface ChartBasicStyle {
/**
* 最大行数
*/
maxLines?: boolean
maxLines?: number
}
/**
* 表头属性

View File

@ -5,3 +5,13 @@ interface AreaNode {
pid: string
children: AreaNode[]
}
interface CustomGeoArea {
id: string
name: string
}
type CustomGeoSubArea = CustomGeoArea & {
geoAreaId: string
scope: string
}

View File

@ -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

View File

@ -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)
}

View File

@ -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<ChoroplethOptions, Choropleth> {
}
async drawChart(drawOption: L7PlotDrawOptions<Choropleth>): Promise<Choropleth> {
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<ChoroplethOptions, Choropleth> {
} else {
data = sourceData
}
const geoJson = cloneDeep(await getGeoJsonFile(areaId))
let options: ChoroplethOptions = {
preserveDrawingBuffer: true,
map: {
@ -157,7 +199,7 @@ export class Map extends L7PlotChartView<ChoroplethOptions, Choropleth> {
// 禁用线上地图数据
customFetchGeoData: () => null
}
const context = { drawOption, geoJson }
const context: Record<string, any> = { 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<ChoroplethOptions, Choropleth> {
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<ChoroplethOptions, Choropleth> {
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<ChoroplethOptions, Choropleth> {
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<string, any>
context: Record<string, any>
): 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<ChoroplethOptions, Choropleth> {
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<ChoroplethOptions, Choropleth> {
})
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<ChoroplethOptions, Choropleth> {
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<ChoroplethOptions, Choropleth> {
return options
}
protected configCustomArea(
chart: Chart,
options: ChoroplethOptions,
context: Record<string, any>
): 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<string, SeriesFormatter>
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<ChoroplethOptions, Choropleth> {
this.configStyle,
this.configTooltip,
this.configBasicStyle,
this.customConfigLegend
)(chart, options, context)
this.customConfigLegend,
this.configCustomArea
)(chart, options, context, this)
}
}

View File

@ -21,6 +21,7 @@ export interface L7PlotDrawOptions<P> extends AntVDrawOptions<P> {
areaId?: string
level?: ViewLevel['level']
geoJson?: FeatureCollection
scope?: string[]
}
// S2 or others to be defined next
export abstract class L7PlotChartView<

View File

@ -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<HTMLElement>(null)
let scope
let mapTimer: number
const renderL7Plot = async (chart: ChartObj, chartView: L7PlotChartView<any, any>, callback) => {
const map = parseJson(chart.customAttr).map
@ -293,7 +299,8 @@ const renderL7Plot = async (chart: ChartObj, chartView: L7PlotChartView<any, any
container: containerId,
chart,
areaId,
action
action,
scope
})
callback?.()
emit('resetLoading')

View File

@ -3,11 +3,6 @@
<el-aside class="geonetry-aside">
<div class="geo-title">
<span>{{ t('online_map.geometry') }}</span>
<span class="add-icon-span" @click="add()">
<el-icon>
<Icon name="icon_add_outlined"><icon_add_outlined class="svg-icon" /></Icon>
</el-icon>
</span>
</div>
<div class="geo-search">
<el-input
@ -46,85 +41,252 @@
:title="data.name"
v-html="data.colorName && keyword ? data.colorName : data.name"
/>
<span class="geo-operate-container">
<span v-if="data.id === '000'" class="add-icon-span" @click.stop="add()">
<el-icon>
<Icon name="icon_add_outlined"><icon_add_outlined class="svg-icon" /></Icon>
</el-icon>
</span>
<span class="geo-operate-container" v-if="data.custom">
<el-tooltip
v-if="data.custom"
class="box-item"
effect="dark"
:content="t('common.delete')"
placement="top"
>
<el-icon @click.stop="delHandler(data)" class="hover-icon">
<Icon name="icon_delete-trash_outlined"
><icon_deleteTrash_outlined class="svg-icon"
/></Icon>
<Icon name="icon_delete-trash_outlined">
<icon_deleteTrash_outlined class="svg-icon" />
</Icon>
</el-icon>
</el-tooltip>
</span>
</span>
</template>
</el-tree>
<el-tree
menu
ref="customAreaTreeRef"
node-key="id"
:data="customTreeData"
:highlight-current="true"
:expand-on-click-node="false"
:default-expand-all="false"
:filter-node-method="filterResourceNode"
@node-click="loadCustomSubArea"
>
<template #default="{ data }">
<span class="custom-area-root">
<span class="label" :title="data.name">
{{ data.name }}
</span>
<span class="opt-icon" v-if="data.id === '000'" @click.stop="editCustomArea()">
<el-icon>
<Icon name="icon_add_outlined"><icon_add_outlined /></Icon>
</el-icon>
</span>
<el-dropdown placement="bottom-end" popper-class="area-opt-popper" v-else>
<span class="opt-icon">
<el-icon>
<Icon name="icon_more_outlined"><icon_more_outlined /></Icon>
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="editCustomArea(data)">重命名</el-dropdown-item>
<el-dropdown-item @click.stop="deleteCustomArea(data)">删除</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</span>
</template>
<template #empty> 空的列表 </template>
</el-tree>
</el-scrollbar>
</div>
</el-aside>
<el-main class="geometry-main">
<div class="geo-content-container" v-if="!selectedData">
<EmptyBackground img-type="noneWhite" :description="t('system.on_the_left')" />
</div>
<div v-else class="geo-content-container">
<div class="geo-content-top">
<span>{{ selectedData.name }}</span>
<template v-if="showGeoJson">
<div class="geo-content-container" v-if="!selectedData">
<EmptyBackground img-type="noneWhite" :description="t('system.on_the_left')" />
</div>
<div class="geo-content-middle">
<div class="geo-area">
<div v-else class="geo-content-container">
<div class="geo-content-top">
<span>{{ selectedData.name }}</span>
</div>
<div class="geo-content-middle">
<div class="geo-area">
<div class="area-label">
<span>{{ t('system.region_code') }}</span>
</div>
<div class="area-content">
<span>{{ selectedData.id }}</span>
</div>
</div>
<div class="geo-area">
<div class="area-label">
<span>{{ t('system.superior_region') }}</span>
</div>
<div class="area-content">
<span>{{ selectedData.parentName || '-' }}</span>
<span v-if="selectedData.pid" class="area-secondary">{{
'(' + selectedData.pid + ')'
}}</span>
</div>
</div>
</div>
<div class="geo-content-bottom">
<div class="area-label">
<span>{{ t('system.region_code') }}</span>
</div>
<div class="area-content">
<span>{{ selectedData.id }}</span>
</div>
</div>
<div class="geo-area">
<div class="area-label">
<span>{{ t('system.superior_region') }}</span>
</div>
<div class="area-content">
<span>{{ selectedData.parentName || '-' }}</span>
<span v-if="selectedData.pid" class="area-secondary">{{
'(' + selectedData.pid + ')'
}}</span>
<span>{{ t('system.coordinate_file') }}</span>
</div>
<el-scrollbar class="area-content-geo">
<span>{{ selectedData.geoJson }}</span>
</el-scrollbar>
</div>
</div>
<div class="geo-content-bottom">
<div class="area-label">
<span>{{ t('system.coordinate_file') }}</span>
</div>
<el-scrollbar class="area-content-geo">
<span>{{ selectedData.geoJson }}</span>
</el-scrollbar>
</template>
<template v-else>
<div v-if="showCustomEmpty">
<EmptyBackground img-type="noneWhite" :description="t('system.on_the_left')" />
</div>
</div>
<div class="sub-area-view" v-else>
<div id="map-container" class="map-container"></div>
<el-divider />
<div class="sub-area-editor">
<span class="header">
<span class="label">
<span>自定义区域</span>
<span>(仅对中国的省份直辖市支持自定义地理区域)</span>
</span>
<span class="add-btn" @click="editCustomSubArea">
<el-icon>
<Icon name="icon_add_outlined"><icon_add_outlined /></Icon>
</el-icon>
<span>添加区域</span>
</span>
</span>
<el-table :data="subAreaList" stripe style="width: 100%">
<el-table-column prop="name" label="区域名称">
<template #default="{ row, $index }">
<span
class="area-color-symbol"
:style="{ backgroundColor: AREA_COLOR[$index % AREA_COLOR.length] }"
></span>
<span>{{ row.name }}</span>
</template>
</el-table-column>
<el-table-column prop="scopeName" label="区域范围" />
<el-table-column fixed="right" label="操作" min-width="120">
<template #default="{ row }">
<div class="area-edit-btn">
<span @click="editCustomSubArea(row)">
<el-icon>
<Icon name="icon_edit_outlined"><icon_edit_outlined /></Icon>
</el-icon>
</span>
<span @click="deleteCustomSubArea(row)">
<el-icon>
<Icon name="icon_delete-trash_outlined"><icon_deleteTrash_outlined /></Icon>
</el-icon>
</span>
</div>
</template>
</el-table-column>
</el-table>
</div>
</div>
</template>
</el-main>
</el-container>
<geometry-edit ref="editor" @saved="loadTreeData(false)" />
<el-dialog
v-model="customAreaDialog"
:title="`${editedCustomArea.id ? '编辑' : '新建'}自定义地理区域`"
width="500"
>
<el-form
ref="areaFormRef"
:model="editedCustomArea"
label-position="top"
label-width="auto"
:rules="areaRules"
>
<el-form-item label-position="top" required prop="name">
<el-input v-model="editedCustomArea.name" :minlenegth="1" :maxlength="50" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="customAreaDialog = false">取消</el-button>
<el-button type="primary" @click="saveGeoArea()"> 确定 </el-button>
</div>
</template>
</el-dialog>
<el-dialog
v-model="customSubAreaDialog"
:title="`${customSubArea.id ? '编辑' : '新建'}自定义区域`"
width="500"
>
<el-form
ref="subAreaFormRef"
:model="customSubArea"
label-position="top"
label-width="auto"
:rules="areaRules"
>
<el-form-item label="区域名称" label-position="top" required prop="name">
<el-input v-model="customSubArea.name" :minlenegth="1" :maxlength="50" />
</el-form-item>
<el-form-item label="请选择省份或直辖市" label-position="top" required prop="scopeArr">
<el-select v-model="customSubArea.scopeArr" multiple style="width: 100%" filterable>
<el-option
v-for="item in subAreaOptions"
:key="item.id"
:value="item.id"
:label="item.name"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="customSubAreaDialog = false">取消</el-button>
<el-button type="primary" @click="saveGeoSubArea()"> 确定 </el-button>
</div>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import icon_add_outlined from '@/assets/svg/icon_add_outlined.svg'
import icon_more_outlined from '@/assets/svg/icon_more_outlined.svg'
import icon_edit_outlined from '@/assets/svg/icon_edit_outlined.svg'
import icon_searchOutline_outlined from '@/assets/svg/icon_search-outline_outlined.svg'
import icon_deleteTrash_outlined from '@/assets/svg/icon_delete-trash_outlined.svg'
import { ref } from 'vue'
import { onBeforeMount, reactive, ref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { getWorldTree } from '@/api/map'
import {
getWorldTree,
listCustomGeoArea,
saveCustomGeoArea,
saveCustomGeoSubArea,
listSubAreaOptions,
deleteCustomGeoArea,
getCustomGeoArea,
deleteCustomGeoSubArea
} from '@/api/map'
import EmptyBackground from '@/components/empty-background/src/EmptyBackground.vue'
import { getGeoJsonFile } from '@/views/chart/components/js/util'
import { cloneDeep } from 'lodash-es'
import { cloneDeep, debounce } from 'lodash-es'
import { setColorName } from '@/utils/utils'
import GeometryEdit from './GeometryEdit.vue'
import { useCache } from '@/hooks/web/useCache'
import { ElMessage, ElMessageBox } from 'element-plus-secondary'
import { ElMessage, ElMessageBox, FormRules } from 'element-plus-secondary'
import request from '@/config/axios'
import { Choropleth } from '@antv/l7plot/dist/esm/plots/choropleth'
import { ChoroplethOptions, TextLayer } from '@antv/l7plot/dist/esm'
import { nextTick } from 'vue'
import { centroid } from '@turf/centroid'
import { FeatureCollection } from '@antv/l7plot/dist/esm/plots/choropleth/types'
const { wsCache } = useCache()
const { t } = useI18n()
const keyword = ref('')
@ -137,9 +299,14 @@ interface Tree {
const areaTreeRef = ref(null)
const loading = ref(false)
const selectedData = ref(null)
const showGeoJson = ref(true)
const handleNodeClick = async (data: Tree) => {
selectedData.value = data
customAreaTreeRef.value?.setCurrentKey(null)
mapInstance?.destroy()
mapInstance = null
curCustomGeoArea.id = ''
const geoJson = cloneDeep(await getGeoJsonFile(data['id']))
selectedData.value['geoJson'] = JSON.stringify(geoJson)
const pid = data['pid']
@ -149,6 +316,7 @@ const handleNodeClick = async (data: Tree) => {
selectedData.value.parentName = parent.data.name
}
}
showGeoJson.value = true
}
const delHandler = data => {
ElMessageBox.confirm(t('system.delete_this_node'), {
@ -205,6 +373,326 @@ const add = (pid?: string) => {
}
loadTreeData(true)
// geoArea
const customTreeData = ref([
{
id: '000',
name: '自定义地理区域'
}
])
const customAreaDialog = ref(false)
const curCustomGeoArea: CustomGeoArea = reactive({
id: '',
name: ''
})
const customAreaTreeRef = ref()
const areaFormRef = ref()
const saveGeoArea = async () => {
areaFormRef.value?.validate(async valid => {
if (valid) {
await saveCustomGeoArea(editedCustomArea)
await loadCustomGeoArea()
customAreaDialog.value = false
}
})
}
const loadCustomGeoArea = async () => {
await listCustomGeoArea().then(res => {
if (res.data?.length) {
customTreeData.value[0].children = res.data
if (curCustomGeoArea.id) {
nextTick(() => {
customAreaTreeRef.value?.setCurrentKey(curCustomGeoArea.id)
})
}
} else {
customTreeData.value[0].children = null
}
})
}
const editedCustomArea = reactive({
id: '',
name: ''
})
const editCustomArea = (data?) => {
if (data) {
editedCustomArea.id = data.id
editedCustomArea.name = data.name
} else {
editedCustomArea.id = ''
editedCustomArea.name = ''
}
customAreaDialog.value = true
}
const deleteCustomArea = data => {
ElMessageBox.confirm(
'该操作会导致使用了自定义区域的地图无法正常展示,确定删除?',
`删除[${data.name}]`,
{
type: 'warning',
confirmButtonType: 'danger',
customClass: 'area-delete-dialog'
}
)
.then(async () => {
await deleteCustomGeoArea(data.id)
await loadCustomGeoArea()
if (!customTreeData.value[0].children?.length || data.id === curCustomGeoArea.id) {
showCustomEmpty.value = true
mapInstance?.destroy()
mapInstance = null
} else {
showCustomEmpty.value = false
}
})
.catch(() => {
//
})
}
let areaNameMap: Record<string, string> = null
const loadSubAreaOptions = () => {
listSubAreaOptions().then(res => {
subAreaOptions.value.splice(0, subAreaOptions.value.length, ...res.data)
if (!areaNameMap) {
areaNameMap = subAreaOptions.value.reduce((p, n) => {
p[n.id] = n.name
return p
}, {})
}
})
}
const showCustomEmpty = ref(false)
const loadCustomSubArea = async (node, reload?) => {
areaTreeRef.value?.setCurrentKey(null)
if (node.id === '000' || (curCustomGeoArea.id === node.id && reload !== true)) {
return
}
showCustomEmpty.value = false
curCustomGeoArea.id = node.id
getCustomGeoArea(node.id).then(res => {
const tmpList = res.data.reduce((p, n) => {
const ids = n.scope?.split(',') || []
n.scopeArr = ids
const nameArr = []
ids.forEach(id => {
nameArr.push(areaNameMap?.[id])
})
const area = {
...n,
scopeName: nameArr.join(',')
}
p.push(area)
return p
}, [])
subAreaList.value.splice(0, subAreaList.value.length, ...tmpList)
showGeoJson.value = false
nextTick(() => {
debounceRender()
})
})
}
// geoSubArea
const customSubAreaDialog = ref(false)
const customSubArea = reactive({
id: '',
name: '',
scope: '',
geoAreaId: '',
scopeArr: []
})
const subAreaOptions = ref([])
const subAreaList = ref([])
const subAreaFormRef = ref()
const areaRules = reactive<FormRules>({
name: [
{ required: true, message: '请输入名称', trigger: 'blur' },
{ min: 1, max: 50, message: '名称长度为 1~50 格字符', trigger: 'blur' }
],
scopeArr: [{ type: 'array', required: true, message: '请选择区域', trigger: 'change' }]
})
const editCustomSubArea = (subArea?) => {
customSubArea.geoAreaId = curCustomGeoArea.id
if (!subArea) {
customSubArea.name = ''
customSubArea.scopeArr = []
customSubArea.id = ''
customSubArea.scope = ''
} else {
customSubArea.name = subArea.name
customSubArea.scopeArr = subArea.scopeArr
customSubArea.id = subArea.id
customSubArea.scope = subArea.scope
}
customSubAreaDialog.value = true
}
const saveGeoSubArea = async () => {
subAreaFormRef.value?.validate(async valid => {
if (!valid) {
return
}
customSubArea.scope = customSubArea.scopeArr.join(',')
await saveCustomGeoSubArea(customSubArea)
await loadCustomSubArea({ id: curCustomGeoArea.id }, true)
customSubAreaDialog.value = false
})
}
const deleteCustomSubArea = async data => {
ElMessageBox.confirm('确定删除该自定义区域?', `删除[${data.name}]`, {
type: 'warning',
confirmButtonType: 'danger',
customClass: 'area-delete-dialog'
})
.then(async () => {
await deleteCustomGeoSubArea(data.id)
await loadCustomSubArea({ id: curCustomGeoArea.id }, true)
})
.catch(() => {
//
})
}
loadCustomGeoArea()
loadSubAreaOptions()
const AREA_COLOR = [
'#1E90FF',
'#90EE90',
'#00CED1',
'#E2BD84',
'#7A90E0',
'#3BA272',
'#2BE7FF',
'#0A8ADA',
'#FFD700'
]
const mapOption: ChoroplethOptions = {
map: {
type: 'mapbox',
style: 'blank'
},
geoArea: {
type: 'geojson'
},
source: {
data: [],
joinBy: {
sourceField: 'name',
geoField: 'name'
}
},
viewLevel: {
adcode: 'all',
level: 'world'
},
autoFit: true,
chinaBorder: false,
style: {
stroke: 'grey',
opacity: 1,
lineWidth: 0.6,
lineOpacity: 1
},
label: {
field: 'name',
style: {
fill: 'black',
textAnchor: 'center'
}
},
state: {
active: { stroke: 'green', lineWidth: 1 }
},
legend: false,
tooltip: false,
// 线
customFetchGeoData: () => null
}
let mapInstance: Choropleth = null
const renderMap = async () => {
if (!mapOption.source.joinBy.geoData) {
const chinaGeoJson = cloneDeep(await getGeoJsonFile('156'))
mapOption.source.joinBy.geoData = chinaGeoJson
}
const areaMap = mapOption.source.joinBy.geoData.features.reduce((p, n) => {
if (n.properties['adcode']) {
p['156' + n.properties['adcode']] = n
}
return p
}, {})
const areaTextLocation = []
subAreaList.value?.forEach(area => {
const areaJsonArr = []
area.scopeArr?.forEach(adcode => {
const json = areaMap[adcode]
json && areaJsonArr.push(json)
})
if (areaJsonArr.length) {
const areaJson: FeatureCollection = {
type: 'FeatureCollection',
features: areaJsonArr
}
const center = centroid(areaJson)
areaTextLocation.push({
name: area.name,
x: center.geometry.coordinates[0],
y: center.geometry.coordinates[1]
})
}
})
const areaTextLayer = new TextLayer({
name: 'areaTextLayer',
source: {
data: areaTextLocation,
parser: {
type: 'json',
x: 'x',
y: 'y'
}
},
field: 'name',
style: {
fill: 'black',
fontSize: 20,
opacity: 1,
fontWeight: 'bold',
textAnchor: 'center'
}
})
if (mapInstance) {
const layer = mapInstance.getLayerByName('areaTextLayer')
if (layer) {
mapInstance.removeLayer(layer)
}
mapInstance.addLayer(areaTextLayer)
mapInstance.update({})
} else {
mapOption.color = {
field: ['name', 'adcode'],
value: area => {
let color = 'white'
subAreaList.value?.forEach((subArea, i) => {
if (subArea.scope?.includes(area.adcode)) {
color = AREA_COLOR[i % AREA_COLOR.length]
}
})
return color
},
scale: {
type: 'quantize',
unknown: 'white'
}
}
mapInstance = new Choropleth('map-container', mapOption)
mapInstance.on('loaded', () => {
mapInstance.addLayer(areaTextLayer)
})
}
}
const debounceRender = debounce(renderMap, 500)
onBeforeMount(() => {
mapInstance?.destroy()
})
</script>
<style lang="less" scoped>
@ -225,19 +713,6 @@ loadTreeData(true)
font-weight: 500;
line-height: 24px;
}
.add-icon-span {
color: var(--ed-color-primary);
height: 20px;
width: 20px;
i {
left: 2px;
}
&:hover {
background: #1f232926;
cursor: pointer;
}
border-radius: 2px;
}
margin-bottom: 16px;
}
.geo-search {
@ -314,7 +789,6 @@ loadTreeData(true)
flex: 1;
display: flex;
align-items: center;
box-sizing: content-box;
padding-right: 4px;
overflow: hidden;
justify-content: space-between;
@ -333,5 +807,119 @@ loadTreeData(true)
padding-left: 4px;
}
}
.add-icon-span {
color: var(--ed-color-primary);
padding: 3px;
border-radius: 3px;
line-height: 1;
&:hover {
background: #1f232926;
cursor: pointer;
}
}
}
.custom-area-root {
display: flex;
flex: 1;
justify-content: space-between;
align-items: center;
align-content: center;
padding-right: 4px;
.label {
max-width: 150px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.opt-icon {
color: var(--ed-color-primary);
padding: 3px;
border-radius: 3px;
line-height: 1;
&:hover {
background: #1f232926;
cursor: pointer;
}
}
}
.sub-area-view {
display: flex;
flex-direction: column;
width: 100;
height: 100%;
.map-container {
flex: 7;
}
.ed-divider {
margin: 10px 0;
}
.sub-area-editor {
flex: 3;
.header {
display: flex;
justify-content: space-between;
align-items: center;
.label {
:first-child {
font-size: 18px;
font-weight: bold;
margin-right: 10px;
}
:last-child {
font-size: 16px;
}
}
.add-btn {
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
color: var(--ed-color-primary);
padding: 3px;
:first-child {
margin-right: 2px;
}
&:hover {
border-radius: 6px;
background: #1f232926;
}
}
}
.area-color-symbol {
width: 10px;
height: 10px;
display: inline-block;
border-radius: 5px;
margin-right: 4px;
}
}
.area-edit-btn {
color: var(--ed-color-primary);
span {
padding: 3px;
cursor: pointer;
&:hover {
border-radius: 6px;
background: #1f232926;
}
}
}
}
</style>
<style lang="less">
.area-opt-popper {
margin-right: -20px !important;
}
.area-delete-dialog {
.ed-message-box__header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
.ed-message-box__headerbtn {
position: static;
}
}
}
</style>

View File

@ -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<CustomGeoArea> listCustomGeoArea();
@Operation(summary = "查询自定义地理区域详情")
@GetMapping("/geoArea/{id}")
List<CustomGeoSubArea> 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<AreaNode> getCustomGeoSubAreaOptions();
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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<String> origins) {

View File

@ -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";
}

View File

@ -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/")