feat: download
diff --git a/src/components/BBody.vue b/src/components/BBody.vue
index 5972da0..0246a5b 100644
--- a/src/components/BBody.vue
+++ b/src/components/BBody.vue
@@ -6,10 +6,6 @@
                     动态排序柱状图生成器
                 </h1>
                 <div id="el-config" class="align-middle">
-                    <!-- <div class="my-3">
-                        <el-button onclick="run()" size="medium">运行</el-button>
-                        <el-button size="medium">导出</el-button>
-                    </div> -->
                     <el-form ref="form">
                         <div class="grid grid-cols-3 form-row">
                             <label class="col-span-1">标题</label>
@@ -19,6 +15,7 @@
                                 size="medium"
                                 class="col-span-2"
                                 v-model="title"
+                                @change="runChart"
                             >
                             </el-input>
                         </div>
@@ -31,11 +28,12 @@
                                 size="medium"
                                 class="col-span-2"
                                 v-model="maxDataCnt"
+                                @change="runChart"
                             >
                             </el-input>
                         </div>
                         <div class="grid grid-cols-3 form-row">
-                            <label class="col-span-1">显示排名上限</label>
+                            <label class="col-span-1">每行动画时长(毫秒)</label>
                             <el-input
                                 id="input-animation-duration"
                                 type="number"
@@ -43,9 +41,13 @@
                                 size="medium"
                                 class="col-span-2"
                                 v-model="animationDuration"
+                                @change="runChart"
                             >
                             </el-input>
                         </div>
+                        <el-form-item>
+                            <el-button @click="download">下载</el-button>
+                        </el-form-item>
                     </el-form>
                 </div>
             </el-card>
@@ -54,6 +56,7 @@
                 body-style="height: 100%"
             >
                 <BTable
+                    ref="btable"
                     @after-change="tableAfterChange"
                 />
             </el-card>
@@ -62,6 +65,7 @@
                 body-style="height: 100%"
             >
                 <BChart
+                    ref="bchart"
                     :title="title"
                     :chartData="chartData"
                     :maxDataCnt="maxDataCnt"
@@ -76,6 +80,7 @@
 import {defineComponent} from 'vue';
 import BTable, {ChartData} from './BTable.vue';
 import BChart from './BChart.vue';
+import template from '../helper/template';
 
 export default defineComponent({
     name: 'BBody',
@@ -84,7 +89,7 @@
             title: '汽车销量',
             maxDataCnt: null,
             chartData: null,
-            animationDuration: 5000
+            animationDuration: 3000
         }
     },
     components: {
@@ -96,6 +101,32 @@
     methods: {
         tableAfterChange(data: ChartData) {
             this.chartData = data;
+        },
+
+        runChart() {
+            (this.$refs.bchart as any).run();
+        },
+
+        download() {
+            let html = template;
+            const map = {
+                animationDuration: this.animationDuration,
+                maxDataCnt: this.maxDataCnt,
+                title: this.title,
+                data: (this.$refs.btable as any).getChartData()
+            };
+            for (let attr in map) {
+                const value = (map as any)[attr];
+                html = html.replace(`{{${attr}}}`, JSON.stringify(value));
+            }
+
+            const element = document.createElement('a');
+            element.setAttribute('href', 'data:text/html;charset=utf-8,' + encodeURIComponent(html));
+            element.setAttribute('download', 'echarts-bar-racing.html');
+            element.style.display = 'none';
+            document.body.appendChild(element);
+            element.click();
+            document.body.removeChild(element);
         }
     }
 })
diff --git a/src/components/BChart.vue b/src/components/BChart.vue
index fc55c9f..51f4f01 100644
--- a/src/components/BChart.vue
+++ b/src/components/BChart.vue
@@ -2,7 +2,7 @@
     <div>
         <div slot="header" class="clearfix text-base">
             预览
-            <a href="#">
+            <a href="javascript:;" @click="run()">
                 <i class="el-icon-refresh"></i>
             </a>
         </div>
@@ -60,18 +60,16 @@
             }
 
             chart = echarts.init(this.$refs.chart as HTMLElement);
