目标
使用echarts的拖拽模板绘制曲线并导出为svg。
顺便练练vue3、@vueuse、echarts的熟练度。
效果图
Demo
源码
<!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>Document</title>
<style>
body,
html {
margin: 0
}
.draggable-bar {
position: fixed;
z-index: 1001;
width: 16px;
height: 16px;
background-color: rgba(0, 0, 0, 0.2);
cursor: move;
}
.tool-bar {
position: absolute;
top: 16px;
}
.box {
position: relative;
width: max-content;
}
.chart-box {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.points {
width: 100%;
}
</style>
</head>
<body>
<div id="app">
<div class="draggable-bar" :style="dragStyle" ref="dragEl">
<div class="tool-bar">
<input type="number" v-model="pointSize" />
<input type="checkbox" v-model="isShowPoint" />
<input type="file" ref="fileEl" v-show="false" accept="image/*" />
<button @click="handleAdd">+</button>
<button @click="handleRemove">-</button>
<button @click="handleSelectImg">选择图片</button>
<button @click="handleExportSvg">下载svg</button>
<br />
<textarea class="points" v-model="pointsJson"></textarea>
</div>
</div>
<div ref="boxEl" class="box">
<img :src="imgBase64" />
<div ref="chartBox" class="chart-box"></div>
</div>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script crossorigin="anonymous"
integrity="sha512-UN8wX5Zf4Af6/2UJOYTYyWLHdua4SWMd1pnIxNoDCtqdaAMk1TQdvwwgoG7ShvuOS1d9jCerLNzwfvRmL7N4iA=="
src="https://lib.baomitu.com/echarts/5.2.0/echarts.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
<script src="https://unpkg.com/@vueuse/shared"></script>
<script src="https://unpkg.com/@vueuse/core"></script>
<script>
const { createApp, ref, unref, reactive, computed, watch, getCurrentInstance } = Vue;
const { useEventListener, useMounted, get, set, tryOnMounted, useLocalStorage, useDraggable, useBase64, useResizeObserver, useToggle } = VueUse;
window.onload = function () {
const app = createApp({
setup() {
let myChart;
const boxEl = ref(); // echarts组件外框
const chartBox = ref(); // echarts组件依赖元素
const dragEl = ref(); // 工具栏拖拽用元素
const fileEl = ref(); // 文件选择元素
const isShowPoint = useLocalStorage("isShowPoint", true); // 是否显示控制点
const pointSize = useLocalStorage("pointSize", 16); // 控制点大小
const points = useLocalStorage("points", [[10, 10]]); // 控制点位数组
const pointsJson = computed({
get: () => JSON.stringify(get(points)),
set: str => set(points, JSON.parse(!!str ? str : `[[10, 10]]`))
}); // 控制点位数组显示用Json串
const [resizeObserver, boxResize] = useToggle() // 切换图片时用的监听器和监听触发方法
const imgBase64 = useLocalStorage("imgBase64", null); // 选择完的图片
const grid = {
top: 0,
right: 0,
bottom: 0,
left: 0
};
const getAxisDef = (other) => {
return _.assign({
type: 'value',
min: 0,
show: false,
}, other);
};
// x轴配置
const xAxis = computed(() => {
get(resizeObserver) // 图片切换时的监听,切换图片会重新计算
return getAxisDef({ max: get(chartBox)?.offsetWidth ?? 0 });
});
// y轴配置
const yAxis = computed(() => {
get(resizeObserver) // 图片切换时的监听,切换图片会重新计算
return getAxisDef({ max: get(chartBox)?.offsetHeight ?? 0 });
});
// 制作的曲线的基础配置
const line = computed(() => {
return {
type: 'line',
smooth: true,
showSymbol: get(isShowPoint),
symbolSize: get(pointSize),
data: get(points),
}
});
// 控制点事件的绑定位置
const graphic = computed(() => {
return get(points).map((x, i) => ({
type: 'circle',
position: myChart.convertToPixel('grid', x),
shape: {
cx: 0,
cy: 0,
r: get(pointSize) / 2
},
invisible: true,
draggable: get(isShowPoint),
ondrag: ({ offsetX, offsetY }) => {
let newPoint = myChart.convertFromPixel('grid', [offsetX, offsetY]);
get(points).splice(i, 1, newPoint);
},
z: 1000
}))
});
// echarts的配置
const option = computed(() => {
return {
grid,
xAxis: get(xAxis),
yAxis: get(yAxis),
series: [get(line)],
color: ['#000000'],
}
})
tryOnMounted(() => {
// 初始化echarts为svg渲染模式
myChart = echarts.init(get(chartBox), null, { renderer: 'svg' });
// 首次设定,因为graphic的绘制需要线条先描绘出来,所以还需要再设定一次配置
myChart.setOption(get(option));
// 修改配置时,触发配置重新设定,echarts是差分配置,所以放全部配置也无所谓
watch(() => option, () => {
myChart.setOption(_.assign({ graphic: get(graphic) }, get(option)));
}, {
immediate: true,
deep: true
});
});
// 注册工具栏拖拽行为的控制器
const { style: dragStyle } = useDraggable(dragEl, { exact: true });
// 添加控制点事件
function handleAdd() {
let last = _.last(get(points));
get(points).push(last.map(x => x + get(pointSize)));
}
// 删除控制点事件
function handleRemove() {
get(points).pop();
}
// 选择图片的事件
function handleSelectImg() {
let el = get(fileEl);
el.value = "";
useEventListener(fileEl, "change", async (e) => {
const { execute } = useBase64(e.target.files[0]);
const base64 = await execute();
set(imgBase64, base64);
}, { once: true });
el.click();
}
// 切换图片时,触发echarts重绘
useResizeObserver(boxEl, () => {
myChart.resize();
myChart.clear();
boxResize();
})
// 导出svg事件
function handleExportSvg() {
const el = unref(chartBox);
const old = unref(isShowPoint);
const path = el.querySelector("path");
set(isShowPoint, false);
const blob = new Blob([`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1" baseProfile="full" width="${el.offsetWidth}" height="${el.offsetHeight}">
${path.outerHTML}</svg>`], { type: "text/xml" });
const a = document.createElement("a")
a.href = URL.createObjectURL(blob)
a.download = `${Date.now()}.svg` // 这里填保存成的文件名
a.click()
URL.revokeObjectURL(a.href)
a.remove();
set(isShowPoint, old)
}
return {
boxEl,
chartBox,
dragEl,
fileEl,
dragStyle,
isShowPoint,
pointSize,
pointsJson,
option,
imgBase64,
handleAdd,
handleRemove,
handleSelectImg,
handleExportSvg,
};
}
});
app.mount("#app");
}
</script>
</body>
</html>