| <template> |
| <el-collapse> |
| <el-collapse-item title="颜色" name="color"> |
| <h5>配色方案</h5> |
| <div> |
| <div |
| class="color-palette" |
| v-for="palette in colorPalettes" |
| v-bind:key="palette.bgColor" |
| :style="{ background: palette.bgColor }" |
| @click="useColorPalette(palette)" |
| > |
| <div |
| class="color" |
| v-for="color in palette.themeColors" |
| v-bind:key="color" |
| :style="{ background: color }" |
| ></div> |
| </div> |
| </div> |
| |
| <h5>背景色</h5> |
| <el-color-picker v-model="bgColor" size="small" @change="change"> |
| </el-color-picker> |
| |
| <h5>色相范围</h5> |
| <el-row> |
| <el-col :span="14"> |
| <el-color-picker |
| v-for="(color, index) in themeColors" |
| v-bind:key="index" |
| v-model="themeColors[index]" |
| size="small" |
| @change="changeColor" |
| > |
| </el-color-picker> |
| <div class="color-picker-btn" @click="removeThemeColor()"> |
| <i class="el-icon-minus"></i> |
| </div> |
| <div class="color-picker-btn" @click="addThemeColor()"> |
| <i class="el-icon-plus"></i> |
| </div> |
| </el-col> |
| <el-col :span="8" :offset="2"> |
| <el-checkbox>关联数据大小</el-checkbox> |
| </el-col> |
| </el-row> |
| |
| <h5>饱和度范围</h5> |
| <el-row> |
| <el-col :span="13" :offset="1"> |
| <el-slider |
| v-model="saturation" |
| range |
| show-tooltip |
| :max="100" |
| :step="1" |
| input-size="medium" |
| @change="change" |
| > |
| </el-slider> |
| </el-col> |
| <el-col :span="8" :offset="2"> |
| <el-checkbox>关联数据大小</el-checkbox> |
| </el-col> |
| </el-row> |
| |
| <h5>亮度范围</h5> |
| <el-row> |
| <el-col :span="13" :offset="1"> |
| <el-slider |
| v-model="lightness" |
| range |
| show-tooltip |
| :max="100" |
| :step="1" |
| input-size="medium" |
| @change="change" |
| > |
| </el-slider> |
| </el-col> |
| <el-col :span="8" :offset="2"> |
| <el-checkbox>关联数据大小</el-checkbox> |
| </el-col> |
| </el-row> |
| |
| <!-- <h5>透明度范围</h5> |
| <el-row> |
| <el-col :span="22" :offset="1"> |
| <el-slider |
| v-model="alpha" |
| range |
| show-tooltip |
| :max="1" |
| :step="0.05" |
| input-size="medium" |
| > |
| </el-slider> |
| </el-col> |
| </el-row> --> |
| </el-collapse-item> |
| |
| <el-collapse-item title="文字" name="font"> |
| <el-row> |
| <el-col :span="12"> |
| <h5>字体</h5> |
| </el-col> |
| <el-col :span="12"> |
| <el-select |
| v-model="selectedFontFamily" |
| placeholder="请选择字体" |
| @change="changeFontFamily" |
| > |
| <el-option-group |
| v-for="group in fontFamilies" |
| :key="group.name" |
| :label="group.name" |
| > |
| <el-option |
| v-for="item in group.children" |
| :key="item" |
| :label="item.name" |
| :value="item.value" |
| > |
| </el-option> |
| </el-option-group> |
| </el-select> |
| </el-col> |
| </el-row> |
| |
| <h5>字号范围</h5> |
| <el-row> |
| <el-col :span="22" :offset="1"> |
| <el-slider |
| v-model="fontSize" |
| range |
| show-tooltip |
| :max="200" |
| :step="1" |
| input-size="medium" |
| @change="change" |
| > |
| </el-slider> |
| </el-col> |
| </el-row> |
| |
| <h5>旋转范围</h5> |
| <el-row> |
| <el-col :span="22" :offset="1"> |
| <el-slider |
| v-model="rotate" |
| range |
| show-tooltip |
| :min="-180" |
| :max="180" |
| :step="45" |
| input-size="medium" |
| @change="change" |
| > |
| </el-slider> |
| </el-col> |
| </el-row> |
| </el-collapse-item> |
| |
| <el-collapse-item title="形状" name="mask"> |
| <h5>宽度(百分比)</h5> |
| <el-row> |
| <el-col :span="22" :offset="1"> |
| <el-slider |
| v-model="width" |
| show-tooltip |
| :max="100" |
| :step="5" |
| input-size="medium" |
| @change="change" |
| > |
| </el-slider> |
| </el-col> |
| </el-row> |
| |
| <h5>高度(百分比)</h5> |
| <el-row> |
| <el-col :span="22" :offset="1"> |
| <el-slider |
| v-model="height" |
| show-tooltip |
| :max="100" |
| :step="5" |
| input-size="medium" |
| @change="change" |
| > |
| </el-slider> |
| </el-col> |
| </el-row> |
| |
| <el-row> |
| <el-col :span="12"> |
| <h5>遮罩形状</h5> |
| </el-col> |
| <el-col :span="12" class="header-right"> |
| <el-checkbox |
| v-model="shapeRatio" |
| @change="change" |
| > |
| 保持长宽比 |
| </el-checkbox> |
| </el-col> |
| </el-row> |
| <div class="img-select" |
| v-for="item in masks" |
| :key="item.value" |
| :value="item.value" |
| v-bind:class="{ selected: item.value === selectedMask }" |
| v-bind:title="item.name" |
| @click="changeMask(item.value)" |
| > |
| <img v-bind:src="item.value + '.png'" /> |
| <div class="mark" v-if="item.isFromFreepik">*</div> |
| </div> |
| <el-upload |
| class="img-select img-uploader" |
| v-bind:class="{ selected: imageUrl === selectedMask }" |
| action="https://jsonplaceholder.typicode.com/posts/" |
| :show-file-list="false" |
| list-type="picture" |
| :on-success="handleImageSuccess" |
| :before-upload="beforeImageUpload"> |
| <img v-if="imageUrl" :src="imageUrl"> |
| <i v-else class="el-icon-plus img-uploader-icon"></i> |
| </el-upload> |
| <div class="hint"> |
| 带 * 的形状来自 <a href="https://www.freepik.com" title="Freepik" _target="_blank">Freepik</a>,查看<a href="https://media.flaticon.com/license/license.pdf" _target="_blank">版权</a> |
| </div> |
| </el-collapse-item> |
| </el-collapse> |
| </template> |
| |
| <script setup lang="ts"> |
| import { ref } from 'vue'; |
| import Color from 'color'; |
| import WebFont from 'webfontloader'; |
| |
| const colorPalettes = [ |
| { |
| bgColor: '#f8eddc', |
| themeColors: ['#f19d70', '#b7b7a4', '#6b705c'] |
| }, |
| { |
| bgColor: '#eef6fa', |
| themeColors: ['#5470c6', '#91cc75', '#fac858', '#ee6666'] |
| }, |
| { |
| bgColor: '#dbf3ff', |
| themeColors: ['#4aa6d5', '#d3b16a', '#fb8500'] |
| }, |
| { |
| bgColor: '#fbffd1', |
| themeColors: ['#132a13', '#90a955', '#ecf39e'] |
| } |
| ]; |
| |
| const bgColor = ref(colorPalettes[0].bgColor); |
| const themeColors = ref(colorPalettes[0].themeColors); |
| const saturation = ref([50, 80]); |
| const lightness = ref([50, 80]); |
| const alpha = ref([0.5, 0.8]); |
| const selectedFontFamily = ref('Arial'); |
| const fontSize = ref([4, 100]); |
| const rotate = ref([-90, 90]); |
| const width = ref(90); |
| const height = ref(90); |
| const selectedMask = ref('heart'); |
| const shapeRatio = ref(true); |
| const imageUrl = ref(''); |
| |
| const normalizeFont = (font: string | { name: string; value: string }) => { |
| if (typeof font === 'string') { |
| return { |
| name: font, |
| value: font |
| }; |
| } |
| return font; |
| }; |
| |
| const sortFont = ( |
| a: { name: string; value: string }, |
| b: { name: string; value: string } |
| ) => { |
| if (a.name < b.name) { |
| return -1; |
| } |
| if (a.name > b.name) { |
| return 1; |
| } |
| return 0; |
| }; |
| |
| const fontFamilies = [ |
| { |
| name: '系统字体', |
| children: [ |
| 'Arial', |
| 'Times New Roman', |
| 'Courier New', |
| 'Georgia', |
| { name: '宋体/华文宋体', value: 'SimSun, STSong' }, |
| { name: '黑体/华文黑体', value: 'SimHei, STHeiti' }, |
| { name: '微软雅黑', value: 'Microsoft YaHei' } |
| ] |
| .map(normalizeFont) |
| .sort(sortFont) |
| }, |
| { |
| name: 'Google Fonts', |
| children: [ |
| 'Pushster', |
| 'Lato', |
| 'Roboto', |
| 'Roboto Slab', |
| 'Open Sans', |
| 'Oswald', |
| 'Ubuntu', |
| 'Lobster', |
| { name: '马善政毛笔楷书', value: 'Ma Shan Zheng' }, |
| { name: '站酷庆科黄油体', value: 'ZCOOL QingKe HuangYou' }, |
| { name: '站酷小薇LOGO体', value: 'ZCOOL XiaoWei' } |
| ] |
| .map(normalizeFont) |
| .sort(sortFont) |
| } |
| ]; |
| const webFontsLoaded: string[] = []; |
| |
| const masks = [ |
| { |
| name: 'ECharts 0', |
| value: 'echarts-0' |
| }, |
| { |
| name: 'ECharts 1', |
| value: 'echarts-1' |
| }, |
| { |
| name: '圆形', |
| value: 'circle' |
| }, |
| { |
| name: '方形', |
| value: 'rect' |
| }, |
| { |
| name: '云', |
| value: 'cloud', |
| isFromFreepik: true |
| }, |
| { |
| name: '菱形', |
| value: 'rhombus', |
| isFromFreepik: true |
| }, |
| { |
| name: '三角形', |
| value: 'triangle', |
| isFromFreepik: true |
| }, |
| { |
| name: '五边形', |
| value: 'pentagon', |
| isFromFreepik: true |
| }, |
| { |
| name: '五角星', |
| value: 'star', |
| isFromFreepik: true |
| }, |
| { |
| name: '圆角矩形', |
| value: 'rounded-rectangle', |
| isFromFreepik: true |
| }, |
| { |
| name: '爱心', |
| value: 'heart', |
| isFromFreepik: true |
| }, |
| { |
| name: '六边形', |
| value: 'hexagonal', |
| isFromFreepik: true |
| }, |
| { |
| name: '闪电', |
| value: 'flash', |
| isFromFreepik: true |
| }, |
| { |
| name: '水滴', |
| value: 'drop', |
| isFromFreepik: true |
| }, |
| { |
| name: '六瓣花', |
| value: 'flower', |
| isFromFreepik: true |
| }, |
| { |
| name: '喷溅', |
| value: 'splash', |
| isFromFreepik: true |
| }, |
| { |
| name: '地标', |
| value: 'maps-and-flags', |
| isFromFreepik: true |
| }, |
| { |
| name: '问号', |
| value: 'question-mark', |
| isFromFreepik: true |
| }, |
| { |
| name: 'WOW', |
| value: 'wow' |
| } |
| ]; |
| |
| const emit = defineEmits(['change', 'fontLoading', 'fontLoaded']); |
| |
| defineExpose({ getConfig }); |
| |
| setTimeout(() => { |
| changeColor(); |
| }, 0); |
| |
| function changeFontFamily() { |
| let fontFamily = selectedFontFamily.value; |
| const font = fontFamilies[1].children.find(item => item.value === fontFamily); |
| if (font && !webFontsLoaded.includes(font.value)) { |
| emit('fontLoading'); |
| WebFont.load({ |
| google: { |
| families: [fontFamily] |
| }, |
| fontactive: () => { |
| webFontsLoaded.push(font.value); |
| emit('fontLoaded'); |
| change(); |
| } |
| }); |
| return; |
| } |
| |
| change(); |
| } |
| |
| function changeMask(value: string) { |
| selectedMask.value = value; |
| change(); |
| } |
| |
| function changeColor() { |
| let minS = 100; |
| let maxS = 0; |
| let minL = 100; |
| let maxL = 0; |
| themeColors.value.forEach(color => { |
| const c = Color(color); |
| const s = Math.round(c.saturationv()); |
| const l = Math.round(c.lightness()); |
| if (s < minS) { |
| minS = s; |
| } |
| if (s > maxS) { |
| maxS = s; |
| } |
| if (l < minL) { |
| minL = l; |
| } |
| if (l > maxL) { |
| maxL = l; |
| } |
| }); |
| saturation.value = [minS, maxS]; |
| lightness.value = [minL, maxL]; |
| change(); |
| } |
| |
| function change() { |
| emit('change'); |
| } |
| |
| function useColorPalette(palette: { bgColor: string; themeColors: string[] }) { |
| themeColors.value = palette.themeColors; |
| bgColor.value = palette.bgColor; |
| changeColor(); |
| } |
| |
| function removeThemeColor() { |
| if (themeColors.value.length === 1) { |
| return; |
| } |
| themeColors.value.splice(themeColors.value.length - 1, 1); |
| emit('change'); |
| } |
| |
| function addThemeColor() { |
| themeColors.value.push( |
| themeColors.value.length === 0 |
| ? '#555' |
| : themeColors.value[themeColors.value.length - 1] |
| ); |
| emit('change'); |
| } |
| |
| function handleImageSuccess(res: any, file: any) { |
| // console.log(res, file); |
| imageUrl.value = URL.createObjectURL(file.raw); |
| selectedMask.value = imageUrl.value; |
| change(); |
| } |
| |
| function beforeImageUpload(file: any) { |
| // TODO: loading |
| return true; |
| } |
| |
| function getConfig() { |
| return { |
| bgColor: bgColor.value, |
| themeColors: themeColors.value, |
| saturation: saturation.value, |
| lightness: lightness.value, |
| alpha: alpha.value, |
| fontFamily: selectedFontFamily.value, |
| fontSize: fontSize.value, |
| rotate: rotate.value, |
| width: width.value, |
| height: height.value, |
| shape: selectedMask.value, |
| shapeRatio: shapeRatio.value, |
| }; |
| } |
| </script> |
| |
| <style lang="scss"> |
| $brand-color: #409eff; |
| $border-color: #e6e6e6; |
| |
| .title-right { |
| position: absolute; |
| top: 10px; |
| right: 0; |
| |
| a { |
| display: inline-block; |
| } |
| } |
| |
| .header-right { |
| text-align: right; |
| } |
| |
| .el-color-picker { |
| margin-right: 5px; |
| } |
| |
| .color-picker-btn, |
| .color-palette { |
| display: inline-block; |
| width: 30px; |
| height: 30px; |
| margin-right: 5px; |
| padding: 3px 6px; |
| box-sizing: border-box; |
| vertical-align: top; |
| border: 1px solid $border-color; |
| border-radius: 4px; |
| color: $brand-color; |
| } |
| |
| .color-palette { |
| display: inline-block; |
| margin-bottom: 5px; |
| width: auto; |
| height: 33px; |
| padding: 5px; |
| cursor: pointer; |
| |
| .color { |
| display: inline-block; |
| width: 20px; |
| height: 20px; |
| margin-right: 5px; |
| border-radius: 3px; |
| |
| &:last-child { |
| margin-right: 0; |
| } |
| } |
| } |
| |
| .hint { |
| margin: 5px 0; |
| color: #888; |
| |
| a { |
| color: $brand-color; |
| } |
| } |
| |
| .img-select { |
| position: relative; |
| display: inline-block; |
| width: 57px; |
| height: 57px; |
| margin-right: 5px; |
| border: 1px solid $border-color; |
| padding: 5px; |
| border-radius: 4px; |
| opacity: 0.5; |
| cursor: pointer; |
| |
| &.selected { |
| border-color: $brand-color; |
| opacity: 1; |
| } |
| |
| img { |
| max-width: 100%; |
| max-height: 100%; |
| } |
| |
| .mark { |
| position: absolute; |
| top: -2px; |
| right: 4px; |
| color: #000; |
| } |
| } |
| |
| .img-uploader { |
| position: relative; |
| overflow: hidden; |
| top: 6px; |
| } |
| |
| .img-uploader-icon { |
| font-size: 28px; |
| line-height: 55px; |
| text-align: center; |
| } |
| |
| .el-upload { |
| display: block; |
| } |
| |
| .el-checkbox { |
| --el-checkbox-font-color: #888; |
| margin-top: 7px; |
| } |
| |
| .text-pad { |
| padding: 8px 0; |
| } |
| |
| .el-dropdown-link { |
| cursor: pointer; |
| color: $brand-color; |
| } |
| |
| .el-icon-arrow-down { |
| font-size: 12px; |
| } |
| |
| h5 { |
| margin: 8px 0; |
| font-size: 13px; |
| color: #666; |
| } |
| </style> |