-            const animationDuration = this.animationDuration || 5000;
+            const animationDuration =/* this.animationDuration ||*/ 5000;
 
             const option = {
-                dataset: {
-                    source: this.chartData
-                },
                 xAxis: {
                     type: 'value',
                     max: 'dataMax'
                 },
                 yAxis: {
                     type: 'category',
+                    data: (this.chartData[0] as string[]).slice(1),
                     inverse: true,
                     animationDuration: 300,
                     animationDurationUpdate: 300,
@@ -80,30 +78,37 @@
                 series: [{
                     id: 'bar',
                     type: 'bar',
-                    encode: {
-                        x: 2
-                    },
+                    data: (this.chartData[headerLength] as string[]).slice(1).map(str => parseInt(str, 10)),
                     seriesLayoutBy: 'row',
                     realtimeSort: true,
                     label: {
                         show: true,
-                        position: 'right',
-                        valueAnimation: true
+                        position: 'right'
                     },
                     itemStyle: {
-                        color: param => {
-                            return param.data[1] || colorAll[param.dataIndex % colorAll.length];
+                        color: (param: any) => {
+                            return (this.chartData[1] as string[])[param.dataIndex + 1] || colorAll[param.dataIndex % colorAll.length];
                         }
                     }
                 }],
                 grid: {
-                    right: 60
+                    right: 60,
+                    bottom: 30
                 },
-                title: {
+                title: [{
+                    text: (this.chartData as any)[headerLength][0],
+                    right: 20,
+                    bottom: 15,
+                    textStyle: {
+                        color: '#ccc',
+                        opacity: 0.3,
+                        fontSize: 70
+                    }
+                }, {
                     text: this.title,
                     left: 10,
                     top: 10
-                },
+                }],
                 animationDuration: 0,
                 animationDurationUpdate: animationDuration,
                 animationEasing: 'linear',
@@ -118,14 +123,12 @@
                     let timeout: number;
                     const timeoutCb = function () {
                         chart.setOption({
-                            // title: [{
-                            //     text: getDataName(i)
-                            // }],
                             series: [{
                                 type: 'bar',
                                 id: 'bar',
-                                encode: {
-                                    x: i + headerLength + 1
+                                data: (that.chartData[headerLength + i + 1] as string[]).slice(1).map(str => parseInt(str, 10)),
+                                label: {
+                                    valueAnimation: true
                                 }
                             }]
                         });
diff --git a/src/components/BTable.vue b/src/components/BTable.vue
index 34dc0a3..d972959 100644
--- a/src/components/BTable.vue
+++ b/src/components/BTable.vue
@@ -25,9 +25,9 @@
             tableData: [
                 ['Name', 'Ford', 'Tesla', 'Toyota', 'Honda'],
                 ['Color', '', '', '', ''],
-                ['2017', '10', '11', '12', '13'],
-                ['2018', '20', '11', '14', '13'],
-                ['2019', '30', '15', '12', '13']
+                ['2017', '13', '11', '12', '14'],
+                ['2018', '20', '44', '34', '39'],
+                ['2019', '62', '75', '58', '63']
             ],
             table: null,
             debouncedTableChange: null
diff --git a/src/components/HelloWorld.vue b/src/components/HelloWorld.vue
deleted file mode 100644
index 6583f4b..0000000
--- a/src/components/HelloWorld.vue
+++ /dev/null
@@ -1,34 +0,0 @@
-<template>
-<h1>{{msg}}</h1>
-<el-input-number v-model="count"></el-input-number>
-<p>Input Number: {{count}}</p>
-</template>
-
-<script lang="ts" setup>
-import { ref, defineProps } from 'vue';
-
-defineProps({
-    msg: {
-        type: String,
-        required: true
-    }
-});
-
-const count = ref(10);
-</script>
-
-<style scoped lang="scss">
-a {
-    color: #42b983;
-}
-label {
-    margin: 0 0.5em;
-    font-weight: bold;
-}
-code {
-    background-color: #eee;
-    padding: 2px 4px;
-    border-radius: 4px;
-    color: #304455;
-}
-</style>
\ No newline at end of file
diff --git a/src/components/btable.ts b/src/components/btable.ts
deleted file mode 100644
index 43eafec..0000000
--- a/src/components/btable.ts
+++ /dev/null
@@ -1,243 +0,0 @@
-import * as $ from 'jquery';
-import Handsontable from 'handsontable';
-import * as echarts from 'echarts';
-
-const headerLength = 2;
-
-let chart: echarts.ECharts;
-const data = [
-    ["Name", "Ford", "Tesla", "Toyota", "Honda"],
-    ["Color", "", "", "", ""],
-    ["2017", 10, 11, 12, 13],
-    ["2018", 20, 11, 14, 13],
-    ["2019", 30, 15, 12, 13]
-];
-
-function initTable() {
-    for (let i = 0; i < data.length; ++i) {
-        for (let j = data[i].length; j < 50; ++j) {
-            data[i].push('');
-        }
-    }
-    for (let i = data.length; i < 100; ++i) {
-        const row = [];
-        for (let j = 0; j < 50; ++j) {
-            row.push('');
-        }
-        data.push(row);
-    }
-
-    // function colorRenderer(instance, td, row, col) {
-    //     //- console.log(instance);
-
-    // }
-
-    const container = document.getElementById('table-panel') as Element;
-    console.log(container)
-
-    const table = new Handsontable(container, {
-        data: data,
-        rowHeaders: true,
-        colHeaders: true,
-        filters: true,
-        dropdownMenu: true,
-        // cell: [{
-        //     row: 0,
-        //     col: 0,
-        //     readOnly: true
-        // }, {
-        //     row: 1,
-        //     col: 0,
-        //     readOnly: true
-        // }],
-        //- cells: function (row, col) {
-        //-     if (row === 1) {
-        //-         return {
-        //-             renderer: colorRenderer
-        //-         }
-        //-     }
-        //-     else {
-        //-         return {};
-        //-     }
-        //- }
-    });
-    table.updateSettings({
-        afterChange: function () {
-            run();
-        }
-    });
-
-    chart = echarts.init($('#bar-race-preview')[0]);
-    run();
-}
-
-const timeoutHandlers: number[] = [];
-
-function clearTimeoutHandlers() {
-    for (let i = 0; i < timeoutHandlers.length; ++i) {
-        clearTimeout(timeoutHandlers[i]);
-        timeoutHandlers.splice(i, 1);
-    }
-}
-function removeTimeoutHandlers(handler: number) {
-    for (let i = 0; i < timeoutHandlers.length; ++i) {
-        if (timeoutHandlers[i] === handler) {
-            timeoutHandlers.splice(i, 1);
-        }
-    }
-}
-
-function initEvents() {
-    $('.form-group').change(function () {
-        run();
-    });
-}
-
-
-function run() {
-    clearTimeoutHandlers();
-
-    const title = $('#input-title').val();
-    const max = $('#input-max').val();
-    chart.setOption({
-        title: [{
-            text: getDataName(0),
-            textStyle: {
-                fontFamily: 'monospace',
-                fontSize: 80,
-                color: 'rgba(100, 100, 100, 0.2)'
-            },
-            bottom: 60,
-            right: 20
-        }, {
-            text: title
-        }],
-        grid: {
-            right: 20
-        },
-        yAxis: {
-            type: 'category',
-            data: getYData(),
-            inverse: true,
-            max: max,
-            animationDuration: 0,
-            animationDurationUpdate: 0
-        },
-        xAxis: {},
-        series: [{
-            type: 'bar',
-            data: getChartData(0),
-            realtimeSort: true,
-            colorBy: 'item',
-            label: {
-                show: true,
-                position: 'insideRight'
-            }
-        }],
-        animationDurationUpdate: 5000,
-        animationEasing: 'linear',
-        animationEasingUpdate: 'linear'
-    }, true);
-
-    const rows = trimRows();
-    for (let i = 1; i < rows.length; ++i) {
-        (function (i) {
-            var dataRow = getChartData(i);
-            let timeout: number;
-            const timeoutCb = function () {
-                chart.setOption({
-                    title: [{
-                        text: getDataName(i)
-                    }],
-                    yAxis: {
-                        animationDuration: 300,
-                        animationDurationUpdate: 300,
-                    },
-                    series: [{
-                        type: 'bar',
-                        data: dataRow
-                    }]
-                });
-                removeTimeoutHandlers(timeout);
-            };
-            timeout = window.setTimeout(timeoutCb, (i - 1) * 5000);
-            timeoutHandlers.push(timeout);
-        })(i);
-    }
-}
-
-const trimColumns = function (rowData: (string | number)[]) {
-    for (let i = rowData.length - 1; i > 0; --i) {
-        if (rowData[i] && rowData[i] !== '') {
-            return rowData.slice(1, i + 1);
-        }
-    }
-    return [];
-};
-
-const trimRows = function () {
-    if (data.length <= headerLength) {
-        return [];
-    }
-    for (let i = data.length - 1; i >= headerLength; --i) {
-        let isEmpty = true;
-        for (let j = 1; j < data[i].length; ++j) {
-            if (data[i][j] && data[i][j] !== '') {
-                isEmpty = false;
-                break;
-            }
-        }
-        if (!isEmpty) {
-            return data.slice(headerLength, i + 1);
-        }
-    }
-    return [];
-};
-
-const getYData = function () {
-    if (data.length <= headerLength) {
-        return [];
-    }
-    return trimColumns(data[0]);
-};
-
-const getChartData = function (id: number) {
-    if (data.length <= id + headerLength) {
-        return [];
-    }
-    return trimColumns(data[id + headerLength]);
-};
-
-const getDataName = function (id: number) {
-    if (data.length <= id + headerLength) {
-        return '';
-    }
-    else {
-        return data[id + headerLength][0];
-    }
-}
-
-function download() {
-    // saveFile()
-}
-
-function saveFile(name: string, type: string) {
-    // if (isSafari()) {
-    //     window.open('data:text/plain;charset=utf-8,' + encodeURIComponent(data));
-    // } else {
-    //     try {
-    //         var file = new Blob([data], {type: type});
-    //         saveAs(file, name);
-    //     } catch(e) {
-    //         console.error(e);
-    //         window.open('data:text/plain;charset=utf-8,' + encodeURIComponent(data));
-    //     }
-    // }
-}
-
-function isSafari() {
-    return navigator.userAgent.indexOf('Safari') > 0 &&
-        navigator.userAgent.indexOf('Chrome') < 0;
-}
-
-export default {initTable};
diff --git a/src/helper/template.ts b/src/helper/template.ts
new file mode 100644
index 0000000..6f89e3c
--- /dev/null
+++ b/src/helper/template.ts
@@ -0,0 +1,154 @@
+export default `
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Apache ECharts Bar Racing</title>
+
+    <style>
+        #chart {
+            width: 100%;
+            height: 500px;
+            border: 1px solid #ddd;
+        }
+    </style>
+
+    <script src="https://cdn.jsdelivr.net/npm/echarts@5.1.2/dist/echarts.js"></script>
+</head>
+<body>
+    <div id="chart"></div>
+
+    <script>
+        var colorAll = [
+            '#5470c6',
+            '#91cc75',
+            '#fac858',
+            '#ee6666',
+            '#73c0de',
+            '#3ba272',
+            '#fc8452',
+            '#9a60b4',
+            '#ea7ccc'
+        ];
+        var headerLength = 2;
+
+        var animationDuration = {{animationDuration}};
+        var maxDataCnt = {{maxDataCnt}};
+        var title = {{title}};
+        var data = {{data}};
+
+        var chart;
+        var timeoutHandlers = [];
+
+        run();
+
+        function run() {
+            clearTimeoutHandlers();
+            if (chart) {
+                chart.dispose();
+            }
+
+            chart = echarts.init(document.getElementById('chart'));
+            var option = {
+                xAxis: {
+                    type: 'value',
+                    max: 'dataMax'
+                },
+                yAxis: {
+                    type: 'category',
+                    data: data[0].slice(1),
+                    inverse: true,
+                    animationDuration: 300,
+                    animationDurationUpdate: 300,
+                    max: maxDataCnt ? maxDataCnt - 1 : null
+                },
+                series: [{
+                    id: 'bar',
+                    type: 'bar',
+                    data: getDataLine(0),
+                    seriesLayoutBy: 'row',
+                    realtimeSort: true,
+                    label: {
+                        show: true,
+                        position: 'right'
+                    },
+                    itemStyle: {
+                        color: function (param) {
+                            return data[1][param.dataIndex + 1] || colorAll[param.dataIndex % colorAll.length];
+                        }
+                    }
+                }],
+                grid: {
+                    right: 60,
+                    bottom: 30
+                },
+                title: [{
+                    text: 'aaa',
+                    right: 20,
+                    bottom: 15,
+                    textStyle: {
+                        color: '#ccc',
+                        opacity: 0.3,
+                        fontSize: 70
+                    }
+                }, {
+                    text: title,
+                    left: 10,
+                    top: 10
+                }],
+                animationDuration: 0,
+                animationDurationUpdate: animationDuration,
+                animationEasing: 'linear',
+                animationEasingUpdate: 'linear'
+            };
+            chart.setOption(option, true);
+
+            var dataCnt = data.length - headerLength - 1;
+            for (var i = 0; i < dataCnt; ++i) {
+                (function (i) {
+                    var timeout;
+                    var timeoutCb = function () {
+                        chart.setOption({
+                            series: [{
+                                type: 'bar',
+                                id: 'bar',
+                                data: getDataLine(i + 1),
+                                label: {
+                                    valueAnimation: true
+                                }
+                            }]
+                        });
+                        removeTimeoutHandlers(timeout);
+                    };
+                    timeout = window.setTimeout(timeoutCb, i * animationDuration);
+                    timeoutHandlers.push(timeout);
+                })(i);
+            }
+        }
+
+        function getDataLine(n) {
+            return data[headerLength + n].slice(1).map(function (n) {
+                return parseInt(n, 10);
+            });
+        }
+
+        function clearTimeoutHandlers() {
+            for (let i = 0; i < timeoutHandlers.length; ++i) {
+                clearTimeout(timeoutHandlers[i]);
+            }
+            timeoutHandlers = [];
+        }
+
+        function removeTimeoutHandlers(handler) {
+            for (let i = 0; i < timeoutHandlers.length; ++i) {
+                if (timeoutHandlers[i] === handler) {
+                    timeoutHandlers.splice(i, 1);
+                }
+            }
+        }
+    </script>
+</body>
+</html>
+`;