Merge pull request #12797 from dataease/pr@dev-v2@feat_online_map_custom_style

feat(图表): 在线地图支持自定义风格 #10408
This commit is contained in:
wisonic-s 2024-10-21 15:06:43 +08:00 committed by GitHub
commit 060ca6ad33
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 148 additions and 67 deletions

View File

@ -1,6 +1,7 @@
package io.dataease.system.manage; package io.dataease.system.manage;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.dataease.api.system.request.OnlineMapEditor;
import io.dataease.api.system.vo.SettingItemVO; import io.dataease.api.system.vo.SettingItemVO;
import io.dataease.datasource.server.DatasourceServer; import io.dataease.datasource.server.DatasourceServer;
import io.dataease.license.config.XpackInteract; import io.dataease.license.config.XpackInteract;
@ -13,11 +14,13 @@ import io.dataease.utils.IDUtils;
import io.dataease.utils.SystemSettingUtils; import io.dataease.utils.SystemSettingUtils;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import java.beans.PropertyDescriptor;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -30,7 +33,7 @@ public class SysParameterManage {
@Value("${dataease.demo-tips-content:#{null}}") @Value("${dataease.demo-tips-content:#{null}}")
private String demoTipsContent; private String demoTipsContent;
private static final String mapKey = "map.key"; private static final String MAP_KEY_PREFIX = "map.";
@Resource @Resource
private CoreSysSettingMapper coreSysSettingMapper; private CoreSysSettingMapper coreSysSettingMapper;
@ -50,26 +53,39 @@ public class SysParameterManage {
return null; return null;
} }
public String queryOnlineMap() { public OnlineMapEditor queryOnlineMap() {
return singleVal(mapKey); var editor = new OnlineMapEditor();
List<String> fields = BeanUtils.getFieldNames(OnlineMapEditor.class);
Map<String, String> mapVal = groupVal(MAP_KEY_PREFIX);
fields.forEach(field -> {
String val = mapVal.get(MAP_KEY_PREFIX + field);
if (StringUtils.isNotBlank(val)) {
BeanUtils.setFieldValueByName(editor, field, val, String.class);
}
});
return editor;
} }
public void saveOnlineMap(String val) { public void saveOnlineMap(OnlineMapEditor editor) {
List<String> fieldNames = BeanUtils.getFieldNames(OnlineMapEditor.class);
QueryWrapper<CoreSysSetting> queryWrapper = new QueryWrapper<>(); fieldNames.forEach(field -> {
queryWrapper.eq("pkey", mapKey); QueryWrapper<CoreSysSetting> queryWrapper = new QueryWrapper<>();
CoreSysSetting sysSetting = coreSysSettingMapper.selectOne(queryWrapper); queryWrapper.eq("pkey", MAP_KEY_PREFIX + field);
if (ObjectUtils.isEmpty(sysSetting)) { CoreSysSetting sysSetting = coreSysSettingMapper.selectOne(queryWrapper);
sysSetting = new CoreSysSetting(); var val = (String) BeanUtils.getFieldValueByName(field, editor);
sysSetting.setId(IDUtils.snowID()); if (ObjectUtils.isEmpty(sysSetting)) {
sysSetting.setPkey(mapKey); sysSetting = new CoreSysSetting();
sysSetting.setId(IDUtils.snowID());
sysSetting.setPkey(MAP_KEY_PREFIX + field);
sysSetting.setPval(val == null ? "" : val);
sysSetting.setType("text");
sysSetting.setSort(1);
coreSysSettingMapper.insert(sysSetting);
return;
}
sysSetting.setPval(val); sysSetting.setPval(val);
sysSetting.setType("text"); coreSysSettingMapper.updateById(sysSetting);
sysSetting.setSort(1); });
coreSysSettingMapper.insert(sysSetting);
}
sysSetting.setPval(val);
coreSysSettingMapper.updateById(sysSetting);
} }
@ -81,7 +97,7 @@ public class SysParameterManage {
if (!CollectionUtils.isEmpty(sysSettings)) { if (!CollectionUtils.isEmpty(sysSettings)) {
return sysSettings.stream().collect(Collectors.toMap(CoreSysSetting::getPkey, CoreSysSetting::getPval)); return sysSettings.stream().collect(Collectors.toMap(CoreSysSetting::getPkey, CoreSysSetting::getPval));
} }
return null; return new HashMap<>();
} }
public List<CoreSysSetting> groupList(String groupKey) { public List<CoreSysSetting> groupList(String groupKey) {

View File

@ -27,13 +27,12 @@ public class SysParameterServer implements SysParameterApi {
@Override @Override
public void saveOnlineMap(OnlineMapEditor editor) { public void saveOnlineMap(OnlineMapEditor editor) {
sysParameterManage.saveOnlineMap(editor.getKey()); sysParameterManage.saveOnlineMap(editor);
} }
@Override @Override
public String queryOnlineMap() { public OnlineMapEditor queryOnlineMap() {
String key = sysParameterManage.queryOnlineMap(); return sysParameterManage.queryOnlineMap();
return StringUtils.isNotBlank(key) ? key : "";
} }
@Override @Override

View File

@ -1451,20 +1451,17 @@ export default {
start_point: '起点经纬度', start_point: '起点经纬度',
end_point: '终点经纬度', end_point: '终点经纬度',
line: '线条', line: '线条',
map_style: '风格', map_style: '地图风格',
map_style_url: '地图风格 URL',
map_pitch: '倾角', map_pitch: '倾角',
map_rotation: '旋转', map_rotation: '旋转',
map_style_normal: '标准', map_style_normal: '标准',
map_style_light: '明亮', map_style_light: '明亮',
map_style_dark: '暗黑', map_style_dark: '暗黑',
map_style_whitesmoke: '远山黛',
map_style_fresh: '草色青', map_style_fresh: '草色青',
map_style_grey: '雅士灰', map_style_grey: '雅士灰',
map_style_graffiti: '涂鸦',
map_style_macaron: '马卡龙',
map_style_blue: '靛青蓝', map_style_blue: '靛青蓝',
map_style_darkblue: '极夜蓝', map_style_darkblue: '极夜蓝',
map_style_wine: '酱籽',
map_line_type: '类型', map_line_type: '类型',
type: '类型', type: '类型',
map_line_width: '线条宽度', map_line_width: '线条宽度',
@ -1542,7 +1539,8 @@ export default {
map_symbol_hexagram: '菱形', map_symbol_hexagram: '菱形',
tip: '提示', tip: '提示',
hide: '隐藏', hide: '隐藏',
show_label: '显示标签' show_label: '显示标签',
security_code: '安全密钥'
}, },
dataset: { dataset: {
scope_edit: '仅编辑时生效', scope_edit: '仅编辑时生效',

View File

@ -205,6 +205,10 @@ declare interface ChartBasicStyle {
* 地图主题风格 * 地图主题风格
*/ */
mapStyle: string mapStyle: string
/**
* 自定义地图风格url
*/
mapStyleUrl: string
heatMapType?: string heatMapType?: string
heatMapIntensity?: number heatMapIntensity?: number
heatMapRadius?: number heatMapRadius?: number

View File

@ -3,12 +3,18 @@ import { store } from '@/store'
import { FeatureCollection } from '@antv/l7plot/dist/esm/plots/choropleth/types' import { FeatureCollection } from '@antv/l7plot/dist/esm/plots/choropleth/types'
interface MapStore { interface MapStore {
mapCache: Record<string, FeatureCollection> mapCache: Record<string, FeatureCollection>
mapKey: string mapKey: {
key: string
securityCode: string
}
} }
export const useMapStore = defineStore('map', { export const useMapStore = defineStore('map', {
state: (): MapStore => ({ state: (): MapStore => ({
mapCache: {}, mapCache: {},
mapKey: '' mapKey: {
key: '',
securityCode: ''
}
}), }),
actions: { actions: {
setMap({ id, geoJson }) { setMap({ id, geoJson }) {

View File

@ -213,13 +213,10 @@ const mapStyleOptions = [
{ name: t('chart.map_style_darkblue'), value: 'darkblue' }, { name: t('chart.map_style_darkblue'), value: 'darkblue' },
{ name: t('chart.map_style_light'), value: 'light' }, { name: t('chart.map_style_light'), value: 'light' },
{ name: t('chart.map_style_dark'), value: 'dark' }, { name: t('chart.map_style_dark'), value: 'dark' },
{ name: t('chart.map_style_whitesmoke'), value: 'whitesmoke' },
{ name: t('chart.map_style_fresh'), value: 'fresh' }, { name: t('chart.map_style_fresh'), value: 'fresh' },
{ name: t('chart.map_style_grey'), value: 'grey' }, { name: t('chart.map_style_grey'), value: 'grey' },
{ name: t('chart.map_style_graffiti'), value: 'graffiti' },
{ name: t('chart.map_style_macaron'), value: 'macaron' },
{ name: t('chart.map_style_blue'), value: 'blue' }, { name: t('chart.map_style_blue'), value: 'blue' },
{ name: t('chart.map_style_wine'), value: 'wine' } { name: t('commons.custom'), value: 'custom' }
] ]
const heatMapTypeOptions = [ const heatMapTypeOptions = [
{ name: t('chart.heatmap_classics'), value: 'heatmap' }, { name: t('chart.heatmap_classics'), value: 'heatmap' },
@ -374,7 +371,7 @@ onMounted(() => {
<el-row style="flex: 1"> <el-row style="flex: 1">
<el-col> <el-col>
<el-form-item <el-form-item
:label="t('chart.chart_map') + t('chart.map_style')" :label="t('chart.map_style')"
class="form-item" class="form-item"
:class="'form-item-' + themes" :class="'form-item-' + themes"
> >
@ -393,6 +390,21 @@ onMounted(() => {
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
<el-row style="flex: 1" v-if="state.basicStyleForm.mapStyle === 'custom'">
<el-col>
<el-form-item
:label="t('chart.map_style_url')"
class="form-item"
:class="'form-item-' + themes"
>
<el-input
:effect="themes"
v-model="state.basicStyleForm.mapStyleUrl"
@change="changeBasicStyle('mapStyleUrl')"
/>
</el-form-item>
</el-col>
</el-row>
<div class="alpha-setting"> <div class="alpha-setting">
<label class="alpha-label" :class="{ dark: 'dark' === themes }"> <label class="alpha-label" :class="{ dark: 'dark' === themes }">
{{ t('chart.chart_map') + t('chart.map_pitch') }} {{ t('chart.chart_map') + t('chart.map_pitch') }}

View File

@ -1586,7 +1586,8 @@ export const DEFAULT_BASIC_STYLE: ChartBasicStyle = {
layout: 'horizontal', layout: 'horizontal',
mapSymbolSizeMin: 4, mapSymbolSizeMin: 4,
mapSymbolSizeMax: 30, mapSymbolSizeMax: 30,
showLabel: true showLabel: true,
mapStyleUrl: ''
} }
export const BASE_VIEW_CONFIG = { export const BASE_VIEW_CONFIG = {

View File

@ -74,14 +74,17 @@ export class FlowMap extends L7ChartView<Scene, L7Config> {
const xAxisExt = deepCopy(chart.xAxisExt) const xAxisExt = deepCopy(chart.xAxisExt)
const { basicStyle, misc } = deepCopy(parseJson(chart.customAttr)) const { basicStyle, misc } = deepCopy(parseJson(chart.customAttr))
const mapStyle = `amap://styles/${basicStyle.mapStyle ? basicStyle.mapStyle : 'normal'}` let mapStyle = basicStyle.mapStyleUrl
const key = await this.getMapKey() if (basicStyle.mapStyle !== 'custom') {
mapStyle = `amap://styles/${basicStyle.mapStyle ? basicStyle.mapStyle : 'normal'}`
}
const mapKey = await this.getMapKey()
// 底层 // 底层
const scene = new Scene({ const scene = new Scene({
id: container, id: container,
logoVisible: false, logoVisible: false,
map: new GaodeMap({ map: new GaodeMap({
token: key ?? undefined, token: mapKey?.key ?? undefined,
style: mapStyle, style: mapStyle,
pitch: misc.mapPitch, pitch: misc.mapPitch,
zoom: 2.5, zoom: 2.5,

View File

@ -56,14 +56,17 @@ export class HeatMap extends L7ChartView<Scene, L7Config> {
basicStyle = parseJson(chart.customAttr).basicStyle basicStyle = parseJson(chart.customAttr).basicStyle
miscStyle = parseJson(chart.customAttr).misc miscStyle = parseJson(chart.customAttr).misc
} }
const mapStyle = `amap://styles/${basicStyle.mapStyle ? basicStyle.mapStyle : 'normal'}` let mapStyle = basicStyle.mapStyleUrl
const key = await this.getMapKey() if (basicStyle.mapStyle !== 'custom') {
mapStyle = `amap://styles/${basicStyle.mapStyle ? basicStyle.mapStyle : 'normal'}`
}
const mapKey = await this.getMapKey()
// 底层 // 底层
const scene = new Scene({ const scene = new Scene({
id: container, id: container,
logoVisible: false, logoVisible: false,
map: new GaodeMap({ map: new GaodeMap({
token: key ?? undefined, token: mapKey?.key ?? undefined,
style: mapStyle, style: mapStyle,
pitch: miscStyle.mapPitch, pitch: miscStyle.mapPitch,
zoom: 2.5, zoom: 2.5,

View File

@ -85,14 +85,17 @@ export class SymbolicMap extends L7ChartView<Scene, L7Config> {
miscStyle = parseJson(chart.customAttr).misc miscStyle = parseJson(chart.customAttr).misc
} }
const mapStyle = `amap://styles/${basicStyle.mapStyle ? basicStyle.mapStyle : 'normal'}` let mapStyle = basicStyle.mapStyleUrl
const key = await this.getMapKey() if (basicStyle.mapStyle !== 'custom') {
mapStyle = `amap://styles/${basicStyle.mapStyle ? basicStyle.mapStyle : 'normal'}`
}
const mapKey = await this.getMapKey()
// 底层 // 底层
const scene = new Scene({ const scene = new Scene({
id: container, id: container,
logoVisible: false, logoVisible: false,
map: new GaodeMap({ map: new GaodeMap({
token: key ?? undefined, token: mapKey?.key ?? undefined,
style: mapStyle, style: mapStyle,
pitch: miscStyle.mapPitch, pitch: miscStyle.mapPitch,
center: [104.434765, 38.256735], center: [104.434765, 38.256735],

View File

@ -114,9 +114,14 @@ export abstract class L7ChartView<
} }
protected getMapKey = async () => { protected getMapKey = async () => {
if (!mapStore.mapKey) { if (!mapStore.mapKey.key) {
await queryMapKeyApi().then(res => mapStore.setKey(res.data)) await queryMapKeyApi().then(res => mapStore.setKey(res.data))
} }
if (mapStore.mapKey.securityCode) {
window._AMapSecurityConfig = {
securityJsCode: mapStore.mapKey.securityCode
}
}
return mapStore.mapKey return mapStore.mapKey
} }

View File

@ -4,19 +4,33 @@
<div class="geo-title"> <div class="geo-title">
<span>{{ t('online_map.onlinemap') }}</span> <span>{{ t('online_map.onlinemap') }}</span>
</div> </div>
<div class="online-form-item"> <el-row>
<div class="map-item"> <el-col>
<div class="map-item-label"> <div class="online-form-item">
<span class="form-label">Key</span> <div class="map-item">
<div class="map-item-label">
<span class="form-label">Key</span>
</div>
</div>
<div class="map-item">
<el-input v-model="mapEditor.key" />
</div>
<div class="map-item">
<div class="map-item-label">
<span class="form-label">{{ t('chart.security_code') }}</span>
</div>
</div>
<div class="map-item">
<el-input v-model="mapEditor.securityCode" />
</div>
</div> </div>
</div> </el-col>
<div class="map-item"> </el-row>
<el-input v-model="key" /> <el-row>
</div> <el-button type="primary" :disabled="!mapEditor.key" @click="saveHandler">
</div> {{ t('commons.save') }}
<el-button type="primary" :disabled="!key" @click="saveHandler">{{ </el-button>
t('commons.save') </el-row>
}}</el-button>
</el-aside> </el-aside>
<el-main> <el-main>
<div v-show="mapLoaded" v-if="!mapReloading" class="de-map-container" :id="domId" /> <div v-show="mapLoaded" v-if="!mapReloading" class="de-map-container" :id="domId" />
@ -30,23 +44,26 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { nextTick, onMounted, ref } from 'vue' import { nextTick, onMounted, reactive, ref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n' import { useI18n } from '@/hooks/web/useI18n'
import { queryMapKeyApi, saveMapKeyApi } from '@/api/setting/sysParameter' import { queryMapKeyApi, saveMapKeyApi } from '@/api/setting/sysParameter'
import { ElMessage } from 'element-plus-secondary' import { ElMessage } from 'element-plus-secondary'
import EmptyBackground from '@/components/empty-background/src/EmptyBackground.vue' import EmptyBackground from '@/components/empty-background/src/EmptyBackground.vue'
const { t } = useI18n() const { t } = useI18n()
const key = ref('') const mapEditor = reactive({
key: '',
securityCode: ''
})
const mapInstance = ref(null) const mapInstance = ref(null)
const mapReloading = ref(false) const mapReloading = ref(false)
const domId = ref('de-map-container') const domId = ref('de-map-container')
const mapLoaded = ref(false) const mapLoaded = ref(false)
const loadMap = () => { const loadMap = () => {
if (!key.value) { if (!mapEditor.key) {
return return
} }
const mykey = key.value const mykey = mapEditor.key
const url = `https://webapi.amap.com/maps?v=2.0&key=${mykey}` const url = `https://webapi.amap.com/maps?v=2.0&key=${mykey}`
loadScript(url) loadScript(url)
@ -84,7 +101,7 @@ const createMapInstance = () => {
mapLoaded.value = true mapLoaded.value = true
} }
const saveHandler = () => { const saveHandler = () => {
saveMapKeyApi({ key: key.value }) saveMapKeyApi(mapEditor)
.then(() => { .then(() => {
ElMessage.success(t('commons.save_success')) ElMessage.success(t('commons.save_success'))
initLoad() initLoad()
@ -96,7 +113,8 @@ const saveHandler = () => {
const initLoad = () => { const initLoad = () => {
queryMapKeyApi() queryMapKeyApi()
.then(res => { .then(res => {
key.value = res.data mapEditor.key = res.data.key
mapEditor.securityCode = res.data.securityCode
loadMap() loadMap()
}) })
.catch(e => { .catch(e => {
@ -154,7 +172,6 @@ onMounted(() => {
} }
} }
.online-form-item { .online-form-item {
height: 64px;
margin-bottom: 16px; margin-bottom: 16px;
.map-item { .map-item {
height: 32px; height: 32px;

View File

@ -30,7 +30,7 @@ public interface SysParameterApi {
@Operation(summary = "查询在线地图") @Operation(summary = "查询在线地图")
@GetMapping("/queryOnlineMap") @GetMapping("/queryOnlineMap")
String queryOnlineMap(); OnlineMapEditor queryOnlineMap();
@Operation(summary = "查询基础设置(非xpack)") @Operation(summary = "查询基础设置(非xpack)")
@GetMapping("basic/query") @GetMapping("basic/query")

View File

@ -10,4 +10,6 @@ import java.io.Serializable;
public class OnlineMapEditor implements Serializable { public class OnlineMapEditor implements Serializable {
@Schema(description = "在线地图key", requiredMode = Schema.RequiredMode.REQUIRED) @Schema(description = "在线地图key", requiredMode = Schema.RequiredMode.REQUIRED)
private String key; private String key;
@Schema(description = "在线地图安全密钥", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private String securityCode;
} }

View File

@ -2,7 +2,10 @@ package io.dataease.utils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import java.lang.reflect.Field;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
public class BeanUtils { public class BeanUtils {
@ -65,4 +68,13 @@ public class BeanUtils {
return null; return null;
} }
} }
public static List<String> getFieldNames(Class<?> clazz) {
List<String> fieldNames = new ArrayList<>();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
fieldNames.add(field.getName());
}
return fieldNames;
}
} }