feat(图表): 符号地图大小配置项优化

This commit is contained in:
ulleo
2024-12-11 14:35:07 +08:00
parent d21cabd990
commit b5260ffd50
8 changed files with 362 additions and 232 deletions

View File

@@ -1867,6 +1867,13 @@ Scatter chart (bubble) chart: {a} (series name), {b} (data name), {c} (value arr
export_raw_details: 'Raw Details',
field_is_empty_export_error: 'No fields available, unable to export',
chart_symbolic_map: 'Symbolic map',
symbolic: 'Symbolic',
symbolic_shape: 'Symbolic Shape',
symbolic_upload_hint: 'Supports SVG, JPG, JPEG, PNG files within 1MB',
symbolic_range: 'Range',
symbolic_error_icon: 'Please select the correct icon file!',
symbolic_error_size: 'The file size cannot exceed 1MB!',
symbolic_error_range: 'The second range value must be greater than the first range value',
chart_stock_line: 'K line',
liquid_condition_style_tips: `Condition style settings, determine water wave chart interval colors, leave blank to disable thresholds, range (0-100), incremental levels<br>Example: input 30,70; this means: divided into 3 segments, namely [0,30], [30,70], [70,100]`,
conversion_rate: 'Conversion rate',

View File

@@ -1827,6 +1827,13 @@ export default {
export_raw_details: '導出原始明細',
field_is_empty_export_error: '目前無欄位,無法匯出',
chart_symbolic_map: '符號地圖',
symbolic: '符號',
symbolic_shape: '符號形狀',
symbolic_upload_hint: '支持 1MB 以內的 SVG, JPG, JPEG, PNG 文件',
symbolic_range: '區間',
symbolic_error_icon: '請選擇正確的圖標文件!',
symbolic_error_size: '文件大小不能超過 1MB!',
symbolic_error_range: '第二個區間值必須大于第一個區間值',
chart_stock_line: 'K 線圖',
liquid_condition_style_tips: `條件樣式設定,決定水波圖區間顏色,為空則不啟用閾值,範圍(0-100),逐級遞增<br/>例如:輸入 30,70表示分為3段分別為[0,30],[30,70],[70,100]`,
conversion_rate: '轉換率',

View File

@@ -1829,6 +1829,13 @@ export default {
export_raw_details: '导出原始明细',
field_is_empty_export_error: '当前无字段,无法导出',
chart_symbolic_map: '符号地图',
symbolic: '符号',
symbolic_shape: '符号形状',
symbolic_upload_hint: '支持 1MB 以内的 SVG, JPG, JPEG, PNG 文件',
symbolic_range: '区间',
symbolic_error_icon: '请选择正确的图标文件!',
symbolic_error_size: '文件大小不能超过 1MB!',
symbolic_error_range: '第二个区间值必须大于第一个区间值',
chart_stock_line: 'K 线图',
liquid_condition_style_tips: `条件样式设置,决定水波图颜色,为空则不开启阈值,范围(0-100),逐级递增<br/>例如:输入 30,70表示分为3段分别为[0,30],[30,70],[70,100]`,
conversion_rate: '转化率',

View File

@@ -2,6 +2,7 @@ declare type EditorProperty =
| 'background-overall-component'
| 'border-style'
| 'basic-style-selector'
| 'symbolic-style-selector'
| 'dual-basic-style-selector'
| 'label-selector'
| 'tooltip-selector'

View File

@@ -14,6 +14,7 @@ import { storeToRefs } from 'pinia'
import CollapseSwitchItem from '@/components/collapse-switch-item/src/CollapseSwitchItem.vue'
import { ElCollapse, ElCollapseItem } from 'element-plus-secondary'
import BasicStyleSelector from '@/views/chart/components/editor/editor-style/components/BasicStyleSelector.vue'
import SymbolicStyleSelector from '@/views/chart/components/editor/editor-style/components/SymbolicStyleSelector.vue'
import DualBasicStyleSelector from '@/views/chart/components/editor/editor-style/components/DualBasicStyleSelector.vue'
import ComponentPosition from '@/components/visualization/common/ComponentPosition.vue'
import BackgroundOverallCommon from '@/components/visualization/component-background/BackgroundOverallCommon.vue'
@@ -363,6 +364,22 @@ watch(
@onStyleAttrChange="onStyleAttrChange"
></common-border-setting>
</collapse-switch-item>
<el-collapse-item
:effect="themes"
name="symbolicStyle"
:title="t('chart.symbolic')"
v-if="showProperties('symbolic-style-selector')"
>
<SymbolicStyleSelector
:property-inner="propertyInnerAll['symbolic-style-selector']"
:themes="themes"
:chart="chart"
@onBasicStyleChange="onBasicStyleChange"
@onMiscChange="onMiscChange"
/>
</el-collapse-item>
<el-collapse-item
:effect="themes"
name="events"

View File

@@ -253,89 +253,6 @@ const heatMapTypeOptions = [
{ name: t('chart.heatmap3D'), value: 'heatmap3D' }
]
const mapSymbolOptions = [
{ name: t('chart.line_symbol_circle'), value: 'circle' },
{ name: t('chart.line_symbol_rect'), value: 'square' },
{ name: t('chart.line_symbol_triangle'), value: 'triangle' },
{ name: t('chart.map_symbol_pentagon'), value: 'pentagon' },
{ name: t('chart.map_symbol_hexagon'), value: 'hexagon' },
{ name: t('chart.map_symbol_octagon'), value: 'octogon' },
{ name: t('chart.line_symbol_diamond'), value: 'rhombus' },
{ name: t('commons.custom'), value: 'custom' }
]
const iconUpload = ref()
const acceptedFileType = ['image/svg+xml', 'image/jpeg', 'image/png']
const onIconChange: UploadProps['onChange'] = async uploadFile => {
const rawFile = uploadFile.raw
let validIcon = true
if (!acceptedFileType.includes(rawFile.type)) {
ElMessage.error('请选择正确的图标文件!')
validIcon = false
}
if (rawFile.size / 1024 / 1024 > 1) {
ElMessage.error('文件大小不能超过 1MB!')
validIcon = false
}
if (!validIcon) {
iconUpload.value?.clearFiles()
state.fileList.splice(0)
const customIcon = state.basicStyleForm.customIcon
if (customIcon) {
let file = ''
// 图片
if (customIcon.startsWith('data')) {
file = customIcon
} else {
// svg
file = svgStrToUrl(customIcon)
}
file && (state.fileList[0] = { url: file })
}
} else {
if (rawFile.type === 'image/svg+xml') {
state.basicStyleForm.customIcon = await rawFile.text()
changeBasicStyle('customIcon')
} else {
const fileReader = new FileReader()
fileReader.onloadend = () => {
state.basicStyleForm.customIcon = fileReader.result as string
changeBasicStyle('customIcon')
}
fileReader.readAsDataURL(rawFile)
}
}
}
const changeMapSymbol = () => {
const { mapSymbol, customIcon } = state.basicStyleForm
if (mapSymbol === 'custom' && customIcon) {
let file
if (customIcon.startsWith('data')) {
file = customIcon
} else {
file = svgStrToUrl(state.basicStyleForm.customIcon)
}
file && (state.fileList[0] = { url: file })
}
changeBasicStyle('mapSymbol')
}
const customSymbolicMapSizeRange = computed(() => {
let { extBubble } = JSON.parse(JSON.stringify(props.chart))
return ['symbolic-map'].includes(props.chart.type) && extBubble?.length > 0
})
const mapCustomRangeValidate = prop => {
const { mapSymbolSizeMax = '0', mapSymbolSizeMin = '1' } = state.basicStyleForm
let max = parseInt(mapSymbolSizeMax)
let min = parseInt(mapSymbolSizeMin)
state.basicStyleForm.mapSymbolSizeMin = Math.max(min, 0)
state.basicStyleForm.mapSymbolSizeMax = Math.max(max, 1)
if (max < min) {
ElMessage.warning('第二个区间值必须大于第一个区间值')
return
}
changeBasicStyle(prop)
}
/**
* 表格是否合并单元格
*/
@@ -571,154 +488,7 @@ onMounted(() => {
</el-col>
</el-row>
</div>
<div class="map-flow-style" v-if="showProperty('symbolicMapStyle')">
<el-row style="flex: 1">
<el-col>
<el-form-item class="form-item" :class="'form-item-' + themes">
<template #label>
<span class="data-area-label">
<span style="margin-right: 4px">{{ t('chart.symbolic_map_symbol_shape') }}</span>
<el-tooltip class="item" effect="dark" placement="bottom">
<template #content>
<div>{{ t('chart.symbolic_map_symbol_shape_tip') }}</div>
</template>
<el-icon class="hint-icon" :class="{ 'hint-icon--dark': themes === 'dark' }">
<Icon name="icon_info_outlined"><icon_info_outlined class="svg-icon" /></Icon>
</el-icon>
</el-tooltip>
</span>
</template>
<el-select
:effect="themes"
v-model="state.basicStyleForm.mapSymbol"
@change="changeMapSymbol()"
>
<el-option
v-for="item in mapSymbolOptions"
:key="item.name"
:label="item.name"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row style="flex: 1" v-if="state.basicStyleForm.mapSymbol === 'custom'">
<el-col>
<el-form-item class="form-item uploader" :class="'form-item-' + themes">
<div class="avatar-uploader-container" :class="`img-area_${themes}`">
<el-upload
action="#"
accept=".svg,.png,.jpeg,.jpg"
class="avatar-uploader"
list-type="picture-card"
ref="iconUpload"
:effect="themes"
:auto-upload="false"
:file-list="state.fileList"
:on-change="onIconChange"
:limit="1"
>
<el-icon><Plus /></el-icon>
</el-upload>
</div>
</el-form-item>
</el-col>
</el-row>
<div class="alpha-setting">
<label class="alpha-label" :class="{ dark: 'dark' === themes }">
{{ t('chart.size') }}
</label>
<el-row style="flex: 1">
<el-col>
<el-form-item class="form-item alpha-slider" :class="'form-item-' + themes">
<el-slider
:effect="themes"
:min="1"
:max="40"
v-model="state.basicStyleForm.mapSymbolSize"
@change="changeBasicStyle('mapSymbolSize')"
:disabled="customSymbolicMapSizeRange"
/>
</el-form-item>
</el-col>
</el-row>
</div>
<div class="alpha-setting">
<label class="alpha-label" :class="{ dark: 'dark' === themes }">
{{ t('chart.size_range') }}
</label>
<el-row style="flex: 1">
<el-col :span="11">
<el-form-item class="form-item alpha-slider" :class="'form-item-' + themes">
<el-input
type="number"
:effect="themes"
v-model="state.basicStyleForm.mapSymbolSizeMin"
class="basic-input-number"
:controls="false"
@blur="mapCustomRangeValidate('mapSymbolSizeMin')"
:disabled="!customSymbolicMapSizeRange"
>
</el-input>
</el-form-item>
</el-col>
<el-col :span="1.2">
<span>-</span>
</el-col>
<el-col :span="11">
<el-form-item class="form-item alpha-slider" :class="'form-item-' + themes">
<el-input
type="number"
:effect="themes"
v-model="state.basicStyleForm.mapSymbolSizeMax"
class="basic-input-number"
:controls="false"
@blur="mapCustomRangeValidate('mapSymbolSizeMax')"
:disabled="!customSymbolicMapSizeRange"
>
</el-input>
</el-form-item>
</el-col>
</el-row>
</div>
<div v-if="state.basicStyleForm.mapSymbol !== 'custom'" class="alpha-setting">
<label class="alpha-label" :class="{ dark: 'dark' === themes }">
{{ t('chart.not_alpha') }}
</label>
<el-row style="flex: 1">
<el-col>
<el-form-item class="form-item alpha-slider" :class="'form-item-' + themes">
<el-slider
:effect="themes"
:min="1"
:max="10"
v-model="state.basicStyleForm.mapSymbolOpacity"
@change="changeBasicStyle('mapSymbolOpacity')"
/>
</el-form-item>
</el-col>
</el-row>
</div>
<div v-if="state.basicStyleForm.mapSymbol !== 'custom'" class="alpha-setting">
<label class="alpha-label" :class="{ dark: 'dark' === themes }">
{{ t('visualization.borderWidth') }}
</label>
<el-row style="flex: 1">
<el-col>
<el-form-item class="form-item alpha-slider" :class="'form-item-' + themes">
<el-slider
:effect="themes"
:min="1"
:max="5"
v-model="state.basicStyleForm.mapSymbolStrokeWidth"
@change="changeBasicStyle('mapSymbolStrokeWidth')"
/>
</el-form-item>
</el-col>
</el-row>
</div>
</div>
<!--flow map end-->
<!--map start-->
<el-row :gutter="8">

View File

@@ -0,0 +1,320 @@
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
import { DEFAULT_BASIC_STYLE } from '@/views/chart/components/editor/util/chart'
import { ElMessage, UploadProps } from 'element-plus-secondary'
import { svgStrToUrl } from '@/views/chart/components/js/util'
import { useI18n } from '@/hooks/web/useI18n'
const props = withDefaults(
defineProps<{
chart: ChartObj
themes?: EditorTheme
propertyInner?: Array<string>
}>(),
{
themes: 'dark'
}
)
const { t } = useI18n()
const showProperty = prop => props.propertyInner?.includes(prop)
const emit = defineEmits(['onBasicStyleChange', 'onMiscChange'])
const state = reactive({
basicStyleForm: JSON.parse(JSON.stringify(DEFAULT_BASIC_STYLE)) as ChartBasicStyle,
customColor: null,
colorIndex: 0,
fieldColumnWidth: {
fieldId: '',
width: 0
},
fileList: []
})
const changeBasicStyle = (prop?: string, requestData = false) => {
emit('onBasicStyleChange', { data: state.basicStyleForm, requestData }, prop)
}
const iconUpload = ref()
const acceptedFileType = ['image/svg+xml', 'image/jpeg', 'image/png']
const mapSymbolOptions = [
{ name: t('chart.line_symbol_circle'), value: 'circle' },
{ name: t('chart.line_symbol_rect'), value: 'square' },
{ name: t('chart.line_symbol_triangle'), value: 'triangle' },
{ name: t('chart.map_symbol_pentagon'), value: 'pentagon' },
{ name: t('chart.map_symbol_hexagon'), value: 'hexagon' },
{ name: t('chart.map_symbol_octagon'), value: 'octogon' },
{ name: t('chart.line_symbol_diamond'), value: 'rhombus' },
{ name: t('commons.custom'), value: 'custom' }
]
const onIconChange: UploadProps['onChange'] = async uploadFile => {
const rawFile = uploadFile.raw
let validIcon = true
if (!acceptedFileType.includes(rawFile.type)) {
ElMessage.error(t('chart.symbolic_error_icon'))
validIcon = false
}
if (rawFile.size / 1024 / 1024 > 1) {
ElMessage.error(t('chart.symbolic_error_size'))
validIcon = false
}
if (!validIcon) {
iconUpload.value?.clearFiles()
state.fileList.splice(0)
const customIcon = state.basicStyleForm.customIcon
if (customIcon) {
let file = ''
// 图片
if (customIcon.startsWith('data')) {
file = customIcon
} else {
// svg
file = svgStrToUrl(customIcon)
}
file && (state.fileList[0] = { url: file })
}
} else {
if (rawFile.type === 'image/svg+xml') {
state.basicStyleForm.customIcon = await rawFile.text()
changeBasicStyle('customIcon')
} else {
const fileReader = new FileReader()
fileReader.onloadend = () => {
state.basicStyleForm.customIcon = fileReader.result as string
changeBasicStyle('customIcon')
}
fileReader.readAsDataURL(rawFile)
}
}
}
const changeMapSymbol = () => {
const { mapSymbol, customIcon } = state.basicStyleForm
if (mapSymbol === 'custom' && customIcon) {
let file
if (customIcon.startsWith('data')) {
file = customIcon
} else {
file = svgStrToUrl(state.basicStyleForm.customIcon)
}
file && (state.fileList[0] = { url: file })
}
changeBasicStyle('mapSymbol')
}
const customSymbolicMapSizeRange = computed(() => {
let { extBubble } = JSON.parse(JSON.stringify(props.chart))
return ['symbolic-map'].includes(props.chart.type) && extBubble?.length > 0
})
const mapCustomRangeValidate = prop => {
const { mapSymbolSizeMax = '0', mapSymbolSizeMin = '1' } = state.basicStyleForm
let max = parseInt(mapSymbolSizeMax)
let min = parseInt(mapSymbolSizeMin)
state.basicStyleForm.mapSymbolSizeMin = Math.max(min, 0)
state.basicStyleForm.mapSymbolSizeMax = Math.max(max, 1)
if (max < min) {
ElMessage.warning(t('chart.symbolic_error_range'))
return
}
changeBasicStyle(prop)
}
</script>
<template>
<div style="width: 100%">
<div class="map-flow-style" v-if="showProperty('symbolicMapStyle')">
<el-row style="flex: 1">
<el-col>
<el-form-item class="form-item" :class="'form-item-' + themes">
<template #label>
<span class="data-area-label">
<span style="margin-right: 4px">{{ t('chart.symbolic_shape') }}</span>
<el-tooltip class="item" effect="dark" placement="bottom">
<template v-if="state.basicStyleForm.mapSymbol === 'custom'" #content>
<div>{{ t('chart.symbolic_upload_hint') }}</div>
</template>
<el-icon class="hint-icon" :class="{ 'hint-icon--dark': themes === 'dark' }">
<Icon name="icon_info_outlined"><icon_info_outlined class="svg-icon" /></Icon>
</el-icon>
</el-tooltip>
</span>
</template>
<el-select
:effect="themes"
v-model="state.basicStyleForm.mapSymbol"
@change="changeMapSymbol()"
>
<el-option
v-for="item in mapSymbolOptions"
:key="item.name"
:label="item.name"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row style="flex: 1" v-if="state.basicStyleForm.mapSymbol === 'custom'">
<el-col>
<el-form-item class="form-item uploader" :class="'form-item-' + themes">
<div class="avatar-uploader-container" :class="`img-area_${themes}`">
<el-upload
action="#"
accept=".svg,.png,.jpeg,.jpg"
class="avatar-uploader"
list-type="picture-card"
ref="iconUpload"
:effect="themes"
:auto-upload="false"
:file-list="state.fileList"
:on-change="onIconChange"
:limit="1"
>
<el-icon><Plus /></el-icon>
</el-upload>
</div>
</el-form-item>
</el-col>
</el-row>
<div class="alpha-setting">
<label class="alpha-label" :class="{ dark: 'dark' === themes }">
{{ t('chart.size') }}
</label>
<el-row style="flex: 1">
<el-col>
<el-form-item class="form-item alpha-slider" :class="'form-item-' + themes">
<el-slider
:effect="themes"
:min="1"
:max="40"
v-model="state.basicStyleForm.mapSymbolSize"
@change="changeBasicStyle('mapSymbolSize')"
:disabled="customSymbolicMapSizeRange"
/>
</el-form-item>
</el-col>
</el-row>
</div>
<div class="alpha-setting">
<label class="alpha-label" :class="{ dark: 'dark' === themes }">
{{ t('chart.size') }}{{ t('chart.symbolic_range') }}
</label>
<el-row style="flex: 1">
<el-col :span="11">
<el-form-item class="form-item alpha-slider" :class="'form-item-' + themes">
<el-input
type="number"
:effect="themes"
v-model="state.basicStyleForm.mapSymbolSizeMin"
class="basic-input-number"
:controls="false"
@blur="mapCustomRangeValidate('mapSymbolSizeMin')"
:disabled="!customSymbolicMapSizeRange"
>
</el-input>
</el-form-item>
</el-col>
<el-col :span="1.2">
<span>-</span>
</el-col>
<el-col :span="11">
<el-form-item class="form-item alpha-slider" :class="'form-item-' + themes">
<el-input
type="number"
:effect="themes"
v-model="state.basicStyleForm.mapSymbolSizeMax"
class="basic-input-number"
:controls="false"
@blur="mapCustomRangeValidate('mapSymbolSizeMax')"
:disabled="!customSymbolicMapSizeRange"
>
</el-input>
</el-form-item>
</el-col>
</el-row>
</div>
<div v-if="state.basicStyleForm.mapSymbol !== 'custom'" class="alpha-setting">
<label class="alpha-label" :class="{ dark: 'dark' === themes }">
{{ t('chart.not_alpha') }}
</label>
<el-row style="flex: 1">
<el-col>
<el-form-item class="form-item alpha-slider" :class="'form-item-' + themes">
<el-slider
:effect="themes"
:min="1"
:max="10"
v-model="state.basicStyleForm.mapSymbolOpacity"
@change="changeBasicStyle('mapSymbolOpacity')"
/>
</el-form-item>
</el-col>
</el-row>
</div>
<div v-if="state.basicStyleForm.mapSymbol !== 'custom'" class="alpha-setting">
<label class="alpha-label" :class="{ dark: 'dark' === themes }">
{{ t('visualization.borderWidth') }}
</label>
<el-row style="flex: 1">
<el-col>
<el-form-item class="form-item alpha-slider" :class="'form-item-' + themes">
<el-slider
:effect="themes"
:min="1"
:max="5"
v-model="state.basicStyleForm.mapSymbolStrokeWidth"
@change="changeBasicStyle('mapSymbolStrokeWidth')"
/>
</el-form-item>
</el-col>
</el-row>
</div>
</div>
</div>
</template>
<style scoped lang="less">
.color-picker-style {
cursor: pointer;
z-index: 1003;
}
.alpha-setting {
display: flex;
width: 100%;
.alpha-slider {
padding: 0 8px;
:deep(.ed-slider__button-wrapper) {
--ed-slider-button-wrapper-size: 36px;
--ed-slider-button-size: 16px;
}
}
.alpha-label {
padding-right: 8px;
font-size: 12px;
font-style: normal;
font-weight: 400;
height: 32px;
line-height: 32px;
display: inline-flex;
align-items: flex-start;
min-width: 56px;
&.dark {
color: #a6a6a6;
}
}
}
.data-area-label {
text-align: left;
position: relative;
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
}
</style>

View File

@@ -25,6 +25,7 @@ export class SymbolicMap extends L7ChartView<Scene, L7Config> {
'background-overall-component',
'border-style',
'basic-style-selector',
'symbolic-style-selector',
'title-selector',
'label-selector',
'tooltip-selector'
@@ -35,13 +36,13 @@ export class SymbolicMap extends L7ChartView<Scene, L7Config> {
'colors',
'alpha',
'mapBaseStyle',
'symbolicMapStyle',
'zoom',
'showLabel',
'autoFit',
'mapCenter',
'zoomLevel'
],
'symbolic-style-selector': ['symbolicMapStyle'],
'label-selector': ['color', 'fontSize', 'showFields', 'customContent'],
'tooltip-selector': [
'color',