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>