mirror of
https://github.com/ecomfe/vue-echarts.git
synced 2025-08-14 19:23:28 +08:00

* chore: suppress warning in demo * chore: prevent multiple intializations of esbuild-wasm in demo HMR
432 lines
8.7 KiB
Vue
432 lines
8.7 KiB
Vue
<script setup>
|
|
import {
|
|
ref,
|
|
computed,
|
|
watch,
|
|
onBeforeUnmount,
|
|
onMounted,
|
|
nextTick,
|
|
} from "vue";
|
|
import { useLocalStorage } from "@vueuse/core";
|
|
import "highlight.js/styles/github.css";
|
|
import hljs from "highlight.js/lib/core";
|
|
import javascript from "highlight.js/lib/languages/javascript";
|
|
import typescript from "highlight.js/lib/languages/typescript";
|
|
import hljsVuePlugin from "@highlightjs/vue-plugin";
|
|
import { initialize, transform } from "esbuild-wasm";
|
|
import wasmURL from "esbuild-wasm/esbuild.wasm?url";
|
|
import { track } from "@vercel/analytics";
|
|
|
|
import { getImportsFromOption } from "./utils/codegen";
|
|
|
|
hljs.registerLanguage("javascript", javascript);
|
|
hljs.registerLanguage("typescript", typescript);
|
|
const CodeHighlight = hljsVuePlugin.component;
|
|
|
|
const codegenOptions = useLocalStorage("ve.codegenOptions", {
|
|
indent: " ",
|
|
quote: "'",
|
|
multiline: false,
|
|
maxLen: 80,
|
|
semi: false,
|
|
includeType: false,
|
|
});
|
|
|
|
const props = defineProps({ open: Boolean, renderer: String });
|
|
const emit = defineEmits(["update:open"]);
|
|
|
|
const dialog = ref(null);
|
|
let clickFrom = null;
|
|
|
|
function onMousedown(e) {
|
|
clickFrom = e.target;
|
|
}
|
|
|
|
function closeFromOutside() {
|
|
if (dialog.value?.contains(clickFrom)) {
|
|
return;
|
|
}
|
|
close();
|
|
}
|
|
|
|
function close() {
|
|
emit("update:open", false);
|
|
}
|
|
|
|
const renderer = ref(props.renderer);
|
|
const source = ref(null);
|
|
watch(
|
|
() => props.open,
|
|
async (val) => {
|
|
if (val) {
|
|
renderer.value = props.renderer;
|
|
}
|
|
|
|
await nextTick();
|
|
|
|
if (initializing.value) {
|
|
return;
|
|
}
|
|
source.value?.focus();
|
|
},
|
|
);
|
|
|
|
const copied = ref(false);
|
|
const initializing = ref(true);
|
|
const optionCode = ref("");
|
|
const transformedCode = ref("");
|
|
const transformErrors = ref([]);
|
|
|
|
onMounted(async () => {
|
|
// prevent multiple initializations during HMR
|
|
if (!window.__esbuildInitialized) {
|
|
await initialize({ wasmURL });
|
|
window.__esbuildInitialized = true;
|
|
}
|
|
|
|
initializing.value = false;
|
|
|
|
optionCode.value = `{
|
|
title: {
|
|
text: 'Referer of a Website',
|
|
subtext: 'Fake Data',
|
|
left: 'center'
|
|
},
|
|
tooltip: {
|
|
trigger: 'item'
|
|
},
|
|
legend: {
|
|
orient: 'vertical',
|
|
left: 'left'
|
|
},
|
|
series: [
|
|
{
|
|
name: 'Access From',
|
|
type: 'pie',
|
|
radius: '50%',
|
|
data: [
|
|
{ value: 1048, name: 'Search Engine' },
|
|
{ value: 735, name: 'Direct' },
|
|
{ value: 580, name: 'Email' },
|
|
{ value: 484, name: 'Union Ads' },
|
|
{ value: 300, name: 'Video Ads' }
|
|
],
|
|
emphasis: {
|
|
itemStyle: {
|
|
shadowBlur: 10,
|
|
shadowOffsetX: 0,
|
|
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}`;
|
|
|
|
await nextTick();
|
|
|
|
source.value?.focus();
|
|
});
|
|
|
|
watch(optionCode, async (val) => {
|
|
try {
|
|
transformedCode.value = await transform(`(${val})`, { loader: "ts" });
|
|
transformErrors.value = [];
|
|
} catch (e) {
|
|
transformErrors.value = e.errors;
|
|
}
|
|
});
|
|
|
|
function formatError(errors) {
|
|
return errors
|
|
.map(({ text, location: { lineText, line, column, length } }) => {
|
|
const digit = Math.ceil(Math.log10(line)) || 1;
|
|
lineText = line === 1 ? lineText.slice(1) : lineText;
|
|
lineText =
|
|
line === optionCode.value.split("\n").length
|
|
? lineText.slice(0, -1)
|
|
: lineText;
|
|
column = line === 1 ? column - 1 : column;
|
|
|
|
return `/* ${text} */
|
|
|
|
// ${line} | ${lineText}
|
|
// ${" ".repeat(digit)} | ${" ".repeat(column)}${"~".repeat(length)}
|
|
`;
|
|
})
|
|
.join("\n\n");
|
|
}
|
|
|
|
const importCode = computed(() => {
|
|
if (optionCode.value.trim() === "") {
|
|
return "// Paste your option code first";
|
|
}
|
|
|
|
if (transformErrors.value.length) {
|
|
return formatError(transformErrors.value);
|
|
}
|
|
|
|
try {
|
|
return getImportsFromOption(eval(transformedCode.value.code), {
|
|
renderer: renderer.value,
|
|
...codegenOptions.value,
|
|
});
|
|
} catch (e) {
|
|
return `/* Invalid ECharts option */
|
|
|
|
// ${e.message}
|
|
`;
|
|
}
|
|
});
|
|
|
|
watch(importCode, () => {
|
|
copied.value = false;
|
|
});
|
|
|
|
// copy message
|
|
const messageOpen = ref(false);
|
|
let messageTimer;
|
|
|
|
function trackCopy(from) {
|
|
if (copied.value) {
|
|
// only track copy after modifications
|
|
return;
|
|
}
|
|
|
|
copied.value = true;
|
|
track("copy-code", { from });
|
|
}
|
|
|
|
function copy() {
|
|
trackCopy("button");
|
|
clearTimeout(messageTimer);
|
|
|
|
navigator.clipboard.writeText(importCode.value);
|
|
messageOpen.value = true;
|
|
|
|
messageTimer = setTimeout(() => {
|
|
messageOpen.value = false;
|
|
}, 2018);
|
|
}
|
|
|
|
onBeforeUnmount(() => {
|
|
clearTimeout(messageTimer);
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<aside
|
|
class="modal"
|
|
:class="{ open: props.open }"
|
|
@mousedown="onMousedown"
|
|
@click="closeFromOutside"
|
|
@keydown.esc="close"
|
|
>
|
|
<section class="dialog" ref="dialog">
|
|
<h2>✨ <code>import</code> code generator</h2>
|
|
<section class="options">
|
|
<label>
|
|
Renderer
|
|
<select v-model="renderer">
|
|
<option value="canvas">Canvas</option>
|
|
<option value="svg">SVG</option>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
TypeScript
|
|
<input type="checkbox" v-model="codegenOptions.includeType" />
|
|
</label>
|
|
<label>
|
|
Multiline
|
|
<input type="checkbox" v-model="codegenOptions.multiline" />
|
|
</label>
|
|
<label>
|
|
Semi
|
|
<input type="checkbox" v-model="codegenOptions.semi" />
|
|
</label>
|
|
<label>
|
|
Quote
|
|
<select v-model="codegenOptions.quote">
|
|
<option value="'">single</option>
|
|
<option value='"'>double</option>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
Indent
|
|
<select v-model="codegenOptions.indent">
|
|
<option value=" ">2</option>
|
|
<option value=" ">4</option>
|
|
<option value=" ">Tab</option>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
Max length
|
|
<input
|
|
type="number"
|
|
step="10"
|
|
v-model.number="codegenOptions.maxLen"
|
|
/>
|
|
</label>
|
|
</section>
|
|
<section class="code">
|
|
<textarea
|
|
ref="source"
|
|
class="option-code"
|
|
v-model="optionCode"
|
|
:placeholder="
|
|
initializing
|
|
? 'Initializing...'
|
|
: 'Paste your option code (TS/JS literal) here...'
|
|
"
|
|
:disabled="initializing"
|
|
autofocus
|
|
spellcheck="false"
|
|
></textarea>
|
|
<div class="import-code" @copy="trackCopy('system')">
|
|
<code-highlight
|
|
:language="codegenOptions.includeType ? 'typescript' : 'javascript'"
|
|
:code="importCode"
|
|
/>
|
|
</div>
|
|
<button
|
|
class="copy"
|
|
@click="copy"
|
|
:disabled="importCode.startsWith('/*') || importCode.startsWith('//')"
|
|
>
|
|
Copy
|
|
</button>
|
|
</section>
|
|
</section>
|
|
</aside>
|
|
|
|
<aside class="message" :class="{ open: messageOpen }">
|
|
Copied to clipboard
|
|
</aside>
|
|
</template>
|
|
|
|
<style>
|
|
input::-webkit-outer-spin-button,
|
|
input::-webkit-inner-spin-button {
|
|
-webkit-appearance: none;
|
|
margin: 0;
|
|
}
|
|
|
|
/* Works for Firefox */
|
|
input[type="number"] {
|
|
appearance: textfield;
|
|
}
|
|
|
|
input[type="text"],
|
|
input[type="number"] {
|
|
cursor: text;
|
|
}
|
|
|
|
.dialog {
|
|
display: flex;
|
|
flex-direction: column;
|
|
width: 80vw;
|
|
height: 90vh;
|
|
border-radius: 6px;
|
|
overflow: hidden;
|
|
background-color: #fff;
|
|
box-shadow: 0 0 45px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
.options {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: stretch;
|
|
gap: 16px;
|
|
padding: 16px;
|
|
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
|
|
|
label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
input,
|
|
button,
|
|
select {
|
|
height: 2.4em;
|
|
}
|
|
|
|
input[type="number"] {
|
|
width: 60px;
|
|
}
|
|
}
|
|
|
|
.code {
|
|
position: relative;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: stretch;
|
|
flex-grow: 1;
|
|
min-height: 0;
|
|
tab-size: 4;
|
|
|
|
.option-code,
|
|
.import-code {
|
|
flex: 0 0 50%;
|
|
margin: 0;
|
|
border: none;
|
|
line-height: 1.2;
|
|
font-size: 13px;
|
|
overflow: auto;
|
|
}
|
|
|
|
.import-code {
|
|
border-left: 1px solid rgba(0, 0, 0, 0.1);
|
|
|
|
pre {
|
|
display: block;
|
|
width: 100%;
|
|
height: 100%;
|
|
margin: 0;
|
|
padding: 0;
|
|
background: #fff;
|
|
box-shadow: none;
|
|
}
|
|
|
|
code {
|
|
height: 100%;
|
|
}
|
|
}
|
|
|
|
.option-code {
|
|
padding: 1em;
|
|
outline: none;
|
|
resize: none;
|
|
}
|
|
}
|
|
|
|
.copy {
|
|
position: absolute;
|
|
right: 10px;
|
|
top: 10px;
|
|
}
|
|
|
|
.message {
|
|
position: fixed;
|
|
z-index: 2147483647;
|
|
bottom: 2rem;
|
|
left: 50%;
|
|
padding: 0.5rem 0.75rem;
|
|
background-color: rgba(45, 52, 64, 0.98);
|
|
box-shadow: 0 4px 16px rgba(45, 52, 64, 0.6);
|
|
color: #fff;
|
|
font-size: 0.875rem;
|
|
transform: translate(-50%, 200%);
|
|
border-radius: 4px;
|
|
opacity: 0;
|
|
transition:
|
|
transform 0.2s,
|
|
opacity 0.2s;
|
|
}
|
|
|
|
.message.open {
|
|
transform: translate(-50%, 0);
|
|
opacity: 1;
|
|
}
|
|
</style>
|