feat: export image
diff --git a/src/App.vue b/src/App.vue
index 3d99246..dede2ab 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -5,7 +5,7 @@
{{ $t('title') }}
</h3>
<el-tabs type="card" v-model="activeName">
- <el-tab-pane label="样式" name="config">
+ <el-tab-pane id="config-tab" label="样式" name="config">
<WConfig
ref="wconfig"
@change="onChange"
@@ -17,7 +17,48 @@
<el-tab-pane label="数据" name="data">
<WData ref="wdata" @change="onChange"></WData>
</el-tab-pane>
- <el-tab-pane label="导出" name="export">导出</el-tab-pane>
+ <el-tab-pane label="导出" name="export">
+ <WExport @sizeChange="onSizeChange" @download="onDownload"></WExport>
+ </el-tab-pane>
+ <el-tab-pane label="关于" name="about">
+ <h5>项目说明</h5>
+ <p class="small">
+ <a
+ href="https://github.com/ecomfe/echarts-wordcloud"
+ target="_blank"
+ >echarts-wordcloud</a
+ >
+ 是
+ <a href="https://github.com/apache/echarts" target="_blank"
+ >Apache ECharts</a
+ >
+ 基于
+ <a href="https://github.com/timdream/wordcloud2.js" target="_blank"
+ >wordcloud2.js</a
+ >
+ 的插件,需要使用 JavaScript
+ 开发。本项目是基于该插件的字符云生成器,用于方便用户无代码生成字符云效果。如果需要更丰富的效果,可以考虑基于上述字符云插件开发。
+ </p>
+ <h5>项目源码</h5>
+ <p class="small">
+ <a
+ href="https://github.com/apache/echarts-wordcloud-generator"
+ target="_blank"
+ >
+ apache/echarts-wordcloud-generator
+ </a>
+ </p>
+ <h5>版权说明</h5>
+ <p class="small">
+ 从本工具下载的图片可免费个人使用或商业使用。源代码版权:
+ <a
+ href="https://github.com/apache/echarts-wordcloud-generator/blob/master/LICENSE"
+ target="_blank"
+ >
+ Apache License 2.0</a
+ >。
+ </p>
+ </el-tab-pane>
</el-tabs>
</el-aside>
<el-main>
@@ -32,6 +73,7 @@
import WChart from './components/WChart.vue';
import WConfig from './components/WConfig.vue';
import WData from './components/WData.vue';
+import WExport from './components/WExport.vue';
import defaultData from './data/defaultData';
const wconfig = ref<any>(null);
@@ -42,7 +84,19 @@
const activeName = 'config';
function onChange() {
- wchart.value?.run(wdata.value?.data, wconfig.value?.getConfig());
+ wchart.value?.run(
+ wdata.value?.data,
+ wdata.value?.fillSmall,
+ wconfig.value?.getConfig()
+ );
+}
+
+function onSizeChange(size: { width: number; height: number }) {
+ wchart.value?.resize(size.width, size.height);
+}
+
+function onDownload() {
+ wchart.value?.download();
}
function onFontLoading() {
@@ -62,6 +116,10 @@
h4 {
margin: 10px 0;
}
+
+.el-collapse:first-child {
+ border-top: 0;
+}
</style>
<style scoped lang="scss">
@@ -85,4 +143,13 @@
height: calc(100vh - 50px);
overflow: auto;
}
+
+#config-tab {
+ margin-top: -15px;
+}
+
+.small {
+ margin: 5px 0 20px 0;
+ font-size: 14px;
+}
</style>
diff --git a/src/components/WChart.vue b/src/components/WChart.vue
index 61f8b0b..cbdeea6 100644
--- a/src/components/WChart.vue
+++ b/src/components/WChart.vue
@@ -1,6 +1,13 @@
<template>
<div v-loading="isWebFontLoading" element-loading-text="字体加载中">
- <div class="chart" ref="chartRef">aaa</div>
+ <div
+ class="chart"
+ ref="chartRef"
+ v-bind:style="{
+ width: width + 'px',
+ height: height + 'px'
+ }"
+ ></div>
</div>
</template>
@@ -10,16 +17,17 @@
import 'echarts-wordcloud';
import Color from 'color';
-// const props = defineProps({
-// foo: String
-// });
const chart = shallowRef<echarts.ECharts | null>(null);
const chartRef = ref<HTMLElement | null>(null);
const isWebFontLoading = ref(false);
+const width = ref(800);
+const height = ref(600);
defineExpose({
run,
- setLoading
+ setLoading,
+ resize,
+ download
});
type Config = {
@@ -41,14 +49,53 @@
isWebFontLoading.value = isLoading;
}
-function run(data?: [], config?: Config) {
+function resize(w: number, h: number) {
+ w = Math.max(100, w);
+ h = Math.max(100, h);
+ if (width.value !== w || height.value !== h) {
+ width.value = w;
+ height.value = h;
+ }
+ setTimeout(() => {
+ chart.value!.resize();
+ });
+}
+
+function download() {
+ if (chart.value) {
+ try {
+ const url = chart.value.getDataURL();
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = 'wordcloud.png';
+ a.click();
+ } catch (e) {
+ console.error(e);
+ alert('保存出错了,可以尝试在右图中点击鼠标右键,并选择“图片储存为”');
+ }
+ }
+}
+
+function run(data?: [], fillSmall?: boolean, config?: Config) {
+ const chartData = (data ? data.slice() : []) as {
+ name: string;
+ value: number;
+ }[];
+ // if (fillSmall) {
+ // const smallData = chartData.filter((d) => d.value < 10);
+ // const maxData = 400;
+ // for (let i = chartData.length; i < maxData; ++i) {
+ // for (let j = 0; j < smallData.length && i < maxData; ++j) {
+ // chartData.push({
+ // name: smallData[j].name,
+ // value: smallData[j].value
+ // });
+ // }
+ // }
+ // }
+
const hues = config
- ? config.themeColors.map(
- color =>
- Color(color)
- .hsl()
- .object().h
- )
+ ? config.themeColors.map((color) => Color(color).hsl().object().h)
: [];
function getHue() {
@@ -72,7 +119,7 @@
series: [
{
type: 'wordCloud',
- data: data || [],
+ data: chartData,
gridSize: 4,
sizeRange: config?.fontSize,
rotationRange: config?.rotate,
@@ -111,8 +158,7 @@
maskImage.onerror = () => {
render();
};
- }
- else {
+ } else {
render();
}
}
diff --git a/src/components/WConfig.vue b/src/components/WConfig.vue
index 637fb48..259aba8 100644
--- a/src/components/WConfig.vue
+++ b/src/components/WConfig.vue
@@ -25,7 +25,7 @@
<h5>色相范围</h5>
<el-row>
- <el-col :span="14">
+ <el-col :span="22">
<el-color-picker
v-for="(color, index) in themeColors"
v-bind:key="index"
@@ -41,14 +41,14 @@
<i class="el-icon-plus"></i>
</div>
</el-col>
- <el-col :span="8" :offset="2">
+ <!-- <el-col :span="8" :offset="2">
<el-checkbox>关联数据大小</el-checkbox>
- </el-col>
+ </el-col> -->
</el-row>
<h5>饱和度范围</h5>
<el-row>
- <el-col :span="13" :offset="1">
+ <el-col :span="22" :offset="1">
<el-slider
v-model="saturation"
range
@@ -60,14 +60,14 @@
>
</el-slider>
</el-col>
- <el-col :span="8" :offset="2">
+ <!-- <el-col :span="8" :offset="2">
<el-checkbox>关联数据大小</el-checkbox>
- </el-col>
+ </el-col> -->
</el-row>
<h5>亮度范围</h5>
<el-row>
- <el-col :span="13" :offset="1">
+ <el-col :span="22" :offset="1">
<el-slider
v-model="lightness"
range
@@ -79,9 +79,9 @@
>
</el-slider>
</el-col>
- <el-col :span="8" :offset="2">
+ <!-- <el-col :span="8" :offset="2">
<el-checkbox>关联数据大小</el-checkbox>
- </el-col>
+ </el-col> -->
</el-row>
<!-- <h5>透明度范围</h5>
@@ -198,15 +198,13 @@
<h5>遮罩形状</h5>
</el-col>
<el-col :span="12" class="header-right">
- <el-checkbox
- v-model="shapeRatio"
- @change="change"
- >
+ <el-checkbox v-model="shapeRatio" @change="change">
保持长宽比
</el-checkbox>
</el-col>
</el-row>
- <div class="img-select"
+ <div
+ class="img-select"
v-for="item in masks"
:key="item.value"
:value="item.value"
@@ -224,12 +222,20 @@
:show-file-list="false"
list-type="picture"
:on-success="handleImageSuccess"
- :before-upload="beforeImageUpload">
- <img v-if="imageUrl" :src="imageUrl">
+ :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>
+ 带 * 的形状来自
+ <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>
@@ -237,7 +243,6 @@
<script setup lang="ts">
import { ref } from 'vue';
-import Color from 'color';
import WebFont from 'webfontloader';
const colorPalettes = [
@@ -261,8 +266,8 @@
const bgColor = ref(colorPalettes[0].bgColor);
const themeColors = ref(colorPalettes[0].themeColors);
-const saturation = ref([50, 80]);
-const lightness = ref([50, 80]);
+const saturation = ref([20, 40]);
+const lightness = ref([50, 75]);
const alpha = ref([0.5, 0.8]);
const selectedFontFamily = ref('Arial');
const fontSize = ref([4, 100]);
@@ -435,7 +440,9 @@
function changeFontFamily() {
let fontFamily = selectedFontFamily.value;
- const font = fontFamilies[1].children.find(item => item.value === fontFamily);
+ const font = fontFamilies[1].children.find(
+ (item) => item.value === fontFamily
+ );
if (font && !webFontsLoaded.includes(font.value)) {
emit('fontLoading');
WebFont.load({
@@ -460,29 +467,29 @@
}
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];
+ // 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();
}
@@ -538,7 +545,7 @@
width: width.value,
height: height.value,
shape: selectedMask.value,
- shapeRatio: shapeRatio.value,
+ shapeRatio: shapeRatio.value
};
}
</script>
diff --git a/src/components/WData.vue b/src/components/WData.vue
index a7bfc47..637827b 100644
--- a/src/components/WData.vue
+++ b/src/components/WData.vue
@@ -1,5 +1,6 @@
<template>
<div>
+ <!-- <el-checkbox :v-model="fillSmall">用小于 10 的数据填充剩余空间</el-checkbox> -->
<el-table class="word-table" :data="tableData" empty-text="无数据">
<el-table-column prop="name" label="文本">
<template v-slot="scope">
@@ -47,7 +48,8 @@
<el-table class="word-table" :show-header="false" :data="pendingData">
<el-table-column fixed prop="name" label="文本">
<template v-slot="scope">
- <el-input size="small" v-model="scope.row.name"> </el-input>
+ <el-input size="small" v-model="scope.row.name" placeholder="文本">
+ </el-input>
</template>
</el-table-column>
<el-table-column prop="value" label="大小">
@@ -55,7 +57,7 @@
<el-input
size="small"
v-model="scope.row.value"
- placeholder="留空则随机大小"
+ placeholder="相对大小"
>
</el-input>
</template>
@@ -69,9 +71,9 @@
</el-table-column>
</el-table>
<div class="padding">
- <el-button type="primary" size="medium" @click="removeAll()">
+ <!-- <el-button type="primary" size="medium" @click="removeAll()">
导入 .cvs
- </el-button>
+ </el-button> -->
<el-button type="danger" size="medium" @click="removeAll()">
清空
</el-button>
@@ -80,7 +82,7 @@
</template>
<script setup lang="ts">
-import { ref, defineEmits } from "vue";
+import { ref, defineEmits } from 'vue';
type WordData = {
name?: string;
@@ -90,36 +92,37 @@
const tableData = ref([] as WordData[]);
const pendingData = ref([{}] as WordData[]);
+const fillSmall = ref(true);
-const emit = defineEmits(["change"]);
+const emit = defineEmits(['change']);
-defineExpose({ data: tableData, setData });
+defineExpose({ data: tableData, setData, fillSmall });
function setData(data: WordData[]) {
tableData.value = data;
- emit("change");
+ emit('change');
}
function removeData(row: WordData) {
- tableData.value = tableData.value.filter(item => item !== row);
- emit("change");
+ tableData.value = tableData.value.filter((item) => item !== row);
+ emit('change');
}
-function edit(row: WordData, attr: "name" | "value") {
+function edit(row: WordData, attr: 'name' | 'value') {
row.editAttr = attr;
}
function doneEditing(row: WordData) {
row.editAttr = undefined;
- emit("change");
+ emit('change');
}
function addRow() {
if (pendingData.value[0].name) {
- const rawValue = (pendingData.value[0].value as unknown) as string;
+ const rawValue = pendingData.value[0].value as unknown as string;
const value = parseFloat(rawValue);
- if (rawValue != null && rawValue !== "" && isNaN(value)) {
- alert("请输入数字");
+ if (rawValue != null && rawValue !== '' && isNaN(value)) {
+ alert('请输入数字');
return;
} else {
tableData.value.push({
@@ -127,14 +130,14 @@
value
});
pendingData.value = [{}];
- emit("change");
+ emit('change');
}
}
}
function removeAll() {
tableData.value = [];
- emit("change");
+ emit('change');
}
</script>
diff --git a/src/components/WExport.vue b/src/components/WExport.vue
new file mode 100644
index 0000000..1e617ec
--- /dev/null
+++ b/src/components/WExport.vue
@@ -0,0 +1,72 @@
+<template>
+ <h5>画布尺寸</h5>
+ <el-row>
+ <el-col :span="4">
+ <h5>宽度</h5>
+ </el-col>
+ <el-col :span="7">
+ <el-input
+ type="number"
+ v-model="width"
+ :min="100"
+ :max="2000"
+ :step="10"
+ size="small"
+ @change="onSizeChange"
+ >
+ </el-input>
+ </el-col>
+ <el-col :span="4" :offset="1">
+ <h5>高度</h5>
+ </el-col>
+ <el-col :span="7">
+ <el-input
+ type="number"
+ v-model="height"
+ :min="100"
+ :max="2000"
+ :step="10"
+ size="small"
+ @change="onSizeChange"
+ >
+ </el-input>
+ </el-col>
+ </el-row>
+ <div class="padding">
+ <el-button type="primary" size="medium" @click="download()">
+ 下载图片
+ </el-button>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue';
+
+const width = ref('800');
+const height = ref('600');
+
+defineExpose({ getSize });
+
+const emit = defineEmits(['sizeChange', 'download']);
+
+function getSize() {
+ return {
+ width: parseInt(width.value, 10),
+ height: parseInt(height.value, 10)
+ };
+}
+
+function onSizeChange() {
+ emit('sizeChange', getSize());
+}
+
+function download() {
+ emit('download');
+}
+</script>
+
+<style lang="scss">
+.padding {
+ padding: 20px 0;
+}
+</style>